diff --git a/.coffeelintignore b/.coffeelintignore
new file mode 100644
index 00000000000..1db51fed757
--- /dev/null
+++ b/.coffeelintignore
@@ -0,0 +1 @@
+spec/fixtures
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000000..b54c3b8df0a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,4 @@
+**/spec/fixtures/**/*.js
+node_modules
+/vendor/
+/out/
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 00000000000..62995e39c06
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,44 @@
+{
+ "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
+ }
+ }
+ ]
+}
diff --git a/.gitattributes b/.gitattributes
index 6eb1a23ae9a..d2728b92676 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -12,3 +12,16 @@ 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
diff --git a/.github/lock.yml b/.github/lock.yml
new file mode 100644
index 00000000000..ba43e69a4b9
--- /dev/null
+++ b/.github/lock.yml
@@ -0,0 +1,17 @@
+# 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
diff --git a/.github/move.yml b/.github/move.yml
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/.github/no-response.yml b/.github/no-response.yml
new file mode 100644
index 00000000000..1c8799d1351
--- /dev/null
+++ b/.github/no-response.yml
@@ -0,0 +1,15 @@
+# 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.
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 00000000000..4888a3bb691
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,35 @@
+# 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
diff --git a/.gitignore b/.gitignore
index 1257ab37108..c743195e322 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,21 @@
*.swp
*~
.DS_Store
+.eslintcache
Thumbs.db
.project
.svn
.nvm-version
+.vscode
+.python-version
node_modules
-npm-debug.log
-debug.log
+*.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/
diff --git a/.node-version b/.node-version
deleted file mode 100644
index 87a1cf595a6..00000000000
--- a/.node-version
+++ /dev/null
@@ -1 +0,0 @@
-v0.12.0
diff --git a/.pairs b/.pairs
deleted file mode 100644
index 2955310284b..00000000000
--- a/.pairs
+++ /dev/null
@@ -1,17 +0,0 @@
-pairs:
- ns: Nathan Sobo; nathan
- cj: Corey Johnson; cj
- dg: David Graham; dgraham
- ks: Kevin Sawicki; kevin
- jc: Jerry Cheung; jerry
- bl: Brian Lopez; brian
- jp: Justin Palmer; justin
- gt: Garen Torikian; garen
- mc: Matt Colyer; mcolyer
- bo: Ben Ogle; benogle
- jr: Jason Rudolph; jasonrudolph
- jl: Jessica Lord; jlord
- dh: Daniel Hengeveld; danielh
-email:
- domain: github.com
-#global: true
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 00000000000..544138be456
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,3 @@
+{
+ "singleQuote": true
+}
diff --git a/.python-version b/.python-version
deleted file mode 100644
index 49cdd668e1c..00000000000
--- a/.python-version
+++ /dev/null
@@ -1 +0,0 @@
-2.7.6
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index ab65307ae82..00000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-git:
- depth: 10
-
-env:
- global:
- - ATOM_ACCESS_TOKEN=da809a6077bb1b0aa7c5623f7b2d5f1fec2faae4
-
- matrix:
- - NODE_VERSION=0.12
-
-os:
- - linux
- - osx
-
-sudo: false
-
-install:
- - git clone https://github.com/creationix/nvm.git /tmp/.nvm
- - source /tmp/.nvm/nvm.sh
- - nvm install $NODE_VERSION
- - nvm use $NODE_VERSION
-
-script: script/cibuild
-
-notifications:
- email:
- on_success: never
- on_failure: change
-
-addons:
- apt:
- packages:
- - build-essential
- - git
- - libgnome-keyring-dev
- - fakeroot
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000000..d503b174046
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,46 @@
+# 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/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fe9a742b470..6e7e80bfb0e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,90 +2,231 @@
:+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 just guidelines, not rules, use your best judgment and feel free to
-propose changes to this document in a pull request.
-
-## Submitting Issues
-
-* You can create an issue [here](https://github.com/atom/atom/issues/new), but
- before doing that please read the notes below on debugging and submitting issues,
- and include as many details as possible with your report.
-* Check the [debugging guide](https://atom.io/docs/latest/hacking-atom-debugging) for tips
- on debugging. You might be able to find the cause of the problem and fix
- things yourself.
-* Include the version of Atom you are using and the OS.
-* Include screenshots and animated GIFs whenever possible; they are immensely
- helpful.
-* Include the behavior you expected and other places you've seen that behavior
- such as Emacs, vi, Xcode, etc.
-* Check the dev tools (`alt-cmd-i`) for errors to include. If the dev tools
- are open _before_ the error is triggered, a full stack trace for the error
- will be logged. If you can reproduce the error, use this approach to get the
- full stack trace and include it in the issue.
-* On Mac, check Console.app for stack traces to include if reporting a crash.
-* Perform a [cursory search](https://github.com/issues?q=+is%3Aissue+user%3Aatom)
- to see if a similar issue has already been submitted.
-* Please setup a [profile picture](https://help.github.com/articles/how-do-i-set-up-my-profile-picture)
- to make yourself recognizable and so we can all get to know each other better.
-
-### Package Repositories
-
-This is the repository for the core Atom editor only. Atom comes bundled with
-many packages and themes that are stored in other repos under the
-[Atom organization](https://github.com/atom) such as
-[tabs](https://github.com/atom/tabs),
-[find-and-replace](https://github.com/atom/find-and-replace),
-[language-javascript](https://github.com/atom/language-javascript), and
-[atom-light-ui](https://github.com/atom/atom-light-ui).
-
-If your issue is related to a specific package, open an issue on that package's
-issue tracker. If you're unsure which package is causing your problem or if
-you're having an issue with Atom core, open an issue on this repository.
-
-For more information on how to work with Atom's official packages, see
-[Contributing to Atom Packages](https://github.com/atom/atom/blob/master/docs/contributing-to-packages.md)
-
-## Pull Requests
-
-* Include screenshots and animated GIFs in your pull request whenever possible.
-* Follow the [CoffeeScript](#coffeescript-styleguide),
- [JavaScript](https://github.com/styleguide/javascript),
- and [CSS](https://github.com/styleguide/css) styleguides.
-* Include thoughtfully-worded, well-structured
- [Jasmine](http://jasmine.github.io/) specs in the `./spec` folder. Run them using `apm test`. See the [Specs Styleguide](#specs-styleguide) below.
-* Document new code based on the
- [Documentation Styleguide](#documentation-styleguide)
-* End files with a newline.
-* Place requires in the following order:
- * Built in Node Modules (such as `path`)
- * Built in Atom and Atom Shell Modules (such as `atom`, `shell`)
- * 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:
- * Use `require('fs-plus').getHomeDirectory()` to get the home directory.
- * Use `path.join()` to concatenate filenames.
- * Use `os.tmpdir()` rather than `/tmp` when you need to reference the
- temporary directory.
-* Using a plain `return` when returning explicitly at the end of a function.
- * Not `return null`, `return undefined`, `null`, or `undefined`
+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 message board](https://github.com/atom/atom/discussions)
+* [Atom FAQ](https://flight-manual.atom.io/faq/)
+
+## 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 [faq](https://flight-manual.atom.io/faq/) and 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.
-## Git Commit Messages
+#### 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
+* 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 Mac OS
+ * :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
@@ -96,10 +237,40 @@ For more information on how to work with Atom's official packages, see
* :arrow_down: `:arrow_down:` when downgrading dependencies
* :shirt: `:shirt:` when removing linter warnings
-## CoffeeScript Styleguide
+### 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`
@@ -113,15 +284,24 @@ For more information on how to work with Atom's official packages, see
* 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
+### Specs Styleguide
-- Include thoughtfully-worded, well-structured
- [Jasmine](http://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.
+- 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
+#### Example
```coffee
describe 'a dog', ->
@@ -132,7 +312,7 @@ describe 'a dog', ->
# spec here
```
-## Documentation Styleguide
+### Documentation Styleguide
* Use [AtomDoc](https://github.com/atom/atomdoc).
* Use [Markdown](https://daringfireball.net/projects/markdown).
@@ -141,7 +321,7 @@ describe 'a dog', ->
* Reference instance methods with `{ClassName::methodName}`
* Reference class methods with `{ClassName.methodName}`
-### Example
+#### Example
```coffee
# Public: Disable the package with the given name.
@@ -155,3 +335,169 @@ describe 'a dog', ->
# 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/
diff --git a/Dockerfile b/Dockerfile
index d792c30c5c7..0bcc7eca762 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,22 +1,31 @@
-# VERSION: 0.1
-# DESCRIPTION: Image to build Atom and create a .rpm file
+# VERSION: 0.2
+# DESCRIPTION: Image to build Atom
-# Base docker image
-FROM fedora:21
+FROM ubuntu:20.04
# Install dependencies
-RUN yum install -y \
- make \
- gcc \
- gcc-c++ \
- glibc-devel \
- git-core \
- libgnome-keyring-devel \
- rpmdevtools \
- nodejs \
- npm
+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/*
-RUN npm install -g npm@1.4.28 --loglevel error
+# Update npm and dependencies
+RUN npm install -g npm --loglevel error
-ADD . /atom
-WORKDIR /atom
+# Use python2 by default
+RUN npm config set python /usr/bin/python2 -g
+
+ENTRYPOINT ["/usr/bin/env", "sh", "-c"]
+CMD ["bash"]
diff --git a/LICENSE.md b/LICENSE.md
index bbb875dc281..6c66ba9599f 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,4 +1,4 @@
-Copyright (c) 2015 GitHub Inc.
+Copyright (c) 2011-2022 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000000..cdf247b79a4
--- /dev/null
+++ b/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,10 @@
+⚛👋 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"
diff --git a/README.md b/README.md
index 8958f303dbb..2d8dcc3422f 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,35 @@
-
+# Atom
-[](https://travis-ci.org/atom/atom)
-[](https://david-dm.org/atom/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/atom/electron), and based on everything we love about our favorite editors. We designed it to be deeply customizable, but still approachable using the default configuration.
+> Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/)
-Visit [atom.io](https://atom.io) to learn more or visit the [Atom forum](https://discuss.atom.io).
+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.
-Visit [issue #3684](https://github.com/atom/atom/issues/3684) to learn more
-about the Atom 1.0 roadmap.
+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://atom.io/docs/latest/) is free and available online, along with ePub, PDF and mobi versions. You can find the source to the manual in [atom/docs](https://github.com/atom/docs).
+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
-### OS X
+### Prerequisites
+- [Git](https://git-scm.com)
+
+### macOS
Download the latest [Atom release](https://github.com/atom/atom/releases/latest).
@@ -29,41 +37,51 @@ Atom will automatically update when a new release is available.
### Windows
-Download the latest [AtomSetup.exe installer](https://github.com/atom/atom/releases/latest).
+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 an `atom-windows.zip` file from the [releases page](https://github.com/atom/atom/releases/latest).
+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.
+Using [Chocolatey](https://chocolatey.org)? Run `cinst Atom` to install the latest version of Atom.
-### Debian Linux (Ubuntu)
+### Linux
-Currently only a 64-bit version is available.
+Atom is only available for 64-bit Linux systems.
-1. Download `atom-amd64.deb` from the [Atom releases page](https://github.com/atom/atom/releases/latest).
-2. Run `sudo dpkg --install atom-amd64.deb` on the downloaded package.
-3. Launch Atom using the installed `atom` command.
+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.
-The Linux version does not currently automatically update so you will need to
-repeat these steps to upgrade to future releases.
+#### Archive extraction
-### Red Hat Linux (Fedora, CentOS, Red Hat)
+An archive is available for people who don't want to install `atom` as root.
-Currently only a 64-bit version is available.
+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. Download `atom.x86_64.rpm` from the [Atom releases page](https://github.com/atom/atom/releases/latest).
-2. Run `sudo yum localinstall atom.x86_64.rpm` on the downloaded package.
-3. Launch Atom using the installed `atom` command.
+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](docs/build-instructions/linux.md)
-* [OS X](docs/build-instructions/os-x.md)
-* [FreeBSD](docs/build-instructions/freebsd.md)
-* [Windows](docs/build-instructions/windows.md)
+* [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).
diff --git a/SUPPORT.md b/SUPPORT.md
new file mode 100644
index 00000000000..55483c8050b
--- /dev/null
+++ b/SUPPORT.md
@@ -0,0 +1,9 @@
+# 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.
diff --git a/apm/package-lock.json b/apm/package-lock.json
new file mode 100644
index 00000000000..069dfba288d
--- /dev/null
+++ b/apm/package-lock.json
@@ -0,0 +1,4730 @@
+{
+ "name": "atom-bundled-apm",
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "atom-package-manager": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/atom-package-manager/-/atom-package-manager-2.6.2.tgz",
+ "integrity": "sha512-nDzUDyN/TYx4YEZ/lrMFXrfAQFVth0/GZ2Ih05HPpBo7aVlZLnDo1c1B2jxrp0BboIoEoEMJ/JZlQ5mmwGCMaA==",
+ "requires": {
+ "@atom/plist": "0.4.4",
+ "asar-require": "0.3.0",
+ "async": "^3.2.0",
+ "colors": "~1.4.0",
+ "first-mate": "^7.4.1",
+ "fs-plus": "3.x",
+ "git-utils": "^5.7.1",
+ "glob": "^7.1.6",
+ "hosted-git-info": "^3.0.7",
+ "keytar": "^6.0.1",
+ "mv": "2.1.1",
+ "ncp": "~2.0.0",
+ "npm": "^6.14.9",
+ "open": "7.3.0",
+ "q": "~1.5.1",
+ "read": "~1.0.7",
+ "request": "^2.88.2",
+ "rimraf": "^3.0.2",
+ "season": "^6.0.2",
+ "semver": "^7.3.4",
+ "tar": "^6.0.5",
+ "temp": "^0.9.4",
+ "underscore-plus": "1.x",
+ "wordwrap": "1.0.0",
+ "wrench": "~1.5.1",
+ "yargs": "^3.32.0"
+ },
+ "dependencies": {
+ "@atom/plist": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/@atom/plist/-/plist-0.4.4.tgz",
+ "integrity": "sha512-EYwsXOY+Jp9vQ2yNWHuWaJufI58vbQWg235OkvGBWnITFVUOdow49OBj7ET9HCksZz560yXbSa1ywzqDIrFPZw==",
+ "requires": {
+ "xmlbuilder": "0.4.x",
+ "xmldom": "0.1.x"
+ }
+ },
+ "abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
+ },
+ "ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+ },
+ "aproba": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
+ },
+ "are-we-there-yet": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz",
+ "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==",
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+ },
+ "readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "asar": {
+ "version": "0.12.1",
+ "resolved": "https://registry.npmjs.org/asar/-/asar-0.12.1.tgz",
+ "integrity": "sha1-35Q+jrXNdPvKBmPi10uPK3J7UI8=",
+ "requires": {
+ "chromium-pickle-js": "^0.1.0",
+ "commander": "^2.9.0",
+ "cuint": "^0.2.1",
+ "glob": "^6.0.4",
+ "minimatch": "^3.0.0",
+ "mkdirp": "^0.5.0",
+ "mksnapshot": "^0.3.0",
+ "tmp": "0.0.28"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
+ "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
+ "requires": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "2 || 3",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ }
+ }
+ },
+ "asar-require": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/asar-require/-/asar-require-0.3.0.tgz",
+ "integrity": "sha1-R+TLRBSJSthplTbNDFjAySFRtFs=",
+ "requires": {
+ "asar": "0.12.1"
+ }
+ },
+ "asn1": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+ "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+ "requires": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
+ },
+ "async": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
+ "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
+ },
+ "aws-sign2": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
+ },
+ "aws4": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
+ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
+ },
+ "bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+ "requires": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
+ "binary": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
+ "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
+ "requires": {
+ "buffers": "~0.1.1",
+ "chainsaw": "~0.1.0"
+ }
+ },
+ "bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "requires": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ }
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "requires": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "buffers": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
+ "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s="
+ },
+ "camelcase": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+ "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8="
+ },
+ "caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
+ },
+ "chainsaw": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
+ "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
+ "requires": {
+ "traverse": ">=0.3.0 <0.4"
+ }
+ },
+ "chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
+ },
+ "chromium-pickle-js": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.1.0.tgz",
+ "integrity": "sha1-HUixB9ghJqLz4hHC6iX4A7pVGyE="
+ },
+ "cliui": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+ "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+ "requires": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wrap-ansi": "^2.0.0"
+ }
+ },
+ "code-point-at": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
+ },
+ "coffee-script": {
+ "version": "1.12.7",
+ "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz",
+ "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw=="
+ },
+ "colors": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
+ "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
+ },
+ "combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+ },
+ "console-control-strings": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+ "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+ },
+ "cson-parser": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/cson-parser/-/cson-parser-1.3.5.tgz",
+ "integrity": "sha1-fsZ14DkUVTO/KmqFYHPxWZ2cLSQ=",
+ "requires": {
+ "coffee-script": "^1.10.0"
+ }
+ },
+ "cuint": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
+ "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs="
+ },
+ "d": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz",
+ "integrity": "sha1-2hhMU10Y2O57oqoim5FACfrhEwk=",
+ "requires": {
+ "es5-ext": "~0.10.2"
+ }
+ },
+ "dashdash": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
+ },
+ "decompress-response": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
+ "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
+ "requires": {
+ "mimic-response": "^2.0.0"
+ }
+ },
+ "decompress-zip": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/decompress-zip/-/decompress-zip-0.3.3.tgz",
+ "integrity": "sha512-/fy1L4s+4jujqj3kNptWjilFw3E6De8U6XUFvqmh4npN3Vsypm3oT2V0bXcmbBWS+5j5tr4okYaFrOmyZkszEg==",
+ "requires": {
+ "binary": "^0.3.0",
+ "graceful-fs": "^4.1.3",
+ "mkpath": "^0.1.0",
+ "nopt": "^3.0.1",
+ "q": "^1.1.2",
+ "readable-stream": "^1.1.8",
+ "touch": "0.0.3"
+ }
+ },
+ "deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
+ },
+ "detect-libc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
+ },
+ "ecc-jsbn": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+ "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+ "requires": {
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "emissary": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/emissary/-/emissary-1.3.3.tgz",
+ "integrity": "sha1-phjZLWgrIy0xER3DYlpd9mF5lgY=",
+ "requires": {
+ "es6-weak-map": "^0.1.2",
+ "mixto": "1.x",
+ "property-accessors": "^1.1",
+ "underscore-plus": "1.x"
+ }
+ },
+ "end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
+ "es5-ext": {
+ "version": "0.10.53",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
+ "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
+ "requires": {
+ "es6-iterator": "~2.0.3",
+ "es6-symbol": "~3.1.3",
+ "next-tick": "~1.0.0"
+ },
+ "dependencies": {
+ "d": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+ "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+ "requires": {
+ "es5-ext": "^0.10.50",
+ "type": "^1.0.1"
+ }
+ },
+ "es6-iterator": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+ "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
+ "requires": {
+ "d": "1",
+ "es5-ext": "^0.10.35",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "es6-symbol": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
+ "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
+ "requires": {
+ "d": "^1.0.1",
+ "ext": "^1.1.2"
+ }
+ }
+ }
+ },
+ "es6-iterator": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-0.1.3.tgz",
+ "integrity": "sha1-1vWLjE/EE8JJtLqhl2j45NfIlE4=",
+ "requires": {
+ "d": "~0.1.1",
+ "es5-ext": "~0.10.5",
+ "es6-symbol": "~2.0.1"
+ }
+ },
+ "es6-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-2.0.1.tgz",
+ "integrity": "sha1-dhtcZ8/U8dGK+yNPaR1nhoLLO/M=",
+ "requires": {
+ "d": "~0.1.1",
+ "es5-ext": "~0.10.5"
+ }
+ },
+ "es6-weak-map": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-0.1.4.tgz",
+ "integrity": "sha1-cGzvnpmqI2undmwjnIueKG6n0ig=",
+ "requires": {
+ "d": "~0.1.1",
+ "es5-ext": "~0.10.6",
+ "es6-iterator": "~0.1.3",
+ "es6-symbol": "~2.0.1"
+ }
+ },
+ "event-kit": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/event-kit/-/event-kit-2.5.3.tgz",
+ "integrity": "sha512-b7Qi1JNzY4BfAYfnIRanLk0DOD1gdkWHT4GISIn8Q2tAf3LpU8SP2CMwWaq40imYoKWbtN4ZhbSRxvsnikooZQ=="
+ },
+ "expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
+ },
+ "ext": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
+ "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==",
+ "requires": {
+ "type": "^2.0.0"
+ },
+ "dependencies": {
+ "type": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz",
+ "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw=="
+ }
+ }
+ },
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
+ },
+ "extsprintf": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+ },
+ "first-mate": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/first-mate/-/first-mate-7.4.1.tgz",
+ "integrity": "sha512-SEG5W0aajCvK/Ngoo3he3Ib4DsT+CRPhBAgSju5hksBLvvUfRWP7Jf3+HQ+CNTD4GZZqbDNOEJNOxbf35EblrQ==",
+ "requires": {
+ "emissary": "^1",
+ "event-kit": "^2.2.0",
+ "fs-plus": "^3.0.0",
+ "grim": "^2.0.1",
+ "oniguruma": "7.2.1",
+ "season": "^6.0.2",
+ "underscore-plus": "^1"
+ }
+ },
+ "forever-agent": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
+ },
+ "form-data": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
+ "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.6",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+ },
+ "fs-extra": {
+ "version": "0.26.7",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz",
+ "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=",
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^2.1.0",
+ "klaw": "^1.0.0",
+ "path-is-absolute": "^1.0.0",
+ "rimraf": "^2.2.8"
+ },
+ "dependencies": {
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
+ }
+ },
+ "fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "requires": {
+ "minipass": "^3.0.0"
+ }
+ },
+ "fs-plus": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/fs-plus/-/fs-plus-3.1.1.tgz",
+ "integrity": "sha512-Se2PJdOWXqos1qVTkvqqjb0CSnfBnwwD+pq+z4ksT+e97mEShod/hrNg0TRCCsXPbJzcIq+NuzQhigunMWMJUA==",
+ "requires": {
+ "async": "^1.5.2",
+ "mkdirp": "^0.5.1",
+ "rimraf": "^2.5.2",
+ "underscore-plus": "1.x"
+ },
+ "dependencies": {
+ "async": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
+ },
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+ },
+ "gauge": {
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+ "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+ "requires": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ }
+ },
+ "getpass": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+ "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "git-utils": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/git-utils/-/git-utils-5.7.1.tgz",
+ "integrity": "sha512-+mWdJDq9emWoq6GzzrGEB7SIBmAk0lNNv2wgNkgwTVZUkAFkWvgRsJ+Kvs3d1QQD6WG6vczti2WLpjmh2Twtlw==",
+ "requires": {
+ "fs-plus": "^3.0.0",
+ "nan": "^2.14.0"
+ }
+ },
+ "github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4="
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.6",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
+ "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ=="
+ },
+ "grim": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/grim/-/grim-2.0.3.tgz",
+ "integrity": "sha512-FM20Ump11qYLK9k9DbL8yzVpy+YBieya1JG15OeH8s+KbHq8kL4SdwRtURwIUHniSxb24EoBUpwKfFjGNVi4/Q==",
+ "requires": {
+ "event-kit": "^2.0.0"
+ }
+ },
+ "har-schema": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
+ },
+ "har-validator": {
+ "version": "5.1.5",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
+ "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
+ "requires": {
+ "ajv": "^6.12.3",
+ "har-schema": "^2.0.0"
+ }
+ },
+ "has-unicode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
+ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
+ },
+ "hosted-git-info": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz",
+ "integrity": "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==",
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "http-signature": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+ "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "jsprim": "^1.2.2",
+ "sshpk": "^1.7.0"
+ }
+ },
+ "ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
+ },
+ "invert-kv": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
+ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY="
+ },
+ "is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+ },
+ "is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "requires": {
+ "is-docker": "^2.0.0"
+ }
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+ },
+ "isstream": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
+ },
+ "jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM="
+ },
+ "json-schema": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+ },
+ "json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+ },
+ "jsonfile": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
+ "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
+ "requires": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "jsprim": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+ "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+ "requires": {
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.3.0",
+ "json-schema": "0.2.3",
+ "verror": "1.10.0"
+ }
+ },
+ "keytar": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/keytar/-/keytar-6.0.1.tgz",
+ "integrity": "sha512-1Ihpf2tdM3sLwGMkYHXYhVC/hx5BDR7CWFL4IrBA3IDZo0xHhS2nM+tU9Y+u/U7okNfbVkwmKsieLkcWRMh93g==",
+ "requires": {
+ "node-addon-api": "^3.0.0",
+ "prebuild-install": "5.3.4"
+ }
+ },
+ "klaw": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
+ "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=",
+ "requires": {
+ "graceful-fs": "^4.1.9"
+ }
+ },
+ "lcid": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
+ "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
+ "requires": {
+ "invert-kv": "^1.0.0"
+ }
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "mime-db": {
+ "version": "1.47.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz",
+ "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw=="
+ },
+ "mime-types": {
+ "version": "2.1.30",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz",
+ "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==",
+ "requires": {
+ "mime-db": "1.47.0"
+ }
+ },
+ "mimic-response": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
+ "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA=="
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
+ },
+ "minipass": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz",
+ "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==",
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "requires": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ }
+ },
+ "mixto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/mixto/-/mixto-1.0.0.tgz",
+ "integrity": "sha1-wyDvYbUvKJj1IuF9i7xtUG2EJbY="
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+ "requires": {
+ "minimist": "^1.2.5"
+ }
+ },
+ "mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
+ },
+ "mkpath": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/mkpath/-/mkpath-0.1.0.tgz",
+ "integrity": "sha1-dVSm+Nhxg0zJe1RisSLEwSTW3pE="
+ },
+ "mksnapshot": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/mksnapshot/-/mksnapshot-0.3.5.tgz",
+ "integrity": "sha512-PSBoZaj9h9myC3uRRW62RxmX8mrN3XbOkMEyURUD7v5CeJgtYTar50XU738t7Q0LtG1pBPtp5n5QwDGggRnEvw==",
+ "requires": {
+ "decompress-zip": "0.3.x",
+ "fs-extra": "0.26.7",
+ "request": "2.x"
+ }
+ },
+ "mute-stream": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
+ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
+ },
+ "mv": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
+ "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
+ "requires": {
+ "mkdirp": "~0.5.1",
+ "ncp": "~2.0.0",
+ "rimraf": "~2.4.0"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
+ "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
+ "requires": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "2 || 3",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "rimraf": {
+ "version": "2.4.5",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
+ "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
+ "requires": {
+ "glob": "^6.0.1"
+ }
+ }
+ }
+ },
+ "nan": {
+ "version": "2.14.2",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
+ "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
+ },
+ "napi-build-utils": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
+ "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
+ },
+ "ncp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
+ "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M="
+ },
+ "next-tick": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+ "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
+ },
+ "node-abi": {
+ "version": "2.26.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.26.0.tgz",
+ "integrity": "sha512-ag/Vos/mXXpWLLAYWsAoQdgS+gW7IwvgMLOgqopm/DbzAjazLltzgzpVMsFlgmo9TzG5hGXeaBZx2AI731RIsQ==",
+ "requires": {
+ "semver": "^5.4.1"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+ }
+ }
+ },
+ "node-addon-api": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz",
+ "integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw=="
+ },
+ "noop-logger": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz",
+ "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI="
+ },
+ "nopt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+ "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "npm": {
+ "version": "6.14.13",
+ "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.13.tgz",
+ "integrity": "sha512-SRl4jJi0EBHY2xKuu98FLRMo3VhYQSA6otyLnjSEiHoSG/9shXCFNJy9tivpUJvtkN9s6VDdItHa5Rn+fNBzag==",
+ "requires": {
+ "JSONStream": "^1.3.5",
+ "abbrev": "~1.1.1",
+ "ansicolors": "~0.3.2",
+ "ansistyles": "~0.1.3",
+ "aproba": "^2.0.0",
+ "archy": "~1.0.0",
+ "bin-links": "^1.1.8",
+ "bluebird": "^3.5.5",
+ "byte-size": "^5.0.1",
+ "cacache": "^12.0.3",
+ "call-limit": "^1.1.1",
+ "chownr": "^1.1.4",
+ "ci-info": "^2.0.0",
+ "cli-columns": "^3.1.2",
+ "cli-table3": "^0.5.1",
+ "cmd-shim": "^3.0.3",
+ "columnify": "~1.5.4",
+ "config-chain": "^1.1.12",
+ "debuglog": "*",
+ "detect-indent": "~5.0.0",
+ "detect-newline": "^2.1.0",
+ "dezalgo": "~1.0.3",
+ "editor": "~1.0.0",
+ "figgy-pudding": "^3.5.1",
+ "find-npm-prefix": "^1.0.2",
+ "fs-vacuum": "~1.2.10",
+ "fs-write-stream-atomic": "~1.0.10",
+ "gentle-fs": "^2.3.1",
+ "glob": "^7.1.6",
+ "graceful-fs": "^4.2.4",
+ "has-unicode": "~2.0.1",
+ "hosted-git-info": "^2.8.9",
+ "iferr": "^1.0.2",
+ "imurmurhash": "*",
+ "infer-owner": "^1.0.4",
+ "inflight": "~1.0.6",
+ "inherits": "^2.0.4",
+ "ini": "^1.3.8",
+ "init-package-json": "^1.10.3",
+ "is-cidr": "^3.0.0",
+ "json-parse-better-errors": "^1.0.2",
+ "lazy-property": "~1.0.0",
+ "libcipm": "^4.0.8",
+ "libnpm": "^3.0.1",
+ "libnpmaccess": "^3.0.2",
+ "libnpmhook": "^5.0.3",
+ "libnpmorg": "^1.0.1",
+ "libnpmsearch": "^2.0.2",
+ "libnpmteam": "^1.0.2",
+ "libnpx": "^10.2.4",
+ "lock-verify": "^2.1.0",
+ "lockfile": "^1.0.4",
+ "lodash._baseindexof": "*",
+ "lodash._baseuniq": "~4.6.0",
+ "lodash._bindcallback": "*",
+ "lodash._cacheindexof": "*",
+ "lodash._createcache": "*",
+ "lodash._getnative": "*",
+ "lodash.clonedeep": "~4.5.0",
+ "lodash.restparam": "*",
+ "lodash.union": "~4.6.0",
+ "lodash.uniq": "~4.5.0",
+ "lodash.without": "~4.4.0",
+ "lru-cache": "^5.1.1",
+ "meant": "^1.0.2",
+ "mississippi": "^3.0.0",
+ "mkdirp": "^0.5.5",
+ "move-concurrently": "^1.0.1",
+ "node-gyp": "^5.1.0",
+ "nopt": "^4.0.3",
+ "normalize-package-data": "^2.5.0",
+ "npm-audit-report": "^1.3.3",
+ "npm-cache-filename": "~1.0.2",
+ "npm-install-checks": "^3.0.2",
+ "npm-lifecycle": "^3.1.5",
+ "npm-package-arg": "^6.1.1",
+ "npm-packlist": "^1.4.8",
+ "npm-pick-manifest": "^3.0.2",
+ "npm-profile": "^4.0.4",
+ "npm-registry-fetch": "^4.0.7",
+ "npm-user-validate": "^1.0.1",
+ "npmlog": "~4.1.2",
+ "once": "~1.4.0",
+ "opener": "^1.5.2",
+ "osenv": "^0.1.5",
+ "pacote": "^9.5.12",
+ "path-is-inside": "~1.0.2",
+ "promise-inflight": "~1.0.1",
+ "qrcode-terminal": "^0.12.0",
+ "query-string": "^6.8.2",
+ "qw": "~1.0.1",
+ "read": "~1.0.7",
+ "read-cmd-shim": "^1.0.5",
+ "read-installed": "~4.0.3",
+ "read-package-json": "^2.1.1",
+ "read-package-tree": "^5.3.1",
+ "readable-stream": "^3.6.0",
+ "readdir-scoped-modules": "^1.1.0",
+ "request": "^2.88.0",
+ "retry": "^0.12.0",
+ "rimraf": "^2.7.1",
+ "safe-buffer": "^5.1.2",
+ "semver": "^5.7.1",
+ "sha": "^3.0.0",
+ "slide": "~1.1.6",
+ "sorted-object": "~2.0.1",
+ "sorted-union-stream": "~2.1.3",
+ "ssri": "^6.0.2",
+ "stringify-package": "^1.0.1",
+ "tar": "^4.4.13",
+ "text-table": "~0.2.0",
+ "tiny-relative-date": "^1.3.0",
+ "uid-number": "0.0.6",
+ "umask": "~1.1.0",
+ "unique-filename": "^1.1.1",
+ "unpipe": "~1.0.0",
+ "update-notifier": "^2.5.0",
+ "uuid": "^3.3.3",
+ "validate-npm-package-license": "^3.0.4",
+ "validate-npm-package-name": "~3.0.0",
+ "which": "^1.3.1",
+ "worker-farm": "^1.7.0",
+ "write-file-atomic": "^2.4.3"
+ },
+ "dependencies": {
+ "JSONStream": {
+ "version": "1.3.5",
+ "bundled": true,
+ "requires": {
+ "jsonparse": "^1.2.0",
+ "through": ">=2.2.7 <3"
+ }
+ },
+ "abbrev": {
+ "version": "1.1.1",
+ "bundled": true
+ },
+ "agent-base": {
+ "version": "4.3.0",
+ "bundled": true,
+ "requires": {
+ "es6-promisify": "^5.0.0"
+ }
+ },
+ "agentkeepalive": {
+ "version": "3.5.2",
+ "bundled": true,
+ "requires": {
+ "humanize-ms": "^1.2.1"
+ }
+ },
+ "ansi-align": {
+ "version": "2.0.0",
+ "bundled": true,
+ "requires": {
+ "string-width": "^2.0.0"
+ }
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "bundled": true
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "bundled": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "ansicolors": {
+ "version": "0.3.2",
+ "bundled": true
+ },
+ "ansistyles": {
+ "version": "0.1.3",
+ "bundled": true
+ },
+ "aproba": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "archy": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "are-we-there-yet": {
+ "version": "1.1.4",
+ "bundled": true,
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "asap": {
+ "version": "2.0.6",
+ "bundled": true
+ },
+ "asn1": {
+ "version": "0.2.4",
+ "bundled": true,
+ "requires": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
+ "assert-plus": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "bundled": true
+ },
+ "aws-sign2": {
+ "version": "0.7.0",
+ "bundled": true
+ },
+ "aws4": {
+ "version": "1.8.0",
+ "bundled": true
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
+ "bin-links": {
+ "version": "1.1.8",
+ "bundled": true,
+ "requires": {
+ "bluebird": "^3.5.3",
+ "cmd-shim": "^3.0.0",
+ "gentle-fs": "^2.3.0",
+ "graceful-fs": "^4.1.15",
+ "npm-normalize-package-bin": "^1.0.0",
+ "write-file-atomic": "^2.3.0"
+ }
+ },
+ "bluebird": {
+ "version": "3.5.5",
+ "bundled": true
+ },
+ "boxen": {
+ "version": "1.3.0",
+ "bundled": true,
+ "requires": {
+ "ansi-align": "^2.0.0",
+ "camelcase": "^4.0.0",
+ "chalk": "^2.0.1",
+ "cli-boxes": "^1.0.0",
+ "string-width": "^2.0.0",
+ "term-size": "^1.2.0",
+ "widest-line": "^2.0.0"
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "bundled": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "buffer-from": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "builtins": {
+ "version": "1.0.3",
+ "bundled": true
+ },
+ "byline": {
+ "version": "5.0.0",
+ "bundled": true
+ },
+ "byte-size": {
+ "version": "5.0.1",
+ "bundled": true
+ },
+ "cacache": {
+ "version": "12.0.3",
+ "bundled": true,
+ "requires": {
+ "bluebird": "^3.5.5",
+ "chownr": "^1.1.1",
+ "figgy-pudding": "^3.5.1",
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.1.15",
+ "infer-owner": "^1.0.3",
+ "lru-cache": "^5.1.1",
+ "mississippi": "^3.0.0",
+ "mkdirp": "^0.5.1",
+ "move-concurrently": "^1.0.1",
+ "promise-inflight": "^1.0.1",
+ "rimraf": "^2.6.3",
+ "ssri": "^6.0.1",
+ "unique-filename": "^1.1.1",
+ "y18n": "^4.0.0"
+ }
+ },
+ "call-limit": {
+ "version": "1.1.1",
+ "bundled": true
+ },
+ "camelcase": {
+ "version": "4.1.0",
+ "bundled": true
+ },
+ "capture-stack-trace": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "caseless": {
+ "version": "0.12.0",
+ "bundled": true
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "bundled": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "chownr": {
+ "version": "1.1.4",
+ "bundled": true
+ },
+ "ci-info": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "cidr-regex": {
+ "version": "2.0.10",
+ "bundled": true,
+ "requires": {
+ "ip-regex": "^2.1.0"
+ }
+ },
+ "cli-boxes": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "cli-columns": {
+ "version": "3.1.2",
+ "bundled": true,
+ "requires": {
+ "string-width": "^2.0.0",
+ "strip-ansi": "^3.0.1"
+ }
+ },
+ "cli-table3": {
+ "version": "0.5.1",
+ "bundled": true,
+ "requires": {
+ "colors": "^1.1.2",
+ "object-assign": "^4.1.0",
+ "string-width": "^2.1.1"
+ }
+ },
+ "cliui": {
+ "version": "5.0.0",
+ "bundled": true,
+ "requires": {
+ "string-width": "^3.1.0",
+ "strip-ansi": "^5.2.0",
+ "wrap-ansi": "^5.1.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "bundled": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "bundled": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "bundled": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "clone": {
+ "version": "1.0.4",
+ "bundled": true
+ },
+ "cmd-shim": {
+ "version": "3.0.3",
+ "bundled": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "mkdirp": "~0.5.0"
+ }
+ },
+ "code-point-at": {
+ "version": "1.1.0",
+ "bundled": true
+ },
+ "color-convert": {
+ "version": "1.9.1",
+ "bundled": true,
+ "requires": {
+ "color-name": "^1.1.1"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "bundled": true
+ },
+ "colors": {
+ "version": "1.3.3",
+ "bundled": true,
+ "optional": true
+ },
+ "columnify": {
+ "version": "1.5.4",
+ "bundled": true,
+ "requires": {
+ "strip-ansi": "^3.0.0",
+ "wcwidth": "^1.0.0"
+ }
+ },
+ "combined-stream": {
+ "version": "1.0.6",
+ "bundled": true,
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "bundled": true
+ },
+ "concat-stream": {
+ "version": "1.6.2",
+ "bundled": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "config-chain": {
+ "version": "1.1.12",
+ "bundled": true,
+ "requires": {
+ "ini": "^1.3.4",
+ "proto-list": "~1.2.1"
+ }
+ },
+ "configstore": {
+ "version": "3.1.5",
+ "bundled": true,
+ "requires": {
+ "dot-prop": "^4.2.1",
+ "graceful-fs": "^4.1.2",
+ "make-dir": "^1.0.0",
+ "unique-string": "^1.0.0",
+ "write-file-atomic": "^2.0.0",
+ "xdg-basedir": "^3.0.0"
+ }
+ },
+ "console-control-strings": {
+ "version": "1.1.0",
+ "bundled": true
+ },
+ "copy-concurrently": {
+ "version": "1.0.5",
+ "bundled": true,
+ "requires": {
+ "aproba": "^1.1.1",
+ "fs-write-stream-atomic": "^1.0.8",
+ "iferr": "^0.1.5",
+ "mkdirp": "^0.5.1",
+ "rimraf": "^2.5.4",
+ "run-queue": "^1.0.0"
+ },
+ "dependencies": {
+ "aproba": {
+ "version": "1.2.0",
+ "bundled": true
+ },
+ "iferr": {
+ "version": "0.1.5",
+ "bundled": true
+ }
+ }
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "create-error-class": {
+ "version": "3.0.2",
+ "bundled": true,
+ "requires": {
+ "capture-stack-trace": "^1.0.0"
+ }
+ },
+ "cross-spawn": {
+ "version": "5.1.0",
+ "bundled": true,
+ "requires": {
+ "lru-cache": "^4.0.1",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "dependencies": {
+ "lru-cache": {
+ "version": "4.1.5",
+ "bundled": true,
+ "requires": {
+ "pseudomap": "^1.0.2",
+ "yallist": "^2.1.2"
+ }
+ },
+ "yallist": {
+ "version": "2.1.2",
+ "bundled": true
+ }
+ }
+ },
+ "crypto-random-string": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "cyclist": {
+ "version": "0.2.2",
+ "bundled": true
+ },
+ "dashdash": {
+ "version": "1.14.1",
+ "bundled": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "debug": {
+ "version": "3.1.0",
+ "bundled": true,
+ "requires": {
+ "ms": "2.0.0"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.0.0",
+ "bundled": true
+ }
+ }
+ },
+ "debuglog": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "bundled": true
+ },
+ "decode-uri-component": {
+ "version": "0.2.0",
+ "bundled": true
+ },
+ "deep-extend": {
+ "version": "0.6.0",
+ "bundled": true
+ },
+ "defaults": {
+ "version": "1.0.3",
+ "bundled": true,
+ "requires": {
+ "clone": "^1.0.2"
+ }
+ },
+ "define-properties": {
+ "version": "1.1.3",
+ "bundled": true,
+ "requires": {
+ "object-keys": "^1.0.12"
+ }
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "detect-indent": {
+ "version": "5.0.0",
+ "bundled": true
+ },
+ "detect-newline": {
+ "version": "2.1.0",
+ "bundled": true
+ },
+ "dezalgo": {
+ "version": "1.0.3",
+ "bundled": true,
+ "requires": {
+ "asap": "^2.0.0",
+ "wrappy": "1"
+ }
+ },
+ "dot-prop": {
+ "version": "4.2.1",
+ "bundled": true,
+ "requires": {
+ "is-obj": "^1.0.0"
+ }
+ },
+ "dotenv": {
+ "version": "5.0.1",
+ "bundled": true
+ },
+ "duplexer3": {
+ "version": "0.1.4",
+ "bundled": true
+ },
+ "duplexify": {
+ "version": "3.6.0",
+ "bundled": true,
+ "requires": {
+ "end-of-stream": "^1.0.0",
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.0",
+ "stream-shift": "^1.0.0"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "ecc-jsbn": {
+ "version": "0.1.2",
+ "bundled": true,
+ "optional": true,
+ "requires": {
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "editor": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "emoji-regex": {
+ "version": "7.0.3",
+ "bundled": true
+ },
+ "encoding": {
+ "version": "0.1.12",
+ "bundled": true,
+ "requires": {
+ "iconv-lite": "~0.4.13"
+ }
+ },
+ "end-of-stream": {
+ "version": "1.4.1",
+ "bundled": true,
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
+ "env-paths": {
+ "version": "2.2.0",
+ "bundled": true
+ },
+ "err-code": {
+ "version": "1.1.2",
+ "bundled": true
+ },
+ "errno": {
+ "version": "0.1.7",
+ "bundled": true,
+ "requires": {
+ "prr": "~1.0.1"
+ }
+ },
+ "es-abstract": {
+ "version": "1.12.0",
+ "bundled": true,
+ "requires": {
+ "es-to-primitive": "^1.1.1",
+ "function-bind": "^1.1.1",
+ "has": "^1.0.1",
+ "is-callable": "^1.1.3",
+ "is-regex": "^1.0.4"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.0",
+ "bundled": true,
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "es6-promise": {
+ "version": "4.2.8",
+ "bundled": true
+ },
+ "es6-promisify": {
+ "version": "5.0.0",
+ "bundled": true,
+ "requires": {
+ "es6-promise": "^4.0.3"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "bundled": true
+ },
+ "execa": {
+ "version": "0.7.0",
+ "bundled": true,
+ "requires": {
+ "cross-spawn": "^5.0.1",
+ "get-stream": "^3.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ },
+ "dependencies": {
+ "get-stream": {
+ "version": "3.0.0",
+ "bundled": true
+ }
+ }
+ },
+ "extend": {
+ "version": "3.0.2",
+ "bundled": true
+ },
+ "extsprintf": {
+ "version": "1.3.0",
+ "bundled": true
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "figgy-pudding": {
+ "version": "3.5.1",
+ "bundled": true
+ },
+ "find-npm-prefix": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "flush-write-stream": {
+ "version": "1.0.3",
+ "bundled": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.4"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "forever-agent": {
+ "version": "0.6.1",
+ "bundled": true
+ },
+ "form-data": {
+ "version": "2.3.2",
+ "bundled": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "1.0.6",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "from2": {
+ "version": "2.3.0",
+ "bundled": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.0"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "fs-minipass": {
+ "version": "1.2.7",
+ "bundled": true,
+ "requires": {
+ "minipass": "^2.6.0"
+ },
+ "dependencies": {
+ "minipass": {
+ "version": "2.9.0",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
+ }
+ }
+ }
+ },
+ "fs-vacuum": {
+ "version": "1.2.10",
+ "bundled": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "path-is-inside": "^1.0.1",
+ "rimraf": "^2.5.2"
+ }
+ },
+ "fs-write-stream-atomic": {
+ "version": "1.0.10",
+ "bundled": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "iferr": "^0.1.5",
+ "imurmurhash": "^0.1.4",
+ "readable-stream": "1 || 2"
+ },
+ "dependencies": {
+ "iferr": {
+ "version": "0.1.5",
+ "bundled": true
+ },
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "bundled": true
+ },
+ "gauge": {
+ "version": "2.7.4",
+ "bundled": true,
+ "requires": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ },
+ "dependencies": {
+ "aproba": {
+ "version": "1.2.0",
+ "bundled": true
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "bundled": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ }
+ }
+ },
+ "genfun": {
+ "version": "5.0.0",
+ "bundled": true
+ },
+ "gentle-fs": {
+ "version": "2.3.1",
+ "bundled": true,
+ "requires": {
+ "aproba": "^1.1.2",
+ "chownr": "^1.1.2",
+ "cmd-shim": "^3.0.3",
+ "fs-vacuum": "^1.2.10",
+ "graceful-fs": "^4.1.11",
+ "iferr": "^0.1.5",
+ "infer-owner": "^1.0.4",
+ "mkdirp": "^0.5.1",
+ "path-is-inside": "^1.0.2",
+ "read-cmd-shim": "^1.0.1",
+ "slide": "^1.1.6"
+ },
+ "dependencies": {
+ "aproba": {
+ "version": "1.2.0",
+ "bundled": true
+ },
+ "iferr": {
+ "version": "0.1.5",
+ "bundled": true
+ }
+ }
+ },
+ "get-caller-file": {
+ "version": "2.0.5",
+ "bundled": true
+ },
+ "get-stream": {
+ "version": "4.1.0",
+ "bundled": true,
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "getpass": {
+ "version": "0.1.7",
+ "bundled": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "glob": {
+ "version": "7.1.6",
+ "bundled": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "global-dirs": {
+ "version": "0.1.1",
+ "bundled": true,
+ "requires": {
+ "ini": "^1.3.4"
+ }
+ },
+ "got": {
+ "version": "6.7.1",
+ "bundled": true,
+ "requires": {
+ "create-error-class": "^3.0.0",
+ "duplexer3": "^0.1.4",
+ "get-stream": "^3.0.0",
+ "is-redirect": "^1.0.0",
+ "is-retry-allowed": "^1.0.0",
+ "is-stream": "^1.0.0",
+ "lowercase-keys": "^1.0.0",
+ "safe-buffer": "^5.0.1",
+ "timed-out": "^4.0.0",
+ "unzip-response": "^2.0.1",
+ "url-parse-lax": "^1.0.0"
+ },
+ "dependencies": {
+ "get-stream": {
+ "version": "3.0.0",
+ "bundled": true
+ }
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.4",
+ "bundled": true
+ },
+ "har-schema": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "har-validator": {
+ "version": "5.1.5",
+ "bundled": true,
+ "requires": {
+ "ajv": "^6.12.3",
+ "har-schema": "^2.0.0"
+ },
+ "dependencies": {
+ "ajv": {
+ "version": "6.12.6",
+ "bundled": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "bundled": true
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "bundled": true
+ }
+ }
+ },
+ "has": {
+ "version": "1.0.3",
+ "bundled": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "bundled": true
+ },
+ "has-symbols": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "has-unicode": {
+ "version": "2.0.1",
+ "bundled": true
+ },
+ "hosted-git-info": {
+ "version": "2.8.9",
+ "bundled": true
+ },
+ "http-cache-semantics": {
+ "version": "3.8.1",
+ "bundled": true
+ },
+ "http-proxy-agent": {
+ "version": "2.1.0",
+ "bundled": true,
+ "requires": {
+ "agent-base": "4",
+ "debug": "3.1.0"
+ }
+ },
+ "http-signature": {
+ "version": "1.2.0",
+ "bundled": true,
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "jsprim": "^1.2.2",
+ "sshpk": "^1.7.0"
+ }
+ },
+ "https-proxy-agent": {
+ "version": "2.2.4",
+ "bundled": true,
+ "requires": {
+ "agent-base": "^4.3.0",
+ "debug": "^3.1.0"
+ }
+ },
+ "humanize-ms": {
+ "version": "1.2.1",
+ "bundled": true,
+ "requires": {
+ "ms": "^2.0.0"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.23",
+ "bundled": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "iferr": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "ignore-walk": {
+ "version": "3.0.3",
+ "bundled": true,
+ "requires": {
+ "minimatch": "^3.0.4"
+ }
+ },
+ "import-lazy": {
+ "version": "2.1.0",
+ "bundled": true
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "bundled": true
+ },
+ "infer-owner": {
+ "version": "1.0.4",
+ "bundled": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "bundled": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "bundled": true
+ },
+ "ini": {
+ "version": "1.3.8",
+ "bundled": true
+ },
+ "init-package-json": {
+ "version": "1.10.3",
+ "bundled": true,
+ "requires": {
+ "glob": "^7.1.1",
+ "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0",
+ "promzard": "^0.3.0",
+ "read": "~1.0.1",
+ "read-package-json": "1 || 2",
+ "semver": "2.x || 3.x || 4 || 5",
+ "validate-npm-package-license": "^3.0.1",
+ "validate-npm-package-name": "^3.0.0"
+ }
+ },
+ "ip": {
+ "version": "1.1.5",
+ "bundled": true
+ },
+ "ip-regex": {
+ "version": "2.1.0",
+ "bundled": true
+ },
+ "is-callable": {
+ "version": "1.1.4",
+ "bundled": true
+ },
+ "is-ci": {
+ "version": "1.2.1",
+ "bundled": true,
+ "requires": {
+ "ci-info": "^1.5.0"
+ },
+ "dependencies": {
+ "ci-info": {
+ "version": "1.6.0",
+ "bundled": true
+ }
+ }
+ },
+ "is-cidr": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "cidr-regex": "^2.0.10"
+ }
+ },
+ "is-date-object": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "bundled": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "is-installed-globally": {
+ "version": "0.1.0",
+ "bundled": true,
+ "requires": {
+ "global-dirs": "^0.1.0",
+ "is-path-inside": "^1.0.0"
+ }
+ },
+ "is-npm": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "is-obj": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "is-path-inside": {
+ "version": "1.0.1",
+ "bundled": true,
+ "requires": {
+ "path-is-inside": "^1.0.1"
+ }
+ },
+ "is-redirect": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "is-regex": {
+ "version": "1.0.4",
+ "bundled": true,
+ "requires": {
+ "has": "^1.0.1"
+ }
+ },
+ "is-retry-allowed": {
+ "version": "1.2.0",
+ "bundled": true
+ },
+ "is-stream": {
+ "version": "1.1.0",
+ "bundled": true
+ },
+ "is-symbol": {
+ "version": "1.0.2",
+ "bundled": true,
+ "requires": {
+ "has-symbols": "^1.0.0"
+ }
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "isstream": {
+ "version": "0.1.2",
+ "bundled": true
+ },
+ "jsbn": {
+ "version": "0.1.1",
+ "bundled": true,
+ "optional": true
+ },
+ "json-parse-better-errors": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "json-schema": {
+ "version": "0.2.3",
+ "bundled": true
+ },
+ "json-stringify-safe": {
+ "version": "5.0.1",
+ "bundled": true
+ },
+ "jsonparse": {
+ "version": "1.3.1",
+ "bundled": true
+ },
+ "jsprim": {
+ "version": "1.4.1",
+ "bundled": true,
+ "requires": {
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.3.0",
+ "json-schema": "0.2.3",
+ "verror": "1.10.0"
+ }
+ },
+ "latest-version": {
+ "version": "3.1.0",
+ "bundled": true,
+ "requires": {
+ "package-json": "^4.0.0"
+ }
+ },
+ "lazy-property": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "libcipm": {
+ "version": "4.0.8",
+ "bundled": true,
+ "requires": {
+ "bin-links": "^1.1.2",
+ "bluebird": "^3.5.1",
+ "figgy-pudding": "^3.5.1",
+ "find-npm-prefix": "^1.0.2",
+ "graceful-fs": "^4.1.11",
+ "ini": "^1.3.5",
+ "lock-verify": "^2.1.0",
+ "mkdirp": "^0.5.1",
+ "npm-lifecycle": "^3.0.0",
+ "npm-logical-tree": "^1.2.1",
+ "npm-package-arg": "^6.1.0",
+ "pacote": "^9.1.0",
+ "read-package-json": "^2.0.13",
+ "rimraf": "^2.6.2",
+ "worker-farm": "^1.6.0"
+ }
+ },
+ "libnpm": {
+ "version": "3.0.1",
+ "bundled": true,
+ "requires": {
+ "bin-links": "^1.1.2",
+ "bluebird": "^3.5.3",
+ "find-npm-prefix": "^1.0.2",
+ "libnpmaccess": "^3.0.2",
+ "libnpmconfig": "^1.2.1",
+ "libnpmhook": "^5.0.3",
+ "libnpmorg": "^1.0.1",
+ "libnpmpublish": "^1.1.2",
+ "libnpmsearch": "^2.0.2",
+ "libnpmteam": "^1.0.2",
+ "lock-verify": "^2.0.2",
+ "npm-lifecycle": "^3.0.0",
+ "npm-logical-tree": "^1.2.1",
+ "npm-package-arg": "^6.1.0",
+ "npm-profile": "^4.0.2",
+ "npm-registry-fetch": "^4.0.0",
+ "npmlog": "^4.1.2",
+ "pacote": "^9.5.3",
+ "read-package-json": "^2.0.13",
+ "stringify-package": "^1.0.0"
+ }
+ },
+ "libnpmaccess": {
+ "version": "3.0.2",
+ "bundled": true,
+ "requires": {
+ "aproba": "^2.0.0",
+ "get-stream": "^4.0.0",
+ "npm-package-arg": "^6.1.0",
+ "npm-registry-fetch": "^4.0.0"
+ }
+ },
+ "libnpmconfig": {
+ "version": "1.2.1",
+ "bundled": true,
+ "requires": {
+ "figgy-pudding": "^3.5.1",
+ "find-up": "^3.0.0",
+ "ini": "^1.3.5"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "2.2.0",
+ "bundled": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "p-try": {
+ "version": "2.2.0",
+ "bundled": true
+ }
+ }
+ },
+ "libnpmhook": {
+ "version": "5.0.3",
+ "bundled": true,
+ "requires": {
+ "aproba": "^2.0.0",
+ "figgy-pudding": "^3.4.1",
+ "get-stream": "^4.0.0",
+ "npm-registry-fetch": "^4.0.0"
+ }
+ },
+ "libnpmorg": {
+ "version": "1.0.1",
+ "bundled": true,
+ "requires": {
+ "aproba": "^2.0.0",
+ "figgy-pudding": "^3.4.1",
+ "get-stream": "^4.0.0",
+ "npm-registry-fetch": "^4.0.0"
+ }
+ },
+ "libnpmpublish": {
+ "version": "1.1.2",
+ "bundled": true,
+ "requires": {
+ "aproba": "^2.0.0",
+ "figgy-pudding": "^3.5.1",
+ "get-stream": "^4.0.0",
+ "lodash.clonedeep": "^4.5.0",
+ "normalize-package-data": "^2.4.0",
+ "npm-package-arg": "^6.1.0",
+ "npm-registry-fetch": "^4.0.0",
+ "semver": "^5.5.1",
+ "ssri": "^6.0.1"
+ }
+ },
+ "libnpmsearch": {
+ "version": "2.0.2",
+ "bundled": true,
+ "requires": {
+ "figgy-pudding": "^3.5.1",
+ "get-stream": "^4.0.0",
+ "npm-registry-fetch": "^4.0.0"
+ }
+ },
+ "libnpmteam": {
+ "version": "1.0.2",
+ "bundled": true,
+ "requires": {
+ "aproba": "^2.0.0",
+ "figgy-pudding": "^3.4.1",
+ "get-stream": "^4.0.0",
+ "npm-registry-fetch": "^4.0.0"
+ }
+ },
+ "libnpx": {
+ "version": "10.2.4",
+ "bundled": true,
+ "requires": {
+ "dotenv": "^5.0.1",
+ "npm-package-arg": "^6.0.0",
+ "rimraf": "^2.6.2",
+ "safe-buffer": "^5.1.0",
+ "update-notifier": "^2.3.0",
+ "which": "^1.3.0",
+ "y18n": "^4.0.0",
+ "yargs": "^14.2.3"
+ }
+ },
+ "lock-verify": {
+ "version": "2.1.0",
+ "bundled": true,
+ "requires": {
+ "npm-package-arg": "^6.1.0",
+ "semver": "^5.4.1"
+ }
+ },
+ "lockfile": {
+ "version": "1.0.4",
+ "bundled": true,
+ "requires": {
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "lodash._baseindexof": {
+ "version": "3.1.0",
+ "bundled": true
+ },
+ "lodash._baseuniq": {
+ "version": "4.6.0",
+ "bundled": true,
+ "requires": {
+ "lodash._createset": "~4.0.0",
+ "lodash._root": "~3.0.0"
+ }
+ },
+ "lodash._bindcallback": {
+ "version": "3.0.1",
+ "bundled": true
+ },
+ "lodash._cacheindexof": {
+ "version": "3.0.2",
+ "bundled": true
+ },
+ "lodash._createcache": {
+ "version": "3.1.2",
+ "bundled": true,
+ "requires": {
+ "lodash._getnative": "^3.0.0"
+ }
+ },
+ "lodash._createset": {
+ "version": "4.0.3",
+ "bundled": true
+ },
+ "lodash._getnative": {
+ "version": "3.9.1",
+ "bundled": true
+ },
+ "lodash._root": {
+ "version": "3.0.1",
+ "bundled": true
+ },
+ "lodash.clonedeep": {
+ "version": "4.5.0",
+ "bundled": true
+ },
+ "lodash.restparam": {
+ "version": "3.6.1",
+ "bundled": true
+ },
+ "lodash.union": {
+ "version": "4.6.0",
+ "bundled": true
+ },
+ "lodash.uniq": {
+ "version": "4.5.0",
+ "bundled": true
+ },
+ "lodash.without": {
+ "version": "4.4.0",
+ "bundled": true
+ },
+ "lowercase-keys": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "lru-cache": {
+ "version": "5.1.1",
+ "bundled": true,
+ "requires": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "make-dir": {
+ "version": "1.3.0",
+ "bundled": true,
+ "requires": {
+ "pify": "^3.0.0"
+ }
+ },
+ "make-fetch-happen": {
+ "version": "5.0.2",
+ "bundled": true,
+ "requires": {
+ "agentkeepalive": "^3.4.1",
+ "cacache": "^12.0.0",
+ "http-cache-semantics": "^3.8.1",
+ "http-proxy-agent": "^2.1.0",
+ "https-proxy-agent": "^2.2.3",
+ "lru-cache": "^5.1.1",
+ "mississippi": "^3.0.0",
+ "node-fetch-npm": "^2.0.2",
+ "promise-retry": "^1.1.1",
+ "socks-proxy-agent": "^4.0.0",
+ "ssri": "^6.0.0"
+ }
+ },
+ "meant": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "mime-db": {
+ "version": "1.35.0",
+ "bundled": true
+ },
+ "mime-types": {
+ "version": "2.1.19",
+ "bundled": true,
+ "requires": {
+ "mime-db": "~1.35.0"
+ }
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "bundled": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.5",
+ "bundled": true
+ },
+ "minizlib": {
+ "version": "1.3.3",
+ "bundled": true,
+ "requires": {
+ "minipass": "^2.9.0"
+ },
+ "dependencies": {
+ "minipass": {
+ "version": "2.9.0",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
+ }
+ }
+ }
+ },
+ "mississippi": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "concat-stream": "^1.5.0",
+ "duplexify": "^3.4.2",
+ "end-of-stream": "^1.1.0",
+ "flush-write-stream": "^1.0.0",
+ "from2": "^2.1.0",
+ "parallel-transform": "^1.1.0",
+ "pump": "^3.0.0",
+ "pumpify": "^1.3.3",
+ "stream-each": "^1.1.0",
+ "through2": "^2.0.0"
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "bundled": true,
+ "requires": {
+ "minimist": "^1.2.5"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.5",
+ "bundled": true
+ }
+ }
+ },
+ "move-concurrently": {
+ "version": "1.0.1",
+ "bundled": true,
+ "requires": {
+ "aproba": "^1.1.1",
+ "copy-concurrently": "^1.0.0",
+ "fs-write-stream-atomic": "^1.0.8",
+ "mkdirp": "^0.5.1",
+ "rimraf": "^2.5.4",
+ "run-queue": "^1.0.3"
+ },
+ "dependencies": {
+ "aproba": {
+ "version": "1.2.0",
+ "bundled": true
+ }
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "bundled": true
+ },
+ "mute-stream": {
+ "version": "0.0.7",
+ "bundled": true
+ },
+ "node-fetch-npm": {
+ "version": "2.0.2",
+ "bundled": true,
+ "requires": {
+ "encoding": "^0.1.11",
+ "json-parse-better-errors": "^1.0.0",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "node-gyp": {
+ "version": "5.1.0",
+ "bundled": true,
+ "requires": {
+ "env-paths": "^2.2.0",
+ "glob": "^7.1.4",
+ "graceful-fs": "^4.2.2",
+ "mkdirp": "^0.5.1",
+ "nopt": "^4.0.1",
+ "npmlog": "^4.1.2",
+ "request": "^2.88.0",
+ "rimraf": "^2.6.3",
+ "semver": "^5.7.1",
+ "tar": "^4.4.12",
+ "which": "^1.3.1"
+ }
+ },
+ "nopt": {
+ "version": "4.0.3",
+ "bundled": true,
+ "requires": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ }
+ },
+ "normalize-package-data": {
+ "version": "2.5.0",
+ "bundled": true,
+ "requires": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ },
+ "dependencies": {
+ "resolve": {
+ "version": "1.10.0",
+ "bundled": true,
+ "requires": {
+ "path-parse": "^1.0.6"
+ }
+ }
+ }
+ },
+ "npm-audit-report": {
+ "version": "1.3.3",
+ "bundled": true,
+ "requires": {
+ "cli-table3": "^0.5.0",
+ "console-control-strings": "^1.1.0"
+ }
+ },
+ "npm-bundled": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "npm-normalize-package-bin": "^1.0.1"
+ }
+ },
+ "npm-cache-filename": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "npm-install-checks": {
+ "version": "3.0.2",
+ "bundled": true,
+ "requires": {
+ "semver": "^2.3.0 || 3.x || 4 || 5"
+ }
+ },
+ "npm-lifecycle": {
+ "version": "3.1.5",
+ "bundled": true,
+ "requires": {
+ "byline": "^5.0.0",
+ "graceful-fs": "^4.1.15",
+ "node-gyp": "^5.0.2",
+ "resolve-from": "^4.0.0",
+ "slide": "^1.1.6",
+ "uid-number": "0.0.6",
+ "umask": "^1.1.0",
+ "which": "^1.3.1"
+ }
+ },
+ "npm-logical-tree": {
+ "version": "1.2.1",
+ "bundled": true
+ },
+ "npm-normalize-package-bin": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "npm-package-arg": {
+ "version": "6.1.1",
+ "bundled": true,
+ "requires": {
+ "hosted-git-info": "^2.7.1",
+ "osenv": "^0.1.5",
+ "semver": "^5.6.0",
+ "validate-npm-package-name": "^3.0.0"
+ }
+ },
+ "npm-packlist": {
+ "version": "1.4.8",
+ "bundled": true,
+ "requires": {
+ "ignore-walk": "^3.0.1",
+ "npm-bundled": "^1.0.1",
+ "npm-normalize-package-bin": "^1.0.1"
+ }
+ },
+ "npm-pick-manifest": {
+ "version": "3.0.2",
+ "bundled": true,
+ "requires": {
+ "figgy-pudding": "^3.5.1",
+ "npm-package-arg": "^6.0.0",
+ "semver": "^5.4.1"
+ }
+ },
+ "npm-profile": {
+ "version": "4.0.4",
+ "bundled": true,
+ "requires": {
+ "aproba": "^1.1.2 || 2",
+ "figgy-pudding": "^3.4.1",
+ "npm-registry-fetch": "^4.0.0"
+ }
+ },
+ "npm-registry-fetch": {
+ "version": "4.0.7",
+ "bundled": true,
+ "requires": {
+ "JSONStream": "^1.3.4",
+ "bluebird": "^3.5.1",
+ "figgy-pudding": "^3.4.1",
+ "lru-cache": "^5.1.1",
+ "make-fetch-happen": "^5.0.0",
+ "npm-package-arg": "^6.1.0",
+ "safe-buffer": "^5.2.0"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.2.1",
+ "bundled": true
+ }
+ }
+ },
+ "npm-run-path": {
+ "version": "2.0.2",
+ "bundled": true,
+ "requires": {
+ "path-key": "^2.0.0"
+ }
+ },
+ "npm-user-validate": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "npmlog": {
+ "version": "4.1.2",
+ "bundled": true,
+ "requires": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "oauth-sign": {
+ "version": "0.9.0",
+ "bundled": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "bundled": true
+ },
+ "object-keys": {
+ "version": "1.0.12",
+ "bundled": true
+ },
+ "object.getownpropertydescriptors": {
+ "version": "2.0.3",
+ "bundled": true,
+ "requires": {
+ "define-properties": "^1.1.2",
+ "es-abstract": "^1.5.1"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "bundled": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "opener": {
+ "version": "1.5.2",
+ "bundled": true
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "bundled": true,
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "p-finally": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "package-json": {
+ "version": "4.0.1",
+ "bundled": true,
+ "requires": {
+ "got": "^6.7.1",
+ "registry-auth-token": "^3.0.1",
+ "registry-url": "^3.0.3",
+ "semver": "^5.1.0"
+ }
+ },
+ "pacote": {
+ "version": "9.5.12",
+ "bundled": true,
+ "requires": {
+ "bluebird": "^3.5.3",
+ "cacache": "^12.0.2",
+ "chownr": "^1.1.2",
+ "figgy-pudding": "^3.5.1",
+ "get-stream": "^4.1.0",
+ "glob": "^7.1.3",
+ "infer-owner": "^1.0.4",
+ "lru-cache": "^5.1.1",
+ "make-fetch-happen": "^5.0.0",
+ "minimatch": "^3.0.4",
+ "minipass": "^2.3.5",
+ "mississippi": "^3.0.0",
+ "mkdirp": "^0.5.1",
+ "normalize-package-data": "^2.4.0",
+ "npm-normalize-package-bin": "^1.0.0",
+ "npm-package-arg": "^6.1.0",
+ "npm-packlist": "^1.1.12",
+ "npm-pick-manifest": "^3.0.0",
+ "npm-registry-fetch": "^4.0.0",
+ "osenv": "^0.1.5",
+ "promise-inflight": "^1.0.1",
+ "promise-retry": "^1.1.1",
+ "protoduck": "^5.0.1",
+ "rimraf": "^2.6.2",
+ "safe-buffer": "^5.1.2",
+ "semver": "^5.6.0",
+ "ssri": "^6.0.1",
+ "tar": "^4.4.10",
+ "unique-filename": "^1.1.1",
+ "which": "^1.3.1"
+ },
+ "dependencies": {
+ "minipass": {
+ "version": "2.9.0",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
+ }
+ }
+ }
+ },
+ "parallel-transform": {
+ "version": "1.1.0",
+ "bundled": true,
+ "requires": {
+ "cyclist": "~0.2.2",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.1.5"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "path-exists": {
+ "version": "3.0.0",
+ "bundled": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "path-is-inside": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "path-key": {
+ "version": "2.0.1",
+ "bundled": true
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "bundled": true
+ },
+ "performance-now": {
+ "version": "2.1.0",
+ "bundled": true
+ },
+ "pify": {
+ "version": "3.0.0",
+ "bundled": true
+ },
+ "prepend-http": {
+ "version": "1.0.4",
+ "bundled": true
+ },
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "promise-inflight": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "promise-retry": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "err-code": "^1.0.0",
+ "retry": "^0.10.0"
+ },
+ "dependencies": {
+ "retry": {
+ "version": "0.10.1",
+ "bundled": true
+ }
+ }
+ },
+ "promzard": {
+ "version": "0.3.0",
+ "bundled": true,
+ "requires": {
+ "read": "1"
+ }
+ },
+ "proto-list": {
+ "version": "1.2.4",
+ "bundled": true
+ },
+ "protoduck": {
+ "version": "5.0.1",
+ "bundled": true,
+ "requires": {
+ "genfun": "^5.0.0"
+ }
+ },
+ "prr": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "pseudomap": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "psl": {
+ "version": "1.1.29",
+ "bundled": true
+ },
+ "pump": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "pumpify": {
+ "version": "1.5.1",
+ "bundled": true,
+ "requires": {
+ "duplexify": "^3.6.0",
+ "inherits": "^2.0.3",
+ "pump": "^2.0.0"
+ },
+ "dependencies": {
+ "pump": {
+ "version": "2.0.1",
+ "bundled": true,
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ }
+ }
+ },
+ "punycode": {
+ "version": "1.4.1",
+ "bundled": true
+ },
+ "qrcode-terminal": {
+ "version": "0.12.0",
+ "bundled": true
+ },
+ "qs": {
+ "version": "6.5.2",
+ "bundled": true
+ },
+ "query-string": {
+ "version": "6.8.2",
+ "bundled": true,
+ "requires": {
+ "decode-uri-component": "^0.2.0",
+ "split-on-first": "^1.0.0",
+ "strict-uri-encode": "^2.0.0"
+ }
+ },
+ "qw": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "rc": {
+ "version": "1.2.8",
+ "bundled": true,
+ "requires": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ }
+ },
+ "read": {
+ "version": "1.0.7",
+ "bundled": true,
+ "requires": {
+ "mute-stream": "~0.0.4"
+ }
+ },
+ "read-cmd-shim": {
+ "version": "1.0.5",
+ "bundled": true,
+ "requires": {
+ "graceful-fs": "^4.1.2"
+ }
+ },
+ "read-installed": {
+ "version": "4.0.3",
+ "bundled": true,
+ "requires": {
+ "debuglog": "^1.0.1",
+ "graceful-fs": "^4.1.2",
+ "read-package-json": "^2.0.0",
+ "readdir-scoped-modules": "^1.0.0",
+ "semver": "2 || 3 || 4 || 5",
+ "slide": "~1.1.3",
+ "util-extend": "^1.0.1"
+ }
+ },
+ "read-package-json": {
+ "version": "2.1.1",
+ "bundled": true,
+ "requires": {
+ "glob": "^7.1.1",
+ "graceful-fs": "^4.1.2",
+ "json-parse-better-errors": "^1.0.1",
+ "normalize-package-data": "^2.0.0",
+ "npm-normalize-package-bin": "^1.0.0"
+ }
+ },
+ "read-package-tree": {
+ "version": "5.3.1",
+ "bundled": true,
+ "requires": {
+ "read-package-json": "^2.0.0",
+ "readdir-scoped-modules": "^1.0.0",
+ "util-promisify": "^2.1.0"
+ }
+ },
+ "readable-stream": {
+ "version": "3.6.0",
+ "bundled": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "readdir-scoped-modules": {
+ "version": "1.1.0",
+ "bundled": true,
+ "requires": {
+ "debuglog": "^1.0.1",
+ "dezalgo": "^1.0.0",
+ "graceful-fs": "^4.1.2",
+ "once": "^1.3.0"
+ }
+ },
+ "registry-auth-token": {
+ "version": "3.4.0",
+ "bundled": true,
+ "requires": {
+ "rc": "^1.1.6",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "registry-url": {
+ "version": "3.1.0",
+ "bundled": true,
+ "requires": {
+ "rc": "^1.0.1"
+ }
+ },
+ "request": {
+ "version": "2.88.0",
+ "bundled": true,
+ "requires": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.0",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.4.3",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ }
+ },
+ "require-directory": {
+ "version": "2.1.1",
+ "bundled": true
+ },
+ "require-main-filename": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "resolve-from": {
+ "version": "4.0.0",
+ "bundled": true
+ },
+ "retry": {
+ "version": "0.12.0",
+ "bundled": true
+ },
+ "rimraf": {
+ "version": "2.7.1",
+ "bundled": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "run-queue": {
+ "version": "1.0.3",
+ "bundled": true,
+ "requires": {
+ "aproba": "^1.1.1"
+ },
+ "dependencies": {
+ "aproba": {
+ "version": "1.2.0",
+ "bundled": true
+ }
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "bundled": true
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "bundled": true
+ },
+ "semver": {
+ "version": "5.7.1",
+ "bundled": true
+ },
+ "semver-diff": {
+ "version": "2.1.0",
+ "bundled": true,
+ "requires": {
+ "semver": "^5.0.3"
+ }
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "sha": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "graceful-fs": "^4.1.2"
+ }
+ },
+ "shebang-command": {
+ "version": "1.2.0",
+ "bundled": true,
+ "requires": {
+ "shebang-regex": "^1.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "bundled": true
+ },
+ "slide": {
+ "version": "1.1.6",
+ "bundled": true
+ },
+ "smart-buffer": {
+ "version": "4.1.0",
+ "bundled": true
+ },
+ "socks": {
+ "version": "2.3.3",
+ "bundled": true,
+ "requires": {
+ "ip": "1.1.5",
+ "smart-buffer": "^4.1.0"
+ }
+ },
+ "socks-proxy-agent": {
+ "version": "4.0.2",
+ "bundled": true,
+ "requires": {
+ "agent-base": "~4.2.1",
+ "socks": "~2.3.2"
+ },
+ "dependencies": {
+ "agent-base": {
+ "version": "4.2.1",
+ "bundled": true,
+ "requires": {
+ "es6-promisify": "^5.0.0"
+ }
+ }
+ }
+ },
+ "sorted-object": {
+ "version": "2.0.1",
+ "bundled": true
+ },
+ "sorted-union-stream": {
+ "version": "2.1.3",
+ "bundled": true,
+ "requires": {
+ "from2": "^1.3.0",
+ "stream-iterate": "^1.1.0"
+ },
+ "dependencies": {
+ "from2": {
+ "version": "1.3.0",
+ "bundled": true,
+ "requires": {
+ "inherits": "~2.0.1",
+ "readable-stream": "~1.1.10"
+ }
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "bundled": true
+ },
+ "readable-stream": {
+ "version": "1.1.14",
+ "bundled": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "bundled": true
+ }
+ }
+ },
+ "spdx-correct": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-exceptions": {
+ "version": "2.1.0",
+ "bundled": true
+ },
+ "spdx-expression-parse": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-license-ids": {
+ "version": "3.0.5",
+ "bundled": true
+ },
+ "split-on-first": {
+ "version": "1.1.0",
+ "bundled": true
+ },
+ "sshpk": {
+ "version": "1.14.2",
+ "bundled": true,
+ "requires": {
+ "asn1": "~0.2.3",
+ "assert-plus": "^1.0.0",
+ "bcrypt-pbkdf": "^1.0.0",
+ "dashdash": "^1.12.0",
+ "ecc-jsbn": "~0.1.1",
+ "getpass": "^0.1.1",
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.0.2",
+ "tweetnacl": "~0.14.0"
+ }
+ },
+ "ssri": {
+ "version": "6.0.2",
+ "bundled": true,
+ "requires": {
+ "figgy-pudding": "^3.5.1"
+ }
+ },
+ "stream-each": {
+ "version": "1.2.2",
+ "bundled": true,
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "stream-shift": "^1.0.0"
+ }
+ },
+ "stream-iterate": {
+ "version": "1.2.0",
+ "bundled": true,
+ "requires": {
+ "readable-stream": "^2.1.5",
+ "stream-shift": "^1.0.0"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "stream-shift": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "strict-uri-encode": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "bundled": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "bundled": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "bundled": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ }
+ }
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.2.0",
+ "bundled": true
+ }
+ }
+ },
+ "stringify-package": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "bundled": true,
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "strip-eof": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "bundled": true
+ },
+ "supports-color": {
+ "version": "5.4.0",
+ "bundled": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "tar": {
+ "version": "4.4.13",
+ "bundled": true,
+ "requires": {
+ "chownr": "^1.1.1",
+ "fs-minipass": "^1.2.5",
+ "minipass": "^2.8.6",
+ "minizlib": "^1.2.1",
+ "mkdirp": "^0.5.0",
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.3"
+ },
+ "dependencies": {
+ "minipass": {
+ "version": "2.9.0",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
+ }
+ }
+ }
+ },
+ "term-size": {
+ "version": "1.2.0",
+ "bundled": true,
+ "requires": {
+ "execa": "^0.7.0"
+ }
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "bundled": true
+ },
+ "through": {
+ "version": "2.3.8",
+ "bundled": true
+ },
+ "through2": {
+ "version": "2.0.3",
+ "bundled": true,
+ "requires": {
+ "readable-stream": "^2.1.5",
+ "xtend": "~4.0.1"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "timed-out": {
+ "version": "4.0.1",
+ "bundled": true
+ },
+ "tiny-relative-date": {
+ "version": "1.3.0",
+ "bundled": true
+ },
+ "tough-cookie": {
+ "version": "2.4.3",
+ "bundled": true,
+ "requires": {
+ "psl": "^1.1.24",
+ "punycode": "^1.4.1"
+ }
+ },
+ "tunnel-agent": {
+ "version": "0.6.0",
+ "bundled": true,
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "tweetnacl": {
+ "version": "0.14.5",
+ "bundled": true,
+ "optional": true
+ },
+ "typedarray": {
+ "version": "0.0.6",
+ "bundled": true
+ },
+ "uid-number": {
+ "version": "0.0.6",
+ "bundled": true
+ },
+ "umask": {
+ "version": "1.1.0",
+ "bundled": true
+ },
+ "unique-filename": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "unique-slug": "^2.0.0"
+ }
+ },
+ "unique-slug": {
+ "version": "2.0.0",
+ "bundled": true,
+ "requires": {
+ "imurmurhash": "^0.1.4"
+ }
+ },
+ "unique-string": {
+ "version": "1.0.0",
+ "bundled": true,
+ "requires": {
+ "crypto-random-string": "^1.0.0"
+ }
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "unzip-response": {
+ "version": "2.0.1",
+ "bundled": true
+ },
+ "update-notifier": {
+ "version": "2.5.0",
+ "bundled": true,
+ "requires": {
+ "boxen": "^1.2.1",
+ "chalk": "^2.0.1",
+ "configstore": "^3.0.0",
+ "import-lazy": "^2.1.0",
+ "is-ci": "^1.0.10",
+ "is-installed-globally": "^0.1.0",
+ "is-npm": "^1.0.0",
+ "latest-version": "^3.0.0",
+ "semver-diff": "^2.0.0",
+ "xdg-basedir": "^3.0.0"
+ }
+ },
+ "uri-js": {
+ "version": "4.4.0",
+ "bundled": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ },
+ "dependencies": {
+ "punycode": {
+ "version": "2.1.1",
+ "bundled": true
+ }
+ }
+ },
+ "url-parse-lax": {
+ "version": "1.0.0",
+ "bundled": true,
+ "requires": {
+ "prepend-http": "^1.0.1"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "util-extend": {
+ "version": "1.0.3",
+ "bundled": true
+ },
+ "util-promisify": {
+ "version": "2.1.0",
+ "bundled": true,
+ "requires": {
+ "object.getownpropertydescriptors": "^2.0.3"
+ }
+ },
+ "uuid": {
+ "version": "3.3.3",
+ "bundled": true
+ },
+ "validate-npm-package-license": {
+ "version": "3.0.4",
+ "bundled": true,
+ "requires": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "validate-npm-package-name": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "builtins": "^1.0.3"
+ }
+ },
+ "verror": {
+ "version": "1.10.0",
+ "bundled": true,
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ }
+ },
+ "wcwidth": {
+ "version": "1.0.1",
+ "bundled": true,
+ "requires": {
+ "defaults": "^1.0.3"
+ }
+ },
+ "which": {
+ "version": "1.3.1",
+ "bundled": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-module": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "wide-align": {
+ "version": "1.1.2",
+ "bundled": true,
+ "requires": {
+ "string-width": "^1.0.2"
+ },
+ "dependencies": {
+ "string-width": {
+ "version": "1.0.2",
+ "bundled": true,
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ }
+ }
+ },
+ "widest-line": {
+ "version": "2.0.1",
+ "bundled": true,
+ "requires": {
+ "string-width": "^2.1.1"
+ }
+ },
+ "worker-farm": {
+ "version": "1.7.0",
+ "bundled": true,
+ "requires": {
+ "errno": "~0.1.7"
+ }
+ },
+ "wrap-ansi": {
+ "version": "5.1.0",
+ "bundled": true,
+ "requires": {
+ "ansi-styles": "^3.2.0",
+ "string-width": "^3.0.0",
+ "strip-ansi": "^5.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "bundled": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "bundled": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "bundled": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "write-file-atomic": {
+ "version": "2.4.3",
+ "bundled": true,
+ "requires": {
+ "graceful-fs": "^4.1.11",
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "xdg-basedir": {
+ "version": "3.0.0",
+ "bundled": true
+ },
+ "xtend": {
+ "version": "4.0.1",
+ "bundled": true
+ },
+ "y18n": {
+ "version": "4.0.1",
+ "bundled": true
+ },
+ "yallist": {
+ "version": "3.0.3",
+ "bundled": true
+ },
+ "yargs": {
+ "version": "14.2.3",
+ "bundled": true,
+ "requires": {
+ "cliui": "^5.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^15.0.1"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "4.1.0",
+ "bundled": true
+ },
+ "find-up": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "locate-path": "^3.0.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "bundled": true
+ },
+ "locate-path": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ }
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "bundled": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "3.0.0",
+ "bundled": true,
+ "requires": {
+ "p-limit": "^2.0.0"
+ }
+ },
+ "p-try": {
+ "version": "2.2.0",
+ "bundled": true
+ },
+ "string-width": {
+ "version": "3.1.0",
+ "bundled": true,
+ "requires": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "5.2.0",
+ "bundled": true,
+ "requires": {
+ "ansi-regex": "^4.1.0"
+ }
+ }
+ }
+ },
+ "yargs-parser": {
+ "version": "15.0.1",
+ "bundled": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "5.3.1",
+ "bundled": true
+ }
+ }
+ }
+ }
+ },
+ "npmlog": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
+ "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
+ "requires": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
+ },
+ "oauth-sign": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "oniguruma": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/oniguruma/-/oniguruma-7.2.1.tgz",
+ "integrity": "sha512-WPS/e1uzhswPtJSe+Zls/kAj27+lEqZjCmRSjnYk/Z4L2Mu+lJC2JWtkZhPJe4kZeTQfz7ClcLyXlI4J68MG2w==",
+ "requires": {
+ "nan": "^2.14.0"
+ }
+ },
+ "open": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-7.3.0.tgz",
+ "integrity": "sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw==",
+ "requires": {
+ "is-docker": "^2.0.0",
+ "is-wsl": "^2.1.1"
+ }
+ },
+ "os-locale": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+ "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
+ "requires": {
+ "lcid": "^1.0.0"
+ }
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+ },
+ "performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
+ },
+ "prebuild-install": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.4.tgz",
+ "integrity": "sha512-AkKN+pf4fSEihjapLEEj8n85YIw/tN6BQqkhzbDc0RvEZGdkpJBGMUYx66AAMcPG2KzmPQS7Cm16an4HVBRRMA==",
+ "requires": {
+ "detect-libc": "^1.0.3",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp": "^0.5.1",
+ "napi-build-utils": "^1.0.1",
+ "node-abi": "^2.7.0",
+ "noop-logger": "^0.1.1",
+ "npmlog": "^4.0.1",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^3.0.3",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0",
+ "which-pm-runs": "^1.0.0"
+ }
+ },
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+ },
+ "property-accessors": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/property-accessors/-/property-accessors-1.1.3.tgz",
+ "integrity": "sha1-Hd6EAkYxhlkJ7zBwM2VoDF+SixU=",
+ "requires": {
+ "es6-weak-map": "^0.1.2",
+ "mixto": "1.x"
+ }
+ },
+ "psl": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
+ "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
+ },
+ "pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
+ },
+ "q": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
+ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
+ },
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+ },
+ "rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "requires": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ }
+ },
+ "read": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
+ "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=",
+ "requires": {
+ "mute-stream": "~0.0.4"
+ }
+ },
+ "readable-stream": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+ "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "request": {
+ "version": "2.88.2",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
+ "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
+ "requires": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.3",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.5.0",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ }
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "season": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/season/-/season-6.0.2.tgz",
+ "integrity": "sha1-naWPsd3SSCTXYhstxjpxI7UCF7Y=",
+ "requires": {
+ "cson-parser": "^1.3.0",
+ "fs-plus": "^3.0.0",
+ "yargs": "^3.23.0"
+ }
+ },
+ "semver": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
+ "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
+ },
+ "signal-exit": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
+ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
+ },
+ "simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
+ },
+ "simple-get": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz",
+ "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==",
+ "requires": {
+ "decompress-response": "^4.2.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "sshpk": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
+ "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==",
+ "requires": {
+ "asn1": "~0.2.3",
+ "assert-plus": "^1.0.0",
+ "bcrypt-pbkdf": "^1.0.0",
+ "dashdash": "^1.12.0",
+ "ecc-jsbn": "~0.1.1",
+ "getpass": "^0.1.1",
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.0.2",
+ "tweetnacl": "~0.14.0"
+ }
+ },
+ "string-width": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
+ },
+ "tar": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz",
+ "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==",
+ "requires": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^3.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "dependencies": {
+ "chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="
+ },
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
+ }
+ }
+ },
+ "tar-fs": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
+ "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+ "requires": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "requires": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ }
+ }
+ },
+ "temp": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
+ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
+ "requires": {
+ "mkdirp": "^0.5.1",
+ "rimraf": "~2.6.2"
+ },
+ "dependencies": {
+ "rimraf": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
+ }
+ },
+ "tmp": {
+ "version": "0.0.28",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.28.tgz",
+ "integrity": "sha1-Fyc1t/YU6nrzlmT6hM8N5OUV0SA=",
+ "requires": {
+ "os-tmpdir": "~1.0.1"
+ }
+ },
+ "touch": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-0.0.3.tgz",
+ "integrity": "sha1-Ua7z1ElXHU8oel2Hyci0kYGg2x0=",
+ "requires": {
+ "nopt": "~1.0.10"
+ },
+ "dependencies": {
+ "nopt": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+ "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
+ "requires": {
+ "abbrev": "1"
+ }
+ }
+ }
+ },
+ "tough-cookie": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
+ "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
+ "requires": {
+ "psl": "^1.1.28",
+ "punycode": "^2.1.1"
+ }
+ },
+ "traverse": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
+ "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk="
+ },
+ "tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
+ },
+ "type": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
+ "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
+ },
+ "underscore": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz",
+ "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g=="
+ },
+ "underscore-plus": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/underscore-plus/-/underscore-plus-1.7.0.tgz",
+ "integrity": "sha512-A3BEzkeicFLnr+U/Q3EyWwJAQPbA19mtZZ4h+lLq3ttm9kn8WC4R3YpuJZEXmWdLjYP47Zc8aLZm9kwdv+zzvA==",
+ "requires": {
+ "underscore": "^1.9.1"
+ }
+ },
+ "uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+ },
+ "uuid": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
+ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
+ },
+ "verror": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+ "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ }
+ },
+ "which-pm-runs": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz",
+ "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs="
+ },
+ "wide-align": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+ "requires": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "window-size": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz",
+ "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY="
+ },
+ "wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
+ },
+ "wrap-ansi": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+ "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
+ "requires": {
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "wrench": {
+ "version": "1.5.9",
+ "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.5.9.tgz",
+ "integrity": "sha1-QRaRxjqbJTGxcAJnJ5veyiOyFCo="
+ },
+ "xmlbuilder": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.3.tgz",
+ "integrity": "sha1-xGFLp04K0ZbmCcknLNnh3bKKilg="
+ },
+ "xmldom": {
+ "version": "0.1.31",
+ "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz",
+ "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ=="
+ },
+ "y18n": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz",
+ "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ=="
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "yargs": {
+ "version": "3.32.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz",
+ "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=",
+ "requires": {
+ "camelcase": "^2.0.1",
+ "cliui": "^3.0.3",
+ "decamelize": "^1.1.1",
+ "os-locale": "^1.4.0",
+ "string-width": "^1.0.1",
+ "window-size": "^0.1.4",
+ "y18n": "^3.2.0"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/apm/package.json b/apm/package.json
index 64f93716943..55505df2f02 100644
--- a/apm/package.json
+++ b/apm/package.json
@@ -6,6 +6,6 @@
"url": "https://github.com/atom/atom.git"
},
"dependencies": {
- "atom-package-manager": "0.168.0"
+ "atom-package-manager": "2.6.2"
}
}
diff --git a/atom.sh b/atom.sh
index 236f49ff654..ef709ebb343 100755
--- a/atom.sh
+++ b/atom.sh
@@ -1,20 +1,49 @@
-#!/usr/bin/env bash
+#!/bin/bash
if [ "$(uname)" == 'Darwin' ]; then
OS='Mac'
elif [ "$(expr substr $(uname -s) 1 5)" == 'Linux' ]; then
OS='Linux'
-elif [ "$(expr substr $(uname -s) 1 10)" == 'MINGW32_NT' ]; then
- OS='Cygwin'
else
echo "Your platform ($(uname -a)) is not supported."
exit 1
fi
-while getopts ":wtfvh-:" opt; do
+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
;;
@@ -22,11 +51,20 @@ while getopts ":wtfvh-:" opt; do
REDIRECT_STDERR=1
EXPECT_OUTPUT=1
;;
- foreground|test)
+ 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
;;
@@ -40,44 +78,100 @@ while getopts ":wtfvh-:" opt; do
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
- ATOM_APP_NAME=Atom.app
+ 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 isnt set, check /Applications and then ~/Applications for Atom.app
+ # 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 havent found an Atom.app, use spotlight to search for Atom
+ # 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, it is usually located in /Applications. Set the ATOM_PATH environment variable to the directory containing Atom.app."
+ 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" --executed-from="$(pwd)" --pid=$$ "$@"
- exit $?
+ "$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)/..)
- ATOM_PATH="$USR_DIRECTORY/share/atom/atom"
- ATOM_HOME="${ATOM_HOME:-$HOME/.atom}"
- mkdir -p "$ATOM_HOME"
+ 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}
@@ -85,7 +179,12 @@ elif [ $OS == 'Linux' ]; then
if [ $EXPECT_OUTPUT ]; then
"$ATOM_PATH" --executed-from="$(pwd)" --pid=$$ "$@"
- exit $?
+ 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
@@ -103,8 +202,20 @@ on_die() {
}
trap 'on_die' SIGQUIT SIGTERM
-# If the wait flag is set, don't exit this process until Atom tells it to.
+# 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
diff --git a/benchmark/benchmark-bootstrap.coffee b/benchmark/benchmark-bootstrap.coffee
deleted file mode 100644
index 6e9d6dd1f76..00000000000
--- a/benchmark/benchmark-bootstrap.coffee
+++ /dev/null
@@ -1,12 +0,0 @@
-require '../src/window'
-Atom = require '../src/atom'
-window.atom = Atom.loadOrCreate('spec')
-atom.show() unless atom.getLoadSettings().exitWhenDone
-window.atom = atom
-
-{runSpecSuite} = require '../spec/jasmine-helper'
-
-atom.openDevTools()
-
-document.title = "Benchmark Suite"
-runSpecSuite('../benchmark/benchmark-suite', atom.getLoadSettings().logFile)
diff --git a/benchmark/benchmark-helper.coffee b/benchmark/benchmark-helper.coffee
deleted file mode 100644
index d831572c5cd..00000000000
--- a/benchmark/benchmark-helper.coffee
+++ /dev/null
@@ -1,115 +0,0 @@
-require '../spec/spec-helper'
-
-path = require 'path'
-{$} = require '../src/space-pen-extensions'
-{Point} = require 'atom'
-_ = require 'underscore-plus'
-fs = require 'fs-plus'
-Project = require '../src/project'
-TokenizedBuffer = require '../src/tokenized-buffer'
-
-defaultCount = 100
-window.pbenchmark = (args...) -> window.benchmark(args..., profile: true)
-window.fbenchmark = (args...) -> window.benchmark(args..., focused: true)
-window.fpbenchmark = (args...) -> window.benchmark(args..., profile: true, focused: true)
-window.pfbenchmark = window.fpbenchmark
-
-window.benchmarkFixturesProject = new Project(path.join(__dirname, 'fixtures'))
-
-beforeEach ->
- window.project = window.benchmarkFixturesProject
- jasmine.unspy(window, 'setTimeout')
- jasmine.unspy(window, 'clearTimeout')
- jasmine.unspy(TokenizedBuffer::, 'tokenizeInBackground')
-
-window.benchmark = (args...) ->
- description = args.shift()
- if typeof args[0] is 'number'
- count = args.shift()
- else
- count = defaultCount
- [fn, options] = args
- {profile, focused} = (options ? {})
-
- method = if focused then fit else it
- method description, ->
- total = measure ->
- console.profile(description) if profile
- _.times count, fn
- console.profileEnd(description) if profile
- avg = total / count
-
- fullname = @getFullName().replace(/\s|\.$/g, "")
- report = "#{fullname}: #{total} / #{count} = #{avg}ms"
- console.log(report)
-
- if atom.getLoadSettings().exitWhenDone
- url = "https://github.com/_stats"
- data = [type: 'timing', metric: "atom.#{fullname}", ms: avg]
- $.ajax url,
- async: false
- data: JSON.stringify(data)
- error: (args...) ->
- console.log "Failed to send atom.#{fullname}\n#{JSON.stringify(args)}"
-
-window.measure = (fn) ->
- start = new Date().getTime()
- fn()
- new Date().getTime() - start
-
-window.waitsForPromise = (fn) ->
- window.waitsFor (moveOn) ->
- fn().done(moveOn)
-
-window.keyIdentifierForKey = (key) ->
- if key.length > 1 # named key
- key
- else
- charCode = key.toUpperCase().charCodeAt(0)
- "U+00" + charCode.toString(16)
-
-window.keydownEvent = (key, properties={}) ->
- $.Event "keydown", _.extend({originalEvent: {keyIdentifier: keyIdentifierForKey(key)}}, properties)
-
-window.clickEvent = (properties={}) ->
- $.Event "click", properties
-
-window.mouseEvent = (type, properties) ->
- if properties.point
- {point, editorView} = properties
- {top, left} = @pagePixelPositionForPoint(editorView, point)
- properties.pageX = left + 1
- properties.pageY = top + 1
- properties.originalEvent ?= {detail: 1}
- $.Event type, properties
-
-window.mousedownEvent = (properties={}) ->
- window.mouseEvent('mousedown', properties)
-
-window.mousemoveEvent = (properties={}) ->
- window.mouseEvent('mousemove', properties)
-
-window.pagePixelPositionForPoint = (editorView, point) ->
- point = Point.fromObject point
- top = editorView.lines.offset().top + point.row * editorView.lineHeight
- left = editorView.lines.offset().left + point.column * editorView.charWidth - editorView.lines.scrollLeft()
- {top, left}
-
-window.seteditorViewWidthInChars = (editorView, widthInChars, charWidth=editorView.charWidth) ->
- editorView.width(charWidth * widthInChars + editorView.lines.position().left)
-
-$.fn.resultOfTrigger = (type) ->
- event = $.Event(type)
- this.trigger(event)
- event.result
-
-$.fn.enableKeymap = ->
- @on 'keydown', (e) -> window.keymap.handleKeyEvent(e)
-
-$.fn.attachToDom = ->
- $('#jasmine-content').append(this)
-
-$.fn.textInput = (data) ->
- event = document.createEvent 'TextEvent'
- event.initTextEvent('textInput', true, true, window, data)
- this.each -> this.dispatchEvent(event)
diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee
deleted file mode 100644
index 7b06d9758f5..00000000000
--- a/benchmark/benchmark-suite.coffee
+++ /dev/null
@@ -1,219 +0,0 @@
-require './benchmark-helper'
-{$} = require '../src/space-pen-extensions'
-_ = require 'underscore-plus'
-{WorkspaceView} = require 'atom'
-TokenizedBuffer = require '../src/tokenized-buffer'
-
-describe "editorView.", ->
- editorView = null
-
- beforeEach ->
- atom.workspaceViewParentSelector = '#jasmine-content'
- atom.workspaceView = atom.views.getView(atom.workspace).__spacePenView
- atom.workspaceView.attachToDom()
-
- atom.workspaceView.width(1024)
- atom.workspaceView.height(768)
- atom.workspaceView.openSync()
- editorView = atom.workspaceView.getActiveView()
-
- afterEach ->
- if editorView.pendingDisplayUpdate
- waitsFor "editor to finish rendering", (done) ->
- editorView.on 'editor:display-updated', done
-
- describe "keymap.", ->
- event = null
-
- beforeEach ->
- event = keydownEvent('x', target: editorView.hiddenInput[0])
-
- benchmark "keydown-event-with-no-binding", 10, ->
- keymap.handleKeyEvent(event)
-
- describe "opening-buffers.", ->
- benchmark "300-line-file.", ->
- buffer = project.bufferForPathSync('medium.coffee')
-
- describe "empty-file.", ->
- benchmark "insert-delete", ->
- editorView.insertText('x')
- editorView.backspace()
-
- describe "300-line-file.", ->
- beforeEach ->
- atom.workspaceView.openSync('medium.coffee')
-
- describe "at-begining.", ->
- benchmark "insert-delete", ->
- editorView.insertText('x')
- editorView.backspace()
-
- benchmark "insert-delete-rehighlight", ->
- editorView.insertText('"')
- editorView.backspace()
-
- describe "at-end.", ->
- beforeEach ->
- editorView.moveToBottom()
-
- benchmark "insert-delete", ->
- editorView.insertText('"')
- editorView.backspace()
-
- describe "empty-vs-set-innerHTML.", ->
- [firstRow, lastRow] = []
- beforeEach ->
- firstRow = editorView.getModel().getFirstVisibleScreenRow()
- lastRow = editorView.getModel().getLastVisibleScreenRow()
-
- benchmark "build-gutter-html.", 1000, ->
- editorView.gutter.renderLineNumbers(null, firstRow, lastRow)
-
- benchmark "set-innerHTML.", 1000, ->
- editorView.gutter.renderLineNumbers(null, firstRow, lastRow)
- editorView.gutter.lineNumbers[0].innerHtml = ''
-
- benchmark "empty.", 1000, ->
- editorView.gutter.renderLineNumbers(null, firstRow, lastRow)
- editorView.gutter.lineNumbers.empty()
-
- describe "positionLeftForLineAndColumn.", ->
- line = null
- beforeEach ->
- editorView.scrollTop(2000)
- editorView.resetDisplay()
- line = editorView.lineElementForScreenRow(106)[0]
-
- describe "one-line.", ->
- beforeEach ->
- editorView.clearCharacterWidthCache()
-
- benchmark "uncached", 5000, ->
- editorView.positionLeftForLineAndColumn(line, 106, 82)
- editorView.clearCharacterWidthCache()
-
- benchmark "cached", 5000, ->
- editorView.positionLeftForLineAndColumn(line, 106, 82)
-
- describe "multiple-lines.", ->
- [firstRow, lastRow] = []
- beforeEach ->
- firstRow = editorView.getModel().getFirstVisibleScreenRow()
- lastRow = editorView.getModel().getLastVisibleScreenRow()
-
- benchmark "cache-entire-visible-area", 100, ->
- for i in [firstRow..lastRow]
- line = editorView.lineElementForScreenRow(i)[0]
- editorView.positionLeftForLineAndColumn(line, i, Math.max(0, editorView.getModel().lineTextForBufferRow(i).length))
-
- describe "text-rendering.", ->
- beforeEach ->
- editorView.scrollTop(2000)
-
- benchmark "resetDisplay", 50, ->
- editorView.resetDisplay()
-
- benchmark "htmlForScreenRows", 1000, ->
- lastRow = editorView.getLastScreenRow()
- editorView.htmlForScreenRows(0, lastRow)
-
- benchmark "htmlForScreenRows.htmlParsing", 50, ->
- lastRow = editorView.getLastScreenRow()
- html = editorView.htmlForScreenRows(0, lastRow)
-
- div = document.createElement('div')
- div.innerHTML = html
-
- describe "gutter-api.", ->
- describe "getLineNumberElementsForClass.", ->
- beforeEach ->
- editorView.gutter.addClassToLine(20, 'omgwow')
- editorView.gutter.addClassToLine(40, 'omgwow')
-
- benchmark "DOM", 20000, ->
- editorView.gutter.getLineNumberElementsForClass('omgwow')
-
- benchmark "getLineNumberElement.DOM", 20000, ->
- editorView.gutter.getLineNumberElement(12)
-
- benchmark "toggle-class", 2000, ->
- editorView.gutter.addClassToLine(40, 'omgwow')
- editorView.gutter.removeClassFromLine(40, 'omgwow')
-
- describe "find-then-unset.", ->
- classes = ['one', 'two', 'three', 'four']
-
- benchmark "single-class", 200, ->
- editorView.gutter.addClassToLine(30, 'omgwow')
- editorView.gutter.addClassToLine(40, 'omgwow')
- editorView.gutter.removeClassFromAllLines('omgwow')
-
- benchmark "multiple-class", 200, ->
- editorView.gutter.addClassToLine(30, 'one')
- editorView.gutter.addClassToLine(30, 'two')
-
- editorView.gutter.addClassToLine(40, 'two')
- editorView.gutter.addClassToLine(40, 'three')
- editorView.gutter.addClassToLine(40, 'four')
-
- for klass in classes
- editorView.gutter.removeClassFromAllLines(klass)
-
- describe "line-htmlification.", ->
- div = null
- html = null
- beforeEach ->
- lastRow = editorView.getLastScreenRow()
- html = editorView.htmlForScreenRows(0, lastRow)
- div = document.createElement('div')
-
- benchmark "setInnerHTML", 1, ->
- div.innerHTML = html
-
- describe "9000-line-file.", ->
- benchmark "opening.", 5, ->
- atom.workspaceView.openSync('huge.js')
-
- describe "after-opening.", ->
- beforeEach ->
- atom.workspaceView.openSync('huge.js')
-
- benchmark "moving-to-eof.", 1, ->
- editorView.moveToBottom()
-
- describe "on-first-line.", ->
- benchmark "inserting-newline", 5, ->
- editorView.insertNewline()
-
- describe "on-last-visible-line.", ->
- beforeEach ->
- editorView.setCursorScreenPosition([editorView.getLastVisibleScreenRow(), 0])
-
- benchmark "move-down-and-scroll", 300, ->
- editorView.trigger 'move-down'
-
- describe "at-eof.", ->
- endPosition = null
-
- beforeEach ->
- editorView.moveToBottom()
- endPosition = editorView.getCursorScreenPosition()
-
- benchmark "move-to-beginning-of-word", ->
- editorView.moveToBeginningOfWord()
- editorView.setCursorScreenPosition(endPosition)
-
- benchmark "insert", ->
- editorView.insertText('x')
-
-describe "TokenizedBuffer.", ->
- describe "coffee-script-grammar.", ->
- [languageMode, buffer] = []
-
- beforeEach ->
- editor = benchmarkFixturesProject.openSync('medium.coffee')
- {languageMode, buffer} = editor
-
- benchmark "construction", 20, ->
- new TokenizedBuffer(buffer, {languageMode, tabLength: 2})
diff --git a/benchmark/browser-process-startup.coffee b/benchmark/browser-process-startup.coffee
deleted file mode 100755
index 2b06eaaa4c2..00000000000
--- a/benchmark/browser-process-startup.coffee
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/usr/bin/env coffee
-
-{spawn, exec} = require 'child_process'
-fs = require 'fs'
-os = require 'os'
-path = require 'path'
-_ = require 'underscore-plus'
-temp = require 'temp'
-
-directoryToOpen = temp.mkdirSync('browser-process-startup-')
-socketPath = path.join(os.tmpdir(), "atom-#{process.env.USER}.sock")
-numberOfRuns = 10
-
-deleteSocketFile = ->
- try
- fs.unlinkSync(socketPath) if fs.existsSync(socketPath)
- catch error
- console.error(error)
-
-launchAtom = (callback) ->
- deleteSocketFile()
-
- cmd = 'atom'
- args = ['--safe', '--new-window', '--foreground', directoryToOpen]
- atomProcess = spawn(cmd, args)
-
- output = ''
- startupTimes = []
- dataListener = (data) ->
- output += data
- if match = /App load time: (\d+)/.exec(output)
- startupTime = parseInt(match[1])
- atomProcess.stderr.removeListener 'data', dataListener
- atomProcess.kill()
- exec 'pkill -9 Atom', (error) ->
- console.error(error) if error?
- callback(startupTime)
-
- atomProcess.stderr.on 'data', dataListener
-
-startupTimes = []
-collector = (startupTime) ->
- startupTimes.push(startupTime)
- if startupTimes.length < numberOfRuns
- launchAtom(collector)
- else
- maxTime = _.max(startupTimes)
- minTime = _.min(startupTimes)
- totalTime = startupTimes.reduce (previousValue=0, currentValue) -> previousValue + currentValue
- console.log "Startup Runs: #{startupTimes.length}"
- console.log "First run time: #{startupTimes[0]}ms"
- console.log "Max time: #{maxTime}ms"
- console.log "Min time: #{minTime}ms"
- console.log "Average time: #{Math.round(totalTime/startupTimes.length)}ms"
-
-launchAtom(collector)
diff --git a/benchmark/fixtures/huge.js b/benchmark/fixtures/huge.js
deleted file mode 100644
index 0b1562f1455..00000000000
--- a/benchmark/fixtures/huge.js
+++ /dev/null
@@ -1,9245 +0,0 @@
-/*!
- * jQuery JavaScript Library v1.7.1
- * http://jquery.com/
- *
- * Copyright 2011, John Resig
- * Dual licensed under the MIT or GPL Version 2 licenses.
- * http://jquery.org/license
- *
- * Includes Sizzle.js
- * http://sizzlejs.com/
- * Copyright 2011, The Dojo Foundation
- * Released under the MIT, BSD, and GPL Licenses.
- *
- * Date: Mon Nov 21 21:11:03 2011 -0500
- */
-(function( window, undefined ) {
-
-// Use the correct document accordingly with window argument (sandbox)
-var document = window.document,
- navigator = window.navigator,
- location = window.location;
-var jQuery = (function() {
-
-// Define a local copy of jQuery
-var jQuery = function( selector, context ) {
- // The jQuery object is actually just the init constructor 'enhanced'
- return new jQuery.fn.init( selector, context, rootjQuery );
- },
-
- // Map over jQuery in case of overwrite
- _jQuery = window.jQuery,
-
- // Map over the $ in case of overwrite
- _$ = window.$,
-
- // A central reference to the root jQuery(document)
- rootjQuery,
-
- // A simple way to check for HTML strings or ID strings
- // Prioritize #id over to avoid XSS via location.hash (#9521)
- quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,
-
- // Check if a string has a non-whitespace character in it
- rnotwhite = /\S/,
-
- // Used for trimming whitespace
- trimLeft = /^\s+/,
- trimRight = /\s+$/,
-
- // Match a standalone tag
- rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/,
-
- // JSON RegExp
- rvalidchars = /^[\],:{}\s]*$/,
- rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,
- rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,
- rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,
-
- // Useragent RegExp
- rwebkit = /(webkit)[ \/]([\w.]+)/,
- ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/,
- rmsie = /(msie) ([\w.]+)/,
- rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/,
-
- // Matches dashed string for camelizing
- rdashAlpha = /-([a-z]|[0-9])/ig,
- rmsPrefix = /^-ms-/,
-
- // Used by jQuery.camelCase as callback to replace()
- fcamelCase = function( all, letter ) {
- return ( letter + "" ).toUpperCase();
- },
-
- // Keep a UserAgent string for use with jQuery.browser
- userAgent = navigator.userAgent,
-
- // For matching the engine and version of the browser
- browserMatch,
-
- // The deferred used on DOM ready
- readyList,
-
- // The ready event handler
- DOMContentLoaded,
-
- // Save a reference to some core methods
- toString = Object.prototype.toString,
- hasOwn = Object.prototype.hasOwnProperty,
- push = Array.prototype.push,
- slice = Array.prototype.slice,
- trim = String.prototype.trim,
- indexOf = Array.prototype.indexOf,
-
- // [[Class]] -> type pairs
- class2type = {};
-
-jQuery.fn = jQuery.prototype = {
- constructor: jQuery,
- init: function( selector, context, rootjQuery ) {
- var match, elem, ret, doc;
-
- // Handle $(""), $(null), or $(undefined)
- if ( !selector ) {
- return this;
- }
-
- // Handle $(DOMElement)
- if ( selector.nodeType ) {
- this.context = this[0] = selector;
- this.length = 1;
- return this;
- }
-
- // The body element only exists once, optimize finding it
- if ( selector === "body" && !context && document.body ) {
- this.context = document;
- this[0] = document.body;
- this.selector = selector;
- this.length = 1;
- return this;
- }
-
- // Handle HTML strings
- if ( typeof selector === "string" ) {
- // Are we dealing with HTML string or an ID?
- if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
- // Assume that strings that start and end with <> are HTML and skip the regex check
- match = [ null, selector, null ];
-
- } else {
- match = quickExpr.exec( selector );
- }
-
- // Verify a match, and that no context was specified for #id
- if ( match && (match[1] || !context) ) {
-
- // HANDLE: $(html) -> $(array)
- if ( match[1] ) {
- context = context instanceof jQuery ? context[0] : context;
- doc = ( context ? context.ownerDocument || context : document );
-
- // If a single string is passed in and it's a single tag
- // just do a createElement and skip the rest
- ret = rsingleTag.exec( selector );
-
- if ( ret ) {
- if ( jQuery.isPlainObject( context ) ) {
- selector = [ document.createElement( ret[1] ) ];
- jQuery.fn.attr.call( selector, context, true );
-
- } else {
- selector = [ doc.createElement( ret[1] ) ];
- }
-
- } else {
- ret = jQuery.buildFragment( [ match[1] ], [ doc ] );
- selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes;
- }
-
- return jQuery.merge( this, selector );
-
- // HANDLE: $("#id")
- } else {
- elem = document.getElementById( match[2] );
-
- // Check parentNode to catch when Blackberry 4.6 returns
- // nodes that are no longer in the document #6963
- if ( elem && elem.parentNode ) {
- // Handle the case where IE and Opera return items
- // by name instead of ID
- if ( elem.id !== match[2] ) {
- return rootjQuery.find( selector );
- }
-
- // Otherwise, we inject the element directly into the jQuery object
- this.length = 1;
- this[0] = elem;
- }
-
- this.context = document;
- this.selector = selector;
- return this;
- }
-
- // HANDLE: $(expr, $(...))
- } else if ( !context || context.jquery ) {
- return ( context || rootjQuery ).find( selector );
-
- // HANDLE: $(expr, context)
- // (which is just equivalent to: $(context).find(expr)
- } else {
- return this.constructor( context ).find( selector );
- }
-
- // HANDLE: $(function)
- // Shortcut for document ready
- } else if ( jQuery.isFunction( selector ) ) {
- return rootjQuery.ready( selector );
- }
-
- if ( selector.selector !== undefined ) {
- this.selector = selector.selector;
- this.context = selector.context;
- }
-
- return jQuery.makeArray( selector, this );
- },
-
- // Start with an empty selector
- selector: "",
-
- // The current version of jQuery being used
- jquery: "1.7.1",
-
- // The default length of a jQuery object is 0
- length: 0,
-
- // The number of elements contained in the matched element set
- size: function() {
- return this.length;
- },
-
- toArray: function() {
- return slice.call( this, 0 );
- },
-
- // Get the Nth element in the matched element set OR
- // Get the whole matched element set as a clean array
- get: function( num ) {
- return num == null ?
-
- // Return a 'clean' array
- this.toArray() :
-
- // Return just the object
- ( num < 0 ? this[ this.length + num ] : this[ num ] );
- },
-
- // Take an array of elements and push it onto the stack
- // (returning the new matched element set)
- pushStack: function( elems, name, selector ) {
- // Build a new jQuery matched element set
- var ret = this.constructor();
-
- if ( jQuery.isArray( elems ) ) {
- push.apply( ret, elems );
-
- } else {
- jQuery.merge( ret, elems );
- }
-
- // Add the old object onto the stack (as a reference)
- ret.prevObject = this;
-
- ret.context = this.context;
-
- if ( name === "find" ) {
- ret.selector = this.selector + ( this.selector ? " " : "" ) + selector;
- } else if ( name ) {
- ret.selector = this.selector + "." + name + "(" + selector + ")";
- }
-
- // Return the newly-formed element set
- return ret;
- },
-
- // Execute a callback for every element in the matched set.
- // (You can seed the arguments with an array of args, but this is
- // only used internally.)
- each: function( callback, args ) {
- return jQuery.each( this, callback, args );
- },
-
- ready: function( fn ) {
- // Attach the listeners
- jQuery.bindReady();
-
- // Add the callback
- readyList.add( fn );
-
- return this;
- },
-
- eq: function( i ) {
- i = +i;
- return i === -1 ?
- this.slice( i ) :
- this.slice( i, i + 1 );
- },
-
- first: function() {
- return this.eq( 0 );
- },
-
- last: function() {
- return this.eq( -1 );
- },
-
- slice: function() {
- return this.pushStack( slice.apply( this, arguments ),
- "slice", slice.call(arguments).join(",") );
- },
-
- map: function( callback ) {
- return this.pushStack( jQuery.map(this, function( elem, i ) {
- return callback.call( elem, i, elem );
- }));
- },
-
- end: function() {
- return this.prevObject || this.constructor(null);
- },
-
- // For internal use only.
- // Behaves like an Array's method, not like a jQuery method.
- push: push,
- sort: [].sort,
- splice: [].splice
-};
-
-// Give the init function the jQuery prototype for later instantiation
-jQuery.fn.init.prototype = jQuery.fn;
-
-jQuery.extend = jQuery.fn.extend = function() {
- var options, name, src, copy, copyIsArray, clone,
- target = arguments[0] || {},
- i = 1,
- length = arguments.length,
- deep = false;
-
- // Handle a deep copy situation
- if ( typeof target === "boolean" ) {
- deep = target;
- target = arguments[1] || {};
- // skip the boolean and the target
- i = 2;
- }
-
- // Handle case when target is a string or something (possible in deep copy)
- if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
- target = {};
- }
-
- // extend jQuery itself if only one argument is passed
- if ( length === i ) {
- target = this;
- --i;
- }
-
- for ( ; i < length; i++ ) {
- // Only deal with non-null/undefined values
- if ( (options = arguments[ i ]) != null ) {
- // Extend the base object
- for ( name in options ) {
- src = target[ name ];
- copy = options[ name ];
-
- // Prevent never-ending loop
- if ( target === copy ) {
- continue;
- }
-
- // Recurse if we're merging plain objects or arrays
- if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
- if ( copyIsArray ) {
- copyIsArray = false;
- clone = src && jQuery.isArray(src) ? src : [];
-
- } else {
- clone = src && jQuery.isPlainObject(src) ? src : {};
- }
-
- // Never move original objects, clone them
- target[ name ] = jQuery.extend( deep, clone, copy );
-
- // Don't bring in undefined values
- } else if ( copy !== undefined ) {
- target[ name ] = copy;
- }
- }
- }
- }
-
- // Return the modified object
- return target;
-};
-
-jQuery.extend({
- noConflict: function( deep ) {
- if ( window.$ === jQuery ) {
- window.$ = _$;
- }
-
- if ( deep && window.jQuery === jQuery ) {
- window.jQuery = _jQuery;
- }
-
- return jQuery;
- },
-
- // Is the DOM ready to be used? Set to true once it occurs.
- isReady: false,
-
- // A counter to track how many items to wait for before
- // the ready event fires. See #6781
- readyWait: 1,
-
- // Hold (or release) the ready event
- holdReady: function( hold ) {
- if ( hold ) {
- jQuery.readyWait++;
- } else {
- jQuery.ready( true );
- }
- },
-
- // Handle when the DOM is ready
- ready: function( wait ) {
- // Either a released hold or an DOMready/load event and not yet ready
- if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) {
- // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
- if ( !document.body ) {
- return setTimeout( jQuery.ready, 1 );
- }
-
- // Remember that the DOM is ready
- jQuery.isReady = true;
-
- // If a normal DOM Ready event fired, decrement, and wait if need be
- if ( wait !== true && --jQuery.readyWait > 0 ) {
- return;
- }
-
- // If there are functions bound, to execute
- readyList.fireWith( document, [ jQuery ] );
-
- // Trigger any bound ready events
- if ( jQuery.fn.trigger ) {
- jQuery( document ).trigger( "ready" ).off( "ready" );
- }
- }
- },
-
- bindReady: function() {
- if ( readyList ) {
- return;
- }
-
- readyList = jQuery.Callbacks( "once memory" );
-
- // Catch cases where $(document).ready() is called after the
- // browser event has already occurred.
- if ( document.readyState === "complete" ) {
- // Handle it asynchronously to allow scripts the opportunity to delay ready
- return setTimeout( jQuery.ready, 1 );
- }
-
- // Mozilla, Opera and webkit nightlies currently support this event
- if ( document.addEventListener ) {
- // Use the handy event callback
- document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
-
- // A fallback to window.onload, that will always work
- window.addEventListener( "load", jQuery.ready, false );
-
- // If IE event model is used
- } else if ( document.attachEvent ) {
- // ensure firing before onload,
- // maybe late but safe also for iframes
- document.attachEvent( "onreadystatechange", DOMContentLoaded );
-
- // A fallback to window.onload, that will always work
- window.attachEvent( "onload", jQuery.ready );
-
- // If IE and not a frame
- // continually check to see if the document is ready
- var toplevel = false;
-
- try {
- toplevel = window.frameElement == null;
- } catch(e) {}
-
- if ( document.documentElement.doScroll && toplevel ) {
- doScrollCheck();
- }
- }
- },
-
- // See test/unit/core.js for details concerning isFunction.
- // Since version 1.3, DOM methods and functions like alert
- // aren't supported. They return false on IE (#2968).
- isFunction: function( obj ) {
- return jQuery.type(obj) === "function";
- },
-
- isArray: Array.isArray || function( obj ) {
- return jQuery.type(obj) === "array";
- },
-
- // A crude way of determining if an object is a window
- isWindow: function( obj ) {
- return obj && typeof obj === "object" && "setInterval" in obj;
- },
-
- isNumeric: function( obj ) {
- return !isNaN( parseFloat(obj) ) && isFinite( obj );
- },
-
- type: function( obj ) {
- return obj == null ?
- String( obj ) :
- class2type[ toString.call(obj) ] || "object";
- },
-
- isPlainObject: function( obj ) {
- // Must be an Object.
- // Because of IE, we also have to check the presence of the constructor property.
- // Make sure that DOM nodes and window objects don't pass through, as well
- if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
- return false;
- }
-
- try {
- // Not own constructor property must be Object
- if ( obj.constructor &&
- !hasOwn.call(obj, "constructor") &&
- !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
- return false;
- }
- } catch ( e ) {
- // IE8,9 Will throw exceptions on certain host objects #9897
- return false;
- }
-
- // Own properties are enumerated firstly, so to speed up,
- // if last one is own, then all properties are own.
-
- var key;
- for ( key in obj ) {}
-
- return key === undefined || hasOwn.call( obj, key );
- },
-
- isEmptyObject: function( obj ) {
- for ( var name in obj ) {
- return false;
- }
- return true;
- },
-
- error: function( msg ) {
- throw new Error( msg );
- },
-
- parseJSON: function( data ) {
- if ( typeof data !== "string" || !data ) {
- return null;
- }
-
- // Make sure leading/trailing whitespace is removed (IE can't handle it)
- data = jQuery.trim( data );
-
- // Attempt to parse using the native JSON parser first
- if ( window.JSON && window.JSON.parse ) {
- return window.JSON.parse( data );
- }
-
- // Make sure the incoming data is actual JSON
- // Logic borrowed from http://json.org/json2.js
- if ( rvalidchars.test( data.replace( rvalidescape, "@" )
- .replace( rvalidtokens, "]" )
- .replace( rvalidbraces, "")) ) {
-
- return ( new Function( "return " + data ) )();
-
- }
- jQuery.error( "Invalid JSON: " + data );
- },
-
- // Cross-browser xml parsing
- parseXML: function( data ) {
- var xml, tmp;
- try {
- if ( window.DOMParser ) { // Standard
- tmp = new DOMParser();
- xml = tmp.parseFromString( data , "text/xml" );
- } else { // IE
- xml = new ActiveXObject( "Microsoft.XMLDOM" );
- xml.async = "false";
- xml.loadXML( data );
- }
- } catch( e ) {
- xml = undefined;
- }
- if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) {
- jQuery.error( "Invalid XML: " + data );
- }
- return xml;
- },
-
- noop: function() {},
-
- // Evaluates a script in a global context
- // Workarounds based on findings by Jim Driscoll
- // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
- globalEval: function( data ) {
- if ( data && rnotwhite.test( data ) ) {
- // We use execScript on Internet Explorer
- // We use an anonymous function so that context is window
- // rather than jQuery in Firefox
- ( window.execScript || function( data ) {
- window[ "eval" ].call( window, data );
- } )( data );
- }
- },
-
- // Convert dashed to camelCase; used by the css and data modules
- // Microsoft forgot to hump their vendor prefix (#9572)
- camelCase: function( string ) {
- return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
- },
-
- nodeName: function( elem, name ) {
- return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
- },
-
- // args is for internal usage only
- each: function( object, callback, args ) {
- var name, i = 0,
- length = object.length,
- isObj = length === undefined || jQuery.isFunction( object );
-
- if ( args ) {
- if ( isObj ) {
- for ( name in object ) {
- if ( callback.apply( object[ name ], args ) === false ) {
- break;
- }
- }
- } else {
- for ( ; i < length; ) {
- if ( callback.apply( object[ i++ ], args ) === false ) {
- break;
- }
- }
- }
-
- // A special, fast, case for the most common use of each
- } else {
- if ( isObj ) {
- for ( name in object ) {
- if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
- break;
- }
- }
- } else {
- for ( ; i < length; ) {
- if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) {
- break;
- }
- }
- }
- }
-
- return object;
- },
-
- // Use native String.trim function wherever possible
- trim: trim ?
- function( text ) {
- return text == null ?
- "" :
- trim.call( text );
- } :
-
- // Otherwise use our own trimming functionality
- function( text ) {
- return text == null ?
- "" :
- text.toString().replace( trimLeft, "" ).replace( trimRight, "" );
- },
-
- // results is for internal usage only
- makeArray: function( array, results ) {
- var ret = results || [];
-
- if ( array != null ) {
- // The window, strings (and functions) also have 'length'
- // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930
- var type = jQuery.type( array );
-
- if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) {
- push.call( ret, array );
- } else {
- jQuery.merge( ret, array );
- }
- }
-
- return ret;
- },
-
- inArray: function( elem, array, i ) {
- var len;
-
- if ( array ) {
- if ( indexOf ) {
- return indexOf.call( array, elem, i );
- }
-
- len = array.length;
- i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;
-
- for ( ; i < len; i++ ) {
- // Skip accessing in sparse arrays
- if ( i in array && array[ i ] === elem ) {
- return i;
- }
- }
- }
-
- return -1;
- },
-
- merge: function( first, second ) {
- var i = first.length,
- j = 0;
-
- if ( typeof second.length === "number" ) {
- for ( var l = second.length; j < l; j++ ) {
- first[ i++ ] = second[ j ];
- }
-
- } else {
- while ( second[j] !== undefined ) {
- first[ i++ ] = second[ j++ ];
- }
- }
-
- first.length = i;
-
- return first;
- },
-
- grep: function( elems, callback, inv ) {
- var ret = [], retVal;
- inv = !!inv;
-
- // Go through the array, only saving the items
- // that pass the validator function
- for ( var i = 0, length = elems.length; i < length; i++ ) {
- retVal = !!callback( elems[ i ], i );
- if ( inv !== retVal ) {
- ret.push( elems[ i ] );
- }
- }
-
- return ret;
- },
-
- // arg is for internal usage only
- map: function( elems, callback, arg ) {
- var value, key, ret = [],
- i = 0,
- length = elems.length,
- // jquery objects are treated as arrays
- isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ;
-
- // Go through the array, translating each of the items to their
- if ( isArray ) {
- for ( ; i < length; i++ ) {
- value = callback( elems[ i ], i, arg );
-
- if ( value != null ) {
- ret[ ret.length ] = value;
- }
- }
-
- // Go through every key on the object,
- } else {
- for ( key in elems ) {
- value = callback( elems[ key ], key, arg );
-
- if ( value != null ) {
- ret[ ret.length ] = value;
- }
- }
- }
-
- // Flatten any nested arrays
- return ret.concat.apply( [], ret );
- },
-
- // A global GUID counter for objects
- guid: 1,
-
- // Bind a function to a context, optionally partially applying any
- // arguments.
- proxy: function( fn, context ) {
- if ( typeof context === "string" ) {
- var tmp = fn[ context ];
- context = fn;
- fn = tmp;
- }
-
- // Quick check to determine if target is callable, in the spec
- // this throws a TypeError, but we will just return undefined.
- if ( !jQuery.isFunction( fn ) ) {
- return undefined;
- }
-
- // Simulated bind
- var args = slice.call( arguments, 2 ),
- proxy = function() {
- return fn.apply( context, args.concat( slice.call( arguments ) ) );
- };
-
- // Set the guid of unique handler to the same of original handler, so it can be removed
- proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;
-
- return proxy;
- },
-
- // Mutifunctional method to get and set values to a collection
- // The value/s can optionally be executed if it's a function
- access: function( elems, key, value, exec, fn, pass ) {
- var length = elems.length;
-
- // Setting many attributes
- if ( typeof key === "object" ) {
- for ( var k in key ) {
- jQuery.access( elems, k, key[k], exec, fn, value );
- }
- return elems;
- }
-
- // Setting one attribute
- if ( value !== undefined ) {
- // Optionally, function values get executed if exec is true
- exec = !pass && exec && jQuery.isFunction(value);
-
- for ( var i = 0; i < length; i++ ) {
- fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );
- }
-
- return elems;
- }
-
- // Getting an attribute
- return length ? fn( elems[0], key ) : undefined;
- },
-
- now: function() {
- return ( new Date() ).getTime();
- },
-
- // Use of jQuery.browser is frowned upon.
- // More details: http://docs.jquery.com/Utilities/jQuery.browser
- uaMatch: function( ua ) {
- ua = ua.toLowerCase();
-
- var match = rwebkit.exec( ua ) ||
- ropera.exec( ua ) ||
- rmsie.exec( ua ) ||
- ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) ||
- [];
-
- return { browser: match[1] || "", version: match[2] || "0" };
- },
-
- sub: function() {
- function jQuerySub( selector, context ) {
- return new jQuerySub.fn.init( selector, context );
- }
- jQuery.extend( true, jQuerySub, this );
- jQuerySub.superclass = this;
- jQuerySub.fn = jQuerySub.prototype = this();
- jQuerySub.fn.constructor = jQuerySub;
- jQuerySub.sub = this.sub;
- jQuerySub.fn.init = function init( selector, context ) {
- if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) {
- context = jQuerySub( context );
- }
-
- return jQuery.fn.init.call( this, selector, context, rootjQuerySub );
- };
- jQuerySub.fn.init.prototype = jQuerySub.fn;
- var rootjQuerySub = jQuerySub(document);
- return jQuerySub;
- },
-
- browser: {}
-});
-
-// Populate the class2type map
-jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) {
- class2type[ "[object " + name + "]" ] = name.toLowerCase();
-});
-
-browserMatch = jQuery.uaMatch( userAgent );
-if ( browserMatch.browser ) {
- jQuery.browser[ browserMatch.browser ] = true;
- jQuery.browser.version = browserMatch.version;
-}
-
-// Deprecated, use jQuery.browser.webkit instead
-if ( jQuery.browser.webkit ) {
- jQuery.browser.safari = true;
-}
-
-// IE doesn't match non-breaking spaces with \s
-if ( rnotwhite.test( "\xA0" ) ) {
- trimLeft = /^[\s\xA0]+/;
- trimRight = /[\s\xA0]+$/;
-}
-
-// All jQuery objects should point back to these
-rootjQuery = jQuery(document);
-
-// Cleanup functions for the document ready method
-if ( document.addEventListener ) {
- DOMContentLoaded = function() {
- document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
- jQuery.ready();
- };
-
-} else if ( document.attachEvent ) {
- DOMContentLoaded = function() {
- // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
- if ( document.readyState === "complete" ) {
- document.detachEvent( "onreadystatechange", DOMContentLoaded );
- jQuery.ready();
- }
- };
-}
-
-// The DOM ready check for Internet Explorer
-function doScrollCheck() {
- if ( jQuery.isReady ) {
- return;
- }
-
- try {
- // If IE is used, use the trick by Diego Perini
- // http://javascript.nwbox.com/IEContentLoaded/
- document.documentElement.doScroll("left");
- } catch(e) {
- setTimeout( doScrollCheck, 1 );
- return;
- }
-
- // and execute any waiting functions
- jQuery.ready();
-}
-
-return jQuery;
-
-})();
-
-
-// String to Object flags format cache
-var flagsCache = {};
-
-// Convert String-formatted flags into Object-formatted ones and store in cache
-function createFlags( flags ) {
- var object = flagsCache[ flags ] = {},
- i, length;
- flags = flags.split( /\s+/ );
- for ( i = 0, length = flags.length; i < length; i++ ) {
- object[ flags[i] ] = true;
- }
- return object;
-}
-
-/*
- * Create a callback list using the following parameters:
- *
- * flags: an optional list of space-separated flags that will change how
- * the callback list behaves
- *
- * By default a callback list will act like an event callback list and can be
- * "fired" multiple times.
- *
- * Possible flags:
- *
- * once: will ensure the callback list can only be fired once (like a Deferred)
- *
- * memory: will keep track of previous values and will call any callback added
- * after the list has been fired right away with the latest "memorized"
- * values (like a Deferred)
- *
- * unique: will ensure a callback can only be added once (no duplicate in the list)
- *
- * stopOnFalse: interrupt callings when a callback returns false
- *
- */
-jQuery.Callbacks = function( flags ) {
-
- // Convert flags from String-formatted to Object-formatted
- // (we check in cache first)
- flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {};
-
- var // Actual callback list
- list = [],
- // Stack of fire calls for repeatable lists
- stack = [],
- // Last fire value (for non-forgettable lists)
- memory,
- // Flag to know if list is currently firing
- firing,
- // First callback to fire (used internally by add and fireWith)
- firingStart,
- // End of the loop when firing
- firingLength,
- // Index of currently firing callback (modified by remove if needed)
- firingIndex,
- // Add one or several callbacks to the list
- add = function( args ) {
- var i,
- length,
- elem,
- type,
- actual;
- for ( i = 0, length = args.length; i < length; i++ ) {
- elem = args[ i ];
- type = jQuery.type( elem );
- if ( type === "array" ) {
- // Inspect recursively
- add( elem );
- } else if ( type === "function" ) {
- // Add if not in unique mode and callback is not in
- if ( !flags.unique || !self.has( elem ) ) {
- list.push( elem );
- }
- }
- }
- },
- // Fire callbacks
- fire = function( context, args ) {
- args = args || [];
- memory = !flags.memory || [ context, args ];
- firing = true;
- firingIndex = firingStart || 0;
- firingStart = 0;
- firingLength = list.length;
- for ( ; list && firingIndex < firingLength; firingIndex++ ) {
- if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) {
- memory = true; // Mark as halted
- break;
- }
- }
- firing = false;
- if ( list ) {
- if ( !flags.once ) {
- if ( stack && stack.length ) {
- memory = stack.shift();
- self.fireWith( memory[ 0 ], memory[ 1 ] );
- }
- } else if ( memory === true ) {
- self.disable();
- } else {
- list = [];
- }
- }
- },
- // Actual Callbacks object
- self = {
- // Add a callback or a collection of callbacks to the list
- add: function() {
- if ( list ) {
- var length = list.length;
- add( arguments );
- // Do we need to add the callbacks to the
- // current firing batch?
- if ( firing ) {
- firingLength = list.length;
- // With memory, if we're not firing then
- // we should call right away, unless previous
- // firing was halted (stopOnFalse)
- } else if ( memory && memory !== true ) {
- firingStart = length;
- fire( memory[ 0 ], memory[ 1 ] );
- }
- }
- return this;
- },
- // Remove a callback from the list
- remove: function() {
- if ( list ) {
- var args = arguments,
- argIndex = 0,
- argLength = args.length;
- for ( ; argIndex < argLength ; argIndex++ ) {
- for ( var i = 0; i < list.length; i++ ) {
- if ( args[ argIndex ] === list[ i ] ) {
- // Handle firingIndex and firingLength
- if ( firing ) {
- if ( i <= firingLength ) {
- firingLength--;
- if ( i <= firingIndex ) {
- firingIndex--;
- }
- }
- }
- // Remove the element
- list.splice( i--, 1 );
- // If we have some unicity property then
- // we only need to do this once
- if ( flags.unique ) {
- break;
- }
- }
- }
- }
- }
- return this;
- },
- // Control if a given callback is in the list
- has: function( fn ) {
- if ( list ) {
- var i = 0,
- length = list.length;
- for ( ; i < length; i++ ) {
- if ( fn === list[ i ] ) {
- return true;
- }
- }
- }
- return false;
- },
- // Remove all callbacks from the list
- empty: function() {
- list = [];
- return this;
- },
- // Have the list do nothing anymore
- disable: function() {
- list = stack = memory = undefined;
- return this;
- },
- // Is it disabled?
- disabled: function() {
- return !list;
- },
- // Lock the list in its current state
- lock: function() {
- stack = undefined;
- if ( !memory || memory === true ) {
- self.disable();
- }
- return this;
- },
- // Is it locked?
- locked: function() {
- return !stack;
- },
- // Call all callbacks with the given context and arguments
- fireWith: function( context, args ) {
- if ( stack ) {
- if ( firing ) {
- if ( !flags.once ) {
- stack.push( [ context, args ] );
- }
- } else if ( !( flags.once && memory ) ) {
- fire( context, args );
- }
- }
- return this;
- },
- // Call all the callbacks with the given arguments
- fire: function() {
- self.fireWith( this, arguments );
- return this;
- },
- // To know if the callbacks have already been called at least once
- fired: function() {
- return !!memory;
- }
- };
-
- return self;
-};
-
-
-
-
-var // Static reference to slice
- sliceDeferred = [].slice;
-
-jQuery.extend({
-
- Deferred: function( func ) {
- var doneList = jQuery.Callbacks( "once memory" ),
- failList = jQuery.Callbacks( "once memory" ),
- progressList = jQuery.Callbacks( "memory" ),
- state = "pending",
- lists = {
- resolve: doneList,
- reject: failList,
- notify: progressList
- },
- promise = {
- done: doneList.add,
- fail: failList.add,
- progress: progressList.add,
-
- state: function() {
- return state;
- },
-
- // Deprecated
- isResolved: doneList.fired,
- isRejected: failList.fired,
-
- then: function( doneCallbacks, failCallbacks, progressCallbacks ) {
- deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks );
- return this;
- },
- always: function() {
- deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments );
- return this;
- },
- pipe: function( fnDone, fnFail, fnProgress ) {
- return jQuery.Deferred(function( newDefer ) {
- jQuery.each( {
- done: [ fnDone, "resolve" ],
- fail: [ fnFail, "reject" ],
- progress: [ fnProgress, "notify" ]
- }, function( handler, data ) {
- var fn = data[ 0 ],
- action = data[ 1 ],
- returned;
- if ( jQuery.isFunction( fn ) ) {
- deferred[ handler ](function() {
- returned = fn.apply( this, arguments );
- if ( returned && jQuery.isFunction( returned.promise ) ) {
- returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify );
- } else {
- newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] );
- }
- });
- } else {
- deferred[ handler ]( newDefer[ action ] );
- }
- });
- }).promise();
- },
- // Get a promise for this deferred
- // If obj is provided, the promise aspect is added to the object
- promise: function( obj ) {
- if ( obj == null ) {
- obj = promise;
- } else {
- for ( var key in promise ) {
- obj[ key ] = promise[ key ];
- }
- }
- return obj;
- }
- },
- deferred = promise.promise({}),
- key;
-
- for ( key in lists ) {
- deferred[ key ] = lists[ key ].fire;
- deferred[ key + "With" ] = lists[ key ].fireWith;
- }
-
- // Handle state
- deferred.done( function() {
- state = "resolved";
- }, failList.disable, progressList.lock ).fail( function() {
- state = "rejected";
- }, doneList.disable, progressList.lock );
-
- // Call given func if any
- if ( func ) {
- func.call( deferred, deferred );
- }
-
- // All done!
- return deferred;
- },
-
- // Deferred helper
- when: function( firstParam ) {
- var args = sliceDeferred.call( arguments, 0 ),
- i = 0,
- length = args.length,
- pValues = new Array( length ),
- count = length,
- pCount = length,
- deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ?
- firstParam :
- jQuery.Deferred(),
- promise = deferred.promise();
- function resolveFunc( i ) {
- return function( value ) {
- args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
- if ( !( --count ) ) {
- deferred.resolveWith( deferred, args );
- }
- };
- }
- function progressFunc( i ) {
- return function( value ) {
- pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
- deferred.notifyWith( promise, pValues );
- };
- }
- if ( length > 1 ) {
- for ( ; i < length; i++ ) {
- if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) {
- args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) );
- } else {
- --count;
- }
- }
- if ( !count ) {
- deferred.resolveWith( deferred, args );
- }
- } else if ( deferred !== firstParam ) {
- deferred.resolveWith( deferred, length ? [ firstParam ] : [] );
- }
- return promise;
- }
-});
-
-
-
-
-jQuery.support = (function() {
-
- var support,
- all,
- a,
- select,
- opt,
- input,
- marginDiv,
- fragment,
- tds,
- events,
- eventName,
- i,
- isSupported,
- div = document.createElement( "div" ),
- documentElement = document.documentElement;
-
- // Preliminary tests
- div.setAttribute("className", "t");
- div.innerHTML = "
a";
-
- all = div.getElementsByTagName( "*" );
- a = div.getElementsByTagName( "a" )[ 0 ];
-
- // Can't get basic test support
- if ( !all || !all.length || !a ) {
- return {};
- }
-
- // First batch of supports tests
- select = document.createElement( "select" );
- opt = select.appendChild( document.createElement("option") );
- input = div.getElementsByTagName( "input" )[ 0 ];
-
- support = {
- // IE strips leading whitespace when .innerHTML is used
- leadingWhitespace: ( div.firstChild.nodeType === 3 ),
-
- // Make sure that tbody elements aren't automatically inserted
- // IE will insert them into empty tables
- tbody: !div.getElementsByTagName("tbody").length,
-
- // Make sure that link elements get serialized correctly by innerHTML
- // This requires a wrapper element in IE
- htmlSerialize: !!div.getElementsByTagName("link").length,
-
- // Get the style information from getAttribute
- // (IE uses .cssText instead)
- style: /top/.test( a.getAttribute("style") ),
-
- // Make sure that URLs aren't manipulated
- // (IE normalizes it by default)
- hrefNormalized: ( a.getAttribute("href") === "/a" ),
-
- // Make sure that element opacity exists
- // (IE uses filter instead)
- // Use a regex to work around a WebKit issue. See #5145
- opacity: /^0.55/.test( a.style.opacity ),
-
- // Verify style float existence
- // (IE uses styleFloat instead of cssFloat)
- cssFloat: !!a.style.cssFloat,
-
- // Make sure that if no value is specified for a checkbox
- // that it defaults to "on".
- // (WebKit defaults to "" instead)
- checkOn: ( input.value === "on" ),
-
- // Make sure that a selected-by-default option has a working selected property.
- // (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
- optSelected: opt.selected,
-
- // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
- getSetAttribute: div.className !== "t",
-
- // Tests for enctype support on a form(#6743)
- enctype: !!document.createElement("form").enctype,
-
- // Makes sure cloning an html5 element does not cause problems
- // Where outerHTML is undefined, this still works
- html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>",
-
- // Will be defined later
- submitBubbles: true,
- changeBubbles: true,
- focusinBubbles: false,
- deleteExpando: true,
- noCloneEvent: true,
- inlineBlockNeedsLayout: false,
- shrinkWrapBlocks: false,
- reliableMarginRight: true
- };
-
- // Make sure checked status is properly cloned
- input.checked = true;
- support.noCloneChecked = input.cloneNode( true ).checked;
-
- // Make sure that the options inside disabled selects aren't marked as disabled
- // (WebKit marks them as disabled)
- select.disabled = true;
- support.optDisabled = !opt.disabled;
-
- // Test to see if it's possible to delete an expando from an element
- // Fails in Internet Explorer
- try {
- delete div.test;
- } catch( e ) {
- support.deleteExpando = false;
- }
-
- if ( !div.addEventListener && div.attachEvent && div.fireEvent ) {
- div.attachEvent( "onclick", function() {
- // Cloning a node shouldn't copy over any
- // bound event handlers (IE does this)
- support.noCloneEvent = false;
- });
- div.cloneNode( true ).fireEvent( "onclick" );
- }
-
- // Check if a radio maintains its value
- // after being appended to the DOM
- input = document.createElement("input");
- input.value = "t";
- input.setAttribute("type", "radio");
- support.radioValue = input.value === "t";
-
- input.setAttribute("checked", "checked");
- div.appendChild( input );
- fragment = document.createDocumentFragment();
- fragment.appendChild( div.lastChild );
-
- // WebKit doesn't clone checked state correctly in fragments
- support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;
-
- // Check if a disconnected checkbox will retain its checked
- // value of true after appended to the DOM (IE6/7)
- support.appendChecked = input.checked;
-
- fragment.removeChild( input );
- fragment.appendChild( div );
-
- div.innerHTML = "";
-
- // Check if div with explicit width and no margin-right incorrectly
- // gets computed margin-right based on width of container. For more
- // info see bug #3333
- // Fails in WebKit before Feb 2011 nightlies
- // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
- if ( window.getComputedStyle ) {
- marginDiv = document.createElement( "div" );
- marginDiv.style.width = "0";
- marginDiv.style.marginRight = "0";
- div.style.width = "2px";
- div.appendChild( marginDiv );
- support.reliableMarginRight =
- ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0;
- }
-
- // Technique from Juriy Zaytsev
- // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/
- // We only care about the case where non-standard event systems
- // are used, namely in IE. Short-circuiting here helps us to
- // avoid an eval call (in setAttribute) which can cause CSP
- // to go haywire. See: https://developer.mozilla.org/en/Security/CSP
- if ( div.attachEvent ) {
- for( i in {
- submit: 1,
- change: 1,
- focusin: 1
- }) {
- eventName = "on" + i;
- isSupported = ( eventName in div );
- if ( !isSupported ) {
- div.setAttribute( eventName, "return;" );
- isSupported = ( typeof div[ eventName ] === "function" );
- }
- support[ i + "Bubbles" ] = isSupported;
- }
- }
-
- fragment.removeChild( div );
-
- // Null elements to avoid leaks in IE
- fragment = select = opt = marginDiv = div = input = null;
-
- // Run tests that need a body at doc ready
- jQuery(function() {
- var container, outer, inner, table, td, offsetSupport,
- conMarginTop, ptlm, vb, style, html,
- body = document.getElementsByTagName("body")[0];
-
- if ( !body ) {
- // Return for frameset docs that don't have a body
- return;
- }
-
- conMarginTop = 1;
- ptlm = "position:absolute;top:0;left:0;width:1px;height:1px;margin:0;";
- vb = "visibility:hidden;border:0;";
- style = "style='" + ptlm + "border:5px solid #000;padding:0;'";
- html = "
" +
- "
" +
- "
";
-
- container = document.createElement("div");
- container.style.cssText = vb + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px";
- body.insertBefore( container, body.firstChild );
-
- // Construct the test element
- div = document.createElement("div");
- container.appendChild( div );
-
- // Check if table cells still have offsetWidth/Height when they are set
- // to display:none and there are still other visible table cells in a
- // table row; if so, offsetWidth/Height are not reliable for use when
- // determining if an element has been hidden directly using
- // display:none (it is still safe to use offsets if a parent element is
- // hidden; don safety goggles and see bug #4512 for more information).
- // (only IE 8 fails this test)
- div.innerHTML = "
t
";
- tds = div.getElementsByTagName( "td" );
- isSupported = ( tds[ 0 ].offsetHeight === 0 );
-
- tds[ 0 ].style.display = "";
- tds[ 1 ].style.display = "none";
-
- // Check if empty table cells still have offsetWidth/Height
- // (IE <= 8 fail this test)
- support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );
-
- // Figure out if the W3C box model works as expected
- div.innerHTML = "";
- div.style.width = div.style.paddingLeft = "1px";
- jQuery.boxModel = support.boxModel = div.offsetWidth === 2;
-
- if ( typeof div.style.zoom !== "undefined" ) {
- // Check if natively block-level elements act like inline-block
- // elements when setting their display to 'inline' and giving
- // them layout
- // (IE < 8 does this)
- div.style.display = "inline";
- div.style.zoom = 1;
- support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 );
-
- // Check if elements with layout shrink-wrap their children
- // (IE 6 does this)
- div.style.display = "";
- div.innerHTML = "";
- support.shrinkWrapBlocks = ( div.offsetWidth !== 2 );
- }
-
- div.style.cssText = ptlm + vb;
- div.innerHTML = html;
-
- outer = div.firstChild;
- inner = outer.firstChild;
- td = outer.nextSibling.firstChild.firstChild;
-
- offsetSupport = {
- doesNotAddBorder: ( inner.offsetTop !== 5 ),
- doesAddBorderForTableAndCells: ( td.offsetTop === 5 )
- };
-
- inner.style.position = "fixed";
- inner.style.top = "20px";
-
- // safari subtracts parent border width here which is 5px
- offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 );
- inner.style.position = inner.style.top = "";
-
- outer.style.overflow = "hidden";
- outer.style.position = "relative";
-
- offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 );
- offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop );
-
- body.removeChild( container );
- div = container = null;
-
- jQuery.extend( support, offsetSupport );
- });
-
- return support;
-})();
-
-
-
-
-var rbrace = /^(?:\{.*\}|\[.*\])$/,
- rmultiDash = /([A-Z])/g;
-
-jQuery.extend({
- cache: {},
-
- // Please use with caution
- uuid: 0,
-
- // Unique for each copy of jQuery on the page
- // Non-digits removed to match rinlinejQuery
- expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),
-
- // The following elements throw uncatchable exceptions if you
- // attempt to add expando properties to them.
- noData: {
- "embed": true,
- // Ban all objects except for Flash (which handle expandos)
- "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
- "applet": true
- },
-
- hasData: function( elem ) {
- elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
- return !!elem && !isEmptyDataObject( elem );
- },
-
- data: function( elem, name, data, pvt /* Internal Use Only */ ) {
- if ( !jQuery.acceptData( elem ) ) {
- return;
- }
-
- var privateCache, thisCache, ret,
- internalKey = jQuery.expando,
- getByName = typeof name === "string",
-
- // We have to handle DOM nodes and JS objects differently because IE6-7
- // can't GC object references properly across the DOM-JS boundary
- isNode = elem.nodeType,
-
- // Only DOM nodes need the global jQuery cache; JS object data is
- // attached directly to the object so GC can occur automatically
- cache = isNode ? jQuery.cache : elem,
-
- // Only defining an ID for JS objects if its cache already exists allows
- // the code to shortcut on the same path as a DOM node with no cache
- id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey,
- isEvents = name === "events";
-
- // Avoid doing any more work than we need to when trying to get data on an
- // object that has no data at all
- if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) {
- return;
- }
-
- if ( !id ) {
- // Only DOM nodes need a new unique ID for each element since their data
- // ends up in the global cache
- if ( isNode ) {
- elem[ internalKey ] = id = ++jQuery.uuid;
- } else {
- id = internalKey;
- }
- }
-
- if ( !cache[ id ] ) {
- cache[ id ] = {};
-
- // Avoids exposing jQuery metadata on plain JS objects when the object
- // is serialized using JSON.stringify
- if ( !isNode ) {
- cache[ id ].toJSON = jQuery.noop;
- }
- }
-
- // An object can be passed to jQuery.data instead of a key/value pair; this gets
- // shallow copied over onto the existing cache
- if ( typeof name === "object" || typeof name === "function" ) {
- if ( pvt ) {
- cache[ id ] = jQuery.extend( cache[ id ], name );
- } else {
- cache[ id ].data = jQuery.extend( cache[ id ].data, name );
- }
- }
-
- privateCache = thisCache = cache[ id ];
-
- // jQuery data() is stored in a separate object inside the object's internal data
- // cache in order to avoid key collisions between internal data and user-defined
- // data.
- if ( !pvt ) {
- if ( !thisCache.data ) {
- thisCache.data = {};
- }
-
- thisCache = thisCache.data;
- }
-
- if ( data !== undefined ) {
- thisCache[ jQuery.camelCase( name ) ] = data;
- }
-
- // Users should not attempt to inspect the internal events object using jQuery.data,
- // it is undocumented and subject to change. But does anyone listen? No.
- if ( isEvents && !thisCache[ name ] ) {
- return privateCache.events;
- }
-
- // Check for both converted-to-camel and non-converted data property names
- // If a data property was specified
- if ( getByName ) {
-
- // First Try to find as-is property data
- ret = thisCache[ name ];
-
- // Test for null|undefined property data
- if ( ret == null ) {
-
- // Try to find the camelCased property
- ret = thisCache[ jQuery.camelCase( name ) ];
- }
- } else {
- ret = thisCache;
- }
-
- return ret;
- },
-
- removeData: function( elem, name, pvt /* Internal Use Only */ ) {
- if ( !jQuery.acceptData( elem ) ) {
- return;
- }
-
- var thisCache, i, l,
-
- // Reference to internal data cache key
- internalKey = jQuery.expando,
-
- isNode = elem.nodeType,
-
- // See jQuery.data for more information
- cache = isNode ? jQuery.cache : elem,
-
- // See jQuery.data for more information
- id = isNode ? elem[ internalKey ] : internalKey;
-
- // If there is already no cache entry for this object, there is no
- // purpose in continuing
- if ( !cache[ id ] ) {
- return;
- }
-
- if ( name ) {
-
- thisCache = pvt ? cache[ id ] : cache[ id ].data;
-
- if ( thisCache ) {
-
- // Support array or space separated string names for data keys
- if ( !jQuery.isArray( name ) ) {
-
- // try the string as a key before any manipulation
- if ( name in thisCache ) {
- name = [ name ];
- } else {
-
- // split the camel cased version by spaces unless a key with the spaces exists
- name = jQuery.camelCase( name );
- if ( name in thisCache ) {
- name = [ name ];
- } else {
- name = name.split( " " );
- }
- }
- }
-
- for ( i = 0, l = name.length; i < l; i++ ) {
- delete thisCache[ name[i] ];
- }
-
- // If there is no data left in the cache, we want to continue
- // and let the cache object itself get destroyed
- if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) {
- return;
- }
- }
- }
-
- // See jQuery.data for more information
- if ( !pvt ) {
- delete cache[ id ].data;
-
- // Don't destroy the parent cache unless the internal data object
- // had been the only thing left in it
- if ( !isEmptyDataObject(cache[ id ]) ) {
- return;
- }
- }
-
- // Browsers that fail expando deletion also refuse to delete expandos on
- // the window, but it will allow it on all other JS objects; other browsers
- // don't care
- // Ensure that `cache` is not a window object #10080
- if ( jQuery.support.deleteExpando || !cache.setInterval ) {
- delete cache[ id ];
- } else {
- cache[ id ] = null;
- }
-
- // We destroyed the cache and need to eliminate the expando on the node to avoid
- // false lookups in the cache for entries that no longer exist
- if ( isNode ) {
- // IE does not allow us to delete expando properties from nodes,
- // nor does it have a removeAttribute function on Document nodes;
- // we must handle all of these cases
- if ( jQuery.support.deleteExpando ) {
- delete elem[ internalKey ];
- } else if ( elem.removeAttribute ) {
- elem.removeAttribute( internalKey );
- } else {
- elem[ internalKey ] = null;
- }
- }
- },
-
- // For internal use only.
- _data: function( elem, name, data ) {
- return jQuery.data( elem, name, data, true );
- },
-
- // A method for determining if a DOM node can handle the data expando
- acceptData: function( elem ) {
- if ( elem.nodeName ) {
- var match = jQuery.noData[ elem.nodeName.toLowerCase() ];
-
- if ( match ) {
- return !(match === true || elem.getAttribute("classid") !== match);
- }
- }
-
- return true;
- }
-});
-
-jQuery.fn.extend({
- data: function( key, value ) {
- var parts, attr, name,
- data = null;
-
- if ( typeof key === "undefined" ) {
- if ( this.length ) {
- data = jQuery.data( this[0] );
-
- if ( this[0].nodeType === 1 && !jQuery._data( this[0], "parsedAttrs" ) ) {
- attr = this[0].attributes;
- for ( var i = 0, l = attr.length; i < l; i++ ) {
- name = attr[i].name;
-
- if ( name.indexOf( "data-" ) === 0 ) {
- name = jQuery.camelCase( name.substring(5) );
-
- dataAttr( this[0], name, data[ name ] );
- }
- }
- jQuery._data( this[0], "parsedAttrs", true );
- }
- }
-
- return data;
-
- } else if ( typeof key === "object" ) {
- return this.each(function() {
- jQuery.data( this, key );
- });
- }
-
- parts = key.split(".");
- parts[1] = parts[1] ? "." + parts[1] : "";
-
- if ( value === undefined ) {
- data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
-
- // Try to fetch any internally stored data first
- if ( data === undefined && this.length ) {
- data = jQuery.data( this[0], key );
- data = dataAttr( this[0], key, data );
- }
-
- return data === undefined && parts[1] ?
- this.data( parts[0] ) :
- data;
-
- } else {
- return this.each(function() {
- var self = jQuery( this ),
- args = [ parts[0], value ];
-
- self.triggerHandler( "setData" + parts[1] + "!", args );
- jQuery.data( this, key, value );
- self.triggerHandler( "changeData" + parts[1] + "!", args );
- });
- }
- },
-
- removeData: function( key ) {
- return this.each(function() {
- jQuery.removeData( this, key );
- });
- }
-});
-
-function dataAttr( elem, key, data ) {
- // If nothing was found internally, try to fetch any
- // data from the HTML5 data-* attribute
- if ( data === undefined && elem.nodeType === 1 ) {
-
- var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
-
- data = elem.getAttribute( name );
-
- if ( typeof data === "string" ) {
- try {
- data = data === "true" ? true :
- data === "false" ? false :
- data === "null" ? null :
- jQuery.isNumeric( data ) ? parseFloat( data ) :
- rbrace.test( data ) ? jQuery.parseJSON( data ) :
- data;
- } catch( e ) {}
-
- // Make sure we set the data so it isn't changed later
- jQuery.data( elem, key, data );
-
- } else {
- data = undefined;
- }
- }
-
- return data;
-}
-
-// checks a cache object for emptiness
-function isEmptyDataObject( obj ) {
- for ( var name in obj ) {
-
- // if the public data object is empty, the private is still empty
- if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) {
- continue;
- }
- if ( name !== "toJSON" ) {
- return false;
- }
- }
-
- return true;
-}
-
-
-
-
-function handleQueueMarkDefer( elem, type, src ) {
- var deferDataKey = type + "defer",
- queueDataKey = type + "queue",
- markDataKey = type + "mark",
- defer = jQuery._data( elem, deferDataKey );
- if ( defer &&
- ( src === "queue" || !jQuery._data(elem, queueDataKey) ) &&
- ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) {
- // Give room for hard-coded callbacks to fire first
- // and eventually mark/queue something else on the element
- setTimeout( function() {
- if ( !jQuery._data( elem, queueDataKey ) &&
- !jQuery._data( elem, markDataKey ) ) {
- jQuery.removeData( elem, deferDataKey, true );
- defer.fire();
- }
- }, 0 );
- }
-}
-
-jQuery.extend({
-
- _mark: function( elem, type ) {
- if ( elem ) {
- type = ( type || "fx" ) + "mark";
- jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 );
- }
- },
-
- _unmark: function( force, elem, type ) {
- if ( force !== true ) {
- type = elem;
- elem = force;
- force = false;
- }
- if ( elem ) {
- type = type || "fx";
- var key = type + "mark",
- count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 );
- if ( count ) {
- jQuery._data( elem, key, count );
- } else {
- jQuery.removeData( elem, key, true );
- handleQueueMarkDefer( elem, type, "mark" );
- }
- }
- },
-
- queue: function( elem, type, data ) {
- var q;
- if ( elem ) {
- type = ( type || "fx" ) + "queue";
- q = jQuery._data( elem, type );
-
- // Speed up dequeue by getting out quickly if this is just a lookup
- if ( data ) {
- if ( !q || jQuery.isArray(data) ) {
- q = jQuery._data( elem, type, jQuery.makeArray(data) );
- } else {
- q.push( data );
- }
- }
- return q || [];
- }
- },
-
- dequeue: function( elem, type ) {
- type = type || "fx";
-
- var queue = jQuery.queue( elem, type ),
- fn = queue.shift(),
- hooks = {};
-
- // If the fx queue is dequeued, always remove the progress sentinel
- if ( fn === "inprogress" ) {
- fn = queue.shift();
- }
-
- if ( fn ) {
- // Add a progress sentinel to prevent the fx queue from being
- // automatically dequeued
- if ( type === "fx" ) {
- queue.unshift( "inprogress" );
- }
-
- jQuery._data( elem, type + ".run", hooks );
- fn.call( elem, function() {
- jQuery.dequeue( elem, type );
- }, hooks );
- }
-
- if ( !queue.length ) {
- jQuery.removeData( elem, type + "queue " + type + ".run", true );
- handleQueueMarkDefer( elem, type, "queue" );
- }
- }
-});
-
-jQuery.fn.extend({
- queue: function( type, data ) {
- if ( typeof type !== "string" ) {
- data = type;
- type = "fx";
- }
-
- if ( data === undefined ) {
- return jQuery.queue( this[0], type );
- }
- return this.each(function() {
- var queue = jQuery.queue( this, type, data );
-
- if ( type === "fx" && queue[0] !== "inprogress" ) {
- jQuery.dequeue( this, type );
- }
- });
- },
- dequeue: function( type ) {
- return this.each(function() {
- jQuery.dequeue( this, type );
- });
- },
- // Based off of the plugin by Clint Helfers, with permission.
- // http://blindsignals.com/index.php/2009/07/jquery-delay/
- delay: function( time, type ) {
- time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
- type = type || "fx";
-
- return this.queue( type, function( next, hooks ) {
- var timeout = setTimeout( next, time );
- hooks.stop = function() {
- clearTimeout( timeout );
- };
- });
- },
- clearQueue: function( type ) {
- return this.queue( type || "fx", [] );
- },
- // Get a promise resolved when queues of a certain type
- // are emptied (fx is the type by default)
- promise: function( type, object ) {
- if ( typeof type !== "string" ) {
- object = type;
- type = undefined;
- }
- type = type || "fx";
- var defer = jQuery.Deferred(),
- elements = this,
- i = elements.length,
- count = 1,
- deferDataKey = type + "defer",
- queueDataKey = type + "queue",
- markDataKey = type + "mark",
- tmp;
- function resolve() {
- if ( !( --count ) ) {
- defer.resolveWith( elements, [ elements ] );
- }
- }
- while( i-- ) {
- if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) ||
- ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) ||
- jQuery.data( elements[ i ], markDataKey, undefined, true ) ) &&
- jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) {
- count++;
- tmp.add( resolve );
- }
- }
- resolve();
- return defer.promise();
- }
-});
-
-
-
-
-var rclass = /[\n\t\r]/g,
- rspace = /\s+/,
- rreturn = /\r/g,
- rtype = /^(?:button|input)$/i,
- rfocusable = /^(?:button|input|object|select|textarea)$/i,
- rclickable = /^a(?:rea)?$/i,
- rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,
- getSetAttribute = jQuery.support.getSetAttribute,
- nodeHook, boolHook, fixSpecified;
-
-jQuery.fn.extend({
- attr: function( name, value ) {
- return jQuery.access( this, name, value, true, jQuery.attr );
- },
-
- removeAttr: function( name ) {
- return this.each(function() {
- jQuery.removeAttr( this, name );
- });
- },
-
- prop: function( name, value ) {
- return jQuery.access( this, name, value, true, jQuery.prop );
- },
-
- removeProp: function( name ) {
- name = jQuery.propFix[ name ] || name;
- return this.each(function() {
- // try/catch handles cases where IE balks (such as removing a property on window)
- try {
- this[ name ] = undefined;
- delete this[ name ];
- } catch( e ) {}
- });
- },
-
- addClass: function( value ) {
- var classNames, i, l, elem,
- setClass, c, cl;
-
- if ( jQuery.isFunction( value ) ) {
- return this.each(function( j ) {
- jQuery( this ).addClass( value.call(this, j, this.className) );
- });
- }
-
- if ( value && typeof value === "string" ) {
- classNames = value.split( rspace );
-
- for ( i = 0, l = this.length; i < l; i++ ) {
- elem = this[ i ];
-
- if ( elem.nodeType === 1 ) {
- if ( !elem.className && classNames.length === 1 ) {
- elem.className = value;
-
- } else {
- setClass = " " + elem.className + " ";
-
- for ( c = 0, cl = classNames.length; c < cl; c++ ) {
- if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) {
- setClass += classNames[ c ] + " ";
- }
- }
- elem.className = jQuery.trim( setClass );
- }
- }
- }
- }
-
- return this;
- },
-
- removeClass: function( value ) {
- var classNames, i, l, elem, className, c, cl;
-
- if ( jQuery.isFunction( value ) ) {
- return this.each(function( j ) {
- jQuery( this ).removeClass( value.call(this, j, this.className) );
- });
- }
-
- if ( (value && typeof value === "string") || value === undefined ) {
- classNames = ( value || "" ).split( rspace );
-
- for ( i = 0, l = this.length; i < l; i++ ) {
- elem = this[ i ];
-
- if ( elem.nodeType === 1 && elem.className ) {
- if ( value ) {
- className = (" " + elem.className + " ").replace( rclass, " " );
- for ( c = 0, cl = classNames.length; c < cl; c++ ) {
- className = className.replace(" " + classNames[ c ] + " ", " ");
- }
- elem.className = jQuery.trim( className );
-
- } else {
- elem.className = "";
- }
- }
- }
- }
-
- return this;
- },
-
- toggleClass: function( value, stateVal ) {
- var type = typeof value,
- isBool = typeof stateVal === "boolean";
-
- if ( jQuery.isFunction( value ) ) {
- return this.each(function( i ) {
- jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );
- });
- }
-
- return this.each(function() {
- if ( type === "string" ) {
- // toggle individual class names
- var className,
- i = 0,
- self = jQuery( this ),
- state = stateVal,
- classNames = value.split( rspace );
-
- while ( (className = classNames[ i++ ]) ) {
- // check each className given, space seperated list
- state = isBool ? state : !self.hasClass( className );
- self[ state ? "addClass" : "removeClass" ]( className );
- }
-
- } else if ( type === "undefined" || type === "boolean" ) {
- if ( this.className ) {
- // store className if set
- jQuery._data( this, "__className__", this.className );
- }
-
- // toggle whole className
- this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
- }
- });
- },
-
- hasClass: function( selector ) {
- var className = " " + selector + " ",
- i = 0,
- l = this.length;
- for ( ; i < l; i++ ) {
- if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) {
- return true;
- }
- }
-
- return false;
- },
-
- val: function( value ) {
- var hooks, ret, isFunction,
- elem = this[0];
-
- if ( !arguments.length ) {
- if ( elem ) {
- hooks = jQuery.valHooks[ elem.nodeName.toLowerCase() ] || jQuery.valHooks[ elem.type ];
-
- if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
- return ret;
- }
-
- ret = elem.value;
-
- return typeof ret === "string" ?
- // handle most common string cases
- ret.replace(rreturn, "") :
- // handle cases where value is null/undef or number
- ret == null ? "" : ret;
- }
-
- return;
- }
-
- isFunction = jQuery.isFunction( value );
-
- return this.each(function( i ) {
- var self = jQuery(this), val;
-
- if ( this.nodeType !== 1 ) {
- return;
- }
-
- if ( isFunction ) {
- val = value.call( this, i, self.val() );
- } else {
- val = value;
- }
-
- // Treat null/undefined as ""; convert numbers to string
- if ( val == null ) {
- val = "";
- } else if ( typeof val === "number" ) {
- val += "";
- } else if ( jQuery.isArray( val ) ) {
- val = jQuery.map(val, function ( value ) {
- return value == null ? "" : value + "";
- });
- }
-
- hooks = jQuery.valHooks[ this.nodeName.toLowerCase() ] || jQuery.valHooks[ this.type ];
-
- // If set returns undefined, fall back to normal setting
- if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) {
- this.value = val;
- }
- });
- }
-});
-
-jQuery.extend({
- valHooks: {
- option: {
- get: function( elem ) {
- // attributes.value is undefined in Blackberry 4.7 but
- // uses .value. See #6932
- var val = elem.attributes.value;
- return !val || val.specified ? elem.value : elem.text;
- }
- },
- select: {
- get: function( elem ) {
- var value, i, max, option,
- index = elem.selectedIndex,
- values = [],
- options = elem.options,
- one = elem.type === "select-one";
-
- // Nothing was selected
- if ( index < 0 ) {
- return null;
- }
-
- // Loop through all the selected options
- i = one ? index : 0;
- max = one ? index + 1 : options.length;
- for ( ; i < max; i++ ) {
- option = options[ i ];
-
- // Don't return options that are disabled or in a disabled optgroup
- if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) &&
- (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) {
-
- // Get the specific value for the option
- value = jQuery( option ).val();
-
- // We don't need an array for one selects
- if ( one ) {
- return value;
- }
-
- // Multi-Selects return an array
- values.push( value );
- }
- }
-
- // Fixes Bug #2551 -- select.val() broken in IE after form.reset()
- if ( one && !values.length && options.length ) {
- return jQuery( options[ index ] ).val();
- }
-
- return values;
- },
-
- set: function( elem, value ) {
- var values = jQuery.makeArray( value );
-
- jQuery(elem).find("option").each(function() {
- this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
- });
-
- if ( !values.length ) {
- elem.selectedIndex = -1;
- }
- return values;
- }
- }
- },
-
- attrFn: {
- val: true,
- css: true,
- html: true,
- text: true,
- data: true,
- width: true,
- height: true,
- offset: true
- },
-
- attr: function( elem, name, value, pass ) {
- var ret, hooks, notxml,
- nType = elem.nodeType;
-
- // don't get/set attributes on text, comment and attribute nodes
- if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
- return;
- }
-
- if ( pass && name in jQuery.attrFn ) {
- return jQuery( elem )[ name ]( value );
- }
-
- // Fallback to prop when attributes are not supported
- if ( typeof elem.getAttribute === "undefined" ) {
- return jQuery.prop( elem, name, value );
- }
-
- notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
-
- // All attributes are lowercase
- // Grab necessary hook if one is defined
- if ( notxml ) {
- name = name.toLowerCase();
- hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook );
- }
-
- if ( value !== undefined ) {
-
- if ( value === null ) {
- jQuery.removeAttr( elem, name );
- return;
-
- } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) {
- return ret;
-
- } else {
- elem.setAttribute( name, "" + value );
- return value;
- }
-
- } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) {
- return ret;
-
- } else {
-
- ret = elem.getAttribute( name );
-
- // Non-existent attributes return null, we normalize to undefined
- return ret === null ?
- undefined :
- ret;
- }
- },
-
- removeAttr: function( elem, value ) {
- var propName, attrNames, name, l,
- i = 0;
-
- if ( value && elem.nodeType === 1 ) {
- attrNames = value.toLowerCase().split( rspace );
- l = attrNames.length;
-
- for ( ; i < l; i++ ) {
- name = attrNames[ i ];
-
- if ( name ) {
- propName = jQuery.propFix[ name ] || name;
-
- // See #9699 for explanation of this approach (setting first, then removal)
- jQuery.attr( elem, name, "" );
- elem.removeAttribute( getSetAttribute ? name : propName );
-
- // Set corresponding property to false for boolean attributes
- if ( rboolean.test( name ) && propName in elem ) {
- elem[ propName ] = false;
- }
- }
- }
- }
- },
-
- attrHooks: {
- type: {
- set: function( elem, value ) {
- // We can't allow the type property to be changed (since it causes problems in IE)
- if ( rtype.test( elem.nodeName ) && elem.parentNode ) {
- jQuery.error( "type property can't be changed" );
- } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) {
- // Setting the type on a radio button after the value resets the value in IE6-9
- // Reset value to it's default in case type is set after value
- // This is for element creation
- var val = elem.value;
- elem.setAttribute( "type", value );
- if ( val ) {
- elem.value = val;
- }
- return value;
- }
- }
- },
- // Use the value property for back compat
- // Use the nodeHook for button elements in IE6/7 (#1954)
- value: {
- get: function( elem, name ) {
- if ( nodeHook && jQuery.nodeName( elem, "button" ) ) {
- return nodeHook.get( elem, name );
- }
- return name in elem ?
- elem.value :
- null;
- },
- set: function( elem, value, name ) {
- if ( nodeHook && jQuery.nodeName( elem, "button" ) ) {
- return nodeHook.set( elem, value, name );
- }
- // Does not return so that setAttribute is also used
- elem.value = value;
- }
- }
- },
-
- propFix: {
- tabindex: "tabIndex",
- readonly: "readOnly",
- "for": "htmlFor",
- "class": "className",
- maxlength: "maxLength",
- cellspacing: "cellSpacing",
- cellpadding: "cellPadding",
- rowspan: "rowSpan",
- colspan: "colSpan",
- usemap: "useMap",
- frameborder: "frameBorder",
- contenteditable: "contentEditable"
- },
-
- prop: function( elem, name, value ) {
- var ret, hooks, notxml,
- nType = elem.nodeType;
-
- // don't get/set properties on text, comment and attribute nodes
- if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
- return;
- }
-
- notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
-
- if ( notxml ) {
- // Fix name and attach hooks
- name = jQuery.propFix[ name ] || name;
- hooks = jQuery.propHooks[ name ];
- }
-
- if ( value !== undefined ) {
- if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
- return ret;
-
- } else {
- return ( elem[ name ] = value );
- }
-
- } else {
- if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
- return ret;
-
- } else {
- return elem[ name ];
- }
- }
- },
-
- propHooks: {
- tabIndex: {
- get: function( elem ) {
- // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
- // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
- var attributeNode = elem.getAttributeNode("tabindex");
-
- return attributeNode && attributeNode.specified ?
- parseInt( attributeNode.value, 10 ) :
- rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
- 0 :
- undefined;
- }
- }
- }
-});
-
-// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional)
-jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex;
-
-// Hook for boolean attributes
-boolHook = {
- get: function( elem, name ) {
- // Align boolean attributes with corresponding properties
- // Fall back to attribute presence where some booleans are not supported
- var attrNode,
- property = jQuery.prop( elem, name );
- return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ?
- name.toLowerCase() :
- undefined;
- },
- set: function( elem, value, name ) {
- var propName;
- if ( value === false ) {
- // Remove boolean attributes when set to false
- jQuery.removeAttr( elem, name );
- } else {
- // value is true since we know at this point it's type boolean and not false
- // Set boolean attributes to the same name and set the DOM property
- propName = jQuery.propFix[ name ] || name;
- if ( propName in elem ) {
- // Only set the IDL specifically if it already exists on the element
- elem[ propName ] = true;
- }
-
- elem.setAttribute( name, name.toLowerCase() );
- }
- return name;
- }
-};
-
-// IE6/7 do not support getting/setting some attributes with get/setAttribute
-if ( !getSetAttribute ) {
-
- fixSpecified = {
- name: true,
- id: true
- };
-
- // Use this for any attribute in IE6/7
- // This fixes almost every IE6/7 issue
- nodeHook = jQuery.valHooks.button = {
- get: function( elem, name ) {
- var ret;
- ret = elem.getAttributeNode( name );
- return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ?
- ret.nodeValue :
- undefined;
- },
- set: function( elem, value, name ) {
- // Set the existing or create a new attribute node
- var ret = elem.getAttributeNode( name );
- if ( !ret ) {
- ret = document.createAttribute( name );
- elem.setAttributeNode( ret );
- }
- return ( ret.nodeValue = value + "" );
- }
- };
-
- // Apply the nodeHook to tabindex
- jQuery.attrHooks.tabindex.set = nodeHook.set;
-
- // Set width and height to auto instead of 0 on empty string( Bug #8150 )
- // This is for removals
- jQuery.each([ "width", "height" ], function( i, name ) {
- jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
- set: function( elem, value ) {
- if ( value === "" ) {
- elem.setAttribute( name, "auto" );
- return value;
- }
- }
- });
- });
-
- // Set contenteditable to false on removals(#10429)
- // Setting to empty string throws an error as an invalid value
- jQuery.attrHooks.contenteditable = {
- get: nodeHook.get,
- set: function( elem, value, name ) {
- if ( value === "" ) {
- value = "false";
- }
- nodeHook.set( elem, value, name );
- }
- };
-}
-
-
-// Some attributes require a special call on IE
-if ( !jQuery.support.hrefNormalized ) {
- jQuery.each([ "href", "src", "width", "height" ], function( i, name ) {
- jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
- get: function( elem ) {
- var ret = elem.getAttribute( name, 2 );
- return ret === null ? undefined : ret;
- }
- });
- });
-}
-
-if ( !jQuery.support.style ) {
- jQuery.attrHooks.style = {
- get: function( elem ) {
- // Return undefined in the case of empty string
- // Normalize to lowercase since IE uppercases css property names
- return elem.style.cssText.toLowerCase() || undefined;
- },
- set: function( elem, value ) {
- return ( elem.style.cssText = "" + value );
- }
- };
-}
-
-// Safari mis-reports the default selected property of an option
-// Accessing the parent's selectedIndex property fixes it
-if ( !jQuery.support.optSelected ) {
- jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, {
- get: function( elem ) {
- var parent = elem.parentNode;
-
- if ( parent ) {
- parent.selectedIndex;
-
- // Make sure that it also works with optgroups, see #5701
- if ( parent.parentNode ) {
- parent.parentNode.selectedIndex;
- }
- }
- return null;
- }
- });
-}
-
-// IE6/7 call enctype encoding
-if ( !jQuery.support.enctype ) {
- jQuery.propFix.enctype = "encoding";
-}
-
-// Radios and checkboxes getter/setter
-if ( !jQuery.support.checkOn ) {
- jQuery.each([ "radio", "checkbox" ], function() {
- jQuery.valHooks[ this ] = {
- get: function( elem ) {
- // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified
- return elem.getAttribute("value") === null ? "on" : elem.value;
- }
- };
- });
-}
-jQuery.each([ "radio", "checkbox" ], function() {
- jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], {
- set: function( elem, value ) {
- if ( jQuery.isArray( value ) ) {
- return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );
- }
- }
- });
-});
-
-
-
-
-var rformElems = /^(?:textarea|input|select)$/i,
- rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/,
- rhoverHack = /\bhover(\.\S+)?\b/,
- rkeyEvent = /^key/,
- rmouseEvent = /^(?:mouse|contextmenu)|click/,
- rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
- rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,
- quickParse = function( selector ) {
- var quick = rquickIs.exec( selector );
- if ( quick ) {
- // 0 1 2 3
- // [ _, tag, id, class ]
- quick[1] = ( quick[1] || "" ).toLowerCase();
- quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" );
- }
- return quick;
- },
- quickIs = function( elem, m ) {
- var attrs = elem.attributes || {};
- return (
- (!m[1] || elem.nodeName.toLowerCase() === m[1]) &&
- (!m[2] || (attrs.id || {}).value === m[2]) &&
- (!m[3] || m[3].test( (attrs[ "class" ] || {}).value ))
- );
- },
- hoverHack = function( events ) {
- return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" );
- };
-
-/*
- * Helper functions for managing events -- not part of the public interface.
- * Props to Dean Edwards' addEvent library for many of the ideas.
- */
-jQuery.event = {
-
- add: function( elem, types, handler, data, selector ) {
-
- var elemData, eventHandle, events,
- t, tns, type, namespaces, handleObj,
- handleObjIn, quick, handlers, special;
-
- // Don't attach events to noData or text/comment nodes (allow plain objects tho)
- if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) {
- return;
- }
-
- // Caller can pass in an object of custom data in lieu of the handler
- if ( handler.handler ) {
- handleObjIn = handler;
- handler = handleObjIn.handler;
- }
-
- // Make sure that the handler has a unique ID, used to find/remove it later
- if ( !handler.guid ) {
- handler.guid = jQuery.guid++;
- }
-
- // Init the element's event structure and main handler, if this is the first
- events = elemData.events;
- if ( !events ) {
- elemData.events = events = {};
- }
- eventHandle = elemData.handle;
- if ( !eventHandle ) {
- elemData.handle = eventHandle = function( e ) {
- // Discard the second event of a jQuery.event.trigger() and
- // when an event is called after a page has unloaded
- return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ?
- jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
- undefined;
- };
- // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
- eventHandle.elem = elem;
- }
-
- // Handle multiple events separated by a space
- // jQuery(...).bind("mouseover mouseout", fn);
- types = jQuery.trim( hoverHack(types) ).split( " " );
- for ( t = 0; t < types.length; t++ ) {
-
- tns = rtypenamespace.exec( types[t] ) || [];
- type = tns[1];
- namespaces = ( tns[2] || "" ).split( "." ).sort();
-
- // If event changes its type, use the special event handlers for the changed type
- special = jQuery.event.special[ type ] || {};
-
- // If selector defined, determine special event api type, otherwise given type
- type = ( selector ? special.delegateType : special.bindType ) || type;
-
- // Update special based on newly reset type
- special = jQuery.event.special[ type ] || {};
-
- // handleObj is passed to all event handlers
- handleObj = jQuery.extend({
- type: type,
- origType: tns[1],
- data: data,
- handler: handler,
- guid: handler.guid,
- selector: selector,
- quick: quickParse( selector ),
- namespace: namespaces.join(".")
- }, handleObjIn );
-
- // Init the event handler queue if we're the first
- handlers = events[ type ];
- if ( !handlers ) {
- handlers = events[ type ] = [];
- handlers.delegateCount = 0;
-
- // Only use addEventListener/attachEvent if the special events handler returns false
- if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
- // Bind the global event handler to the element
- if ( elem.addEventListener ) {
- elem.addEventListener( type, eventHandle, false );
-
- } else if ( elem.attachEvent ) {
- elem.attachEvent( "on" + type, eventHandle );
- }
- }
- }
-
- if ( special.add ) {
- special.add.call( elem, handleObj );
-
- if ( !handleObj.handler.guid ) {
- handleObj.handler.guid = handler.guid;
- }
- }
-
- // Add to the element's handler list, delegates in front
- if ( selector ) {
- handlers.splice( handlers.delegateCount++, 0, handleObj );
- } else {
- handlers.push( handleObj );
- }
-
- // Keep track of which events have ever been used, for event optimization
- jQuery.event.global[ type ] = true;
- }
-
- // Nullify elem to prevent memory leaks in IE
- elem = null;
- },
-
- global: {},
-
- // Detach an event or set of events from an element
- remove: function( elem, types, handler, selector, mappedTypes ) {
-
- var elemData = jQuery.hasData( elem ) && jQuery._data( elem ),
- t, tns, type, origType, namespaces, origCount,
- j, events, special, handle, eventType, handleObj;
-
- if ( !elemData || !(events = elemData.events) ) {
- return;
- }
-
- // Once for each type.namespace in types; type may be omitted
- types = jQuery.trim( hoverHack( types || "" ) ).split(" ");
- for ( t = 0; t < types.length; t++ ) {
- tns = rtypenamespace.exec( types[t] ) || [];
- type = origType = tns[1];
- namespaces = tns[2];
-
- // Unbind all events (on this namespace, if provided) for the element
- if ( !type ) {
- for ( type in events ) {
- jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
- }
- continue;
- }
-
- special = jQuery.event.special[ type ] || {};
- type = ( selector? special.delegateType : special.bindType ) || type;
- eventType = events[ type ] || [];
- origCount = eventType.length;
- namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null;
-
- // Remove matching events
- for ( j = 0; j < eventType.length; j++ ) {
- handleObj = eventType[ j ];
-
- if ( ( mappedTypes || origType === handleObj.origType ) &&
- ( !handler || handler.guid === handleObj.guid ) &&
- ( !namespaces || namespaces.test( handleObj.namespace ) ) &&
- ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) {
- eventType.splice( j--, 1 );
-
- if ( handleObj.selector ) {
- eventType.delegateCount--;
- }
- if ( special.remove ) {
- special.remove.call( elem, handleObj );
- }
- }
- }
-
- // Remove generic event handler if we removed something and no more handlers exist
- // (avoids potential for endless recursion during removal of special event handlers)
- if ( eventType.length === 0 && origCount !== eventType.length ) {
- if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {
- jQuery.removeEvent( elem, type, elemData.handle );
- }
-
- delete events[ type ];
- }
- }
-
- // Remove the expando if it's no longer used
- if ( jQuery.isEmptyObject( events ) ) {
- handle = elemData.handle;
- if ( handle ) {
- handle.elem = null;
- }
-
- // removeData also checks for emptiness and clears the expando if empty
- // so use it instead of delete
- jQuery.removeData( elem, [ "events", "handle" ], true );
- }
- },
-
- // Events that are safe to short-circuit if no handlers are attached.
- // Native DOM events should not be added, they may have inline handlers.
- customEvent: {
- "getData": true,
- "setData": true,
- "changeData": true
- },
-
- trigger: function( event, data, elem, onlyHandlers ) {
- // Don't do events on text and comment nodes
- if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) {
- return;
- }
-
- // Event object or event type
- var type = event.type || event,
- namespaces = [],
- cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType;
-
- // focus/blur morphs to focusin/out; ensure we're not firing them right now
- if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
- return;
- }
-
- if ( type.indexOf( "!" ) >= 0 ) {
- // Exclusive events trigger only for the exact event (no namespaces)
- type = type.slice(0, -1);
- exclusive = true;
- }
-
- if ( type.indexOf( "." ) >= 0 ) {
- // Namespaced trigger; create a regexp to match event type in handle()
- namespaces = type.split(".");
- type = namespaces.shift();
- namespaces.sort();
- }
-
- if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) {
- // No jQuery handlers for this event type, and it can't have inline handlers
- return;
- }
-
- // Caller can pass in an Event, Object, or just an event type string
- event = typeof event === "object" ?
- // jQuery.Event object
- event[ jQuery.expando ] ? event :
- // Object literal
- new jQuery.Event( type, event ) :
- // Just the event type (string)
- new jQuery.Event( type );
-
- event.type = type;
- event.isTrigger = true;
- event.exclusive = exclusive;
- event.namespace = namespaces.join( "." );
- event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null;
- ontype = type.indexOf( ":" ) < 0 ? "on" + type : "";
-
- // Handle a global trigger
- if ( !elem ) {
-
- // TODO: Stop taunting the data cache; remove global events and always attach to document
- cache = jQuery.cache;
- for ( i in cache ) {
- if ( cache[ i ].events && cache[ i ].events[ type ] ) {
- jQuery.event.trigger( event, data, cache[ i ].handle.elem, true );
- }
- }
- return;
- }
-
- // Clean up the event in case it is being reused
- event.result = undefined;
- if ( !event.target ) {
- event.target = elem;
- }
-
- // Clone any incoming data and prepend the event, creating the handler arg list
- data = data != null ? jQuery.makeArray( data ) : [];
- data.unshift( event );
-
- // Allow special events to draw outside the lines
- special = jQuery.event.special[ type ] || {};
- if ( special.trigger && special.trigger.apply( elem, data ) === false ) {
- return;
- }
-
- // Determine event propagation path in advance, per W3C events spec (#9951)
- // Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
- eventPath = [[ elem, special.bindType || type ]];
- if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
-
- bubbleType = special.delegateType || type;
- cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode;
- old = null;
- for ( ; cur; cur = cur.parentNode ) {
- eventPath.push([ cur, bubbleType ]);
- old = cur;
- }
-
- // Only add window if we got to document (e.g., not plain obj or detached DOM)
- if ( old && old === elem.ownerDocument ) {
- eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]);
- }
- }
-
- // Fire handlers on the event path
- for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) {
-
- cur = eventPath[i][0];
- event.type = eventPath[i][1];
-
- handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" );
- if ( handle ) {
- handle.apply( cur, data );
- }
- // Note that this is a bare JS function and not a jQuery handler
- handle = ontype && cur[ ontype ];
- if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) {
- event.preventDefault();
- }
- }
- event.type = type;
-
- // If nobody prevented the default action, do it now
- if ( !onlyHandlers && !event.isDefaultPrevented() ) {
-
- if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) &&
- !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) {
-
- // Call a native DOM method on the target with the same name name as the event.
- // Can't use an .isFunction() check here because IE6/7 fails that test.
- // Don't do default actions on window, that's where global variables be (#6170)
- // IE<9 dies on focus/blur to hidden element (#1486)
- if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) {
-
- // Don't re-trigger an onFOO event when we call its FOO() method
- old = elem[ ontype ];
-
- if ( old ) {
- elem[ ontype ] = null;
- }
-
- // Prevent re-triggering of the same event, since we already bubbled it above
- jQuery.event.triggered = type;
- elem[ type ]();
- jQuery.event.triggered = undefined;
-
- if ( old ) {
- elem[ ontype ] = old;
- }
- }
- }
- }
-
- return event.result;
- },
-
- dispatch: function( event ) {
-
- // Make a writable jQuery.Event from the native event object
- event = jQuery.event.fix( event || window.event );
-
- var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []),
- delegateCount = handlers.delegateCount,
- args = [].slice.call( arguments, 0 ),
- run_all = !event.exclusive && !event.namespace,
- handlerQueue = [],
- i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related;
-
- // Use the fix-ed jQuery.Event rather than the (read-only) native event
- args[0] = event;
- event.delegateTarget = this;
-
- // Determine handlers that should run if there are delegated events
- // Avoid disabled elements in IE (#6911) and non-left-click bubbling in Firefox (#3861)
- if ( delegateCount && !event.target.disabled && !(event.button && event.type === "click") ) {
-
- // Pregenerate a single jQuery object for reuse with .is()
- jqcur = jQuery(this);
- jqcur.context = this.ownerDocument || this;
-
- for ( cur = event.target; cur != this; cur = cur.parentNode || this ) {
- selMatch = {};
- matches = [];
- jqcur[0] = cur;
- for ( i = 0; i < delegateCount; i++ ) {
- handleObj = handlers[ i ];
- sel = handleObj.selector;
-
- if ( selMatch[ sel ] === undefined ) {
- selMatch[ sel ] = (
- handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel )
- );
- }
- if ( selMatch[ sel ] ) {
- matches.push( handleObj );
- }
- }
- if ( matches.length ) {
- handlerQueue.push({ elem: cur, matches: matches });
- }
- }
- }
-
- // Add the remaining (directly-bound) handlers
- if ( handlers.length > delegateCount ) {
- handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) });
- }
-
- // Run delegates first; they may want to stop propagation beneath us
- for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) {
- matched = handlerQueue[ i ];
- event.currentTarget = matched.elem;
-
- for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) {
- handleObj = matched.matches[ j ];
-
- // Triggered event must either 1) be non-exclusive and have no namespace, or
- // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
- if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) {
-
- event.data = handleObj.data;
- event.handleObj = handleObj;
-
- ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
- .apply( matched.elem, args );
-
- if ( ret !== undefined ) {
- event.result = ret;
- if ( ret === false ) {
- event.preventDefault();
- event.stopPropagation();
- }
- }
- }
- }
- }
-
- return event.result;
- },
-
- // Includes some event props shared by KeyEvent and MouseEvent
- // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 ***
- props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
-
- fixHooks: {},
-
- keyHooks: {
- props: "char charCode key keyCode".split(" "),
- filter: function( event, original ) {
-
- // Add which for key events
- if ( event.which == null ) {
- event.which = original.charCode != null ? original.charCode : original.keyCode;
- }
-
- return event;
- }
- },
-
- mouseHooks: {
- props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
- filter: function( event, original ) {
- var eventDoc, doc, body,
- button = original.button,
- fromElement = original.fromElement;
-
- // Calculate pageX/Y if missing and clientX/Y available
- if ( event.pageX == null && original.clientX != null ) {
- eventDoc = event.target.ownerDocument || document;
- doc = eventDoc.documentElement;
- body = eventDoc.body;
-
- event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
- event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 );
- }
-
- // Add relatedTarget, if necessary
- if ( !event.relatedTarget && fromElement ) {
- event.relatedTarget = fromElement === event.target ? original.toElement : fromElement;
- }
-
- // Add which for click: 1 === left; 2 === middle; 3 === right
- // Note: button is not normalized, so don't use it
- if ( !event.which && button !== undefined ) {
- event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
- }
-
- return event;
- }
- },
-
- fix: function( event ) {
- if ( event[ jQuery.expando ] ) {
- return event;
- }
-
- // Create a writable copy of the event object and normalize some properties
- var i, prop,
- originalEvent = event,
- fixHook = jQuery.event.fixHooks[ event.type ] || {},
- copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
-
- event = jQuery.Event( originalEvent );
-
- for ( i = copy.length; i; ) {
- prop = copy[ --i ];
- event[ prop ] = originalEvent[ prop ];
- }
-
- // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2)
- if ( !event.target ) {
- event.target = originalEvent.srcElement || document;
- }
-
- // Target should not be a text node (#504, Safari)
- if ( event.target.nodeType === 3 ) {
- event.target = event.target.parentNode;
- }
-
- // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8)
- if ( event.metaKey === undefined ) {
- event.metaKey = event.ctrlKey;
- }
-
- return fixHook.filter? fixHook.filter( event, originalEvent ) : event;
- },
-
- special: {
- ready: {
- // Make sure the ready event is setup
- setup: jQuery.bindReady
- },
-
- load: {
- // Prevent triggered image.load events from bubbling to window.load
- noBubble: true
- },
-
- focus: {
- delegateType: "focusin"
- },
- blur: {
- delegateType: "focusout"
- },
-
- beforeunload: {
- setup: function( data, namespaces, eventHandle ) {
- // We only want to do this special case on windows
- if ( jQuery.isWindow( this ) ) {
- this.onbeforeunload = eventHandle;
- }
- },
-
- teardown: function( namespaces, eventHandle ) {
- if ( this.onbeforeunload === eventHandle ) {
- this.onbeforeunload = null;
- }
- }
- }
- },
-
- simulate: function( type, elem, event, bubble ) {
- // Piggyback on a donor event to simulate a different one.
- // Fake originalEvent to avoid donor's stopPropagation, but if the
- // simulated event prevents default then we do the same on the donor.
- var e = jQuery.extend(
- new jQuery.Event(),
- event,
- { type: type,
- isSimulated: true,
- originalEvent: {}
- }
- );
- if ( bubble ) {
- jQuery.event.trigger( e, null, elem );
- } else {
- jQuery.event.dispatch.call( elem, e );
- }
- if ( e.isDefaultPrevented() ) {
- event.preventDefault();
- }
- }
-};
-
-// Some plugins are using, but it's undocumented/deprecated and will be removed.
-// The 1.7 special event interface should provide all the hooks needed now.
-jQuery.event.handle = jQuery.event.dispatch;
-
-jQuery.removeEvent = document.removeEventListener ?
- function( elem, type, handle ) {
- if ( elem.removeEventListener ) {
- elem.removeEventListener( type, handle, false );
- }
- } :
- function( elem, type, handle ) {
- if ( elem.detachEvent ) {
- elem.detachEvent( "on" + type, handle );
- }
- };
-
-jQuery.Event = function( src, props ) {
- // Allow instantiation without the 'new' keyword
- if ( !(this instanceof jQuery.Event) ) {
- return new jQuery.Event( src, props );
- }
-
- // Event object
- if ( src && src.type ) {
- this.originalEvent = src;
- this.type = src.type;
-
- // Events bubbling up the document may have been marked as prevented
- // by a handler lower down the tree; reflect the correct value.
- this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false ||
- src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;
-
- // Event type
- } else {
- this.type = src;
- }
-
- // Put explicitly provided properties onto the event object
- if ( props ) {
- jQuery.extend( this, props );
- }
-
- // Create a timestamp if incoming event doesn't have one
- this.timeStamp = src && src.timeStamp || jQuery.now();
-
- // Mark it as fixed
- this[ jQuery.expando ] = true;
-};
-
-function returnFalse() {
- return false;
-}
-function returnTrue() {
- return true;
-}
-
-// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
-// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
-jQuery.Event.prototype = {
- preventDefault: function() {
- this.isDefaultPrevented = returnTrue;
-
- var e = this.originalEvent;
- if ( !e ) {
- return;
- }
-
- // if preventDefault exists run it on the original event
- if ( e.preventDefault ) {
- e.preventDefault();
-
- // otherwise set the returnValue property of the original event to false (IE)
- } else {
- e.returnValue = false;
- }
- },
- stopPropagation: function() {
- this.isPropagationStopped = returnTrue;
-
- var e = this.originalEvent;
- if ( !e ) {
- return;
- }
- // if stopPropagation exists run it on the original event
- if ( e.stopPropagation ) {
- e.stopPropagation();
- }
- // otherwise set the cancelBubble property of the original event to true (IE)
- e.cancelBubble = true;
- },
- stopImmediatePropagation: function() {
- this.isImmediatePropagationStopped = returnTrue;
- this.stopPropagation();
- },
- isDefaultPrevented: returnFalse,
- isPropagationStopped: returnFalse,
- isImmediatePropagationStopped: returnFalse
-};
-
-// Create mouseenter/leave events using mouseover/out and event-time checks
-jQuery.each({
- mouseenter: "mouseover",
- mouseleave: "mouseout"
-}, function( orig, fix ) {
- jQuery.event.special[ orig ] = {
- delegateType: fix,
- bindType: fix,
-
- handle: function( event ) {
- var target = this,
- related = event.relatedTarget,
- handleObj = event.handleObj,
- selector = handleObj.selector,
- ret;
-
- // For mousenter/leave call the handler if related is outside the target.
- // NB: No relatedTarget if the mouse left/entered the browser window
- if ( !related || (related !== target && !jQuery.contains( target, related )) ) {
- event.type = handleObj.origType;
- ret = handleObj.handler.apply( this, arguments );
- event.type = fix;
- }
- return ret;
- }
- };
-});
-
-// IE submit delegation
-if ( !jQuery.support.submitBubbles ) {
-
- jQuery.event.special.submit = {
- setup: function() {
- // Only need this for delegated form submit events
- if ( jQuery.nodeName( this, "form" ) ) {
- return false;
- }
-
- // Lazy-add a submit handler when a descendant form may potentially be submitted
- jQuery.event.add( this, "click._submit keypress._submit", function( e ) {
- // Node name check avoids a VML-related crash in IE (#9807)
- var elem = e.target,
- form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined;
- if ( form && !form._submit_attached ) {
- jQuery.event.add( form, "submit._submit", function( event ) {
- // If form was submitted by the user, bubble the event up the tree
- if ( this.parentNode && !event.isTrigger ) {
- jQuery.event.simulate( "submit", this.parentNode, event, true );
- }
- });
- form._submit_attached = true;
- }
- });
- // return undefined since we don't need an event listener
- },
-
- teardown: function() {
- // Only need this for delegated form submit events
- if ( jQuery.nodeName( this, "form" ) ) {
- return false;
- }
-
- // Remove delegated handlers; cleanData eventually reaps submit handlers attached above
- jQuery.event.remove( this, "._submit" );
- }
- };
-}
-
-// IE change delegation and checkbox/radio fix
-if ( !jQuery.support.changeBubbles ) {
-
- jQuery.event.special.change = {
-
- setup: function() {
-
- if ( rformElems.test( this.nodeName ) ) {
- // IE doesn't fire change on a check/radio until blur; trigger it on click
- // after a propertychange. Eat the blur-change in special.change.handle.
- // This still fires onchange a second time for check/radio after blur.
- if ( this.type === "checkbox" || this.type === "radio" ) {
- jQuery.event.add( this, "propertychange._change", function( event ) {
- if ( event.originalEvent.propertyName === "checked" ) {
- this._just_changed = true;
- }
- });
- jQuery.event.add( this, "click._change", function( event ) {
- if ( this._just_changed && !event.isTrigger ) {
- this._just_changed = false;
- jQuery.event.simulate( "change", this, event, true );
- }
- });
- }
- return false;
- }
- // Delegated event; lazy-add a change handler on descendant inputs
- jQuery.event.add( this, "beforeactivate._change", function( e ) {
- var elem = e.target;
-
- if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) {
- jQuery.event.add( elem, "change._change", function( event ) {
- if ( this.parentNode && !event.isSimulated && !event.isTrigger ) {
- jQuery.event.simulate( "change", this.parentNode, event, true );
- }
- });
- elem._change_attached = true;
- }
- });
- },
-
- handle: function( event ) {
- var elem = event.target;
-
- // Swallow native change events from checkbox/radio, we already triggered them above
- if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) {
- return event.handleObj.handler.apply( this, arguments );
- }
- },
-
- teardown: function() {
- jQuery.event.remove( this, "._change" );
-
- return rformElems.test( this.nodeName );
- }
- };
-}
-
-// Create "bubbling" focus and blur events
-if ( !jQuery.support.focusinBubbles ) {
- jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
-
- // Attach a single capturing handler while someone wants focusin/focusout
- var attaches = 0,
- handler = function( event ) {
- jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
- };
-
- jQuery.event.special[ fix ] = {
- setup: function() {
- if ( attaches++ === 0 ) {
- document.addEventListener( orig, handler, true );
- }
- },
- teardown: function() {
- if ( --attaches === 0 ) {
- document.removeEventListener( orig, handler, true );
- }
- }
- };
- });
-}
-
-jQuery.fn.extend({
-
- on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
- var origFn, type;
-
- // Types can be a map of types/handlers
- if ( typeof types === "object" ) {
- // ( types-Object, selector, data )
- if ( typeof selector !== "string" ) {
- // ( types-Object, data )
- data = selector;
- selector = undefined;
- }
- for ( type in types ) {
- this.on( type, selector, data, types[ type ], one );
- }
- return this;
- }
-
- if ( data == null && fn == null ) {
- // ( types, fn )
- fn = selector;
- data = selector = undefined;
- } else if ( fn == null ) {
- if ( typeof selector === "string" ) {
- // ( types, selector, fn )
- fn = data;
- data = undefined;
- } else {
- // ( types, data, fn )
- fn = data;
- data = selector;
- selector = undefined;
- }
- }
- if ( fn === false ) {
- fn = returnFalse;
- } else if ( !fn ) {
- return this;
- }
-
- if ( one === 1 ) {
- origFn = fn;
- fn = function( event ) {
- // Can use an empty set, since event contains the info
- jQuery().off( event );
- return origFn.apply( this, arguments );
- };
- // Use same guid so caller can remove using origFn
- fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
- }
- return this.each( function() {
- jQuery.event.add( this, types, fn, data, selector );
- });
- },
- one: function( types, selector, data, fn ) {
- return this.on.call( this, types, selector, data, fn, 1 );
- },
- off: function( types, selector, fn ) {
- if ( types && types.preventDefault && types.handleObj ) {
- // ( event ) dispatched jQuery.Event
- var handleObj = types.handleObj;
- jQuery( types.delegateTarget ).off(
- handleObj.namespace? handleObj.type + "." + handleObj.namespace : handleObj.type,
- handleObj.selector,
- handleObj.handler
- );
- return this;
- }
- if ( typeof types === "object" ) {
- // ( types-object [, selector] )
- for ( var type in types ) {
- this.off( type, selector, types[ type ] );
- }
- return this;
- }
- if ( selector === false || typeof selector === "function" ) {
- // ( types [, fn] )
- fn = selector;
- selector = undefined;
- }
- if ( fn === false ) {
- fn = returnFalse;
- }
- return this.each(function() {
- jQuery.event.remove( this, types, fn, selector );
- });
- },
-
- bind: function( types, data, fn ) {
- return this.on( types, null, data, fn );
- },
- unbind: function( types, fn ) {
- return this.off( types, null, fn );
- },
-
- live: function( types, data, fn ) {
- jQuery( this.context ).on( types, this.selector, data, fn );
- return this;
- },
- die: function( types, fn ) {
- jQuery( this.context ).off( types, this.selector || "**", fn );
- return this;
- },
-
- delegate: function( selector, types, data, fn ) {
- return this.on( types, selector, data, fn );
- },
- undelegate: function( selector, types, fn ) {
- // ( namespace ) or ( selector, types [, fn] )
- return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn );
- },
-
- trigger: function( type, data ) {
- return this.each(function() {
- jQuery.event.trigger( type, data, this );
- });
- },
- triggerHandler: function( type, data ) {
- if ( this[0] ) {
- return jQuery.event.trigger( type, data, this[0], true );
- }
- },
-
- toggle: function( fn ) {
- // Save reference to arguments for access in closure
- var args = arguments,
- guid = fn.guid || jQuery.guid++,
- i = 0,
- toggler = function( event ) {
- // Figure out which function to execute
- var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i;
- jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 );
-
- // Make sure that clicks stop
- event.preventDefault();
-
- // and execute the function
- return args[ lastToggle ].apply( this, arguments ) || false;
- };
-
- // link all the functions, so any of them can unbind this click handler
- toggler.guid = guid;
- while ( i < args.length ) {
- args[ i++ ].guid = guid;
- }
-
- return this.click( toggler );
- },
-
- hover: function( fnOver, fnOut ) {
- return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
- }
-});
-
-jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
- "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
- "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {
-
- // Handle event binding
- jQuery.fn[ name ] = function( data, fn ) {
- if ( fn == null ) {
- fn = data;
- data = null;
- }
-
- return arguments.length > 0 ?
- this.on( name, null, data, fn ) :
- this.trigger( name );
- };
-
- if ( jQuery.attrFn ) {
- jQuery.attrFn[ name ] = true;
- }
-
- if ( rkeyEvent.test( name ) ) {
- jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks;
- }
-
- if ( rmouseEvent.test( name ) ) {
- jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks;
- }
-});
-
-
-
-/*!
- * Sizzle CSS Selector Engine
- * Copyright 2011, The Dojo Foundation
- * Released under the MIT, BSD, and GPL Licenses.
- * More information: http://sizzlejs.com/
- */
-(function(){
-
-var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
- expando = "sizcache" + (Math.random() + '').replace('.', ''),
- done = 0,
- toString = Object.prototype.toString,
- hasDuplicate = false,
- baseHasDuplicate = true,
- rBackslash = /\\/g,
- rReturn = /\r\n/g,
- rNonWord = /\W/;
-
-// Here we check if the JavaScript engine is using some sort of
-// optimization where it does not always call our comparision
-// function. If that is the case, discard the hasDuplicate value.
-// Thus far that includes Google Chrome.
-[0, 0].sort(function() {
- baseHasDuplicate = false;
- return 0;
-});
-
-var Sizzle = function( selector, context, results, seed ) {
- results = results || [];
- context = context || document;
-
- var origContext = context;
-
- if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
- return [];
- }
-
- if ( !selector || typeof selector !== "string" ) {
- return results;
- }
-
- var m, set, checkSet, extra, ret, cur, pop, i,
- prune = true,
- contextXML = Sizzle.isXML( context ),
- parts = [],
- soFar = selector;
-
- // Reset the position of the chunker regexp (start from head)
- do {
- chunker.exec( "" );
- m = chunker.exec( soFar );
-
- if ( m ) {
- soFar = m[3];
-
- parts.push( m[1] );
-
- if ( m[2] ) {
- extra = m[3];
- break;
- }
- }
- } while ( m );
-
- if ( parts.length > 1 && origPOS.exec( selector ) ) {
-
- if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
- set = posProcess( parts[0] + parts[1], context, seed );
-
- } else {
- set = Expr.relative[ parts[0] ] ?
- [ context ] :
- Sizzle( parts.shift(), context );
-
- while ( parts.length ) {
- selector = parts.shift();
-
- if ( Expr.relative[ selector ] ) {
- selector += parts.shift();
- }
-
- set = posProcess( selector, set, seed );
- }
- }
-
- } else {
- // Take a shortcut and set the context if the root selector is an ID
- // (but not if it'll be faster if the inner selector is an ID)
- if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
- Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {
-
- ret = Sizzle.find( parts.shift(), context, contextXML );
- context = ret.expr ?
- Sizzle.filter( ret.expr, ret.set )[0] :
- ret.set[0];
- }
-
- if ( context ) {
- ret = seed ?
- { expr: parts.pop(), set: makeArray(seed) } :
- Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
-
- set = ret.expr ?
- Sizzle.filter( ret.expr, ret.set ) :
- ret.set;
-
- if ( parts.length > 0 ) {
- checkSet = makeArray( set );
-
- } else {
- prune = false;
- }
-
- while ( parts.length ) {
- cur = parts.pop();
- pop = cur;
-
- if ( !Expr.relative[ cur ] ) {
- cur = "";
- } else {
- pop = parts.pop();
- }
-
- if ( pop == null ) {
- pop = context;
- }
-
- Expr.relative[ cur ]( checkSet, pop, contextXML );
- }
-
- } else {
- checkSet = parts = [];
- }
- }
-
- if ( !checkSet ) {
- checkSet = set;
- }
-
- if ( !checkSet ) {
- Sizzle.error( cur || selector );
- }
-
- if ( toString.call(checkSet) === "[object Array]" ) {
- if ( !prune ) {
- results.push.apply( results, checkSet );
-
- } else if ( context && context.nodeType === 1 ) {
- for ( i = 0; checkSet[i] != null; i++ ) {
- if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {
- results.push( set[i] );
- }
- }
-
- } else {
- for ( i = 0; checkSet[i] != null; i++ ) {
- if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
- results.push( set[i] );
- }
- }
- }
-
- } else {
- makeArray( checkSet, results );
- }
-
- if ( extra ) {
- Sizzle( extra, origContext, results, seed );
- Sizzle.uniqueSort( results );
- }
-
- return results;
-};
-
-Sizzle.uniqueSort = function( results ) {
- if ( sortOrder ) {
- hasDuplicate = baseHasDuplicate;
- results.sort( sortOrder );
-
- if ( hasDuplicate ) {
- for ( var i = 1; i < results.length; i++ ) {
- if ( results[i] === results[ i - 1 ] ) {
- results.splice( i--, 1 );
- }
- }
- }
- }
-
- return results;
-};
-
-Sizzle.matches = function( expr, set ) {
- return Sizzle( expr, null, null, set );
-};
-
-Sizzle.matchesSelector = function( node, expr ) {
- return Sizzle( expr, null, null, [node] ).length > 0;
-};
-
-Sizzle.find = function( expr, context, isXML ) {
- var set, i, len, match, type, left;
-
- if ( !expr ) {
- return [];
- }
-
- for ( i = 0, len = Expr.order.length; i < len; i++ ) {
- type = Expr.order[i];
-
- if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
- left = match[1];
- match.splice( 1, 1 );
-
- if ( left.substr( left.length - 1 ) !== "\\" ) {
- match[1] = (match[1] || "").replace( rBackslash, "" );
- set = Expr.find[ type ]( match, context, isXML );
-
- if ( set != null ) {
- expr = expr.replace( Expr.match[ type ], "" );
- break;
- }
- }
- }
- }
-
- if ( !set ) {
- set = typeof context.getElementsByTagName !== "undefined" ?
- context.getElementsByTagName( "*" ) :
- [];
- }
-
- return { set: set, expr: expr };
-};
-
-Sizzle.filter = function( expr, set, inplace, not ) {
- var match, anyFound,
- type, found, item, filter, left,
- i, pass,
- old = expr,
- result = [],
- curLoop = set,
- isXMLFilter = set && set[0] && Sizzle.isXML( set[0] );
-
- while ( expr && set.length ) {
- for ( type in Expr.filter ) {
- if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
- filter = Expr.filter[ type ];
- left = match[1];
-
- anyFound = false;
-
- match.splice(1,1);
-
- if ( left.substr( left.length - 1 ) === "\\" ) {
- continue;
- }
-
- if ( curLoop === result ) {
- result = [];
- }
-
- if ( Expr.preFilter[ type ] ) {
- match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
-
- if ( !match ) {
- anyFound = found = true;
-
- } else if ( match === true ) {
- continue;
- }
- }
-
- if ( match ) {
- for ( i = 0; (item = curLoop[i]) != null; i++ ) {
- if ( item ) {
- found = filter( item, match, i, curLoop );
- pass = not ^ found;
-
- if ( inplace && found != null ) {
- if ( pass ) {
- anyFound = true;
-
- } else {
- curLoop[i] = false;
- }
-
- } else if ( pass ) {
- result.push( item );
- anyFound = true;
- }
- }
- }
- }
-
- if ( found !== undefined ) {
- if ( !inplace ) {
- curLoop = result;
- }
-
- expr = expr.replace( Expr.match[ type ], "" );
-
- if ( !anyFound ) {
- return [];
- }
-
- break;
- }
- }
- }
-
- // Improper expression
- if ( expr === old ) {
- if ( anyFound == null ) {
- Sizzle.error( expr );
-
- } else {
- break;
- }
- }
-
- old = expr;
- }
-
- return curLoop;
-};
-
-Sizzle.error = function( msg ) {
- throw new Error( "Syntax error, unrecognized expression: " + msg );
-};
-
-/**
- * Utility function for retreiving the text value of an array of DOM nodes
- * @param {Array|Element} elem
- */
-var getText = Sizzle.getText = function( elem ) {
- var i, node,
- nodeType = elem.nodeType,
- ret = "";
-
- if ( nodeType ) {
- if ( nodeType === 1 || nodeType === 9 ) {
- // Use textContent || innerText for elements
- if ( typeof elem.textContent === 'string' ) {
- return elem.textContent;
- } else if ( typeof elem.innerText === 'string' ) {
- // Replace IE's carriage returns
- return elem.innerText.replace( rReturn, '' );
- } else {
- // Traverse it's children
- for ( elem = elem.firstChild; elem; elem = elem.nextSibling) {
- ret += getText( elem );
- }
- }
- } else if ( nodeType === 3 || nodeType === 4 ) {
- return elem.nodeValue;
- }
- } else {
-
- // If no nodeType, this is expected to be an array
- for ( i = 0; (node = elem[i]); i++ ) {
- // Do not traverse comment nodes
- if ( node.nodeType !== 8 ) {
- ret += getText( node );
- }
- }
- }
- return ret;
-};
-
-var Expr = Sizzle.selectors = {
- order: [ "ID", "NAME", "TAG" ],
-
- match: {
- ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
- CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
- NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,
- ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,
- TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,
- CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,
- POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,
- PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
- },
-
- leftMatch: {},
-
- attrMap: {
- "class": "className",
- "for": "htmlFor"
- },
-
- attrHandle: {
- href: function( elem ) {
- return elem.getAttribute( "href" );
- },
- type: function( elem ) {
- return elem.getAttribute( "type" );
- }
- },
-
- relative: {
- "+": function(checkSet, part){
- var isPartStr = typeof part === "string",
- isTag = isPartStr && !rNonWord.test( part ),
- isPartStrNotTag = isPartStr && !isTag;
-
- if ( isTag ) {
- part = part.toLowerCase();
- }
-
- for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
- if ( (elem = checkSet[i]) ) {
- while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
-
- checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
- elem || false :
- elem === part;
- }
- }
-
- if ( isPartStrNotTag ) {
- Sizzle.filter( part, checkSet, true );
- }
- },
-
- ">": function( checkSet, part ) {
- var elem,
- isPartStr = typeof part === "string",
- i = 0,
- l = checkSet.length;
-
- if ( isPartStr && !rNonWord.test( part ) ) {
- part = part.toLowerCase();
-
- for ( ; i < l; i++ ) {
- elem = checkSet[i];
-
- if ( elem ) {
- var parent = elem.parentNode;
- checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;
- }
- }
-
- } else {
- for ( ; i < l; i++ ) {
- elem = checkSet[i];
-
- if ( elem ) {
- checkSet[i] = isPartStr ?
- elem.parentNode :
- elem.parentNode === part;
- }
- }
-
- if ( isPartStr ) {
- Sizzle.filter( part, checkSet, true );
- }
- }
- },
-
- "": function(checkSet, part, isXML){
- var nodeCheck,
- doneName = done++,
- checkFn = dirCheck;
-
- if ( typeof part === "string" && !rNonWord.test( part ) ) {
- part = part.toLowerCase();
- nodeCheck = part;
- checkFn = dirNodeCheck;
- }
-
- checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML );
- },
-
- "~": function( checkSet, part, isXML ) {
- var nodeCheck,
- doneName = done++,
- checkFn = dirCheck;
-
- if ( typeof part === "string" && !rNonWord.test( part ) ) {
- part = part.toLowerCase();
- nodeCheck = part;
- checkFn = dirNodeCheck;
- }
-
- checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML );
- }
- },
-
- find: {
- ID: function( match, context, isXML ) {
- if ( typeof context.getElementById !== "undefined" && !isXML ) {
- var m = context.getElementById(match[1]);
- // Check parentNode to catch when Blackberry 4.6 returns
- // nodes that are no longer in the document #6963
- return m && m.parentNode ? [m] : [];
- }
- },
-
- NAME: function( match, context ) {
- if ( typeof context.getElementsByName !== "undefined" ) {
- var ret = [],
- results = context.getElementsByName( match[1] );
-
- for ( var i = 0, l = results.length; i < l; i++ ) {
- if ( results[i].getAttribute("name") === match[1] ) {
- ret.push( results[i] );
- }
- }
-
- return ret.length === 0 ? null : ret;
- }
- },
-
- TAG: function( match, context ) {
- if ( typeof context.getElementsByTagName !== "undefined" ) {
- return context.getElementsByTagName( match[1] );
- }
- }
- },
- preFilter: {
- CLASS: function( match, curLoop, inplace, result, not, isXML ) {
- match = " " + match[1].replace( rBackslash, "" ) + " ";
-
- if ( isXML ) {
- return match;
- }
-
- for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
- if ( elem ) {
- if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) {
- if ( !inplace ) {
- result.push( elem );
- }
-
- } else if ( inplace ) {
- curLoop[i] = false;
- }
- }
- }
-
- return false;
- },
-
- ID: function( match ) {
- return match[1].replace( rBackslash, "" );
- },
-
- TAG: function( match, curLoop ) {
- return match[1].replace( rBackslash, "" ).toLowerCase();
- },
-
- CHILD: function( match ) {
- if ( match[1] === "nth" ) {
- if ( !match[2] ) {
- Sizzle.error( match[0] );
- }
-
- match[2] = match[2].replace(/^\+|\s*/g, '');
-
- // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
- var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec(
- match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||
- !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
-
- // calculate the numbers (first)n+(last) including if they are negative
- match[2] = (test[1] + (test[2] || 1)) - 0;
- match[3] = test[3] - 0;
- }
- else if ( match[2] ) {
- Sizzle.error( match[0] );
- }
-
- // TODO: Move to normal caching system
- match[0] = done++;
-
- return match;
- },
-
- ATTR: function( match, curLoop, inplace, result, not, isXML ) {
- var name = match[1] = match[1].replace( rBackslash, "" );
-
- if ( !isXML && Expr.attrMap[name] ) {
- match[1] = Expr.attrMap[name];
- }
-
- // Handle if an un-quoted value was used
- match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" );
-
- if ( match[2] === "~=" ) {
- match[4] = " " + match[4] + " ";
- }
-
- return match;
- },
-
- PSEUDO: function( match, curLoop, inplace, result, not ) {
- if ( match[1] === "not" ) {
- // If we're dealing with a complex expression, or a simple one
- if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
- match[3] = Sizzle(match[3], null, null, curLoop);
-
- } else {
- var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
-
- if ( !inplace ) {
- result.push.apply( result, ret );
- }
-
- return false;
- }
-
- } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {
- return true;
- }
-
- return match;
- },
-
- POS: function( match ) {
- match.unshift( true );
-
- return match;
- }
- },
-
- filters: {
- enabled: function( elem ) {
- return elem.disabled === false && elem.type !== "hidden";
- },
-
- disabled: function( elem ) {
- return elem.disabled === true;
- },
-
- checked: function( elem ) {
- return elem.checked === true;
- },
-
- selected: function( elem ) {
- // Accessing this property makes selected-by-default
- // options in Safari work properly
- if ( elem.parentNode ) {
- elem.parentNode.selectedIndex;
- }
-
- return elem.selected === true;
- },
-
- parent: function( elem ) {
- return !!elem.firstChild;
- },
-
- empty: function( elem ) {
- return !elem.firstChild;
- },
-
- has: function( elem, i, match ) {
- return !!Sizzle( match[3], elem ).length;
- },
-
- header: function( elem ) {
- return (/h\d/i).test( elem.nodeName );
- },
-
- text: function( elem ) {
- var attr = elem.getAttribute( "type" ), type = elem.type;
- // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)
- // use getAttribute instead to test this case
- return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null );
- },
-
- radio: function( elem ) {
- return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type;
- },
-
- checkbox: function( elem ) {
- return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type;
- },
-
- file: function( elem ) {
- return elem.nodeName.toLowerCase() === "input" && "file" === elem.type;
- },
-
- password: function( elem ) {
- return elem.nodeName.toLowerCase() === "input" && "password" === elem.type;
- },
-
- submit: function( elem ) {
- var name = elem.nodeName.toLowerCase();
- return (name === "input" || name === "button") && "submit" === elem.type;
- },
-
- image: function( elem ) {
- return elem.nodeName.toLowerCase() === "input" && "image" === elem.type;
- },
-
- reset: function( elem ) {
- var name = elem.nodeName.toLowerCase();
- return (name === "input" || name === "button") && "reset" === elem.type;
- },
-
- button: function( elem ) {
- var name = elem.nodeName.toLowerCase();
- return name === "input" && "button" === elem.type || name === "button";
- },
-
- input: function( elem ) {
- return (/input|select|textarea|button/i).test( elem.nodeName );
- },
-
- focus: function( elem ) {
- return elem === elem.ownerDocument.activeElement;
- }
- },
- setFilters: {
- first: function( elem, i ) {
- return i === 0;
- },
-
- last: function( elem, i, match, array ) {
- return i === array.length - 1;
- },
-
- even: function( elem, i ) {
- return i % 2 === 0;
- },
-
- odd: function( elem, i ) {
- return i % 2 === 1;
- },
-
- lt: function( elem, i, match ) {
- return i < match[3] - 0;
- },
-
- gt: function( elem, i, match ) {
- return i > match[3] - 0;
- },
-
- nth: function( elem, i, match ) {
- return match[3] - 0 === i;
- },
-
- eq: function( elem, i, match ) {
- return match[3] - 0 === i;
- }
- },
- filter: {
- PSEUDO: function( elem, match, i, array ) {
- var name = match[1],
- filter = Expr.filters[ name ];
-
- if ( filter ) {
- return filter( elem, i, match, array );
-
- } else if ( name === "contains" ) {
- return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0;
-
- } else if ( name === "not" ) {
- var not = match[3];
-
- for ( var j = 0, l = not.length; j < l; j++ ) {
- if ( not[j] === elem ) {
- return false;
- }
- }
-
- return true;
-
- } else {
- Sizzle.error( name );
- }
- },
-
- CHILD: function( elem, match ) {
- var first, last,
- doneName, parent, cache,
- count, diff,
- type = match[1],
- node = elem;
-
- switch ( type ) {
- case "only":
- case "first":
- while ( (node = node.previousSibling) ) {
- if ( node.nodeType === 1 ) {
- return false;
- }
- }
-
- if ( type === "first" ) {
- return true;
- }
-
- node = elem;
-
- case "last":
- while ( (node = node.nextSibling) ) {
- if ( node.nodeType === 1 ) {
- return false;
- }
- }
-
- return true;
-
- case "nth":
- first = match[2];
- last = match[3];
-
- if ( first === 1 && last === 0 ) {
- return true;
- }
-
- doneName = match[0];
- parent = elem.parentNode;
-
- if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) {
- count = 0;
-
- for ( node = parent.firstChild; node; node = node.nextSibling ) {
- if ( node.nodeType === 1 ) {
- node.nodeIndex = ++count;
- }
- }
-
- parent[ expando ] = doneName;
- }
-
- diff = elem.nodeIndex - last;
-
- if ( first === 0 ) {
- return diff === 0;
-
- } else {
- return ( diff % first === 0 && diff / first >= 0 );
- }
- }
- },
-
- ID: function( elem, match ) {
- return elem.nodeType === 1 && elem.getAttribute("id") === match;
- },
-
- TAG: function( elem, match ) {
- return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match;
- },
-
- CLASS: function( elem, match ) {
- return (" " + (elem.className || elem.getAttribute("class")) + " ")
- .indexOf( match ) > -1;
- },
-
- ATTR: function( elem, match ) {
- var name = match[1],
- result = Sizzle.attr ?
- Sizzle.attr( elem, name ) :
- Expr.attrHandle[ name ] ?
- Expr.attrHandle[ name ]( elem ) :
- elem[ name ] != null ?
- elem[ name ] :
- elem.getAttribute( name ),
- value = result + "",
- type = match[2],
- check = match[4];
-
- return result == null ?
- type === "!=" :
- !type && Sizzle.attr ?
- result != null :
- type === "=" ?
- value === check :
- type === "*=" ?
- value.indexOf(check) >= 0 :
- type === "~=" ?
- (" " + value + " ").indexOf(check) >= 0 :
- !check ?
- value && result !== false :
- type === "!=" ?
- value !== check :
- type === "^=" ?
- value.indexOf(check) === 0 :
- type === "$=" ?
- value.substr(value.length - check.length) === check :
- type === "|=" ?
- value === check || value.substr(0, check.length + 1) === check + "-" :
- false;
- },
-
- POS: function( elem, match, i, array ) {
- var name = match[2],
- filter = Expr.setFilters[ name ];
-
- if ( filter ) {
- return filter( elem, i, match, array );
- }
- }
- }
-};
-
-var origPOS = Expr.match.POS,
- fescape = function(all, num){
- return "\\" + (num - 0 + 1);
- };
-
-for ( var type in Expr.match ) {
- Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) );
- Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) );
-}
-
-var makeArray = function( array, results ) {
- array = Array.prototype.slice.call( array, 0 );
-
- if ( results ) {
- results.push.apply( results, array );
- return results;
- }
-
- return array;
-};
-
-// Perform a simple check to determine if the browser is capable of
-// converting a NodeList to an array using builtin methods.
-// Also verifies that the returned array holds DOM nodes
-// (which is not the case in the Blackberry browser)
-try {
- Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType;
-
-// Provide a fallback method if it does not work
-} catch( e ) {
- makeArray = function( array, results ) {
- var i = 0,
- ret = results || [];
-
- if ( toString.call(array) === "[object Array]" ) {
- Array.prototype.push.apply( ret, array );
-
- } else {
- if ( typeof array.length === "number" ) {
- for ( var l = array.length; i < l; i++ ) {
- ret.push( array[i] );
- }
-
- } else {
- for ( ; array[i]; i++ ) {
- ret.push( array[i] );
- }
- }
- }
-
- return ret;
- };
-}
-
-var sortOrder, siblingCheck;
-
-if ( document.documentElement.compareDocumentPosition ) {
- sortOrder = function( a, b ) {
- if ( a === b ) {
- hasDuplicate = true;
- return 0;
- }
-
- if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
- return a.compareDocumentPosition ? -1 : 1;
- }
-
- return a.compareDocumentPosition(b) & 4 ? -1 : 1;
- };
-
-} else {
- sortOrder = function( a, b ) {
- // The nodes are identical, we can exit early
- if ( a === b ) {
- hasDuplicate = true;
- return 0;
-
- // Fallback to using sourceIndex (in IE) if it's available on both nodes
- } else if ( a.sourceIndex && b.sourceIndex ) {
- return a.sourceIndex - b.sourceIndex;
- }
-
- var al, bl,
- ap = [],
- bp = [],
- aup = a.parentNode,
- bup = b.parentNode,
- cur = aup;
-
- // If the nodes are siblings (or identical) we can do a quick check
- if ( aup === bup ) {
- return siblingCheck( a, b );
-
- // If no parents were found then the nodes are disconnected
- } else if ( !aup ) {
- return -1;
-
- } else if ( !bup ) {
- return 1;
- }
-
- // Otherwise they're somewhere else in the tree so we need
- // to build up a full list of the parentNodes for comparison
- while ( cur ) {
- ap.unshift( cur );
- cur = cur.parentNode;
- }
-
- cur = bup;
-
- while ( cur ) {
- bp.unshift( cur );
- cur = cur.parentNode;
- }
-
- al = ap.length;
- bl = bp.length;
-
- // Start walking down the tree looking for a discrepancy
- for ( var i = 0; i < al && i < bl; i++ ) {
- if ( ap[i] !== bp[i] ) {
- return siblingCheck( ap[i], bp[i] );
- }
- }
-
- // We ended someplace up the tree so do a sibling check
- return i === al ?
- siblingCheck( a, bp[i], -1 ) :
- siblingCheck( ap[i], b, 1 );
- };
-
- siblingCheck = function( a, b, ret ) {
- if ( a === b ) {
- return ret;
- }
-
- var cur = a.nextSibling;
-
- while ( cur ) {
- if ( cur === b ) {
- return -1;
- }
-
- cur = cur.nextSibling;
- }
-
- return 1;
- };
-}
-
-// Check to see if the browser returns elements by name when
-// querying by getElementById (and provide a workaround)
-(function(){
- // We're going to inject a fake input element with a specified name
- var form = document.createElement("div"),
- id = "script" + (new Date()).getTime(),
- root = document.documentElement;
-
- form.innerHTML = "";
-
- // Inject it into the root element, check its status, and remove it quickly
- root.insertBefore( form, root.firstChild );
-
- // The workaround has to do additional checks after a getElementById
- // Which slows things down for other browsers (hence the branching)
- if ( document.getElementById( id ) ) {
- Expr.find.ID = function( match, context, isXML ) {
- if ( typeof context.getElementById !== "undefined" && !isXML ) {
- var m = context.getElementById(match[1]);
-
- return m ?
- m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ?
- [m] :
- undefined :
- [];
- }
- };
-
- Expr.filter.ID = function( elem, match ) {
- var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
-
- return elem.nodeType === 1 && node && node.nodeValue === match;
- };
- }
-
- root.removeChild( form );
-
- // release memory in IE
- root = form = null;
-})();
-
-(function(){
- // Check to see if the browser returns only elements
- // when doing getElementsByTagName("*")
-
- // Create a fake element
- var div = document.createElement("div");
- div.appendChild( document.createComment("") );
-
- // Make sure no comments are found
- if ( div.getElementsByTagName("*").length > 0 ) {
- Expr.find.TAG = function( match, context ) {
- var results = context.getElementsByTagName( match[1] );
-
- // Filter out possible comments
- if ( match[1] === "*" ) {
- var tmp = [];
-
- for ( var i = 0; results[i]; i++ ) {
- if ( results[i].nodeType === 1 ) {
- tmp.push( results[i] );
- }
- }
-
- results = tmp;
- }
-
- return results;
- };
- }
-
- // Check to see if an attribute returns normalized href attributes
- div.innerHTML = "";
-
- if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
- div.firstChild.getAttribute("href") !== "#" ) {
-
- Expr.attrHandle.href = function( elem ) {
- return elem.getAttribute( "href", 2 );
- };
- }
-
- // release memory in IE
- div = null;
-})();
-
-if ( document.querySelectorAll ) {
- (function(){
- var oldSizzle = Sizzle,
- div = document.createElement("div"),
- id = "__sizzle__";
-
- div.innerHTML = "";
-
- // Safari can't handle uppercase or unicode characters when
- // in quirks mode.
- if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
- return;
- }
-
- Sizzle = function( query, context, extra, seed ) {
- context = context || document;
-
- // Only use querySelectorAll on non-XML documents
- // (ID selectors don't work in non-HTML documents)
- if ( !seed && !Sizzle.isXML(context) ) {
- // See if we find a selector to speed up
- var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query );
-
- if ( match && (context.nodeType === 1 || context.nodeType === 9) ) {
- // Speed-up: Sizzle("TAG")
- if ( match[1] ) {
- return makeArray( context.getElementsByTagName( query ), extra );
-
- // Speed-up: Sizzle(".CLASS")
- } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) {
- return makeArray( context.getElementsByClassName( match[2] ), extra );
- }
- }
-
- if ( context.nodeType === 9 ) {
- // Speed-up: Sizzle("body")
- // The body element only exists once, optimize finding it
- if ( query === "body" && context.body ) {
- return makeArray( [ context.body ], extra );
-
- // Speed-up: Sizzle("#ID")
- } else if ( match && match[3] ) {
- var elem = context.getElementById( match[3] );
-
- // Check parentNode to catch when Blackberry 4.6 returns
- // nodes that are no longer in the document #6963
- if ( elem && elem.parentNode ) {
- // Handle the case where IE and Opera return items
- // by name instead of ID
- if ( elem.id === match[3] ) {
- return makeArray( [ elem ], extra );
- }
-
- } else {
- return makeArray( [], extra );
- }
- }
-
- try {
- return makeArray( context.querySelectorAll(query), extra );
- } catch(qsaError) {}
-
- // qSA works strangely on Element-rooted queries
- // We can work around this by specifying an extra ID on the root
- // and working up from there (Thanks to Andrew Dupont for the technique)
- // IE 8 doesn't work on object elements
- } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
- var oldContext = context,
- old = context.getAttribute( "id" ),
- nid = old || id,
- hasParent = context.parentNode,
- relativeHierarchySelector = /^\s*[+~]/.test( query );
-
- if ( !old ) {
- context.setAttribute( "id", nid );
- } else {
- nid = nid.replace( /'/g, "\\$&" );
- }
- if ( relativeHierarchySelector && hasParent ) {
- context = context.parentNode;
- }
-
- try {
- if ( !relativeHierarchySelector || hasParent ) {
- return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra );
- }
-
- } catch(pseudoError) {
- } finally {
- if ( !old ) {
- oldContext.removeAttribute( "id" );
- }
- }
- }
- }
-
- return oldSizzle(query, context, extra, seed);
- };
-
- for ( var prop in oldSizzle ) {
- Sizzle[ prop ] = oldSizzle[ prop ];
- }
-
- // release memory in IE
- div = null;
- })();
-}
-
-(function(){
- var html = document.documentElement,
- matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector;
-
- if ( matches ) {
- // Check to see if it's possible to do matchesSelector
- // on a disconnected node (IE 9 fails this)
- var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ),
- pseudoWorks = false;
-
- try {
- // This should fail with an exception
- // Gecko does not error, returns false instead
- matches.call( document.documentElement, "[test!='']:sizzle" );
-
- } catch( pseudoError ) {
- pseudoWorks = true;
- }
-
- Sizzle.matchesSelector = function( node, expr ) {
- // Make sure that attribute selectors are quoted
- expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']");
-
- if ( !Sizzle.isXML( node ) ) {
- try {
- if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) {
- var ret = matches.call( node, expr );
-
- // IE 9's matchesSelector returns false on disconnected nodes
- if ( ret || !disconnectedMatch ||
- // As well, disconnected nodes are said to be in a document
- // fragment in IE 9, so check for that
- node.document && node.document.nodeType !== 11 ) {
- return ret;
- }
- }
- } catch(e) {}
- }
-
- return Sizzle(expr, null, null, [node]).length > 0;
- };
- }
-})();
-
-(function(){
- var div = document.createElement("div");
-
- div.innerHTML = "";
-
- // Opera can't find a second classname (in 9.6)
- // Also, make sure that getElementsByClassName actually exists
- if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) {
- return;
- }
-
- // Safari caches class attributes, doesn't catch changes (in 3.2)
- div.lastChild.className = "e";
-
- if ( div.getElementsByClassName("e").length === 1 ) {
- return;
- }
-
- Expr.order.splice(1, 0, "CLASS");
- Expr.find.CLASS = function( match, context, isXML ) {
- if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
- return context.getElementsByClassName(match[1]);
- }
- };
-
- // release memory in IE
- div = null;
-})();
-
-function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
- for ( var i = 0, l = checkSet.length; i < l; i++ ) {
- var elem = checkSet[i];
-
- if ( elem ) {
- var match = false;
-
- elem = elem[dir];
-
- while ( elem ) {
- if ( elem[ expando ] === doneName ) {
- match = checkSet[elem.sizset];
- break;
- }
-
- if ( elem.nodeType === 1 && !isXML ){
- elem[ expando ] = doneName;
- elem.sizset = i;
- }
-
- if ( elem.nodeName.toLowerCase() === cur ) {
- match = elem;
- break;
- }
-
- elem = elem[dir];
- }
-
- checkSet[i] = match;
- }
- }
-}
-
-function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
- for ( var i = 0, l = checkSet.length; i < l; i++ ) {
- var elem = checkSet[i];
-
- if ( elem ) {
- var match = false;
-
- elem = elem[dir];
-
- while ( elem ) {
- if ( elem[ expando ] === doneName ) {
- match = checkSet[elem.sizset];
- break;
- }
-
- if ( elem.nodeType === 1 ) {
- if ( !isXML ) {
- elem[ expando ] = doneName;
- elem.sizset = i;
- }
-
- if ( typeof cur !== "string" ) {
- if ( elem === cur ) {
- match = true;
- break;
- }
-
- } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
- match = elem;
- break;
- }
- }
-
- elem = elem[dir];
- }
-
- checkSet[i] = match;
- }
- }
-}
-
-if ( document.documentElement.contains ) {
- Sizzle.contains = function( a, b ) {
- return a !== b && (a.contains ? a.contains(b) : true);
- };
-
-} else if ( document.documentElement.compareDocumentPosition ) {
- Sizzle.contains = function( a, b ) {
- return !!(a.compareDocumentPosition(b) & 16);
- };
-
-} else {
- Sizzle.contains = function() {
- return false;
- };
-}
-
-Sizzle.isXML = function( elem ) {
- // documentElement is verified for cases where it doesn't yet exist
- // (such as loading iframes in IE - #4833)
- var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;
-
- return documentElement ? documentElement.nodeName !== "HTML" : false;
-};
-
-var posProcess = function( selector, context, seed ) {
- var match,
- tmpSet = [],
- later = "",
- root = context.nodeType ? [context] : context;
-
- // Position selectors must be done after the filter
- // And so must :not(positional) so we move all PSEUDOs to the end
- while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
- later += match[0];
- selector = selector.replace( Expr.match.PSEUDO, "" );
- }
-
- selector = Expr.relative[selector] ? selector + "*" : selector;
-
- for ( var i = 0, l = root.length; i < l; i++ ) {
- Sizzle( selector, root[i], tmpSet, seed );
- }
-
- return Sizzle.filter( later, tmpSet );
-};
-
-// EXPOSE
-// Override sizzle attribute retrieval
-Sizzle.attr = jQuery.attr;
-Sizzle.selectors.attrMap = {};
-jQuery.find = Sizzle;
-jQuery.expr = Sizzle.selectors;
-jQuery.expr[":"] = jQuery.expr.filters;
-jQuery.unique = Sizzle.uniqueSort;
-jQuery.text = Sizzle.getText;
-jQuery.isXMLDoc = Sizzle.isXML;
-jQuery.contains = Sizzle.contains;
-
-
-})();
-
-
-var runtil = /Until$/,
- rparentsprev = /^(?:parents|prevUntil|prevAll)/,
- // Note: This RegExp should be improved, or likely pulled from Sizzle
- rmultiselector = /,/,
- isSimple = /^.[^:#\[\.,]*$/,
- slice = Array.prototype.slice,
- POS = jQuery.expr.match.POS,
- // methods guaranteed to produce a unique set when starting from a unique set
- guaranteedUnique = {
- children: true,
- contents: true,
- next: true,
- prev: true
- };
-
-jQuery.fn.extend({
- find: function( selector ) {
- var self = this,
- i, l;
-
- if ( typeof selector !== "string" ) {
- return jQuery( selector ).filter(function() {
- for ( i = 0, l = self.length; i < l; i++ ) {
- if ( jQuery.contains( self[ i ], this ) ) {
- return true;
- }
- }
- });
- }
-
- var ret = this.pushStack( "", "find", selector ),
- length, n, r;
-
- for ( i = 0, l = this.length; i < l; i++ ) {
- length = ret.length;
- jQuery.find( selector, this[i], ret );
-
- if ( i > 0 ) {
- // Make sure that the results are unique
- for ( n = length; n < ret.length; n++ ) {
- for ( r = 0; r < length; r++ ) {
- if ( ret[r] === ret[n] ) {
- ret.splice(n--, 1);
- break;
- }
- }
- }
- }
- }
-
- return ret;
- },
-
- has: function( target ) {
- var targets = jQuery( target );
- return this.filter(function() {
- for ( var i = 0, l = targets.length; i < l; i++ ) {
- if ( jQuery.contains( this, targets[i] ) ) {
- return true;
- }
- }
- });
- },
-
- not: function( selector ) {
- return this.pushStack( winnow(this, selector, false), "not", selector);
- },
-
- filter: function( selector ) {
- return this.pushStack( winnow(this, selector, true), "filter", selector );
- },
-
- is: function( selector ) {
- return !!selector && (
- typeof selector === "string" ?
- // If this is a positional selector, check membership in the returned set
- // so $("p:first").is("p:last") won't return true for a doc with two "p".
- POS.test( selector ) ?
- jQuery( selector, this.context ).index( this[0] ) >= 0 :
- jQuery.filter( selector, this ).length > 0 :
- this.filter( selector ).length > 0 );
- },
-
- closest: function( selectors, context ) {
- var ret = [], i, l, cur = this[0];
-
- // Array (deprecated as of jQuery 1.7)
- if ( jQuery.isArray( selectors ) ) {
- var level = 1;
-
- while ( cur && cur.ownerDocument && cur !== context ) {
- for ( i = 0; i < selectors.length; i++ ) {
-
- if ( jQuery( cur ).is( selectors[ i ] ) ) {
- ret.push({ selector: selectors[ i ], elem: cur, level: level });
- }
- }
-
- cur = cur.parentNode;
- level++;
- }
-
- return ret;
- }
-
- // String
- var pos = POS.test( selectors ) || typeof selectors !== "string" ?
- jQuery( selectors, context || this.context ) :
- 0;
-
- for ( i = 0, l = this.length; i < l; i++ ) {
- cur = this[i];
-
- while ( cur ) {
- if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) {
- ret.push( cur );
- break;
-
- } else {
- cur = cur.parentNode;
- if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) {
- break;
- }
- }
- }
- }
-
- ret = ret.length > 1 ? jQuery.unique( ret ) : ret;
-
- return this.pushStack( ret, "closest", selectors );
- },
-
- // Determine the position of an element within
- // the matched set of elements
- index: function( elem ) {
-
- // No argument, return index in parent
- if ( !elem ) {
- return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1;
- }
-
- // index in selector
- if ( typeof elem === "string" ) {
- return jQuery.inArray( this[0], jQuery( elem ) );
- }
-
- // Locate the position of the desired element
- return jQuery.inArray(
- // If it receives a jQuery object, the first element is used
- elem.jquery ? elem[0] : elem, this );
- },
-
- add: function( selector, context ) {
- var set = typeof selector === "string" ?
- jQuery( selector, context ) :
- jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ),
- all = jQuery.merge( this.get(), set );
-
- return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ?
- all :
- jQuery.unique( all ) );
- },
-
- andSelf: function() {
- return this.add( this.prevObject );
- }
-});
-
-// A painfully simple check to see if an element is disconnected
-// from a document (should be improved, where feasible).
-function isDisconnected( node ) {
- return !node || !node.parentNode || node.parentNode.nodeType === 11;
-}
-
-jQuery.each({
- parent: function( elem ) {
- var parent = elem.parentNode;
- return parent && parent.nodeType !== 11 ? parent : null;
- },
- parents: function( elem ) {
- return jQuery.dir( elem, "parentNode" );
- },
- parentsUntil: function( elem, i, until ) {
- return jQuery.dir( elem, "parentNode", until );
- },
- next: function( elem ) {
- return jQuery.nth( elem, 2, "nextSibling" );
- },
- prev: function( elem ) {
- return jQuery.nth( elem, 2, "previousSibling" );
- },
- nextAll: function( elem ) {
- return jQuery.dir( elem, "nextSibling" );
- },
- prevAll: function( elem ) {
- return jQuery.dir( elem, "previousSibling" );
- },
- nextUntil: function( elem, i, until ) {
- return jQuery.dir( elem, "nextSibling", until );
- },
- prevUntil: function( elem, i, until ) {
- return jQuery.dir( elem, "previousSibling", until );
- },
- siblings: function( elem ) {
- return jQuery.sibling( elem.parentNode.firstChild, elem );
- },
- children: function( elem ) {
- return jQuery.sibling( elem.firstChild );
- },
- contents: function( elem ) {
- return jQuery.nodeName( elem, "iframe" ) ?
- elem.contentDocument || elem.contentWindow.document :
- jQuery.makeArray( elem.childNodes );
- }
-}, function( name, fn ) {
- jQuery.fn[ name ] = function( until, selector ) {
- var ret = jQuery.map( this, fn, until );
-
- if ( !runtil.test( name ) ) {
- selector = until;
- }
-
- if ( selector && typeof selector === "string" ) {
- ret = jQuery.filter( selector, ret );
- }
-
- ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
-
- if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
- ret = ret.reverse();
- }
-
- return this.pushStack( ret, name, slice.call( arguments ).join(",") );
- };
-});
-
-jQuery.extend({
- filter: function( expr, elems, not ) {
- if ( not ) {
- expr = ":not(" + expr + ")";
- }
-
- return elems.length === 1 ?
- jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] :
- jQuery.find.matches(expr, elems);
- },
-
- dir: function( elem, dir, until ) {
- var matched = [],
- cur = elem[ dir ];
-
- while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
- if ( cur.nodeType === 1 ) {
- matched.push( cur );
- }
- cur = cur[dir];
- }
- return matched;
- },
-
- nth: function( cur, result, dir, elem ) {
- result = result || 1;
- var num = 0;
-
- for ( ; cur; cur = cur[dir] ) {
- if ( cur.nodeType === 1 && ++num === result ) {
- break;
- }
- }
-
- return cur;
- },
-
- sibling: function( n, elem ) {
- var r = [];
-
- for ( ; n; n = n.nextSibling ) {
- if ( n.nodeType === 1 && n !== elem ) {
- r.push( n );
- }
- }
-
- return r;
- }
-});
-
-// Implement the identical functionality for filter and not
-function winnow( elements, qualifier, keep ) {
-
- // Can't pass null or undefined to indexOf in Firefox 4
- // Set to 0 to skip string check
- qualifier = qualifier || 0;
-
- if ( jQuery.isFunction( qualifier ) ) {
- return jQuery.grep(elements, function( elem, i ) {
- var retVal = !!qualifier.call( elem, i, elem );
- return retVal === keep;
- });
-
- } else if ( qualifier.nodeType ) {
- return jQuery.grep(elements, function( elem, i ) {
- return ( elem === qualifier ) === keep;
- });
-
- } else if ( typeof qualifier === "string" ) {
- var filtered = jQuery.grep(elements, function( elem ) {
- return elem.nodeType === 1;
- });
-
- if ( isSimple.test( qualifier ) ) {
- return jQuery.filter(qualifier, filtered, !keep);
- } else {
- qualifier = jQuery.filter( qualifier, filtered );
- }
- }
-
- return jQuery.grep(elements, function( elem, i ) {
- return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep;
- });
-}
-
-
-
-
-function createSafeFragment( document ) {
- var list = nodeNames.split( "|" ),
- safeFrag = document.createDocumentFragment();
-
- if ( safeFrag.createElement ) {
- while ( list.length ) {
- safeFrag.createElement(
- list.pop()
- );
- }
- }
- return safeFrag;
-}
-
-var nodeNames = "abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|" +
- "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",
- rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g,
- rleadingWhitespace = /^\s+/,
- rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
- rtagName = /<([\w:]+)/,
- rtbody = /", "" ],
- legend: [ 1, "" ],
- thead: [ 1, "
"
-
- if text is ""
- lineHTML += @buildEmptyLineInnerHTML(id)
- else
- lineHTML += @buildLineInnerHTML(id)
-
- lineHTML += '' if fold
- lineHTML += "
"
- lineHTML
-
- buildEmptyLineInnerHTML: (id) ->
- {indentGuidesVisible} = @newState
- {indentLevel, tabLength, endOfLineInvisibles} = @newState.lines[id]
-
- if indentGuidesVisible and indentLevel > 0
- invisibleIndex = 0
- lineHTML = ''
- for i in [0...indentLevel]
- lineHTML += ""
- for j in [0...tabLength]
- if invisible = endOfLineInvisibles?[invisibleIndex++]
- lineHTML += "#{invisible}"
- else
- lineHTML += ' '
- lineHTML += ""
-
- while invisibleIndex < endOfLineInvisibles?.length
- lineHTML += "#{endOfLineInvisibles[invisibleIndex++]}"
-
- lineHTML
- else
- @buildEndOfLineHTML(id) or ' '
-
- buildLineInnerHTML: (id) ->
- lineState = @newState.lines[id]
- {firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState
- lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
-
- innerHTML = ""
- @tokenIterator.reset(lineState)
-
- while @tokenIterator.next()
- for scope in @tokenIterator.getScopeEnds()
- innerHTML += ""
-
- for scope in @tokenIterator.getScopeStarts()
- innerHTML += ""
-
- tokenStart = @tokenIterator.getScreenStart()
- tokenEnd = @tokenIterator.getScreenEnd()
- tokenText = @tokenIterator.getText()
- isHardTab = @tokenIterator.isHardTab()
-
- if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex
- tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart
- else
- tokenFirstNonWhitespaceIndex = null
-
- if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex
- tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart)
- else
- tokenFirstTrailingWhitespaceIndex = null
-
- hasIndentGuide =
- @newState.indentGuidesVisible and
- (hasLeadingWhitespace or lineIsWhitespaceOnly)
-
- hasInvisibleCharacters =
- (invisibles?.tab and isHardTab) or
- (invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace))
-
- innerHTML += @buildTokenHTML(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters)
-
- for scope in @tokenIterator.getScopeEnds()
- innerHTML += ""
-
- for scope in @tokenIterator.getScopes()
- innerHTML += ""
-
- innerHTML += @buildEndOfLineHTML(id)
- innerHTML
-
- buildTokenHTML: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters) ->
- if isHardTab
- classes = 'hard-tab'
- classes += ' leading-whitespace' if firstNonWhitespaceIndex?
- classes += ' trailing-whitespace' if firstTrailingWhitespaceIndex?
- classes += ' indent-guide' if hasIndentGuide
- classes += ' invisible-character' if hasInvisibleCharacters
- return "#{@escapeTokenText(tokenText)}"
- else
- startIndex = 0
- endIndex = tokenText.length
-
- leadingHtml = ''
- trailingHtml = ''
-
- if firstNonWhitespaceIndex?
- leadingWhitespace = tokenText.substring(0, firstNonWhitespaceIndex)
-
- classes = 'leading-whitespace'
- classes += ' indent-guide' if hasIndentGuide
- classes += ' invisible-character' if hasInvisibleCharacters
-
- leadingHtml = "#{leadingWhitespace}"
- startIndex = firstNonWhitespaceIndex
-
- if firstTrailingWhitespaceIndex?
- tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0
- trailingWhitespace = tokenText.substring(firstTrailingWhitespaceIndex)
-
- classes = 'trailing-whitespace'
- classes += ' indent-guide' if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
- classes += ' invisible-character' if hasInvisibleCharacters
-
- trailingHtml = "#{trailingWhitespace}"
-
- endIndex = firstTrailingWhitespaceIndex
-
- html = leadingHtml
- if tokenText.length > MaxTokenLength
- while startIndex < endIndex
- html += "" + @escapeTokenText(tokenText, startIndex, startIndex + MaxTokenLength) + ""
- startIndex += MaxTokenLength
- else
- html += @escapeTokenText(tokenText, startIndex, endIndex)
-
- html += trailingHtml
- html
-
- escapeTokenText: (tokenText, startIndex, endIndex) ->
- if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length
- tokenText = tokenText.slice(startIndex, endIndex)
- tokenText.replace(TokenTextEscapeRegex, @escapeTokenTextReplace)
-
- escapeTokenTextReplace: (match) ->
- switch match
- when '&' then '&'
- when '"' then '"'
- when "'" then '''
- when '<' then '<'
- when '>' then '>'
- else match
-
- buildEndOfLineHTML: (id) ->
- {endOfLineInvisibles} = @newState.lines[id]
-
- html = ''
- if endOfLineInvisibles?
- for invisible in endOfLineInvisibles
- html += "#{invisible}"
- html
-
- updateLineNode: (id) ->
- oldLineState = @oldState.lines[id]
- newLineState = @newState.lines[id]
-
- lineNode = @lineNodesByLineId[id]
-
- if @newState.scrollWidth isnt @oldState.scrollWidth
- lineNode.style.width = @newState.scrollWidth + 'px'
-
- newDecorationClasses = newLineState.decorationClasses
- oldDecorationClasses = oldLineState.decorationClasses
-
- if oldDecorationClasses?
- for decorationClass in oldDecorationClasses
- unless newDecorationClasses? and decorationClass in newDecorationClasses
- lineNode.classList.remove(decorationClass)
-
- if newDecorationClasses?
- for decorationClass in newDecorationClasses
- unless oldDecorationClasses? and decorationClass in oldDecorationClasses
- lineNode.classList.add(decorationClass)
-
- oldLineState.decorationClasses = newLineState.decorationClasses
-
- if newLineState.top isnt oldLineState.top
- lineNode.style.top = newLineState.top + 'px'
- oldLineState.top = newLineState.cop
-
- if newLineState.screenRow isnt oldLineState.screenRow
- lineNode.dataset.screenRow = newLineState.screenRow
- oldLineState.screenRow = newLineState.screenRow
- @lineIdsByScreenRow[newLineState.screenRow] = id
-
- lineNodeForScreenRow: (screenRow) ->
- @lineNodesByLineId[@lineIdsByScreenRow[screenRow]]
-
- measureLineHeightAndDefaultCharWidth: ->
- @domNode.appendChild(DummyLineNode)
- lineHeightInPixels = DummyLineNode.getBoundingClientRect().height
- charWidth = DummyLineNode.firstChild.getBoundingClientRect().width
- @domNode.removeChild(DummyLineNode)
-
- @presenter.setLineHeight(lineHeightInPixels)
- @presenter.setBaseCharacterWidth(charWidth)
-
- remeasureCharacterWidths: ->
- return unless @presenter.baseCharacterWidth
-
- @clearScopedCharWidths()
- @measureCharactersInNewLines()
-
- measureCharactersInNewLines: ->
- @presenter.batchCharacterMeasurement =>
- for id, lineState of @oldState.lines
- unless @measuredLines.has(id)
- lineNode = @lineNodesByLineId[id]
- @measureCharactersInLine(id, lineState, lineNode)
- return
-
- measureCharactersInLine: (lineId, tokenizedLine, lineNode) ->
- rangeForMeasurement = null
- iterator = null
- charIndex = 0
-
- @tokenIterator.reset(tokenizedLine)
- while @tokenIterator.next()
- scopes = @tokenIterator.getScopes()
- text = @tokenIterator.getText()
- charWidths = @presenter.getScopedCharacterWidths(scopes)
-
- textIndex = 0
- while textIndex < text.length
- if @tokenIterator.isPairedCharacter()
- char = text
- charLength = 2
- textIndex += 2
- else
- char = text[textIndex]
- charLength = 1
- textIndex++
-
- continue if char is '\0'
-
- unless charWidths[char]?
- unless textNode?
- rangeForMeasurement ?= document.createRange()
- iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
- textNode = iterator.nextNode()
- textNodeIndex = 0
- nextTextNodeIndex = textNode.textContent.length
-
- while nextTextNodeIndex <= charIndex
- textNode = iterator.nextNode()
- textNodeIndex = nextTextNodeIndex
- nextTextNodeIndex = textNodeIndex + textNode.textContent.length
-
- i = charIndex - textNodeIndex
- rangeForMeasurement.setStart(textNode, i)
- rangeForMeasurement.setEnd(textNode, i + charLength)
- charWidth = rangeForMeasurement.getBoundingClientRect().width
- @presenter.setScopedCharacterWidth(scopes, char, charWidth)
-
- charIndex += charLength
-
- @measuredLines.add(lineId)
-
- clearScopedCharWidths: ->
- @measuredLines.clear()
- @presenter.clearScopedCharacterWidths()
diff --git a/src/main-process/application-menu.js b/src/main-process/application-menu.js
new file mode 100644
index 00000000000..7a8d6d9599d
--- /dev/null
+++ b/src/main-process/application-menu.js
@@ -0,0 +1,254 @@
+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;
+ }
+};
diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js
new file mode 100644
index 00000000000..f05d4924496
--- /dev/null
+++ b/src/main-process/atom-application.js
@@ -0,0 +1,2115 @@
+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;
+ }
+}
diff --git a/src/main-process/atom-protocol-handler.js b/src/main-process/atom-protocol-handler.js
new file mode 100644
index 00000000000..173733d5c12
--- /dev/null
+++ b/src/main-process/atom-protocol-handler.js
@@ -0,0 +1,54 @@
+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);
+ });
+ }
+};
diff --git a/src/main-process/atom-window.js b/src/main-process/atom-window.js
new file mode 100644
index 00000000000..47f41e6cb53
--- /dev/null
+++ b/src/main-process/atom-window.js
@@ -0,0 +1,539 @@
+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;
+ }
+};
diff --git a/src/main-process/auto-update-manager.js b/src/main-process/auto-update-manager.js
new file mode 100644
index 00000000000..f33d718d282
--- /dev/null
+++ b/src/main-process/auto-update-manager.js
@@ -0,0 +1,200 @@
+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();
+ }
+};
diff --git a/src/main-process/auto-updater-win32.js b/src/main-process/auto-updater-win32.js
new file mode 100644
index 00000000000..99015fd4c13
--- /dev/null
+++ b/src/main-process/auto-updater-win32.js
@@ -0,0 +1,94 @@
+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();
diff --git a/src/main-process/context-menu.js b/src/main-process/context-menu.js
new file mode 100644
index 00000000000..28af6cf357f
--- /dev/null
+++ b/src/main-process/context-menu.js
@@ -0,0 +1,31 @@
+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);
+ }
+ });
+ }
+};
diff --git a/src/main-process/file-recovery-service.js b/src/main-process/file-recovery-service.js
new file mode 100644
index 00000000000..c0a36dfbec7
--- /dev/null
+++ b/src/main-process/file-recovery-service.js
@@ -0,0 +1,183 @@
+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());
+ });
+ });
+ });
+}
diff --git a/src/main-process/main.js b/src/main-process/main.js
new file mode 100644
index 00000000000..28b6f3f266f
--- /dev/null
+++ b/src/main-process/main.js
@@ -0,0 +1,69 @@
+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 args = yargs(process.argv)
+ // Don't handle --help or --version here; they will be handled later.
+ .help(false)
+ .version(false)
+ .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);
diff --git a/src/main-process/parse-command-line.js b/src/main-process/parse-command-line.js
new file mode 100644
index 00000000000..e152ee7e89d
--- /dev/null
+++ b/src/main-process/parse-command-line.js
@@ -0,0 +1,239 @@
+'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.help('help', 'Print this usage message.').alias('h', 'help');
+ 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');
+ options
+ .version(
+ dedent`Atom : ${version}
+ Electron: ${process.versions.electron}
+ Chrome : ${process.versions.chrome}
+ Node : ${process.versions.node}`
+ )
+ .alias('v', 'version');
+
+ // NB: if --help or --version are given, this also displays the relevant message and exits
+ 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)
+ };
+ }
+
+ 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
+ };
+};
diff --git a/src/main-process/spawner.js b/src/main-process/spawner.js
new file mode 100644
index 00000000000..21a673205ff
--- /dev/null
+++ b/src/main-process/spawner.js
@@ -0,0 +1,47 @@
+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();
+};
diff --git a/src/main-process/squirrel-update.js b/src/main-process/squirrel-update.js
new file mode 100644
index 00000000000..86f4148245b
--- /dev/null
+++ b/src/main-process/squirrel-update.js
@@ -0,0 +1,234 @@
+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;
+ }
+};
diff --git a/src/main-process/start.js b/src/main-process/start.js
new file mode 100644
index 00000000000..122c332f67a
--- /dev/null
+++ b/src/main-process/start.js
@@ -0,0 +1,171 @@
+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);
+ }
+ });
+
+ // 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));
+
+ // This must happen after parseCommandLine() because yargs uses console.log
+ // to display the usage message.
+ const previousConsoleLog = console.log;
+ console.log = nslog;
+
+ 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;
+ }
+}
diff --git a/src/main-process/win-powershell.js b/src/main-process/win-powershell.js
new file mode 100644
index 00000000000..1c447c935e9
--- /dev/null
+++ b/src/main-process/win-powershell.js
@@ -0,0 +1,52 @@
+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);
+ }
+ );
diff --git a/src/main-process/win-shell.js b/src/main-process/win-shell.js
new file mode 100644
index 00000000000..366eb513cd9
--- /dev/null
+++ b/src/main-process/win-shell.js
@@ -0,0 +1,104 @@
+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'))
+);
diff --git a/src/marker.coffee b/src/marker.coffee
deleted file mode 100644
index a0d8bda009e..00000000000
--- a/src/marker.coffee
+++ /dev/null
@@ -1,386 +0,0 @@
-_ = require 'underscore-plus'
-{CompositeDisposable, Emitter} = require 'event-kit'
-Grim = require 'grim'
-
-# Essential: Represents a buffer annotation that remains logically stationary
-# even as the buffer changes. This is used to represent cursors, folds, snippet
-# targets, misspelled words, and anything else that needs to track a logical
-# location in the buffer over time.
-#
-# ### Marker Creation
-#
-# Use {TextEditor::markBufferRange} rather than creating Markers directly.
-#
-# ### Head and Tail
-#
-# Markers always have a *head* and sometimes have a *tail*. If you think of a
-# marker as an editor selection, the tail is the part that's stationary and the
-# head is the part that moves when the mouse is moved. A marker without a tail
-# always reports an empty range at the head position. A marker with a head position
-# greater than the tail is in a "normal" orientation. If the head precedes the
-# tail the marker is in a "reversed" orientation.
-#
-# ### Validity
-#
-# Markers are considered *valid* when they are first created. Depending on the
-# invalidation strategy you choose, certain changes to the buffer can cause a
-# marker to become invalid, for example if the text surrounding the marker is
-# deleted. The strategies, in order of descending 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.
-#
-# See {TextEditor::markBufferRange} for usage.
-module.exports =
-class Marker
- bufferMarkerSubscription: null
- oldHeadBufferPosition: null
- oldHeadScreenPosition: null
- oldTailBufferPosition: null
- oldTailScreenPosition: null
- wasValid: true
-
- ###
- Section: Construction and Destruction
- ###
-
- constructor: ({@bufferMarker, @displayBuffer}) ->
- @emitter = new Emitter
- @disposables = new CompositeDisposable
- @id = @bufferMarker.id
- @oldHeadBufferPosition = @getHeadBufferPosition()
- @oldHeadScreenPosition = @getHeadScreenPosition()
- @oldTailBufferPosition = @getTailBufferPosition()
- @oldTailScreenPosition = @getTailScreenPosition()
- @wasValid = @isValid()
-
- @disposables.add @bufferMarker.onDidDestroy => @destroyed()
- @disposables.add @bufferMarker.onDidChange (event) => @notifyObservers(event)
-
- # Essential: Destroys the marker, causing it to emit the 'destroyed' event. Once
- # destroyed, a marker cannot be restored by undo/redo operations.
- destroy: ->
- @bufferMarker.destroy()
- @disposables.dispose()
-
- # Essential: Creates and returns a new {Marker} with the same properties as this
- # marker.
- #
- # * `properties` {Object}
- copy: (properties) ->
- @displayBuffer.getMarker(@bufferMarker.copy(properties).id)
-
- ###
- Section: Event Subscription
- ###
-
- # Essential: Invoke the given callback when the state of the marker changes.
- #
- # * `callback` {Function} to be called when the marker changes.
- # * `event` {Object} with the following keys:
- # * `oldHeadPosition` {Point} representing the former head position
- # * `newHeadPosition` {Point} representing the new head position
- # * `oldTailPosition` {Point} representing the former tail position
- # * `newTailPosition` {Point} representing the new tail position
- # * `wasValid` {Boolean} indicating whether the marker was valid before the change
- # * `isValid` {Boolean} indicating whether the marker is now valid
- # * `hadTail` {Boolean} indicating whether the marker had a tail before the change
- # * `hasTail` {Boolean} indicating whether the marker now has a tail
- # * `oldProperties` {Object} containing the marker's custom properties before the change.
- # * `newProperties` {Object} containing the marker's custom properties after the change.
- # * `textChanged` {Boolean} indicating whether this change was caused by a textual change
- # to the buffer or whether the marker was manipulated directly via its public API.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChange: (callback) ->
- @emitter.on 'did-change', callback
-
- # Essential: Invoke the given callback when the marker is destroyed.
- #
- # * `callback` {Function} to be called when the marker is destroyed.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidDestroy: (callback) ->
- @emitter.on 'did-destroy', callback
-
- ###
- Section: Marker Details
- ###
-
- # Essential: Returns a {Boolean} indicating whether the marker is valid. Markers can be
- # invalidated when a region surrounding them in the buffer is changed.
- isValid: ->
- @bufferMarker.isValid()
-
- # Essential: Returns a {Boolean} indicating whether the marker has been destroyed. A marker
- # can be invalid without being destroyed, in which case undoing the invalidating
- # operation would restore the marker. Once a marker is destroyed by calling
- # {Marker::destroy}, no undo/redo operation can ever bring it back.
- isDestroyed: ->
- @bufferMarker.isDestroyed()
-
- # Essential: Returns a {Boolean} indicating whether the head precedes the tail.
- isReversed: ->
- @bufferMarker.isReversed()
-
- # Essential: Get the invalidation strategy for this marker.
- #
- # Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`.
- #
- # Returns a {String}.
- getInvalidationStrategy: ->
- @bufferMarker.getInvalidationStrategy()
-
- # Essential: Returns an {Object} containing any custom properties associated with
- # the marker.
- getProperties: ->
- @bufferMarker.getProperties()
-
- # Essential: Merges an {Object} containing new properties into the marker's
- # existing properties.
- #
- # * `properties` {Object}
- setProperties: (properties) ->
- @bufferMarker.setProperties(properties)
-
- matchesProperties: (attributes) ->
- attributes = @displayBuffer.translateToBufferMarkerParams(attributes)
- @bufferMarker.matchesParams(attributes)
-
- ###
- Section: Comparing to other markers
- ###
-
- # Essential: Returns a {Boolean} indicating whether this marker is equivalent to
- # another marker, meaning they have the same range and options.
- #
- # * `other` {Marker} other marker
- isEqual: (other) ->
- return false unless other instanceof @constructor
- @bufferMarker.isEqual(other.bufferMarker)
-
- # Essential: Compares this marker to another based on their ranges.
- #
- # * `other` {Marker}
- #
- # Returns a {Number}
- compare: (other) ->
- @bufferMarker.compare(other.bufferMarker)
-
- ###
- Section: Managing the marker's range
- ###
-
- # Essential: Gets the buffer range of the display marker.
- #
- # Returns a {Range}.
- getBufferRange: ->
- @bufferMarker.getRange()
-
- # Essential: Modifies the buffer range of the display marker.
- #
- # * `bufferRange` The new {Range} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation.
- setBufferRange: (bufferRange, properties) ->
- @bufferMarker.setRange(bufferRange, properties)
-
- # Essential: Gets the screen range of the display marker.
- #
- # Returns a {Range}.
- getScreenRange: ->
- @displayBuffer.screenRangeForBufferRange(@getBufferRange(), wrapAtSoftNewlines: true)
-
- # Essential: Modifies the screen range of the display marker.
- #
- # * `screenRange` The new {Range} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation.
- setScreenRange: (screenRange, options) ->
- @setBufferRange(@displayBuffer.bufferRangeForScreenRange(screenRange), options)
-
- # Essential: Retrieves the buffer position of the marker's start. This will always be
- # less than or equal to the result of {Marker::getEndBufferPosition}.
- #
- # Returns a {Point}.
- getStartBufferPosition: ->
- @bufferMarker.getStartPosition()
-
- # Essential: Retrieves the screen position of the marker's start. This will always be
- # less than or equal to the result of {Marker::getEndScreenPosition}.
- #
- # Returns a {Point}.
- getStartScreenPosition: ->
- @displayBuffer.screenPositionForBufferPosition(@getStartBufferPosition(), wrapAtSoftNewlines: true)
-
- # Essential: Retrieves the buffer position of the marker's end. This will always be
- # greater than or equal to the result of {Marker::getStartBufferPosition}.
- #
- # Returns a {Point}.
- getEndBufferPosition: ->
- @bufferMarker.getEndPosition()
-
- # Essential: Retrieves the screen position of the marker's end. This will always be
- # greater than or equal to the result of {Marker::getStartScreenPosition}.
- #
- # Returns a {Point}.
- getEndScreenPosition: ->
- @displayBuffer.screenPositionForBufferPosition(@getEndBufferPosition(), wrapAtSoftNewlines: true)
-
- # Extended: Retrieves the buffer position of the marker's head.
- #
- # Returns a {Point}.
- getHeadBufferPosition: ->
- @bufferMarker.getHeadPosition()
-
- # Extended: Sets the buffer position of the marker's head.
- #
- # * `bufferPosition` The new {Point} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- setHeadBufferPosition: (bufferPosition, properties) ->
- @bufferMarker.setHeadPosition(bufferPosition, properties)
-
- # Extended: Retrieves the screen position of the marker's head.
- #
- # Returns a {Point}.
- getHeadScreenPosition: ->
- @displayBuffer.screenPositionForBufferPosition(@getHeadBufferPosition(), wrapAtSoftNewlines: true)
-
- # Extended: Sets the screen position of the marker's head.
- #
- # * `screenPosition` The new {Point} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- setHeadScreenPosition: (screenPosition, properties) ->
- @setHeadBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, properties))
-
- # Extended: Retrieves the buffer position of the marker's tail.
- #
- # Returns a {Point}.
- getTailBufferPosition: ->
- @bufferMarker.getTailPosition()
-
- # Extended: Sets the buffer position of the marker's tail.
- #
- # * `bufferPosition` The new {Point} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- setTailBufferPosition: (bufferPosition) ->
- @bufferMarker.setTailPosition(bufferPosition)
-
- # Extended: Retrieves the screen position of the marker's tail.
- #
- # Returns a {Point}.
- getTailScreenPosition: ->
- @displayBuffer.screenPositionForBufferPosition(@getTailBufferPosition(), wrapAtSoftNewlines: true)
-
- # Extended: Sets the screen position of the marker's tail.
- #
- # * `screenPosition` The new {Point} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- setTailScreenPosition: (screenPosition, options) ->
- @setTailBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options))
-
- # Extended: Returns a {Boolean} indicating whether the marker has a tail.
- hasTail: ->
- @bufferMarker.hasTail()
-
- # Extended: Plants the marker's tail at the current head position. After calling
- # the marker's tail position will be its head position at the time of the
- # call, regardless of where the marker's head is moved.
- #
- # * `properties` (optional) {Object} properties to associate with the marker.
- plantTail: ->
- @bufferMarker.plantTail()
-
- # Extended: Removes the marker's tail. After calling the marker's head position
- # will be reported as its current tail position until the tail is planted
- # again.
- #
- # * `properties` (optional) {Object} properties to associate with the marker.
- clearTail: (properties) ->
- @bufferMarker.clearTail(properties)
-
- ###
- Section: Private utility methods
- ###
-
- # Returns a {String} representation of the marker
- inspect: ->
- "Marker(id: #{@id}, bufferRange: #{@getBufferRange()})"
-
- destroyed: ->
- delete @displayBuffer.markers[@id]
- @emit 'destroyed' if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-destroy'
- @emitter.dispose()
-
- notifyObservers: ({textChanged}) ->
- textChanged ?= false
-
- newHeadBufferPosition = @getHeadBufferPosition()
- newHeadScreenPosition = @getHeadScreenPosition()
- newTailBufferPosition = @getTailBufferPosition()
- newTailScreenPosition = @getTailScreenPosition()
- isValid = @isValid()
-
- return if isValid is @wasValid and
- newHeadBufferPosition.isEqual(@oldHeadBufferPosition) and
- newHeadScreenPosition.isEqual(@oldHeadScreenPosition) and
- newTailBufferPosition.isEqual(@oldTailBufferPosition) and
- newTailScreenPosition.isEqual(@oldTailScreenPosition)
-
- changeEvent = {
- @oldHeadScreenPosition, newHeadScreenPosition,
- @oldTailScreenPosition, newTailScreenPosition,
- @oldHeadBufferPosition, newHeadBufferPosition,
- @oldTailBufferPosition, newTailBufferPosition,
- textChanged,
- isValid
- }
-
- @oldHeadBufferPosition = newHeadBufferPosition
- @oldHeadScreenPosition = newHeadScreenPosition
- @oldTailBufferPosition = newTailBufferPosition
- @oldTailScreenPosition = newTailScreenPosition
- @wasValid = isValid
-
- @emit 'changed', changeEvent if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-change', changeEvent
-
- getPixelRange: ->
- @displayBuffer.pixelRangeForScreenRange(@getScreenRange(), false)
-
-if Grim.includeDeprecatedAPIs
- EmitterMixin = require('emissary').Emitter
- EmitterMixin.includeInto(Marker)
-
- Marker::on = (eventName) ->
- switch eventName
- when 'changed'
- Grim.deprecate("Use Marker::onDidChange instead")
- when 'destroyed'
- Grim.deprecate("Use Marker::onDidDestroy instead")
- else
- Grim.deprecate("Marker::on is deprecated. Use documented event subscription methods instead.")
-
- EmitterMixin::on.apply(this, arguments)
-
- Marker::getAttributes = ->
- Grim.deprecate 'Use Marker::getProperties instead'
- @getProperties()
-
- Marker::setAttributes = (properties) ->
- Grim.deprecate 'Use Marker::setProperties instead'
- @setProperties(properties)
-
- Marker::matchesAttributes = (attributes) ->
- Grim.deprecate 'Use Marker::matchesProperties instead'
- @matchesProperties(attributes)
diff --git a/src/menu-helpers.coffee b/src/menu-helpers.coffee
deleted file mode 100644
index aa346200c64..00000000000
--- a/src/menu-helpers.coffee
+++ /dev/null
@@ -1,54 +0,0 @@
-_ = require 'underscore-plus'
-
-ItemSpecificities = new WeakMap
-
-merge = (menu, item, itemSpecificity=Infinity) ->
- item = cloneMenuItem(item)
- ItemSpecificities.set(item, itemSpecificity) if itemSpecificity
- matchingItemIndex = findMatchingItemIndex(menu, item)
- matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1
-
- if matchingItem?
- if item.submenu?
- merge(matchingItem.submenu, submenuItem, itemSpecificity) for submenuItem in item.submenu
- else if itemSpecificity
- unless itemSpecificity < ItemSpecificities.get(matchingItem)
- menu[matchingItemIndex] = item
- else unless item.type is 'separator' and _.last(menu)?.type is 'separator'
- menu.push(item)
-
- return
-
-unmerge = (menu, item) ->
- matchingItemIndex = findMatchingItemIndex(menu, item)
- matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1
-
- if matchingItem?
- if item.submenu?
- unmerge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu
-
- unless matchingItem.submenu?.length > 0
- menu.splice(matchingItemIndex, 1)
-
-findMatchingItemIndex = (menu, {type, label, submenu}) ->
- return -1 if type is 'separator'
- for item, index in menu
- if normalizeLabel(item.label) is normalizeLabel(label) and item.submenu? is submenu?
- return index
- -1
-
-normalizeLabel = (label) ->
- return undefined unless label?
-
- if process.platform is 'darwin'
- label
- else
- label.replace(/\&/g, '')
-
-cloneMenuItem = (item) ->
- item = _.pick(item, 'type', 'label', 'enabled', 'visible', 'command', 'submenu', 'commandDetail')
- if item.submenu?
- item.submenu = item.submenu.map (submenuItem) -> cloneMenuItem(submenuItem)
- item
-
-module.exports = {merge, unmerge, normalizeLabel, cloneMenuItem}
diff --git a/src/menu-helpers.js b/src/menu-helpers.js
new file mode 100644
index 00000000000..d096f3f1c1e
--- /dev/null
+++ b/src/menu-helpers.js
@@ -0,0 +1,138 @@
+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
+};
diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee
index b5c9a80e3f2..b6b4720a38c 100644
--- a/src/menu-manager.coffee
+++ b/src/menu-manager.coffee
@@ -1,7 +1,7 @@
path = require 'path'
_ = require 'underscore-plus'
-ipc = require 'ipc'
+{ipcRenderer} = require 'electron'
CSON = require 'season'
fs = require 'fs-plus'
{Disposable} = require 'event-kit'
@@ -59,11 +59,17 @@ platformMenu = require('../package.json')?._atomMenu?.menu
# See {::add} for more info about adding menu's directly.
module.exports =
class MenuManager
- constructor: ({@resourcePath}) ->
+ constructor: ({@resourcePath, @keymapManager, @packageManager}) ->
+ @initialized = false
@pendingUpdateOperation = null
@template = []
- atom.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems()
- atom.packages.onDidActivateInitialPackages => @sortPackagesMenu()
+ @keymapManager.onDidLoadBundledKeymaps => @loadPlatformItems()
+ @packageManager.onDidActivateInitialPackages => @sortPackagesMenu()
+
+ initialize: ({@resourcePath}) ->
+ @keymapManager.onDidReloadKeymap => @update()
+ @update()
+ @initialized = true
# Public: Adds the given items to the application menu.
#
@@ -72,7 +78,7 @@ class MenuManager
# atom.menu.add [
# {
# label: 'Hello'
- # submenu : [{label: 'World!', command: 'hello:world'}]
+ # submenu : [{label: 'World!', id: 'World!', command: 'hello:world'}]
# }
# ]
# ```
@@ -83,11 +89,16 @@ class MenuManager
# * `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)
- @merge(@template, item) for item in items
+
+ for item in items
+ continue unless item.label? # TODO: Should we emit a warning here?
+ @merge(@template, item)
+
@update()
new Disposable => @remove(items)
@@ -95,6 +106,10 @@ class MenuManager
@unmerge(@template, item) for item in items
@update()
+ clear: ->
+ @template = []
+ @update()
+
# Should the binding for the given selector be included in the menu
# commands.
#
@@ -137,13 +152,27 @@ class MenuManager
# Public: Refreshes the currently visible menu.
update: ->
- clearImmediate(@pendingUpdateOperation) if @pendingUpdateOperation?
- @pendingUpdateOperation = setImmediate =>
+ 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 atom.keymaps.getKeyBindings() when @includeSelector(binding.selector)
+ 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?
@@ -162,29 +191,18 @@ class MenuManager
unmerge: (menu, item) ->
MenuHelpers.unmerge(menu, item)
- # OSX can't handle displaying accelerators for multiple keystrokes.
- # If they are sent across, it will stop processing accelerators for the rest
- # of the menu items.
- filterMultipleKeystroke: (keystrokesByCommand) ->
- filtered = {}
- for key, bindings of keystrokesByCommand
- for binding in bindings
- continue if binding.indexOf(' ') isnt -1
-
- filtered[key] ?= []
- filtered[key].push(binding)
- filtered
-
sendToBrowserProcess: (template, keystrokesByCommand) ->
- keystrokesByCommand = @filterMultipleKeystroke(keystrokesByCommand)
- ipc.send 'update-application-menu', template, keystrokesByCommand
+ ipcRenderer.send 'update-application-menu', template, keystrokesByCommand
# Get an {Array} of {String} classes for the given element.
classesForElement: (element) ->
- element?.classList.toString().split(' ') ? []
+ if classList = element?.classList
+ Array::slice.apply(classList)
+ else
+ []
sortPackagesMenu: ->
- packagesMenu = _.find @template, ({label}) -> MenuHelpers.normalizeLabel(label) is 'Packages'
+ packagesMenu = _.find @template, ({id}) -> MenuHelpers.normalizeLabel(id) is 'Packages'
return unless packagesMenu?.submenu?
packagesMenu.submenu.sort (item1, item2) ->
diff --git a/src/menu-sort-helpers.js b/src/menu-sort-helpers.js
new file mode 100644
index 00000000000..9f04d57a6c6
--- /dev/null
+++ b/src/menu-sort-helpers.js
@@ -0,0 +1,184 @@
+// 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 };
diff --git a/src/model.coffee b/src/model.coffee
index 7b38c0eefea..94c06a76f67 100644
--- a/src/model.coffee
+++ b/src/model.coffee
@@ -1,16 +1,7 @@
-Grim = require 'grim'
-if Grim.includeDeprecatedAPIs
- module.exports = require('theorist').Model
- return
-
-PropertyAccessors = require 'property-accessors'
-
nextInstanceId = 1
module.exports =
class Model
- PropertyAccessors.includeInto(this)
-
@resetNextInstanceId: -> nextInstanceId = 1
alive: true
@@ -20,9 +11,7 @@ class Model
assignId: (id) ->
@id ?= id ? nextInstanceId++
-
- @::advisedAccessor 'id',
- set: (id) -> nextInstanceId = id + 1 if id >= nextInstanceId
+ nextInstanceId = id + 1 if id >= nextInstanceId
destroy: ->
return unless @isAlive()
diff --git a/src/module-cache.coffee b/src/module-cache.coffee
deleted file mode 100644
index e9245cf4027..00000000000
--- a/src/module-cache.coffee
+++ /dev/null
@@ -1,313 +0,0 @@
-Module = require 'module'
-path = require 'path'
-semver = require 'semver'
-
-# Extend semver.Range to memoize matched versions for speed
-class Range extends semver.Range
- constructor: ->
- super
- @matchedVersions = new Set()
- @unmatchedVersions = new Set()
-
- test: (version) ->
- return true if @matchedVersions.has(version)
- return false if @unmatchedVersions.has(version)
-
- matches = super
- if matches
- @matchedVersions.add(version)
- else
- @unmatchedVersions.add(version)
- matches
-
-nativeModules = process.binding('natives')
-
-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.
-if process.platform is 'win32'
- isAbsolute = (pathToCheck) ->
- pathToCheck and (pathToCheck[1] is ':' or (pathToCheck[0] is '\\' and pathToCheck[1] is '\\'))
-else
- isAbsolute = (pathToCheck) ->
- pathToCheck and pathToCheck[0] is '/'
-
-isCorePath = (pathToCheck) ->
- pathToCheck.startsWith(cache.resourcePathWithTrailingSlash)
-
-loadDependencies = (modulePath, rootPath, rootMetadata, moduleCache) ->
- fs = require 'fs-plus'
-
- for childPath in fs.listSync(path.join(modulePath, 'node_modules'))
- continue if path.basename(childPath) is '.bin'
- continue if rootPath is modulePath and rootMetadata.packageDependencies?.hasOwnProperty(path.basename(childPath))
-
- childMetadataPath = path.join(childPath, 'package.json')
- continue unless fs.isFileSync(childMetadataPath)
-
- childMetadata = JSON.parse(fs.readFileSync(childMetadataPath))
- if childMetadata?.version
- 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)
-
- return
-
-loadFolderCompatibility = (modulePath, rootPath, rootMetadata, moduleCache) ->
- fs = require 'fs-plus'
-
- metadataPath = path.join(modulePath, 'package.json')
- return unless fs.isFileSync(metadataPath)
-
- dependencies = JSON.parse(fs.readFileSync(metadataPath))?.dependencies ? {}
-
- for name, version of dependencies
- try
- new Range(version)
- catch error
- delete dependencies[name]
-
- onDirectory = (childPath) ->
- path.basename(childPath) isnt 'node_modules'
-
- extensions = ['.js', '.coffee', '.json', '.node']
- paths = {}
- onFile = (childPath) ->
- if path.extname(childPath) in extensions
- relativePath = path.relative(rootPath, path.dirname(childPath))
- paths[relativePath] = true
- fs.traverseTreeSync(modulePath, onFile, onDirectory)
-
- paths = Object.keys(paths)
- if paths.length > 0 and Object.keys(dependencies).length > 0
- moduleCache.folders.push({paths, dependencies})
-
- for childPath in fs.listSync(path.join(modulePath, 'node_modules'))
- continue if path.basename(childPath) is '.bin'
- continue if rootPath is modulePath and rootMetadata.packageDependencies?.hasOwnProperty(path.basename(childPath))
-
- loadFolderCompatibility(childPath, rootPath, rootMetadata, moduleCache)
-
- return
-
-loadExtensions = (modulePath, rootPath, rootMetadata, moduleCache) ->
- fs = require 'fs-plus'
- extensions = ['.js', '.coffee', '.json', '.node']
- nodeModulesPath = path.join(rootPath, 'node_modules')
-
- onFile = (filePath) ->
- filePath = path.relative(rootPath, filePath)
- segments = filePath.split(path.sep)
- return if 'test' in segments
- return if 'tests' in segments
- return if 'spec' in segments
- return if 'specs' in segments
- return if segments.length > 1 and not (segments[0] in ['exports', 'lib', 'node_modules', 'src', 'static', 'vendor'])
-
- extension = path.extname(filePath)
- if extension in extensions
- moduleCache.extensions[extension] ?= []
- moduleCache.extensions[extension].push(filePath)
-
- onDirectory = (childPath) ->
- # Don't include extensions from bundled packages
- # These are generated and stored in the package's own metadata cache
- if rootMetadata.name is 'atom'
- parentPath = path.dirname(childPath)
- if parentPath is nodeModulesPath
- packageName = path.basename(childPath)
- return false if rootMetadata.packageDependencies?.hasOwnProperty(packageName)
-
- true
-
- fs.traverseTreeSync(rootPath, onFile, onDirectory)
-
- return
-
-satisfies = (version, rawRange) ->
- unless parsedRange = cache.ranges[rawRange]
- parsedRange = new Range(rawRange)
- cache.ranges[rawRange] = parsedRange
- parsedRange.test(version)
-
-resolveFilePath = (relativePath, parentModule) ->
- return unless relativePath
- return unless parentModule?.filename
- return unless relativePath[0] is '.' or isAbsolute(relativePath)
-
- resolvedPath = path.resolve(path.dirname(parentModule.filename), relativePath)
- return unless isCorePath(resolvedPath)
-
- extension = path.extname(resolvedPath)
- if extension
- return resolvedPath if cache.extensions[extension]?.has(resolvedPath)
- else
- for extension, paths of cache.extensions
- resolvedPathWithExtension = "#{resolvedPath}#{extension}"
- return resolvedPathWithExtension if paths.has(resolvedPathWithExtension)
-
- return
-
-resolveModulePath = (relativePath, parentModule) ->
- return unless relativePath
- return unless parentModule?.filename
-
- return if nativeModules.hasOwnProperty(relativePath)
- return if relativePath[0] is '.'
- return if isAbsolute(relativePath)
-
- folderPath = path.dirname(parentModule.filename)
-
- range = cache.folders[folderPath]?[relativePath]
- unless range?
- if builtinPath = cache.builtins[relativePath]
- return builtinPath
- else
- return
-
- candidates = cache.dependencies[relativePath]
- return unless candidates?
-
- for version, resolvedPath of candidates
- if Module._cache.hasOwnProperty(resolvedPath) or isCorePath(resolvedPath)
- return resolvedPath if satisfies(version, range)
-
- return
-
-registerBuiltins = (devMode) ->
- if devMode or not cache.resourcePath.startsWith("#{process.resourcesPath}#{path.sep}")
- fs = require 'fs-plus'
- atomCoffeePath = path.join(cache.resourcePath, 'exports', 'atom.coffee')
- cache.builtins.atom = atomCoffeePath if fs.isFileSync(atomCoffeePath)
- cache.builtins.atom ?= path.join(cache.resourcePath, 'exports', 'atom.js')
-
- atomShellRoot = path.join(process.resourcesPath, 'atom.asar')
-
- commonRoot = path.join(atomShellRoot, 'common', 'api', 'lib')
- commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'screen', 'shell']
- for builtin in commonBuiltins
- cache.builtins[builtin] = path.join(commonRoot, "#{builtin}.js")
-
- rendererRoot = path.join(atomShellRoot, 'renderer', 'api', 'lib')
- rendererBuiltins = ['ipc', 'remote']
- for builtin in rendererBuiltins
- cache.builtins[builtin] = path.join(rendererRoot, "#{builtin}.js")
-
-if cache.debug
- cache.findPathCount = 0
- cache.findPathTime = 0
- cache.loadCount = 0
- cache.requireTime = 0
- global.moduleCache = cache
-
- originalLoad = Module::load
- Module::load = ->
- cache.loadCount++
- originalLoad.apply(this, arguments)
-
- originalRequire = Module::require
- Module::require = ->
- startTime = Date.now()
- exports = originalRequire.apply(this, arguments)
- cache.requireTime += Date.now() - startTime
- exports
-
- originalFindPath = Module._findPath
- Module._findPath = (request, paths) ->
- cacheKey = JSON.stringify({request, paths})
- cache.findPathCount++ unless Module._pathCache[cacheKey]
-
- startTime = Date.now()
- foundPath = originalFindPath.apply(global, arguments)
- cache.findPathTime += Date.now() - startTime
- foundPath
-
-exports.create = (modulePath) ->
- fs = require 'fs-plus'
-
- modulePath = fs.realpathSync(modulePath)
- metadataPath = path.join(modulePath, 'package.json')
- metadata = JSON.parse(fs.readFileSync(metadataPath))
-
- 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))
-
- return
-
-exports.register = ({resourcePath, devMode}={}) ->
- return if cache.registered
-
- originalResolveFilename = Module._resolveFilename
- Module._resolveFilename = (relativePath, parentModule) ->
- resolvedPath = resolveModulePath(relativePath, parentModule)
- resolvedPath ?= resolveFilePath(relativePath, parentModule)
- resolvedPath ? originalResolveFilename(relativePath, parentModule)
-
- cache.registered = true
- cache.resourcePath = resourcePath
- cache.resourcePathWithTrailingSlash = "#{resourcePath}#{path.sep}"
- registerBuiltins(devMode)
-
- return
-
-exports.add = (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.
-
- unless metadata?
- try
- metadata = require("#{directoryPath}#{path.sep}package.json")
- catch error
- return
-
- cacheToAdd = metadata?._atomModuleCache
- return unless cacheToAdd?
-
- for dependency in cacheToAdd.dependencies ? []
- cache.dependencies[dependency.name] ?= {}
- cache.dependencies[dependency.name][dependency.version] ?= "#{directoryPath}#{path.sep}#{dependency.path}"
-
- for entry in cacheToAdd.folders ? []
- for folderPath in entry.paths
- if folderPath
- cache.folders["#{directoryPath}#{path.sep}#{folderPath}"] = entry.dependencies
- else
- cache.folders[directoryPath] = entry.dependencies
-
- for extension, paths of cacheToAdd.extensions
- cache.extensions[extension] ?= new Set()
- for filePath in paths
- cache.extensions[extension].add("#{directoryPath}#{path.sep}#{filePath}")
-
- return
-
-exports.cache = cache
diff --git a/src/module-cache.js b/src/module-cache.js
new file mode 100644
index 00000000000..4caa115219a
--- /dev/null
+++ b/src/module-cache.js
@@ -0,0 +1,376 @@
+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;
diff --git a/src/module-utils.js b/src/module-utils.js
new file mode 100644
index 00000000000..dc2173e6f3b
--- /dev/null
+++ b/src/module-utils.js
@@ -0,0 +1,21 @@
+// 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;
diff --git a/src/native-compile-cache.js b/src/native-compile-cache.js
new file mode 100644
index 00000000000..3f82e108e84
--- /dev/null
+++ b/src/native-compile-cache.js
@@ -0,0 +1,129 @@
+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();
diff --git a/src/native-watcher-registry.js b/src/native-watcher-registry.js
new file mode 100644
index 00000000000..54f2cdcd4b1
--- /dev/null
+++ b/src/native-watcher-registry.js
@@ -0,0 +1,447 @@
+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 };
diff --git a/src/notification-manager.coffee b/src/notification-manager.coffee
deleted file mode 100644
index f4ebd97db48..00000000000
--- a/src/notification-manager.coffee
+++ /dev/null
@@ -1,91 +0,0 @@
-{Emitter, Disposable} = require 'event-kit'
-Notification = require '../src/notification'
-
-# Public: A notification manager used to create {Notification}s to be shown
-# to the user.
-module.exports =
-class NotificationManager
- constructor: ->
- @notifications = []
- @emitter = new Emitter
-
- ###
- Section: Events
- ###
-
- # Public: Invoke the given callback after a notification has been added.
- #
- # * `callback` {Function} to be called after the notification is added.
- # * `notification` The {Notification} that was added.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidAddNotification: (callback) ->
- @emitter.on 'did-add-notification', callback
-
- ###
- Section: Adding Notifications
- ###
-
- # Public: Add a success notification.
- #
- # * `message` A {String} message
- # * `options` An options {Object} with optional keys such as:
- # * `detail` A {String} with additional details about the notification
- addSuccess: (message, options) ->
- @addNotification(new Notification('success', message, options))
-
- # Public: Add an informational notification.
- #
- # * `message` A {String} message
- # * `options` An options {Object} with optional keys such as:
- # * `detail` A {String} with additional details about the notification
- addInfo: (message, options) ->
- @addNotification(new Notification('info', message, options))
-
- # Public: Add a warning notification.
- #
- # * `message` A {String} message
- # * `options` An options {Object} with optional keys such as:
- # * `detail` A {String} with additional details about the notification
- addWarning: (message, options) ->
- @addNotification(new Notification('warning', message, options))
-
- # Public: Add an error notification.
- #
- # * `message` A {String} message
- # * `options` An options {Object} with optional keys such as:
- # * `detail` A {String} with additional details about the notification
- addError: (message, options) ->
- @addNotification(new Notification('error', message, options))
-
- # Public: Add a fatal error notification.
- #
- # * `message` A {String} message
- # * `options` An options {Object} with optional keys such as:
- # * `detail` A {String} with additional details about the notification
- addFatalError: (message, options) ->
- @addNotification(new Notification('fatal', message, options))
-
- add: (type, message, options) ->
- @addNotification(new Notification(type, message, options))
-
- addNotification: (notification) ->
- @notifications.push(notification)
- @emitter.emit('did-add-notification', notification)
- notification
-
- ###
- Section: Getting Notifications
- ###
-
- # Public: Get all the notifications.
- #
- # Returns an {Array} of {Notifications}s.
- getNotifications: -> @notifications.slice()
-
- ###
- Section: Managing Notifications
- ###
-
- clear: ->
- @notifications = []
diff --git a/src/notification-manager.js b/src/notification-manager.js
new file mode 100644
index 00000000000..482515304d9
--- /dev/null
+++ b/src/notification-manager.js
@@ -0,0 +1,218 @@
+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');
+ }
+};
diff --git a/src/notification.coffee b/src/notification.coffee
deleted file mode 100644
index 9dfffc59ad6..00000000000
--- a/src/notification.coffee
+++ /dev/null
@@ -1,57 +0,0 @@
-{Emitter} = require 'event-kit'
-
-# Public: A notification to the user containing a message and type.
-module.exports =
-class Notification
- constructor: (@type, @message, @options={}) ->
- @emitter = new Emitter
- @timestamp = new Date()
- @dismissed = true
- @dismissed = false if @isDismissable()
- @displayed = false
-
- onDidDismiss: (callback) ->
- @emitter.on 'did-dismiss', callback
-
- onDidDisplay: (callback) ->
- @emitter.on 'did-display', callback
-
- getOptions: -> @options
-
- # Public: Retrieves the {String} type.
- getType: -> @type
-
- # Public: Retrieves the {String} message.
- getMessage: -> @message
-
- getTimestamp: -> @timestamp
-
- getDetail: -> @options.detail
-
- isEqual: (other) ->
- @getMessage() is other.getMessage() \
- and @getType() is other.getType() \
- and @getDetail() is other.getDetail()
-
- dismiss: ->
- return unless @isDismissable() and not @isDismissed()
- @dismissed = true
- @emitter.emit 'did-dismiss', this
-
- isDismissed: -> @dismissed
-
- isDismissable: -> !!@options.dismissable
-
- wasDisplayed: -> @displayed
-
- setDisplayed: (@displayed) ->
- @emitter.emit 'did-display', this
-
- getIcon: ->
- return @options.icon if @options.icon?
- switch @type
- when 'fatal' then 'bug'
- when 'error' then 'flame'
- when 'warning' then 'alert'
- when 'info' then 'info'
- when 'success' then 'check'
diff --git a/src/notification.js b/src/notification.js
new file mode 100644
index 00000000000..88e82d7ecd1
--- /dev/null
+++ b/src/notification.js
@@ -0,0 +1,128 @@
+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';
+ }
+ }
+};
diff --git a/src/null-grammar.js b/src/null-grammar.js
new file mode 100644
index 00000000000..81a7cd68eee
--- /dev/null
+++ b/src/null-grammar.js
@@ -0,0 +1,42 @@
+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() {}
diff --git a/src/overlay-manager.coffee b/src/overlay-manager.coffee
index 21a484fbe3f..c8c7f4b275c 100644
--- a/src/overlay-manager.coffee
+++ b/src/overlay-manager.coffee
@@ -1,6 +1,9 @@
+ElementResizeDetector = require('element-resize-detector')
+elementResizeDetector = null
+
module.exports =
class OverlayManager
- constructor: (@presenter, @container) ->
+ constructor: (@presenter, @container, @views) ->
@overlaysById = {}
render: (state) ->
@@ -12,6 +15,7 @@ class OverlayManager
unless state.content.overlays.hasOwnProperty(id)
delete @overlaysById[id]
overlayNode.remove()
+ elementResizeDetector.uninstall(overlayNode)
shouldUpdateOverlay: (decorationId, overlay) ->
cachedOverlay = @overlaysById[decorationId]
@@ -19,26 +23,30 @@ class OverlayManager
cachedOverlay.pixelPosition?.top isnt overlay.pixelPosition?.top or
cachedOverlay.pixelPosition?.left isnt overlay.pixelPosition?.left
- measureOverlays: ->
- for decorationId, {itemView} of @overlaysById
- @measureOverlay(decorationId, itemView)
-
measureOverlay: (decorationId, itemView) ->
contentMargin = parseInt(getComputedStyle(itemView)['margin-left']) ? 0
@presenter.setOverlayDimensions(decorationId, itemView.offsetWidth, itemView.offsetHeight, contentMargin)
- renderOverlay: (state, decorationId, {item, pixelPosition}) ->
- itemView = atom.views.getView(item)
+ 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) if overlayNode.childNodes.length is 0
+ 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)
diff --git a/src/package-manager.coffee b/src/package-manager.coffee
deleted file mode 100644
index 8d2f4d66358..00000000000
--- a/src/package-manager.coffee
+++ /dev/null
@@ -1,468 +0,0 @@
-path = require 'path'
-
-_ = require 'underscore-plus'
-{Emitter} = require 'event-kit'
-fs = require 'fs-plus'
-Q = require 'q'
-Grim = require 'grim'
-
-ServiceHub = require 'service-hub'
-Package = require './package'
-ThemePackage = require './theme-package'
-
-# 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: ({configDirPath, @devMode, safeMode, @resourcePath}) ->
- @emitter = new Emitter
- @packageDirPaths = []
- unless safeMode
- if @devMode
- @packageDirPaths.push(path.join(configDirPath, "dev", "packages"))
- @packageDirPaths.push(path.join(configDirPath, "packages"))
-
- @loadedPackages = {}
- @activePackages = {}
- @packageStates = {}
- @serviceHub = new ServiceHub
-
- @packageActivators = []
- @registerPackageActivator(this, ['atom', 'textmate'])
-
- ###
- 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) ->
- @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) ->
- @emitter.on 'did-activate-initial-packages', callback
-
- # 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) ->
- @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) ->
- @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) ->
- @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) ->
- @emitter.on 'did-unload-package', callback
-
- ###
- Section: Package system data
- ###
-
- # Public: Get the path to the apm command.
- #
- # Return a {String} file path to apm.
- getApmPath: ->
- return @apmPath if @apmPath?
-
- commandName = 'apm'
- commandName += '.cmd' if process.platform is 'win32'
- apmRoot = path.join(process.resourcesPath, 'app', 'apm')
- @apmPath = path.join(apmRoot, 'bin', commandName)
- unless fs.isFileSync(@apmPath)
- @apmPath = path.join(apmRoot, 'node_modules', 'atom-package-manager', 'bin', commandName)
- @apmPath
-
- # Public: Get the paths being used to look for packages.
- #
- # Returns an {Array} of {String} directory paths.
- getPackageDirPaths: ->
- _.clone(@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) ->
- return name if fs.isDirectorySync(name)
-
- packagePath = fs.resolve(@packageDirPaths..., name)
- return packagePath if fs.isDirectorySync(packagePath)
-
- packagePath = path.join(@resourcePath, 'node_modules', name)
- return packagePath if @hasAtomEngine(packagePath)
-
- # Public: Is the package with the given name bundled with Atom?
- #
- # * `name` - The {String} package name.
- #
- # Returns a {Boolean}.
- isBundledPackage: (name) ->
- @getPackageDependencies().hasOwnProperty(name)
-
- ###
- Section: Enabling and disabling packages
- ###
-
- # Public: Enable the package with the given name.
- #
- # Returns the {Package} that was enabled or null if it isn't loaded.
- enablePackage: (name) ->
- pack = @loadPackage(name)
- pack?.enable()
- pack
-
- # Public: Disable the package with the given name.
- #
- # Returns the {Package} that was disabled or null if it isn't loaded.
- disablePackage: (name) ->
- pack = @loadPackage(name)
- pack?.disable()
- pack
-
- # Public: Is the package with the given name disabled?
- #
- # * `name` - The {String} package name.
- #
- # Returns a {Boolean}.
- isPackageDisabled: (name) ->
- _.include(atom.config.get('core.disabledPackages') ? [], name)
-
- ###
- Section: Accessing active packages
- ###
-
- # Public: Get an {Array} of all the active {Package}s.
- getActivePackages: ->
- _.values(@activePackages)
-
- # Public: Get the active {Package} with the given name.
- #
- # * `name` - The {String} package name.
- #
- # Returns a {Package} or undefined.
- getActivePackage: (name) ->
- @activePackages[name]
-
- # Public: Is the {Package} with the given name active?
- #
- # * `name` - The {String} package name.
- #
- # Returns a {Boolean}.
- isPackageActive: (name) ->
- @getActivePackage(name)?
-
- ###
- Section: Accessing loaded packages
- ###
-
- # Public: Get an {Array} of all the loaded {Package}s
- getLoadedPackages: ->
- _.values(@loadedPackages)
-
- # Get packages for a certain package type
- #
- # * `types` an {Array} of {String}s like ['atom', 'textmate'].
- getLoadedPackagesForTypes: (types) ->
- pack for pack in @getLoadedPackages() when pack.getType() in types
-
- # Public: Get the loaded {Package} with the given name.
- #
- # * `name` - The {String} package name.
- #
- # Returns a {Package} or undefined.
- getLoadedPackage: (name) ->
- @loadedPackages[name]
-
- # Public: Is the package with the given name loaded?
- #
- # * `name` - The {String} package name.
- #
- # Returns a {Boolean}.
- isPackageLoaded: (name) ->
- @getLoadedPackage(name)?
-
- ###
- Section: Accessing available packages
- ###
-
- # Public: Get an {Array} of {String}s of all the available package paths.
- getAvailablePackagePaths: ->
- packagePaths = []
-
- for packageDirPath in @packageDirPaths
- for packagePath in fs.listSync(packageDirPath)
- packagePaths.push(packagePath) if fs.isDirectorySync(packagePath)
-
- packagesPath = path.join(@resourcePath, 'node_modules')
- for packageName, packageVersion of @getPackageDependencies()
- packagePath = path.join(packagesPath, packageName)
- packagePaths.push(packagePath) if fs.isDirectorySync(packagePath)
-
- _.uniq(packagePaths)
-
- # Public: Get an {Array} of {String}s of all the available package names.
- getAvailablePackageNames: ->
- _.uniq _.map @getAvailablePackagePaths(), (packagePath) -> path.basename(packagePath)
-
- # Public: Get an {Array} of {String}s of all the available package metadata.
- getAvailablePackageMetadata: ->
- packages = []
- for packagePath in @getAvailablePackagePaths()
- name = path.basename(packagePath)
- metadata = @getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true)
- packages.push(metadata)
- packages
-
- ###
- Section: Private
- ###
-
- getPackageState: (name) ->
- @packageStates[name]
-
- setPackageState: (name, state) ->
- @packageStates[name] = state
-
- getPackageDependencies: ->
- unless @packageDependencies?
- try
- @packageDependencies = require('../package.json')?.packageDependencies
- @packageDependencies ?= {}
-
- @packageDependencies
-
- hasAtomEngine: (packagePath) ->
- metadata = Package.loadMetadata(packagePath, true)
- metadata?.engines?.atom?
-
- unobserveDisabledPackages: ->
- @disabledPackagesSubscription?.dispose()
- @disabledPackagesSubscription = null
-
- observeDisabledPackages: ->
- @disabledPackagesSubscription ?= atom.config.onDidChange 'core.disabledPackages', ({newValue, oldValue}) =>
- packagesToEnable = _.difference(oldValue, newValue)
- packagesToDisable = _.difference(newValue, oldValue)
-
- @deactivatePackage(packageName) for packageName in packagesToDisable when @getActivePackage(packageName)
- @activatePackage(packageName) for packageName in packagesToEnable
- null
-
- 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'
-
- # TODO: remove after a few atom versions.
- @uninstallAutocompletePlus()
-
- packagePaths = @getAvailablePackagePaths()
- packagePaths = packagePaths.filter (packagePath) => not @isPackageDisabled(path.basename(packagePath))
- packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath)
- @loadPackage(packagePath) for packagePath in packagePaths
- @emit 'loaded' if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-load-initial-packages'
-
- loadPackage: (nameOrPath) ->
- return pack if pack = @getLoadedPackage(nameOrPath)
-
- if packagePath = @resolvePackagePath(nameOrPath)
- name = path.basename(nameOrPath)
- return pack if pack = @getLoadedPackage(name)
-
- try
- metadata = Package.loadMetadata(packagePath) ? {}
- catch error
- @handleMetadataError(error, packagePath)
- return null
-
- if metadata.theme
- pack = new ThemePackage(packagePath, metadata)
- else
- pack = new Package(packagePath, metadata)
- pack.load()
- @loadedPackages[pack.name] = pack
- @emitter.emit 'did-load-package', pack
- return pack
- else
- console.warn "Could not resolve '#{nameOrPath}' to a package path"
- null
-
- unloadPackages: ->
- @unloadPackage(name) for name in _.keys(@loadedPackages)
- null
-
- unloadPackage: (name) ->
- if @isPackageActive(name)
- throw new Error("Tried to unload active package '#{name}'")
-
- if pack = @getLoadedPackage(name)
- delete @loadedPackages[pack.name]
- @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: ->
- promises = []
- for [activator, types] in @packageActivators
- packages = @getLoadedPackagesForTypes(types)
- promises = promises.concat(activator.activatePackages(packages))
- Q.all(promises).then =>
- @emit 'activated' if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-activate-initial-packages'
-
- # another type of package manager can handle other package types.
- # See ThemeManager
- registerPackageActivator: (activator, types) ->
- @packageActivators.push([activator, types])
-
- activatePackages: (packages) ->
- promises = []
- atom.config.transact =>
- for pack in packages
- promise = @activatePackage(pack.name)
- promises.push(promise) unless pack.hasActivationCommands()
- return
- @observeDisabledPackages()
- promises
-
- # Activate a single package by name
- activatePackage: (name) ->
- if pack = @getActivePackage(name)
- Q(pack)
- else if pack = @loadPackage(name)
- pack.activate().then =>
- @activePackages[pack.name] = pack
- @emitter.emit 'did-activate-package', pack
- pack
- else
- Q.reject(new Error("Failed to load package '#{name}'"))
-
- # Deactivate all packages
- deactivatePackages: ->
- atom.config.transact =>
- @deactivatePackage(pack.name) for pack in @getLoadedPackages()
- return
- @unobserveDisabledPackages()
-
- # Deactivate the package with the given name
- deactivatePackage: (name) ->
- pack = @getLoadedPackage(name)
- if @isPackageActive(name)
- @setPackageState(pack.name, state) if state = pack.serialize?()
- pack.deactivate()
- delete @activePackages[pack.name]
- @emitter.emit 'did-deactivate-package', pack
-
- handleMetadataError: (error, packagePath) ->
- metadataPath = path.join(packagePath, 'package.json')
- detail = "#{error.message} in #{metadataPath}"
- stack = "#{error.stack}\n at #{metadataPath}:1:1"
- message = "Failed to load the #{path.basename(packagePath)} package"
- atom.notifications.addError(message, {stack, detail, dismissable: true})
-
- # TODO: remove these autocomplete-plus specific helpers after a few versions.
- uninstallAutocompletePlus: ->
- packageDir = null
- devDir = path.join("dev", "packages")
- for packageDirPath in @packageDirPaths
- if not packageDirPath.endsWith(devDir)
- packageDir = packageDirPath
- break
-
- if packageDir?
- dirsToRemove = [
- path.join(packageDir, 'autocomplete-plus')
- path.join(packageDir, 'autocomplete-atom-api')
- path.join(packageDir, 'autocomplete-css')
- path.join(packageDir, 'autocomplete-html')
- path.join(packageDir, 'autocomplete-snippets')
- ]
- for dirToRemove in dirsToRemove
- @uninstallDirectory(dirToRemove)
- return
-
- uninstallDirectory: (directory) ->
- symlinkPromise = new Promise (resolve) ->
- fs.isSymbolicLink directory, (isSymLink) -> resolve(isSymLink)
-
- dirPromise = new Promise (resolve) ->
- fs.isDirectory directory, (isDir) -> resolve(isDir)
-
- Promise.all([symlinkPromise, dirPromise]).then (values) ->
- [isSymLink, isDir] = values
- if not isSymLink and isDir
- fs.remove directory, ->
-
-if Grim.includeDeprecatedAPIs
- EmitterMixin = require('emissary').Emitter
- EmitterMixin.includeInto(PackageManager)
-
- PackageManager::on = (eventName) ->
- switch eventName
- when 'loaded'
- Grim.deprecate 'Use PackageManager::onDidLoadInitialPackages instead'
- when 'activated'
- Grim.deprecate 'Use PackageManager::onDidActivateInitialPackages instead'
- else
- Grim.deprecate 'PackageManager::on is deprecated. Use event subscription methods instead.'
- EmitterMixin::on.apply(this, arguments)
-
- PackageManager::onDidLoadAll = (callback) ->
- Grim.deprecate("Use `::onDidLoadInitialPackages` instead.")
- @onDidLoadInitialPackages(callback)
-
- PackageManager::onDidActivateAll = (callback) ->
- Grim.deprecate("Use `::onDidActivateInitialPackages` instead.")
- @onDidActivateInitialPackages(callback)
diff --git a/src/package-manager.js b/src/package-manager.js
new file mode 100644
index 00000000000..163e3ca9d5d
--- /dev/null
+++ b/src/package-manager.js
@@ -0,0 +1,1034 @@
+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;
+ }
+};
diff --git a/src/package-transpilation-registry.js b/src/package-transpilation-registry.js
new file mode 100644
index 00000000000..9cc189101cb
--- /dev/null
+++ b/src/package-transpilation-registry.js
@@ -0,0 +1,212 @@
+'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;
diff --git a/src/package.coffee b/src/package.coffee
deleted file mode 100644
index 65c7af8229d..00000000000
--- a/src/package.coffee
+++ /dev/null
@@ -1,621 +0,0 @@
-path = require 'path'
-normalizePackageData = null
-
-_ = require 'underscore-plus'
-async = require 'async'
-CSON = require 'season'
-fs = require 'fs-plus'
-{Emitter, CompositeDisposable} = require 'event-kit'
-Q = require 'q'
-{includeDeprecatedAPIs, deprecate} = require 'grim'
-
-ModuleCache = require './module-cache'
-ScopedProperties = require './scoped-properties'
-
-packagesCache = require('../package.json')?._atomPackages ? {}
-
-# Loads and activates a package's main module and resources such as
-# stylesheets, keymaps, grammar, editor properties, and menus.
-module.exports =
-class Package
- @isBundledPackagePath: (packagePath) ->
- if atom.packages.devMode
- return false unless atom.packages.resourcePath.startsWith("#{process.resourcesPath}#{path.sep}")
-
- @resourcePathWithTrailingSlash ?= "#{atom.packages.resourcePath}#{path.sep}"
- packagePath?.startsWith(@resourcePathWithTrailingSlash)
-
- @normalizeMetadata: (metadata) ->
- unless metadata?._id
- normalizePackageData ?= require 'normalize-package-data'
- normalizePackageData(metadata)
- if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
- metadata.repository.url = metadata.repository.url.replace(/^git\+/, '')
-
- @loadMetadata: (packagePath, ignoreErrors=false) ->
- packageName = path.basename(packagePath)
- if @isBundledPackagePath(packagePath)
- metadata = packagesCache[packageName]?.metadata
- unless metadata?
- if metadataPath = CSON.resolve(path.join(packagePath, 'package'))
- try
- metadata = CSON.readFileSync(metadataPath)
- @normalizeMetadata(metadata)
- catch error
- throw error unless ignoreErrors
-
- metadata ?= {}
- metadata.name = packageName
-
- if includeDeprecatedAPIs and metadata.stylesheetMain?
- deprecate("Use the `mainStyleSheet` key instead of `stylesheetMain` in the `package.json` of `#{packageName}`", {packageName})
- metadata.mainStyleSheet = metadata.stylesheetMain
-
- if includeDeprecatedAPIs and metadata.stylesheets?
- deprecate("Use the `styleSheets` key instead of `stylesheets` in the `package.json` of `#{packageName}`", {packageName})
- metadata.styleSheets = metadata.stylesheets
-
- metadata
-
- keymaps: null
- menus: null
- stylesheets: null
- stylesheetDisposables: null
- grammars: null
- settings: null
- mainModulePath: null
- resolvedMainModulePath: false
- mainModule: null
-
- ###
- Section: Construction
- ###
-
- constructor: (@path, @metadata) ->
- @emitter = new Emitter
- @metadata ?= Package.loadMetadata(@path)
- @bundledPackage = Package.isBundledPackagePath(@path)
- @name = @metadata?.name ? path.basename(@path)
- ModuleCache.add(@path, @metadata)
- @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) ->
- @emitter.on 'did-deactivate', callback
-
- ###
- Section: Instance Methods
- ###
-
- enable: ->
- atom.config.removeAtKeyPath('core.disabledPackages', @name)
-
- disable: ->
- atom.config.pushAtKeyPath('core.disabledPackages', @name)
-
- isTheme: ->
- @metadata?.theme?
-
- measure: (key, fn) ->
- startTime = Date.now()
- value = fn()
- @[key] = Date.now() - startTime
- value
-
- getType: -> 'atom'
-
- getStyleSheetPriority: -> 0
-
- load: ->
- @measure 'loadTime', =>
- try
- @loadKeymaps()
- @loadMenus()
- @loadStylesheets()
- @settingsPromise = @loadSettings()
- @requireMainModule() unless @hasActivationCommands()
- catch error
- @handleError("Failed to load the #{@name} package", error)
- this
-
- reset: ->
- @stylesheets = []
- @keymaps = []
- @menus = []
- @grammars = []
- @settings = []
-
- activate: ->
- @grammarsPromise ?= @loadGrammars()
-
- unless @activationDeferred?
- @activationDeferred = Q.defer()
- @measure 'activateTime', =>
- try
- @activateResources()
- if @hasActivationCommands()
- @subscribeToActivationCommands()
- else
- @activateNow()
- catch error
- @handleError("Failed to activate the #{@name} package", error)
-
- Q.all([@grammarsPromise, @settingsPromise, @activationDeferred.promise])
-
- activateNow: ->
- try
- @activateConfig()
- @activateStylesheets()
- if @requireMainModule()
- @mainModule.activate?(atom.packages.getPackageState(@name) ? {})
- @mainActivated = true
- @activateServices()
- catch error
- @handleError("Failed to activate the #{@name} package", error)
-
- @activationDeferred?.resolve()
-
- activateConfig: ->
- return if @configActivated
-
- @requireMainModule()
- if @mainModule?
- if @mainModule.config? and typeof @mainModule.config is 'object'
- atom.config.setSchema @name, {type: 'object', properties: @mainModule.config}
- else if includeDeprecatedAPIs and @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object'
- deprecate("""Use a config schema instead. See the configuration section
- of https://atom.io/docs/latest/hacking-atom-package-word-count and
- https://atom.io/docs/api/latest/Config for more details""", {packageName: @name})
- atom.config.setDefaults(@name, @mainModule.configDefaults)
- @mainModule.activateConfig?()
- @configActivated = true
-
- activateStylesheets: ->
- return if @stylesheetsActivated
-
- @stylesheetDisposables = new CompositeDisposable
-
- priority = @getStyleSheetPriority()
- for [sourcePath, source] in @stylesheets
- if match = path.basename(sourcePath).match(/[^.]*\.([^.]*)\./)
- context = match[1]
- else if @metadata.theme is 'syntax'
- context = 'atom-text-editor'
- else
- context = undefined
-
- @stylesheetDisposables.add(atom.styles.addStyleSheet(source, {sourcePath, priority, context}))
- @stylesheetsActivated = true
-
- activateResources: ->
- @activationDisposables = new CompositeDisposable
- @activationDisposables.add(atom.keymaps.add(keymapPath, map)) for [keymapPath, map] in @keymaps
-
- for [menuPath, map] in @menus when map['context-menu']?
- try
- itemsBySelector = map['context-menu']
-
- if includeDeprecatedAPIs
- # Detect deprecated format for items object
- for key, value of itemsBySelector
- unless _.isArray(value)
- deprecate("""
- The context menu CSON format has changed. Please see
- https://atom.io/docs/api/latest/ContextMenuManager#context-menu-cson-format
- for more info.
- """, {packageName: @name})
- itemsBySelector = atom.contextMenu.convertLegacyItemsBySelector(itemsBySelector)
-
- @activationDisposables.add(atom.contextMenu.add(itemsBySelector))
- catch error
- if error.code is 'EBADSELECTOR'
- error.message += " in #{menuPath}"
- error.stack += "\n at #{menuPath}:1:1"
- throw error
-
- @activationDisposables.add(atom.menu.add(map['menu'])) for [menuPath, map] in @menus when map['menu']?
-
- unless @grammarsActivated
- grammar.activate() for grammar in @grammars
- @grammarsActivated = true
-
- settings.activate() for settings in @settings
- @settingsActivated = true
-
- activateServices: ->
- for name, {versions} of @metadata.providedServices
- servicesByVersion = {}
- for version, methodName of versions
- if typeof @mainModule[methodName] is 'function'
- servicesByVersion[version] = @mainModule[methodName]()
- @activationDisposables.add atom.packages.serviceHub.provide(name, servicesByVersion)
-
- for name, {versions} of @metadata.consumedServices
- for version, methodName of versions
- if typeof @mainModule[methodName] is 'function'
- @activationDisposables.add atom.packages.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule))
- return
-
- loadKeymaps: ->
- if @bundledPackage and packagesCache[@name]?
- @keymaps = (["#{atom.packages.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of packagesCache[@name].keymaps)
- else
- @keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath) ? {}]
- return
-
- loadMenus: ->
- if @bundledPackage and packagesCache[@name]?
- @menus = (["#{atom.packages.resourcePath}#{path.sep}#{menuPath}", menuObject] for menuPath, menuObject of packagesCache[@name].menus)
- else
- @menus = @getMenuPaths().map (menuPath) -> [menuPath, CSON.readFileSync(menuPath) ? {}]
- return
-
- getKeymapPaths: ->
- keymapsDirPath = path.join(@path, 'keymaps')
- if @metadata.keymaps
- @metadata.keymaps.map (name) -> fs.resolve(keymapsDirPath, name, ['json', 'cson', ''])
- else
- fs.listSync(keymapsDirPath, ['cson', 'json'])
-
- getMenuPaths: ->
- menusDirPath = path.join(@path, 'menus')
- if @metadata.menus
- @metadata.menus.map (name) -> fs.resolve(menusDirPath, name, ['json', 'cson', ''])
- else
- fs.listSync(menusDirPath, ['cson', 'json'])
-
- loadStylesheets: ->
- @stylesheets = @getStylesheetPaths().map (stylesheetPath) ->
- [stylesheetPath, atom.themes.loadStylesheet(stylesheetPath, true)]
-
- getStylesheetsPath: ->
- if includeDeprecatedAPIs and fs.isDirectorySync(path.join(@path, 'stylesheets'))
- deprecate("Store package style sheets in the `styles/` directory instead of `stylesheets/` in the `#{@name}` package", packageName: @name)
- path.join(@path, 'stylesheets')
- else
- path.join(@path, 'styles')
-
- getStylesheetPaths: ->
- stylesheetDirPath = @getStylesheetsPath()
- if @metadata.mainStyleSheet
- [fs.resolve(@path, @metadata.mainStyleSheet)]
- else if @metadata.styleSheets
- @metadata.styleSheets.map (name) -> fs.resolve(stylesheetDirPath, name, ['css', 'less', ''])
- else if indexStylesheet = fs.resolve(@path, 'index', ['css', 'less'])
- [indexStylesheet]
- else
- fs.listSync(stylesheetDirPath, ['css', 'less'])
-
- loadGrammarsSync: ->
- return if @grammarsLoaded
-
- grammarsDirPath = path.join(@path, 'grammars')
- grammarPaths = fs.listSync(grammarsDirPath, ['json', 'cson'])
- for grammarPath in grammarPaths
- try
- grammar = atom.grammars.readGrammarSync(grammarPath)
- grammar.packageName = @name
- grammar.bundledPackage = @bundledPackage
- @grammars.push(grammar)
- grammar.activate()
- catch error
- console.warn("Failed to load grammar: #{grammarPath}", error.stack ? error)
-
- @grammarsLoaded = true
- @grammarsActivated = true
-
- loadGrammars: ->
- return Q() if @grammarsLoaded
-
- loadGrammar = (grammarPath, callback) =>
- atom.grammars.readGrammar grammarPath, (error, grammar) =>
- if error?
- detail = "#{error.message} in #{grammarPath}"
- stack = "#{error.stack}\n at #{grammarPath}:1:1"
- atom.notifications.addFatalError("Failed to load a #{@name} package grammar", {stack, detail, dismissable: true})
- else
- grammar.packageName = @name
- grammar.bundledPackage = @bundledPackage
- @grammars.push(grammar)
- grammar.activate() if @grammarsActivated
- callback()
-
- deferred = Q.defer()
- grammarsDirPath = path.join(@path, 'grammars')
- fs.exists grammarsDirPath, (grammarsDirExists) ->
- return deferred.resolve() unless grammarsDirExists
-
- fs.list grammarsDirPath, ['json', 'cson'], (error, grammarPaths=[]) ->
- async.each grammarPaths, loadGrammar, -> deferred.resolve()
- deferred.promise
-
- loadSettings: ->
- @settings = []
-
- loadSettingsFile = (settingsPath, callback) =>
- ScopedProperties.load settingsPath, (error, settings) =>
- if error?
- detail = "#{error.message} in #{settingsPath}"
- stack = "#{error.stack}\n at #{settingsPath}:1:1"
- atom.notifications.addFatalError("Failed to load the #{@name} package settings", {stack, detail, dismissable: true})
- else
- @settings.push(settings)
- settings.activate() if @settingsActivated
- callback()
-
- deferred = Q.defer()
-
- if includeDeprecatedAPIs and fs.isDirectorySync(path.join(@path, 'scoped-properties'))
- settingsDirPath = path.join(@path, 'scoped-properties')
- deprecate("Store package settings files in the `settings/` directory instead of `scoped-properties/`", packageName: @name)
- else
- settingsDirPath = path.join(@path, 'settings')
-
- fs.exists settingsDirPath, (settingsDirExists) ->
- return deferred.resolve() unless settingsDirExists
-
- fs.list settingsDirPath, ['json', 'cson'], (error, settingsPaths=[]) ->
- async.each settingsPaths, loadSettingsFile, -> deferred.resolve()
- deferred.promise
-
- serialize: ->
- if @mainActivated
- try
- @mainModule?.serialize?()
- catch e
- console.error "Error serializing package '#{@name}'", e.stack
-
- deactivate: ->
- @activationDeferred?.reject()
- @activationDeferred = null
- @activationCommandSubscriptions?.dispose()
- @deactivateResources()
- @deactivateConfig()
- if @mainActivated
- try
- @mainModule?.deactivate?()
- catch e
- console.error "Error deactivating package '#{@name}'", e.stack
- @emit 'deactivated' if includeDeprecatedAPIs
- @emitter.emit 'did-deactivate'
-
- deactivateConfig: ->
- @mainModule?.deactivateConfig?()
- @configActivated = false
-
- deactivateResources: ->
- grammar.deactivate() for grammar in @grammars
- settings.deactivate() for settings in @settings
- @stylesheetDisposables?.dispose()
- @activationDisposables?.dispose()
- @stylesheetsActivated = false
- @grammarsActivated = false
- @settingsActivated = false
-
- reloadStylesheets: ->
- oldSheets = _.clone(@stylesheets)
-
- try
- @loadStylesheets()
- catch error
- @handleError("Failed to reload the #{@name} package stylesheets", error)
-
- @stylesheetDisposables?.dispose()
- @stylesheetDisposables = new CompositeDisposable
- @stylesheetsActivated = false
- @activateStylesheets()
-
- requireMainModule: ->
- return @mainModule if @mainModuleRequired
- unless @isCompatible()
- console.warn """
- Failed to require the main module of '#{@name}' because it requires an incompatible native module.
- Run `apm rebuild` in the package directory to resolve.
- """
- return
- mainModulePath = @getMainModulePath()
- if fs.isFileSync(mainModulePath)
- @mainModuleRequired = true
- @mainModule = require(mainModulePath)
-
- getMainModulePath: ->
- return @mainModulePath if @resolvedMainModulePath
- @resolvedMainModulePath = true
-
- if @bundledPackage and packagesCache[@name]?
- if packagesCache[@name].main
- @mainModulePath = "#{atom.packages.resourcePath}#{path.sep}#{packagesCache[@name].main}"
- else
- @mainModulePath = null
- else
- mainModulePath =
- if @metadata.main
- path.join(@path, @metadata.main)
- else
- path.join(@path, 'index')
- @mainModulePath = fs.resolveExtension(mainModulePath, ["", _.keys(require.extensions)...])
-
- hasActivationCommands: ->
- for selector, commands of @getActivationCommands()
- return true if commands.length > 0
- false
-
- subscribeToActivationCommands: ->
- @activationCommandSubscriptions = new CompositeDisposable
- for selector, commands of @getActivationCommands()
- for command in commands
- do (selector, command) =>
- # Add dummy command so it appears in menu.
- # The real command will be registered on package activation
- try
- @activationCommandSubscriptions.add atom.commands.add selector, command, ->
- catch error
- if error.code is 'EBADSELECTOR'
- metadataPath = path.join(@path, 'package.json')
- error.message += " in #{metadataPath}"
- error.stack += "\n at #{metadataPath}:1:1"
- throw error
-
- @activationCommandSubscriptions.add atom.commands.onWillDispatch (event) =>
- return unless event.type is command
- currentTarget = event.target
- while currentTarget
- if currentTarget.webkitMatchesSelector(selector)
- @activationCommandSubscriptions.dispose()
- @activateNow()
- break
- currentTarget = currentTarget.parentElement
- return
- return
-
- getActivationCommands: ->
- return @activationCommands if @activationCommands?
-
- @activationCommands = {}
-
- if @metadata.activationCommands?
- for selector, commands of @metadata.activationCommands
- @activationCommands[selector] ?= []
- if _.isString(commands)
- @activationCommands[selector].push(commands)
- else if _.isArray(commands)
- @activationCommands[selector].push(commands...)
-
- if includeDeprecatedAPIs and @metadata.activationEvents?
- deprecate("""
- Use `activationCommands` instead of `activationEvents` in your package.json
- Commands should be grouped by selector as follows:
- ```json
- "activationCommands": {
- "atom-workspace": ["foo:bar", "foo:baz"],
- "atom-text-editor": ["foo:quux"]
- }
- ```
- """, {packageName: @name})
- if _.isArray(@metadata.activationEvents)
- for eventName in @metadata.activationEvents
- @activationCommands['atom-workspace'] ?= []
- @activationCommands['atom-workspace'].push(eventName)
- else if _.isString(@metadata.activationEvents)
- eventName = @metadata.activationEvents
- @activationCommands['atom-workspace'] ?= []
- @activationCommands['atom-workspace'].push(eventName)
- else
- for eventName, selector of @metadata.activationEvents
- selector ?= 'atom-workspace'
- @activationCommands[selector] ?= []
- @activationCommands[selector].push(eventName)
-
- @activationCommands
-
- # Does the given module path contain native code?
- isNativeModule: (modulePath) ->
- try
- fs.listSync(path.join(modulePath, 'build', 'Release'), ['.node']).length > 0
- catch error
- false
-
- # Get an array of all the native modules that this package depends on.
- # This will recurse through all dependencies.
- getNativeModuleDependencyPaths: ->
- nativeModulePaths = []
-
- traversePath = (nodeModulesPath) =>
- try
- for modulePath in fs.listSync(nodeModulesPath)
- nativeModulePaths.push(modulePath) if @isNativeModule(modulePath)
- traversePath(path.join(modulePath, 'node_modules'))
- return
-
- traversePath(path.join(@path, 'node_modules'))
- nativeModulePaths
-
- # Get the incompatible native modules that this package depends on.
- # This recurses through all dependencies and requires all modules that
- # contain a `.node` file.
- #
- # This information is cached in local storage on a per package/version basis
- # to minimize the impact on startup time.
- getIncompatibleNativeModules: ->
- localStorageKey = "installed-packages:#{@name}:#{@metadata.version}"
- unless atom.inDevMode()
- try
- {incompatibleNativeModules} = JSON.parse(global.localStorage.getItem(localStorageKey)) ? {}
- return incompatibleNativeModules if incompatibleNativeModules?
-
- incompatibleNativeModules = []
- for nativeModulePath in @getNativeModuleDependencyPaths()
- try
- require(nativeModulePath)
- catch error
- try
- version = require("#{nativeModulePath}/package.json").version
- incompatibleNativeModules.push
- path: nativeModulePath
- name: path.basename(nativeModulePath)
- version: version
- error: error.message
-
- global.localStorage.setItem(localStorageKey, JSON.stringify({incompatibleNativeModules}))
- incompatibleNativeModules
-
- # Public: Is this package compatible with this version of Atom?
- #
- # Incompatible packages cannot be activated. This will include packages
- # installed to ~/.atom/packages that were built against node 0.11.10 but
- # now need to be upgrade to node 0.11.13.
- #
- # Returns a {Boolean}, true if compatible, false if incompatible.
- isCompatible: ->
- return @compatible if @compatible?
-
- if @path.indexOf(path.join(atom.packages.resourcePath, 'node_modules') + path.sep) is 0
- # Bundled packages are always considered compatible
- @compatible = true
- else if @getMainModulePath()
- @incompatibleModules = @getIncompatibleNativeModules()
- @compatible = @incompatibleModules.length is 0
- else
- @compatible = true
-
- handleError: (message, error) ->
- if error.filename and error.location and (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}
- at #{location}
- """
- else if error.less and error.filename and error.column? and error.line?
- # Less errors
- location = "#{error.filename}:#{error.line}:#{error.column}"
- detail = "#{error.message} in #{location}"
- stack = """
- LessError: #{error.message}
- at #{location}
- """
- else
- detail = error.message
- stack = error.stack ? error
-
- atom.notifications.addFatalError(message, {stack, detail, dismissable: true})
-
-if includeDeprecatedAPIs
- EmitterMixin = require('emissary').Emitter
- EmitterMixin.includeInto(Package)
-
- Package::on = (eventName) ->
- switch eventName
- when 'deactivated'
- deprecate 'Use Package::onDidDeactivate instead'
- else
- deprecate 'Package::on is deprecated. Use event subscription methods instead.'
- EmitterMixin::on.apply(this, arguments)
diff --git a/src/package.js b/src/package.js
new file mode 100644
index 00000000000..2ef76cc7827
--- /dev/null
+++ b/src/package.js
@@ -0,0 +1,1410 @@
+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 });
+ }
+ }
+}
diff --git a/src/pane-axis-element.coffee b/src/pane-axis-element.coffee
deleted file mode 100644
index f9dd156975c..00000000000
--- a/src/pane-axis-element.coffee
+++ /dev/null
@@ -1,67 +0,0 @@
-{CompositeDisposable} = require 'event-kit'
-{callAttachHooks} = require './space-pen-extensions'
-PaneResizeHandleElement = require './pane-resize-handle-element'
-
-class PaneAxisElement extends HTMLElement
- createdCallback: ->
- @subscriptions = new CompositeDisposable
-
- detachedCallback: ->
- @subscriptions.dispose()
-
- initialize: (@model) ->
- @subscriptions.add @model.onDidAddChild(@childAdded.bind(this))
- @subscriptions.add @model.onDidRemoveChild(@childRemoved.bind(this))
- @subscriptions.add @model.onDidReplaceChild(@childReplaced.bind(this))
- @subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this))
-
- @childAdded({child, index}) for child, index in @model.getChildren()
-
- switch @model.getOrientation()
- when 'horizontal'
- @classList.add('horizontal', 'pane-row')
- when 'vertical'
- @classList.add('vertical', 'pane-column')
- this
-
- isPaneResizeHandleElement: (element) ->
- element?.nodeName.toLowerCase() is 'atom-pane-resize-handle'
-
- childAdded: ({child, index}) ->
- view = atom.views.getView(child)
- @insertBefore(view, @children[index * 2])
-
- prevElement = view.previousSibling
- # if previous element is not pane resize element, then insert new resize element
- if prevElement? and not @isPaneResizeHandleElement(prevElement)
- resizeHandle = document.createElement('atom-pane-resize-handle')
- @insertBefore(resizeHandle, view)
-
- nextElement = view.nextSibling
- # if next element isnot resize element, then insert new resize element
- if nextElement? and not @isPaneResizeHandleElement(nextElement)
- resizeHandle = document.createElement('atom-pane-resize-handle')
- @insertBefore(resizeHandle, nextElement)
-
- callAttachHooks(view) # for backward compatibility with SpacePen views
-
- childRemoved: ({child}) ->
- view = atom.views.getView(child)
- siblingView = view.previousSibling
- # make sure next sibling view is pane resize view
- if siblingView? and @isPaneResizeHandleElement(siblingView)
- siblingView.remove()
- view.remove()
-
- childReplaced: ({index, oldChild, newChild}) ->
- focusedElement = document.activeElement if @hasFocus()
- @childRemoved({child: oldChild, index})
- @childAdded({child: newChild, index})
- focusedElement?.focus() if document.activeElement is document.body
-
- flexScaleChanged: (flexScale) -> @style.flexGrow = flexScale
-
- hasFocus: ->
- this is document.activeElement or @contains(document.activeElement)
-
-module.exports = PaneAxisElement = document.registerElement 'atom-pane-axis', prototype: PaneAxisElement.prototype
diff --git a/src/pane-axis-element.js b/src/pane-axis-element.js
new file mode 100644
index 00000000000..1a620f9df0f
--- /dev/null
+++ b/src/pane-axis-element.js
@@ -0,0 +1,126 @@
+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
+};
diff --git a/src/pane-axis.coffee b/src/pane-axis.coffee
deleted file mode 100644
index 1fba48d3788..00000000000
--- a/src/pane-axis.coffee
+++ /dev/null
@@ -1,145 +0,0 @@
-{Emitter, CompositeDisposable} = require 'event-kit'
-{flatten} = require 'underscore-plus'
-Serializable = require 'serializable'
-Model = require './model'
-
-module.exports =
-class PaneAxis extends Model
- atom.deserializers.add(this)
- Serializable.includeInto(this)
-
- parent: null
- container: null
- orientation: null
-
- constructor: ({@container, @orientation, children, flexScale}={}) ->
- @emitter = new Emitter
- @subscriptionsByChild = new WeakMap
- @subscriptions = new CompositeDisposable
- @children = []
- if children?
- @addChild(child) for child in children
- @flexScale = flexScale ? 1
-
- deserializeParams: (params) ->
- {container} = params
- params.children = params.children.map (childState) -> atom.deserializers.deserialize(childState, {container})
- params
-
- serializeParams: ->
- children: @children.map (child) -> child.serialize()
- orientation: @orientation
- flexScale: @flexScale
-
- getFlexScale: -> @flexScale
-
- setFlexScale: (@flexScale) ->
- @emitter.emit 'did-change-flex-scale', @flexScale
- @flexScale
-
- getParent: -> @parent
-
- setParent: (@parent) -> @parent
-
- getContainer: -> @container
-
- setContainer: (@container) -> @container
-
- getOrientation: -> @orientation
-
- getChildren: -> @children.slice()
-
- getPanes: ->
- flatten(@children.map (child) -> child.getPanes())
-
- getItems: ->
- flatten(@children.map (child) -> child.getItems())
-
- onDidAddChild: (fn) ->
- @emitter.on 'did-add-child', fn
-
- onDidRemoveChild: (fn) ->
- @emitter.on 'did-remove-child', fn
-
- onDidReplaceChild: (fn) ->
- @emitter.on 'did-replace-child', fn
-
- onDidDestroy: (fn) ->
- @emitter.on 'did-destroy', fn
-
- onDidChangeFlexScale: (fn) ->
- @emitter.on 'did-change-flex-scale', fn
-
- observeFlexScale: (fn) ->
- fn(@flexScale)
- @onDidChangeFlexScale(fn)
-
- addChild: (child, index=@children.length) ->
- child.setParent(this)
- child.setContainer(@container)
-
- @subscribeToChild(child)
-
- @children.splice(index, 0, child)
- @emitter.emit 'did-add-child', {child, index}
-
- adjustFlexScale: ->
- # get current total flex scale of children
- total = 0
- total += child.getFlexScale() for child in @children
-
- needTotal = @children.length
- # set every child's flex scale by the ratio
- for child in @children
- child.setFlexScale(needTotal * child.getFlexScale() / total)
-
- removeChild: (child, replacing=false) ->
- index = @children.indexOf(child)
- throw new Error("Removing non-existent child") if index is -1
-
- @unsubscribeFromChild(child)
-
- @children.splice(index, 1)
- @adjustFlexScale()
- @emitter.emit 'did-remove-child', {child, index}
- @reparentLastChild() if not replacing and @children.length < 2
-
- replaceChild: (oldChild, newChild) ->
- @unsubscribeFromChild(oldChild)
- @subscribeToChild(newChild)
-
- newChild.setParent(this)
- newChild.setContainer(@container)
-
- index = @children.indexOf(oldChild)
- @children.splice(index, 1, newChild)
- @emitter.emit 'did-replace-child', {oldChild, newChild, index}
-
- insertChildBefore: (currentChild, newChild) ->
- index = @children.indexOf(currentChild)
- @addChild(newChild, index)
-
- insertChildAfter: (currentChild, newChild) ->
- index = @children.indexOf(currentChild)
- @addChild(newChild, index + 1)
-
- reparentLastChild: ->
- lastChild = @children[0]
- lastChild.setFlexScale(@flexScale)
- @parent.replaceChild(this, lastChild)
- @destroy()
-
- subscribeToChild: (child) ->
- subscription = child.onDidDestroy => @removeChild(child)
- @subscriptionsByChild.set(child, subscription)
- @subscriptions.add(subscription)
-
- unsubscribeFromChild: (child) ->
- subscription = @subscriptionsByChild.get(child)
- @subscriptions.remove(subscription)
- subscription.dispose()
-
- destroyed: ->
- @subscriptions.dispose()
- @emitter.emit 'did-destroy'
- @emitter.dispose()
diff --git a/src/pane-axis.js b/src/pane-axis.js
new file mode 100644
index 00000000000..08afa2351f9
--- /dev/null
+++ b/src/pane-axis.js
@@ -0,0 +1,208 @@
+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;
diff --git a/src/pane-container-element.coffee b/src/pane-container-element.coffee
deleted file mode 100644
index 94b008255c8..00000000000
--- a/src/pane-container-element.coffee
+++ /dev/null
@@ -1,82 +0,0 @@
-{CompositeDisposable} = require 'event-kit'
-Grim = require 'grim'
-{callAttachHooks} = require './space-pen-extensions'
-PaneContainerView = null
-_ = require 'underscore-plus'
-
-module.exports =
-class PaneContainerElement extends HTMLElement
- createdCallback: ->
- @subscriptions = new CompositeDisposable
- @classList.add 'panes'
-
- if Grim.includeDeprecatedAPIs
- PaneContainerView ?= require './pane-container-view'
- @__spacePenView = new PaneContainerView(this)
-
- initialize: (@model) ->
- @subscriptions.add @model.observeRoot(@rootChanged.bind(this))
- @__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs
- this
-
- rootChanged: (root) ->
- focusedElement = document.activeElement if @hasFocus()
- @firstChild?.remove()
- if root?
- view = atom.views.getView(root)
- @appendChild(view)
- callAttachHooks(view)
- focusedElement?.focus()
-
- hasFocus: ->
- this is document.activeElement or @contains(document.activeElement)
-
- focusPaneViewAbove: ->
- @nearestPaneInDirection('above')?.focus()
-
- focusPaneViewBelow: ->
- @nearestPaneInDirection('below')?.focus()
-
- focusPaneViewOnLeft: ->
- @nearestPaneInDirection('left')?.focus()
-
- focusPaneViewOnRight: ->
- @nearestPaneInDirection('right')?.focus()
-
- nearestPaneInDirection: (direction) ->
- distance = (pointA, pointB) ->
- x = pointB.x - pointA.x
- y = pointB.y - pointA.y
- Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
-
- paneView = atom.views.getView(@model.getActivePane())
- box = @boundingBoxForPaneView(paneView)
-
- paneViews = _.toArray(@querySelectorAll('atom-pane'))
- .filter (otherPaneView) =>
- otherBox = @boundingBoxForPaneView(otherPaneView)
- switch direction
- when 'left' then otherBox.right.x <= box.left.x
- when 'right' then otherBox.left.x >= box.right.x
- when 'above' then otherBox.bottom.y <= box.top.y
- when 'below' then otherBox.top.y >= box.bottom.y
- .sort (paneViewA, paneViewB) =>
- boxA = @boundingBoxForPaneView(paneViewA)
- boxB = @boundingBoxForPaneView(paneViewB)
- switch direction
- when 'left' then distance(box.left, boxA.right) - distance(box.left, boxB.right)
- when 'right' then distance(box.right, boxA.left) - distance(box.right, boxB.left)
- when 'above' then distance(box.top, boxA.bottom) - distance(box.top, boxB.bottom)
- when 'below' then distance(box.bottom, boxA.top) - distance(box.bottom, boxB.top)
-
- paneViews[0]
-
- boundingBoxForPaneView: (paneView) ->
- boundingBox = paneView.getBoundingClientRect()
-
- 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}
-
-module.exports = PaneContainerElement = document.registerElement 'atom-pane-container', prototype: PaneContainerElement.prototype
diff --git a/src/pane-container-element.js b/src/pane-container-element.js
new file mode 100644
index 00000000000..f435ceef529
--- /dev/null
+++ b/src/pane-container-element.js
@@ -0,0 +1,54 @@
+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
+};
diff --git a/src/pane-container-view.coffee b/src/pane-container-view.coffee
deleted file mode 100644
index d92e99299fb..00000000000
--- a/src/pane-container-view.coffee
+++ /dev/null
@@ -1,89 +0,0 @@
-{deprecate} = require 'grim'
-Delegator = require 'delegato'
-{CompositeDisposable} = require 'event-kit'
-{$, View, callAttachHooks} = require './space-pen-extensions'
-PaneView = require './pane-view'
-PaneContainer = require './pane-container'
-
-# Manages the list of panes within a {WorkspaceView}
-module.exports =
-class PaneContainerView extends View
- Delegator.includeInto(this)
-
- @delegatesMethod 'saveAll', toProperty: 'model'
-
- @content: ->
- @div class: 'panes'
-
- constructor: (@element) ->
- super
- @subscriptions = new CompositeDisposable
-
- setModel: (@model) ->
- @subscriptions.add @model.onDidChangeActivePaneItem(@onActivePaneItemChanged)
-
- getRoot: ->
- view = atom.views.getView(@model.getRoot())
- view.__spacePenView ? view
-
- onActivePaneItemChanged: (activeItem) =>
- @trigger 'pane-container:active-pane-item-changed', [activeItem]
-
- confirmClose: ->
- @model.confirmClose()
-
- getPaneViews: ->
- @find('atom-pane').views()
-
- indexOfPane: (paneView) ->
- @getPaneViews().indexOf(paneView.view())
-
- paneAtIndex: (index) ->
- @getPaneViews()[index]
-
- eachPaneView: (callback) ->
- callback(paneView) for paneView in @getPaneViews()
- paneViewAttached = (e) -> callback($(e.target).view())
- @on 'pane:attached', paneViewAttached
- off: => @off 'pane:attached', paneViewAttached
-
- getFocusedPane: ->
- @find('atom-pane:has(:focus)').view()
-
- getActivePane: ->
- deprecate("Use PaneContainerView::getActivePaneView instead.")
- @getActivePaneView()
-
- getActivePaneView: ->
- atom.views.getView(@model.getActivePane()).__spacePenView
-
- getActivePaneItem: ->
- @model.getActivePaneItem()
-
- getActiveView: ->
- @getActivePaneView()?.activeView
-
- paneForUri: (uri) ->
- atom.views.getView(@model.paneForURI(uri)).__spacePenView
-
- focusNextPaneView: ->
- @model.activateNextPane()
-
- focusPreviousPaneView: ->
- @model.activatePreviousPane()
-
- focusPaneViewAbove: ->
- @element.focusPaneViewAbove()
-
- focusPaneViewBelow: ->
- @element.focusPaneViewBelow()
-
- focusPaneViewOnLeft: ->
- @element.focusPaneViewOnLeft()
-
- focusPaneViewOnRight: ->
- @element.focusPaneViewOnRight()
-
- getPanes: ->
- deprecate("Use PaneContainerView::getPaneViews() instead")
- @getPaneViews()
diff --git a/src/pane-container.coffee b/src/pane-container.coffee
deleted file mode 100644
index 26ef22cacae..00000000000
--- a/src/pane-container.coffee
+++ /dev/null
@@ -1,246 +0,0 @@
-{find, flatten} = require 'underscore-plus'
-Grim = require 'grim'
-{Emitter, CompositeDisposable} = require 'event-kit'
-Serializable = require 'serializable'
-{createGutterView} = require './gutter-component-helpers'
-Gutter = require './gutter'
-Model = require './model'
-Pane = require './pane'
-PaneElement = require './pane-element'
-PaneContainerElement = require './pane-container-element'
-PaneAxisElement = require './pane-axis-element'
-PaneAxis = require './pane-axis'
-TextEditor = require './text-editor'
-TextEditorElement = require './text-editor-element'
-ItemRegistry = require './item-registry'
-
-module.exports =
-class PaneContainer extends Model
- atom.deserializers.add(this)
- Serializable.includeInto(this)
-
- @version: 1
-
- root: null
-
- constructor: (params) ->
- super
-
- unless Grim.includeDeprecatedAPIs
- @activePane = params?.activePane
-
- @emitter = new Emitter
- @subscriptions = new CompositeDisposable
-
- @itemRegistry = new ItemRegistry
- @registerViewProviders()
-
- @setRoot(params?.root ? new Pane)
- @setActivePane(@getPanes()[0]) unless @getActivePane()
-
- @destroyEmptyPanes() if params?.destroyEmptyPanes
-
- @monitorActivePaneItem()
- @monitorPaneItems()
-
- deserializeParams: (params) ->
- params.root = atom.deserializers.deserialize(params.root, container: this)
- params.destroyEmptyPanes = atom.config.get('core.destroyEmptyPanes')
- params.activePane = find params.root.getPanes(), (pane) -> pane.id is params.activePaneId
- params
-
- serializeParams: (params) ->
- root: @root?.serialize()
- activePaneId: @activePane.id
-
- registerViewProviders: ->
- atom.views.addViewProvider PaneContainer, (model) ->
- new PaneContainerElement().initialize(model)
- atom.views.addViewProvider PaneAxis, (model) ->
- new PaneAxisElement().initialize(model)
- atom.views.addViewProvider Pane, (model) ->
- new PaneElement().initialize(model)
- atom.views.addViewProvider TextEditor, (model) ->
- new TextEditorElement().initialize(model)
- atom.views.addViewProvider(Gutter, createGutterView)
-
- onDidChangeRoot: (fn) ->
- @emitter.on 'did-change-root', fn
-
- observeRoot: (fn) ->
- fn(@getRoot())
- @onDidChangeRoot(fn)
-
- onDidAddPane: (fn) ->
- @emitter.on 'did-add-pane', fn
-
- observePanes: (fn) ->
- fn(pane) for pane in @getPanes()
- @onDidAddPane ({pane}) -> fn(pane)
-
- onDidDestroyPane: (fn) ->
- @emitter.on 'did-destroy-pane', fn
-
- onDidChangeActivePane: (fn) ->
- @emitter.on 'did-change-active-pane', fn
-
- observeActivePane: (fn) ->
- fn(@getActivePane())
- @onDidChangeActivePane(fn)
-
- onDidAddPaneItem: (fn) ->
- @emitter.on 'did-add-pane-item', fn
-
- observePaneItems: (fn) ->
- fn(item) for item in @getPaneItems()
- @onDidAddPaneItem ({item}) -> fn(item)
-
- onDidChangeActivePaneItem: (fn) ->
- @emitter.on 'did-change-active-pane-item', fn
-
- observeActivePaneItem: (fn) ->
- fn(@getActivePaneItem())
- @onDidChangeActivePaneItem(fn)
-
- onWillDestroyPaneItem: (fn) ->
- @emitter.on 'will-destroy-pane-item', fn
-
- onDidDestroyPaneItem: (fn) ->
- @emitter.on 'did-destroy-pane-item', fn
-
- getRoot: -> @root
-
- setRoot: (@root) ->
- @root.setParent(this)
- @root.setContainer(this)
- @emitter.emit 'did-change-root', @root
- if not @getActivePane()? and @root instanceof Pane
- @setActivePane(@root)
-
- replaceChild: (oldChild, newChild) ->
- throw new Error("Replacing non-existent child") if oldChild isnt @root
- @setRoot(newChild)
-
- getPanes: ->
- @getRoot().getPanes()
-
- getPaneItems: ->
- @getRoot().getItems()
-
- getActivePane: ->
- @activePane
-
- setActivePane: (activePane) ->
- if activePane isnt @activePane
- unless activePane in @getPanes()
- throw new Error("Setting active pane that is not present in pane container")
-
- @activePane = activePane
- @emitter.emit 'did-change-active-pane', @activePane
- @activePane
-
- getActivePaneItem: ->
- @getActivePane().getActiveItem()
-
- paneForURI: (uri) ->
- find @getPanes(), (pane) -> pane.itemForURI(uri)?
-
- paneForItem: (item) ->
- find @getPanes(), (pane) -> item in pane.getItems()
-
- saveAll: ->
- pane.saveItems() for pane in @getPanes()
- return
-
- confirmClose: (options) ->
- allSaved = true
-
- for pane in @getPanes()
- for item in pane.getItems()
- unless pane.promptToSaveItem(item, options)
- allSaved = false
- break
-
- allSaved
-
- activateNextPane: ->
- panes = @getPanes()
- if panes.length > 1
- currentIndex = panes.indexOf(@activePane)
- nextIndex = (currentIndex + 1) % panes.length
- panes[nextIndex].activate()
- true
- else
- false
-
- activatePreviousPane: ->
- panes = @getPanes()
- if panes.length > 1
- currentIndex = panes.indexOf(@activePane)
- previousIndex = currentIndex - 1
- previousIndex = panes.length - 1 if previousIndex < 0
- panes[previousIndex].activate()
- true
- else
- false
-
- destroyEmptyPanes: ->
- pane.destroy() for pane in @getPanes() when pane.items.length is 0
- return
-
- willDestroyPaneItem: (event) ->
- @emitter.emit 'will-destroy-pane-item', event
-
- didDestroyPaneItem: (event) ->
- @emitter.emit 'did-destroy-pane-item', event
-
- didAddPane: (event) ->
- @emitter.emit 'did-add-pane', event
-
- didDestroyPane: (event) ->
- @emitter.emit 'did-destroy-pane', event
-
- # Called by Model superclass when destroyed
- destroyed: ->
- pane.destroy() for pane in @getPanes()
- @subscriptions.dispose()
- @emitter.dispose()
-
- monitorActivePaneItem: ->
- childSubscription = null
- @subscriptions.add @observeActivePane (activePane) =>
- if childSubscription?
- @subscriptions.remove(childSubscription)
- childSubscription.dispose()
-
- childSubscription = activePane.observeActiveItem (activeItem) =>
- @emitter.emit 'did-change-active-pane-item', activeItem
-
- @subscriptions.add(childSubscription)
-
- monitorPaneItems: ->
- @subscriptions.add @observePanes (pane) =>
- for item, index in pane.getItems()
- @addedPaneItem(item, pane, index)
-
- pane.onDidAddItem ({item, index}) =>
- @addedPaneItem(item, pane, index)
-
- pane.onDidRemoveItem ({item}) =>
- @removedPaneItem(item)
-
- addedPaneItem: (item, pane, index) ->
- @itemRegistry.addItem(item)
- @emitter.emit 'did-add-pane-item', {item, pane, index}
-
- removedPaneItem: (item) ->
- @itemRegistry.removeItem(item)
-
-if Grim.includeDeprecatedAPIs
- PaneContainer.properties
- activePane: null
-
- PaneContainer.behavior 'activePaneItem', ->
- @$activePane
- .switch((activePane) -> activePane?.$activeItem)
- .distinctUntilChanged()
diff --git a/src/pane-container.js b/src/pane-container.js
new file mode 100644
index 00000000000..bc2f2a6f656
--- /dev/null
+++ b/src/pane-container.js
@@ -0,0 +1,351 @@
+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);
+ }
+ }
+};
diff --git a/src/pane-element.coffee b/src/pane-element.coffee
deleted file mode 100644
index 15990bcc212..00000000000
--- a/src/pane-element.coffee
+++ /dev/null
@@ -1,160 +0,0 @@
-path = require 'path'
-{CompositeDisposable} = require 'event-kit'
-Grim = require 'grim'
-{$, callAttachHooks, callRemoveHooks} = require './space-pen-extensions'
-PaneView = null
-
-class PaneElement extends HTMLElement
- attached: false
-
- createdCallback: ->
- @attached = false
- @subscriptions = new CompositeDisposable
- @inlineDisplayStyles = new WeakMap
-
- @initializeContent()
- @subscribeToDOMEvents()
- @createSpacePenShim() if Grim.includeDeprecatedAPIs
-
- attachedCallback: ->
- @attached = true
- @focus() if @model.isFocused()
-
- detachedCallback: ->
- @attached = false
-
- initializeContent: ->
- @setAttribute 'class', 'pane'
- @setAttribute 'tabindex', -1
- @appendChild @itemViews = document.createElement('div')
- @itemViews.setAttribute 'class', 'item-views'
-
- subscribeToDOMEvents: ->
- handleFocus = (event) =>
- @model.focus()
- if event.target is this and view = @getActiveView()
- view.focus()
- event.stopPropagation()
-
- handleBlur = (event) =>
- @model.blur() unless @contains(event.relatedTarget)
-
- handleDragOver = (event) ->
- event.preventDefault()
- event.stopPropagation()
-
- handleDrop = (event) =>
- event.preventDefault()
- event.stopPropagation()
- @getModel().activate()
- pathsToOpen = Array::map.call event.dataTransfer.files, (file) -> file.path
- atom.open({pathsToOpen}) if pathsToOpen.length > 0
-
- @addEventListener 'focus', handleFocus, true
- @addEventListener 'blur', handleBlur, true
- @addEventListener 'dragover', handleDragOver
- @addEventListener 'drop', handleDrop
-
- createSpacePenShim: ->
- PaneView ?= require './pane-view'
- @__spacePenView = new PaneView(this)
-
- initialize: (@model) ->
- @subscriptions.add @model.onDidActivate(@activated.bind(this))
- @subscriptions.add @model.observeActive(@activeStatusChanged.bind(this))
- @subscriptions.add @model.observeActiveItem(@activeItemChanged.bind(this))
- @subscriptions.add @model.onDidRemoveItem(@itemRemoved.bind(this))
- @subscriptions.add @model.onDidDestroy(@paneDestroyed.bind(this))
- @subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this))
-
- @__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs
- this
-
- getModel: -> @model
-
- activated: ->
- @focus()
-
- activeStatusChanged: (active) ->
- if active
- @classList.add('active')
- else
- @classList.remove('active')
-
- activeItemChanged: (item) ->
- delete @dataset.activeItemName
- delete @dataset.activeItemPath
-
- return unless item?
-
- hasFocus = @hasFocus()
- itemView = atom.views.getView(item)
-
- if itemPath = item.getPath?()
- @dataset.activeItemName = path.basename(itemPath)
- @dataset.activeItemPath = itemPath
-
- unless @itemViews.contains(itemView)
- @itemViews.appendChild(itemView)
- callAttachHooks(itemView)
-
- for child in @itemViews.children
- if child is itemView
- @showItemView(child) if @attached
- else
- @hideItemView(child)
-
- itemView.focus() if hasFocus
-
- showItemView: (itemView) ->
- inlineDisplayStyle = @inlineDisplayStyles.get(itemView)
- if inlineDisplayStyle?
- itemView.style.display = inlineDisplayStyle
- else
- itemView.style.display = ''
-
- hideItemView: (itemView) ->
- inlineDisplayStyle = itemView.style.display
- unless inlineDisplayStyle is 'none'
- @inlineDisplayStyles.set(itemView, inlineDisplayStyle) if inlineDisplayStyle?
- itemView.style.display = 'none'
-
- itemRemoved: ({item, index, destroyed}) ->
- if viewToRemove = atom.views.getView(item)
- callRemoveHooks(viewToRemove) if destroyed
- viewToRemove.remove()
-
- paneDestroyed: ->
- @subscriptions.dispose()
-
- flexScaleChanged: (flexScale) ->
- @style.flexGrow = flexScale
-
- getActiveView: -> atom.views.getView(@model.getActiveItem())
-
- hasFocus: ->
- this is document.activeElement or @contains(document.activeElement)
-
-atom.commands.add 'atom-pane',
- 'pane:save-items': -> @getModel().saveItems()
- 'pane:show-next-item': -> @getModel().activateNextItem()
- 'pane:show-previous-item': -> @getModel().activatePreviousItem()
- 'pane:show-item-1': -> @getModel().activateItemAtIndex(0)
- 'pane:show-item-2': -> @getModel().activateItemAtIndex(1)
- 'pane:show-item-3': -> @getModel().activateItemAtIndex(2)
- 'pane:show-item-4': -> @getModel().activateItemAtIndex(3)
- 'pane:show-item-5': -> @getModel().activateItemAtIndex(4)
- 'pane:show-item-6': -> @getModel().activateItemAtIndex(5)
- 'pane:show-item-7': -> @getModel().activateItemAtIndex(6)
- 'pane:show-item-8': -> @getModel().activateItemAtIndex(7)
- 'pane:show-item-9': -> @getModel().activateItemAtIndex(8)
- 'pane:move-item-right': -> @getModel().moveItemRight()
- 'pane:move-item-left': -> @getModel().moveItemLeft()
- 'pane:split-left': -> @getModel().splitLeft(copyActiveItem: true)
- 'pane:split-right': -> @getModel().splitRight(copyActiveItem: true)
- 'pane:split-up': -> @getModel().splitUp(copyActiveItem: true)
- 'pane:split-down': -> @getModel().splitDown(copyActiveItem: true)
- 'pane:close': -> @getModel().close()
- 'pane:close-other-items': -> @getModel().destroyInactiveItems()
-
-module.exports = PaneElement = document.registerElement 'atom-pane', prototype: PaneElement.prototype
diff --git a/src/pane-element.js b/src/pane-element.js
new file mode 100644
index 00000000000..60b8cf5d003
--- /dev/null
+++ b/src/pane-element.js
@@ -0,0 +1,226 @@
+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
+};
diff --git a/src/pane-resize-handle-element.coffee b/src/pane-resize-handle-element.coffee
deleted file mode 100644
index 078bb44acc7..00000000000
--- a/src/pane-resize-handle-element.coffee
+++ /dev/null
@@ -1,68 +0,0 @@
-class PaneResizeHandleElement extends HTMLElement
- createdCallback: ->
- @resizePane = @resizePane.bind(this)
- @resizeStopped = @resizeStopped.bind(this)
- @subscribeToDOMEvents()
-
- subscribeToDOMEvents: ->
- @addEventListener 'dblclick', @resizeToFitContent.bind(this)
- @addEventListener 'mousedown', @resizeStarted.bind(this)
-
- attachedCallback: ->
- @isHorizontal = @parentElement.classList.contains("horizontal")
- @classList.add if @isHorizontal then 'horizontal' else 'vertical'
-
- detachedCallback: ->
- @resizeStopped()
-
- resizeToFitContent: ->
- # clear flex-grow css style of both pane
- @previousSibling.model.setFlexScale(1)
- @nextSibling.model.setFlexScale(1)
-
- resizeStarted: (e) ->
- e.stopPropagation()
- document.addEventListener 'mousemove', @resizePane
- document.addEventListener 'mouseup', @resizeStopped
-
- resizeStopped: ->
- document.removeEventListener 'mousemove', @resizePane
- document.removeEventListener 'mouseup', @resizeStopped
-
- calcRatio: (ratio1, ratio2, total) ->
- allRatio = ratio1 + ratio2
- [total * ratio1 / allRatio, total * ratio2 / allRatio]
-
- setFlexGrow: (prevSize, nextSize) ->
- @prevModel = @previousSibling.model
- @nextModel = @nextSibling.model
- totalScale = @prevModel.getFlexScale() + @nextModel.getFlexScale()
- flexGrows = @calcRatio(prevSize, nextSize, totalScale)
- @prevModel.setFlexScale flexGrows[0]
- @nextModel.setFlexScale flexGrows[1]
-
- fixInRange: (val, minValue, maxValue) ->
- Math.min(Math.max(val, minValue), maxValue)
-
- resizePane: ({clientX, clientY, which}) ->
- return @resizeStopped() unless which is 1
- return @resizeStopped() unless @previousSibling? and @nextSibling?
-
- if @isHorizontal
- totalWidth = @previousSibling.clientWidth + @nextSibling.clientWidth
- #get the left and right width after move the resize view
- leftWidth = clientX - @previousSibling.getBoundingClientRect().left
- leftWidth = @fixInRange(leftWidth, 0, totalWidth)
- rightWidth = totalWidth - leftWidth
- # set the flex grow by the ratio of left width and right width
- # to change pane width
- @setFlexGrow(leftWidth, rightWidth)
- else
- totalHeight = @previousSibling.clientHeight + @nextSibling.clientHeight
- topHeight = clientY - @previousSibling.getBoundingClientRect().top
- topHeight = @fixInRange(topHeight, 0, totalHeight)
- bottomHeight = totalHeight - topHeight
- @setFlexGrow(topHeight, bottomHeight)
-
-module.exports = PaneResizeHandleElement =
-document.registerElement 'atom-pane-resize-handle', prototype: PaneResizeHandleElement.prototype
diff --git a/src/pane-resize-handle-element.js b/src/pane-resize-handle-element.js
new file mode 100644
index 00000000000..ede61042f8b
--- /dev/null
+++ b/src/pane-resize-handle-element.js
@@ -0,0 +1,120 @@
+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
+};
diff --git a/src/pane-view.coffee b/src/pane-view.coffee
deleted file mode 100644
index 775514ca297..00000000000
--- a/src/pane-view.coffee
+++ /dev/null
@@ -1,167 +0,0 @@
-{$, View} = require './space-pen-extensions'
-Delegator = require 'delegato'
-{deprecate} = require 'grim'
-{CompositeDisposable} = require 'event-kit'
-PropertyAccessors = require 'property-accessors'
-
-Pane = require './pane'
-
-# A container which can contains multiple items to be switched between.
-#
-# Items can be almost anything however most commonly they're {TextEditorView}s.
-#
-# Most packages won't need to use this class, unless you're interested in
-# building a package that deals with switching between panes or items.
-module.exports =
-class PaneView extends View
- Delegator.includeInto(this)
- PropertyAccessors.includeInto(this)
-
- @delegatesProperties 'items', 'activeItem', toProperty: 'model'
- @delegatesMethods 'getItems', 'activateNextItem', 'activatePreviousItem', 'getActiveItemIndex',
- 'activateItemAtIndex', 'activateItem', 'addItem', 'itemAtIndex', 'moveItem', 'moveItemToPane',
- 'destroyItem', 'destroyItems', 'destroyActiveItem', 'destroyInactiveItems',
- 'saveActiveItem', 'saveActiveItemAs', 'saveItem', 'saveItemAs', 'saveItems',
- 'itemForUri', 'activateItemForUri', 'promptToSaveItem', 'copyActiveItem', 'isActive',
- 'activate', 'getActiveItem', toProperty: 'model'
-
- previousActiveItem: null
- attached: false
-
- constructor: (@element) ->
- @itemViews = $(element.itemViews)
- super
-
- setModel: (@model) ->
- @subscriptions = new CompositeDisposable
- @subscriptions.add @model.observeActiveItem(@onActiveItemChanged)
- @subscriptions.add @model.onDidAddItem(@onItemAdded)
- @subscriptions.add @model.onDidRemoveItem(@onItemRemoved)
- @subscriptions.add @model.onDidMoveItem(@onItemMoved)
- @subscriptions.add @model.onWillDestroyItem(@onBeforeItemDestroyed)
- @subscriptions.add @model.observeActive(@onActiveStatusChanged)
- @subscriptions.add @model.onDidDestroy(@onPaneDestroyed)
-
- afterAttach: ->
- @container ?= @closest('atom-pane-container').view()
- @trigger('pane:attached', [this]) unless @attached
- @attached = true
-
- onPaneDestroyed: =>
- @container?.trigger 'pane:removed', [this]
- @subscriptions.dispose()
-
- remove: ->
- @model.destroy() unless @model.isDestroyed()
-
- # Essential: Returns the {Pane} model underlying this pane view
- getModel: -> @model
-
- # Deprecated: Use ::destroyItem
- removeItem: (item) ->
- deprecate("Use PaneView::destroyItem instead")
- @destroyItem(item)
-
- # Deprecated: Use ::activateItem
- showItem: (item) ->
- deprecate("Use PaneView::activateItem instead")
- @activateItem(item)
-
- # Deprecated: Use ::activateItemForUri
- showItemForUri: (item) ->
- deprecate("Use PaneView::activateItemForUri instead")
- @activateItemForUri(item)
-
- # Deprecated: Use ::activateItemAtIndex
- showItemAtIndex: (index) ->
- deprecate("Use PaneView::activateItemAtIndex instead")
- @activateItemAtIndex(index)
-
- # Deprecated: Use ::activateNextItem
- showNextItem: ->
- deprecate("Use PaneView::activateNextItem instead")
- @activateNextItem()
-
- # Deprecated: Use ::activatePreviousItem
- showPreviousItem: ->
- deprecate("Use PaneView::activatePreviousItem instead")
- @activatePreviousItem()
-
- onActiveStatusChanged: (active) =>
- if active
- @trigger 'pane:became-active'
- else
- @trigger 'pane:became-inactive'
-
- # Public: Returns the next pane, ordered by creation.
- getNextPane: ->
- panes = @container?.getPaneViews()
- return unless panes.length > 1
- nextIndex = (panes.indexOf(this) + 1) % panes.length
- panes[nextIndex]
-
- getActivePaneItem: ->
- @activeItem
-
- onActiveItemChanged: (item) =>
- @activeItemDisposables.dispose() if @activeItemDisposables?
- @activeItemDisposables = new CompositeDisposable()
-
- if @previousActiveItem?.off?
- @previousActiveItem.off 'title-changed', @activeItemTitleChanged
- @previousActiveItem.off 'modified-status-changed', @activeItemModifiedChanged
- @previousActiveItem = item
-
- return unless item?
-
- if item.onDidChangeTitle?
- disposable = item.onDidChangeTitle(@activeItemTitleChanged)
- @activeItemDisposables.add(disposable) if disposable?.dispose?
- else if item.on?
- disposable = item.on('title-changed', @activeItemTitleChanged)
- @activeItemDisposables.add(disposable) if disposable?.dispose?
-
- if item.onDidChangeModified?
- disposable = item.onDidChangeModified(@activeItemModifiedChanged)
- @activeItemDisposables.add(disposable) if disposable?.dispose?
- else if item.on?
- item.on('modified-status-changed', @activeItemModifiedChanged)
- @activeItemDisposables.add(disposable) if disposable?.dispose?
-
- @trigger 'pane:active-item-changed', [item]
-
- onItemAdded: ({item, index}) =>
- @trigger 'pane:item-added', [item, index]
-
- onItemRemoved: ({item, index, destroyed}) =>
- @trigger 'pane:item-removed', [item, index]
-
- onItemMoved: ({item, newIndex}) =>
- @trigger 'pane:item-moved', [item, newIndex]
-
- onBeforeItemDestroyed: ({item}) =>
- @unsubscribe(item) if typeof item.off is 'function'
- @trigger 'pane:before-item-destroyed', [item]
-
- activeItemTitleChanged: =>
- @trigger 'pane:active-item-title-changed'
-
- activeItemModifiedChanged: =>
- @trigger 'pane:active-item-modified-status-changed'
-
- @::accessor 'activeView', ->
- element = atom.views.getView(@activeItem)
- $(element).view() ? element
-
- splitLeft: (items...) -> atom.views.getView(@model.splitLeft({items})).__spacePenView
-
- splitRight: (items...) -> atom.views.getView(@model.splitRight({items})).__spacePenView
-
- splitUp: (items...) -> atom.views.getView(@model.splitUp({items})).__spacePenView
-
- splitDown: (items...) -> atom.views.getView(@model.splitDown({items})).__spacePenView
-
- getContainer: -> @closest('atom-pane-container').view()
-
- focus: ->
- @element.focus()
diff --git a/src/pane.coffee b/src/pane.coffee
deleted file mode 100644
index 38c8d9201c2..00000000000
--- a/src/pane.coffee
+++ /dev/null
@@ -1,739 +0,0 @@
-{find, compact, extend, last} = require 'underscore-plus'
-{Emitter} = require 'event-kit'
-Serializable = require 'serializable'
-Grim = require 'grim'
-Model = require './model'
-PaneAxis = require './pane-axis'
-TextEditor = require './text-editor'
-
-# 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.
-module.exports =
-class Pane extends Model
- atom.deserializers.add(this)
- Serializable.includeInto(this)
-
- constructor: (params) ->
- super
-
- unless Grim.includeDeprecatedAPIs
- @container = params?.container
- @activeItem = params?.activeItem
-
- @emitter = new Emitter
- @itemSubscriptions = new WeakMap
- @items = []
-
- @addItems(compact(params?.items ? []))
- @setActiveItem(@items[0]) unless @getActiveItem()?
- @setFlexScale(params?.flexScale ? 1)
-
- # Called by the Serializable mixin during serialization.
- serializeParams: ->
- if typeof @activeItem?.getURI is 'function'
- activeItemURI = @activeItem.getURI()
- else if Grim.includeDeprecatedAPIs and typeof @activeItem?.getUri is 'function'
- activeItemURI = @activeItem.getUri()
-
- id: @id
- items: compact(@items.map((item) -> item.serialize?()))
- activeItemURI: activeItemURI
- focused: @focused
- flexScale: @flexScale
-
- # Called by the Serializable mixin during deserialization.
- deserializeParams: (params) ->
- {items, activeItemURI, activeItemUri} = params
- activeItemURI ?= activeItemUri
- params.items = compact(items.map (itemState) -> atom.deserializers.deserialize(itemState))
- params.activeItem = find params.items, (item) ->
- if typeof item.getURI is 'function'
- itemURI = item.getURI()
- else if Grim.includeDeprecatedAPIs and typeof item.getUri is 'function'
- itemURI = item.getUri()
-
- itemURI is activeItemURI
- params
-
- getParent: -> @parent
-
- setParent: (@parent) -> @parent
-
- getContainer: -> @container
-
- setContainer: (container) ->
- unless container is @container
- @container = container
- container.didAddPane({pane: this})
-
- setFlexScale: (@flexScale) ->
- @emitter.emit 'did-change-flex-scale', @flexScale
- @flexScale
-
- getFlexScale: -> @flexScale
- ###
- Section: Event Subscription
- ###
-
- # Public: Invoke the given callback when the pane resize
- #
- # the callback will be invoked when pane's flexScale property changes
- #
- # * `callback` {Function} to be called when the pane is resized
- #
- # Returns a {Disposable} on which '.dispose()' can be called to unsubscribe.
- onDidChangeFlexScale: (callback) ->
- @emitter.on 'did-change-flex-scale', callback
-
- # Public: Invoke the given callback with 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.
- observeFlexScale: (callback) ->
- callback(@flexScale)
- @onDidChangeFlexScale(callback)
-
- # Public: Invoke the given callback when the pane is activated.
- #
- # The given callback will be invoked whenever {::activate} is called on the
- # pane, even if it is already active at the time.
- #
- # * `callback` {Function} to be called when the pane is activated.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidActivate: (callback) ->
- @emitter.on 'did-activate', callback
-
- # Public: Invoke the given callback when the pane is destroyed.
- #
- # * `callback` {Function} to be called when the pane is destroyed.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidDestroy: (callback) ->
- @emitter.on 'did-destroy', callback
-
- # Public: Invoke the given callback when the value of the {::isActive}
- # property changes.
- #
- # * `callback` {Function} to be called when the value of the {::isActive}
- # property changes.
- # * `active` {Boolean} indicating whether the pane is active.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangeActive: (callback) ->
- @container.onDidChangeActivePane (activePane) =>
- callback(this is activePane)
-
- # Public: Invoke the given callback with the current and future values of the
- # {::isActive} property.
- #
- # * `callback` {Function} to be called with the current and future values of
- # the {::isActive} property.
- # * `active` {Boolean} indicating whether the pane is active.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- observeActive: (callback) ->
- callback(@isActive())
- @onDidChangeActive(callback)
-
- # Public: Invoke the given callback when an item is added to the pane.
- #
- # * `callback` {Function} to be called with when items are added.
- # * `event` {Object} with the following keys:
- # * `item` The added pane item.
- # * `index` {Number} indicating where the item is located.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidAddItem: (callback) ->
- @emitter.on 'did-add-item', callback
-
- # Public: Invoke the given callback when an item is removed from the pane.
- #
- # * `callback` {Function} to be called with when items are removed.
- # * `event` {Object} with the following keys:
- # * `item` The removed pane item.
- # * `index` {Number} indicating where the item was located.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidRemoveItem: (callback) ->
- @emitter.on 'did-remove-item', callback
-
- # Public: Invoke the given callback when an item is moved within the pane.
- #
- # * `callback` {Function} to be called with when items are moved.
- # * `event` {Object} with the following keys:
- # * `item` The removed pane item.
- # * `oldIndex` {Number} indicating where the item was located.
- # * `newIndex` {Number} indicating where the item is now located.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidMoveItem: (callback) ->
- @emitter.on 'did-move-item', callback
-
- # Public: Invoke the given callback with all current and future items.
- #
- # * `callback` {Function} to be called with current and future items.
- # * `item` An item that is present in {::getItems} at the time of
- # subscription or that is added at some later time.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- observeItems: (callback) ->
- callback(item) for item in @getItems()
- @onDidAddItem ({item}) -> callback(item)
-
- # Public: Invoke the given callback when the value of {::getActiveItem}
- # changes.
- #
- # * `callback` {Function} to be called with when the active item changes.
- # * `activeItem` The current active item.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangeActiveItem: (callback) ->
- @emitter.on 'did-change-active-item', callback
-
- # Public: Invoke the given callback with the current and future values of
- # {::getActiveItem}.
- #
- # * `callback` {Function} to be called with the current and future active
- # items.
- # * `activeItem` The current active item.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- observeActiveItem: (callback) ->
- callback(@getActiveItem())
- @onDidChangeActiveItem(callback)
-
- # Public: Invoke the given callback before items are destroyed.
- #
- # * `callback` {Function} to be called before items are destroyed.
- # * `event` {Object} with the following keys:
- # * `item` The item that will be destroyed.
- # * `index` The location of the item.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to
- # unsubscribe.
- onWillDestroyItem: (callback) ->
- @emitter.on 'will-destroy-item', callback
-
- # Called by the view layer to indicate that the pane has gained focus.
- focus: ->
- @focused = true
- @activate() unless @isActive()
-
- # Called by the view layer to indicate that the pane has lost focus.
- blur: ->
- @focused = false
- true # if this is called from an event handler, don't cancel it
-
- isFocused: -> @focused
-
- getPanes: -> [this]
-
- unsubscribeFromItem: (item) ->
- @itemSubscriptions.get(item)?.dispose()
- @itemSubscriptions.delete(item)
-
- ###
- Section: Items
- ###
-
- # Public: Get the items in this pane.
- #
- # Returns an {Array} of items.
- getItems: ->
- @items.slice()
-
- # Public: Get the active pane item in this pane.
- #
- # Returns a pane item.
- getActiveItem: -> @activeItem
-
- setActiveItem: (activeItem) ->
- unless activeItem is @activeItem
- @activeItem = activeItem
- @emitter.emit 'did-change-active-item', @activeItem
- @activeItem
-
- # Return an {TextEditor} if the pane item is an {TextEditor}, or null otherwise.
- getActiveEditor: ->
- @activeItem if @activeItem instanceof TextEditor
-
- # Public: Return the item at the given index.
- #
- # * `index` {Number}
- #
- # Returns an item or `null` if no item exists at the given index.
- itemAtIndex: (index) ->
- @items[index]
-
- # Public: Makes the next item active.
- activateNextItem: ->
- index = @getActiveItemIndex()
- if index < @items.length - 1
- @activateItemAtIndex(index + 1)
- else
- @activateItemAtIndex(0)
-
- # Public: Makes the previous item active.
- activatePreviousItem: ->
- index = @getActiveItemIndex()
- if index > 0
- @activateItemAtIndex(index - 1)
- else
- @activateItemAtIndex(@items.length - 1)
-
- # Public: Move the active tab to the right.
- moveItemRight: ->
- index = @getActiveItemIndex()
- rightItemIndex = index + 1
- @moveItem(@getActiveItem(), rightItemIndex) unless rightItemIndex > @items.length - 1
-
- # Public: Move the active tab to the left
- moveItemLeft: ->
- index = @getActiveItemIndex()
- leftItemIndex = index - 1
- @moveItem(@getActiveItem(), leftItemIndex) unless leftItemIndex < 0
-
- # Public: Get the index of the active item.
- #
- # Returns a {Number}.
- getActiveItemIndex: ->
- @items.indexOf(@activeItem)
-
- # Public: Activate the item at the given index.
- #
- # * `index` {Number}
- activateItemAtIndex: (index) ->
- @activateItem(@itemAtIndex(index))
-
- # Public: Make the given item *active*, causing it to be displayed by
- # the pane's view.
- activateItem: (item) ->
- if item?
- @addItem(item)
- @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.
- # * `index` (optional) {Number} indicating the index at which to add the item.
- # If omitted, the item is added after the current active item.
- #
- # Returns the added item.
- addItem: (item, index=@getActiveItemIndex() + 1) ->
- return if item in @items
-
- if typeof item.onDidDestroy is 'function'
- @itemSubscriptions.set item, item.onDidDestroy => @removeItem(item, true)
- else if Grim.includeDeprecatedAPIs and typeof item.on is 'function'
- @subscribe item, 'destroyed', => @removeItem(item, true)
-
- @items.splice(index, 0, item)
- @emit 'item-added', item, index if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-add-item', {item, index}
- @setActiveItem(item) unless @getActiveItem()?
- item
-
- # Public: Add the given items to the pane.
- #
- # * `items` An {Array} of items to add. Items can be views or models with
- # associated views. Any objects that are already present in the pane's
- # current items will not be added again.
- # * `index` (optional) {Number} index at which to add the items. If omitted,
- # the item is # added after the current active item.
- #
- # Returns an {Array} of added items.
- addItems: (items, index=@getActiveItemIndex() + 1) ->
- items = items.filter (item) => not (item in @items)
- @addItem(item, index + i) for item, i in items
- items
-
- removeItem: (item, destroyed=false) ->
- index = @items.indexOf(item)
- return if index is -1
-
- if Grim.includeDeprecatedAPIs and typeof item.on is 'function'
- @unsubscribe item
- @unsubscribeFromItem(item)
-
- if item is @activeItem
- if @items.length is 1
- @setActiveItem(undefined)
- else if index is 0
- @activateNextItem()
- else
- @activatePreviousItem()
- @items.splice(index, 1)
- @emit 'item-removed', item, index, destroyed if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-remove-item', {item, index, destroyed}
- @container?.didDestroyPaneItem({item, index, pane: this}) if destroyed
- @destroy() if @items.length is 0 and atom.config.get('core.destroyEmptyPanes')
-
- # Public: Move the given item to the given index.
- #
- # * `item` The item to move.
- # * `index` {Number} indicating the index to which to move the item.
- moveItem: (item, newIndex) ->
- oldIndex = @items.indexOf(item)
- @items.splice(oldIndex, 1)
- @items.splice(newIndex, 0, item)
- @emit 'item-moved', item, newIndex if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-move-item', {item, oldIndex, newIndex}
-
- # Public: Move the given item to the given index on another pane.
- #
- # * `item` The item to move.
- # * `pane` {Pane} to which to move the item.
- # * `index` {Number} indicating the index to which to move the item in the
- # given pane.
- moveItemToPane: (item, pane, index) ->
- @removeItem(item)
- pane.addItem(item, index)
-
- # Public: Destroy the active item and activate the next item.
- destroyActiveItem: ->
- @destroyItem(@activeItem)
- false
-
- # Public: Destroy the given item.
- #
- # If the item is active, the next item will be activated. If the item is the
- # last item, the pane will be destroyed if the `core.destroyEmptyPanes` config
- # setting is `true`.
- #
- # * `item` Item to destroy
- destroyItem: (item) ->
- index = @items.indexOf(item)
- if index isnt -1
- @emit 'before-item-destroyed', item if Grim.includeDeprecatedAPIs
- @emitter.emit 'will-destroy-item', {item, index}
- @container?.willDestroyPaneItem({item, index, pane: this})
- if @promptToSaveItem(item)
- @removeItem(item, true)
- item.destroy?()
- true
- else
- false
-
- # Public: Destroy all items.
- destroyItems: ->
- @destroyItem(item) for item in @getItems()
- return
-
- # Public: Destroy all items except for the active item.
- destroyInactiveItems: ->
- @destroyItem(item) for item in @getItems() when item isnt @activeItem
- return
-
- promptToSaveItem: (item, options={}) ->
- return true unless item.shouldPromptToSave?(options)
-
- if typeof item.getURI is 'function'
- uri = item.getURI()
- else if typeof item.getUri is 'function'
- uri = item.getUri()
- else
- return true
-
- chosen = atom.confirm
- message: "'#{item.getTitle?() ? uri}' has changes, do you want to save them?"
- detailedMessage: "Your changes will be lost if you close this item without saving."
- buttons: ["Save", "Cancel", "Don't Save"]
-
- switch chosen
- when 0 then @saveItem(item, -> true)
- when 1 then false
- when 2 then true
-
- # Public: Save the active item.
- saveActiveItem: (nextAction) ->
- @saveItem(@getActiveItem(), nextAction)
-
- # Public: Prompt the user for a location and save the active item with the
- # path they select.
- #
- # * `nextAction` (optional) {Function} which will be called after the item is
- # successfully saved.
- saveActiveItemAs: (nextAction) ->
- @saveItemAs(@getActiveItem(), nextAction)
-
- # Public: Save the given item.
- #
- # * `item` The item to save.
- # * `nextAction` (optional) {Function} which will be called after the item is
- # successfully saved.
- saveItem: (item, nextAction) ->
- if typeof item?.getURI is 'function'
- itemURI = item.getURI()
- else if typeof item?.getUri is 'function'
- itemURI = item.getUri()
-
- if itemURI?
- try
- item.save?()
- catch error
- @handleSaveError(error)
- nextAction?()
- else
- @saveItemAs(item, nextAction)
-
- # Public: Prompt the user for a location and save the active item with the
- # path they select.
- #
- # * `item` The item to save.
- # * `nextAction` (optional) {Function} which will be called after the item is
- # successfully saved.
- saveItemAs: (item, nextAction) ->
- return unless item?.saveAs?
-
- itemPath = item.getPath?()
- newItemPath = atom.showSaveDialogSync(itemPath)
- if newItemPath
- try
- item.saveAs(newItemPath)
- catch error
- @handleSaveError(error)
- nextAction?()
-
- # Public: Save all items.
- saveItems: ->
- @saveItem(item) for item in @getItems()
- return
-
- # Public: Return the first item that matches the given URI or undefined if
- # none exists.
- #
- # * `uri` {String} containing a URI.
- itemForURI: (uri) ->
- find @items, (item) ->
- if typeof item.getURI is 'function'
- itemUri = item.getURI()
- else if typeof item.getUri is 'function'
- itemUri = item.getUri()
-
- itemUri is uri
-
- # Public: Activate the first item that matches the given URI.
- #
- # Returns a {Boolean} indicating whether an item matching the URI was found.
- activateItemForURI: (uri) ->
- if item = @itemForURI(uri)
- @activateItem(item)
- true
- else
- false
-
- copyActiveItem: ->
- if @activeItem?
- @activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize())
-
- ###
- Section: Lifecycle
- ###
-
- # Public: Determine whether the pane is active.
- #
- # Returns a {Boolean}.
- isActive: ->
- @container?.getActivePane() is this
-
- # Public: Makes this pane the *active* pane, causing it to gain focus.
- activate: ->
- throw new Error("Pane has been destroyed") if @isDestroyed()
-
- @container?.setActivePane(this)
- @emit 'activated' if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-activate'
-
- # Public: Close the pane and destroy all its items.
- #
- # If this is the last pane, all the items will be destroyed but the pane
- # itself will not be destroyed.
- destroy: ->
- if @container?.isAlive() and @container.getPanes().length is 1
- @destroyItems()
- else
- super
-
- # Called by model superclass.
- destroyed: ->
- @container.activateNextPane() if @isActive()
- @emitter.emit 'did-destroy'
- @emitter.dispose()
- item.destroy?() for item in @items.slice()
- @container?.didDestroyPane(pane: this)
-
- ###
- Section: Splitting
- ###
-
- # Public: Create a new pane to the left of this pane.
- #
- # * `params` (optional) {Object} with the following keys:
- # * `items` (optional) {Array} of items to add to the new pane.
- # * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
- #
- # Returns the new {Pane}.
- splitLeft: (params) ->
- @split('horizontal', 'before', params)
-
- # Public: Create a new pane to the right of this pane.
- #
- # * `params` (optional) {Object} with the following keys:
- # * `items` (optional) {Array} of items to add to the new pane.
- # * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
- #
- # Returns the new {Pane}.
- splitRight: (params) ->
- @split('horizontal', 'after', params)
-
- # Public: Creates a new pane above the receiver.
- #
- # * `params` (optional) {Object} with the following keys:
- # * `items` (optional) {Array} of items to add to the new pane.
- # * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
- #
- # Returns the new {Pane}.
- splitUp: (params) ->
- @split('vertical', 'before', params)
-
- # Public: Creates a new pane below the receiver.
- #
- # * `params` (optional) {Object} with the following keys:
- # * `items` (optional) {Array} of items to add to the new pane.
- # * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
- #
- # Returns the new {Pane}.
- splitDown: (params) ->
- @split('vertical', 'after', params)
-
- split: (orientation, side, params) ->
- if params?.copyActiveItem
- params.items ?= []
- params.items.push(@copyActiveItem())
-
- if @parent.orientation isnt orientation
- @parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this], @flexScale}))
- @setFlexScale(1)
-
- newPane = new @constructor(params)
- switch side
- when 'before' then @parent.insertChildBefore(this, newPane)
- when 'after' then @parent.insertChildAfter(this, newPane)
-
- newPane.activate()
- newPane
-
- # If the parent is a horizontal axis, returns its first child if it is a pane;
- # otherwise returns this pane.
- findLeftmostSibling: ->
- if @parent.orientation is 'horizontal'
- [leftmostSibling] = @parent.children
- if leftmostSibling instanceof PaneAxis
- this
- else
- leftmostSibling
- else
- this
-
- # 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: ->
- if @parent.orientation is 'horizontal'
- rightmostSibling = last(@parent.children)
- if rightmostSibling instanceof PaneAxis
- @splitRight()
- else
- rightmostSibling
- else
- @splitRight()
-
- close: ->
- @destroy() if @confirmClose()
-
- confirmClose: ->
- for item in @getItems()
- return false unless @promptToSaveItem(item)
- true
-
- handleSaveError: (error) ->
- if error.code is 'EISDIR' or error.message.endsWith('is a directory')
- atom.notifications.addWarning("Unable to save file: #{error.message}")
- else if error.code is 'EACCES' and error.path?
- atom.notifications.addWarning("Unable to save file: Permission denied '#{error.path}'")
- else if error.code in ['EPERM', 'EBUSY', 'UNKNOWN', 'EEXIST'] and error.path?
- atom.notifications.addWarning("Unable to save file '#{error.path}'", detail: error.message)
- else if error.code is 'EROFS' and error.path?
- atom.notifications.addWarning("Unable to save file: Read-only file system '#{error.path}'")
- else if error.code is 'ENOSPC' and error.path?
- atom.notifications.addWarning("Unable to save file: No space left on device '#{error.path}'")
- else if error.code is 'ENXIO' and error.path?
- atom.notifications.addWarning("Unable to save file: No such device or address '#{error.path}'")
- else if errorMatch = /ENOTDIR, not a directory '([^']+)'/.exec(error.message)
- fileName = errorMatch[1]
- atom.notifications.addWarning("Unable to save file: A directory in the path '#{fileName}' could not be written to")
- else
- throw error
-
-if Grim.includeDeprecatedAPIs
- Pane.properties
- container: undefined
- activeItem: undefined
- focused: false
-
- Pane.behavior 'active', ->
- @$container
- .switch((container) -> container?.$activePane)
- .map((activePane) => activePane is this)
- .distinctUntilChanged()
-
- Pane::on = (eventName) ->
- switch eventName
- when 'activated'
- Grim.deprecate("Use Pane::onDidActivate instead")
- when 'destroyed'
- Grim.deprecate("Use Pane::onDidDestroy instead")
- when 'item-added'
- Grim.deprecate("Use Pane::onDidAddItem instead")
- when 'item-removed'
- Grim.deprecate("Use Pane::onDidRemoveItem instead")
- when 'item-moved'
- Grim.deprecate("Use Pane::onDidMoveItem instead")
- when 'before-item-destroyed'
- Grim.deprecate("Use Pane::onWillDestroyItem instead")
- else
- Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.")
- super
-
- Pane::behavior = (behaviorName) ->
- switch behaviorName
- when 'active'
- Grim.deprecate("The $active behavior property is deprecated. Use ::observeActive or ::onDidChangeActive instead.")
- when 'container'
- Grim.deprecate("The $container behavior property is deprecated.")
- when 'activeItem'
- Grim.deprecate("The $activeItem behavior property is deprecated. Use ::observeActiveItem or ::onDidChangeActiveItem instead.")
- when 'focused'
- Grim.deprecate("The $focused behavior property is deprecated.")
- else
- Grim.deprecate("Pane::behavior is deprecated. Use event subscription methods instead.")
-
- super
-
- Pane::itemForUri = (uri) ->
- Grim.deprecate("Use `::itemForURI` instead.")
- @itemForURI(uri)
-
- Pane::activateItemForUri = (uri) ->
- Grim.deprecate("Use `::activateItemForURI` instead.")
- @activateItemForURI(uri)
-else
- Pane::container = undefined
- Pane::activeItem = undefined
- Pane::focused = undefined
diff --git a/src/pane.js b/src/pane.js
new file mode 100644
index 00000000000..a55575ae049
--- /dev/null
+++ b/src/pane.js
@@ -0,0 +1,1430 @@
+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);
+ }
+}
diff --git a/src/panel-container-element.coffee b/src/panel-container-element.coffee
deleted file mode 100644
index e1459e6d63c..00000000000
--- a/src/panel-container-element.coffee
+++ /dev/null
@@ -1,47 +0,0 @@
-{CompositeDisposable} = require 'event-kit'
-
-class PanelContainerElement extends HTMLElement
- createdCallback: ->
- @subscriptions = new CompositeDisposable
-
- initialize: (@model) ->
- @subscriptions.add @model.onDidAddPanel(@panelAdded.bind(this))
- @subscriptions.add @model.onDidRemovePanel(@panelRemoved.bind(this))
- @subscriptions.add @model.onDidDestroy(@destroyed.bind(this))
- @classList.add(@model.getLocation())
- this
-
- getModel: -> @model
-
- panelAdded: ({panel, index}) ->
- panelElement = atom.views.getView(panel)
- panelElement.classList.add(@model.getLocation())
- if @model.isModal()
- panelElement.classList.add("overlay", "from-top")
- else
- panelElement.classList.add("tool-panel", "panel-#{@model.getLocation()}")
-
- if index >= @childNodes.length
- @appendChild(panelElement)
- else
- referenceItem = @childNodes[index]
- @insertBefore(panelElement, referenceItem)
-
- if @model.isModal()
- @hideAllPanelsExcept(panel)
- @subscriptions.add panel.onDidChangeVisible (visible) =>
- @hideAllPanelsExcept(panel) if visible
-
- panelRemoved: ({panel, index}) ->
- @removeChild(atom.views.getView(panel))
-
- destroyed: ->
- @subscriptions.dispose()
- @parentNode?.removeChild(this)
-
- hideAllPanelsExcept: (excludedPanel) ->
- for panel in @model.getPanels()
- panel.hide() unless panel is excludedPanel
- return
-
-module.exports = PanelContainerElement = document.registerElement 'atom-panel-container', prototype: PanelContainerElement.prototype
diff --git a/src/panel-container-element.js b/src/panel-container-element.js
new file mode 100644
index 00000000000..4e7f6024f38
--- /dev/null
+++ b/src/panel-container-element.js
@@ -0,0 +1,123 @@
+'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
+};
diff --git a/src/panel-container.coffee b/src/panel-container.coffee
deleted file mode 100644
index 322773f6998..00000000000
--- a/src/panel-container.coffee
+++ /dev/null
@@ -1,71 +0,0 @@
-{Emitter, CompositeDisposable} = require 'event-kit'
-
-module.exports =
-class PanelContainer
- constructor: ({@location}={}) ->
- @emitter = new Emitter
- @subscriptions = new CompositeDisposable
- @panels = []
-
- destroy: ->
- panel.destroy() for panel in @getPanels()
- @subscriptions.dispose()
- @emitter.emit 'did-destroy', this
- @emitter.dispose()
-
- ###
- Section: Event Subscription
- ###
-
- onDidAddPanel: (callback) ->
- @emitter.on 'did-add-panel', callback
-
- onDidRemovePanel: (callback) ->
- @emitter.on 'did-remove-panel', callback
-
- onDidDestroy: (callback) ->
- @emitter.on 'did-destroy', callback
-
- ###
- Section: Panels
- ###
-
- getLocation: -> @location
-
- isModal: -> @location is 'modal'
-
- getPanels: -> @panels
-
- addPanel: (panel) ->
- @subscriptions.add panel.onDidDestroy(@panelDestroyed.bind(this))
-
- index = @getPanelIndex(panel)
- if index is @panels.length
- @panels.push(panel)
- else
- @panels.splice(index, 0, panel)
-
- @emitter.emit 'did-add-panel', {panel, index}
- panel
-
- panelForItem: (item) ->
- for panel in @panels
- return panel if panel.getItem() is item
- null
-
- panelDestroyed: (panel) ->
- index = @panels.indexOf(panel)
- if index > -1
- @panels.splice(index, 1)
- @emitter.emit 'did-remove-panel', {panel, index}
-
- getPanelIndex: (panel) ->
- priority = panel.getPriority()
- if @location in ['bottom', 'right']
- for p, i in @panels by -1
- return i + 1 if priority < p.getPriority()
- 0
- else
- for p, i in @panels
- return i if priority < p.getPriority()
- @panels.length
diff --git a/src/panel-container.js b/src/panel-container.js
new file mode 100644
index 00000000000..5a003f3f83c
--- /dev/null
+++ b/src/panel-container.js
@@ -0,0 +1,118 @@
+'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;
+ }
+ }
+};
diff --git a/src/panel-element.coffee b/src/panel-element.coffee
deleted file mode 100644
index 69b836aeb10..00000000000
--- a/src/panel-element.coffee
+++ /dev/null
@@ -1,37 +0,0 @@
-{CompositeDisposable} = require 'event-kit'
-{callAttachHooks} = require './space-pen-extensions'
-Panel = require './panel'
-
-class PanelElement extends HTMLElement
- createdCallback: ->
- @subscriptions = new CompositeDisposable
-
- initialize: (@model) ->
- @appendChild(@getItemView())
-
- @classList.add(@model.getClassName().split(' ')...) if @model.getClassName()?
- @subscriptions.add @model.onDidChangeVisible(@visibleChanged.bind(this))
- @subscriptions.add @model.onDidDestroy(@destroyed.bind(this))
- this
-
- getModel: ->
- @model ?= new Panel
-
- getItemView: ->
- atom.views.getView(@getModel().getItem())
-
- attachedCallback: ->
- callAttachHooks(@getItemView()) # for backward compatibility with SpacePen views
- @visibleChanged(@getModel().isVisible())
-
- visibleChanged: (visible) ->
- if visible
- @style.display = null
- else
- @style.display = 'none'
-
- destroyed: ->
- @subscriptions.dispose()
- @parentNode?.removeChild(this)
-
-module.exports = PanelElement = document.registerElement 'atom-panel', prototype: PanelElement.prototype
diff --git a/src/panel.coffee b/src/panel.coffee
deleted file mode 100644
index 6a1e2be0929..00000000000
--- a/src/panel.coffee
+++ /dev/null
@@ -1,75 +0,0 @@
-{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: [tree-view](https://github.com/atom/tree-view),
-# [status-bar](https://github.com/atom/status-bar),
-# and [find-and-replace](https://github.com/atom/find-and-replace) all use
-# panels.
-module.exports =
-class Panel
- ###
- Section: Construction and Destruction
- ###
-
- constructor: ({@item, @visible, @priority, @className}={}) ->
- @emitter = new Emitter
- @visible ?= true
- @priority ?= 100
-
- # Public: Destroy and remove this panel from the UI.
- destroy: ->
- @hide()
- @emitter.emit 'did-destroy', this
- @emitter.dispose()
-
- ###
- 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) ->
- @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) ->
- @emitter.on 'did-destroy', callback
-
- ###
- Section: Panel Details
- ###
-
- # Public: Returns the panel's item.
- getItem: -> @item
-
- # Public: Returns a {Number} indicating this panel's priority.
- getPriority: -> @priority
-
- getClassName: -> @className
-
- # Public: Returns a {Boolean} true when the panel is visible.
- isVisible: -> @visible
-
- # Public: Hide this panel
- hide: ->
- wasVisible = @visible
- @visible = false
- @emitter.emit 'did-change-visible', @visible if wasVisible
-
- # Public: Show this panel
- show: ->
- wasVisible = @visible
- @visible = true
- @emitter.emit 'did-change-visible', @visible unless wasVisible
diff --git a/src/panel.js b/src/panel.js
new file mode 100644
index 00000000000..accc419cc82
--- /dev/null
+++ b/src/panel.js
@@ -0,0 +1,109 @@
+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);
+ }
+};
diff --git a/src/path-watcher.js b/src/path-watcher.js
new file mode 100644
index 00000000000..11064eaf28a
--- /dev/null
+++ b/src/path-watcher.js
@@ -0,0 +1,822 @@
+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 };
diff --git a/src/project.coffee b/src/project.coffee
deleted file mode 100644
index 75cdb714fc8..00000000000
--- a/src/project.coffee
+++ /dev/null
@@ -1,498 +0,0 @@
-path = require 'path'
-url = require 'url'
-
-_ = require 'underscore-plus'
-fs = require 'fs-plus'
-Q = require 'q'
-{includeDeprecatedAPIs, deprecate} = require 'grim'
-{Emitter} = require 'event-kit'
-Serializable = require 'serializable'
-TextBuffer = require 'text-buffer'
-Grim = require 'grim'
-
-DefaultDirectoryProvider = require './default-directory-provider'
-Model = require './model'
-TextEditor = require './text-editor'
-Task = require './task'
-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
- atom.deserializers.add(this)
- Serializable.includeInto(this)
-
- ###
- Section: Construction and Destruction
- ###
-
- constructor: ({path, paths, @buffers}={}) ->
- @emitter = new Emitter
- @buffers ?= []
- @rootDirectories = []
- @repositories = []
-
- @directoryProviders = [new DefaultDirectoryProvider()]
- atom.packages.serviceHub.consume(
- 'atom.directory-provider',
- '^0.1.0',
- # New providers are added to the front of @directoryProviders because
- # DefaultDirectoryProvider is a catch-all that will always provide a Directory.
- (provider) => @directoryProviders.unshift(provider))
-
- # Mapping from the real path of a {Directory} to a {Promise} that resolves
- # to either a {Repository} or null. Ideally, the {Directory} would be used
- # as the key; however, there can be multiple {Directory} objects created for
- # the same real path, so it is not a good key.
- @repositoryPromisesByPath = new Map()
-
- # Note that the GitRepositoryProvider is registered synchronously so that
- # it is available immediately on startup.
- @repositoryProviders = [new GitRepositoryProvider(this)]
- atom.packages.serviceHub.consume(
- 'atom.repository-provider',
- '^0.1.0',
- (provider) =>
- @repositoryProviders.push(provider)
-
- # If a path in getPaths() does not have a corresponding Repository, try
- # to assign one by running through setPaths() again now that
- # @repositoryProviders has been updated.
- if null in @repositories
- @setPaths(@getPaths())
- )
-
- @subscribeToBuffer(buffer) for buffer in @buffers
-
- if Grim.includeDeprecatedAPIs and path?
- Grim.deprecate("Pass 'paths' array instead of 'path' to project constructor")
-
- paths ?= _.compact([path])
- @setPaths(paths)
-
- destroyed: ->
- buffer.destroy() for buffer in @getBuffers()
- @setPaths([])
-
- destroyUnretainedBuffers: ->
- buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained()
- return
-
- ###
- Section: Serialization
- ###
-
- serializeParams: ->
- paths: @getPaths()
- buffers: _.compact(@buffers.map (buffer) -> buffer.serialize() if buffer.isRetained())
-
- deserializeParams: (params) ->
- params.buffers = _.compact params.buffers.map (bufferState) ->
- # Check that buffer's file path is accessible
- return if fs.isDirectorySync(bufferState.filePath)
- if bufferState.filePath
- try
- fs.closeSync(fs.openSync(bufferState.filePath, 'r'))
- catch error
- return unless error.code is 'ENOENT'
-
- atom.deserializers.deserialize(bufferState)
- params
-
- ###
- Section: Event Subscription
- ###
-
- # Public: Invoke the given callback when the project paths change.
- #
- # * `callback` {Function} to be called after the project paths change.
- # * `projectPaths` An {Array} of {String} project paths.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangePaths: (callback) ->
- @emitter.on 'did-change-paths', callback
-
- onDidAddBuffer: (callback) ->
- @emitter.on 'did-add-buffer', callback
-
- ###
- Section: Accessing the git repository
- ###
-
- # Public: Get an {Array} of {GitRepository}s associated with the project's
- # directories.
- #
- # This method will be removed in 2.0 because it does synchronous I/O.
- # Prefer the following, which evaluates to a {Promise} that resolves to an
- # {Array} of {Repository} objects:
- # ```
- # Promise.all(project.getDirectories().map(
- # project.repositoryForDirectory.bind(project)))
- # ```
- getRepositories: -> @repositories
-
- # Public: Get the repository for a given directory asynchronously.
- #
- # * `directory` {Directory} for which to get a {Repository}.
- #
- # Returns a {Promise} that resolves with either:
- # * {Repository} if a repository can be created for the given directory
- # * `null` if no repository can be created for the given directory.
- repositoryForDirectory: (directory) ->
- pathForDirectory = directory.getRealPathSync()
- promise = @repositoryPromisesByPath.get(pathForDirectory)
- unless promise
- promises = @repositoryProviders.map (provider) ->
- provider.repositoryForDirectory(directory)
- promise = Promise.all(promises).then (repositories) =>
- repo = _.find(repositories, (repo) -> repo?) ? null
-
- # If no repository is found, remove the entry in for the directory in
- # @repositoryPromisesByPath in case some other RepositoryProvider is
- # registered in the future that could supply a Repository for the
- # directory.
- @repositoryPromisesByPath.delete(pathForDirectory) unless repo?
- repo
- @repositoryPromisesByPath.set(pathForDirectory, promise)
- promise
-
- ###
- Section: Managing Paths
- ###
-
- # Public: Get an {Array} of {String}s containing the paths of the project's
- # directories.
- getPaths: -> rootDirectory.getPath() for rootDirectory in @rootDirectories
-
- # Public: Set the paths of the project's directories.
- #
- # * `projectPaths` {Array} of {String} paths.
- setPaths: (projectPaths) ->
- if includeDeprecatedAPIs
- rootDirectory.off() for rootDirectory in @rootDirectories
-
- repository?.destroy() for repository in @repositories
- @rootDirectories = []
- @repositories = []
-
- @addPath(projectPath, emitEvent: false) for projectPath in projectPaths
-
- @emit "path-changed" if includeDeprecatedAPIs
- @emitter.emit 'did-change-paths', projectPaths
-
- # Public: Add a path to the project's list of root paths
- #
- # * `projectPath` {String} The path to the directory to add.
- addPath: (projectPath, options) ->
- for directory in @getDirectories()
- # Apparently a Directory does not believe it can contain itself, so we
- # must also check whether the paths match.
- return if directory.contains(projectPath) or directory.getPath() is projectPath
-
- directory = null
- for provider in @directoryProviders
- break if directory = provider.directoryForURISync?(projectPath)
- if directory is null
- # This should never happen because DefaultDirectoryProvider should always
- # return a Directory.
- throw new Error(projectPath + ' could not be resolved to a directory')
- @rootDirectories.push(directory)
-
- repo = null
- for provider in @repositoryProviders
- break if repo = provider.repositoryForDirectorySync?(directory)
- @repositories.push(repo ? null)
-
- unless options?.emitEvent is false
- @emit "path-changed" if includeDeprecatedAPIs
- @emitter.emit 'did-change-paths', @getPaths()
-
- # Public: remove a path from the project's list of root paths.
- #
- # * `projectPath` {String} The path to remove.
- removePath: (projectPath) ->
- # The projectPath may be a URI, in which case it should not be normalized.
- unless projectPath in @getPaths()
- projectPath = path.normalize(projectPath)
-
- indexToRemove = null
- for directory, i in @rootDirectories
- if directory.getPath() is projectPath
- indexToRemove = i
- break
-
- if indexToRemove?
- [removedDirectory] = @rootDirectories.splice(indexToRemove, 1)
- [removedRepository] = @repositories.splice(indexToRemove, 1)
- removedDirectory.off() if includeDeprecatedAPIs
- removedRepository?.destroy() unless removedRepository in @repositories
- @emit "path-changed" if includeDeprecatedAPIs
- @emitter.emit "did-change-paths", @getPaths()
- true
- else
- false
-
- # Public: Get an {Array} of {Directory}s associated with this project.
- getDirectories: ->
- @rootDirectories
-
- resolvePath: (uri) ->
- return unless uri
-
- if uri?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme
- uri
- else
- if fs.isAbsolute(uri)
- path.normalize(fs.absolute(uri))
-
- # TODO: what should we do here when there are multiple directories?
- else if projectPath = @getPaths()[0]
- path.normalize(fs.absolute(path.join(projectPath, uri)))
- else
- undefined
-
- relativize: (fullPath) ->
- @relativizePath(fullPath)[1]
-
- # Public: Get the path to the project directory that contains the given path,
- # and the relative path from that project directory to the given path.
- #
- # * `fullPath` {String} An absolute path.
- #
- # Returns an {Array} with two elements:
- # * `projectPath` The {String} path to the project directory that contains the
- # given path, or `null` if none is found.
- # * `relativePath` {String} The relative path from the project directory to
- # the given path.
- relativizePath: (fullPath) ->
- for rootDirectory in @rootDirectories
- relativePath = rootDirectory.relativize(fullPath)
- return [rootDirectory.getPath(), relativePath] unless relativePath is fullPath
- [null, fullPath]
-
- # Public: Determines whether the given path (real or symbolic) is inside the
- # project's directory.
- #
- # This method does not actually check if the path exists, it just checks their
- # locations relative to each other.
- #
- # ## Examples
- #
- # Basic operation
- #
- # ```coffee
- # # Project's root directory is /foo/bar
- # project.contains('/foo/bar/baz') # => true
- # project.contains('/usr/lib/baz') # => false
- # ```
- #
- # Existence of the path is not required
- #
- # ```coffee
- # # Project's root directory is /foo/bar
- # fs.existsSync('/foo/bar/baz') # => false
- # project.contains('/foo/bar/baz') # => true
- # ```
- #
- # * `pathToCheck` {String} path
- #
- # Returns whether the path is inside the project's root directory.
- contains: (pathToCheck) ->
- @rootDirectories.some (dir) -> dir.contains(pathToCheck)
-
- ###
- Section: Private
- ###
-
- # Given a path to a file, this constructs and associates a new
- # {TextEditor}, showing the file.
- #
- # * `filePath` The {String} path of the file to associate with.
- # * `options` Options that you can pass to the {TextEditor} constructor.
- #
- # Returns a promise that resolves to an {TextEditor}.
- open: (filePath, options={}) ->
- filePath = @resolvePath(filePath)
-
- if filePath?
- try
- fs.closeSync(fs.openSync(filePath, 'r'))
- catch error
- # allow ENOENT errors to create an editor for paths that dont exist
- throw error unless error.code is 'ENOENT'
-
- @bufferForPath(filePath).then (buffer) =>
- @buildEditorForBuffer(buffer, options)
-
- # Retrieves all the {TextBuffer}s in the project; that is, the
- # buffers for all open files.
- #
- # Returns an {Array} of {TextBuffer}s.
- getBuffers: ->
- @buffers.slice()
-
- # Is the buffer for the given path modified?
- isPathModified: (filePath) ->
- @findBufferForPath(@resolvePath(filePath))?.isModified()
-
- findBufferForPath: (filePath) ->
- _.find @buffers, (buffer) -> buffer.getPath() is filePath
-
- # Only to be used in specs
- bufferForPathSync: (filePath) ->
- absoluteFilePath = @resolvePath(filePath)
- existingBuffer = @findBufferForPath(absoluteFilePath) if filePath
- existingBuffer ? @buildBufferSync(absoluteFilePath)
-
- # 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: (filePath) ->
- absoluteFilePath = @resolvePath(filePath)
- existingBuffer = @findBufferForPath(absoluteFilePath) if absoluteFilePath
- Q(existingBuffer ? @buildBuffer(absoluteFilePath))
-
- bufferForId: (id) ->
- _.find @buffers, (buffer) -> buffer.id is id
-
- # Still needed when deserializing a tokenized buffer
- buildBufferSync: (absoluteFilePath) ->
- buffer = new TextBuffer({filePath: absoluteFilePath})
- @addBuffer(buffer)
- buffer.loadSync()
- buffer
-
- # Given a file path, this sets its {TextBuffer}.
- #
- # * `absoluteFilePath` A {String} representing a path.
- # * `text` The {String} text to use as a buffer.
- #
- # Returns a promise that resolves to the {TextBuffer}.
- buildBuffer: (absoluteFilePath) ->
- if fs.getSizeSync(absoluteFilePath) >= 2 * 1048576 # 2MB
- error = new Error("Atom can only handle files < 2MB for now.")
- error.code = 'EFILETOOLARGE'
- throw error
-
- buffer = new TextBuffer({filePath: absoluteFilePath})
- @addBuffer(buffer)
- buffer.load()
- .then((buffer) -> buffer)
- .catch(=> @removeBuffer(buffer))
-
- addBuffer: (buffer, options={}) ->
- @addBufferAtIndex(buffer, @buffers.length, options)
- @subscribeToBuffer(buffer)
-
- addBufferAtIndex: (buffer, index, options={}) ->
- @buffers.splice(index, 0, buffer)
- @subscribeToBuffer(buffer)
- @emit 'buffer-created', buffer if includeDeprecatedAPIs
- @emitter.emit 'did-add-buffer', buffer
- buffer
-
- # Removes a {TextBuffer} association from the project.
- #
- # Returns the removed {TextBuffer}.
- removeBuffer: (buffer) ->
- index = @buffers.indexOf(buffer)
- @removeBufferAtIndex(index) unless index is -1
-
- removeBufferAtIndex: (index, options={}) ->
- [buffer] = @buffers.splice(index, 1)
- buffer?.destroy()
-
- buildEditorForBuffer: (buffer, editorOptions) ->
- editor = new TextEditor(_.extend({buffer, registerEditor: true}, editorOptions))
- editor
-
- eachBuffer: (args...) ->
- subscriber = args.shift() if args.length > 1
- callback = args.shift()
-
- callback(buffer) for buffer in @getBuffers()
- if subscriber
- subscriber.subscribe this, 'buffer-created', (buffer) -> callback(buffer)
- else
- @on 'buffer-created', (buffer) -> callback(buffer)
-
- subscribeToBuffer: (buffer) ->
- buffer.onDidDestroy => @removeBuffer(buffer)
- buffer.onWillThrowWatchError ({error, handle}) ->
- handle()
- atom.notifications.addWarning """
- Unable to read file after file `#{error.eventType}` event.
- Make sure you have permission to access `#{buffer.getPath()}`.
- """,
- detail: error.message
- dismissable: true
-
-if includeDeprecatedAPIs
- Project.pathForRepositoryUrl = (repoUrl) ->
- deprecate '::pathForRepositoryUrl will be removed. Please remove from your code.'
- [repoName] = url.parse(repoUrl).path.split('/')[-1..]
- repoName = repoName.replace(/\.git$/, '')
- path.join(atom.config.get('core.projectHome'), repoName)
-
- Project::registerOpener = (opener) ->
- deprecate("Use Workspace::addOpener instead")
- atom.workspace.addOpener(opener)
-
- Project::unregisterOpener = (opener) ->
- deprecate("Call .dispose() on the Disposable returned from ::addOpener instead")
- atom.workspace.unregisterOpener(opener)
-
- Project::eachEditor = (callback) ->
- deprecate("Use Workspace::observeTextEditors instead")
- atom.workspace.observeTextEditors(callback)
-
- Project::getEditors = ->
- deprecate("Use Workspace::getTextEditors instead")
- atom.workspace.getTextEditors()
-
- Project::on = (eventName) ->
- if eventName is 'path-changed'
- Grim.deprecate("Use Project::onDidChangePaths instead")
- else
- Grim.deprecate("Project::on is deprecated. Use documented event subscription methods instead.")
- super
-
- Project::getRepo = ->
- Grim.deprecate("Use ::getRepositories instead")
- @getRepositories()[0]
-
- Project::getPath = ->
- Grim.deprecate("Use ::getPaths instead")
- @getPaths()[0]
-
- Project::setPath = (path) ->
- Grim.deprecate("Use ::setPaths instead")
- @setPaths([path])
-
- Project::getRootDirectory = ->
- Grim.deprecate("Use ::getDirectories instead")
- @getDirectories()[0]
-
- Project::resolve = (uri) ->
- Grim.deprecate("Use `Project::getDirectories()[0]?.resolve()` instead")
- @resolvePath(uri)
-
- Project::scan = (regex, options={}, iterator) ->
- Grim.deprecate("Use atom.workspace.scan instead of atom.project.scan")
- atom.workspace.scan(regex, options, iterator)
-
- Project::replace = (regex, replacementText, filePaths, iterator) ->
- Grim.deprecate("Use atom.workspace.replace instead of atom.project.replace")
- atom.workspace.replace(regex, replacementText, filePaths, iterator)
-
- Project::openSync = (filePath, options={}) ->
- deprecate("Use Project::open instead")
- filePath = @resolvePath(filePath)
- @buildEditorForBuffer(@bufferForPathSync(filePath), options)
diff --git a/src/project.js b/src/project.js
new file mode 100644
index 00000000000..d917c6fe812
--- /dev/null
+++ b/src/project.js
@@ -0,0 +1,876 @@
+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
+ });
+ });
+ }
+};
diff --git a/src/protocol-handler-installer.js b/src/protocol-handler-installer.js
new file mode 100644
index 00000000000..6b904fc7a49
--- /dev/null
+++ b/src/protocol-handler-installer.js
@@ -0,0 +1,117 @@
+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)
+ }
+ ]
+ }
+ );
+ }
+};
diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee
new file mode 100644
index 00000000000..35b566fe7a2
--- /dev/null
+++ b/src/register-default-commands.coffee
@@ -0,0 +1,348 @@
+{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)
diff --git a/src/reopen-project-list-view.js b/src/reopen-project-list-view.js
new file mode 100644
index 00000000000..2ae7577e763
--- /dev/null
+++ b/src/reopen-project-list-view.js
@@ -0,0 +1,77 @@
+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(', ');
+ }
+};
diff --git a/src/reopen-project-menu-manager.js b/src/reopen-project-menu-manager.js
new file mode 100644
index 00000000000..d1d55395780
--- /dev/null
+++ b/src/reopen-project-menu-manager.js
@@ -0,0 +1,175 @@
+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);
+ }
+};
diff --git a/src/repository-status-handler.coffee b/src/repository-status-handler.coffee
deleted file mode 100644
index d0763fd5af3..00000000000
--- a/src/repository-status-handler.coffee
+++ /dev/null
@@ -1,35 +0,0 @@
-Git = require 'git-utils'
-path = require 'path'
-
-module.exports = (repoPath) ->
- repo = Git.open(repoPath)
-
- upstream = {}
- statuses = {}
- submodules = {}
- branch = null
-
- if repo?
- # Statuses in main repo
- workingDirectoryPath = repo.getWorkingDirectory()
- for filePath, status of repo.getStatus()
- statuses[filePath] = status
-
- # Statuses in submodules
- for submodulePath, submoduleRepo of repo.submodules
- submodules[submodulePath] =
- branch: submoduleRepo.getHead()
- upstream: submoduleRepo.getAheadBehindCount()
-
- workingDirectoryPath = submoduleRepo.getWorkingDirectory()
- for filePath, status of submoduleRepo.getStatus()
- absolutePath = path.join(workingDirectoryPath, filePath)
- # Make path relative to parent repository
- relativePath = repo.relativize(absolutePath)
- statuses[relativePath] = status
-
- upstream = repo.getAheadBehindCount()
- branch = repo.getHead()
- repo.release()
-
- {statuses, upstream, branch, submodules}
diff --git a/src/ripgrep-directory-searcher.js b/src/ripgrep-directory-searcher.js
new file mode 100644
index 00000000000..55bfe4e1d04
--- /dev/null
+++ b/src/ripgrep-directory-searcher.js
@@ -0,0 +1,426 @@
+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;
+ }
+};
diff --git a/src/row-map.coffee b/src/row-map.coffee
deleted file mode 100644
index 5510c142114..00000000000
--- a/src/row-map.coffee
+++ /dev/null
@@ -1,120 +0,0 @@
-{spliceWithArray} = require 'underscore-plus'
-
-# Used by the display buffer to map screen rows to buffer rows and vice-versa.
-# This mapping may not be 1:1 due to folds and soft-wraps. This object maintains
-# an array of regions, which contain `bufferRows` and `screenRows` fields.
-#
-# Rectangular Regions:
-# If a region has the same number of buffer rows and screen rows, it is referred
-# to as "rectangular", and represents one or more non-soft-wrapped, non-folded
-# lines.
-#
-# Trapezoidal Regions:
-# If a region has one buffer row and more than one screen row, it represents a
-# soft-wrapped line. If a region has one screen row and more than one buffer
-# row, it represents folded lines
-module.exports =
-class RowMap
- constructor: ->
- @regions = []
-
- # Public: Returns a copy of all the regions in the map
- getRegions: ->
- @regions.slice()
-
- # Public: Returns an end-row-exclusive range of screen rows corresponding to
- # the given buffer row. If the buffer row is soft-wrapped, the range may span
- # multiple screen rows. Otherwise it will span a single screen row.
- screenRowRangeForBufferRow: (targetBufferRow) ->
- {region, bufferRows, screenRows} = @traverseToBufferRow(targetBufferRow)
-
- if region? and region.bufferRows isnt region.screenRows
- [screenRows, screenRows + region.screenRows]
- else
- screenRows += targetBufferRow - bufferRows
- [screenRows, screenRows + 1]
-
- # Public: Returns an end-row-exclusive range of buffer rows corresponding to
- # the given screen row. If the screen row is the first line of a folded range
- # of buffer rows, the range may span multiple buffer rows. Otherwise it will
- # span a single buffer row.
- bufferRowRangeForScreenRow: (targetScreenRow) ->
- {region, screenRows, bufferRows} = @traverseToScreenRow(targetScreenRow)
- if region? and region.bufferRows isnt region.screenRows
- [bufferRows, bufferRows + region.bufferRows]
- else
- bufferRows += targetScreenRow - screenRows
- [bufferRows, bufferRows + 1]
-
- # Public: If the given buffer row is part of a folded row range, returns that
- # row range. Otherwise returns a range spanning only the given buffer row.
- bufferRowRangeForBufferRow: (targetBufferRow) ->
- {region, bufferRows} = @traverseToBufferRow(targetBufferRow)
- if region? and region.bufferRows isnt region.screenRows
- [bufferRows, bufferRows + region.bufferRows]
- else
- [targetBufferRow, targetBufferRow + 1]
-
- # Public: Given a starting buffer row, the number of buffer rows to replace,
- # and an array of regions of shape {bufferRows: n, screenRows: m}, splices
- # the regions at the appropriate location in the map. This method is used by
- # display buffer to keep the map updated when the underlying buffer changes.
- spliceRegions: (startBufferRow, bufferRowCount, regions) ->
- endBufferRow = startBufferRow + bufferRowCount
- {index, bufferRows} = @traverseToBufferRow(startBufferRow)
- precedingRows = startBufferRow - bufferRows
-
- count = 0
- while region = @regions[index + count]
- count++
- bufferRows += region.bufferRows
- if bufferRows >= endBufferRow
- followingRows = bufferRows - endBufferRow
- break
-
- if precedingRows > 0
- regions.unshift({bufferRows: precedingRows, screenRows: precedingRows})
-
- if followingRows > 0
- regions.push({bufferRows: followingRows, screenRows: followingRows})
-
- spliceWithArray(@regions, index, count, regions)
- @mergeAdjacentRectangularRegions(index - 1, index + regions.length)
-
- traverseToBufferRow: (targetBufferRow) ->
- bufferRows = 0
- screenRows = 0
- for region, index in @regions
- if (bufferRows + region.bufferRows) > targetBufferRow
- return {region, index, screenRows, bufferRows}
- bufferRows += region.bufferRows
- screenRows += region.screenRows
- {index, screenRows, bufferRows}
-
- traverseToScreenRow: (targetScreenRow) ->
- bufferRows = 0
- screenRows = 0
- for region, index in @regions
- if (screenRows + region.screenRows) > targetScreenRow
- return {region, index, screenRows, bufferRows}
- bufferRows += region.bufferRows
- screenRows += region.screenRows
- {index, screenRows, bufferRows}
-
- mergeAdjacentRectangularRegions: (startIndex, endIndex) ->
- for index in [endIndex..startIndex]
- if 0 < index < @regions.length
- leftRegion = @regions[index - 1]
- rightRegion = @regions[index]
- leftIsRectangular = leftRegion.bufferRows is leftRegion.screenRows
- rightIsRectangular = rightRegion.bufferRows is rightRegion.screenRows
- if leftIsRectangular and rightIsRectangular
- @regions.splice index - 1, 2,
- bufferRows: leftRegion.bufferRows + rightRegion.bufferRows
- screenRows: leftRegion.screenRows + rightRegion.screenRows
- return
-
- # Public: Returns an array of strings describing the map's regions.
- inspect: ->
- for {bufferRows, screenRows} in @regions
- "#{bufferRows}:#{screenRows}"
diff --git a/src/safe-clipboard.coffee b/src/safe-clipboard.coffee
deleted file mode 100644
index 8301f9d54e2..00000000000
--- a/src/safe-clipboard.coffee
+++ /dev/null
@@ -1,6 +0,0 @@
-# Using clipboard in renderer process is not safe on Linux.
-module.exports =
- if process.platform is 'linux' and process.type is 'renderer'
- require('remote').require('clipboard')
- else
- require('clipboard')
diff --git a/src/scan-handler.coffee b/src/scan-handler.coffee
index 74e15d930a2..db2e8299b35 100644
--- a/src/scan-handler.coffee
+++ b/src/scan-handler.coffee
@@ -1,17 +1,14 @@
-_ = require "underscore-plus"
path = require "path"
async = require "async"
{PathSearcher, PathScanner, search} = require 'scandal'
-module.exports = (rootPaths, regexSource, options) ->
+module.exports = (rootPaths, regexSource, options, searchOptions={}) ->
callback = @async()
- rootPath = rootPaths[0]
-
PATHS_COUNTER_SEARCHED_CHUNK = 50
pathsSearched = 0
- searcher = new PathSearcher()
+ searcher = new PathSearcher(searchOptions)
searcher.on 'file-error', ({code, path, message}) ->
emit('scan:file-error', {code, path, message})
@@ -26,9 +23,9 @@ module.exports = (rootPaths, regexSource, options) ->
async.each(
rootPaths,
(rootPath, next) ->
- options2 = _.extend {}, options,
+ options2 = Object.assign {}, options,
inclusions: processPaths(rootPath, options.inclusions)
- exclusions: processPaths(rootPath, options.exclusions)
+ globalExclusions: processPaths(rootPath, options.globalExclusions)
scanner = new PathScanner(rootPath, options2)
diff --git a/src/scope-descriptor.coffee b/src/scope-descriptor.coffee
deleted file mode 100644
index 7940cc630ac..00000000000
--- a/src/scope-descriptor.coffee
+++ /dev/null
@@ -1,49 +0,0 @@
-# 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 {Strings}
-# 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.
-#
-# * {Editor::getRootScopeDescriptor} to get the language's descriptor.
-# * {Editor::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](https://atom.io/docs/latest/behind-atom-scoped-settings-scopes-and-scope-descriptors)
-# for more information.
-module.exports =
-class ScopeDescriptor
- @fromObject: (scopes) ->
- if scopes instanceof ScopeDescriptor
- scopes
- else
- new ScopeDescriptor({scopes})
-
- ###
- Section: Construction and Destruction
- ###
-
- # Public: Create a {ScopeDescriptor} object.
- #
- # * `object` {Object}
- # * `scopes` {Array} of {String}s
- constructor: ({@scopes}) ->
-
- # Public: Returns an {Array} of {String}s
- getScopesArray: -> @scopes
-
- getScopeChain: ->
- @scopes
- .map (scope) ->
- scope = ".#{scope}" unless scope[0] is '.'
- scope
- .join(' ')
-
- toString: ->
- @getScopeChain()
diff --git a/src/scope-descriptor.js b/src/scope-descriptor.js
new file mode 100644
index 00000000000..111ce567643
--- /dev/null
+++ b/src/scope-descriptor.js
@@ -0,0 +1,83 @@
+// 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;
+ }
+};
diff --git a/src/scoped-properties.coffee b/src/scoped-properties.coffee
deleted file mode 100644
index 8bf4c5ed371..00000000000
--- a/src/scoped-properties.coffee
+++ /dev/null
@@ -1,23 +0,0 @@
-CSON = require 'season'
-{CompositeDisposable} = require 'event-kit'
-
-module.exports =
-class ScopedProperties
- @load: (scopedPropertiesPath, callback) ->
- CSON.readFile scopedPropertiesPath, (error, scopedProperties={}) ->
- if error?
- callback(error)
- else
- callback(null, new ScopedProperties(scopedPropertiesPath, scopedProperties))
-
- constructor: (@path, @scopedProperties) ->
-
- activate: ->
- for selector, properties of @scopedProperties
- atom.config.set(null, properties, scopeSelector: selector, source: @path)
- return
-
- deactivate: ->
- for selector of @scopedProperties
- atom.config.unset(null, scopeSelector: selector, source: @path)
- return
diff --git a/src/scroll-view.coffee b/src/scroll-view.coffee
deleted file mode 100644
index 86743be4c3a..00000000000
--- a/src/scroll-view.coffee
+++ /dev/null
@@ -1,38 +0,0 @@
-{View} = require './space-pen-extensions'
-
-# Deprecated: Represents a view that scrolls.
-#
-# Handles several core events to update scroll position:
-#
-# * `core:move-up` Scrolls the view up
-# * `core:move-down` Scrolls the view down
-# * `core:page-up` Scrolls the view up by the height of the page
-# * `core:page-down` Scrolls the view down by the height of the page
-# * `core:move-to-top` Scrolls the editor to the top
-# * `core:move-to-bottom` Scroll the editor to the bottom
-#
-# Subclasses must call `super` if overriding the `initialize` method.
-#
-# ## Examples
-#
-# ```coffee
-# {ScrollView} = require 'atom'
-#
-# class MyView extends ScrollView
-# @content: ->
-# @div()
-#
-# initialize: ->
-# super
-# @text('super long content that will scroll')
-# ```
-#
-module.exports =
-class ScrollView extends View
- initialize: ->
- @on 'core:move-up', => @scrollUp()
- @on 'core:move-down', => @scrollDown()
- @on 'core:page-up', => @pageUp()
- @on 'core:page-down', => @pageDown()
- @on 'core:move-to-top', => @scrollToTop()
- @on 'core:move-to-bottom', => @scrollToBottom()
diff --git a/src/scrollbar-component.coffee b/src/scrollbar-component.coffee
deleted file mode 100644
index 45cfbd24031..00000000000
--- a/src/scrollbar-component.coffee
+++ /dev/null
@@ -1,75 +0,0 @@
-module.exports =
-class ScrollbarComponent
- constructor: ({@orientation, @onScroll}) ->
- @domNode = document.createElement('div')
- @domNode.classList.add "#{@orientation}-scrollbar"
- @domNode.style['-webkit-transform'] = 'translateZ(0)' # See atom/atom#3559
- @domNode.style.left = 0 if @orientation is 'horizontal'
-
- @contentNode = document.createElement('div')
- @contentNode.classList.add "scrollbar-content"
- @domNode.appendChild(@contentNode)
-
- @domNode.addEventListener 'scroll', @onScrollCallback
-
- getDomNode: ->
- @domNode
-
- updateSync: (state) ->
- @oldState ?= {}
- switch @orientation
- when 'vertical'
- @newState = state.verticalScrollbar
- @updateVertical()
- when 'horizontal'
- @newState = state.horizontalScrollbar
- @updateHorizontal()
-
- if @newState.visible isnt @oldState.visible
- if @newState.visible
- @domNode.style.display = ''
- else
- @domNode.style.display = 'none'
- @oldState.visible = @newState.visible
-
- updateVertical: ->
- if @newState.width isnt @oldState.width
- @domNode.style.width = @newState.width + 'px'
- @oldState.width = @newState.width
-
- if @newState.bottom isnt @oldState.bottom
- @domNode.style.bottom = @newState.bottom + 'px'
- @oldState.bottom = @newState.bottom
-
- if @newState.scrollHeight isnt @oldState.scrollHeight
- @contentNode.style.height = @newState.scrollHeight + 'px'
- @oldState.scrollHeight = @newState.scrollHeight
-
- if @newState.scrollTop isnt @oldState.scrollTop
- @domNode.scrollTop = @newState.scrollTop
- @oldState.scrollTop = @newState.scrollTop
-
- updateHorizontal: ->
- if @newState.height isnt @oldState.height
- @domNode.style.height = @newState.height + 'px'
- @oldState.height = @newState.height
-
- if @newState.right isnt @oldState.right
- @domNode.style.right = @newState.right + 'px'
- @oldState.right = @newState.right
-
- if @newState.scrollWidth isnt @oldState.scrollWidth
- @contentNode.style.width = @newState.scrollWidth + 'px'
- @oldState.scrollWidth = @newState.scrollWidth
-
- if @newState.scrollLeft isnt @oldState.scrollLeft
- @domNode.scrollLeft = @newState.scrollLeft
- @oldState.scrollLeft = @newState.scrollLeft
-
-
- onScrollCallback: =>
- switch @orientation
- when 'vertical'
- @onScroll(@domNode.scrollTop)
- when 'horizontal'
- @onScroll(@domNode.scrollLeft)
diff --git a/src/scrollbar-corner-component.coffee b/src/scrollbar-corner-component.coffee
deleted file mode 100644
index bc059f12cd7..00000000000
--- a/src/scrollbar-corner-component.coffee
+++ /dev/null
@@ -1,38 +0,0 @@
-module.exports =
-class ScrollbarCornerComponent
- constructor: ->
- @domNode = document.createElement('div')
- @domNode.classList.add('scrollbar-corner')
-
- @contentNode = document.createElement('div')
- @domNode.appendChild(@contentNode)
-
- getDomNode: ->
- @domNode
-
- updateSync: (state) ->
- @oldState ?= {}
- @newState ?= {}
-
- newHorizontalState = state.horizontalScrollbar
- newVerticalState = state.verticalScrollbar
- @newState.visible = newHorizontalState.visible and newVerticalState.visible
- @newState.height = newHorizontalState.height
- @newState.width = newVerticalState.width
-
- if @newState.visible isnt @oldState.visible
- if @newState.visible
- @domNode.style.display = ''
- else
- @domNode.style.display = 'none'
- @oldState.visible = @newState.visible
-
- if @newState.height isnt @oldState.height
- @domNode.style.height = @newState.height + 'px'
- @contentNode.style.height = @newState.height + 1 + 'px'
- @oldState.height = @newState.height
-
- if @newState.width isnt @oldState.width
- @domNode.style.width = @newState.width + 'px'
- @contentNode.style.width = @newState.width + 1 + 'px'
- @oldState.width = @newState.width
diff --git a/src/select-list-view.coffee b/src/select-list-view.coffee
deleted file mode 100644
index 046be1a3d51..00000000000
--- a/src/select-list-view.coffee
+++ /dev/null
@@ -1,312 +0,0 @@
-{$, View} = require './space-pen-extensions'
-TextEditorView = require './text-editor-view'
-fuzzyFilter = require('fuzzaldrin').filter
-
-# Deprecated: Provides a view that renders a list of items with an editor that
-# filters the items. Used by many packages such as the fuzzy-finder,
-# command-palette, symbols-view and autocomplete.
-#
-# Subclasses must implement the following methods:
-#
-# * {::viewForItem}
-# * {::confirmed}
-#
-# ## Requiring in packages
-#
-# ```coffee
-# {SelectListView} = require 'atom'
-#
-# class MySelectListView extends SelectListView
-# initialize: ->
-# super
-# @addClass('overlay from-top')
-# @setItems(['Hello', 'World'])
-# atom.workspaceView.append(this)
-# @focusFilterEditor()
-#
-# viewForItem: (item) ->
-# "
#{item}
"
-#
-# confirmed: (item) ->
-# console.log("#{item} was selected")
-# ```
-module.exports =
-class SelectListView extends View
- @content: ->
- @div class: 'select-list', =>
- @subview 'filterEditorView', new TextEditorView(mini: true)
- @div class: 'error-message', outlet: 'error'
- @div class: 'loading', outlet: 'loadingArea', =>
- @span class: 'loading-message', outlet: 'loading'
- @span class: 'badge', outlet: 'loadingBadge'
- @ol class: 'list-group', outlet: 'list'
-
- maxItems: Infinity
- scheduleTimeout: null
- inputThrottle: 50
- cancelling: false
-
- ###
- Section: Construction
- ###
-
- # Essential: Initialize the select list view.
- #
- # This method can be overridden by subclasses but `super` should always
- # be called.
- initialize: ->
- @filterEditorView.getEditor().getBuffer().onDidChange =>
- @schedulePopulateList()
- @filterEditorView.on 'blur', =>
- @cancel() unless @cancelling
-
- # This prevents the focusout event from firing on the filter editor view
- # when the list is scrolled by clicking the scrollbar and dragging.
- @list.on 'mousedown', ({target}) =>
- false if target is @list[0]
-
- @on 'core:move-up', =>
- @selectPreviousItemView()
- @on 'core:move-down', =>
- @selectNextItemView()
- @on 'core:move-to-top', =>
- @selectItemView(@list.find('li:first'))
- @list.scrollToTop()
- false
- @on 'core:move-to-bottom', =>
- @selectItemView(@list.find('li:last'))
- @list.scrollToBottom()
- false
-
- @on 'core:confirm', => @confirmSelection()
- @on 'core:cancel', => @cancel()
-
- @list.on 'mousedown', 'li', (e) =>
- @selectItemView($(e.target).closest('li'))
- e.preventDefault()
-
- @list.on 'mouseup', 'li', (e) =>
- @confirmSelection() if $(e.target).closest('li').hasClass('selected')
- e.preventDefault()
-
- ###
- Section: Methods that must be overridden
- ###
-
- # Essential: Create a view for the given model item.
- #
- # This method must be overridden by subclasses.
- #
- # This is called when the item is about to appended to the list view.
- #
- # * `item` The model item being rendered. This will always be one of the items
- # previously passed to {::setItems}.
- #
- # Returns a String of HTML, DOM element, jQuery object, or View.
- viewForItem: (item) ->
- throw new Error("Subclass must implement a viewForItem(item) method")
-
- # Essential: Callback function for when an item is selected.
- #
- # This method must be overridden by subclasses.
- #
- # * `item` The selected model item. This will always be one of the items
- # previously passed to {::setItems}.
- #
- # Returns a DOM element, jQuery object, or {View}.
- confirmed: (item) ->
- throw new Error("Subclass must implement a confirmed(item) method")
-
- ###
- Section: Managing the list of items
- ###
-
- # Essential: Set the array of items to display in the list.
- #
- # This should be model items not actual views. {::viewForItem} will be
- # called to render the item when it is being appended to the list view.
- #
- # * `items` The {Array} of model items to display in the list (default: []).
- setItems: (@items=[]) ->
- @populateList()
- @setLoading()
-
- # Essential: Get the model item that is currently selected in the list view.
- #
- # Returns a model item.
- getSelectedItem: ->
- @getSelectedItemView().data('select-list-item')
-
- # Extended: Get the property name to use when filtering items.
- #
- # This method may be overridden by classes to allow fuzzy filtering based
- # on a specific property of the item objects.
- #
- # For example if the objects you pass to {::setItems} are of the type
- # `{"id": 3, "name": "Atom"}` then you would return `"name"` from this method
- # to fuzzy filter by that property when text is entered into this view's
- # editor.
- #
- # Returns the property name to fuzzy filter by.
- getFilterKey: ->
-
- # Extended: Get the filter query to use when fuzzy filtering the visible
- # elements.
- #
- # By default this method returns the text in the mini editor but it can be
- # overridden by subclasses if needed.
- #
- # Returns a {String} to use when fuzzy filtering the elements to display.
- getFilterQuery: ->
- @filterEditorView.getEditor().getText()
-
- # Extended: Set the maximum numbers of items to display in the list.
- #
- # * `maxItems` The maximum {Number} of items to display.
- setMaxItems: (@maxItems) ->
-
- # Extended: Populate the list view with the model items previously set by
- # calling {::setItems}.
- #
- # Subclasses may override this method but should always call `super`.
- populateList: ->
- return unless @items?
-
- filterQuery = @getFilterQuery()
- if filterQuery.length
- filteredItems = fuzzyFilter(@items, filterQuery, key: @getFilterKey())
- else
- filteredItems = @items
-
- @list.empty()
- if filteredItems.length
- @setError(null)
-
- for i in [0...Math.min(filteredItems.length, @maxItems)]
- item = filteredItems[i]
- itemView = $(@viewForItem(item))
- itemView.data('select-list-item', item)
- @list.append(itemView)
-
- @selectItemView(@list.find('li:first'))
- else
- @setError(@getEmptyMessage(@items.length, filteredItems.length))
-
- ###
- Section: Messages to the user
- ###
-
- # Essential: Set the error message to display.
- #
- # * `message` The {String} error message (default: '').
- setError: (message='') ->
- if message.length is 0
- @error.text('').hide()
- else
- @setLoading()
- @error.text(message).show()
-
- # Essential: Set the loading message to display.
- #
- # * `message` The {String} loading message (default: '').
- setLoading: (message='') ->
- if message.length is 0
- @loading.text("")
- @loadingBadge.text("")
- @loadingArea.hide()
- else
- @setError()
- @loading.text(message)
- @loadingArea.show()
-
- # Extended: Get the message to display when there are no items.
- #
- # Subclasses may override this method to customize the message.
- #
- # * `itemCount` The {Number} of items in the array specified to {::setItems}
- # * `filteredItemCount` The {Number} of items that pass the fuzzy filter test.
- #
- # Returns a {String} message (default: 'No matches found').
- getEmptyMessage: (itemCount, filteredItemCount) -> 'No matches found'
-
- ###
- Section: View Actions
- ###
-
- # Essential: Cancel and close this select list view.
- #
- # This restores focus to the previously focused element if
- # {::storeFocusedElement} was called prior to this view being attached.
- cancel: ->
- @list.empty()
- @cancelling = true
- filterEditorViewFocused = @filterEditorView.isFocused
- @cancelled()
- @detach()
- @restoreFocus() if filterEditorViewFocused
- @cancelling = false
- clearTimeout(@scheduleTimeout)
-
- # Extended: Focus the fuzzy filter editor view.
- focusFilterEditor: ->
- @filterEditorView.focus()
-
- # Extended: Store the currently focused element. This element will be given
- # back focus when {::cancel} is called.
- storeFocusedElement: ->
- @previouslyFocusedElement = $(document.activeElement)
-
- ###
- Section: Private
- ###
-
- selectPreviousItemView: ->
- view = @getSelectedItemView().prev()
- view = @list.find('li:last') unless view.length
- @selectItemView(view)
-
- selectNextItemView: ->
- view = @getSelectedItemView().next()
- view = @list.find('li:first') unless view.length
- @selectItemView(view)
-
- selectItemView: (view) ->
- return unless view.length
- @list.find('.selected').removeClass('selected')
- view.addClass('selected')
- @scrollToItemView(view)
-
- scrollToItemView: (view) ->
- scrollTop = @list.scrollTop()
- desiredTop = view.position().top + scrollTop
- desiredBottom = desiredTop + view.outerHeight()
-
- if desiredTop < scrollTop
- @list.scrollTop(desiredTop)
- else if desiredBottom > @list.scrollBottom()
- @list.scrollBottom(desiredBottom)
-
- restoreFocus: ->
- if @previouslyFocusedElement?.isOnDom()
- @previouslyFocusedElement.focus()
- else
- atom.workspaceView.focus()
-
- cancelled: ->
- @filterEditorView.getEditor().setText('')
-
- getSelectedItemView: ->
- @list.find('li.selected')
-
- confirmSelection: ->
- item = @getSelectedItem()
- if item?
- @confirmed(item)
- else
- @cancel()
-
- schedulePopulateList: ->
- clearTimeout(@scheduleTimeout)
- populateCallback = =>
- @populateList() if @isOnDom()
- @scheduleTimeout = setTimeout(populateCallback, @inputThrottle)
diff --git a/src/selection.coffee b/src/selection.coffee
deleted file mode 100644
index 6ec874203d5..00000000000
--- a/src/selection.coffee
+++ /dev/null
@@ -1,805 +0,0 @@
-{Point, Range} = require 'text-buffer'
-{pick} = _ = require 'underscore-plus'
-{Emitter} = require 'event-kit'
-Grim = require 'grim'
-Model = require './model'
-
-NonWhitespaceRegExp = /\S/
-
-# Extended: Represents a selection in the {TextEditor}.
-module.exports =
-class Selection extends Model
- cursor: null
- marker: null
- editor: null
- initialScreenRange: null
- wordwise: false
-
- constructor: ({@cursor, @marker, @editor, id}) ->
- @emitter = new Emitter
-
- @assignId(id)
- @cursor.selection = this
- @decoration = @editor.decorateMarker(@marker, type: 'highlight', class: 'selection')
-
- @marker.onDidChange (e) => @screenRangeChanged(e)
- @marker.onDidDestroy =>
- unless @editor.isDestroyed()
- @destroyed = true
- @editor.removeSelection(this)
- @emit 'destroyed' if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-destroy'
- @emitter.dispose()
-
- destroy: ->
- @marker.destroy()
-
- isLastSelection: ->
- this is @editor.getLastSelection()
-
- ###
- Section: Event Subscription
- ###
-
- # Extended: Calls your `callback` when the selection was moved.
- #
- # * `callback` {Function}
- # * `event` {Object}
- # * `oldBufferRange` {Range}
- # * `oldScreenRange` {Range}
- # * `newBufferRange` {Range}
- # * `newScreenRange` {Range}
- # * `selection` {Selection} that triggered the event
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangeRange: (callback) ->
- @emitter.on 'did-change-range', callback
-
- # Extended: Calls your `callback` when the selection was destroyed
- #
- # * `callback` {Function}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidDestroy: (callback) ->
- @emitter.on 'did-destroy', callback
-
- ###
- Section: Managing the selection range
- ###
-
- # Public: Returns the screen {Range} for the selection.
- getScreenRange: ->
- @marker.getScreenRange()
-
- # Public: Modifies the screen range for the selection.
- #
- # * `screenRange` The new {Range} to use.
- # * `options` (optional) {Object} options matching those found in {::setBufferRange}.
- setScreenRange: (screenRange, options) ->
- @setBufferRange(@editor.bufferRangeForScreenRange(screenRange), options)
-
- # Public: Returns the buffer {Range} for the selection.
- getBufferRange: ->
- @marker.getBufferRange()
-
- # Public: Modifies the buffer {Range} for the selection.
- #
- # * `screenRange` The new {Range} to select.
- # * `options` (optional) {Object} with the keys:
- # * `preserveFolds` if `true`, the fold settings are preserved after the
- # selection moves.
- # * `autoscroll` {Boolean} indicating whether to autoscroll to the new
- # range. Defaults to `true` if this is the most recently added selection,
- # `false` otherwise.
- setBufferRange: (bufferRange, options={}) ->
- bufferRange = Range.fromObject(bufferRange)
- options.reversed ?= @isReversed()
- @editor.destroyFoldsContainingBufferRange(bufferRange) unless options.preserveFolds
- @modifySelection =>
- needsFlash = options.flash
- delete options.flash if options.flash?
- @marker.setBufferRange(bufferRange, options)
- @autoscroll() if options?.autoscroll ? @isLastSelection()
- @decoration.flash('flash', @editor.selectionFlashDuration) if needsFlash
-
- # Public: Returns the starting and ending buffer rows the selection is
- # highlighting.
- #
- # Returns an {Array} of two {Number}s: the starting row, and the ending row.
- getBufferRowRange: ->
- range = @getBufferRange()
- start = range.start.row
- end = range.end.row
- end = Math.max(start, end - 1) if range.end.column is 0
- [start, end]
-
- getTailScreenPosition: ->
- @marker.getTailScreenPosition()
-
- getTailBufferPosition: ->
- @marker.getTailBufferPosition()
-
- getHeadScreenPosition: ->
- @marker.getHeadScreenPosition()
-
- getHeadBufferPosition: ->
- @marker.getHeadBufferPosition()
-
- ###
- Section: Info about the selection
- ###
-
- # Public: Determines if the selection contains anything.
- isEmpty: ->
- @getBufferRange().isEmpty()
-
- # Public: Determines if the ending position of a marker is greater than the
- # starting position.
- #
- # This can happen when, for example, you highlight text "up" in a {TextBuffer}.
- isReversed: ->
- @marker.isReversed()
-
- # Public: Returns whether the selection is a single line or not.
- isSingleScreenLine: ->
- @getScreenRange().isSingleLine()
-
- # Public: Returns the text in the selection.
- getText: ->
- @editor.buffer.getTextInRange(@getBufferRange())
-
- # Public: Identifies if a selection intersects with a given buffer range.
- #
- # * `bufferRange` A {Range} to check against.
- #
- # Returns a {Boolean}
- intersectsBufferRange: (bufferRange) ->
- @getBufferRange().intersectsWith(bufferRange)
-
- intersectsScreenRowRange: (startRow, endRow) ->
- @getScreenRange().intersectsRowRange(startRow, endRow)
-
- intersectsScreenRow: (screenRow) ->
- @getScreenRange().intersectsRow(screenRow)
-
- # Public: Identifies if a selection intersects with another selection.
- #
- # * `otherSelection` A {Selection} to check against.
- #
- # Returns a {Boolean}
- intersectsWith: (otherSelection, exclusive) ->
- @getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive)
-
- ###
- Section: Modifying the selected range
- ###
-
- # Public: Clears the selection, moving the marker to the head.
- #
- # * `options` (optional) {Object} with the following keys:
- # * `autoscroll` {Boolean} indicating whether to autoscroll to the new
- # range. Defaults to `true` if this is the most recently added selection,
- # `false` otherwise.
- clear: (options) ->
- @marker.setProperties(goalScreenRange: null)
- @marker.clearTail() unless @retainSelection
- @autoscroll() if options?.autoscroll ? @isLastSelection()
- @finalize()
-
- # Public: Selects the text from the current cursor position to a given screen
- # position.
- #
- # * `position` An instance of {Point}, with a given `row` and `column`.
- selectToScreenPosition: (position) ->
- position = Point.fromObject(position)
-
- @modifySelection =>
- if @initialScreenRange
- if position.isLessThan(@initialScreenRange.start)
- @marker.setScreenRange([position, @initialScreenRange.end], reversed: true)
- else
- @marker.setScreenRange([@initialScreenRange.start, position])
- else
- @cursor.setScreenPosition(position)
-
- if @linewise
- @expandOverLine()
- else if @wordwise
- @expandOverWord()
-
- # Public: Selects the text from the current cursor position to a given buffer
- # position.
- #
- # * `position` An instance of {Point}, with a given `row` and `column`.
- selectToBufferPosition: (position) ->
- @modifySelection => @cursor.setBufferPosition(position)
-
- # Public: Selects the text one position right of the cursor.
- #
- # * `columnCount` (optional) {Number} number of columns to select (default: 1)
- selectRight: (columnCount) ->
- @modifySelection => @cursor.moveRight(columnCount)
-
- # Public: Selects the text one position left of the cursor.
- #
- # * `columnCount` (optional) {Number} number of columns to select (default: 1)
- selectLeft: (columnCount) ->
- @modifySelection => @cursor.moveLeft(columnCount)
-
- # Public: Selects all the text one position above the cursor.
- #
- # * `rowCount` (optional) {Number} number of rows to select (default: 1)
- selectUp: (rowCount) ->
- @modifySelection => @cursor.moveUp(rowCount)
-
- # Public: Selects all the text one position below the cursor.
- #
- # * `rowCount` (optional) {Number} number of rows to select (default: 1)
- selectDown: (rowCount) ->
- @modifySelection => @cursor.moveDown(rowCount)
-
- # Public: Selects all the text from the current cursor position to the top of
- # the buffer.
- selectToTop: ->
- @modifySelection => @cursor.moveToTop()
-
- # Public: Selects all the text from the current cursor position to the bottom
- # of the buffer.
- selectToBottom: ->
- @modifySelection => @cursor.moveToBottom()
-
- # Public: Selects all the text in the buffer.
- selectAll: ->
- @setBufferRange(@editor.buffer.getRange(), autoscroll: false)
-
- # Public: Selects all the text from the current cursor position to the
- # beginning of the line.
- selectToBeginningOfLine: ->
- @modifySelection => @cursor.moveToBeginningOfLine()
-
- # Public: Selects all the text from the current cursor position to the first
- # character of the line.
- selectToFirstCharacterOfLine: ->
- @modifySelection => @cursor.moveToFirstCharacterOfLine()
-
- # Public: Selects all the text from the current cursor position to the end of
- # the line.
- selectToEndOfLine: ->
- @modifySelection => @cursor.moveToEndOfScreenLine()
-
- # Public: Selects all the text from the current cursor position to the
- # beginning of the word.
- selectToBeginningOfWord: ->
- @modifySelection => @cursor.moveToBeginningOfWord()
-
- # Public: Selects all the text from the current cursor position to the end of
- # the word.
- selectToEndOfWord: ->
- @modifySelection => @cursor.moveToEndOfWord()
-
- # Public: Selects all the text from the current cursor position to the
- # beginning of the next word.
- selectToBeginningOfNextWord: ->
- @modifySelection => @cursor.moveToBeginningOfNextWord()
-
- # Public: Selects text to the previous word boundary.
- selectToPreviousWordBoundary: ->
- @modifySelection => @cursor.moveToPreviousWordBoundary()
-
- # Public: Selects text to the next word boundary.
- selectToNextWordBoundary: ->
- @modifySelection => @cursor.moveToNextWordBoundary()
-
- # Public: Selects all the text from the current cursor position to the
- # beginning of the next paragraph.
- selectToBeginningOfNextParagraph: ->
- @modifySelection => @cursor.moveToBeginningOfNextParagraph()
-
- # Public: Selects all the text from the current cursor position to the
- # beginning of the previous paragraph.
- selectToBeginningOfPreviousParagraph: ->
- @modifySelection => @cursor.moveToBeginningOfPreviousParagraph()
-
- # Public: Modifies the selection to encompass the current word.
- #
- # Returns a {Range}.
- selectWord: ->
- options = {}
- options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace()
- if @cursor.isBetweenWordAndNonWord()
- options.includeNonWordCharacters = false
-
- @setBufferRange(@cursor.getCurrentWordBufferRange(options))
- @wordwise = true
- @initialScreenRange = @getScreenRange()
-
- # Public: Expands the newest selection to include the entire word on which
- # the cursors rests.
- expandOverWord: ->
- @setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange()))
-
- # Public: Selects an entire line in the buffer.
- #
- # * `row` The line {Number} to select (default: the row of the cursor).
- selectLine: (row=@cursor.getBufferPosition().row) ->
- range = @editor.bufferRangeForBufferRow(row, includeNewline: true)
- @setBufferRange(@getBufferRange().union(range), autoscroll: true)
- @linewise = true
- @wordwise = false
- @initialScreenRange = @getScreenRange()
-
- # Public: Expands the newest selection to include the entire line on which
- # the cursor currently rests.
- #
- # It also includes the newline character.
- expandOverLine: ->
- range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true))
- @setBufferRange(range)
-
- ###
- 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).
- # * `normalizeLineEndings` (optional) {Boolean} (default: true)
- # * `undo` if `skip`, skips the undo stack for this operation.
- insertText: (text, options={}) ->
- oldBufferRange = @getBufferRange()
- @editor.unfoldBufferRow(oldBufferRange.end.row)
- wasReversed = @isReversed()
- @clear()
-
- autoIndentFirstLine = false
- precedingText = @editor.getTextInRange([[oldBufferRange.start.row, 0], oldBufferRange.start])
- remainingLines = text.split('\n')
- firstInsertedLine = remainingLines.shift()
-
- if options.indentBasis?
- indentAdjustment = @editor.indentLevelForLine(precedingText) - options.indentBasis
- @adjustIndent(remainingLines, indentAdjustment)
-
- if options.autoIndent and not NonWhitespaceRegExp.test(precedingText)
- autoIndentFirstLine = true
- firstLine = precedingText + firstInsertedLine
- desiredIndentLevel = @editor.languageMode.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine)
- indentAdjustment = desiredIndentLevel - @editor.indentLevelForLine(firstLine)
- @adjustIndent(remainingLines, indentAdjustment)
-
- text = firstInsertedLine
- text += '\n' + remainingLines.join('\n') if remainingLines.length > 0
-
- newBufferRange = @editor.buffer.setTextInRange(oldBufferRange, text, pick(options, 'undo', 'normalizeLineEndings'))
-
- if options.select
- @setBufferRange(newBufferRange, reversed: wasReversed)
- else
- @cursor.setBufferPosition(newBufferRange.end, clip: 'forward') if wasReversed
-
- if autoIndentFirstLine
- @editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel)
-
- if options.autoIndentNewline and text is '\n'
- currentIndentation = @editor.indentationForBufferRow(newBufferRange.start.row)
- @editor.autoIndentBufferRow(newBufferRange.end.row, preserveLeadingWhitespace: true, skipBlankLines: false)
- if @editor.indentationForBufferRow(newBufferRange.end.row) < currentIndentation
- @editor.setIndentationForBufferRow(newBufferRange.end.row, currentIndentation)
- else if options.autoDecreaseIndent and NonWhitespaceRegExp.test(text)
- @editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row)
-
- @autoscroll() if @isLastSelection()
-
- newBufferRange
-
- # Public: Removes the first character before the selection if the selection
- # is empty otherwise it deletes the selection.
- backspace: ->
- @selectLeft() if @isEmpty() and not @editor.isFoldedAtScreenRow(@cursor.getScreenRow())
- @deleteSelectedText()
-
- # Public: Removes the selection or, if nothing is selected, then all
- # characters from the start of the selection back to the previous word
- # boundary.
- deleteToPreviousWordBoundary: ->
- @selectToPreviousWordBoundary() if @isEmpty()
- @deleteSelectedText()
-
- # Public: Removes the selection or, if nothing is selected, then all
- # characters from the start of the selection up to the next word
- # boundary.
- deleteToNextWordBoundary: ->
- @selectToNextWordBoundary() if @isEmpty()
- @deleteSelectedText()
-
- # Public: Removes from the start of the selection to the beginning of the
- # current word if the selection is empty otherwise it deletes the selection.
- deleteToBeginningOfWord: ->
- @selectToBeginningOfWord() if @isEmpty()
- @deleteSelectedText()
-
- # Public: Removes from the beginning of the line which the selection begins on
- # all the way through to the end of the selection.
- deleteToBeginningOfLine: ->
- if @isEmpty() and @cursor.isAtBeginningOfLine()
- @selectLeft()
- else
- @selectToBeginningOfLine()
- @deleteSelectedText()
-
- # Public: Removes the selection or the next character after the start of the
- # selection if the selection is empty.
- delete: ->
- if @isEmpty()
- if @cursor.isAtEndOfLine() and fold = @editor.largestFoldStartingAtScreenRow(@cursor.getScreenRow() + 1)
- @selectToBufferPosition(fold.getBufferRange().end)
- else
- @selectRight()
- @deleteSelectedText()
-
- # Public: If the selection is empty, removes all text from the cursor to the
- # end of the line. If the cursor is already at the end of the line, it
- # removes the following newline. If the selection isn't empty, only deletes
- # the contents of the selection.
- deleteToEndOfLine: ->
- return @delete() if @isEmpty() and @cursor.isAtEndOfLine()
- @selectToEndOfLine() if @isEmpty()
- @deleteSelectedText()
-
- # Public: Removes the selection or all characters from the start of the
- # selection to the end of the current word if nothing is selected.
- deleteToEndOfWord: ->
- @selectToEndOfWord() if @isEmpty()
- @deleteSelectedText()
-
- # Public: Removes only the selected text.
- deleteSelectedText: ->
- bufferRange = @getBufferRange()
- if bufferRange.isEmpty() and fold = @editor.largestFoldContainingBufferRow(bufferRange.start.row)
- bufferRange = bufferRange.union(fold.getBufferRange(includeNewline: true))
- @editor.buffer.delete(bufferRange) unless bufferRange.isEmpty()
- @cursor?.setBufferPosition(bufferRange.start)
-
- # Public: Removes the line at the beginning of the selection if the selection
- # is empty unless the selection spans multiple lines in which case all lines
- # are removed.
- deleteLine: ->
- if @isEmpty()
- start = @cursor.getScreenRow()
- range = @editor.bufferRowsForScreenRows(start, start + 1)
- if range[1] > range[0]
- @editor.buffer.deleteRows(range[0], range[1] - 1)
- else
- @editor.buffer.deleteRow(range[0])
- else
- range = @getBufferRange()
- start = range.start.row
- end = range.end.row
- if end isnt @editor.buffer.getLastRow() and range.end.column is 0
- end--
- @editor.buffer.deleteRows(start, end)
-
- # Public: Joins the current line with the one below it. Lines will
- # be separated by a single space.
- #
- # If there selection spans more than one line, all the lines are joined together.
- joinLines: ->
- selectedRange = @getBufferRange()
- if selectedRange.isEmpty()
- return if selectedRange.start.row is @editor.buffer.getLastRow()
- else
- joinMarker = @editor.markBufferRange(selectedRange, invalidationStrategy: 'never')
-
- rowCount = Math.max(1, selectedRange.getRowCount() - 1)
- for row in [0...rowCount]
- @cursor.setBufferPosition([selectedRange.start.row])
- @cursor.moveToEndOfLine()
-
- # Remove trailing whitespace from the current line
- scanRange = @cursor.getCurrentLineBufferRange()
- trailingWhitespaceRange = null
- @editor.scanInBufferRange /[ \t]+$/, scanRange, ({range}) ->
- trailingWhitespaceRange = range
- if trailingWhitespaceRange?
- @setBufferRange(trailingWhitespaceRange)
- @deleteSelectedText()
-
- currentRow = selectedRange.start.row
- nextRow = currentRow + 1
- insertSpace = nextRow <= @editor.buffer.getLastRow() and
- @editor.buffer.lineLengthForRow(nextRow) > 0 and
- @editor.buffer.lineLengthForRow(currentRow) > 0
- @insertText(' ') if insertSpace
-
- @cursor.moveToEndOfLine()
-
- # Remove leading whitespace from the line below
- @modifySelection =>
- @cursor.moveRight()
- @cursor.moveToFirstCharacterOfLine()
- @deleteSelectedText()
-
- @cursor.moveLeft() if insertSpace
-
- if joinMarker?
- newSelectedRange = joinMarker.getBufferRange()
- @setBufferRange(newSelectedRange)
- joinMarker.destroy()
-
- # Public: Removes one level of indent from the currently selected rows.
- outdentSelectedRows: ->
- [start, end] = @getBufferRowRange()
- buffer = @editor.buffer
- leadingTabRegex = new RegExp("^( {1,#{@editor.getTabLength()}}|\t)")
- for row in [start..end]
- if matchLength = buffer.lineForRow(row).match(leadingTabRegex)?[0].length
- buffer.delete [[row, 0], [row, matchLength]]
- return
-
- # Public: Sets the indentation level of all selected rows to values suggested
- # by the relevant grammars.
- autoIndentSelectedRows: ->
- [start, end] = @getBufferRowRange()
- @editor.autoIndentBufferRows(start, end)
-
- # Public: Wraps the selected lines in comments if they aren't currently part
- # of a comment.
- #
- # Removes the comment if they are currently wrapped in a comment.
- toggleLineComments: ->
- @editor.toggleLineCommentsForBufferRows(@getBufferRowRange()...)
-
- # Public: Cuts the selection until the end of the line.
- cutToEndOfLine: (maintainClipboard) ->
- @selectToEndOfLine() if @isEmpty()
- @cut(maintainClipboard)
-
- # Public: Copies the selection to the clipboard and then deletes it.
- #
- # * `maintainClipboard` {Boolean} (default: false) See {::copy}
- # * `fullLine` {Boolean} (default: false) See {::copy}
- cut: (maintainClipboard=false, fullLine=false) ->
- @copy(maintainClipboard, fullLine)
- @delete()
-
- # Public: Copies the current selection to the clipboard.
- #
- # * `maintainClipboard` {Boolean} if `true`, a specific metadata property
- # is created to store each content copied to the clipboard. The clipboard
- # `text` still contains the concatenation of the clipboard with the
- # current selection. (default: false)
- # * `fullLine` {Boolean} if `true`, the copied text will always be pasted
- # at the beginning of the line containing the cursor, regardless of the
- # cursor's horizontal position. (default: false)
- copy: (maintainClipboard=false, fullLine=false) ->
- return if @isEmpty()
- {start, end} = @getBufferRange()
- selectionText = @editor.getTextInRange([start, end])
- precedingText = @editor.getTextInRange([[start.row, 0], start])
- startLevel = @editor.indentLevelForLine(precedingText)
-
- if maintainClipboard
- {text: clipboardText, metadata} = atom.clipboard.readWithMetadata()
- metadata ?= {}
- unless metadata.selections?
- metadata.selections = [{
- text: clipboardText,
- indentBasis: metadata.indentBasis,
- fullLine: metadata.fullLine,
- }]
- metadata.selections.push({
- text: selectionText,
- indentBasis: startLevel,
- fullLine: fullLine
- })
- atom.clipboard.write([clipboardText, selectionText].join("\n"), metadata)
- else
- atom.clipboard.write(selectionText, {
- indentBasis: startLevel,
- fullLine: fullLine
- })
-
- # Public: Creates a fold containing the current selection.
- fold: ->
- range = @getBufferRange()
- @editor.createFold(range.start.row, range.end.row)
- @cursor.setBufferPosition([range.end.row + 1, 0])
-
- # Private: Increase the indentation level of the given text by given number
- # of levels. Leaves the first line unchanged.
- adjustIndent: (lines, indentAdjustment) ->
- for line, i in lines
- if indentAdjustment is 0 or line is ''
- continue
- else if indentAdjustment > 0
- lines[i] = @editor.buildIndentString(indentAdjustment) + line
- else
- currentIndentLevel = @editor.indentLevelForLine(lines[i])
- indentLevel = Math.max(0, currentIndentLevel + indentAdjustment)
- lines[i] = line.replace(/^[\t ]+/, @editor.buildIndentString(indentLevel))
- return
-
- # Indent the current line(s).
- #
- # If the selection is empty, indents the current line if the cursor precedes
- # non-whitespace characters, and otherwise inserts a tab. If the selection is
- # non empty, calls {::indentSelectedRows}.
- #
- # * `options` (optional) {Object} with the keys:
- # * `autoIndent` If `true`, the line is indented to an automatically-inferred
- # level. Otherwise, {TextEditor::getTabText} is inserted.
- indent: ({autoIndent}={}) ->
- {row, column} = @cursor.getBufferPosition()
-
- if @isEmpty()
- @cursor.skipLeadingWhitespace()
- desiredIndent = @editor.suggestedIndentForBufferRow(row)
- delta = desiredIndent - @cursor.getIndentLevel()
-
- if autoIndent and delta > 0
- delta = Math.max(delta, 1) unless @editor.getSoftTabs()
- @insertText(@editor.buildIndentString(delta))
- else
- @insertText(@editor.buildIndentString(1, @cursor.getBufferColumn()))
- else
- @indentSelectedRows()
-
- # Public: If the selection spans multiple rows, indent all of them.
- indentSelectedRows: ->
- [start, end] = @getBufferRowRange()
- for row in [start..end]
- @editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) is 0
- return
-
- ###
- Section: Managing multiple selections
- ###
-
- # Public: Moves the selection down one row.
- addSelectionBelow: ->
- range = (@getGoalScreenRange() ? @getScreenRange()).copy()
- nextRow = range.end.row + 1
-
- for row in [nextRow..@editor.getLastScreenRow()]
- range.start.row = row
- range.end.row = row
- clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true)
-
- if range.isEmpty()
- continue if range.end.column > 0 and clippedRange.end.column is 0
- else
- continue if clippedRange.isEmpty()
-
- @editor.addSelectionForScreenRange(clippedRange, goalScreenRange: range)
- break
-
- return
-
- # Public: Moves the selection up one row.
- addSelectionAbove: ->
- range = (@getGoalScreenRange() ? @getScreenRange()).copy()
- previousRow = range.end.row - 1
-
- for row in [previousRow..0]
- range.start.row = row
- range.end.row = row
- clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true)
-
- if range.isEmpty()
- continue if range.end.column > 0 and clippedRange.end.column is 0
- else
- continue if clippedRange.isEmpty()
-
- @editor.addSelectionForScreenRange(clippedRange, goalScreenRange: range)
- break
-
- return
-
- # Public: Combines the given selection into this selection and then destroys
- # the given selection.
- #
- # * `otherSelection` A {Selection} to merge with.
- # * `options` (optional) {Object} options matching those found in {::setBufferRange}.
- merge: (otherSelection, options) ->
- myGoalScreenRange = @getGoalScreenRange()
- otherGoalScreenRange = otherSelection.getGoalScreenRange()
-
- if myGoalScreenRange? and otherGoalScreenRange?
- options.goalScreenRange = myGoalScreenRange.union(otherGoalScreenRange)
- else
- options.goalScreenRange = myGoalScreenRange ? otherGoalScreenRange
-
- @setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), _.extend(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) ->
- @getBufferRange().compare(otherSelection.getBufferRange())
-
- ###
- Section: Private Utilities
- ###
-
- screenRangeChanged: (e) ->
- {oldHeadBufferPosition, oldTailBufferPosition} = e
- {oldHeadScreenPosition, oldTailScreenPosition} = e
-
- eventObject =
- oldBufferRange: new Range(oldHeadBufferPosition, oldTailBufferPosition)
- oldScreenRange: new Range(oldHeadScreenPosition, oldTailScreenPosition)
- newBufferRange: @getBufferRange()
- newScreenRange: @getScreenRange()
- selection: this
-
- @emit 'screen-range-changed', @getScreenRange() if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-change-range'
- @editor.selectionRangeChanged(eventObject)
-
- finalize: ->
- @initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange())
- if @isEmpty()
- @wordwise = false
- @linewise = false
-
- autoscroll: ->
- if @marker.hasTail()
- @editor.scrollToScreenRange(@getScreenRange(), reversed: @isReversed())
- else
- @cursor.autoscroll()
-
- clearAutoscroll: ->
-
- modifySelection: (fn) ->
- @retainSelection = true
- @plantTail()
- fn()
- @retainSelection = false
-
- # Sets the marker's tail to the same position as the marker's head.
- #
- # This only works if there isn't already a tail position.
- #
- # Returns a {Point} representing the new tail position.
- plantTail: ->
- @marker.plantTail()
-
- getGoalScreenRange: ->
- if goalScreenRange = @marker.getProperties().goalScreenRange
- Range.fromObject(goalScreenRange)
-
-if Grim.includeDeprecatedAPIs
- Selection::on = (eventName) ->
- switch eventName
- when 'screen-range-changed'
- Grim.deprecate("Use Selection::onDidChangeRange instead. Call ::getScreenRange() yourself in your callback if you need the range.")
- when 'destroyed'
- Grim.deprecate("Use Selection::onDidDestroy instead.")
- else
- Grim.deprecate("Selection::on is deprecated. Use documented event subscription methods instead.")
-
- super
-
- # Deprecated: Use {::deleteToBeginningOfWord} instead.
- Selection::backspaceToBeginningOfWord = ->
- deprecate("Use Selection::deleteToBeginningOfWord() instead")
- @deleteToBeginningOfWord()
-
- # Deprecated: Use {::deleteToBeginningOfLine} instead.
- Selection::backspaceToBeginningOfLine = ->
- deprecate("Use Selection::deleteToBeginningOfLine() instead")
- @deleteToBeginningOfLine()
diff --git a/src/selection.js b/src/selection.js
new file mode 100644
index 00000000000..29381a0fd6f
--- /dev/null
+++ b/src/selection.js
@@ -0,0 +1,1234 @@
+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();
+ }
+};
diff --git a/src/selectors.js b/src/selectors.js
new file mode 100644
index 00000000000..5ee2d681ebe
--- /dev/null
+++ b/src/selectors.js
@@ -0,0 +1,36 @@
+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));
+}
diff --git a/src/space-pen-extensions.coffee b/src/space-pen-extensions.coffee
deleted file mode 100644
index 128ddb8b1c4..00000000000
--- a/src/space-pen-extensions.coffee
+++ /dev/null
@@ -1,157 +0,0 @@
-_ = require 'underscore-plus'
-SpacePen = require 'space-pen'
-{Subscriber} = require 'emissary'
-
-Subscriber.includeInto(SpacePen.View)
-
-jQuery = SpacePen.jQuery
-JQueryCleanData = jQuery.cleanData
-jQuery.cleanData = (elements) ->
- jQuery(element).view()?.unsubscribe?() for element in elements
- JQueryCleanData(elements)
-
-SpacePenCallRemoveHooks = SpacePen.callRemoveHooks
-SpacePen.callRemoveHooks = (element) ->
- view.unsubscribe?() for view in SpacePen.viewsForElement(element)
- SpacePenCallRemoveHooks(element)
-
-NativeEventNames = new Set
-NativeEventNames.add(nativeEvent) for nativeEvent in ["blur", "focus", "focusin",
-"focusout", "load", "resize", "scroll", "unload", "click", "dblclick", "mousedown",
-"mouseup", "mousemove", "mouseover", "mouseout", "mouseenter", "mouseleave", "change",
-"select", "submit", "keydown", "keypress", "keyup", "error", "contextmenu", "textInput",
-"textinput", "beforeunload"]
-
-JQueryTrigger = jQuery.fn.trigger
-jQuery.fn.trigger = (eventName, data) ->
- if NativeEventNames.has(eventName) or typeof eventName is 'object'
- JQueryTrigger.call(this, eventName, data)
- else
- data ?= {}
- data.jQueryTrigger = true
-
- for element in this
- atom.commands.dispatch(element, eventName, data)
- this
-
-HandlersByOriginalHandler = new WeakMap
-CommandDisposablesByElement = new WeakMap
-
-AddEventListener = (element, type, listener) ->
- if NativeEventNames.has(type)
- element.addEventListener(type, listener)
- else
- disposable = atom.commands.add(element, type, listener)
-
- unless disposablesByType = CommandDisposablesByElement.get(element)
- disposablesByType = {}
- CommandDisposablesByElement.set(element, disposablesByType)
-
- unless disposablesByListener = disposablesByType[type]
- disposablesByListener = new WeakMap
- disposablesByType[type] = disposablesByListener
-
- disposablesByListener.set(listener, disposable)
-
-RemoveEventListener = (element, type, listener) ->
- if NativeEventNames.has(type)
- element.removeEventListener(type, listener)
- else
- CommandDisposablesByElement.get(element)?[type]?.get(listener)?.dispose()
-
-JQueryEventAdd = jQuery.event.add
-jQuery.event.add = (elem, types, originalHandler, data, selector) ->
- handler = (event) ->
- if arguments.length is 1 and event.originalEvent?.detail?
- {detail} = event.originalEvent
- if Array.isArray(detail)
- originalHandler.apply(this, [event].concat(detail))
- else
- originalHandler.call(this, event, detail)
- else
- originalHandler.apply(this, arguments)
-
- HandlersByOriginalHandler.set(originalHandler, handler)
-
- JQueryEventAdd.call(this, elem, types, handler, data, selector, AddEventListener if atom?.commands?)
-
-JQueryEventRemove = jQuery.event.remove
-jQuery.event.remove = (elem, types, originalHandler, selector, mappedTypes) ->
- if originalHandler?
- handler = HandlersByOriginalHandler.get(originalHandler) ? originalHandler
- JQueryEventRemove(elem, types, handler, selector, mappedTypes, RemoveEventListener if atom?.commands?)
-
-JQueryContains = jQuery.contains
-
-jQuery.contains = (a, b) ->
- shadowRoot = null
- currentNode = b
- while currentNode
- if currentNode instanceof ShadowRoot and a.contains(currentNode.host)
- return true
- currentNode = currentNode.parentNode
-
- JQueryContains.call(this, a, b)
-
-tooltipDefaults =
- delay:
- show: 1000
- hide: 100
- container: 'body'
- html: true
- placement: 'auto top'
- viewportPadding: 2
-
-humanizeKeystrokes = (keystroke) ->
- keystrokes = keystroke.split(' ')
- keystrokes = (_.humanizeKeystroke(stroke) for stroke in keystrokes)
- keystrokes.join(' ')
-
-getKeystroke = (bindings) ->
- if bindings?.length
- "#{humanizeKeystrokes(bindings[0].keystrokes)}"
- else
- ''
-
-requireBootstrapTooltip = _.once ->
- atom.requireWithGlobals('bootstrap/js/tooltip', {jQuery})
-
-# options from http://getbootstrap.com/javascript/#tooltips
-jQuery.fn.setTooltip = (tooltipOptions, {command, commandElement}={}) ->
- requireBootstrapTooltip()
-
- tooltipOptions = {title: tooltipOptions} if _.isString(tooltipOptions)
-
- if commandElement
- bindings = atom.keymaps.findKeyBindings(command: command, target: commandElement[0])
- else if command
- bindings = atom.keymaps.findKeyBindings(command: command)
-
- tooltipOptions.title = "#{tooltipOptions.title} #{getKeystroke(bindings)}"
-
- @tooltip(jQuery.extend({}, tooltipDefaults, tooltipOptions))
-
-jQuery.fn.hideTooltip = ->
- tip = @data('bs.tooltip')
- if tip
- tip.leave(currentTarget: this)
- tip.hide()
-
-jQuery.fn.destroyTooltip = ->
- @hideTooltip()
- requireBootstrapTooltip()
- @tooltip('destroy')
-
-# Hide tooltips when window is resized
-jQuery(document.body).on 'show.bs.tooltip', ({target}) ->
- windowHandler = -> jQuery(target).hideTooltip()
- jQuery(window).one('resize', windowHandler)
- jQuery(target).one 'hide.bs.tooltip', ->
- jQuery(window).off('resize', windowHandler)
-
-jQuery.fn.setTooltip.getKeystroke = getKeystroke
-jQuery.fn.setTooltip.humanizeKeystrokes = humanizeKeystrokes
-
-Object.defineProperty jQuery.fn, 'element', get: -> @[0]
-
-module.exports = SpacePen
diff --git a/src/startup-time.js b/src/startup-time.js
new file mode 100644
index 00000000000..314415336ce
--- /dev/null
+++ b/src/startup-time.js
@@ -0,0 +1,33 @@
+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 = [];
+ }
+};
diff --git a/src/state-store.js b/src/state-store.js
new file mode 100644
index 00000000000..840ec18319b
--- /dev/null
+++ b/src/state-store.js
@@ -0,0 +1,141 @@
+'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;
+ });
+ });
+ }
+};
diff --git a/src/storage-folder.coffee b/src/storage-folder.coffee
deleted file mode 100644
index bf969dee246..00000000000
--- a/src/storage-folder.coffee
+++ /dev/null
@@ -1,27 +0,0 @@
-path = require "path"
-fs = require "fs-plus"
-
-module.exports =
-class StorageFolder
- constructor: (containingPath) ->
- @path = path.join(containingPath, "storage")
-
- store: (name, object) ->
- fs.writeFileSync(@pathForKey(name), JSON.stringify(object), 'utf8')
-
- load: (name) ->
- statePath = @pathForKey(name)
- try
- stateString = fs.readFileSync(statePath, 'utf8')
- catch error
- unless error.code is 'ENOENT'
- console.warn "Error reading state file: #{statePath}", error.stack, error
- return undefined
-
- try
- JSON.parse(stateString)
- catch error
- console.warn "Error parsing state file: #{statePath}", error.stack, error
-
- pathForKey: (name) -> path.join(@getPath(), name)
- getPath: -> @path
diff --git a/src/storage-folder.js b/src/storage-folder.js
new file mode 100644
index 00000000000..da78f973384
--- /dev/null
+++ b/src/storage-folder.js
@@ -0,0 +1,59 @@
+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;
+ }
+};
diff --git a/src/style-manager.coffee b/src/style-manager.coffee
deleted file mode 100644
index cfe86b3fe79..00000000000
--- a/src/style-manager.coffee
+++ /dev/null
@@ -1,169 +0,0 @@
-fs = require 'fs-plus'
-path = require 'path'
-{Emitter, Disposable} = require 'event-kit'
-
-# 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: ->
- @emitter = new Emitter
- @styleElements = []
- @styleElementsBySourcePath = {}
-
- ###
- 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) ->
- callback(styleElement) for styleElement in @getStyleElements()
- @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) ->
- @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) ->
- @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) ->
- @emitter.on 'did-update-style-element', callback
-
- ###
- Section: Reading Style Elements
- ###
-
- # Extended: Get all loaded style elements.
- getStyleElements: ->
- @styleElements.slice()
-
- addStyleSheet: (source, params) ->
- sourcePath = params?.sourcePath
- context = params?.context
- priority = params?.priority
-
- if sourcePath? and styleElement = @styleElementsBySourcePath[sourcePath]
- updated = true
- else
- styleElement = document.createElement('style')
- if sourcePath?
- styleElement.sourcePath = sourcePath
- styleElement.setAttribute('source-path', sourcePath)
-
- if context?
- styleElement.context = context
- styleElement.setAttribute('context', context)
-
- if priority?
- styleElement.priority = priority
- styleElement.setAttribute('priority', priority)
-
- styleElement.textContent = source
-
- if updated
- @emitter.emit 'did-update-style-element', styleElement
- else
- @addStyleElement(styleElement)
-
- new Disposable => @removeStyleElement(styleElement)
-
- addStyleElement: (styleElement) ->
- {sourcePath, priority} = styleElement
-
- if priority?
- for existingElement, index in @styleElements
- if existingElement.priority > priority
- insertIndex = index
- break
-
- insertIndex ?= @styleElements.length
-
- @styleElements.splice(insertIndex, 0, styleElement)
- @styleElementsBySourcePath[sourcePath] ?= styleElement if sourcePath?
- @emitter.emit 'did-add-style-element', styleElement
-
- removeStyleElement: (styleElement) ->
- index = @styleElements.indexOf(styleElement)
- unless index is -1
- @styleElements.splice(index, 1)
- delete @styleElementsBySourcePath[styleElement.sourcePath] if styleElement.sourcePath?
- @emitter.emit 'did-remove-style-element', styleElement
-
- getSnapshot: ->
- @styleElements.slice()
-
- restoreSnapshot: (styleElementsToRestore) ->
- for styleElement in @getStyleElements()
- @removeStyleElement(styleElement) unless styleElement in styleElementsToRestore
-
- existingStyleElements = @getStyleElements()
- for styleElement in styleElementsToRestore
- @addStyleElement(styleElement) unless styleElement in existingStyleElements
-
- return
-
- ###
- Section: Paths
- ###
-
- # Extended: Get the path of the user style sheet in `~/.atom`.
- #
- # Returns a {String}.
- getUserStyleSheetPath: ->
- stylesheetPath = fs.resolve(path.join(atom.getConfigDirPath(), 'styles'), ['css', 'less'])
- if fs.isFileSync(stylesheetPath)
- stylesheetPath
- else
- path.join(atom.getConfigDirPath(), 'styles.less')
diff --git a/src/style-manager.js b/src/style-manager.js
new file mode 100644
index 00000000000..fa4d3798126
--- /dev/null
+++ b/src/style-manager.js
@@ -0,0 +1,378 @@
+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 };
+ }
+}
diff --git a/src/styles-element.coffee b/src/styles-element.coffee
deleted file mode 100644
index 74ebd23baa3..00000000000
--- a/src/styles-element.coffee
+++ /dev/null
@@ -1,119 +0,0 @@
-{Emitter, CompositeDisposable} = require 'event-kit'
-{includeDeprecatedAPIs} = require 'grim'
-
-class StylesElement extends HTMLElement
- subscriptions: null
- context: null
-
- onDidAddStyleElement: (callback) ->
- @emitter.on 'did-add-style-element', callback
-
- onDidRemoveStyleElement: (callback) ->
- @emitter.on 'did-remove-style-element', callback
-
- onDidUpdateStyleElement: (callback) ->
- @emitter.on 'did-update-style-element', callback
-
- createdCallback: ->
- @emitter = new Emitter
- @styleElementClonesByOriginalElement = new WeakMap
-
- attachedCallback: ->
- if includeDeprecatedAPIs and @context is 'atom-text-editor'
- for styleElement in @children
- @upgradeDeprecatedSelectors(styleElement)
- @initialize()
-
- detachedCallback: ->
- @subscriptions.dispose()
- @subscriptions = null
-
- attributeChangedCallback: (attrName, oldVal, newVal) ->
- @contextChanged() if attrName is 'context'
-
- initialize: ->
- return if @subscriptions?
-
- @subscriptions = new CompositeDisposable
- @context = @getAttribute('context') ? undefined
-
- @subscriptions.add atom.styles.observeStyleElements(@styleElementAdded.bind(this))
- @subscriptions.add atom.styles.onDidRemoveStyleElement(@styleElementRemoved.bind(this))
- @subscriptions.add atom.styles.onDidUpdateStyleElement(@styleElementUpdated.bind(this))
-
- contextChanged: ->
- return unless @subscriptions?
-
- @styleElementRemoved(child) for child in Array::slice.call(@children)
- @context = @getAttribute('context')
- @styleElementAdded(styleElement) for styleElement in atom.styles.getStyleElements()
- return
-
- styleElementAdded: (styleElement) ->
- return unless @styleElementMatchesContext(styleElement)
-
- styleElementClone = styleElement.cloneNode(true)
- styleElementClone.sourcePath = styleElement.sourcePath
- styleElementClone.context = styleElement.context
- styleElementClone.priority = styleElement.priority
- @styleElementClonesByOriginalElement.set(styleElement, styleElementClone)
-
- priority = styleElement.priority
- if priority?
- for child in @children
- if child.priority > priority
- insertBefore = child
- break
-
- @insertBefore(styleElementClone, insertBefore)
-
- if includeDeprecatedAPIs and @context is 'atom-text-editor'
- @upgradeDeprecatedSelectors(styleElementClone)
-
- @emitter.emit 'did-add-style-element', styleElementClone
-
- styleElementRemoved: (styleElement) ->
- return unless @styleElementMatchesContext(styleElement)
-
- styleElementClone = @styleElementClonesByOriginalElement.get(styleElement) ? styleElement
- styleElementClone.remove()
- @emitter.emit 'did-remove-style-element', styleElementClone
-
- styleElementUpdated: (styleElement) ->
- return unless @styleElementMatchesContext(styleElement)
-
- styleElementClone = @styleElementClonesByOriginalElement.get(styleElement)
- styleElementClone.textContent = styleElement.textContent
- @emitter.emit 'did-update-style-element', styleElementClone
-
- styleElementMatchesContext: (styleElement) ->
- not @context? or styleElement.context is @context
-
- upgradeDeprecatedSelectors: (styleElement) ->
- return unless styleElement.sheet?
-
- upgradedSelectors = []
-
- for rule in styleElement.sheet.cssRules
- continue unless rule.selectorText?
- continue if /\:host/.test(rule.selectorText)
-
- inputSelector = rule.selectorText
- outputSelector = rule.selectorText
- .replace(/\.editor-colors($|[ >])/g, ':host$1')
- .replace(/\.editor([:.][^ ,>]+)/g, ':host($1)')
- .replace(/\.editor($|[ ,>])/g, ':host$1')
-
- unless inputSelector is outputSelector
- rule.selectorText = outputSelector
- upgradedSelectors.push({inputSelector, outputSelector})
-
- if upgradedSelectors.length > 0
- warning = "Upgraded the following syntax theme selectors in `#{styleElement.sourcePath}` for shadow DOM compatibility:\n\n"
- for {inputSelector, outputSelector} in upgradedSelectors
- warning += "`#{inputSelector}` => `#{outputSelector}`\n"
-
- warning += "\nSee the upgrade guide for information on removing this warning."
- console.warn(warning)
-
-module.exports = StylesElement = document.registerElement 'atom-styles', prototype: StylesElement.prototype
diff --git a/src/styles-element.js b/src/styles-element.js
new file mode 100644
index 00000000000..94c287dc036
--- /dev/null
+++ b/src/styles-element.js
@@ -0,0 +1,151 @@
+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
+};
diff --git a/src/subscriber-mixin.coffee b/src/subscriber-mixin.coffee
deleted file mode 100644
index b6817ce5369..00000000000
--- a/src/subscriber-mixin.coffee
+++ /dev/null
@@ -1,4 +0,0 @@
-{Subscriber} = require 'emissary'
-SubscriberMixin = componentDidUnmount: -> @unsubscribe()
-Subscriber.extend(SubscriberMixin)
-module.exports = SubscriberMixin
diff --git a/src/syntax-scope-map.js b/src/syntax-scope-map.js
new file mode 100644
index 00000000000..fe3f07dd71a
--- /dev/null
+++ b/src/syntax-scope-map.js
@@ -0,0 +1,182 @@
+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}'`);
+}
diff --git a/src/task-bootstrap.coffee b/src/task-bootstrap.coffee
deleted file mode 100644
index ebb5cdc2b03..00000000000
--- a/src/task-bootstrap.coffee
+++ /dev/null
@@ -1,54 +0,0 @@
-{userAgent, taskPath} = process.env
-handler = null
-
-setupGlobals = ->
- global.attachEvent = ->
- console =
- warn: -> emit 'task:warn', arguments...
- log: -> emit 'task:log', arguments...
- error: -> emit 'task:error', arguments...
- trace: ->
- global.__defineGetter__ 'console', -> console
-
- global.document =
- createElement: ->
- setAttribute: ->
- getElementsByTagName: -> []
- appendChild: ->
- documentElement:
- insertBefore: ->
- removeChild: ->
- getElementById: -> {}
- createComment: -> {}
- createDocumentFragment: -> {}
-
- global.emit = (event, args...) ->
- process.send({event, args})
- global.navigator = {userAgent}
- global.window = global
-
-handleEvents = ->
- process.on 'uncaughtException', (error) ->
- console.error(error.message, error.stack)
- process.on 'message', ({event, args}={}) ->
- return unless event is 'start'
-
- isAsync = false
- async = ->
- isAsync = true
- (result) ->
- emit('task:completed', result)
- result = handler.bind({async})(args...)
- emit('task:completed', result) unless isAsync
-
-setupDeprecations = ->
- Grim = require 'grim'
- Grim.on 'updated', ->
- deprecations = Grim.getDeprecations().map (deprecation) -> deprecation.serialize()
- Grim.clearDeprecations()
- emit('task:deprecations', deprecations)
-
-setupGlobals()
-handleEvents()
-setupDeprecations()
-handler = require(taskPath)
diff --git a/src/task-bootstrap.js b/src/task-bootstrap.js
new file mode 100644
index 00000000000..1edbce8faa6
--- /dev/null
+++ b/src/task-bootstrap.js
@@ -0,0 +1,90 @@
+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);
diff --git a/src/task.coffee b/src/task.coffee
index d752ea11dde..fa09c69f103 100644
--- a/src/task.coffee
+++ b/src/task.coffee
@@ -1,6 +1,6 @@
_ = require 'underscore-plus'
-{fork} = require 'child_process'
-{Emitter} = require 'emissary'
+ChildProcess = require 'child_process'
+{Emitter} = require 'event-kit'
Grim = require 'grim'
# Extended: Run a node script in a separate process.
@@ -38,8 +38,6 @@ Grim = require 'grim'
# ```
module.exports =
class Task
- Emitter.includeInto(this)
-
# Public: A helper method to easily launch and run a task once.
#
# * `taskPath` The {String} path to the CoffeeScript/JavaScript file which
@@ -66,24 +64,13 @@ class Task
# * `taskPath` The {String} path to the CoffeeScript/JavaScript file that
# exports a single {Function} to execute.
constructor: (taskPath) ->
- coffeeCacheRequire = "require('#{require.resolve('coffee-cash')}')"
- coffeeCachePath = require('coffee-cash').getCacheDirectory()
- coffeeStackRequire = "require('#{require.resolve('coffeestack')}')"
- stackCachePath = require('coffeestack').getCacheDirectory()
- taskBootstrapRequire = "require('#{require.resolve('./task-bootstrap')}');"
- bootstrap = """
- #{coffeeCacheRequire}.setCacheDirectory('#{coffeeCachePath}');
- #{coffeeCacheRequire}.register();
- #{coffeeStackRequire}.setCacheDirectory('#{stackCachePath}');
- #{taskBootstrapRequire}
- """
- bootstrap = bootstrap.replace(/\\/g, "\\\\")
+ @emitter = new Emitter
+ compileCachePath = require('./compile-cache').getCacheDirectory()
taskPath = require.resolve(taskPath)
- taskPath = taskPath.replace(/\\/g, "\\\\")
- env = _.extend({}, process.env, {taskPath, userAgent: navigator.userAgent})
- @childProcess = fork '--eval', [bootstrap], {env, silent: true}
+ 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...)
@@ -99,12 +86,16 @@ class Task
handleEvents: ->
@childProcess.removeAllListeners()
@childProcess.on 'message', ({event, args}) =>
- @emit(event, args...) if @childProcess?
+ @emitter.emit(event, args) if @childProcess?
+
# Catch the errors that happened before task-bootstrap.
- @childProcess.stdout.on 'data', (data) ->
- console.log data.toString()
- @childProcess.stderr.on 'data', (data) ->
- console.error data.toString()
+ 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.
#
@@ -143,16 +134,32 @@ class Task
# * `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.call(this, eventName, callback)
+ 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 unless @childProcess?
+ return false unless @childProcess?
@childProcess.removeAllListeners()
+ @childProcess.stdout?.removeAllListeners()
+ @childProcess.stderr?.removeAllListeners()
@childProcess.kill()
@childProcess = null
- undefined
+ 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
diff --git a/src/test.ejs b/src/test.ejs
new file mode 100644
index 00000000000..7b93c31b320
--- /dev/null
+++ b/src/test.ejs
@@ -0,0 +1,9 @@
+
+
+<% if something() { %>
+
+ <%= html `ok how about this` %>
+
+<% } %>
+
+
diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee
deleted file mode 100644
index eb01e0f2391..00000000000
--- a/src/text-editor-component.coffee
+++ /dev/null
@@ -1,800 +0,0 @@
-_ = require 'underscore-plus'
-scrollbarStyle = require 'scrollbar-style'
-{Range, Point} = require 'text-buffer'
-grim = require 'grim'
-{CompositeDisposable} = require 'event-kit'
-ipc = require 'ipc'
-
-TextEditorPresenter = require './text-editor-presenter'
-GutterContainerComponent = require './gutter-container-component'
-InputComponent = require './input-component'
-LinesComponent = require './lines-component'
-ScrollbarComponent = require './scrollbar-component'
-ScrollbarCornerComponent = require './scrollbar-corner-component'
-OverlayManager = require './overlay-manager'
-
-module.exports =
-class TextEditorComponent
- scrollSensitivity: 0.4
- cursorBlinkPeriod: 800
- cursorBlinkResumeDelay: 100
- lineOverdrawMargin: 15
-
- pendingScrollTop: null
- pendingScrollLeft: null
- updateRequested: false
- updatesPaused: false
- updateRequestedWhilePaused: false
- heightAndWidthMeasurementRequested: false
- cursorMoved: false
- selectionChanged: false
- inputEnabled: true
- measureScrollbarsWhenShown: true
- measureLineHeightAndDefaultCharWidthWhenShown: true
- remeasureCharacterWidthsWhenShown: false
- stylingChangeAnimationFrameRequested: false
- gutterComponent: null
- mounted: true
-
- constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, lineOverdrawMargin}) ->
- @lineOverdrawMargin = lineOverdrawMargin if lineOverdrawMargin?
- @disposables = new CompositeDisposable
-
- @observeConfig()
- @setScrollSensitivity(atom.config.get('editor.scrollSensitivity'))
-
- @presenter = new TextEditorPresenter
- model: @editor
- scrollTop: @editor.getScrollTop()
- scrollLeft: @editor.getScrollLeft()
- lineOverdrawMargin: lineOverdrawMargin
- cursorBlinkPeriod: @cursorBlinkPeriod
- cursorBlinkResumeDelay: @cursorBlinkResumeDelay
- stoppedScrollingDelay: 200
-
- @presenter.onDidUpdateState(@requestUpdate)
-
- @domNode = document.createElement('div')
- if @useShadowDOM
- @domNode.classList.add('editor-contents--private')
-
- insertionPoint = document.createElement('content')
- insertionPoint.setAttribute('select', 'atom-overlay')
- @domNode.appendChild(insertionPoint)
- @overlayManager = new OverlayManager(@presenter, @hostElement)
- else
- @domNode.classList.add('editor-contents')
- @overlayManager = new OverlayManager(@presenter, @domNode)
-
- @scrollViewNode = document.createElement('div')
- @scrollViewNode.classList.add('scroll-view')
- @domNode.appendChild(@scrollViewNode)
-
- @mountGutterContainerComponent() if @presenter.getState().gutters.length
-
- @hiddenInputComponent = new InputComponent
- @scrollViewNode.appendChild(@hiddenInputComponent.getDomNode())
-
- @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM})
- @scrollViewNode.appendChild(@linesComponent.getDomNode())
-
- @horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll})
- @scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode())
-
- @verticalScrollbarComponent = new ScrollbarComponent({orientation: 'vertical', onScroll: @onVerticalScroll})
- @domNode.appendChild(@verticalScrollbarComponent.getDomNode())
-
- @scrollbarCornerComponent = new ScrollbarCornerComponent
- @domNode.appendChild(@scrollbarCornerComponent.getDomNode())
-
- @observeEditor()
- @listenForDOMEvents()
-
- @disposables.add @stylesElement.onDidAddStyleElement @onStylesheetsChanged
- @disposables.add @stylesElement.onDidUpdateStyleElement @onStylesheetsChanged
- @disposables.add @stylesElement.onDidRemoveStyleElement @onStylesheetsChanged
- unless atom.themes.isInitialLoadComplete()
- @disposables.add atom.themes.onDidChangeActiveThemes @onAllThemesLoaded
- @disposables.add scrollbarStyle.onDidChangePreferredScrollbarStyle @refreshScrollbars
-
- @disposables.add atom.views.pollDocument(@pollDOM)
-
- @updateSync()
- @checkForVisibilityChange()
-
- destroy: ->
- @mounted = false
- @disposables.dispose()
- @presenter.destroy()
- window.removeEventListener 'resize', @requestHeightAndWidthMeasurement
-
- getDomNode: ->
- @domNode
-
- updateSync: ->
- @oldState ?= {}
- @newState = @presenter.getState()
-
- cursorMoved = @cursorMoved
- selectionChanged = @selectionChanged
- @cursorMoved = false
- @selectionChanged = false
-
- if @editor.getLastSelection()? and not @editor.getLastSelection().isEmpty()
- @domNode.classList.add('has-selection')
- else
- @domNode.classList.remove('has-selection')
-
- if @newState.focused isnt @oldState.focused
- @domNode.classList.toggle('is-focused', @newState.focused)
-
- @performedInitialMeasurement = false if @editor.isDestroyed()
-
- if @performedInitialMeasurement
- if @newState.height isnt @oldState.height
- if @newState.height?
- @domNode.style.height = @newState.height + 'px'
- else
- @domNode.style.height = ''
-
- if @newState.gutters.length
- @mountGutterContainerComponent() unless @gutterContainerComponent?
- @gutterContainerComponent.updateSync(@newState)
- else
- @gutterContainerComponent?.getDomNode()?.remove()
- @gutterContainerComponent = null
-
- @hiddenInputComponent.updateSync(@newState)
- @linesComponent.updateSync(@newState)
- @horizontalScrollbarComponent.updateSync(@newState)
- @verticalScrollbarComponent.updateSync(@newState)
- @scrollbarCornerComponent.updateSync(@newState)
-
- @overlayManager?.render(@newState)
-
- if @editor.isAlive()
- @updateParentViewFocusedClassIfNeeded()
- @updateParentViewMiniClass()
- if grim.includeDeprecatedAPIs
- @hostElement.__spacePenView.trigger 'cursor:moved' if cursorMoved
- @hostElement.__spacePenView.trigger 'selection:changed' if selectionChanged
- @hostElement.__spacePenView.trigger 'editor:display-updated'
-
- readAfterUpdateSync: =>
- @linesComponent.measureCharactersInNewLines() if @isVisible() and not @newState.content.scrollingVertically
- @overlayManager?.measureOverlays()
-
- mountGutterContainerComponent: ->
- @gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown})
- @domNode.insertBefore(@gutterContainerComponent.getDomNode(), @domNode.firstChild)
-
- becameVisible: ->
- @updatesPaused = true
- @measureScrollbars() if @measureScrollbarsWhenShown
- @sampleFontStyling()
- @sampleBackgroundColors()
- @measureWindowSize()
- @measureDimensions()
- @measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown
- @remeasureCharacterWidths() if @remeasureCharacterWidthsWhenShown
- @editor.setVisible(true)
- @performedInitialMeasurement = true
- @updatesPaused = false
- @updateSync() if @canUpdate()
-
- requestUpdate: =>
- return unless @canUpdate()
-
- if @updatesPaused
- @updateRequestedWhilePaused = true
- return
-
- if @hostElement.isUpdatedSynchronously()
- @updateSync()
- else unless @updateRequested
- @updateRequested = true
- atom.views.updateDocument =>
- @updateRequested = false
- @updateSync() if @editor.isAlive()
- atom.views.readDocument(@readAfterUpdateSync)
-
- canUpdate: ->
- @mounted and @editor.isAlive()
-
- requestAnimationFrame: (fn) ->
- @updatesPaused = true
- requestAnimationFrame =>
- fn()
- @updatesPaused = false
- if @updateRequestedWhilePaused and @canUpdate()
- @updateRequestedWhilePaused = false
- @updateSync()
-
- getTopmostDOMNode: ->
- @hostElement
-
- observeEditor: ->
- @disposables.add @editor.observeGrammar(@onGrammarChanged)
- @disposables.add @editor.observeCursors(@onCursorAdded)
- @disposables.add @editor.observeSelections(@onSelectionAdded)
-
- listenForDOMEvents: ->
- @domNode.addEventListener 'mousewheel', @onMouseWheel
- @domNode.addEventListener 'textInput', @onTextInput
- @scrollViewNode.addEventListener 'mousedown', @onMouseDown
- @scrollViewNode.addEventListener 'scroll', @onScrollViewScroll
- window.addEventListener 'resize', @requestHeightAndWidthMeasurement
-
- @listenForIMEEvents()
- @trackSelectionClipboard() if process.platform is 'linux'
-
- listenForIMEEvents: ->
- # 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
- # 4. compositionend fired
- # OR User chooses a completion
- # 4. compositionend fired
- # 5. textInput fired; event.data == the completion string
-
- selectedText = null
- @domNode.addEventListener 'compositionstart', =>
- selectedText = @editor.getSelectedText()
- @domNode.addEventListener 'compositionupdate', (event) =>
- @editor.insertText(event.data, select: true, undo: 'skip')
- @domNode.addEventListener 'compositionend', (event) =>
- @editor.insertText(selectedText, select: true, undo: 'skip')
- event.target.value = ''
-
- # Listen for selection changes and store the currently selected text
- # in the selection clipboard. This is only applicable on Linux.
- trackSelectionClipboard: ->
- timeoutId = null
- writeSelectedTextToSelectionClipboard = =>
- return if @editor.isDestroyed()
- if selectedText = @editor.getSelectedText()
- # This uses ipc.send instead of clipboard.writeText because
- # clipboard.writeText is a sync ipc call on Linux and that
- # will slow down selections.
- ipc.send('write-text-to-selection-clipboard', selectedText)
- @disposables.add @editor.onDidChangeSelectionRange ->
- clearTimeout(timeoutId)
- timeoutId = setTimeout(writeSelectedTextToSelectionClipboard)
-
- observeConfig: ->
- @disposables.add atom.config.onDidChange 'editor.fontSize', @sampleFontStyling
- @disposables.add atom.config.onDidChange 'editor.fontFamily', @sampleFontStyling
- @disposables.add atom.config.onDidChange 'editor.lineHeight', @sampleFontStyling
-
- onGrammarChanged: =>
- if @scopedConfigDisposables?
- @scopedConfigDisposables.dispose()
- @disposables.remove(@scopedConfigDisposables)
-
- @scopedConfigDisposables = new CompositeDisposable
- @disposables.add(@scopedConfigDisposables)
-
- scope = @editor.getRootScopeDescriptor()
- @scopedConfigDisposables.add atom.config.observe 'editor.scrollSensitivity', {scope}, @setScrollSensitivity
-
- focused: ->
- if @mounted
- @presenter.setFocused(true)
- @hiddenInputComponent.getDomNode().focus()
-
- blurred: ->
- if @mounted
- @presenter.setFocused(false)
-
- onTextInput: (event) =>
- event.stopPropagation()
-
- # If we prevent the insertion of a space character, then the browser
- # interprets the spacebar keypress as a page-down command.
- event.preventDefault() unless event.data is ' '
-
- return unless @isInputEnabled()
-
- inputNode = event.target
-
- # Work around of the accented character suggestion feature in OS X.
- # Text input fires before a character is inserted, and if the browser is
- # replacing the previous un-accented character with an accented variant, it
- # will select backward over it.
- selectedLength = inputNode.selectionEnd - inputNode.selectionStart
- @editor.selectLeft() if selectedLength is 1
-
- insertedRange = @editor.transact atom.config.get('editor.undoGroupingInterval'), =>
- @editor.insertText(event.data)
- inputNode.value = event.data if insertedRange
-
- onVerticalScroll: (scrollTop) =>
- return if @updateRequested or scrollTop is @editor.getScrollTop()
-
- animationFramePending = @pendingScrollTop?
- @pendingScrollTop = scrollTop
- unless animationFramePending
- @requestAnimationFrame =>
- pendingScrollTop = @pendingScrollTop
- @pendingScrollTop = null
- @presenter.setScrollTop(pendingScrollTop)
-
- onHorizontalScroll: (scrollLeft) =>
- return if @updateRequested or scrollLeft is @editor.getScrollLeft()
-
- animationFramePending = @pendingScrollLeft?
- @pendingScrollLeft = scrollLeft
- unless animationFramePending
- @requestAnimationFrame =>
- @presenter.setScrollLeft(@pendingScrollLeft)
- @pendingScrollLeft = null
-
- onMouseWheel: (event) =>
- # Only scroll in one direction at a time
- {wheelDeltaX, wheelDeltaY} = event
-
- # Ctrl+MouseWheel adjusts font size.
- if event.ctrlKey and atom.config.get('editor.zoomFontWhenCtrlScrolling')
- if wheelDeltaY > 0
- atom.workspace.increaseFontSize()
- else if wheelDeltaY < 0
- atom.workspace.decreaseFontSize()
- event.preventDefault()
- return
-
- if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)
- # Scrolling horizontally
- previousScrollLeft = @presenter.getScrollLeft()
- @presenter.setScrollLeft(previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity))
- event.preventDefault() unless previousScrollLeft is @presenter.getScrollLeft()
- else
- # Scrolling vertically
- @presenter.setMouseWheelScreenRow(@screenRowForNode(event.target))
- previousScrollTop = @presenter.getScrollTop()
- @presenter.setScrollTop(previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity))
- event.preventDefault() unless previousScrollTop is @presenter.getScrollTop()
-
- onScrollViewScroll: =>
- if @mounted
- console.warn "TextEditorScrollView scrolled when it shouldn't have."
- @scrollViewNode.scrollTop = 0
- @scrollViewNode.scrollLeft = 0
-
- onMouseDown: (event) =>
- unless event.button is 0 or (event.button is 1 and process.platform is 'linux')
- # Only handle mouse down events for left mouse button on all platforms
- # and middle mouse button on Linux since it pastes the selection clipboard
- return
-
- return if event.target?.classList.contains('horizontal-scrollbar')
-
- {detail, shiftKey, metaKey, ctrlKey} = event
-
- # CTRL+click brings up the context menu on OSX, so don't handle those either
- return if ctrlKey and process.platform is 'darwin'
-
- # Prevent focusout event on hidden input if editor is already focused
- event.preventDefault() if @oldState.focused
-
- screenPosition = @screenPositionForMouseEvent(event)
-
- if event.target?.classList.contains('fold-marker')
- bufferRow = @editor.bufferRowForScreenRow(screenPosition.row)
- @editor.unfoldBufferRow(bufferRow)
- return
-
- switch detail
- when 1
- if shiftKey
- @editor.selectToScreenPosition(screenPosition)
- else if metaKey or (ctrlKey and process.platform isnt 'darwin')
- @editor.addCursorAtScreenPosition(screenPosition)
- else
- @editor.setCursorScreenPosition(screenPosition)
- when 2
- @editor.getLastSelection().selectWord()
- when 3
- @editor.getLastSelection().selectLine()
-
- @handleDragUntilMouseUp event, (screenPosition) =>
- @editor.selectToScreenPosition(screenPosition)
-
- onLineNumberGutterMouseDown: (event) =>
- return unless event.button is 0 # only handle the left mouse button
-
- {shiftKey, metaKey, ctrlKey} = event
-
- if shiftKey
- @onGutterShiftClick(event)
- else if metaKey or (ctrlKey and process.platform isnt 'darwin')
- @onGutterMetaClick(event)
- else
- @onGutterClick(event)
-
- onGutterClick: (event) =>
- clickedRow = @screenPositionForMouseEvent(event).row
- clickedBufferRow = @editor.bufferRowForScreenRow(clickedRow)
-
- @editor.setSelectedBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]], preserveFolds: true)
-
- @handleDragUntilMouseUp event, (screenPosition) =>
- dragRow = screenPosition.row
- dragBufferRow = @editor.bufferRowForScreenRow(dragRow)
- if dragBufferRow < clickedBufferRow # dragging up
- @editor.setSelectedBufferRange([[dragBufferRow, 0], [clickedBufferRow + 1, 0]], preserveFolds: true)
- else
- @editor.setSelectedBufferRange([[clickedBufferRow, 0], [dragBufferRow + 1, 0]], preserveFolds: true)
-
- onGutterMetaClick: (event) =>
- clickedRow = @screenPositionForMouseEvent(event).row
- clickedBufferRow = @editor.bufferRowForScreenRow(clickedRow)
-
- bufferRange = new Range([clickedBufferRow, 0], [clickedBufferRow + 1, 0])
- rowSelection = @editor.addSelectionForBufferRange(bufferRange, preserveFolds: true)
-
- @handleDragUntilMouseUp event, (screenPosition) =>
- dragRow = screenPosition.row
- dragBufferRow = @editor.bufferRowForScreenRow(dragRow)
-
- if dragBufferRow < clickedBufferRow # dragging up
- rowSelection.setBufferRange([[dragBufferRow, 0], [clickedBufferRow + 1, 0]], preserveFolds: true)
- else
- rowSelection.setBufferRange([[clickedBufferRow, 0], [dragBufferRow + 1, 0]], preserveFolds: true)
-
- # After updating the selected screen range, merge overlapping selections
- @editor.mergeIntersectingSelections(preserveFolds: true)
-
- # The merge process will possibly destroy the current selection because
- # it will be merged into another one. Therefore, we need to obtain a
- # reference to the new selection that contains the originally selected row
- rowSelection = _.find @editor.getSelections(), (selection) ->
- selection.intersectsBufferRange(bufferRange)
-
- onGutterShiftClick: (event) =>
- clickedRow = @screenPositionForMouseEvent(event).row
- clickedBufferRow = @editor.bufferRowForScreenRow(clickedRow)
- tailPosition = @editor.getLastSelection().getTailScreenPosition()
- tailBufferPosition = @editor.bufferPositionForScreenPosition(tailPosition)
-
- if clickedRow < tailPosition.row
- @editor.selectToBufferPosition([clickedBufferRow, 0])
- else
- @editor.selectToBufferPosition([clickedBufferRow + 1, 0])
-
- @handleDragUntilMouseUp event, (screenPosition) =>
- dragRow = screenPosition.row
- dragBufferRow = @editor.bufferRowForScreenRow(dragRow)
- if dragRow < tailPosition.row # dragging up
- @editor.setSelectedBufferRange([[dragBufferRow, 0], tailBufferPosition], preserveFolds: true)
- else
- @editor.setSelectedBufferRange([tailBufferPosition, [dragBufferRow + 1, 0]], preserveFolds: true)
-
-
- onStylesheetsChanged: (styleElement) =>
- return unless @performedInitialMeasurement
- return unless atom.themes.isInitialLoadComplete()
-
- # This delay prevents the styling from going haywire when stylesheets are
- # reloaded in dev mode. It seems like a workaround for a browser bug, but
- # not totally sure.
-
- unless @stylingChangeAnimationFrameRequested
- @stylingChangeAnimationFrameRequested = true
- requestAnimationFrame =>
- @stylingChangeAnimationFrameRequested = false
- if @mounted
- @refreshScrollbars() if not styleElement.sheet? or @containsScrollbarSelector(styleElement.sheet)
- @handleStylingChange()
-
- onAllThemesLoaded: =>
- @refreshScrollbars()
- @handleStylingChange()
-
- handleStylingChange: =>
- @sampleFontStyling()
- @sampleBackgroundColors()
- @remeasureCharacterWidths()
-
- onSelectionAdded: (selection) =>
- selectionDisposables = new CompositeDisposable
- selectionDisposables.add selection.onDidChangeRange => @onSelectionChanged(selection)
- selectionDisposables.add selection.onDidDestroy =>
- @onSelectionChanged(selection)
- selectionDisposables.dispose()
- @disposables.remove(selectionDisposables)
-
- @disposables.add(selectionDisposables)
-
- if @editor.selectionIntersectsVisibleRowRange(selection)
- @selectionChanged = true
-
- onSelectionChanged: (selection) =>
- if @editor.selectionIntersectsVisibleRowRange(selection)
- @selectionChanged = true
-
- onCursorAdded: (cursor) =>
- @disposables.add cursor.onDidChangePosition @onCursorMoved
-
- onCursorMoved: =>
- @cursorMoved = true
-
- handleDragUntilMouseUp: (event, dragHandler) =>
- dragging = false
- lastMousePosition = {}
- animationLoop = =>
- @requestAnimationFrame =>
- if dragging and @mounted
- screenPosition = @screenPositionForMouseEvent(lastMousePosition)
- dragHandler(screenPosition)
- animationLoop()
- else if not @mounted
- stopDragging()
-
- onMouseMove = (event) ->
- lastMousePosition.clientX = event.clientX
- lastMousePosition.clientY = event.clientY
-
- # Start the animation loop when the mouse moves prior to a mouseup event
- unless dragging
- dragging = true
- animationLoop()
-
- # Stop dragging when cursor enters dev tools because we can't detect mouseup
- onMouseUp() if event.which is 0
-
- onMouseUp = (event) =>
- stopDragging()
- @editor.finalizeSelections()
- pasteSelectionClipboard(event)
-
- stopDragging = ->
- dragging = false
- window.removeEventListener('mousemove', onMouseMove)
- window.removeEventListener('mouseup', onMouseUp)
-
- pasteSelectionClipboard = (event) =>
- if event?.which is 2 and process.platform is 'linux'
- if selection = require('./safe-clipboard').readText('selection')
- @editor.insertText(selection)
-
- window.addEventListener('mousemove', onMouseMove)
- window.addEventListener('mouseup', onMouseUp)
-
- isVisible: ->
- @domNode.offsetHeight > 0 or @domNode.offsetWidth > 0
-
- pollDOM: =>
- unless @checkForVisibilityChange()
- @sampleBackgroundColors()
- @measureDimensions()
- @sampleFontStyling()
- @overlayManager?.measureOverlays()
-
- checkForVisibilityChange: ->
- if @isVisible()
- if @wasVisible
- false
- else
- @becameVisible()
- @wasVisible = true
- else
- @wasVisible = false
-
- requestHeightAndWidthMeasurement: =>
- return if @heightAndWidthMeasurementRequested
-
- @heightAndWidthMeasurementRequested = true
- requestAnimationFrame =>
- @heightAndWidthMeasurementRequested = false
- @measureDimensions()
- @measureWindowSize()
-
- # Measure explicitly-styled height and width and relay them to the model. If
- # these values aren't explicitly styled, we assume the editor is unconstrained
- # and use the scrollHeight / scrollWidth as its height and width in
- # calculations.
- measureDimensions: ->
- return unless @mounted
-
- {position} = getComputedStyle(@hostElement)
- {height} = @hostElement.style
-
- if position is 'absolute' or height
- @presenter.setAutoHeight(false)
- height = @hostElement.offsetHeight
- if height > 0
- @presenter.setExplicitHeight(height)
- else
- @presenter.setAutoHeight(true)
- @presenter.setExplicitHeight(null)
-
- clientWidth = @scrollViewNode.clientWidth
- paddingLeft = parseInt(getComputedStyle(@scrollViewNode).paddingLeft)
- clientWidth -= paddingLeft
- if clientWidth > 0
- @presenter.setContentFrameWidth(clientWidth)
-
- @presenter.setGutterWidth(@gutterContainerComponent?.getDomNode().offsetWidth ? 0)
- @presenter.setBoundingClientRect(@hostElement.getBoundingClientRect())
-
- measureWindowSize: ->
- return unless @mounted
-
- # FIXME: on Ubuntu (via xvfb) `window.innerWidth` reports an incorrect value
- # when window gets resized through `atom.setWindowDimensions({width:
- # windowWidth, height: windowHeight})`.
- @presenter.setWindowSize(window.innerWidth, window.innerHeight)
-
- sampleFontStyling: =>
- oldFontSize = @fontSize
- oldFontFamily = @fontFamily
- oldLineHeight = @lineHeight
-
- {@fontSize, @fontFamily, @lineHeight} = getComputedStyle(@getTopmostDOMNode())
-
- if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight
- @measureLineHeightAndDefaultCharWidth()
-
- if (@fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily) and @performedInitialMeasurement
- @remeasureCharacterWidths()
-
- sampleBackgroundColors: (suppressUpdate) ->
- {backgroundColor} = getComputedStyle(@hostElement)
-
- @presenter.setBackgroundColor(backgroundColor)
-
- lineNumberGutter = @gutterContainerComponent?.getLineNumberGutterComponent()
- if lineNumberGutter
- gutterBackgroundColor = getComputedStyle(lineNumberGutter.getDomNode()).backgroundColor
- @presenter.setGutterBackgroundColor(gutterBackgroundColor)
-
- measureLineHeightAndDefaultCharWidth: ->
- if @isVisible()
- @measureLineHeightAndDefaultCharWidthWhenShown = false
- @linesComponent.measureLineHeightAndDefaultCharWidth()
- else
- @measureLineHeightAndDefaultCharWidthWhenShown = true
-
- remeasureCharacterWidths: ->
- if @isVisible()
- @remeasureCharacterWidthsWhenShown = false
- @linesComponent.remeasureCharacterWidths()
- else
- @remeasureCharacterWidthsWhenShown = true
-
- measureScrollbars: ->
- @measureScrollbarsWhenShown = false
-
- cornerNode = @scrollbarCornerComponent.getDomNode()
- originalDisplayValue = cornerNode.style.display
-
- cornerNode.style.display = 'block'
-
- width = (cornerNode.offsetWidth - cornerNode.clientWidth) or 15
- height = (cornerNode.offsetHeight - cornerNode.clientHeight) or 15
-
- @presenter.setVerticalScrollbarWidth(width)
- @presenter.setHorizontalScrollbarHeight(height)
-
- cornerNode.style.display = originalDisplayValue
-
- containsScrollbarSelector: (stylesheet) ->
- for rule in stylesheet.cssRules
- if rule.selectorText?.indexOf('scrollbar') > -1
- return true
- false
-
- refreshScrollbars: =>
- if @isVisible()
- @measureScrollbarsWhenShown = false
- else
- @measureScrollbarsWhenShown = true
- return
-
- verticalNode = @verticalScrollbarComponent.getDomNode()
- horizontalNode = @horizontalScrollbarComponent.getDomNode()
- cornerNode = @scrollbarCornerComponent.getDomNode()
-
- originalVerticalDisplayValue = verticalNode.style.display
- originalHorizontalDisplayValue = horizontalNode.style.display
- originalCornerDisplayValue = cornerNode.style.display
-
- # First, hide all scrollbars in case they are visible so they take on new
- # styles when they are shown again.
- verticalNode.style.display = 'none'
- horizontalNode.style.display = 'none'
- cornerNode.style.display = 'none'
-
- # Force a reflow
- cornerNode.offsetWidth
-
- # Now measure the new scrollbar dimensions
- @measureScrollbars()
-
- # Now restore the display value for all scrollbars, since they were
- # previously hidden
- verticalNode.style.display = originalVerticalDisplayValue
- horizontalNode.style.display = originalHorizontalDisplayValue
- cornerNode.style.display = originalCornerDisplayValue
-
- consolidateSelections: (e) ->
- e.abortKeyBinding() unless @editor.consolidateSelections()
-
- lineNodeForScreenRow: (screenRow) -> @linesComponent.lineNodeForScreenRow(screenRow)
-
- lineNumberNodeForScreenRow: (screenRow) -> @gutterContainerComponent.getLineNumberGutterComponent().lineNumberNodeForScreenRow(screenRow)
-
- screenRowForNode: (node) ->
- while node?
- if screenRow = node.dataset.screenRow
- return parseInt(screenRow)
- node = node.parentElement
- null
-
- getFontSize: ->
- parseInt(getComputedStyle(@getTopmostDOMNode()).fontSize)
-
- setFontSize: (fontSize) ->
- @getTopmostDOMNode().style.fontSize = fontSize + 'px'
- @sampleFontStyling()
-
- getFontFamily: ->
- getComputedStyle(@getTopmostDOMNode()).fontFamily
-
- setFontFamily: (fontFamily) ->
- @getTopmostDOMNode().style.fontFamily = fontFamily
- @sampleFontStyling()
-
- setLineHeight: (lineHeight) ->
- @getTopmostDOMNode().style.lineHeight = lineHeight
- @sampleFontStyling()
-
- setShowIndentGuide: (showIndentGuide) ->
- atom.config.set("editor.showIndentGuide", showIndentGuide)
-
- setScrollSensitivity: (scrollSensitivity) =>
- if scrollSensitivity = parseInt(scrollSensitivity)
- @scrollSensitivity = Math.abs(scrollSensitivity) / 100
-
- screenPositionForMouseEvent: (event) ->
- pixelPosition = @pixelPositionForMouseEvent(event)
- @editor.screenPositionForPixelPosition(pixelPosition)
-
- pixelPositionForMouseEvent: (event) ->
- {clientX, clientY} = event
-
- linesClientRect = @linesComponent.getDomNode().getBoundingClientRect()
- top = clientY - linesClientRect.top
- left = clientX - linesClientRect.left
- {top, left}
-
- getModel: ->
- @editor
-
- isInputEnabled: -> @inputEnabled
-
- setInputEnabled: (@inputEnabled) -> @inputEnabled
-
- updateParentViewFocusedClassIfNeeded: ->
- if @oldState.focused isnt @newState.focused
- @hostElement.classList.toggle('is-focused', @newState.focused)
- @rootElement.classList.toggle('is-focused', @newState.focused)
- @oldState.focused = @newState.focused
-
- updateParentViewMiniClass: ->
- @hostElement.classList.toggle('mini', @editor.isMini())
- @rootElement.classList.toggle('mini', @editor.isMini())
-
-if grim.includeDeprecatedAPIs
- TextEditorComponent::setInvisibles = (invisibles={}) ->
- grim.deprecate "Use config.set('editor.invisibles', invisibles) instead"
- atom.config.set('editor.invisibles', invisibles)
-
- TextEditorComponent::setShowInvisibles = (showInvisibles) ->
- grim.deprecate "Use config.set('editor.showInvisibles', showInvisibles) instead"
- atom.config.set('editor.showInvisibles', showInvisibles)
diff --git a/src/text-editor-component.js b/src/text-editor-component.js
new file mode 100644
index 00000000000..b00e16afa29
--- /dev/null
+++ b/src/text-editor-component.js
@@ -0,0 +1,5211 @@
+/* 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
+ );
+}
diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee
deleted file mode 100644
index beeae6cc624..00000000000
--- a/src/text-editor-element.coffee
+++ /dev/null
@@ -1,349 +0,0 @@
-{Emitter} = require 'event-kit'
-{View, $, callRemoveHooks} = require 'space-pen'
-Path = require 'path'
-{defaults} = require 'underscore-plus'
-TextBuffer = require 'text-buffer'
-Grim = require 'grim'
-TextEditor = require './text-editor'
-TextEditorComponent = require './text-editor-component'
-TextEditorView = null
-
-ShadowStyleSheet = null
-
-class TextEditorElement extends HTMLElement
- model: null
- componentDescriptor: null
- component: null
- attached: false
- lineOverdrawMargin: null
- focusOnAttach: false
-
- createdCallback: ->
- @emitter = new Emitter
- @initializeContent()
- @createSpacePenShim() if Grim.includeDeprecatedAPIs
- @addEventListener 'focus', @focused.bind(this)
- @addEventListener 'blur', @blurred.bind(this)
-
- initializeContent: (attributes) ->
- @classList.add('editor')
- @setAttribute('tabindex', -1)
-
- if atom.config.get('editor.useShadowDOM')
- @useShadowDOM = true
-
- unless ShadowStyleSheet?
- ShadowStyleSheet = document.createElement('style')
- ShadowStyleSheet.textContent = atom.themes.loadLessStylesheet(require.resolve('../static/text-editor-shadow.less'))
-
- @createShadowRoot()
-
- @shadowRoot.appendChild(ShadowStyleSheet.cloneNode(true))
- @stylesElement = document.createElement('atom-styles')
- @stylesElement.setAttribute('context', 'atom-text-editor')
- @stylesElement.initialize()
-
- @rootElement = document.createElement('div')
- @rootElement.classList.add('editor--private')
-
- @shadowRoot.appendChild(@stylesElement)
- @shadowRoot.appendChild(@rootElement)
- else
- @useShadowDOM = false
-
- @classList.add('editor', 'editor-colors')
- @stylesElement = document.head.querySelector('atom-styles')
- @rootElement = this
-
- createSpacePenShim: ->
- TextEditorView ?= require './text-editor-view'
- @__spacePenView = new TextEditorView(this)
-
- attachedCallback: ->
- @buildModel() unless @getModel()?
- @mountComponent() unless @component?
- @component.checkForVisibilityChange()
- if this is document.activeElement
- @focused()
- @emitter.emit("did-attach")
-
- detachedCallback: ->
- @unmountComponent()
- @emitter.emit("did-detach")
-
- initialize: (model) ->
- @setModel(model)
- this
-
- setModel: (model) ->
- throw new Error("Model already assigned on TextEditorElement") if @model?
- return if model.isDestroyed()
-
- @model = model
- @mountComponent()
- @addGrammarScopeAttribute()
- @addMiniAttributeIfNeeded()
- @addEncodingAttribute()
- @model.onDidChangeGrammar => @addGrammarScopeAttribute()
- @model.onDidChangeEncoding => @addEncodingAttribute()
- @model.onDidDestroy => @unmountComponent()
- @__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs
- @model
-
- getModel: ->
- @model ? @buildModel()
-
- buildModel: ->
- @setModel(new TextEditor(
- buffer: new TextBuffer(@textContent)
- softWrapped: false
- tabLength: 2
- softTabs: true
- mini: @hasAttribute('mini')
- lineNumberGutterVisible: not @hasAttribute('gutter-hidden')
- placeholderText: @getAttribute('placeholder-text')
- ))
-
- mountComponent: ->
- @component = new TextEditorComponent(
- hostElement: this
- rootElement: @rootElement
- stylesElement: @stylesElement
- editor: @model
- lineOverdrawMargin: @lineOverdrawMargin
- useShadowDOM: @useShadowDOM
- )
- @rootElement.appendChild(@component.getDomNode())
-
- if @useShadowDOM
- @shadowRoot.addEventListener('blur', @shadowRootBlurred.bind(this), true)
- else
- inputNode = @component.hiddenInputComponent.getDomNode()
- inputNode.addEventListener 'focus', @focused.bind(this)
- inputNode.addEventListener 'blur', => @dispatchEvent(new FocusEvent('blur', bubbles: false))
-
- unmountComponent: ->
- callRemoveHooks(this)
- if @component?
- @component.destroy()
- @component.getDomNode().remove()
- @component = null
-
- focused: ->
- @component?.focused()
-
- blurred: (event) ->
- unless @useShadowDOM
- if event.relatedTarget is @component.hiddenInputComponent.getDomNode()
- event.stopImmediatePropagation()
- return
-
- @component?.blurred()
-
- # Work around what seems to be a bug in Chromium. Focus can be stolen from the
- # hidden input when clicking on the gutter and transferred to the
- # already-focused host element. The host element never gets a 'focus' event
- # however, which leaves us in a limbo state where the text editor element is
- # focused but the hidden input isn't focused. This always refocuses the hidden
- # input if a blur event occurs in the shadow DOM that is transferring focus
- # back to the host element.
- shadowRootBlurred: (event) ->
- @component.focused() if event.relatedTarget is this
-
- addGrammarScopeAttribute: ->
- grammarScope = @model.getGrammar()?.scopeName?.replace(/\./g, ' ')
- @dataset.grammar = grammarScope
-
- addMiniAttributeIfNeeded: ->
- @setAttributeNode(document.createAttribute("mini")) if @model.isMini()
-
- addEncodingAttribute: ->
- @dataset.encoding = @model.getEncoding()
-
- hasFocus: ->
- this is document.activeElement or @contains(document.activeElement)
-
- setUpdatedSynchronously: (@updatedSynchronously) -> @updatedSynchronously
-
- isUpdatedSynchronously: -> @updatedSynchronously
-
- # Extended: get the width of a character of text displayed in this element.
- #
- # Returns a {Number} of pixels.
- getDefaultCharacterWidth: ->
- @getModel().getDefaultCharWidth()
-
- # Extended: Converts a buffer position to a pixel position.
- #
- # * `bufferPosition` An object that represents a buffer position. It can be either
- # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
- #
- # Returns an {Object} with two values: `top` and `left`, representing the pixel position.
- pixelPositionForBufferPosition: (bufferPosition) ->
- @getModel().pixelPositionForBufferPosition(bufferPosition, true)
-
- # Extended: Converts a screen position to a pixel position.
- #
- # * `screenPosition` An object that represents a screen position. It can be either
- # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
- #
- # Returns an {Object} with two values: `top` and `left`, representing the pixel positions.
- pixelPositionForScreenPosition: (screenPosition) ->
- @getModel().pixelPositionForScreenPosition(screenPosition, true)
-
- # Extended: Retrieves the number of the row that is visible and currently at the
- # top of the editor.
- #
- # Returns a {Number}.
- getFirstVisibleScreenRow: ->
- @getModel().getFirstVisibleScreenRow(true)
-
- # Extended: Retrieves the number of the row that is visible and currently at the
- # bottom of the editor.
- #
- # Returns a {Number}.
- getLastVisibleScreenRow: ->
- @getModel().getLastVisibleScreenRow(true)
-
- # Extended: call the given `callback` when the editor is attached to the DOM.
- #
- # * `callback` {Function}
- onDidAttach: (callback) ->
- @emitter.on("did-attach", callback)
-
- # Extended: call the given `callback` when the editor is detached from the DOM.
- #
- # * `callback` {Function}
- onDidDetach: (callback) ->
- @emitter.on("did-detach", callback)
-
-stopEventPropagation = (commandListeners) ->
- newCommandListeners = {}
- for commandName, commandListener of commandListeners
- do (commandListener) ->
- newCommandListeners[commandName] = (event) ->
- event.stopPropagation()
- commandListener.call(@getModel(), event)
- newCommandListeners
-
-stopEventPropagationAndGroupUndo = (commandListeners) ->
- newCommandListeners = {}
- for commandName, commandListener of commandListeners
- do (commandListener) ->
- newCommandListeners[commandName] = (event) ->
- event.stopPropagation()
- model = @getModel()
- model.transact atom.config.get('editor.undoGroupingInterval'), ->
- commandListener.call(model, event)
- newCommandListeners
-
-atom.commands.add 'atom-text-editor', stopEventPropagation(
- 'core:undo': -> @undo()
- 'core:redo': -> @redo()
- '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: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-first-character-of-line': -> @selectToFirstCharacterOfLine()
- 'editor:select-line': -> @selectLinesContainingCursors()
-)
-
-atom.commands.add 'atom-text-editor', stopEventPropagationAndGroupUndo(
- 'core:backspace': -> @backspace()
- 'core:delete': -> @delete()
- 'core:cut': -> @cutSelectedText()
- 'core:copy': -> @copySelectedText()
- 'core:paste': -> @pasteText()
- '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-line': -> @deleteLine()
- 'editor:cut-to-end-of-line': -> @cutToEndOfLine()
- 'editor:transpose': -> @transpose()
- 'editor:upper-case': -> @upperCase()
- 'editor:lower-case': -> @lowerCase()
-)
-
-atom.commands.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()
- 'editor:unfold-current-row': -> @unfoldCurrentRow()
- 'editor:fold-selection': -> @foldSelectedLines()
- 'editor:fold-at-indent-level-1': -> @foldAllAtIndentLevel(0)
- 'editor:fold-at-indent-level-2': -> @foldAllAtIndentLevel(1)
- 'editor:fold-at-indent-level-3': -> @foldAllAtIndentLevel(2)
- 'editor:fold-at-indent-level-4': -> @foldAllAtIndentLevel(3)
- 'editor:fold-at-indent-level-5': -> @foldAllAtIndentLevel(4)
- 'editor:fold-at-indent-level-6': -> @foldAllAtIndentLevel(5)
- 'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6)
- 'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7)
- 'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8)
- 'editor:log-cursor-scope': -> @logCursorScope()
- 'editor:copy-path': -> @copyPathToClipboard()
- 'editor:toggle-indent-guide': -> atom.config.set('editor.showIndentGuide', not atom.config.get('editor.showIndentGuide'))
- 'editor:toggle-line-numbers': -> atom.config.set('editor.showLineNumbers', not atom.config.get('editor.showLineNumbers'))
- 'editor:scroll-to-cursor': -> @scrollToCursorPosition()
-)
-
-atom.commands.add 'atom-text-editor:not([mini])', stopEventPropagationAndGroupUndo(
- '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': -> @checkoutHeadRevision()
- 'editor:move-line-up': -> @moveLineUp()
- 'editor:move-line-down': -> @moveLineDown()
- 'editor:duplicate-lines': -> @duplicateLines()
- 'editor:join-lines': -> @joinLines()
-)
-
-module.exports = TextEditorElement = document.registerElement 'atom-text-editor', prototype: TextEditorElement.prototype
diff --git a/src/text-editor-element.js b/src/text-editor-element.js
new file mode 100644
index 00000000000..0ce67f7e6cc
--- /dev/null
+++ b/src/text-editor-element.js
@@ -0,0 +1,368 @@
+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;
diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee
deleted file mode 100644
index 3aea57f294a..00000000000
--- a/src/text-editor-presenter.coffee
+++ /dev/null
@@ -1,1373 +0,0 @@
-{CompositeDisposable, Emitter} = require 'event-kit'
-{Point, Range} = require 'text-buffer'
-_ = require 'underscore-plus'
-Decoration = require './decoration'
-
-module.exports =
-class TextEditorPresenter
- toggleCursorBlinkHandle: null
- startBlinkingCursorsAfterDelay: null
- stoppedScrollingTimeoutId: null
- mouseWheelScreenRow: null
- scopedCharacterWidthsChangeCount: 0
- overlayDimensions: {}
-
- constructor: (params) ->
- {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params
- {horizontalScrollbarHeight, verticalScrollbarWidth} = params
- {@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor} = params
- {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params
- @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight
- @measuredVerticalScrollbarWidth = verticalScrollbarWidth
- @gutterWidth ?= 0
-
- @disposables = new CompositeDisposable
- @emitter = new Emitter
- @characterWidthsByScope = {}
- @transferMeasurementsToModel()
- @observeModel()
- @observeConfig()
- @buildState()
- @startBlinkingCursors() if @focused
- @updating = false
-
- destroy: ->
- @disposables.dispose()
-
- # Calls your `callback` when some changes in the model occurred and the current state has been updated.
- onDidUpdateState: (callback) ->
- @emitter.on 'did-update-state', callback
-
- emitDidUpdateState: ->
- @emitter.emit "did-update-state" if @isBatching()
-
- transferMeasurementsToModel: ->
- @model.setHeight(@explicitHeight) if @explicitHeight?
- @model.setWidth(@contentFrameWidth) if @contentFrameWidth?
- @model.setLineHeightInPixels(@lineHeight) if @lineHeight?
- @model.setDefaultCharWidth(@baseCharacterWidth) if @baseCharacterWidth?
- @model.setScrollTop(@scrollTop) if @scrollTop?
- @model.setScrollLeft(@scrollLeft) if @scrollLeft?
- @model.setVerticalScrollbarWidth(@measuredVerticalScrollbarWidth) if @measuredVerticalScrollbarWidth?
- @model.setHorizontalScrollbarHeight(@measuredHorizontalScrollbarHeight) if @measuredHorizontalScrollbarHeight?
-
- # Private: Determines whether {TextEditorPresenter} is currently batching changes.
- # Returns a {Boolean}, `true` if is collecting changes, `false` if is applying them.
- isBatching: ->
- @updating is false
-
- # Public: Gets this presenter's state, updating it just in time before returning from this function.
- # Returns a state {Object}, useful for rendering to screen.
- getState: ->
- @updating = true
-
- @updateContentDimensions()
- @updateScrollbarDimensions()
- @updateStartRow()
- @updateEndRow()
-
- @updateFocusedState() if @shouldUpdateFocusedState
- @updateHeightState() if @shouldUpdateHeightState
- @updateVerticalScrollState() if @shouldUpdateVerticalScrollState
- @updateHorizontalScrollState() if @shouldUpdateHorizontalScrollState
- @updateScrollbarsState() if @shouldUpdateScrollbarsState
- @updateHiddenInputState() if @shouldUpdateHiddenInputState
- @updateContentState() if @shouldUpdateContentState
- @updateDecorations() if @shouldUpdateDecorations
- @updateLinesState() if @shouldUpdateLinesState
- @updateCursorsState() if @shouldUpdateCursorsState
- @updateOverlaysState() if @shouldUpdateOverlaysState
- @updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState
- @updateLineNumbersState() if @shouldUpdateLineNumbersState
- @updateGutterOrderState() if @shouldUpdateGutterOrderState
- @updateCustomGutterDecorationState() if @shouldUpdateCustomGutterDecorationState
- @updating = false
-
- @resetTrackedUpdates()
-
- @state
-
- resetTrackedUpdates: ->
- @shouldUpdateFocusedState = false
- @shouldUpdateHeightState = false
- @shouldUpdateVerticalScrollState = false
- @shouldUpdateHorizontalScrollState = false
- @shouldUpdateScrollbarsState = false
- @shouldUpdateHiddenInputState = false
- @shouldUpdateContentState = false
- @shouldUpdateDecorations = false
- @shouldUpdateLinesState = false
- @shouldUpdateCursorsState = false
- @shouldUpdateOverlaysState = false
- @shouldUpdateLineNumberGutterState = false
- @shouldUpdateLineNumbersState = false
- @shouldUpdateGutterOrderState = false
- @shouldUpdateCustomGutterDecorationState = false
-
- observeModel: ->
- @disposables.add @model.onDidChange =>
- @updateContentDimensions()
- @updateEndRow()
- @shouldUpdateHeightState = true
- @shouldUpdateVerticalScrollState = true
- @shouldUpdateHorizontalScrollState = true
- @shouldUpdateScrollbarsState = true
- @shouldUpdateContentState = true
- @shouldUpdateDecorations = true
- @shouldUpdateLinesState = true
- @shouldUpdateLineNumberGutterState = true
- @shouldUpdateLineNumbersState = true
- @shouldUpdateGutterOrderState = true
- @shouldUpdateCustomGutterDecorationState = true
-
- @emitDidUpdateState()
- @disposables.add @model.onDidChangeGrammar(@didChangeGrammar.bind(this))
- @disposables.add @model.onDidChangePlaceholderText =>
- @shouldUpdateContentState = true
-
- @emitDidUpdateState()
- @disposables.add @model.onDidChangeMini =>
- @shouldUpdateScrollbarsState = true
- @shouldUpdateContentState = true
- @shouldUpdateDecorations = true
- @shouldUpdateLinesState = true
- @shouldUpdateLineNumberGutterState = true
- @shouldUpdateLineNumbersState = true
- @shouldUpdateGutterOrderState = true
- @shouldUpdateCustomGutterDecorationState = true
- @updateScrollbarDimensions()
- @updateCommonGutterState()
-
- @emitDidUpdateState()
- @disposables.add @model.onDidChangeLineNumberGutterVisible =>
- @shouldUpdateLineNumberGutterState = true
- @shouldUpdateGutterOrderState = true
- @updateCommonGutterState()
-
- @emitDidUpdateState()
- @disposables.add @model.onDidAddDecoration(@didAddDecoration.bind(this))
- @disposables.add @model.onDidAddCursor(@didAddCursor.bind(this))
- @disposables.add @model.onDidChangeScrollTop(@setScrollTop.bind(this))
- @disposables.add @model.onDidChangeScrollLeft(@setScrollLeft.bind(this))
- @observeDecoration(decoration) for decoration in @model.getDecorations()
- @observeCursor(cursor) for cursor in @model.getCursors()
- @disposables.add @model.onDidAddGutter(@didAddGutter.bind(this))
- return
-
- observeConfig: ->
- configParams = {scope: @model.getRootScopeDescriptor()}
-
- @scrollPastEnd = atom.config.get('editor.scrollPastEnd', configParams)
- @showLineNumbers = atom.config.get('editor.showLineNumbers', configParams)
- @showIndentGuide = atom.config.get('editor.showIndentGuide', configParams)
-
- if @configDisposables?
- @configDisposables?.dispose()
- @disposables.remove(@configDisposables)
-
- @configDisposables = new CompositeDisposable
- @disposables.add(@configDisposables)
-
- @configDisposables.add atom.config.onDidChange 'editor.showIndentGuide', configParams, ({newValue}) =>
- @showIndentGuide = newValue
- @shouldUpdateContentState = true
-
- @emitDidUpdateState()
- @configDisposables.add atom.config.onDidChange 'editor.scrollPastEnd', configParams, ({newValue}) =>
- @scrollPastEnd = newValue
- @shouldUpdateVerticalScrollState = true
- @shouldUpdateScrollbarsState = true
- @updateScrollHeight()
-
- @emitDidUpdateState()
- @configDisposables.add atom.config.onDidChange 'editor.showLineNumbers', configParams, ({newValue}) =>
- @showLineNumbers = newValue
- @shouldUpdateLineNumberGutterState = true
- @shouldUpdateGutterOrderState = true
- @updateCommonGutterState()
-
- @emitDidUpdateState()
-
- didChangeGrammar: ->
- @observeConfig()
- @shouldUpdateContentState = true
- @shouldUpdateLineNumberGutterState = true
- @shouldUpdateGutterOrderState = true
- @updateCommonGutterState()
-
- @emitDidUpdateState()
-
- buildState: ->
- @state =
- horizontalScrollbar: {}
- verticalScrollbar: {}
- hiddenInput: {}
- content:
- scrollingVertically: false
- cursorsVisible: false
- lines: {}
- highlights: {}
- overlays: {}
- gutters: []
- # Shared state that is copied into ``@state.gutters`.
- @sharedGutterStyles = {}
- @customGutterDecorations = {}
- @lineNumberGutter =
- lineNumbers: {}
-
- @updateState()
-
- updateState: ->
- @updateContentDimensions()
- @updateScrollbarDimensions()
- @updateStartRow()
- @updateEndRow()
-
- @updateFocusedState()
- @updateHeightState()
- @updateVerticalScrollState()
- @updateHorizontalScrollState()
- @updateScrollbarsState()
- @updateHiddenInputState()
- @updateContentState()
- @updateDecorations()
- @updateLinesState()
- @updateCursorsState()
- @updateOverlaysState()
- @updateLineNumberGutterState()
- @updateLineNumbersState()
- @updateCommonGutterState()
- @updateGutterOrderState()
- @updateCustomGutterDecorationState()
-
- @resetTrackedUpdates()
-
- updateFocusedState: ->
- @state.focused = @focused
-
- updateHeightState: ->
- if @autoHeight
- @state.height = @contentHeight
- else
- @state.height = null
-
- updateVerticalScrollState: ->
- @state.content.scrollHeight = @scrollHeight
- @sharedGutterStyles.scrollHeight = @scrollHeight
- @state.verticalScrollbar.scrollHeight = @scrollHeight
-
- @state.content.scrollTop = @scrollTop
- @sharedGutterStyles.scrollTop = @scrollTop
- @state.verticalScrollbar.scrollTop = @scrollTop
-
- updateHorizontalScrollState: ->
- @state.content.scrollWidth = @scrollWidth
- @state.horizontalScrollbar.scrollWidth = @scrollWidth
-
- @state.content.scrollLeft = @scrollLeft
- @state.horizontalScrollbar.scrollLeft = @scrollLeft
-
- updateScrollbarsState: ->
- @state.horizontalScrollbar.visible = @horizontalScrollbarHeight > 0
- @state.horizontalScrollbar.height = @measuredHorizontalScrollbarHeight
- @state.horizontalScrollbar.right = @verticalScrollbarWidth
-
- @state.verticalScrollbar.visible = @verticalScrollbarWidth > 0
- @state.verticalScrollbar.width = @measuredVerticalScrollbarWidth
- @state.verticalScrollbar.bottom = @horizontalScrollbarHeight
-
- updateHiddenInputState: ->
- return unless lastCursor = @model.getLastCursor()
-
- {top, left, height, width} = @pixelRectForScreenRange(lastCursor.getScreenRange())
-
- if @focused
- top -= @scrollTop
- left -= @scrollLeft
- @state.hiddenInput.top = Math.max(Math.min(top, @clientHeight - height), 0)
- @state.hiddenInput.left = Math.max(Math.min(left, @clientWidth - width), 0)
- else
- @state.hiddenInput.top = 0
- @state.hiddenInput.left = 0
-
- @state.hiddenInput.height = height
- @state.hiddenInput.width = Math.max(width, 2)
-
- updateContentState: ->
- @state.content.scrollWidth = @scrollWidth
- @state.content.scrollLeft = @scrollLeft
- @state.content.indentGuidesVisible = not @model.isMini() and @showIndentGuide
- @state.content.backgroundColor = if @model.isMini() then null else @backgroundColor
- @state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null
-
- updateLinesState: ->
- return unless @startRow? and @endRow? and @lineHeight?
-
- visibleLineIds = {}
- row = @startRow
- while row < @endRow
- line = @model.tokenizedLineForScreenRow(row)
- unless line?
- throw new Error("No line exists for row #{row}. Last screen row: #{@model.getLastScreenRow()}")
-
- visibleLineIds[line.id] = true
- if @state.content.lines.hasOwnProperty(line.id)
- @updateLineState(row, line)
- else
- @buildLineState(row, line)
- row++
-
- if @mouseWheelScreenRow?
- if preservedLine = @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)
- visibleLineIds[preservedLine.id] = true
-
- for id, line of @state.content.lines
- unless visibleLineIds.hasOwnProperty(id)
- delete @state.content.lines[id]
- return
-
- updateLineState: (row, line) ->
- lineState = @state.content.lines[line.id]
- lineState.screenRow = row
- lineState.top = row * @lineHeight
- lineState.decorationClasses = @lineDecorationClassesForRow(row)
-
- buildLineState: (row, line) ->
- @state.content.lines[line.id] =
- screenRow: row
- text: line.text
- openScopes: line.openScopes
- tags: line.tags
- specialTokens: line.specialTokens
- firstNonWhitespaceIndex: line.firstNonWhitespaceIndex
- firstTrailingWhitespaceIndex: line.firstTrailingWhitespaceIndex
- invisibles: line.invisibles
- endOfLineInvisibles: line.endOfLineInvisibles
- isOnlyWhitespace: line.isOnlyWhitespace()
- indentLevel: line.indentLevel
- tabLength: line.tabLength
- fold: line.fold
- top: row * @lineHeight
- decorationClasses: @lineDecorationClassesForRow(row)
-
- updateCursorsState: ->
- @state.content.cursors = {}
- @updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation
- return
-
- updateCursorState: (cursor, destroyOnly = false) ->
- delete @state.content.cursors[cursor.id]
-
- return if destroyOnly
- return unless @startRow? and @endRow? and @hasPixelRectRequirements() and @baseCharacterWidth?
- return unless cursor.isVisible() and @startRow <= cursor.getScreenRow() < @endRow
-
- pixelRect = @pixelRectForScreenRange(cursor.getScreenRange())
- pixelRect.width = @baseCharacterWidth if pixelRect.width is 0
- @state.content.cursors[cursor.id] = pixelRect
-
- @emitDidUpdateState()
-
- updateOverlaysState: ->
- return unless @hasOverlayPositionRequirements()
-
- visibleDecorationIds = {}
-
- for decoration in @model.getOverlayDecorations()
- continue unless decoration.getMarker().isValid()
-
- {item, position} = decoration.getProperties()
- if position is 'tail'
- screenPosition = decoration.getMarker().getTailScreenPosition()
- else
- screenPosition = decoration.getMarker().getHeadScreenPosition()
-
- pixelPosition = @pixelPositionForScreenPosition(screenPosition)
-
- {scrollTop, scrollLeft} = @state.content
-
- top = pixelPosition.top + @lineHeight - scrollTop
- left = pixelPosition.left + @gutterWidth - scrollLeft
-
- if overlayDimensions = @overlayDimensions[decoration.id]
- {itemWidth, itemHeight, contentMargin} = overlayDimensions
-
- rightDiff = left + @boundingClientRect.left + itemWidth + contentMargin - @windowWidth
- left -= rightDiff if rightDiff > 0
-
- leftDiff = left + @boundingClientRect.left + contentMargin
- left -= leftDiff if leftDiff < 0
-
- if top + @boundingClientRect.top + itemHeight > @windowHeight and top - (itemHeight + @lineHeight) >= 0
- top -= itemHeight + @lineHeight
-
- pixelPosition.top = top
- pixelPosition.left = left
-
- @state.content.overlays[decoration.id] ?= {item}
- @state.content.overlays[decoration.id].pixelPosition = pixelPosition
- visibleDecorationIds[decoration.id] = true
-
- for id of @state.content.overlays
- delete @state.content.overlays[id] unless visibleDecorationIds[id]
-
- for id of @overlayDimensions
- delete @overlayDimensions[id] unless visibleDecorationIds[id]
-
- return
-
- updateLineNumberGutterState: ->
- @lineNumberGutter.maxLineNumberDigits = @model.getLineCount().toString().length
-
- updateCommonGutterState: ->
- @sharedGutterStyles.backgroundColor = if @gutterBackgroundColor isnt "rgba(0, 0, 0, 0)"
- @gutterBackgroundColor
- else
- @backgroundColor
-
- didAddGutter: (gutter) ->
- gutterDisposables = new CompositeDisposable
- gutterDisposables.add gutter.onDidChangeVisible =>
- @shouldUpdateGutterOrderState = true
- @shouldUpdateCustomGutterDecorationState = true
-
- @emitDidUpdateState()
- gutterDisposables.add gutter.onDidDestroy =>
- @disposables.remove(gutterDisposables)
- gutterDisposables.dispose()
- @shouldUpdateGutterOrderState = true
-
- @emitDidUpdateState()
- # It is not necessary to @updateCustomGutterDecorationState here.
- # The destroyed gutter will be removed from the list of gutters in @state,
- # and thus will be removed from the DOM.
- @disposables.add(gutterDisposables)
- @shouldUpdateGutterOrderState = true
- @shouldUpdateCustomGutterDecorationState = true
-
- @emitDidUpdateState()
-
- updateGutterOrderState: ->
- @state.gutters = []
- if @model.isMini()
- return
- for gutter in @model.getGutters()
- isVisible = @gutterIsVisible(gutter)
- if gutter.name is 'line-number'
- content = @lineNumberGutter
- else
- @customGutterDecorations[gutter.name] ?= {}
- content = @customGutterDecorations[gutter.name]
- @state.gutters.push({
- gutter,
- visible: isVisible,
- styles: @sharedGutterStyles,
- content,
- })
-
- # Updates the decoration state for the gutter with the given gutterName.
- # @customGutterDecorations is an {Object}, with the form:
- # * gutterName : {
- # decoration.id : {
- # top: # of pixels from top
- # height: # of pixels height of this decoration
- # item (optional): HTMLElement or space-pen View
- # class (optional): {String} class
- # }
- # }
- updateCustomGutterDecorationState: ->
- return unless @startRow? and @endRow? and @lineHeight?
-
- if @model.isMini()
- # Mini editors have no gutter decorations.
- # We clear instead of reassigning to preserve the reference.
- @clearAllCustomGutterDecorations()
-
- for gutter in @model.getGutters()
- gutterName = gutter.name
- gutterDecorations = @customGutterDecorations[gutterName]
- if gutterDecorations
- # Clear the gutter decorations; they are rebuilt.
- # We clear instead of reassigning to preserve the reference.
- @clearDecorationsForCustomGutterName(gutterName)
- else
- @customGutterDecorations[gutterName] = {}
- return if not @gutterIsVisible(gutter)
-
- relevantDecorations = @customGutterDecorationsInRange(gutterName, @startRow, @endRow - 1)
- relevantDecorations.forEach (decoration) =>
- decorationRange = decoration.getMarker().getScreenRange()
- @customGutterDecorations[gutterName][decoration.id] =
- top: @lineHeight * decorationRange.start.row
- height: @lineHeight * decorationRange.getRowCount()
- item: decoration.getProperties().item
- class: decoration.getProperties().class
-
- clearAllCustomGutterDecorations: ->
- allGutterNames = Object.keys(@customGutterDecorations)
- for gutterName in allGutterNames
- @clearDecorationsForCustomGutterName(gutterName)
-
- clearDecorationsForCustomGutterName: (gutterName) ->
- gutterDecorations = @customGutterDecorations[gutterName]
- if gutterDecorations
- allDecorationIds = Object.keys(gutterDecorations)
- for decorationId in allDecorationIds
- delete gutterDecorations[decorationId]
-
- gutterIsVisible: (gutterModel) ->
- isVisible = gutterModel.isVisible()
- if gutterModel.name is 'line-number'
- isVisible = isVisible and @showLineNumbers
- isVisible
-
- updateLineNumbersState: ->
- return unless @startRow? and @endRow? and @lineHeight?
-
- visibleLineNumberIds = {}
-
- if @startRow > 0
- rowBeforeStartRow = @startRow - 1
- lastBufferRow = @model.bufferRowForScreenRow(rowBeforeStartRow)
- wrapCount = rowBeforeStartRow - @model.screenRowForBufferRow(lastBufferRow)
- else
- lastBufferRow = null
- wrapCount = 0
-
- if @endRow > @startRow
- for bufferRow, i in @model.bufferRowsForScreenRows(@startRow, @endRow - 1)
- if bufferRow is lastBufferRow
- wrapCount++
- id = bufferRow + '-' + wrapCount
- softWrapped = true
- else
- id = bufferRow
- wrapCount = 0
- lastBufferRow = bufferRow
- softWrapped = false
-
- screenRow = @startRow + i
- top = screenRow * @lineHeight
- decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
- foldable = @model.isFoldableAtScreenRow(screenRow)
-
- @lineNumberGutter.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable}
- visibleLineNumberIds[id] = true
-
- if @mouseWheelScreenRow?
- bufferRow = @model.bufferRowForScreenRow(@mouseWheelScreenRow)
- wrapCount = @mouseWheelScreenRow - @model.screenRowForBufferRow(bufferRow)
- id = bufferRow
- id += '-' + wrapCount if wrapCount > 0
- visibleLineNumberIds[id] = true
-
- for id of @lineNumberGutter.lineNumbers
- delete @lineNumberGutter.lineNumbers[id] unless visibleLineNumberIds[id]
-
- return
-
- updateStartRow: ->
- return unless @scrollTop? and @lineHeight?
-
- startRow = Math.floor(@scrollTop / @lineHeight) - @lineOverdrawMargin
- @startRow = Math.max(0, startRow)
-
- updateEndRow: ->
- return unless @scrollTop? and @lineHeight? and @height?
-
- startRow = Math.max(0, Math.floor(@scrollTop / @lineHeight))
- visibleLinesCount = Math.ceil(@height / @lineHeight) + 1
- endRow = startRow + visibleLinesCount + @lineOverdrawMargin
- @endRow = Math.min(@model.getScreenLineCount(), endRow)
-
- updateScrollWidth: ->
- return unless @contentWidth? and @clientWidth?
-
- scrollWidth = Math.max(@contentWidth, @clientWidth)
- unless @scrollWidth is scrollWidth
- @scrollWidth = scrollWidth
- @updateScrollLeft()
-
- updateScrollHeight: ->
- return unless @contentHeight? and @clientHeight?
-
- contentHeight = @contentHeight
- if @scrollPastEnd
- extraScrollHeight = @clientHeight - (@lineHeight * 3)
- contentHeight += extraScrollHeight if extraScrollHeight > 0
- scrollHeight = Math.max(contentHeight, @height)
-
- unless @scrollHeight is scrollHeight
- @scrollHeight = scrollHeight
- @updateScrollTop()
-
- updateContentDimensions: ->
- if @lineHeight?
- oldContentHeight = @contentHeight
- @contentHeight = @lineHeight * @model.getScreenLineCount()
-
- if @baseCharacterWidth?
- oldContentWidth = @contentWidth
- clip = @model.tokenizedLineForScreenRow(@model.getLongestScreenRow())?.isSoftWrapped()
- @contentWidth = @pixelPositionForScreenPosition([@model.getLongestScreenRow(), @model.getMaxScreenLineLength()], clip).left
- @contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width
-
- if @contentHeight isnt oldContentHeight
- @updateHeight()
- @updateScrollbarDimensions()
- @updateScrollHeight()
-
- if @contentWidth isnt oldContentWidth
- @updateScrollbarDimensions()
- @updateScrollWidth()
-
- updateClientHeight: ->
- return unless @height? and @horizontalScrollbarHeight?
-
- clientHeight = @height - @horizontalScrollbarHeight
- unless @clientHeight is clientHeight
- @clientHeight = clientHeight
- @updateScrollHeight()
- @updateScrollTop()
-
- updateClientWidth: ->
- return unless @contentFrameWidth? and @verticalScrollbarWidth?
-
- clientWidth = @contentFrameWidth - @verticalScrollbarWidth
- unless @clientWidth is clientWidth
- @clientWidth = clientWidth
- @updateScrollWidth()
- @updateScrollLeft()
-
- updateScrollTop: ->
- scrollTop = @constrainScrollTop(@scrollTop)
- unless @scrollTop is scrollTop
- @scrollTop = scrollTop
- @updateStartRow()
- @updateEndRow()
-
- constrainScrollTop: (scrollTop) ->
- return scrollTop unless scrollTop? and @scrollHeight? and @clientHeight?
- Math.max(0, Math.min(scrollTop, @scrollHeight - @clientHeight))
-
- updateScrollLeft: ->
- @scrollLeft = @constrainScrollLeft(@scrollLeft)
-
- constrainScrollLeft: (scrollLeft) ->
- return scrollLeft unless scrollLeft? and @scrollWidth? and @clientWidth?
- Math.max(0, Math.min(scrollLeft, @scrollWidth - @clientWidth))
-
- updateScrollbarDimensions: ->
- return unless @contentFrameWidth? and @height?
- return unless @measuredVerticalScrollbarWidth? and @measuredHorizontalScrollbarHeight?
- return unless @contentWidth? and @contentHeight?
-
- clientWidthWithoutVerticalScrollbar = @contentFrameWidth
- clientWidthWithVerticalScrollbar = clientWidthWithoutVerticalScrollbar - @measuredVerticalScrollbarWidth
- clientHeightWithoutHorizontalScrollbar = @height
- clientHeightWithHorizontalScrollbar = clientHeightWithoutHorizontalScrollbar - @measuredHorizontalScrollbarHeight
-
- horizontalScrollbarVisible =
- not @model.isMini() and
- (@contentWidth > clientWidthWithoutVerticalScrollbar or
- @contentWidth > clientWidthWithVerticalScrollbar and @contentHeight > clientHeightWithoutHorizontalScrollbar)
-
- verticalScrollbarVisible =
- not @model.isMini() and
- (@contentHeight > clientHeightWithoutHorizontalScrollbar or
- @contentHeight > clientHeightWithHorizontalScrollbar and @contentWidth > clientWidthWithoutVerticalScrollbar)
-
- horizontalScrollbarHeight =
- if horizontalScrollbarVisible
- @measuredHorizontalScrollbarHeight
- else
- 0
-
- verticalScrollbarWidth =
- if verticalScrollbarVisible
- @measuredVerticalScrollbarWidth
- else
- 0
-
- unless @horizontalScrollbarHeight is horizontalScrollbarHeight
- @horizontalScrollbarHeight = horizontalScrollbarHeight
- @updateClientHeight()
-
- unless @verticalScrollbarWidth is verticalScrollbarWidth
- @verticalScrollbarWidth = verticalScrollbarWidth
- @updateClientWidth()
-
- lineDecorationClassesForRow: (row) ->
- return null if @model.isMini()
-
- decorationClasses = null
- for id, decoration of @lineDecorationsByScreenRow[row]
- decorationClasses ?= []
- decorationClasses.push(decoration.getProperties().class)
- decorationClasses
-
- lineNumberDecorationClassesForRow: (row) ->
- return null if @model.isMini()
-
- decorationClasses = null
- for id, decoration of @lineNumberDecorationsByScreenRow[row]
- decorationClasses ?= []
- decorationClasses.push(decoration.getProperties().class)
- decorationClasses
-
- # Returns a {Set} of {Decoration}s on the given custom gutter from startRow to endRow (inclusive).
- customGutterDecorationsInRange: (gutterName, startRow, endRow) ->
- decorations = new Set
-
- return decorations if @model.isMini() or gutterName is 'line-number' or
- not @customGutterDecorationsByGutterNameAndScreenRow[gutterName]
-
- for screenRow in [@startRow..@endRow - 1]
- for id, decoration of @customGutterDecorationsByGutterNameAndScreenRow[gutterName][screenRow]
- decorations.add(decoration)
- decorations
-
- getCursorBlinkPeriod: -> @cursorBlinkPeriod
-
- getCursorBlinkResumeDelay: -> @cursorBlinkResumeDelay
-
- setFocused: (focused) ->
- unless @focused is focused
- @focused = focused
- if @focused
- @startBlinkingCursors()
- else
- @stopBlinkingCursors(false)
- @shouldUpdateFocusedState = true
- @shouldUpdateHiddenInputState = true
-
- @emitDidUpdateState()
-
- setScrollTop: (scrollTop) ->
- scrollTop = @constrainScrollTop(scrollTop)
-
- unless @scrollTop is scrollTop or Number.isNaN(scrollTop)
- @scrollTop = scrollTop
- @model.setScrollTop(scrollTop)
- @updateStartRow()
- @updateEndRow()
- @didStartScrolling()
- @shouldUpdateVerticalScrollState = true
- @shouldUpdateHiddenInputState = true
- @shouldUpdateDecorations = true
- @shouldUpdateLinesState = true
- @shouldUpdateCursorsState = true
- @shouldUpdateLineNumbersState = true
- @shouldUpdateCustomGutterDecorationState = true
- @shouldUpdateOverlaysState = true
-
- @emitDidUpdateState()
-
- getScrollTop: ->
- @scrollTop
-
- didStartScrolling: ->
- if @stoppedScrollingTimeoutId?
- clearTimeout(@stoppedScrollingTimeoutId)
- @stoppedScrollingTimeoutId = null
- @stoppedScrollingTimeoutId = setTimeout(@didStopScrolling.bind(this), @stoppedScrollingDelay)
- @state.content.scrollingVertically = true
- @emitDidUpdateState()
-
- didStopScrolling: ->
- @state.content.scrollingVertically = false
- if @mouseWheelScreenRow?
- @mouseWheelScreenRow = null
- @shouldUpdateLinesState = true
- @shouldUpdateLineNumbersState = true
- @shouldUpdateCustomGutterDecorationState = true
-
- @emitDidUpdateState()
-
- setScrollLeft: (scrollLeft) ->
- scrollLeft = @constrainScrollLeft(scrollLeft)
- unless @scrollLeft is scrollLeft or Number.isNaN(scrollLeft)
- oldScrollLeft = @scrollLeft
- @scrollLeft = scrollLeft
- @model.setScrollLeft(scrollLeft)
- @shouldUpdateHorizontalScrollState = true
- @shouldUpdateHiddenInputState = true
- @shouldUpdateCursorsState = true unless oldScrollLeft?
- @shouldUpdateOverlaysState = true
-
- @emitDidUpdateState()
-
- getScrollLeft: ->
- @scrollLeft
-
- setHorizontalScrollbarHeight: (horizontalScrollbarHeight) ->
- unless @measuredHorizontalScrollbarHeight is horizontalScrollbarHeight
- oldHorizontalScrollbarHeight = @measuredHorizontalScrollbarHeight
- @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight
- @model.setHorizontalScrollbarHeight(horizontalScrollbarHeight)
- @updateScrollbarDimensions()
- @shouldUpdateScrollbarsState = true
- @shouldUpdateVerticalScrollState = true
- @shouldUpdateHorizontalScrollState = true
- @shouldUpdateCursorsState = true unless oldHorizontalScrollbarHeight?
-
- @emitDidUpdateState()
-
- setVerticalScrollbarWidth: (verticalScrollbarWidth) ->
- unless @measuredVerticalScrollbarWidth is verticalScrollbarWidth
- oldVerticalScrollbarWidth = @measuredVerticalScrollbarWidth
- @measuredVerticalScrollbarWidth = verticalScrollbarWidth
- @model.setVerticalScrollbarWidth(verticalScrollbarWidth)
- @updateScrollbarDimensions()
- @shouldUpdateScrollbarsState = true
- @shouldUpdateVerticalScrollState = true
- @shouldUpdateHorizontalScrollState = true
- @shouldUpdateCursorsState = true unless oldVerticalScrollbarWidth?
-
- @emitDidUpdateState()
-
- setAutoHeight: (autoHeight) ->
- unless @autoHeight is autoHeight
- @autoHeight = autoHeight
- @shouldUpdateHeightState = true
-
- @emitDidUpdateState()
-
- setExplicitHeight: (explicitHeight) ->
- unless @explicitHeight is explicitHeight
- @explicitHeight = explicitHeight
- @model.setHeight(explicitHeight)
- @updateHeight()
- @shouldUpdateVerticalScrollState = true
- @shouldUpdateScrollbarsState = true
- @shouldUpdateDecorations = true
- @shouldUpdateLinesState = true
- @shouldUpdateCursorsState = true
- @shouldUpdateLineNumbersState = true
- @shouldUpdateCustomGutterDecorationState = true
-
- @emitDidUpdateState()
-
- updateHeight: ->
- height = @explicitHeight ? @contentHeight
- unless @height is height
- @height = height
- @updateScrollbarDimensions()
- @updateClientHeight()
- @updateScrollHeight()
- @updateEndRow()
-
- setContentFrameWidth: (contentFrameWidth) ->
- unless @contentFrameWidth is contentFrameWidth
- oldContentFrameWidth = @contentFrameWidth
- @contentFrameWidth = contentFrameWidth
- @model.setWidth(contentFrameWidth)
- @updateScrollbarDimensions()
- @updateClientWidth()
- @shouldUpdateVerticalScrollState = true
- @shouldUpdateHorizontalScrollState = true
- @shouldUpdateScrollbarsState = true
- @shouldUpdateContentState = true
- @shouldUpdateDecorations = true
- @shouldUpdateLinesState = true
- @shouldUpdateCursorsState = true unless oldContentFrameWidth?
-
- @emitDidUpdateState()
-
- setBoundingClientRect: (boundingClientRect) ->
- unless @clientRectsEqual(@boundingClientRect, boundingClientRect)
- @boundingClientRect = boundingClientRect
- @shouldUpdateOverlaysState = true
-
- @emitDidUpdateState()
-
- clientRectsEqual: (clientRectA, clientRectB) ->
- clientRectA? and clientRectB? and
- clientRectA.top is clientRectB.top and
- clientRectA.left is clientRectB.left and
- clientRectA.width is clientRectB.width and
- clientRectA.height is clientRectB.height
-
- setWindowSize: (width, height) ->
- if @windowWidth isnt width or @windowHeight isnt height
- @windowWidth = width
- @windowHeight = height
- @shouldUpdateOverlaysState = true
-
- @emitDidUpdateState()
-
- setBackgroundColor: (backgroundColor) ->
- unless @backgroundColor is backgroundColor
- @backgroundColor = backgroundColor
- @shouldUpdateContentState = true
- @shouldUpdateLineNumberGutterState = true
- @updateCommonGutterState()
- @shouldUpdateGutterOrderState = true
-
- @emitDidUpdateState()
-
- setGutterBackgroundColor: (gutterBackgroundColor) ->
- unless @gutterBackgroundColor is gutterBackgroundColor
- @gutterBackgroundColor = gutterBackgroundColor
- @shouldUpdateLineNumberGutterState = true
- @updateCommonGutterState()
- @shouldUpdateGutterOrderState = true
-
- @emitDidUpdateState()
-
- setGutterWidth: (gutterWidth) ->
- if @gutterWidth isnt gutterWidth
- @gutterWidth = gutterWidth
- @updateOverlaysState()
-
- setLineHeight: (lineHeight) ->
- unless @lineHeight is lineHeight
- @lineHeight = lineHeight
- @model.setLineHeightInPixels(lineHeight)
- @updateContentDimensions()
- @updateScrollHeight()
- @updateHeight()
- @updateStartRow()
- @updateEndRow()
- @shouldUpdateHeightState = true
- @shouldUpdateHorizontalScrollState = true
- @shouldUpdateVerticalScrollState = true
- @shouldUpdateScrollbarsState = true
- @shouldUpdateHiddenInputState = true
- @shouldUpdateDecorations = true
- @shouldUpdateLinesState = true
- @shouldUpdateCursorsState = true
- @shouldUpdateLineNumbersState = true
- @shouldUpdateCustomGutterDecorationState = true
- @shouldUpdateOverlaysState = true
-
- @emitDidUpdateState()
-
- setMouseWheelScreenRow: (mouseWheelScreenRow) ->
- unless @mouseWheelScreenRow is mouseWheelScreenRow
- @mouseWheelScreenRow = mouseWheelScreenRow
- @didStartScrolling()
-
- setBaseCharacterWidth: (baseCharacterWidth) ->
- unless @baseCharacterWidth is baseCharacterWidth
- @baseCharacterWidth = baseCharacterWidth
- @model.setDefaultCharWidth(baseCharacterWidth)
- @characterWidthsChanged()
-
- getScopedCharacterWidth: (scopeNames, char) ->
- @getScopedCharacterWidths(scopeNames)[char]
-
- getScopedCharacterWidths: (scopeNames) ->
- scope = @characterWidthsByScope
- for scopeName in scopeNames
- scope[scopeName] ?= {}
- scope = scope[scopeName]
- scope.characterWidths ?= {}
- scope.characterWidths
-
- batchCharacterMeasurement: (fn) ->
- oldChangeCount = @scopedCharacterWidthsChangeCount
- @batchingCharacterMeasurement = true
- @model.batchCharacterMeasurement(fn)
- @batchingCharacterMeasurement = false
- @characterWidthsChanged() if oldChangeCount isnt @scopedCharacterWidthsChangeCount
-
- setScopedCharacterWidth: (scopeNames, character, width) ->
- @getScopedCharacterWidths(scopeNames)[character] = width
- @model.setScopedCharWidth(scopeNames, character, width)
- @scopedCharacterWidthsChangeCount++
- @characterWidthsChanged() unless @batchingCharacterMeasurement
-
- characterWidthsChanged: ->
- @updateContentDimensions()
-
- @shouldUpdateHorizontalScrollState = true
- @shouldUpdateVerticalScrollState = true
- @shouldUpdateScrollbarsState = true
- @shouldUpdateHiddenInputState = true
- @shouldUpdateContentState = true
- @shouldUpdateDecorations = true
- @shouldUpdateLinesState = true
- @shouldUpdateCursorsState = true
- @shouldUpdateOverlaysState = true
-
- @emitDidUpdateState()
-
- clearScopedCharacterWidths: ->
- @characterWidthsByScope = {}
- @model.clearScopedCharWidths()
-
- hasPixelPositionRequirements: ->
- @lineHeight? and @baseCharacterWidth?
-
- pixelPositionForScreenPosition: (screenPosition, clip=true) ->
- screenPosition = Point.fromObject(screenPosition)
- screenPosition = @model.clipScreenPosition(screenPosition) if clip
-
- targetRow = screenPosition.row
- targetColumn = screenPosition.column
- baseCharacterWidth = @baseCharacterWidth
-
- top = targetRow * @lineHeight
- left = 0
- column = 0
-
- iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator()
- while iterator.next()
- characterWidths = @getScopedCharacterWidths(iterator.getScopes())
-
- valueIndex = 0
- text = iterator.getText()
- while valueIndex < text.length
- if iterator.isPairedCharacter()
- char = text
- charLength = 2
- valueIndex += 2
- else
- char = text[valueIndex]
- charLength = 1
- valueIndex++
-
- return {top, left} if column is targetColumn
-
- left += characterWidths[char] ? baseCharacterWidth unless char is '\0'
- column += charLength
- {top, left}
-
- hasPixelRectRequirements: ->
- @hasPixelPositionRequirements() and @scrollWidth?
-
- hasOverlayPositionRequirements: ->
- @hasPixelRectRequirements() and @boundingClientRect? and @windowWidth and @windowHeight
-
- pixelRectForScreenRange: (screenRange) ->
- if screenRange.end.row > screenRange.start.row
- top = @pixelPositionForScreenPosition(screenRange.start).top
- left = 0
- height = (screenRange.end.row - screenRange.start.row + 1) * @lineHeight
- width = @scrollWidth
- else
- {top, left} = @pixelPositionForScreenPosition(screenRange.start, false)
- height = @lineHeight
- width = @pixelPositionForScreenPosition(screenRange.end, false).left - left
-
- {top, left, width, height}
-
- observeDecoration: (decoration) ->
- decorationDisposables = new CompositeDisposable
- decorationDisposables.add decoration.getMarker().onDidChange(@decorationMarkerDidChange.bind(this, decoration))
- if decoration.isType('highlight')
- decorationDisposables.add decoration.onDidFlash(@highlightDidFlash.bind(this, decoration))
- decorationDisposables.add decoration.onDidChangeProperties(@decorationPropertiesDidChange.bind(this, decoration))
- decorationDisposables.add decoration.onDidDestroy =>
- @disposables.remove(decorationDisposables)
- decorationDisposables.dispose()
- @didDestroyDecoration(decoration)
- @disposables.add(decorationDisposables)
-
- decorationMarkerDidChange: (decoration, change) ->
- if decoration.isType('line') or decoration.isType('gutter')
- return if change.textChanged
-
- intersectsVisibleRowRange = false
- oldRange = new Range(change.oldTailScreenPosition, change.oldHeadScreenPosition)
- newRange = new Range(change.newTailScreenPosition, change.newHeadScreenPosition)
-
- if oldRange.intersectsRowRange(@startRow, @endRow - 1)
- @removeFromLineDecorationCaches(decoration, oldRange)
- intersectsVisibleRowRange = true
-
- if newRange.intersectsRowRange(@startRow, @endRow - 1)
- @addToLineDecorationCaches(decoration, newRange)
- intersectsVisibleRowRange = true
-
- if intersectsVisibleRowRange
- @shouldUpdateLinesState = true if decoration.isType('line')
- if decoration.isType('line-number')
- @shouldUpdateLineNumbersState = true
- else if decoration.isType('gutter')
- @shouldUpdateCustomGutterDecorationState = true
-
- if decoration.isType('highlight')
- return if change.textChanged
-
- @updateHighlightState(decoration)
-
- if decoration.isType('overlay')
- @shouldUpdateOverlaysState = true
-
- @emitDidUpdateState()
-
- decorationPropertiesDidChange: (decoration, event) ->
- {oldProperties} = event
- if decoration.isType('line') or decoration.isType('gutter')
- @removePropertiesFromLineDecorationCaches(
- decoration.id,
- oldProperties,
- decoration.getMarker().getScreenRange())
- @addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange())
- if decoration.isType('line') or Decoration.isType(oldProperties, 'line')
- @shouldUpdateLinesState = true
- if decoration.isType('line-number') or Decoration.isType(oldProperties, 'line-number')
- @shouldUpdateLineNumbersState = true
- if (decoration.isType('gutter') and not decoration.isType('line-number')) or
- (Decoration.isType(oldProperties, 'gutter') and not Decoration.isType(oldProperties, 'line-number'))
- @shouldUpdateCustomGutterDecorationState = true
- else if decoration.isType('overlay')
- @shouldUpdateOverlaysState = true
- else if decoration.isType('highlight')
- @updateHighlightState(decoration, event)
-
- @emitDidUpdateState()
-
- didDestroyDecoration: (decoration) ->
- if decoration.isType('line') or decoration.isType('gutter')
- @removeFromLineDecorationCaches(decoration, decoration.getMarker().getScreenRange())
- @shouldUpdateLinesState = true if decoration.isType('line')
- if decoration.isType('line-number')
- @shouldUpdateLineNumbersState = true
- else if decoration.isType('gutter')
- @shouldUpdateCustomGutterDecorationState = true
- if decoration.isType('highlight')
- @updateHighlightState(decoration)
- if decoration.isType('overlay')
- @shouldUpdateOverlaysState = true
-
- @emitDidUpdateState()
-
- highlightDidFlash: (decoration) ->
- flash = decoration.consumeNextFlash()
- if decorationState = @state.content.highlights[decoration.id]
- decorationState.flashCount++
- decorationState.flashClass = flash.class
- decorationState.flashDuration = flash.duration
- @emitDidUpdateState()
-
- didAddDecoration: (decoration) ->
- @observeDecoration(decoration)
-
- if decoration.isType('line') or decoration.isType('gutter')
- @addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange())
- @shouldUpdateLinesState = true if decoration.isType('line')
- if decoration.isType('line-number')
- @shouldUpdateLineNumbersState = true
- else if decoration.isType('gutter')
- @shouldUpdateCustomGutterDecorationState = true
- else if decoration.isType('highlight')
- @updateHighlightState(decoration)
- else if decoration.isType('overlay')
- @shouldUpdateOverlaysState = true
-
- @emitDidUpdateState()
-
- updateDecorations: ->
- @lineDecorationsByScreenRow = {}
- @lineNumberDecorationsByScreenRow = {}
- @customGutterDecorationsByGutterNameAndScreenRow = {}
- @highlightDecorationsById = {}
-
- visibleHighlights = {}
- return unless 0 <= @startRow <= @endRow <= Infinity
-
- for markerId, decorations of @model.decorationsForScreenRowRange(@startRow, @endRow - 1)
- range = @model.getMarker(markerId).getScreenRange()
- for decoration in decorations
- if decoration.isType('line') or decoration.isType('gutter')
- @addToLineDecorationCaches(decoration, range)
- else if decoration.isType('highlight')
- visibleHighlights[decoration.id] = @updateHighlightState(decoration)
-
- for id of @state.content.highlights
- unless visibleHighlights[id]
- delete @state.content.highlights[id]
-
- return
-
- removeFromLineDecorationCaches: (decoration, range) ->
- @removePropertiesFromLineDecorationCaches(decoration.id, decoration.getProperties(), range)
-
- removePropertiesFromLineDecorationCaches: (decorationId, decorationProperties, range) ->
- gutterName = decorationProperties.gutterName
- for row in [range.start.row..range.end.row] by 1
- delete @lineDecorationsByScreenRow[row]?[decorationId]
- delete @lineNumberDecorationsByScreenRow[row]?[decorationId]
- delete @customGutterDecorationsByGutterNameAndScreenRow[gutterName]?[row]?[decorationId] if gutterName
- return
-
- addToLineDecorationCaches: (decoration, range) ->
- marker = decoration.getMarker()
- properties = decoration.getProperties()
-
- return unless marker.isValid()
-
- if range.isEmpty()
- return if properties.onlyNonEmpty
- else
- return if properties.onlyEmpty
- omitLastRow = range.end.column is 0
-
- for row in [range.start.row..range.end.row] by 1
- continue if properties.onlyHead and row isnt marker.getHeadScreenPosition().row
- continue if omitLastRow and row is range.end.row
-
- if decoration.isType('line')
- @lineDecorationsByScreenRow[row] ?= {}
- @lineDecorationsByScreenRow[row][decoration.id] = decoration
-
- if decoration.isType('line-number')
- @lineNumberDecorationsByScreenRow[row] ?= {}
- @lineNumberDecorationsByScreenRow[row][decoration.id] = decoration
- else if decoration.isType('gutter')
- gutterName = decoration.getProperties().gutterName
- @customGutterDecorationsByGutterNameAndScreenRow[gutterName] ?= {}
- @customGutterDecorationsByGutterNameAndScreenRow[gutterName][row] ?= {}
- @customGutterDecorationsByGutterNameAndScreenRow[gutterName][row][decoration.id] = decoration
-
- return
-
- updateHighlightState: (decoration) ->
- return unless @startRow? and @endRow? and @lineHeight? and @hasPixelPositionRequirements()
-
- properties = decoration.getProperties()
- marker = decoration.getMarker()
- range = marker.getScreenRange()
-
- if decoration.isDestroyed() or not marker.isValid() or range.isEmpty() or not range.intersectsRowRange(@startRow, @endRow - 1)
- delete @state.content.highlights[decoration.id]
- @emitDidUpdateState()
- return
-
- 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
-
- if range.isEmpty()
- delete @state.content.highlights[decoration.id]
- @emitDidUpdateState()
- return
-
- highlightState = @state.content.highlights[decoration.id] ?= {
- flashCount: 0
- flashDuration: null
- flashClass: null
- }
- highlightState.class = properties.class
- highlightState.deprecatedRegionClass = properties.deprecatedRegionClass
- highlightState.regions = @buildHighlightRegions(range)
- @emitDidUpdateState()
-
- true
-
- buildHighlightRegions: (screenRange) ->
- lineHeightInPixels = @lineHeight
- startPixelPosition = @pixelPositionForScreenPosition(screenRange.start, true)
- endPixelPosition = @pixelPositionForScreenPosition(screenRange.end, true)
- spannedRows = screenRange.end.row - screenRange.start.row + 1
-
- if spannedRows is 1
- [
- top: startPixelPosition.top
- height: lineHeightInPixels
- left: startPixelPosition.left
- width: endPixelPosition.left - startPixelPosition.left
- ]
- else
- regions = []
-
- # First row, extending from selection start to the right side of screen
- regions.push(
- top: startPixelPosition.top
- left: startPixelPosition.left
- height: lineHeightInPixels
- right: 0
- )
-
- # Middle rows, extending from left side to right side of screen
- if spannedRows > 2
- regions.push(
- top: startPixelPosition.top + lineHeightInPixels
- height: endPixelPosition.top - startPixelPosition.top - lineHeightInPixels
- left: 0
- right: 0
- )
-
- # Last row, extending from left side of screen to selection end
- if screenRange.end.column > 0
- regions.push(
- top: endPixelPosition.top
- height: lineHeightInPixels
- left: 0
- width: endPixelPosition.left
- )
-
- regions
-
- setOverlayDimensions: (decorationId, itemWidth, itemHeight, contentMargin) ->
- @overlayDimensions[decorationId] ?= {}
- overlayState = @overlayDimensions[decorationId]
- dimensionsAreEqual = overlayState.itemWidth is itemWidth and
- overlayState.itemHeight is itemHeight and
- overlayState.contentMargin is contentMargin
- unless dimensionsAreEqual
- overlayState.itemWidth = itemWidth
- overlayState.itemHeight = itemHeight
- overlayState.contentMargin = contentMargin
- @shouldUpdateOverlaysState = true
-
- @emitDidUpdateState()
-
- observeCursor: (cursor) ->
- didChangePositionDisposable = cursor.onDidChangePosition =>
- @shouldUpdateHiddenInputState = true if cursor.isLastCursor()
- @pauseCursorBlinking()
- @updateCursorState(cursor)
-
- @emitDidUpdateState()
-
- didChangeVisibilityDisposable = cursor.onDidChangeVisibility =>
- @updateCursorState(cursor)
-
- didDestroyDisposable = cursor.onDidDestroy =>
- @disposables.remove(didChangePositionDisposable)
- @disposables.remove(didChangeVisibilityDisposable)
- @disposables.remove(didDestroyDisposable)
- @shouldUpdateHiddenInputState = true
- @updateCursorState(cursor, true)
-
- @emitDidUpdateState()
-
- @disposables.add(didChangePositionDisposable)
- @disposables.add(didChangeVisibilityDisposable)
- @disposables.add(didDestroyDisposable)
-
- didAddCursor: (cursor) ->
- @observeCursor(cursor)
- @shouldUpdateHiddenInputState = true
- @pauseCursorBlinking()
- @updateCursorState(cursor)
-
- @emitDidUpdateState()
-
- startBlinkingCursors: ->
- unless @toggleCursorBlinkHandle
- @state.content.cursorsVisible = true
- @toggleCursorBlinkHandle = setInterval(@toggleCursorBlink.bind(this), @getCursorBlinkPeriod() / 2)
-
- stopBlinkingCursors: (visible) ->
- if @toggleCursorBlinkHandle
- @state.content.cursorsVisible = visible
- clearInterval(@toggleCursorBlinkHandle)
- @toggleCursorBlinkHandle = null
-
- toggleCursorBlink: ->
- @state.content.cursorsVisible = not @state.content.cursorsVisible
- @emitDidUpdateState()
-
- pauseCursorBlinking: ->
- @stopBlinkingCursors(true)
- @startBlinkingCursorsAfterDelay ?= _.debounce(@startBlinkingCursors, @getCursorBlinkResumeDelay())
- @startBlinkingCursorsAfterDelay()
- @emitDidUpdateState()
diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js
new file mode 100644
index 00000000000..b5264fb5d9c
--- /dev/null
+++ b/src/text-editor-registry.js
@@ -0,0 +1,348 @@
+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() {}
diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee
deleted file mode 100644
index b86367ef45f..00000000000
--- a/src/text-editor-view.coffee
+++ /dev/null
@@ -1,324 +0,0 @@
-{View, $} = require 'space-pen'
-{defaults} = require 'underscore-plus'
-TextBuffer = require 'text-buffer'
-TextEditor = require './text-editor'
-TextEditorElement = require './text-editor-element'
-TextEditorComponent = require './text-editor-component'
-{deprecate} = require 'grim'
-
-# Deprecated: Represents the entire visual pane in Atom.
-#
-# The TextEditorView manages the {TextEditor}, which manages the file buffers.
-# `TextEditorView` is intentionally sparse. Most of the things you'll want
-# to do are on {TextEditor}.
-#
-# ## Examples
-#
-# Requiring in packages
-#
-# ```coffee
-# {TextEditorView} = require 'atom'
-#
-# miniEditorView = new TextEditorView(mini: true)
-# ```
-#
-# Iterating over the open editor views
-#
-# ```coffee
-# for editorView in atom.workspaceView.getEditorViews()
-# console.log(editorView.getModel().getPath())
-# ```
-#
-# Subscribing to every current and future editor
-#
-# ```coffee
-# atom.workspace.eachEditorView (editorView) ->
-# console.log(editorView.getModel().getPath())
-# ```
-module.exports =
-class TextEditorView extends View
- # The constructor for setting up an `TextEditorView` instance.
- #
- # * `modelOrParams` Either an {TextEditor}, or an object with one property, `mini`.
- # If `mini` is `true`, a "miniature" `TextEditor` is constructed.
- # Typically, this is ideal for scenarios where you need an Atom editor,
- # but without all the chrome, like scrollbars, gutter, _e.t.c._.
- #
- constructor: (modelOrParams, props) ->
- # Handle direct construction with an editor or params
- unless modelOrParams instanceof HTMLElement
- if modelOrParams instanceof TextEditor
- model = modelOrParams
- else
- {editor, mini, placeholderText, attributes} = modelOrParams
- model = editor ? new TextEditor
- buffer: new TextBuffer
- softWrapped: false
- tabLength: 2
- softTabs: true
- mini: mini
- placeholderText: placeholderText
-
- element = new TextEditorElement
- element.lineOverdrawMargin = props?.lineOverdrawMargin
- element.setAttribute(name, value) for name, value of attributes if attributes?
- element.setModel(model)
- return element.__spacePenView
-
- # Handle construction with an element
- @element = modelOrParams
- super
-
- setModel: (@model) ->
- @editor = @model
-
- @root = $(@element.rootElement)
-
- @scrollView = @root.find('.scroll-view')
-
- if atom.config.get('editor.useShadowDOM')
- @underlayer = $("").appendTo(this)
- @overlayer = $("").appendTo(this)
- else
- @underlayer = @find('.highlights').addClass('underlayer')
- @overlayer = @find('.lines').addClass('overlayer')
-
- @hiddenInput = @root.find('.hidden-input')
-
- @hiddenInput.on = (args...) =>
- args[0] = 'blur' if args[0] is 'focusout'
- $::on.apply(this, args)
-
- @subscribe atom.config.observe 'editor.showLineNumbers', =>
- @gutter = @root.find('.gutter')
-
- @gutter.removeClassFromAllLines = (klass) =>
- deprecate('Use decorations instead: http://blog.atom.io/2014/07/24/decorations.html')
- @gutter.find('.line-number').removeClass(klass)
-
- @gutter.getLineNumberElement = (bufferRow) =>
- deprecate('Use decorations instead: http://blog.atom.io/2014/07/24/decorations.html')
- @gutter.find("[data-buffer-row='#{bufferRow}']")
-
- @gutter.addClassToLine = (bufferRow, klass) =>
- deprecate('Use decorations instead: http://blog.atom.io/2014/07/24/decorations.html')
- lines = @gutter.find("[data-buffer-row='#{bufferRow}']")
- lines.addClass(klass)
- lines.length > 0
-
- find: ->
- shadowResult = @root.find.apply(@root, arguments)
- if shadowResult.length > 0
- shadowResult
- else
- super
-
- # Public: Get the underlying editor model for this view.
- #
- # Returns an {TextEditor}
- getModel: -> @model
-
- getEditor: -> @model
-
- Object.defineProperty @::, 'lineHeight', get: -> @model.getLineHeightInPixels()
- Object.defineProperty @::, 'charWidth', get: -> @model.getDefaultCharWidth()
- Object.defineProperty @::, 'firstRenderedScreenRow', get: -> @component.getRenderedRowRange()[0]
- Object.defineProperty @::, 'lastRenderedScreenRow', get: -> @component.getRenderedRowRange()[1]
- Object.defineProperty @::, 'active', get: -> @is(@getPaneView()?.activeView)
- Object.defineProperty @::, 'isFocused', get: -> document.activeElement is @element or document.activeElement is @element.component?.hiddenInputComponent?.getDomNode()
- Object.defineProperty @::, 'mini', get: -> @model?.isMini()
- Object.defineProperty @::, 'component', get: -> @element?.component
-
- afterAttach: (onDom) ->
- return unless onDom
- return if @attached
- @attached = true
- @trigger 'editor:attached', [this]
-
- beforeRemove: ->
- @trigger 'editor:detached', [this]
- @trigger 'editor:will-be-removed', [this]
- @attached = false
-
- remove: (selector, keepData) ->
- @model.destroy() unless keepData
- super
-
- scrollTop: (scrollTop) ->
- if scrollTop?
- @model.setScrollTop(scrollTop)
- else
- @model.getScrollTop()
-
- scrollLeft: (scrollLeft) ->
- if scrollLeft?
- @model.setScrollLeft(scrollLeft)
- else
- @model.getScrollLeft()
-
- scrollToBottom: ->
- deprecate 'Use TextEditor::scrollToBottom instead. You can get the editor via editorView.getModel()'
- @model.setScrollBottom(Infinity)
-
- scrollToScreenPosition: (screenPosition, options) ->
- deprecate 'Use TextEditor::scrollToScreenPosition instead. You can get the editor via editorView.getModel()'
- @model.scrollToScreenPosition(screenPosition, options)
-
- scrollToBufferPosition: (bufferPosition, options) ->
- deprecate 'Use TextEditor::scrollToBufferPosition instead. You can get the editor via editorView.getModel()'
- @model.scrollToBufferPosition(bufferPosition, options)
-
- scrollToCursorPosition: ->
- deprecate 'Use TextEditor::scrollToCursorPosition instead. You can get the editor via editorView.getModel()'
- @model.scrollToCursorPosition()
-
- pixelPositionForBufferPosition: (bufferPosition) ->
- deprecate 'Use TextEditorElement::pixelPositionForBufferPosition instead. You can get the editor via editorView.getModel()'
- @model.pixelPositionForBufferPosition(bufferPosition, true)
-
- pixelPositionForScreenPosition: (screenPosition) ->
- deprecate 'Use TextEditorElement::pixelPositionForScreenPosition instead. You can get the editor via editorView.getModel()'
- @model.pixelPositionForScreenPosition(screenPosition, true)
-
- appendToLinesView: (view) ->
- view.css('position', 'absolute')
- view.css('z-index', 1)
- @overlayer.append(view)
-
- splitLeft: ->
- deprecate """
- Use Pane::splitLeft instead.
- To duplicate this editor into the split use:
- editorView.getPaneView().getModel().splitLeft(copyActiveItem: true)
- """
- pane = @getPaneView()
- pane?.splitLeft(pane?.copyActiveItem()).activeView
-
- splitRight: ->
- deprecate """
- Use Pane::splitRight instead.
- To duplicate this editor into the split use:
- editorView.getPaneView().getModel().splitRight(copyActiveItem: true)
- """
- pane = @getPaneView()
- pane?.splitRight(pane?.copyActiveItem()).activeView
-
- splitUp: ->
- deprecate """
- Use Pane::splitUp instead.
- To duplicate this editor into the split use:
- editorView.getPaneView().getModel().splitUp(copyActiveItem: true)
- """
- pane = @getPaneView()
- pane?.splitUp(pane?.copyActiveItem()).activeView
-
- splitDown: ->
- deprecate """
- Use Pane::splitDown instead.
- To duplicate this editor into the split use:
- editorView.getPaneView().getModel().splitDown(copyActiveItem: true)
- """
- pane = @getPaneView()
- pane?.splitDown(pane?.copyActiveItem()).activeView
-
- # Public: Get this {TextEditorView}'s {PaneView}.
- #
- # Returns a {PaneView}
- getPaneView: ->
- @parent('.item-views').parents('atom-pane').view()
- getPane: ->
- deprecate 'Use TextEditorView::getPaneView() instead'
- @getPaneView()
-
- show: ->
- super
- @component?.checkForVisibilityChange()
-
- hide: ->
- super
- @component?.checkForVisibilityChange()
-
- pageDown: ->
- deprecate('Use editorView.getModel().pageDown()')
- @model.pageDown()
-
- pageUp: ->
- deprecate('Use editorView.getModel().pageUp()')
- @model.pageUp()
-
- getFirstVisibleScreenRow: ->
- deprecate 'Use TextEditorElement::getFirstVisibleScreenRow instead.'
- @model.getFirstVisibleScreenRow(true)
-
- getLastVisibleScreenRow: ->
- deprecate 'Use TextEditor::getLastVisibleScreenRow instead. You can get the editor via editorView.getModel()'
- @model.getLastVisibleScreenRow()
-
- getFontFamily: ->
- deprecate 'This is going away. Use atom.config.get("editor.fontFamily") instead'
- @component?.getFontFamily()
-
- setFontFamily: (fontFamily) ->
- deprecate 'This is going away. Use atom.config.set("editor.fontFamily", "my-font") instead'
- @component?.setFontFamily(fontFamily)
-
- getFontSize: ->
- deprecate 'This is going away. Use atom.config.get("editor.fontSize") instead'
- @component?.getFontSize()
-
- setFontSize: (fontSize) ->
- deprecate 'This is going away. Use atom.config.set("editor.fontSize", 12) instead'
- @component?.setFontSize(fontSize)
-
- setLineHeight: (lineHeight) ->
- deprecate 'This is going away. Use atom.config.set("editor.lineHeight", 1.5) instead'
- @component.setLineHeight(lineHeight)
-
- setWidthInChars: (widthInChars) ->
- @component.getDOMNode().style.width = (@model.getDefaultCharWidth() * widthInChars) + 'px'
-
- setShowIndentGuide: (showIndentGuide) ->
- deprecate 'This is going away. Use atom.config.set("editor.showIndentGuide", true|false) instead'
- atom.config.set("editor.showIndentGuide", showIndentGuide)
-
- setSoftWrap: (softWrapped) ->
- deprecate 'Use TextEditor::setSoftWrapped instead. You can get the editor via editorView.getModel()'
- @model.setSoftWrapped(softWrapped)
-
- setShowInvisibles: (showInvisibles) ->
- deprecate 'This is going away. Use atom.config.set("editor.showInvisibles", true|false) instead'
- @component.setShowInvisibles(showInvisibles)
-
- getText: ->
- @model.getText()
-
- setText: (text) ->
- @model.setText(text)
-
- insertText: (text) ->
- @model.insertText(text)
-
- isInputEnabled: ->
- @component.isInputEnabled()
-
- setInputEnabled: (inputEnabled) ->
- @component.setInputEnabled(inputEnabled)
-
- requestDisplayUpdate: ->
- deprecate('Please remove from your code. ::requestDisplayUpdate no longer does anything')
-
- updateDisplay: ->
- deprecate('Please remove from your code. ::updateDisplay no longer does anything')
-
- resetDisplay: ->
- deprecate('Please remove from your code. ::resetDisplay no longer does anything')
-
- redraw: ->
- deprecate('Please remove from your code. ::redraw no longer does anything')
-
- setPlaceholderText: (placeholderText) ->
- deprecate('Use TextEditor::setPlaceholderText instead. eg. editorView.getModel().setPlaceholderText(text)')
- @model.setPlaceholderText(placeholderText)
-
- lineElementForScreenRow: (screenRow) ->
- $(@component.lineNodeForScreenRow(screenRow))
diff --git a/src/text-editor.coffee b/src/text-editor.coffee
deleted file mode 100644
index 4489d82afa6..00000000000
--- a/src/text-editor.coffee
+++ /dev/null
@@ -1,3115 +0,0 @@
-_ = require 'underscore-plus'
-path = require 'path'
-Serializable = require 'serializable'
-Delegator = require 'delegato'
-{includeDeprecatedAPIs, deprecate} = require 'grim'
-{CompositeDisposable, Emitter} = require 'event-kit'
-{Point, Range} = TextBuffer = require 'text-buffer'
-LanguageMode = require './language-mode'
-DisplayBuffer = require './display-buffer'
-Cursor = require './cursor'
-Model = require './model'
-Selection = require './selection'
-TextMateScopeSelector = require('first-mate').ScopeSelector
-{Directory} = require "pathwatcher"
-GutterContainer = require './gutter-container'
-
-# Public: 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. If you're
-# interested in the visual appearance of editors, use {TextEditorView} instead.
-#
-# A single {TextBuffer} can belong to multiple editors. For example, if the
-# same file is open in two different panes, Atom creates a separate editor for
-# each pane. If the buffer is manipulated the changes are reflected in both
-# editors, but each maintains its own cursor position, folded lines, etc.
-#
-# ## Accessing TextEditor Instances
-#
-# The easiest way to get hold of `TextEditor` objects is by registering a callback
-# with `::observeTextEditors` on the `atom.workspace` global. Your callback will
-# then be called with all current editor instances and also when any editor is
-# created in the future.
-#
-# ```coffee
-# atom.workspace.observeTextEditors (editor) ->
-# editor.insertText('Hello World')
-# ```
-#
-# ## Buffer vs. Screen Coordinates
-#
-# Because editors support folds and soft-wrapping, the lines on screen don't
-# always match the lines in the buffer. For example, a long line that soft wraps
-# twice renders as three lines on screen, but only represents one line in the
-# buffer. Similarly, if rows 5-10 are folded, then row 6 on screen corresponds
-# to row 11 in the buffer.
-#
-# Your choice of coordinates systems will depend on what you're trying to
-# achieve. For example, if you're writing a command that jumps the cursor up or
-# down by 10 lines, you'll want to use screen coordinates because the user
-# probably wants to skip lines *on screen*. However, if you're writing a package
-# that jumps between method definitions, you'll want to work in buffer
-# coordinates.
-#
-# **When in doubt, just default to buffer coordinates**, then experiment with
-# soft wraps and folds to ensure your code interacts with them correctly.
-module.exports =
-class TextEditor extends Model
- Serializable.includeInto(this)
- atom.deserializers.add(this)
- Delegator.includeInto(this)
-
- deserializing: false
- callDisplayBufferCreatedHook: false
- registerEditor: false
- buffer: null
- languageMode: null
- cursors: null
- selections: null
- suppressSelectionMerging: false
- updateBatchDepth: 0
- selectionFlashDuration: 500
- gutterContainer: null
-
- @delegatesMethods 'suggestedIndentForBufferRow', 'autoIndentBufferRow', 'autoIndentBufferRows',
- 'autoDecreaseIndentForBufferRow', 'toggleLineCommentForBufferRow', 'toggleLineCommentsForBufferRows',
- toProperty: 'languageMode'
-
- constructor: ({@softTabs, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, buffer, registerEditor, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible}={}) ->
- super
-
- @emitter = new Emitter
- @disposables = new CompositeDisposable
- @cursors = []
- @selections = []
-
- buffer ?= new TextBuffer
- @displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrapped, ignoreInvisibles: @mini})
- @buffer = @displayBuffer.buffer
- @softTabs = @usesSoftTabs() ? @softTabs ? atom.config.get('editor.softTabs') ? true
-
- for marker in @findMarkers(@getSelectionMarkerAttributes())
- marker.setProperties(preserveFolds: true)
- @addSelection(marker)
-
- @subscribeToBuffer()
- @subscribeToDisplayBuffer()
-
- if @getCursors().length is 0 and not suppressCursorCreation
- initialLine = Math.max(parseInt(initialLine) or 0, 0)
- initialColumn = Math.max(parseInt(initialColumn) or 0, 0)
- @addCursorAtBufferPosition([initialLine, initialColumn])
-
- @languageMode = new LanguageMode(this)
-
- @setEncoding(atom.config.get('core.fileEncoding', scope: @getRootScopeDescriptor()))
-
- @disposables.add @displayBuffer.onDidChangeScrollTop (scrollTop) =>
- @emit 'scroll-top-changed', scrollTop if includeDeprecatedAPIs
- @emitter.emit 'did-change-scroll-top', scrollTop
-
- @disposables.add @displayBuffer.onDidChangeScrollLeft (scrollLeft) =>
- @emit 'scroll-left-changed', scrollLeft if includeDeprecatedAPIs
- @emitter.emit 'did-change-scroll-left', scrollLeft
-
- @gutterContainer = new GutterContainer(this)
- @lineNumberGutter = @gutterContainer.addGutter
- name: 'line-number'
- priority: 0
- visible: lineNumberGutterVisible
-
- atom.workspace?.editorAdded(this) if registerEditor
-
- serializeParams: ->
- id: @id
- softTabs: @softTabs
- scrollTop: @scrollTop
- scrollLeft: @scrollLeft
- displayBuffer: @displayBuffer.serialize()
-
- deserializeParams: (params) ->
- params.displayBuffer = DisplayBuffer.deserialize(params.displayBuffer)
- params.registerEditor = true
- params
-
- subscribeToBuffer: ->
- @buffer.retain()
- @disposables.add @buffer.onDidChangePath =>
- unless atom.project.getPaths().length > 0
- atom.project.setPaths([path.dirname(@getPath())])
- @emit "title-changed" if includeDeprecatedAPIs
- @emitter.emit 'did-change-title', @getTitle()
- @emit "path-changed" if includeDeprecatedAPIs
- @emitter.emit 'did-change-path', @getPath()
- @disposables.add @buffer.onDidChangeEncoding =>
- @emitter.emit 'did-change-encoding', @getEncoding()
- @disposables.add @buffer.onDidDestroy => @destroy()
-
- # TODO: remove these when we remove the deprecations. They are old events.
- if includeDeprecatedAPIs
- @subscribe @buffer.onDidStopChanging => @emit "contents-modified"
- @subscribe @buffer.onDidConflict => @emit "contents-conflicted"
- @subscribe @buffer.onDidChangeModified => @emit "modified-status-changed"
-
- @preserveCursorPositionOnBufferReload()
-
- subscribeToDisplayBuffer: ->
- @disposables.add @displayBuffer.onDidCreateMarker @handleMarkerCreated
- @disposables.add @displayBuffer.onDidUpdateMarkers => @mergeIntersectingSelections()
- @disposables.add @displayBuffer.onDidChangeGrammar => @handleGrammarChange()
- @disposables.add @displayBuffer.onDidTokenize => @handleTokenization()
- @disposables.add @displayBuffer.onDidChange (e) =>
- @emit 'screen-lines-changed', e if includeDeprecatedAPIs
- @emitter.emit 'did-change', e
-
- # TODO: remove these when we remove the deprecations. Though, no one is likely using them
- if includeDeprecatedAPIs
- @subscribe @displayBuffer.onDidChangeSoftWrapped (softWrapped) => @emit 'soft-wrap-changed', softWrapped
- @subscribe @displayBuffer.onDidAddDecoration (decoration) => @emit 'decoration-added', decoration
- @subscribe @displayBuffer.onDidRemoveDecoration (decoration) => @emit 'decoration-removed', decoration
-
- destroyed: ->
- @unsubscribe() if includeDeprecatedAPIs
- @disposables.dispose()
- selection.destroy() for selection in @getSelections()
- @buffer.release()
- @displayBuffer.destroy()
- @languageMode.destroy()
- @gutterContainer.destroy()
- @emitter.emit 'did-destroy'
-
- ###
- Section: Event Subscription
- ###
-
- # Essential: Calls your `callback` when the buffer's title has changed.
- #
- # * `callback` {Function}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangeTitle: (callback) ->
- @emitter.on 'did-change-title', callback
-
- # Essential: Calls your `callback` when the buffer's path, and therefore title, has changed.
- #
- # * `callback` {Function}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangePath: (callback) ->
- @emitter.on 'did-change-path', callback
-
- # Essential: Invoke the given callback synchronously when the content of the
- # buffer changes.
- #
- # Because observers are invoked synchronously, it's important not to perform
- # any expensive operations via this method. Consider {::onDidStopChanging} to
- # delay expensive operations until after changes stop occurring.
- #
- # * `callback` {Function}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChange: (callback) ->
- @emitter.on 'did-change', callback
-
- # Essential: Invoke `callback` when the buffer's contents change. It is
- # emit asynchronously 300ms after the last buffer change. This is a good place
- # to handle changes to the buffer without compromising typing performance.
- #
- # * `callback` {Function}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidStopChanging: (callback) ->
- @getBuffer().onDidStopChanging(callback)
-
- # Essential: Calls your `callback` when a {Cursor} is moved. If there are
- # multiple cursors, your callback will be called for each cursor.
- #
- # * `callback` {Function}
- # * `event` {Object}
- # * `oldBufferPosition` {Point}
- # * `oldScreenPosition` {Point}
- # * `newBufferPosition` {Point}
- # * `newScreenPosition` {Point}
- # * `textChanged` {Boolean}
- # * `cursor` {Cursor} that triggered the event
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangeCursorPosition: (callback) ->
- @emitter.on 'did-change-cursor-position', callback
-
- # Essential: Calls your `callback` when a selection's screen range changes.
- #
- # * `callback` {Function}
- # * `event` {Object}
- # * `oldBufferRange` {Range}
- # * `oldScreenRange` {Range}
- # * `newBufferRange` {Range}
- # * `newScreenRange` {Range}
- # * `selection` {Selection} that triggered the event
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangeSelectionRange: (callback) ->
- @emitter.on 'did-change-selection-range', callback
-
- # Extended: Calls your `callback` when soft wrap was enabled or disabled.
- #
- # * `callback` {Function}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangeSoftWrapped: (callback) ->
- @displayBuffer.onDidChangeSoftWrapped(callback)
-
- # Extended: Calls your `callback` when the buffer's encoding has changed.
- #
- # * `callback` {Function}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangeEncoding: (callback) ->
- @emitter.on 'did-change-encoding', callback
-
- # Extended: Calls your `callback` when the grammar that interprets and
- # colorizes the text has been changed. Immediately calls your callback with
- # the current grammar.
- #
- # * `callback` {Function}
- # * `grammar` {Grammar}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- observeGrammar: (callback) ->
- callback(@getGrammar())
- @onDidChangeGrammar(callback)
-
- # Extended: Calls your `callback` when the grammar that interprets and
- # colorizes the text has been changed.
- #
- # * `callback` {Function}
- # * `grammar` {Grammar}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangeGrammar: (callback) ->
- @emitter.on 'did-change-grammar', callback
-
- # Extended: Calls your `callback` when the result of {::isModified} changes.
- #
- # * `callback` {Function}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangeModified: (callback) ->
- @getBuffer().onDidChangeModified(callback)
-
- # Extended: Calls your `callback` when the buffer's underlying file changes on
- # disk at a moment when the result of {::isModified} is true.
- #
- # * `callback` {Function}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidConflict: (callback) ->
- @getBuffer().onDidConflict(callback)
-
- # Extended: Calls your `callback` before text has been inserted.
- #
- # * `callback` {Function}
- # * `event` event {Object}
- # * `text` {String} text to be inserted
- # * `cancel` {Function} Call to prevent the text from being inserted
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onWillInsertText: (callback) ->
- @emitter.on 'will-insert-text', callback
-
- # Extended: Calls your `callback` adter text has been inserted.
- #
- # * `callback` {Function}
- # * `event` event {Object}
- # * `text` {String} text to be inserted
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidInsertText: (callback) ->
- @emitter.on 'did-insert-text', callback
-
- # Public: Invoke the given callback after the buffer is saved to disk.
- #
- # * `callback` {Function} to be called after the buffer is saved.
- # * `event` {Object} with the following keys:
- # * `path` The path to which the buffer was saved.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidSave: (callback) ->
- @getBuffer().onDidSave(callback)
-
- # Public: Invoke the given callback when the editor is destroyed.
- #
- # * `callback` {Function} to be called when the editor is destroyed.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidDestroy: (callback) ->
- @emitter.on 'did-destroy', callback
-
- # Extended: Calls your `callback` when a {Cursor} is added to the editor.
- # Immediately calls your callback for each existing cursor.
- #
- # * `callback` {Function}
- # * `cursor` {Cursor} that was added
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- observeCursors: (callback) ->
- callback(cursor) for cursor in @getCursors()
- @onDidAddCursor(callback)
-
- # Extended: Calls your `callback` when a {Cursor} is added to the editor.
- #
- # * `callback` {Function}
- # * `cursor` {Cursor} that was added
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidAddCursor: (callback) ->
- @emitter.on 'did-add-cursor', callback
-
- # Extended: Calls your `callback` when a {Cursor} is removed from the editor.
- #
- # * `callback` {Function}
- # * `cursor` {Cursor} that was removed
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidRemoveCursor: (callback) ->
- @emitter.on 'did-remove-cursor', callback
-
- # Extended: Calls your `callback` when a {Selection} is added to the editor.
- # Immediately calls your callback for each existing selection.
- #
- # * `callback` {Function}
- # * `selection` {Selection} that was added
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- observeSelections: (callback) ->
- callback(selection) for selection in @getSelections()
- @onDidAddSelection(callback)
-
- # Extended: Calls your `callback` when a {Selection} is added to the editor.
- #
- # * `callback` {Function}
- # * `selection` {Selection} that was added
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidAddSelection: (callback) ->
- @emitter.on 'did-add-selection', callback
-
- # Extended: Calls your `callback` when a {Selection} is removed from the editor.
- #
- # * `callback` {Function}
- # * `selection` {Selection} that was removed
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidRemoveSelection: (callback) ->
- @emitter.on 'did-remove-selection', callback
-
- # Extended: Calls your `callback` with each {Decoration} added to the editor.
- # Calls your `callback` immediately for any existing decorations.
- #
- # * `callback` {Function}
- # * `decoration` {Decoration}
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- observeDecorations: (callback) ->
- @displayBuffer.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) ->
- @displayBuffer.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) ->
- @displayBuffer.onDidRemoveDecoration(callback)
-
- # Extended: Calls your `callback` when the placeholder text is changed.
- #
- # * `callback` {Function}
- # * `placeholderText` {String} new text
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChangePlaceholderText: (callback) ->
- @emitter.on 'did-change-placeholder-text', callback
-
- onDidChangeCharacterWidths: (callback) ->
- @displayBuffer.onDidChangeCharacterWidths(callback)
-
- onDidChangeScrollTop: (callback) ->
- @emitter.on 'did-change-scroll-top', callback
-
- onDidChangeScrollLeft: (callback) ->
- @emitter.on 'did-change-scroll-left', callback
-
- # TODO Remove once the tabs package no longer uses .on subscriptions
- onDidChangeIcon: (callback) ->
- @emitter.on 'did-change-icon', callback
-
- # Public: Retrieves the current {TextBuffer}.
- getBuffer: -> @buffer
-
- # Retrieves the current buffer's URI.
- getURI: -> @buffer.getUri()
-
- # Create an {TextEditor} with its initial state based on this object
- copy: ->
- displayBuffer = @displayBuffer.copy()
- softTabs = @getSoftTabs()
- newEditor = new TextEditor({@buffer, displayBuffer, @tabLength, softTabs, suppressCursorCreation: true, registerEditor: true})
- for marker in @findMarkers(editorId: @id)
- marker.copy(editorId: newEditor.id, preserveFolds: true)
- newEditor
-
- # Controls visibility based on the given {Boolean}.
- setVisible: (visible) -> @displayBuffer.setVisible(visible)
-
- setMini: (mini) ->
- if mini isnt @mini
- @mini = mini
- @displayBuffer.setIgnoreInvisibles(@mini)
- @emitter.emit 'did-change-mini', @mini
- @mini
-
- isMini: -> @mini
-
- onDidChangeMini: (callback) ->
- @emitter.on 'did-change-mini', callback
-
- setLineNumberGutterVisible: (lineNumberGutterVisible) ->
- unless lineNumberGutterVisible is @lineNumberGutter.isVisible()
- if lineNumberGutterVisible
- @lineNumberGutter.show()
- else
- @lineNumberGutter.hide()
- @emitter.emit 'did-change-line-number-gutter-visible', @lineNumberGutter.isVisible()
- @lineNumberGutter.isVisible()
-
- isLineNumberGutterVisible: -> @lineNumberGutter.isVisible()
-
- onDidChangeLineNumberGutterVisible: (callback) ->
- @emitter.on 'did-change-line-number-gutter-visible', callback
-
- # Public: Creates and returns a {Gutter}.
- # See {GutterContainer::addGutter} for more details.
- addGutter: (options) ->
- @gutterContainer.addGutter(options)
-
- # Public: Returns the {Array} of all gutters on this editor.
- getGutters: ->
- @gutterContainer.getGutters()
-
- # Public: Returns the {Gutter} with the given name, or null if it doesn't exist.
- gutterWithName: (name) ->
- @gutterContainer.gutterWithName(name)
-
- # Calls your `callback` when a {Gutter} is added to the editor.
- # Immediately calls your callback for each existing gutter.
- #
- # * `callback` {Function}
- # * `gutter` {Gutter} that currently exists/was added.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- observeGutters: (callback) ->
- @gutterContainer.observeGutters callback
-
- # Calls your `callback` when a {Gutter} is added to the editor.
- #
- # * `callback` {Function}
- # * `gutter` {Gutter} that was added.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidAddGutter: (callback) ->
- @gutterContainer.onDidAddGutter callback
-
- # Calls your `callback` when a {Gutter} is removed from the editor.
- #
- # * `callback` {Function}
- # * `name` The name of the {Gutter} that was removed.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidRemoveGutter: (callback) ->
- @gutterContainer.onDidRemoveGutter callback
-
- # Set the number of characters that can be displayed horizontally in the
- # editor.
- #
- # * `editorWidthInChars` A {Number} representing the width of the {TextEditorView}
- # in characters.
- setEditorWidthInChars: (editorWidthInChars) ->
- @displayBuffer.setEditorWidthInChars(editorWidthInChars)
-
- ###
- Section: File Details
- ###
-
- # Essential: Get the editor's title for display in other parts of the
- # UI such as the tabs.
- #
- # If the editor's buffer is saved, its title is the file name. If it is
- # unsaved, its title is "untitled".
- #
- # Returns a {String}.
- getTitle: ->
- if sessionPath = @getPath()
- path.basename(sessionPath)
- else
- 'untitled'
-
- # Essential: Get the editor's long title for display in other parts of the UI
- # such as the window title.
- #
- # If the editor's buffer is saved, its long title is formatted as
- # " - ". If it is unsaved, its title is "untitled"
- #
- # Returns a {String}.
- getLongTitle: ->
- if sessionPath = @getPath()
- fileName = path.basename(sessionPath)
- directory = atom.project.relativize(path.dirname(sessionPath))
- directory = if directory.length > 0 then directory else path.basename(path.dirname(sessionPath))
- "#{fileName} - #{directory}"
- else
- 'untitled'
-
- # Essential: Returns the {String} path of this editor's text buffer.
- getPath: -> @buffer.getPath()
-
- # Extended: Returns the {String} character set encoding of this editor's text
- # buffer.
- getEncoding: -> @buffer.getEncoding()
-
- # Extended: Set the character set encoding to use in this editor's text
- # buffer.
- #
- # * `encoding` The {String} character set encoding name such as 'utf8'
- setEncoding: (encoding) -> @buffer.setEncoding(encoding)
-
- # Essential: Returns {Boolean} `true` if this editor has been modified.
- isModified: -> @buffer.isModified()
-
- # Essential: Returns {Boolean} `true` if this editor has no content.
- isEmpty: -> @buffer.isEmpty()
-
- # Copies the current file path to the native clipboard.
- copyPathToClipboard: ->
- if filePath = @getPath()
- atom.clipboard.write(filePath)
-
- ###
- Section: File Operations
- ###
-
- # Essential: Saves the editor's text buffer.
- #
- # See {TextBuffer::save} for more details.
- save: -> @buffer.save()
-
- # Public: Saves the editor's text buffer as the given path.
- #
- # See {TextBuffer::saveAs} for more details.
- #
- # * `filePath` A {String} path.
- saveAs: (filePath) -> @buffer.saveAs(filePath)
-
- # Determine whether the user should be prompted to save before closing
- # this editor.
- shouldPromptToSave: ({windowCloseRequested}={}) ->
- if windowCloseRequested
- @isModified()
- else
- @isModified() and not @buffer.hasMultipleEditors()
-
- checkoutHeadRevision: ->
- if filePath = this.getPath()
- atom.project.repositoryForDirectory(new Directory(path.dirname(filePath)))
- .then (repository) =>
- repository?.checkoutHeadForEditor(this)
- else
- Promise.resolve(false)
-
- ###
- Section: Reading Text
- ###
-
- # Essential: Returns a {String} representing the entire contents of the editor.
- getText: -> @buffer.getText()
-
- # Essential: Get the text in the given {Range} in buffer coordinates.
- #
- # * `range` A {Range} or range-compatible {Array}.
- #
- # Returns a {String}.
- getTextInBufferRange: (range) ->
- @buffer.getTextInRange(range)
-
- # Essential: Returns a {Number} representing the number of lines in the buffer.
- getLineCount: -> @buffer.getLineCount()
-
- # Essential: Returns a {Number} representing the number of screen lines in the
- # editor. This accounts for folds.
- getScreenLineCount: -> @displayBuffer.getLineCount()
-
- # Essential: Returns a {Number} representing the last zero-indexed buffer row
- # number of the editor.
- getLastBufferRow: -> @buffer.getLastRow()
-
- # Essential: Returns a {Number} representing the last zero-indexed screen row
- # number of the editor.
- getLastScreenRow: -> @displayBuffer.getLastRow()
-
- # Essential: Returns a {String} representing the contents of the line at the
- # given buffer row.
- #
- # * `bufferRow` A {Number} representing a zero-indexed buffer row.
- lineTextForBufferRow: (bufferRow) -> @buffer.lineForRow(bufferRow)
-
- # Essential: Returns a {String} representing the contents of the line at the
- # given screen row.
- #
- # * `screenRow` A {Number} representing a zero-indexed screen row.
- lineTextForScreenRow: (screenRow) -> @displayBuffer.tokenizedLineForScreenRow(screenRow)?.text
-
- # Gets the screen line for the given screen row.
- #
- # * `screenRow` - A {Number} indicating the screen row.
- #
- # Returns {TokenizedLine}
- tokenizedLineForScreenRow: (screenRow) -> @displayBuffer.tokenizedLineForScreenRow(screenRow)
-
- # {Delegates to: DisplayBuffer.tokenizedLinesForScreenRows}
- tokenizedLinesForScreenRows: (start, end) -> @displayBuffer.tokenizedLinesForScreenRows(start, end)
-
- bufferRowForScreenRow: (row) -> @displayBuffer.bufferRowForScreenRow(row)
-
- # {Delegates to: DisplayBuffer.bufferRowsForScreenRows}
- bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow)
-
- screenRowForBufferRow: (row) -> @displayBuffer.screenRowForBufferRow(row)
-
- # {Delegates to: DisplayBuffer.getMaxLineLength}
- getMaxScreenLineLength: -> @displayBuffer.getMaxLineLength()
-
- getLongestScreenRow: -> @displayBuffer.getLongestScreenRow()
-
- # Returns the range for the given buffer row.
- #
- # * `row` A row {Number}.
- # * `options` (optional) An options hash with an `includeNewline` key.
- #
- # Returns a {Range}.
- bufferRangeForBufferRow: (row, {includeNewline}={}) -> @buffer.rangeForRow(row, includeNewline)
-
- # Get the text in the given {Range}.
- #
- # Returns a {String}.
- getTextInRange: (range) -> @buffer.getTextInRange(range)
-
- # {Delegates to: TextBuffer.isRowBlank}
- isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow)
-
- # {Delegates to: TextBuffer.nextNonBlankRow}
- nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow)
-
- # {Delegates to: TextBuffer.getEndPosition}
- getEofBufferPosition: -> @buffer.getEndPosition()
-
- # Public: Get the {Range} of the paragraph surrounding the most recently added
- # cursor.
- #
- # Returns a {Range}.
- getCurrentParagraphBufferRange: ->
- @getLastCursor().getCurrentParagraphBufferRange()
-
-
- ###
- Section: Mutating Text
- ###
-
- # Essential: Replaces the entire contents of the buffer with the given {String}.
- setText: (text) -> @buffer.setText(text)
-
- # Essential: Set the text in the given {Range} in buffer coordinates.
- #
- # * `range` A {Range} or range-compatible {Array}.
- # * `text` A {String}
- # * `options` (optional) {Object}
- # * `normalizeLineEndings` (optional) {Boolean} (default: true)
- # * `undo` (optional) {String} 'skip' will skip the undo system
- #
- # Returns the {Range} of the newly-inserted text.
- setTextInBufferRange: (range, text, options) -> @getBuffer().setTextInRange(range, text, options)
-
- # Essential: For each selection, replace the selected text with the given text.
- #
- # * `text` A {String} representing the text to insert.
- # * `options` (optional) See {Selection::insertText}.
- #
- # Returns a {Range} when the text has been inserted
- # Returns a {Boolean} false when the text has not been inserted
- insertText: (text, options={}) ->
- willInsert = true
- cancel = -> willInsert = false
- willInsertEvent = {cancel, text}
- @emit('will-insert-text', willInsertEvent) if includeDeprecatedAPIs
- @emitter.emit 'will-insert-text', willInsertEvent
-
- if willInsert
- options.autoIndentNewline ?= @shouldAutoIndent()
- options.autoDecreaseIndent ?= @shouldAutoIndent()
- @mutateSelectedText (selection) =>
- range = selection.insertText(text, options)
- didInsertEvent = {text, range}
- @emit('did-insert-text', didInsertEvent) if includeDeprecatedAPIs
- @emitter.emit 'did-insert-text', didInsertEvent
- range
- else
- false
-
- # Essential: For each selection, replace the selected text with a newline.
- insertNewline: ->
- @insertText('\n')
-
- # Essential: For each selection, if the selection is empty, delete the character
- # following the cursor. Otherwise delete the selected text.
- delete: ->
- @mutateSelectedText (selection) -> selection.delete()
-
- # Essential: For each selection, if the selection is empty, delete the character
- # preceding the cursor. Otherwise delete the selected text.
- backspace: ->
- @mutateSelectedText (selection) -> selection.backspace()
-
- # Extended: Mutate the text of all the selections in a single transaction.
- #
- # All the changes made inside the given {Function} can be reverted with a
- # single call to {::undo}.
- #
- # * `fn` A {Function} that will be called once for each {Selection}. The first
- # argument will be a {Selection} and the second argument will be the
- # {Number} index of that selection.
- mutateSelectedText: (fn) ->
- @mergeIntersectingSelections =>
- @transact =>
- fn(selection, index) for selection, index in @getSelectionsOrderedByBufferPosition()
-
- # Move lines intersection the most recent selection up by one row in screen
- # coordinates.
- moveLineUp: ->
- selection = @getSelectedBufferRange()
- return if selection.start.row is 0
- lastRow = @buffer.getLastRow()
- return if selection.isEmpty() and selection.start.row is lastRow and @buffer.getLastLine() is ''
-
- @transact =>
- foldedRows = []
- rows = [selection.start.row..selection.end.row]
- if selection.start.row isnt selection.end.row and selection.end.column is 0
- rows.pop() unless @isFoldedAtBufferRow(selection.end.row)
-
- # Move line around the fold that is directly above the selection
- precedingScreenRow = @screenPositionForBufferPosition([selection.start.row]).translate([-1])
- precedingBufferRow = @bufferPositionForScreenPosition(precedingScreenRow).row
- if fold = @largestFoldContainingBufferRow(precedingBufferRow)
- insertDelta = fold.getBufferRange().getRowCount()
- else
- insertDelta = 1
-
- for row in rows
- if fold = @displayBuffer.largestFoldStartingAtBufferRow(row)
- bufferRange = fold.getBufferRange()
- startRow = bufferRange.start.row
- endRow = bufferRange.end.row
- foldedRows.push(startRow - insertDelta)
- else
- startRow = row
- endRow = row
-
- insertPosition = Point.fromObject([startRow - insertDelta])
- endPosition = Point.min([endRow + 1], @buffer.getEndPosition())
- lines = @buffer.getTextInRange([[startRow], endPosition])
- if endPosition.row is lastRow and endPosition.column > 0 and not @buffer.lineEndingForRow(endPosition.row)
- lines = "#{lines}\n"
-
- @buffer.deleteRows(startRow, endRow)
-
- # Make sure the inserted text doesn't go into an existing fold
- if fold = @displayBuffer.largestFoldStartingAtBufferRow(insertPosition.row)
- @unfoldBufferRow(insertPosition.row)
- foldedRows.push(insertPosition.row + endRow - startRow + fold.getBufferRange().getRowCount())
-
- @buffer.insert(insertPosition, lines)
-
- # Restore folds that existed before the lines were moved
- for foldedRow in foldedRows when 0 <= foldedRow <= @getLastBufferRow()
- @foldBufferRow(foldedRow)
-
- @setSelectedBufferRange(selection.translate([-insertDelta]), preserveFolds: true, autoscroll: true)
-
- # Move lines intersecting the most recent selection down by one row in screen
- # coordinates.
- moveLineDown: ->
- selection = @getSelectedBufferRange()
- lastRow = @buffer.getLastRow()
- return if selection.end.row is lastRow
- return if selection.end.row is lastRow - 1 and @buffer.getLastLine() is ''
-
- @transact =>
- foldedRows = []
- rows = [selection.end.row..selection.start.row]
- if selection.start.row isnt selection.end.row and selection.end.column is 0
- rows.shift() unless @isFoldedAtBufferRow(selection.end.row)
-
- # Move line around the fold that is directly below the selection
- followingScreenRow = @screenPositionForBufferPosition([selection.end.row]).translate([1])
- followingBufferRow = @bufferPositionForScreenPosition(followingScreenRow).row
- if fold = @largestFoldContainingBufferRow(followingBufferRow)
- insertDelta = fold.getBufferRange().getRowCount()
- else
- insertDelta = 1
-
- for row in rows
- if fold = @displayBuffer.largestFoldStartingAtBufferRow(row)
- bufferRange = fold.getBufferRange()
- startRow = bufferRange.start.row
- endRow = bufferRange.end.row
- foldedRows.push(endRow + insertDelta)
- else
- startRow = row
- endRow = row
-
- if endRow + 1 is lastRow
- endPosition = [endRow, @buffer.lineLengthForRow(endRow)]
- else
- endPosition = [endRow + 1]
- lines = @buffer.getTextInRange([[startRow], endPosition])
- @buffer.deleteRows(startRow, endRow)
-
- insertPosition = Point.min([startRow + insertDelta], @buffer.getEndPosition())
- if insertPosition.row is @buffer.getLastRow() and insertPosition.column > 0
- lines = "\n#{lines}"
-
- # Make sure the inserted text doesn't go into an existing fold
- if fold = @displayBuffer.largestFoldStartingAtBufferRow(insertPosition.row)
- @unfoldBufferRow(insertPosition.row)
- foldedRows.push(insertPosition.row + fold.getBufferRange().getRowCount())
-
- @buffer.insert(insertPosition, lines)
-
- # Restore folds that existed before the lines were moved
- for foldedRow in foldedRows when 0 <= foldedRow <= @getLastBufferRow()
- @foldBufferRow(foldedRow)
-
- @setSelectedBufferRange(selection.translate([insertDelta]), preserveFolds: true, autoscroll: true)
-
- # Duplicate the most recent cursor's current line.
- duplicateLines: ->
- @transact =>
- for selection in @getSelectionsOrderedByBufferPosition().reverse()
- selectedBufferRange = selection.getBufferRange()
- if selection.isEmpty()
- {start} = selection.getScreenRange()
- selection.selectToScreenPosition([start.row + 1, 0])
-
- [startRow, endRow] = selection.getBufferRowRange()
- endRow++
-
- foldedRowRanges =
- @outermostFoldsInBufferRowRange(startRow, endRow)
- .map (fold) -> fold.getBufferRowRange()
-
- rangeToDuplicate = [[startRow, 0], [endRow, 0]]
- textToDuplicate = @getTextInBufferRange(rangeToDuplicate)
- textToDuplicate = '\n' + textToDuplicate if endRow > @getLastBufferRow()
- @buffer.insert([endRow, 0], textToDuplicate)
-
- delta = endRow - startRow
- selection.setBufferRange(selectedBufferRange.translate([delta, 0]))
- for [foldStartRow, foldEndRow] in foldedRowRanges
- @createFold(foldStartRow + delta, foldEndRow + delta)
- return
-
- replaceSelectedText: (options={}, fn) ->
- {selectWordIfEmpty} = options
- @mutateSelectedText (selection) ->
- range = selection.getBufferRange()
- if selectWordIfEmpty and selection.isEmpty()
- selection.selectWord()
- text = selection.getText()
- selection.deleteSelectedText()
- selection.insertText(fn(text))
- selection.setBufferRange(range)
-
- # Split multi-line selections into one selection per line.
- #
- # Operates on all selections. This method breaks apart all multi-line
- # selections to create multiple single-line selections that cumulatively cover
- # the same original area.
- splitSelectionsIntoLines: ->
- @mergeIntersectingSelections =>
- for selection in @getSelections()
- range = selection.getBufferRange()
- continue if range.isSingleLine()
-
- selection.destroy()
- {start, end} = range
- @addSelectionForBufferRange([start, [start.row, Infinity]])
- {row} = start
- while ++row < end.row
- @addSelectionForBufferRange([[row, 0], [row, Infinity]])
- @addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) unless end.column is 0
- return
-
- # Extended: For each selection, transpose the selected text.
- #
- # If the selection is empty, the characters preceding and following the cursor
- # are swapped. Otherwise, the selected characters are reversed.
- transpose: ->
- @mutateSelectedText (selection) ->
- if selection.isEmpty()
- selection.selectRight()
- text = selection.getText()
- selection.delete()
- selection.cursor.moveLeft()
- selection.insertText text
- else
- selection.insertText selection.getText().split('').reverse().join('')
-
- # Extended: Convert the selected text to upper case.
- #
- # For each selection, if the selection is empty, converts the containing word
- # to upper case. Otherwise convert the selected text to upper case.
- upperCase: ->
- @replaceSelectedText selectWordIfEmpty: true, (text) -> text.toUpperCase()
-
- # Extended: Convert the selected text to lower case.
- #
- # For each selection, if the selection is empty, converts the containing word
- # to upper case. Otherwise convert the selected text to upper case.
- lowerCase: ->
- @replaceSelectedText selectWordIfEmpty: true, (text) -> text.toLowerCase()
-
- # Extended: Toggle line comments for rows intersecting selections.
- #
- # If the current grammar doesn't support comments, does nothing.
- toggleLineCommentsInSelection: ->
- @mutateSelectedText (selection) -> selection.toggleLineComments()
-
- # Convert multiple lines to a single line.
- #
- # Operates on all selections. If the selection is empty, joins the current
- # line with the next line. Otherwise it joins all lines that intersect the
- # selection.
- #
- # Joining a line means that multiple lines are converted to a single line with
- # the contents of each of the original non-empty lines separated by a space.
- joinLines: ->
- @mutateSelectedText (selection) -> selection.joinLines()
-
- # Extended: For each cursor, insert a newline at beginning the following line.
- insertNewlineBelow: ->
- @transact =>
- @moveToEndOfLine()
- @insertNewline()
-
- # Extended: For each cursor, insert a newline at the end of the preceding line.
- insertNewlineAbove: ->
- @transact =>
- bufferRow = @getCursorBufferPosition().row
- indentLevel = @indentationForBufferRow(bufferRow)
- onFirstLine = bufferRow is 0
-
- @moveToBeginningOfLine()
- @moveLeft()
- @insertNewline()
-
- if @shouldAutoIndent() and @indentationForBufferRow(bufferRow) < indentLevel
- @setIndentationForBufferRow(bufferRow, indentLevel)
-
- if onFirstLine
- @moveUp()
- @moveToEndOfLine()
-
- # Extended: For each selection, if the selection is empty, delete all characters
- # of the containing word that precede the cursor. Otherwise delete the
- # selected text.
- deleteToBeginningOfWord: ->
- @mutateSelectedText (selection) -> selection.deleteToBeginningOfWord()
-
- # Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the
- # previous word boundary.
- deleteToPreviousWordBoundary: ->
- @mutateSelectedText (selection) -> selection.deleteToPreviousWordBoundary()
-
- # Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the
- # next word boundary.
- deleteToNextWordBoundary: ->
- @mutateSelectedText (selection) -> selection.deleteToNextWordBoundary()
-
- # Extended: For each selection, if the selection is empty, delete all characters
- # of the containing line that precede the cursor. Otherwise delete the
- # selected text.
- deleteToBeginningOfLine: ->
- @mutateSelectedText (selection) -> selection.deleteToBeginningOfLine()
-
- # Extended: For each selection, if the selection is not empty, deletes the
- # selection; otherwise, deletes all characters of the containing line
- # following the cursor. If the cursor is already at the end of the line,
- # deletes the following newline.
- deleteToEndOfLine: ->
- @mutateSelectedText (selection) -> selection.deleteToEndOfLine()
-
- # Extended: For each selection, if the selection is empty, delete all characters
- # of the containing word following the cursor. Otherwise delete the selected
- # text.
- deleteToEndOfWord: ->
- @mutateSelectedText (selection) -> selection.deleteToEndOfWord()
-
- # Extended: Delete all lines intersecting selections.
- deleteLine: ->
- @mergeSelectionsOnSameRows()
- @mutateSelectedText (selection) -> selection.deleteLine()
-
- ###
- Section: History
- ###
-
- # Essential: Undo the last change.
- undo: ->
- @buffer.undo()
- @getLastSelection().autoscroll()
-
- # Essential: Redo the last change.
- redo: ->
- @buffer.redo(this)
- @getLastSelection().autoscroll()
-
- # Extended: Batch multiple operations as a single undo/redo step.
- #
- # Any group of operations that are logically grouped from the perspective of
- # undoing and redoing should be performed in a transaction. If you want to
- # abort the transaction, call {::abortTransaction} to terminate the function's
- # execution and revert any changes performed up to the abortion.
- #
- # * `groupingInterval` (optional) The {Number} of milliseconds for which this
- # transaction should be considered 'groupable' after it begins. If a transaction
- # with a positive `groupingInterval` is committed while the previous transaction is
- # still 'groupable', the two transactions are merged with respect to undo and redo.
- # * `fn` A {Function} to call inside the transaction.
- transact: (groupingInterval, fn) ->
- @buffer.transact(groupingInterval, fn)
-
- # Deprecated: Start an open-ended transaction.
- beginTransaction: (groupingInterval) -> @buffer.beginTransaction(groupingInterval)
-
- # Deprecated: Commit an open-ended transaction started with {::beginTransaction}.
- commitTransaction: -> @buffer.commitTransaction()
-
- # Extended: Abort an open transaction, undoing any operations performed so far
- # within the transaction.
- abortTransaction: -> @buffer.abortTransaction()
-
- # Extended: Create a pointer to the current state of the buffer for use
- # with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}.
- #
- # Returns a checkpoint value.
- createCheckpoint: -> @buffer.createCheckpoint()
-
- # Extended: Revert the buffer to the state it was in when the given
- # checkpoint was created.
- #
- # The redo stack will be empty following this operation, so changes since the
- # checkpoint will be lost. If the given checkpoint is no longer present in the
- # undo history, no changes will be made to the buffer and this method will
- # return `false`.
- #
- # Returns a {Boolean} indicating whether the operation succeeded.
- revertToCheckpoint: (checkpoint) -> @buffer.revertToCheckpoint(checkpoint)
-
- # Extended: Group all changes since the given checkpoint into a single
- # transaction for purposes of undo/redo.
- #
- # If the given checkpoint is no longer present in the undo history, no
- # grouping will be performed and this method will return `false`.
- #
- # Returns a {Boolean} indicating whether the operation succeeded.
- groupChangesSinceCheckpoint: (checkpoint) -> @buffer.groupChangesSinceCheckpoint(checkpoint)
-
- ###
- Section: TextEditor Coordinates
- ###
-
- # Essential: Convert a position in buffer-coordinates to screen-coordinates.
- #
- # The position is clipped via {::clipBufferPosition} prior to the conversion.
- # The position is also clipped via {::clipScreenPosition} following the
- # conversion, which only makes a difference when `options` are supplied.
- #
- # * `bufferPosition` A {Point} or {Array} of [row, column].
- # * `options` (optional) An options hash for {::clipScreenPosition}.
- #
- # Returns a {Point}.
- screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(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) -> @displayBuffer.bufferPositionForScreenPosition(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) -> @displayBuffer.screenRangeForBufferRange(bufferRange)
-
- # 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) -> @displayBuffer.bufferRangeForScreenRange(screenRange)
-
- # Extended: Clip the given {Point} to a valid position in the buffer.
- #
- # If the given {Point} describes a position that is actually reachable by the
- # cursor based on the current contents of the buffer, it is returned
- # unchanged. If the {Point} does not describe a valid position, the closest
- # valid position is returned instead.
- #
- # ## Examples
- #
- # ```coffee
- # editor.clipBufferPosition([-1, -1]) # -> `[0, 0]`
- #
- # # When the line at buffer row 2 is 10 characters long
- # editor.clipBufferPosition([2, Infinity]) # -> `[2, 10]`
- # ```
- #
- # * `bufferPosition` The {Point} representing the position to clip.
- #
- # Returns a {Point}.
- clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition)
-
- # Extended: Clip the start and end of the given range to valid positions in the
- # buffer. See {::clipBufferPosition} for more information.
- #
- # * `range` The {Range} to clip.
- #
- # Returns a {Range}.
- clipBufferRange: (range) -> @buffer.clipRange(range)
-
- # Extended: Clip the given {Point} to a valid position on screen.
- #
- # If the given {Point} describes a position that is actually reachable by the
- # cursor based on the current contents of the screen, it is returned
- # unchanged. If the {Point} does not describe a valid position, the closest
- # valid position is returned instead.
- #
- # ## Examples
- #
- # ```coffee
- # editor.clipScreenPosition([-1, -1]) # -> `[0, 0]`
- #
- # # When the line at screen row 2 is 10 characters long
- # editor.clipScreenPosition([2, Infinity]) # -> `[2, 10]`
- # ```
- #
- # * `screenPosition` The {Point} representing the position to clip.
- # * `options` (optional) {Object}
- # * `wrapBeyondNewlines` {Boolean} if `true`, continues wrapping past newlines
- # * `wrapAtSoftNewlines` {Boolean} if `true`, continues wrapping past soft newlines
- # * `screenLine` {Boolean} if `true`, indicates that you're using a line number, not a row number
- #
- # Returns a {Point}.
- clipScreenPosition: (screenPosition, options) -> @displayBuffer.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: (range, options) -> @displayBuffer.clipScreenRange(range, options)
-
- ###
- Section: Decorations
- ###
-
- # Essential: Adds a decoration that tracks a {Marker}. When the marker moves,
- # is invalidated, or is destroyed, the decoration will be updated to reflect
- # the marker's state.
- #
- # There are three types of supported decorations:
- #
- # * __line__: Adds your CSS `class` to the line nodes within the range
- # marked by the marker
- # * __gutter__: Adds your CSS `class` to the line number nodes within the
- # range marked by the marker
- # * __highlight__: Adds a new highlight div to the editor surrounding the
- # range marked by the marker. When the user selects text, the selection is
- # visualized with a highlight decoration internally. The structure of this
- # highlight will be
- # ```html
- #
- #
- #
- #
- # ```
- #
- # ## Arguments
- #
- # * `marker` A {Marker} you want this decoration to follow.
- # * `decorationParams` An {Object} representing the decoration e.g.
- # `{type: 'line-number', class: 'linter-error'}`
- # * `type` There are a few supported decoration types: `gutter`, `line`,
- # `highlight`, and `overlay`. The behavior of the types are as follows:
- # * `gutter` Adds the given `class` to the line numbers overlapping the
- # rows spanned by the marker.
- # * `line` Adds the given `class` to the lines overlapping the rows
- # spanned by the marker.
- # * `highlight` Creates a `.highlight` div with the nested class with up
- # to 3 nested regions that fill the area spanned by the marker.
- # * `overlay` Positions the view associated with the given item at the
- # head or tail of the given marker, depending on the `position`
- # property.
- # * `class` This CSS class will be applied to the decorated line number,
- # line, or highlight.
- # * `onlyHead` (optional) If `true`, the decoration will only be applied to
- # the head of the marker. Only applicable to the `line` and `gutter`
- # types.
- # * `onlyEmpty` (optional) If `true`, the decoration will only be applied if
- # the associated marker is empty. Only applicable to the `line` and
- # `gutter` types.
- # * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
- # if the associated marker is non-empty. Only applicable to the `line`
- # and gutter types.
- # * `position` (optional) Only applicable to decorations of type `overlay`,
- # controls where the overlay view is positioned relative to the marker.
- # Values can be `'head'` (the default), or `'tail'`.
- # * `gutterName` (optional) Only applicable to the `gutter` type. If provided,
- # the decoration will be applied to the gutter with the specified name.
- #
- # Returns a {Decoration} object
- decorateMarker: (marker, decorationParams) ->
- if includeDeprecatedAPIs and decorationParams.type is 'gutter' and not decorationParams.gutterName
- deprecate("Decorations of `type: 'gutter'` have been renamed to `type: 'line-number'`.")
- decorationParams.type = 'line-number'
- @displayBuffer.decorateMarker(marker, decorationParams)
-
- # Public: Get all the decorations within a screen row range.
- #
- # * `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 {Marker} 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) ->
- @displayBuffer.decorationsForScreenRowRange(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) ->
- @displayBuffer.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) ->
- @displayBuffer.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) ->
- @displayBuffer.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) ->
- @displayBuffer.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) ->
- @displayBuffer.getOverlayDecorations(propertyFilter)
-
- decorationForId: (id) ->
- @displayBuffer.decorationForId(id)
-
- ###
- Section: Markers
- ###
-
- # Essential: Create a marker 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.
- # * `reversed` (optional) Creates the marker in a reversed orientation. (default: false)
- # * `persistent` (optional) Whether to include this marker when serializing the buffer. (default: true)
- # * `invalidate` (optional) 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 {Marker}.
- markBufferRange: (args...) ->
- @displayBuffer.markBufferRange(args...)
-
- # Essential: Create a marker 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.
- # * `reversed` (optional) Creates the marker in a reversed orientation. (default: false)
- # * `persistent` (optional) Whether to include this marker when serializing the buffer. (default: true)
- # * `invalidate` (optional) 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 {Marker}.
- markScreenRange: (args...) ->
- @displayBuffer.markScreenRange(args...)
-
- # Essential: Mark the given position in buffer coordinates.
- #
- # * `position` A {Point} or {Array} of `[row, column]`.
- # * `options` (optional) See {TextBuffer::markRange}.
- #
- # Returns a {Marker}.
- markBufferPosition: (args...) ->
- @displayBuffer.markBufferPosition(args...)
-
- # Essential: Mark the given position in screen coordinates.
- #
- # * `position` A {Point} or {Array} of `[row, column]`.
- # * `options` (optional) See {TextBuffer::markRange}.
- #
- # Returns a {Marker}.
- markScreenPosition: (args...) ->
- @displayBuffer.markScreenPosition(args...)
-
- # Essential: Find all {Marker}s 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.
- findMarkers: (properties) ->
- @displayBuffer.findMarkers(properties)
-
- # Extended: Get the {Marker} for the given marker id.
- #
- # * `id` {Number} id of the marker
- getMarker: (id) ->
- @displayBuffer.getMarker(id)
-
- # Extended: Get all {Marker}s. Consider using {::findMarkers}
- getMarkers: ->
- @displayBuffer.getMarkers()
-
- # Extended: Get the number of markers in this editor's buffer.
- #
- # Returns a {Number}.
- getMarkerCount: ->
- @buffer.getMarkerCount()
-
- # {Delegates to: DisplayBuffer.destroyMarker}
- destroyMarker: (args...) ->
- @displayBuffer.destroyMarker(args...)
-
- ###
- Section: Cursors
- ###
-
- # Essential: Get the position of the most recently added cursor in buffer
- # coordinates.
- #
- # Returns a {Point}
- getCursorBufferPosition: ->
- @getLastCursor().getBufferPosition()
-
- # Essential: Get the position of all the cursor positions in buffer coordinates.
- #
- # Returns {Array} of {Point}s in the order they were added
- getCursorBufferPositions: ->
- cursor.getBufferPosition() for cursor in @getCursors()
-
- # Essential: Move the cursor to the given position in buffer coordinates.
- #
- # If there are multiple cursors, they will be consolidated to a single cursor.
- #
- # * `position` A {Point} or {Array} of `[row, column]`
- # * `options` (optional) An {Object} combining options for {::clipScreenPosition} with:
- # * `autoscroll` Determines whether the editor scrolls to the new cursor's
- # position. Defaults to true.
- setCursorBufferPosition: (position, options) ->
- @moveCursors (cursor) -> cursor.setBufferPosition(position, options)
-
- # Essential: Get the position of the most recently added cursor in screen
- # coordinates.
- #
- # Returns a {Point}.
- getCursorScreenPosition: ->
- @getLastCursor().getScreenPosition()
-
- # Essential: Get the position of all the cursor positions in screen coordinates.
- #
- # Returns {Array} of {Point}s in the order the cursors were added
- getCursorScreenPositions: ->
- cursor.getScreenPosition() for cursor in @getCursors()
-
- # Essential: Move the cursor to the given position in screen coordinates.
- #
- # If there are multiple cursors, they will be consolidated to a single cursor.
- #
- # * `position` A {Point} or {Array} of `[row, column]`
- # * `options` (optional) An {Object} combining options for {::clipScreenPosition} with:
- # * `autoscroll` Determines whether the editor scrolls to the new cursor's
- # position. Defaults to true.
- setCursorScreenPosition: (position, options) ->
- @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) ->
- @markBufferPosition(bufferPosition, @getSelectionMarkerAttributes())
- @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false
- @getLastSelection().cursor
-
- # Essential: Add a cursor at the position in screen coordinates.
- #
- # * `screenPosition` A {Point} or {Array} of `[row, column]`
- #
- # Returns a {Cursor}.
- addCursorAtScreenPosition: (screenPosition, options) ->
- @markScreenPosition(screenPosition, @getSelectionMarkerAttributes())
- @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false
- @getLastSelection().cursor
-
- # Essential: Returns {Boolean} indicating whether or not there are multiple cursors.
- hasMultipleCursors: ->
- @getCursors().length > 1
-
- # Essential: Move every cursor up one row in screen coordinates.
- #
- # * `lineCount` (optional) {Number} number of lines to move
- moveUp: (lineCount) ->
- @moveCursors (cursor) -> cursor.moveUp(lineCount, moveToEndOfSelection: true)
-
- # Essential: Move every cursor down one row in screen coordinates.
- #
- # * `lineCount` (optional) {Number} number of lines to move
- moveDown: (lineCount) ->
- @moveCursors (cursor) -> cursor.moveDown(lineCount, moveToEndOfSelection: true)
-
- # Essential: Move every cursor left one column.
- #
- # * `columnCount` (optional) {Number} number of columns to move (default: 1)
- moveLeft: (columnCount) ->
- @moveCursors (cursor) -> cursor.moveLeft(columnCount, moveToEndOfSelection: true)
-
- # Essential: Move every cursor right one column.
- #
- # * `columnCount` (optional) {Number} number of columns to move (default: 1)
- moveRight: (columnCount) ->
- @moveCursors (cursor) -> cursor.moveRight(columnCount, moveToEndOfSelection: true)
-
- # Essential: Move every cursor to the beginning of its line in buffer coordinates.
- moveToBeginningOfLine: ->
- @moveCursors (cursor) -> cursor.moveToBeginningOfLine()
-
- # Essential: Move every cursor to the beginning of its line in screen coordinates.
- moveToBeginningOfScreenLine: ->
- @moveCursors (cursor) -> cursor.moveToBeginningOfScreenLine()
-
- # Essential: Move every cursor to the first non-whitespace character of its line.
- moveToFirstCharacterOfLine: ->
- @moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine()
-
- # Essential: Move every cursor to the end of its line in buffer coordinates.
- moveToEndOfLine: ->
- @moveCursors (cursor) -> cursor.moveToEndOfLine()
-
- # Essential: Move every cursor to the end of its line in screen coordinates.
- moveToEndOfScreenLine: ->
- @moveCursors (cursor) -> cursor.moveToEndOfScreenLine()
-
- # Essential: Move every cursor to the beginning of its surrounding word.
- moveToBeginningOfWord: ->
- @moveCursors (cursor) -> cursor.moveToBeginningOfWord()
-
- # Essential: Move every cursor to the end of its surrounding word.
- moveToEndOfWord: ->
- @moveCursors (cursor) -> cursor.moveToEndOfWord()
-
- # Cursor Extended
-
- # Extended: Move every cursor to the top of the buffer.
- #
- # If there are multiple cursors, they will be merged into a single cursor.
- moveToTop: ->
- @moveCursors (cursor) -> cursor.moveToTop()
-
- # Extended: Move every cursor to the bottom of the buffer.
- #
- # If there are multiple cursors, they will be merged into a single cursor.
- moveToBottom: ->
- @moveCursors (cursor) -> cursor.moveToBottom()
-
- # Extended: Move every cursor to the beginning of the next word.
- moveToBeginningOfNextWord: ->
- @moveCursors (cursor) -> cursor.moveToBeginningOfNextWord()
-
- # Extended: Move every cursor to the previous word boundary.
- moveToPreviousWordBoundary: ->
- @moveCursors (cursor) -> cursor.moveToPreviousWordBoundary()
-
- # Extended: Move every cursor to the next word boundary.
- moveToNextWordBoundary: ->
- @moveCursors (cursor) -> cursor.moveToNextWordBoundary()
-
- # Extended: Move every cursor to the beginning of the next paragraph.
- moveToBeginningOfNextParagraph: ->
- @moveCursors (cursor) -> cursor.moveToBeginningOfNextParagraph()
-
- # Extended: Move every cursor to the beginning of the previous paragraph.
- moveToBeginningOfPreviousParagraph: ->
- @moveCursors (cursor) -> cursor.moveToBeginningOfPreviousParagraph()
-
- # Extended: Returns the most recently added {Cursor}
- getLastCursor: ->
- _.last(@cursors)
-
- # Extended: Returns the word surrounding the most recently added cursor.
- #
- # * `options` (optional) See {Cursor::getBeginningOfCurrentWordBufferPosition}.
- getWordUnderCursor: (options) ->
- @getTextInBufferRange(@getLastCursor().getCurrentWordBufferRange(options))
-
- # Extended: Get an Array of all {Cursor}s.
- getCursors: ->
- @cursors.slice()
-
- # Extended: Get all {Cursors}s, ordered by their position in the buffer
- # instead of the order in which they were added.
- #
- # Returns an {Array} of {Selection}s.
- getCursorsOrderedByBufferPosition: ->
- @getCursors().sort (a, b) -> a.compare(b)
-
- # Add a cursor based on the given {Marker}.
- addCursor: (marker) ->
- cursor = new Cursor(editor: this, marker: marker)
- @cursors.push(cursor)
- @decorateMarker(marker, type: 'line-number', class: 'cursor-line')
- @decorateMarker(marker, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true)
- @decorateMarker(marker, type: 'line', class: 'cursor-line', onlyEmpty: true)
- @emit 'cursor-added', cursor if includeDeprecatedAPIs
- @emitter.emit 'did-add-cursor', cursor
- cursor
-
- # Remove the given cursor from this editor.
- removeCursor: (cursor) ->
- _.remove(@cursors, cursor)
- @emit 'cursor-removed', cursor if includeDeprecatedAPIs
- @emitter.emit 'did-remove-cursor', cursor
-
- moveCursors: (fn) ->
- fn(cursor) for cursor in @getCursors()
- @mergeCursors()
-
- cursorMoved: (event) ->
- @emit 'cursor-moved', event if includeDeprecatedAPIs
- @emitter.emit 'did-change-cursor-position', event
-
- # Merge cursors that have the same screen position
- mergeCursors: ->
- positions = {}
- for cursor in @getCursors()
- position = cursor.getBufferPosition().toString()
- if positions.hasOwnProperty(position)
- cursor.destroy()
- else
- positions[position] = true
- return
-
- preserveCursorPositionOnBufferReload: ->
- cursorPosition = null
- @disposables.add @buffer.onWillReload =>
- cursorPosition = @getCursorBufferPosition()
- @disposables.add @buffer.onDidReload =>
- @setCursorBufferPosition(cursorPosition) if cursorPosition
- cursorPosition = null
-
- ###
- Section: Selections
- ###
-
- # Essential: Get the selected text of the most recently added selection.
- #
- # Returns a {String}.
- getSelectedText: ->
- @getLastSelection().getText()
-
- # Essential: Get the {Range} of the most recently added selection in buffer
- # coordinates.
- #
- # Returns a {Range}.
- getSelectedBufferRange: ->
- @getLastSelection().getBufferRange()
-
- # Essential: Get the {Range}s of all selections in buffer coordinates.
- #
- # The ranges are sorted by when the selections were added. Most recent at the end.
- #
- # Returns an {Array} of {Range}s.
- getSelectedBufferRanges: ->
- selection.getBufferRange() for selection in @getSelections()
-
- # Essential: Set the selected range in buffer coordinates. If there are multiple
- # selections, they are reduced to a single selection with the given range.
- #
- # * `bufferRange` A {Range} or range-compatible {Array}.
- # * `options` (optional) An options {Object}:
- # * `reversed` A {Boolean} indicating whether to create the selection in a
- # reversed orientation.
- setSelectedBufferRange: (bufferRange, options) ->
- @setSelectedBufferRanges([bufferRange], options)
-
- # Essential: Set the selected ranges in buffer coordinates. If there are multiple
- # selections, they are replaced by new selections with the given ranges.
- #
- # * `bufferRanges` An {Array} of {Range}s or range-compatible {Array}s.
- # * `options` (optional) An options {Object}:
- # * `reversed` A {Boolean} indicating whether to create the selection in a
- # reversed orientation.
- setSelectedBufferRanges: (bufferRanges, options={}) ->
- throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length
-
- selections = @getSelections()
- selection.destroy() for selection in selections[bufferRanges.length...]
-
- @mergeIntersectingSelections options, =>
- for bufferRange, i in bufferRanges
- bufferRange = Range.fromObject(bufferRange)
- if selections[i]
- selections[i].setBufferRange(bufferRange, options)
- else
- @addSelectionForBufferRange(bufferRange, options)
- return
-
- # Essential: Get the {Range} of the most recently added selection in screen
- # coordinates.
- #
- # Returns a {Range}.
- getSelectedScreenRange: ->
- @getLastSelection().getScreenRange()
-
- # Essential: Get the {Range}s of all selections in screen coordinates.
- #
- # The ranges are sorted by when the selections were added. Most recent at the end.
- #
- # Returns an {Array} of {Range}s.
- getSelectedScreenRanges: ->
- selection.getScreenRange() for selection in @getSelections()
-
- # Essential: Set the selected range in screen coordinates. If there are multiple
- # selections, they are reduced to a single selection with the given range.
- #
- # * `screenRange` A {Range} or range-compatible {Array}.
- # * `options` (optional) An options {Object}:
- # * `reversed` A {Boolean} indicating whether to create the selection in a
- # reversed orientation.
- setSelectedScreenRange: (screenRange, options) ->
- @setSelectedBufferRange(@bufferRangeForScreenRange(screenRange, options), options)
-
- # Essential: Set the selected ranges in screen coordinates. If there are multiple
- # selections, they are replaced by new selections with the given ranges.
- #
- # * `screenRanges` An {Array} of {Range}s or range-compatible {Array}s.
- # * `options` (optional) An options {Object}:
- # * `reversed` A {Boolean} indicating whether to create the selection in a
- # reversed orientation.
- setSelectedScreenRanges: (screenRanges, options={}) ->
- throw new Error("Passed an empty array to setSelectedScreenRanges") unless screenRanges.length
-
- selections = @getSelections()
- selection.destroy() for selection in selections[screenRanges.length...]
-
- @mergeIntersectingSelections options, =>
- for screenRange, i in screenRanges
- screenRange = Range.fromObject(screenRange)
- if selections[i]
- selections[i].setScreenRange(screenRange, options)
- else
- @addSelectionForScreenRange(screenRange, options)
- return
-
- # Essential: Add a selection for the given range in buffer coordinates.
- #
- # * `bufferRange` A {Range}
- # * `options` (optional) An options {Object}:
- # * `reversed` A {Boolean} indicating whether to create the selection in a
- # reversed orientation.
- #
- # Returns the added {Selection}.
- addSelectionForBufferRange: (bufferRange, options={}) ->
- @markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options))
- @getLastSelection().autoscroll() unless options.autoscroll is false
- @getLastSelection()
-
- # Essential: Add a selection for the given range in screen coordinates.
- #
- # * `screenRange` A {Range}
- # * `options` (optional) An options {Object}:
- # * `reversed` A {Boolean} indicating whether to create the selection in a
- # reversed orientation.
- #
- # Returns the added {Selection}.
- addSelectionForScreenRange: (screenRange, options={}) ->
- @markScreenRange(screenRange, _.defaults(@getSelectionMarkerAttributes(), options))
- @getLastSelection().autoscroll() unless options.autoscroll is false
- @getLastSelection()
-
- # Essential: Select from the current cursor position to the given position in
- # buffer coordinates.
- #
- # This method may merge selections that end up intesecting.
- #
- # * `position` An instance of {Point}, with a given `row` and `column`.
- selectToBufferPosition: (position) ->
- lastSelection = @getLastSelection()
- lastSelection.selectToBufferPosition(position)
- @mergeIntersectingSelections(reversed: lastSelection.isReversed())
-
- # Essential: Select from the current cursor position to the given position in
- # screen coordinates.
- #
- # This method may merge selections that end up intesecting.
- #
- # * `position` An instance of {Point}, with a given `row` and `column`.
- selectToScreenPosition: (position) ->
- lastSelection = @getLastSelection()
- lastSelection.selectToScreenPosition(position)
- @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 intesecting.
- selectUp: (rowCount) ->
- @expandSelectionsBackward (selection) -> selection.selectUp(rowCount)
-
- # Essential: Move the cursor of each selection one character downward while
- # preserving the selection's tail position.
- #
- # * `rowCount` (optional) {Number} number of rows to select (default: 1)
- #
- # This method may merge selections that end up intesecting.
- selectDown: (rowCount) ->
- @expandSelectionsForward (selection) -> selection.selectDown(rowCount)
-
- # Essential: Move the cursor of each selection one character leftward while
- # preserving the selection's tail position.
- #
- # * `columnCount` (optional) {Number} number of columns to select (default: 1)
- #
- # This method may merge selections that end up intesecting.
- selectLeft: (columnCount) ->
- @expandSelectionsBackward (selection) -> selection.selectLeft(columnCount)
-
- # Essential: Move the cursor of each selection one character rightward while
- # preserving the selection's tail position.
- #
- # * `columnCount` (optional) {Number} number of columns to select (default: 1)
- #
- # This method may merge selections that end up intesecting.
- selectRight: (columnCount) ->
- @expandSelectionsForward (selection) -> selection.selectRight(columnCount)
-
- # Essential: Select from the top of the buffer to the end of the last selection
- # in the buffer.
- #
- # This method merges multiple selections into a single selection.
- selectToTop: ->
- @expandSelectionsBackward (selection) -> selection.selectToTop()
-
- # Essential: Selects from the top of the first selection in the buffer to the end
- # of the buffer.
- #
- # This method merges multiple selections into a single selection.
- selectToBottom: ->
- @expandSelectionsForward (selection) -> selection.selectToBottom()
-
- # Essential: Select all text in the buffer.
- #
- # This method merges multiple selections into a single selection.
- selectAll: ->
- @expandSelectionsForward (selection) -> selection.selectAll()
-
- # Essential: Move the cursor of each selection to the beginning of its line
- # while preserving the selection's tail position.
- #
- # This method may merge selections that end up intesecting.
- selectToBeginningOfLine: ->
- @expandSelectionsBackward (selection) -> selection.selectToBeginningOfLine()
-
- # Essential: Move the cursor of each selection to the first non-whitespace
- # character of its line while preserving the selection's tail position. If the
- # cursor is already on the first character of the line, move it to the
- # beginning of the line.
- #
- # This method may merge selections that end up intersecting.
- selectToFirstCharacterOfLine: ->
- @expandSelectionsBackward (selection) -> selection.selectToFirstCharacterOfLine()
-
- # Essential: Move the cursor of each selection to the end of its line while
- # preserving the selection's tail position.
- #
- # This method may merge selections that end up intersecting.
- selectToEndOfLine: ->
- @expandSelectionsForward (selection) -> selection.selectToEndOfLine()
-
- # Essential: Expand selections to the beginning of their containing word.
- #
- # Operates on all selections. Moves the cursor to the beginning of the
- # containing word while preserving the selection's tail position.
- selectToBeginningOfWord: ->
- @expandSelectionsBackward (selection) -> selection.selectToBeginningOfWord()
-
- # Essential: Expand selections to the end of their containing word.
- #
- # Operates on all selections. Moves the cursor to the end of the containing
- # word while preserving the selection's tail position.
- selectToEndOfWord: ->
- @expandSelectionsForward (selection) -> selection.selectToEndOfWord()
-
- # Essential: For each cursor, select the containing line.
- #
- # This method merges selections on successive lines.
- selectLinesContainingCursors: ->
- @expandSelectionsForward (selection) -> selection.selectLine()
-
- # Essential: Select the word surrounding each cursor.
- selectWordsContainingCursors: ->
- @expandSelectionsForward (selection) -> selection.selectWord()
-
- # Selection Extended
-
- # Extended: For each selection, move its cursor to the preceding word boundary
- # while maintaining the selection's tail position.
- #
- # This method may merge selections that end up intersecting.
- selectToPreviousWordBoundary: ->
- @expandSelectionsBackward (selection) -> selection.selectToPreviousWordBoundary()
-
- # Extended: For each selection, move its cursor to the next word boundary while
- # maintaining the selection's tail position.
- #
- # This method may merge selections that end up intersecting.
- selectToNextWordBoundary: ->
- @expandSelectionsForward (selection) -> selection.selectToNextWordBoundary()
-
- # Extended: Expand selections to the beginning of the next word.
- #
- # Operates on all selections. Moves the cursor to the beginning of the next
- # word while preserving the selection's tail position.
- selectToBeginningOfNextWord: ->
- @expandSelectionsForward (selection) -> selection.selectToBeginningOfNextWord()
-
- # Extended: Expand selections to the beginning of the next paragraph.
- #
- # Operates on all selections. Moves the cursor to the beginning of the next
- # paragraph while preserving the selection's tail position.
- selectToBeginningOfNextParagraph: ->
- @expandSelectionsForward (selection) -> selection.selectToBeginningOfNextParagraph()
-
- # Extended: Expand selections to the beginning of the next paragraph.
- #
- # Operates on all selections. Moves the cursor to the beginning of the next
- # paragraph while preserving the selection's tail position.
- selectToBeginningOfPreviousParagraph: ->
- @expandSelectionsBackward (selection) -> selection.selectToBeginningOfPreviousParagraph()
-
- # Extended: Select the range of the given marker if it is valid.
- #
- # * `marker` A {Marker}
- #
- # Returns the selected {Range} or `undefined` if the marker is invalid.
- selectMarker: (marker) ->
- if marker.isValid()
- range = marker.getBufferRange()
- @setSelectedBufferRange(range)
- range
-
- # Extended: Get the most recently added {Selection}.
- #
- # Returns a {Selection}.
- getLastSelection: ->
- _.last(@selections)
-
- # Extended: Get current {Selection}s.
- #
- # Returns: An {Array} of {Selection}s.
- getSelections: ->
- @selections.slice()
-
- # Extended: Get all {Selection}s, ordered by their position in the buffer
- # instead of the order in which they were added.
- #
- # Returns an {Array} of {Selection}s.
- getSelectionsOrderedByBufferPosition: ->
- @getSelections().sort (a, b) -> a.compare(b)
-
- # Extended: Determine if a given range in buffer coordinates intersects a
- # selection.
- #
- # * `bufferRange` A {Range} or range-compatible {Array}.
- #
- # Returns a {Boolean}.
- selectionIntersectsBufferRange: (bufferRange) ->
- _.any @getSelections(), (selection) ->
- selection.intersectsBufferRange(bufferRange)
-
- # Selections Private
-
- # Add a similarly-shaped selection to the next eligible line below
- # each selection.
- #
- # Operates on all selections. If the selection is empty, adds an empty
- # selection to the next following non-empty line as close to the current
- # selection's column as possible. If the selection is non-empty, adds a
- # selection to the next line that is long enough for a non-empty selection
- # starting at the same column as the current selection to be added to it.
- addSelectionBelow: ->
- @expandSelectionsForward (selection) -> selection.addSelectionBelow()
-
- # Add a similarly-shaped selection to the next eligible line above
- # each selection.
- #
- # Operates on all selections. If the selection is empty, adds an empty
- # selection to the next preceding non-empty line as close to the current
- # selection's column as possible. If the selection is non-empty, adds a
- # selection to the next line that is long enough for a non-empty selection
- # starting at the same column as the current selection to be added to it.
- addSelectionAbove: ->
- @expandSelectionsBackward (selection) -> selection.addSelectionAbove()
-
- # Calls the given function with each selection, then merges selections
- expandSelectionsForward: (fn) ->
- @mergeIntersectingSelections =>
- fn(selection) for selection in @getSelections()
- return
-
- # Calls the given function with each selection, then merges selections in the
- # reversed orientation
- expandSelectionsBackward: (fn) ->
- @mergeIntersectingSelections reversed: true, =>
- fn(selection) for selection in @getSelections()
- return
-
- finalizeSelections: ->
- selection.finalize() for selection in @getSelections()
- return
-
- selectionsForScreenRows: (startRow, endRow) ->
- @getSelections().filter (selection) -> selection.intersectsScreenRowRange(startRow, endRow)
-
- # Merges intersecting selections. If passed a function, it executes
- # the function with merging suppressed, then merges intersecting selections
- # afterward.
- mergeIntersectingSelections: (args...) ->
- @mergeSelections args..., (previousSelection, currentSelection) ->
- exclusive = not currentSelection.isEmpty() and not previousSelection.isEmpty()
-
- previousSelection.intersectsWith(currentSelection, exclusive)
-
- mergeSelectionsOnSameRows: (args...) ->
- @mergeSelections args..., (previousSelection, currentSelection) ->
- screenRange = currentSelection.getScreenRange()
-
- previousSelection.intersectsScreenRowRange(screenRange.start.row, screenRange.end.row)
-
- mergeSelections: (args...) ->
- mergePredicate = args.pop()
- fn = args.pop() if _.isFunction(_.last(args))
- options = args.pop() ? {}
-
- return fn?() if @suppressSelectionMerging
-
- if fn?
- @suppressSelectionMerging = true
- result = fn()
- @suppressSelectionMerging = false
-
- reducer = (disjointSelections, selection) ->
- adjacentSelection = _.last(disjointSelections)
- if mergePredicate(adjacentSelection, selection)
- adjacentSelection.merge(selection, options)
- disjointSelections
- else
- disjointSelections.concat([selection])
-
- [head, tail...] = @getSelectionsOrderedByBufferPosition()
- _.reduce(tail, reducer, [head])
- return result if fn?
-
- # Add a {Selection} based on the given {Marker}.
- #
- # * `marker` The {Marker} to highlight
- # * `options` (optional) An {Object} that pertains to the {Selection} constructor.
- #
- # Returns the new {Selection}.
- addSelection: (marker, options={}) ->
- unless marker.getProperties().preserveFolds
- @destroyFoldsContainingBufferRange(marker.getBufferRange())
- cursor = @addCursor(marker)
- selection = new Selection(_.extend({editor: this, marker, cursor}, options))
- @selections.push(selection)
- selectionBufferRange = selection.getBufferRange()
- @mergeIntersectingSelections(preserveFolds: marker.getProperties().preserveFolds)
-
- if selection.destroyed
- for selection in @getSelections()
- if selection.intersectsBufferRange(selectionBufferRange)
- return selection
- else
- @emit 'selection-added', selection if includeDeprecatedAPIs
- @emitter.emit 'did-add-selection', selection
- selection
-
- # Remove the given selection.
- removeSelection: (selection) ->
- _.remove(@selections, selection)
- @emit 'selection-removed', selection if includeDeprecatedAPIs
- @emitter.emit 'did-remove-selection', selection
-
- # Reduce one or more selections to a single empty selection based on the most
- # recently added cursor.
- clearSelections: (options) ->
- @consolidateSelections()
- @getLastSelection().clear(options)
-
- # Reduce multiple selections to the most recently added selection.
- consolidateSelections: ->
- selections = @getSelections()
- if selections.length > 1
- selection.destroy() for selection in selections[0...-1]
- true
- else
- false
-
- # Called by the selection
- selectionRangeChanged: (event) ->
- @emit 'selection-screen-range-changed', event if includeDeprecatedAPIs
- @emitter.emit 'did-change-selection-range', event
-
- ###
- 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.
- # * `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, iterator) -> @buffer.scan(regex, iterator)
-
- # Public: Scan regular expression matches in a given range, calling the given
- # iterator function on each match.
- #
- # * `regex` A {RegExp} to search for.
- # * `range` A {Range} in which to search.
- # * `iterator` A {Function} that's called on each match with an {Object}
- # containing the following keys:
- # * `match` The current regular expression match.
- # * `matchText` A {String} with the text of the match.
- # * `range` The {Range} of the match.
- # * `stop` Call this {Function} to terminate the scan.
- # * `replace` Call this {Function} with a {String} to replace the match.
- scanInBufferRange: (regex, range, iterator) -> @buffer.scanInRange(regex, range, iterator)
-
- # Public: Scan regular expression matches in a given range in reverse order,
- # calling the given iterator function on each match.
- #
- # * `regex` A {RegExp} to search for.
- # * `range` A {Range} in which to search.
- # * `iterator` A {Function} that's called on each match with an {Object}
- # containing the following keys:
- # * `match` The current regular expression match.
- # * `matchText` A {String} with the text of the match.
- # * `range` The {Range} of the match.
- # * `stop` Call this {Function} to terminate the scan.
- # * `replace` Call this {Function} with a {String} to replace the match.
- backwardsScanInBufferRange: (regex, range, iterator) -> @buffer.backwardsScanInRange(regex, range, iterator)
-
- ###
- Section: Tab Behavior
- ###
-
- # Essential: Returns a {Boolean} indicating whether softTabs are enabled for this
- # editor.
- getSoftTabs: -> @softTabs
-
- # Essential: Enable or disable soft tabs for this editor.
- #
- # * `softTabs` A {Boolean}
- setSoftTabs: (@softTabs) -> @softTabs
-
- # Essential: Toggle soft tabs for this editor
- toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs())
-
- # Essential: Get the on-screen length of tab characters.
- #
- # Returns a {Number}.
- getTabLength: -> @displayBuffer.getTabLength()
-
- # Essential: Set the on-screen length of tab characters. Setting this to a
- # {Number} This will override the `editor.tabLength` setting.
- #
- # * `tabLength` {Number} length of a single tab. Setting to `null` will
- # fallback to using the `editor.tabLength` config setting
- setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength)
-
- # 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: ->
- # FIXME Remove once this can be specified as a scoped setting in the
- # language-make package
- return false if @getGrammar().scopeName is 'source.makefile'
-
- for bufferRow in [0..@buffer.getLastRow()]
- continue if @displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
-
- line = @buffer.lineForRow(bufferRow)
- return true if line[0] is ' '
- return false if line[0] is '\t'
-
- undefined
-
- # Extended: Get the text representing a single level of indent.
- #
- # If soft tabs are enabled, the text is composed of N spaces, where N is the
- # tab length. Otherwise the text is a tab character (`\t`).
- #
- # Returns a {String}.
- getTabText: -> @buildIndentString(1)
-
- # If soft tabs are enabled, convert all hard tabs to soft tabs in the given
- # {Range}.
- normalizeTabsInBufferRange: (bufferRange) ->
- return unless @getSoftTabs()
- @scanInBufferRange /\t/g, bufferRange, ({replace}) => replace(@getTabText())
-
- ###
- Section: Soft Wrap Behavior
- ###
-
- # Essential: Determine whether lines in this editor are soft-wrapped.
- #
- # Returns a {Boolean}.
- isSoftWrapped: (softWrapped) -> @displayBuffer.isSoftWrapped()
-
- # Essential: Enable or disable soft wrapping for this editor.
- #
- # * `softWrapped` A {Boolean}
- #
- # Returns a {Boolean}.
- setSoftWrapped: (softWrapped) -> @displayBuffer.setSoftWrapped(softWrapped)
-
- # Essential: Toggle soft wrapping for this editor
- #
- # Returns a {Boolean}.
- toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped())
-
- # Public: Gets the column at which column will soft wrap
- getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn()
-
- ###
- Section: Indentation
- ###
-
- # Essential: Get the indentation level of the given a buffer row.
- #
- # Returns how deeply the given row is indented based on the soft tabs and
- # tab length settings of this editor. Note that if soft tabs are enabled and
- # the tab length is 2, a row with 4 leading spaces would have an indentation
- # level of 2.
- #
- # * `bufferRow` A {Number} indicating the buffer row.
- #
- # Returns a {Number}.
- indentationForBufferRow: (bufferRow) ->
- @indentLevelForLine(@lineTextForBufferRow(bufferRow))
-
- # Essential: Set the indentation level for the given buffer row.
- #
- # Inserts or removes hard tabs or spaces based on the soft tabs and tab length
- # settings of this editor in order to bring it to the given indentation level.
- # Note that if soft tabs are enabled and the tab length is 2, a row with 4
- # leading spaces would have an indentation level of 2.
- #
- # * `bufferRow` A {Number} indicating the buffer row.
- # * `newLevel` A {Number} indicating the new indentation level.
- # * `options` (optional) An {Object} with the following keys:
- # * `preserveLeadingWhitespace` `true` to preserve any whitespace already at
- # the beginning of the line (default: false).
- setIndentationForBufferRow: (bufferRow, newLevel, {preserveLeadingWhitespace}={}) ->
- if preserveLeadingWhitespace
- endColumn = 0
- else
- endColumn = @lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length
- newIndentString = @buildIndentString(newLevel)
- @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString)
-
- # Extended: Indent rows intersecting selections by one level.
- indentSelectedRows: ->
- @mutateSelectedText (selection) -> selection.indentSelectedRows()
-
- # Extended: Outdent rows intersecting selections by one level.
- outdentSelectedRows: ->
- @mutateSelectedText (selection) -> selection.outdentSelectedRows()
-
- # Extended: Get the indentation level of the given line of text.
- #
- # Returns 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) ->
- @displayBuffer.indentLevelForLine(line)
-
- # Extended: Indent rows intersecting selections based on the grammar's suggested
- # indent level.
- autoIndentSelectedRows: ->
- @mutateSelectedText (selection) -> selection.autoIndentSelectedRows()
-
- # Indent all lines intersecting selections. See {Selection::indent} for more
- # information.
- indent: (options={}) ->
- options.autoIndent ?= @shouldAutoIndent()
- @mutateSelectedText (selection) -> selection.indent(options)
-
- # Constructs the string used for tabs.
- buildIndentString: (number, column=0) ->
- if @getSoftTabs()
- tabStopViolation = column % @getTabLength()
- _.multiplyString(" ", Math.floor(number * @getTabLength()) - tabStopViolation)
- else
- _.multiplyString("\t", Math.floor(number))
-
- ###
- Section: Grammars
- ###
-
- # Essential: Get the current {Grammar} of this editor.
- getGrammar: ->
- @displayBuffer.getGrammar()
-
- # Essential: Set the current {Grammar} of this editor.
- #
- # Assigning a grammar will cause the editor to re-tokenize based on the new
- # grammar.
- #
- # * `grammar` {Grammar}
- setGrammar: (grammar) ->
- @displayBuffer.setGrammar(grammar)
-
- # Reload the grammar based on the file name.
- reloadGrammar: ->
- @displayBuffer.reloadGrammar()
-
- ###
- 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: ->
- @displayBuffer.getRootScopeDescriptor()
-
- # Essential: Get the syntactic scopeDescriptor for the given position in buffer
- # coordinates. Useful with {Config::get}.
- #
- # For example, if called with a position inside the parameter list of an
- # anonymous CoffeeScript function, the method returns the following array:
- # `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]`
- #
- # * `bufferPosition` A {Point} or {Array} of [row, column].
- #
- # Returns a {ScopeDescriptor}.
- scopeDescriptorForBufferPosition: (bufferPosition) ->
- @displayBuffer.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) ->
- @displayBuffer.bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition())
-
- # Extended: Determine if the given row is entirely a comment
- isBufferRowCommented: (bufferRow) ->
- if match = @lineTextForBufferRow(bufferRow).match(/\S/)
- @commentScopeSelector ?= new TextMateScopeSelector('comment.*')
- @commentScopeSelector.matches(@scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes)
-
- logCursorScope: ->
- scopeDescriptor = @getLastCursor().getScopeDescriptor()
- list = scopeDescriptor.scopes.toString().split(',')
- list = list.map (item) -> "* #{item}"
- content = "Scopes at Cursor\n#{list.join('\n')}"
-
- atom.notifications.addInfo(content, dismissable: true)
-
- # {Delegates to: DisplayBuffer.tokenForBufferPosition}
- tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition)
-
- ###
- Section: Clipboard Operations
- ###
-
- # Essential: For each selection, copy the selected text.
- copySelectedText: ->
- maintainClipboard = false
- for selection in @getSelectionsOrderedByBufferPosition()
- if selection.isEmpty()
- previousRange = selection.getBufferRange()
- selection.selectLine()
- selection.copy(maintainClipboard, true)
- selection.setBufferRange(previousRange)
- else
- selection.copy(maintainClipboard, false)
- maintainClipboard = true
- return
-
- # Essential: For each selection, cut the selected text.
- cutSelectedText: ->
- maintainClipboard = false
- @mutateSelectedText (selection) ->
- if selection.isEmpty()
- selection.selectLine()
- selection.cut(maintainClipboard, true)
- else
- selection.cut(maintainClipboard, false)
- maintainClipboard = true
-
- # Essential: For each selection, replace the selected text with the contents of
- # the clipboard.
- #
- # If the clipboard contains the same number of selections as the current
- # editor, each selection will be replaced with the content of the
- # corresponding clipboard selection text.
- #
- # * `options` (optional) See {Selection::insertText}.
- pasteText: (options={}) ->
- {text: clipboardText, metadata} = atom.clipboard.readWithMetadata()
- metadata ?= {}
- options.autoIndent = @shouldAutoIndentOnPaste()
-
- @mutateSelectedText (selection, index) =>
- if metadata.selections?.length is @getSelections().length
- {text, indentBasis, fullLine} = metadata.selections[index]
- else
- {indentBasis, fullLine} = metadata
- text = clipboardText
-
- delete options.indentBasis
- {cursor} = selection
- if indentBasis?
- containsNewlines = text.indexOf('\n') isnt -1
- if containsNewlines or not cursor.hasPrecedingCharactersOnLine()
- options.indentBasis ?= indentBasis
-
- if fullLine and selection.isEmpty()
- oldPosition = selection.getBufferRange().start
- selection.setBufferRange([[oldPosition.row, 0], [oldPosition.row, 0]])
- selection.insertText(text, options)
- newPosition = oldPosition.translate([1, 0])
- selection.setBufferRange([newPosition, newPosition])
- else
- selection.insertText(text, options)
-
- # Public: For each selection, if the selection is empty, cut all characters
- # of the containing line following the cursor. Otherwise cut the selected
- # text.
- cutToEndOfLine: ->
- maintainClipboard = false
- @mutateSelectedText (selection) ->
- selection.cutToEndOfLine(maintainClipboard)
- maintainClipboard = true
-
- ###
- Section: Folds
- ###
-
- # Essential: Fold the most recent cursor's row based on its indentation level.
- #
- # The fold will extend from the nearest preceding line with a lower
- # indentation level up to the nearest following row with a lower indentation
- # level.
- foldCurrentRow: ->
- bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row
- @foldBufferRow(bufferRow)
-
- # Essential: Unfold the most recent cursor's row by one level.
- unfoldCurrentRow: ->
- bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row
- @unfoldBufferRow(bufferRow)
-
- # 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) ->
- @languageMode.foldBufferRow(bufferRow)
-
- # Essential: Unfold all folds containing the given row in buffer coordinates.
- #
- # * `bufferRow` A {Number}
- unfoldBufferRow: (bufferRow) ->
- @displayBuffer.unfoldBufferRow(bufferRow)
-
- # Extended: For each selection, fold the rows it intersects.
- foldSelectedLines: ->
- selection.fold() for selection in @getSelections()
- return
-
- # Extended: Fold all foldable lines.
- foldAll: ->
- @languageMode.foldAll()
-
- # Extended: Unfold all existing folds.
- unfoldAll: ->
- @languageMode.unfoldAll()
-
- # Extended: Fold all foldable lines at the given indent level.
- #
- # * `level` A {Number}.
- foldAllAtIndentLevel: (level) ->
- @languageMode.foldAllAtIndentLevel(level)
-
- # 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) ->
- # @languageMode.isFoldableAtBufferRow(bufferRow)
- @displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow)?.foldable ? false
-
- # 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) ->
- bufferRow = @displayBuffer.bufferRowForScreenRow(screenRow)
- @isFoldableAtBufferRow(bufferRow)
-
- # Extended: Fold the given buffer row if it isn't currently folded, and unfold
- # it otherwise.
- toggleFoldAtBufferRow: (bufferRow) ->
- if @isFoldedAtBufferRow(bufferRow)
- @unfoldBufferRow(bufferRow)
- else
- @foldBufferRow(bufferRow)
-
- # Extended: Determine whether the most recently added cursor's row is folded.
- #
- # Returns a {Boolean}.
- isFoldedAtCursorRow: ->
- @isFoldedAtScreenRow(@getCursorScreenPosition().row)
-
- # Extended: Determine whether the given row in buffer coordinates is folded.
- #
- # * `bufferRow` A {Number}
- #
- # Returns a {Boolean}.
- isFoldedAtBufferRow: (bufferRow) ->
- @displayBuffer.isFoldedAtBufferRow(bufferRow)
-
- # Extended: Determine whether the given row in screen coordinates is folded.
- #
- # * `screenRow` A {Number}
- #
- # Returns a {Boolean}.
- isFoldedAtScreenRow: (screenRow) ->
- @displayBuffer.isFoldedAtScreenRow(screenRow)
-
- # TODO: Rename to foldRowRange?
- createFold: (startRow, endRow) ->
- @displayBuffer.createFold(startRow, endRow)
-
- # {Delegates to: DisplayBuffer.destroyFoldWithId}
- destroyFoldWithId: (id) ->
- @displayBuffer.destroyFoldWithId(id)
-
- # Remove any {Fold}s found that intersect the given buffer range.
- destroyFoldsIntersectingBufferRange: (bufferRange) ->
- @destroyFoldsContainingBufferRange(bufferRange)
-
- for row in [bufferRange.end.row..bufferRange.start.row]
- fold.destroy() for fold in @displayBuffer.foldsStartingAtBufferRow(row)
-
- return
-
- # Remove any {Fold}s found that contain the given buffer range.
- destroyFoldsContainingBufferRange: (bufferRange) ->
- @unfoldBufferRow(bufferRange.start.row)
- @unfoldBufferRow(bufferRange.end.row)
-
- # {Delegates to: DisplayBuffer.largestFoldContainingBufferRow}
- largestFoldContainingBufferRow: (bufferRow) ->
- @displayBuffer.largestFoldContainingBufferRow(bufferRow)
-
- # {Delegates to: DisplayBuffer.largestFoldStartingAtScreenRow}
- largestFoldStartingAtScreenRow: (screenRow) ->
- @displayBuffer.largestFoldStartingAtScreenRow(screenRow)
-
- # {Delegates to: DisplayBuffer.outermostFoldsForBufferRowRange}
- outermostFoldsInBufferRowRange: (startRow, endRow) ->
- @displayBuffer.outermostFoldsInBufferRowRange(startRow, endRow)
-
- ###
- Section: Scrolling the TextEditor
- ###
-
- # Essential: Scroll the editor to reveal the most recently added cursor if it is
- # off-screen.
- #
- # * `options` (optional) {Object}
- # * `center` Center the editor around the cursor if possible. (default: true)
- scrollToCursorPosition: (options) ->
- @getLastCursor().autoscroll(center: options?.center ? true)
-
- # Essential: Scrolls the editor to the given buffer position.
- #
- # * `bufferPosition` An object that represents a buffer position. It can be either
- # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
- # * `options` (optional) {Object}
- # * `center` Center the editor around the position if possible. (default: false)
- scrollToBufferPosition: (bufferPosition, options) ->
- @displayBuffer.scrollToBufferPosition(bufferPosition, options)
-
- # Essential: Scrolls the editor to the given screen position.
- #
- # * `screenPosition` 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)
- scrollToScreenPosition: (screenPosition, options) ->
- @displayBuffer.scrollToScreenPosition(screenPosition, options)
-
- # Essential: Scrolls the editor to the top
- scrollToTop: ->
- @setScrollTop(0)
-
- # Essential: Scrolls the editor to the bottom
- scrollToBottom: ->
- @setScrollBottom(Infinity)
-
- scrollToScreenRange: (screenRange, options) -> @displayBuffer.scrollToScreenRange(screenRange, options)
-
- horizontallyScrollable: -> @displayBuffer.horizontallyScrollable()
-
- verticallyScrollable: -> @displayBuffer.verticallyScrollable()
-
- getHorizontalScrollbarHeight: -> @displayBuffer.getHorizontalScrollbarHeight()
- setHorizontalScrollbarHeight: (height) -> @displayBuffer.setHorizontalScrollbarHeight(height)
-
- getVerticalScrollbarWidth: -> @displayBuffer.getVerticalScrollbarWidth()
- setVerticalScrollbarWidth: (width) -> @displayBuffer.setVerticalScrollbarWidth(width)
-
- pageUp: ->
- newScrollTop = @getScrollTop() - @getHeight()
- @moveUp(@getRowsPerPage())
- @setScrollTop(newScrollTop)
-
- pageDown: ->
- newScrollTop = @getScrollTop() + @getHeight()
- @moveDown(@getRowsPerPage())
- @setScrollTop(newScrollTop)
-
- selectPageUp: ->
- @selectUp(@getRowsPerPage())
-
- selectPageDown: ->
- @selectDown(@getRowsPerPage())
-
- # Returns the number of rows per page
- getRowsPerPage: ->
- Math.max(1, Math.ceil(@getHeight() / @getLineHeightInPixels()))
-
- ###
- Section: Config
- ###
-
- shouldAutoIndent: ->
- atom.config.get("editor.autoIndent", scope: @getRootScopeDescriptor())
-
- shouldAutoIndentOnPaste: ->
- atom.config.get("editor.autoIndentOnPaste", scope: @getRootScopeDescriptor())
-
- ###
- Section: Event Handlers
- ###
-
- handleTokenization: ->
- @softTabs = @usesSoftTabs() ? @softTabs
-
- handleGrammarChange: ->
- @unfoldAll()
- @emit 'grammar-changed' if includeDeprecatedAPIs
- @emitter.emit 'did-change-grammar', @getGrammar()
-
- handleMarkerCreated: (marker) =>
- if marker.matchesProperties(@getSelectionMarkerAttributes())
- @addSelection(marker)
-
- ###
- Section: TextEditor Rendering
- ###
-
- # Public: Retrieves the greyed out placeholder of a mini editor.
- #
- # Returns a {String}.
- getPlaceholderText: ->
- @placeholderText
-
- # Public: 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) ->
- return if @placeholderText is placeholderText
- @placeholderText = placeholderText
- @emitter.emit 'did-change-placeholder-text', @placeholderText
-
- getFirstVisibleScreenRow: (suppressDeprecation) ->
- unless suppressDeprecation
- deprecate("This is now a view method. Call TextEditorElement::getFirstVisibleScreenRow instead.")
- @getVisibleRowRange()[0]
-
- getLastVisibleScreenRow: (suppressDeprecation) ->
- unless suppressDeprecation
- deprecate("This is now a view method. Call TextEditorElement::getLastVisibleScreenRow instead.")
- @getVisibleRowRange()[1]
-
- pixelPositionForBufferPosition: (bufferPosition, suppressDeprecation) ->
- unless suppressDeprecation
- deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead")
- @displayBuffer.pixelPositionForBufferPosition(bufferPosition)
-
- pixelPositionForScreenPosition: (screenPosition, suppressDeprecation) ->
- unless suppressDeprecation
- deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead")
- @displayBuffer.pixelPositionForScreenPosition(screenPosition)
-
- getSelectionMarkerAttributes: ->
- type: 'selection', editorId: @id, invalidate: 'never'
-
- getVerticalScrollMargin: -> @displayBuffer.getVerticalScrollMargin()
- setVerticalScrollMargin: (verticalScrollMargin) -> @displayBuffer.setVerticalScrollMargin(verticalScrollMargin)
-
- getHorizontalScrollMargin: -> @displayBuffer.getHorizontalScrollMargin()
- setHorizontalScrollMargin: (horizontalScrollMargin) -> @displayBuffer.setHorizontalScrollMargin(horizontalScrollMargin)
-
- getLineHeightInPixels: -> @displayBuffer.getLineHeightInPixels()
- setLineHeightInPixels: (lineHeightInPixels) -> @displayBuffer.setLineHeightInPixels(lineHeightInPixels)
-
- batchCharacterMeasurement: (fn) -> @displayBuffer.batchCharacterMeasurement(fn)
-
- getScopedCharWidth: (scopeNames, char) -> @displayBuffer.getScopedCharWidth(scopeNames, char)
- setScopedCharWidth: (scopeNames, char, width) -> @displayBuffer.setScopedCharWidth(scopeNames, char, width)
-
- getScopedCharWidths: (scopeNames) -> @displayBuffer.getScopedCharWidths(scopeNames)
-
- clearScopedCharWidths: -> @displayBuffer.clearScopedCharWidths()
-
- getDefaultCharWidth: -> @displayBuffer.getDefaultCharWidth()
- setDefaultCharWidth: (defaultCharWidth) -> @displayBuffer.setDefaultCharWidth(defaultCharWidth)
-
- setHeight: (height) -> @displayBuffer.setHeight(height)
- getHeight: -> @displayBuffer.getHeight()
-
- getClientHeight: -> @displayBuffer.getClientHeight()
-
- setWidth: (width) -> @displayBuffer.setWidth(width)
- getWidth: -> @displayBuffer.getWidth()
-
- getScrollTop: -> @displayBuffer.getScrollTop()
- setScrollTop: (scrollTop) -> @displayBuffer.setScrollTop(scrollTop)
-
- getScrollBottom: -> @displayBuffer.getScrollBottom()
- setScrollBottom: (scrollBottom) -> @displayBuffer.setScrollBottom(scrollBottom)
-
- getScrollLeft: -> @displayBuffer.getScrollLeft()
- setScrollLeft: (scrollLeft) -> @displayBuffer.setScrollLeft(scrollLeft)
-
- getScrollRight: -> @displayBuffer.getScrollRight()
- setScrollRight: (scrollRight) -> @displayBuffer.setScrollRight(scrollRight)
-
- getScrollHeight: -> @displayBuffer.getScrollHeight()
- getScrollWidth: -> @displayBuffer.getScrollWidth()
-
- getVisibleRowRange: -> @displayBuffer.getVisibleRowRange()
-
- intersectsVisibleRowRange: (startRow, endRow) -> @displayBuffer.intersectsVisibleRowRange(startRow, endRow)
-
- selectionIntersectsVisibleRowRange: (selection) -> @displayBuffer.selectionIntersectsVisibleRowRange(selection)
-
- screenPositionForPixelPosition: (pixelPosition) -> @displayBuffer.screenPositionForPixelPosition(pixelPosition)
-
- pixelRectForScreenRange: (screenRange) -> @displayBuffer.pixelRectForScreenRange(screenRange)
-
- ###
- Section: Utility
- ###
-
- inspect: ->
- ""
-
- logScreenLines: (start, end) -> @displayBuffer.logLines(start, end)
-
-if includeDeprecatedAPIs
- TextEditor.delegatesProperties '$lineHeightInPixels', '$defaultCharWidth', '$height', '$width',
- '$verticalScrollbarWidth', '$horizontalScrollbarHeight', '$scrollTop', '$scrollLeft',
- toProperty: 'displayBuffer'
-
- TextEditor::getViewClass = ->
- require './text-editor-view'
-
- TextEditor::joinLine = ->
- deprecate("Use TextEditor::joinLines() instead")
- @joinLines()
-
- TextEditor::scopesAtCursor = ->
- deprecate 'Use editor.getLastCursor().getScopeDescriptor() instead'
- @getLastCursor().getScopeDescriptor().getScopesArray()
-
- TextEditor::getCursorScopes = ->
- deprecate 'Use editor.getLastCursor().getScopeDescriptor() instead'
- @scopesAtCursor()
-
- TextEditor::getUri = ->
- deprecate("Use `::getURI` instead")
- @getURI()
-
- TextEditor::lineForBufferRow = (bufferRow) ->
- deprecate 'Use TextEditor::lineTextForBufferRow(bufferRow) instead'
- @lineTextForBufferRow(bufferRow)
-
- TextEditor::lineForScreenRow = (screenRow) ->
- deprecate "TextEditor::tokenizedLineForScreenRow(bufferRow) is the new name. But it's private. Try to use TextEditor::lineTextForScreenRow instead"
- @tokenizedLineForScreenRow(screenRow)
-
- TextEditor::linesForScreenRows = (start, end) ->
- deprecate "Use TextEditor::tokenizedLinesForScreenRows instead"
- @tokenizedLinesForScreenRows(start, end)
-
- TextEditor::lineLengthForBufferRow = (row) ->
- deprecate "Use editor.lineTextForBufferRow(row).length instead"
- @lineTextForBufferRow(row).length
-
- TextEditor::duplicateLine = ->
- deprecate("Use TextEditor::duplicateLines() instead")
- @duplicateLines()
-
- TextEditor::scopesForBufferPosition = (bufferPosition) ->
- deprecate 'Use ::scopeDescriptorForBufferPosition instead. The return value has changed! It now returns a `ScopeDescriptor`'
- @scopeDescriptorForBufferPosition(bufferPosition).getScopesArray()
-
- TextEditor::toggleSoftWrap = ->
- deprecate("Use TextEditor::toggleSoftWrapped instead")
- @toggleSoftWrapped()
-
- TextEditor::setSoftWrap = (softWrapped) ->
- deprecate("Use TextEditor::setSoftWrapped instead")
- @setSoftWrapped(softWrapped)
-
- TextEditor::backspaceToBeginningOfWord = ->
- deprecate("Use TextEditor::deleteToBeginningOfWord() instead")
- @deleteToBeginningOfWord()
-
- TextEditor::backspaceToBeginningOfLine = ->
- deprecate("Use TextEditor::deleteToBeginningOfLine() instead")
- @deleteToBeginningOfLine()
-
- TextEditor::getGutterDecorations = (propertyFilter) ->
- deprecate("Use ::getLineNumberDecorations instead")
- @getLineNumberDecorations(propertyFilter)
-
- TextEditor::getCursorScreenRow = ->
- deprecate('Use `editor.getCursorScreenPosition().row` instead')
- @getCursorScreenPosition().row
-
- TextEditor::moveCursorUp = (lineCount) ->
- deprecate("Use TextEditor::moveUp() instead")
- @moveUp(lineCount)
-
- TextEditor::moveCursorDown = (lineCount) ->
- deprecate("Use TextEditor::moveDown() instead")
- @moveDown(lineCount)
-
- TextEditor::moveCursorLeft = ->
- deprecate("Use TextEditor::moveLeft() instead")
- @moveLeft()
-
- TextEditor::moveCursorRight = ->
- deprecate("Use TextEditor::moveRight() instead")
- @moveRight()
-
- TextEditor::moveCursorToBeginningOfLine = ->
- deprecate("Use TextEditor::moveToBeginningOfLine() instead")
- @moveToBeginningOfLine()
-
- TextEditor::moveCursorToBeginningOfScreenLine = ->
- deprecate("Use TextEditor::moveToBeginningOfScreenLine() instead")
- @moveToBeginningOfScreenLine()
-
- TextEditor::moveCursorToFirstCharacterOfLine = ->
- deprecate("Use TextEditor::moveToFirstCharacterOfLine() instead")
- @moveToFirstCharacterOfLine()
-
- TextEditor::moveCursorToEndOfLine = ->
- deprecate("Use TextEditor::moveToEndOfLine() instead")
- @moveToEndOfLine()
-
- TextEditor::moveCursorToEndOfScreenLine = ->
- deprecate("Use TextEditor::moveToEndOfScreenLine() instead")
- @moveToEndOfScreenLine()
-
- TextEditor::moveCursorToBeginningOfWord = ->
- deprecate("Use TextEditor::moveToBeginningOfWord() instead")
- @moveToBeginningOfWord()
-
- TextEditor::moveCursorToEndOfWord = ->
- deprecate("Use TextEditor::moveToEndOfWord() instead")
- @moveToEndOfWord()
-
- TextEditor::moveCursorToTop = ->
- deprecate("Use TextEditor::moveToTop() instead")
- @moveToTop()
-
- TextEditor::moveCursorToBottom = ->
- deprecate("Use TextEditor::moveToBottom() instead")
- @moveToBottom()
-
- TextEditor::moveCursorToBeginningOfNextWord = ->
- deprecate("Use TextEditor::moveToBeginningOfNextWord() instead")
- @moveToBeginningOfNextWord()
-
- TextEditor::moveCursorToPreviousWordBoundary = ->
- deprecate("Use TextEditor::moveToPreviousWordBoundary() instead")
- @moveToPreviousWordBoundary()
-
- TextEditor::moveCursorToNextWordBoundary = ->
- deprecate("Use TextEditor::moveToNextWordBoundary() instead")
- @moveToNextWordBoundary()
-
- TextEditor::moveCursorToBeginningOfNextParagraph = ->
- deprecate("Use TextEditor::moveToBeginningOfNextParagraph() instead")
- @moveToBeginningOfNextParagraph()
-
- TextEditor::moveCursorToBeginningOfPreviousParagraph = ->
- deprecate("Use TextEditor::moveToBeginningOfPreviousParagraph() instead")
- @moveToBeginningOfPreviousParagraph()
-
- TextEditor::getCursor = ->
- deprecate("Use TextEditor::getLastCursor() instead")
- @getLastCursor()
-
- TextEditor::selectLine = ->
- deprecate('Use TextEditor::selectLinesContainingCursors instead')
- @selectLinesContainingCursors()
-
- TextEditor::selectWord = ->
- deprecate('Use TextEditor::selectWordsContainingCursors instead')
- @selectWordsContainingCursors()
-
- TextEditor::getSelection = (index) ->
- if index?
- deprecate("Use TextEditor::getSelections()[index] instead when getting a specific selection")
- @getSelections()[index]
- else
- deprecate("Use TextEditor::getLastSelection() instead")
- @getLastSelection()
-
- TextEditor::getSoftWrapped = ->
- deprecate("Use TextEditor::isSoftWrapped instead")
- @displayBuffer.isSoftWrapped()
-
- EmitterMixin = require('emissary').Emitter
- TextEditor::on = (eventName) ->
- switch eventName
- when 'title-changed'
- deprecate("Use TextEditor::onDidChangeTitle instead")
- when 'path-changed'
- deprecate("Use TextEditor::onDidChangePath instead")
- when 'modified-status-changed'
- deprecate("Use TextEditor::onDidChangeModified instead")
- when 'soft-wrap-changed'
- deprecate("Use TextEditor::onDidChangeSoftWrapped instead")
- when 'grammar-changed'
- deprecate("Use TextEditor::onDidChangeGrammar instead")
- when 'character-widths-changed'
- deprecate("Use TextEditor::onDidChangeCharacterWidths instead")
- when 'contents-modified'
- deprecate("Use TextEditor::onDidStopChanging instead")
- when 'contents-conflicted'
- deprecate("Use TextEditor::onDidConflict instead")
-
- when 'will-insert-text'
- deprecate("Use TextEditor::onWillInsertText instead")
- when 'did-insert-text'
- deprecate("Use TextEditor::onDidInsertText instead")
-
- when 'cursor-added'
- deprecate("Use TextEditor::onDidAddCursor instead")
- when 'cursor-removed'
- deprecate("Use TextEditor::onDidRemoveCursor instead")
- when 'cursor-moved'
- deprecate("Use TextEditor::onDidChangeCursorPosition instead")
-
- when 'selection-added'
- deprecate("Use TextEditor::onDidAddSelection instead")
- when 'selection-removed'
- deprecate("Use TextEditor::onDidRemoveSelection instead")
- when 'selection-screen-range-changed'
- deprecate("Use TextEditor::onDidChangeSelectionRange instead")
-
- when 'decoration-added'
- deprecate("Use TextEditor::onDidAddDecoration instead")
- when 'decoration-removed'
- deprecate("Use TextEditor::onDidRemoveDecoration instead")
- when 'decoration-updated'
- deprecate("Use Decoration::onDidChangeProperties instead. You will get the decoration back from `TextEditor::decorateMarker()`")
- when 'decoration-changed'
- deprecate("Use Marker::onDidChange instead. e.g. `editor::decorateMarker(...).getMarker().onDidChange()`")
-
- when 'screen-lines-changed'
- deprecate("Use TextEditor::onDidChange instead")
-
- when 'scroll-top-changed'
- deprecate("Use TextEditor::onDidChangeScrollTop instead")
- when 'scroll-left-changed'
- deprecate("Use TextEditor::onDidChangeScrollLeft instead")
-
- else
- deprecate("TextEditor::on is deprecated. Use documented event subscription methods instead.")
-
- EmitterMixin::on.apply(this, arguments)
diff --git a/src/text-editor.js b/src/text-editor.js
new file mode 100644
index 00000000000..ed6b0e68f8d
--- /dev/null
+++ b/src/text-editor.js
@@ -0,0 +1,5855 @@
+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();
+ }
+}
diff --git a/src/text-mate-language-mode.js b/src/text-mate-language-mode.js
new file mode 100644
index 00000000000..13d5ed13f0e
--- /dev/null
+++ b/src/text-mate-language-mode.js
@@ -0,0 +1,1014 @@
+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;
diff --git a/src/text-utils.coffee b/src/text-utils.coffee
deleted file mode 100644
index 37955dcd6ae..00000000000
--- a/src/text-utils.coffee
+++ /dev/null
@@ -1,73 +0,0 @@
-isHighSurrogate = (charCode) ->
- 0xD800 <= charCode <= 0xDBFF
-
-isLowSurrogate = (charCode) ->
- 0xDC00 <= charCode <= 0xDFFF
-
-isVariationSelector = (charCode) ->
- 0xFE00 <= charCode <= 0xFE0F
-
-isCombiningCharacter = (charCode) ->
- 0x0300 <= charCode <= 0x036F or
- 0x1AB0 <= charCode <= 0x1AFF or
- 0x1DC0 <= charCode <= 0x1DFF or
- 0x20D0 <= charCode <= 0x20FF or
- 0xFE20 <= charCode <= 0xFE2F
-
-# Are the given character codes a high/low surrogate pair?
-#
-# * `charCodeA` The first character code {Number}.
-# * `charCode2` The second character code {Number}.
-#
-# Return a {Boolean}.
-isSurrogatePair = (charCodeA, charCodeB) ->
- isHighSurrogate(charCodeA) and isLowSurrogate(charCodeB)
-
-# Are the given character codes a variation sequence?
-#
-# * `charCodeA` The first character code {Number}.
-# * `charCode2` The second character code {Number}.
-#
-# Return a {Boolean}.
-isVariationSequence = (charCodeA, charCodeB) ->
- not isVariationSelector(charCodeA) and isVariationSelector(charCodeB)
-
-# Are the given character codes a combined character pair?
-#
-# * `charCodeA` The first character code {Number}.
-# * `charCode2` The second character code {Number}.
-#
-# Return a {Boolean}.
-isCombinedCharacter = (charCodeA, charCodeB) ->
- not isCombiningCharacter(charCodeA) and isCombiningCharacter(charCodeB)
-
-# Is the character at the given index the start of high/low surrogate pair
-# a variation sequence, or a combined character?
-#
-# * `string` The {String} to check for a surrogate pair, variation sequence,
-# or combined character.
-# * `index` The {Number} index to look for a surrogate pair, variation
-# sequence, or combined character.
-#
-# Return a {Boolean}.
-isPairedCharacter = (string, index=0) ->
- charCodeA = string.charCodeAt(index)
- charCodeB = string.charCodeAt(index + 1)
- isSurrogatePair(charCodeA, charCodeB) or
- isVariationSequence(charCodeA, charCodeB) or
- isCombinedCharacter(charCodeA, charCodeB)
-
-# Does the given string contain at least surrogate pair, variation sequence,
-# or combined character?
-#
-# * `string` The {String} to check for the presence of paired characters.
-#
-# Returns a {Boolean}.
-hasPairedCharacter = (string) ->
- index = 0
- while index < string.length
- return true if isPairedCharacter(string, index)
- index++
- false
-
-module.exports = {isPairedCharacter, hasPairedCharacter}
diff --git a/src/text-utils.js b/src/text-utils.js
new file mode 100644
index 00000000000..d5f9e23b484
--- /dev/null
+++ b/src/text-utils.js
@@ -0,0 +1,141 @@
+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
+};
diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee
deleted file mode 100644
index 01bb9652a8d..00000000000
--- a/src/theme-manager.coffee
+++ /dev/null
@@ -1,383 +0,0 @@
-path = require 'path'
-_ = require 'underscore-plus'
-{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
-{File} = require 'pathwatcher'
-fs = require 'fs-plus'
-Q = require 'q'
-Grim = require 'grim'
-
-# 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, @resourcePath, @configDirPath, @safeMode}) ->
- @emitter = new Emitter
- @styleSheetDisposablesBySourcePath = {}
- @lessCache = null
- @initialLoadComplete = false
- @packageManager.registerPackageActivator(this, ['theme'])
- @sheetsByStyleElement = new WeakMap
-
- stylesElement = document.head.querySelector('atom-styles')
- stylesElement.onDidAddStyleElement @styleElementAdded.bind(this)
- stylesElement.onDidRemoveStyleElement @styleElementRemoved.bind(this)
- stylesElement.onDidUpdateStyleElement @styleElementUpdated.bind(this)
-
- styleElementAdded: (styleElement) ->
- {sheet} = styleElement
- @sheetsByStyleElement.set(styleElement, sheet)
- @emit 'stylesheet-added', sheet if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-add-stylesheet', sheet
- @emit 'stylesheets-changed' if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-change-stylesheets'
-
- styleElementRemoved: (styleElement) ->
- sheet = @sheetsByStyleElement.get(styleElement)
- @emit 'stylesheet-removed', sheet if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-remove-stylesheet', sheet
- @emit 'stylesheets-changed' if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-change-stylesheets'
-
- styleElementUpdated: ({sheet}) ->
- @emit 'stylesheet-removed', sheet if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-remove-stylesheet', sheet
- @emit 'stylesheet-added', sheet if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-add-stylesheet', sheet
- @emit 'stylesheets-changed' if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-change-stylesheets'
-
- ###
- Section: Event Subscription
- ###
-
- # Essential: Invoke `callback` when style sheet changes associated with
- # updating the list of active themes have completed.
- #
- # * `callback` {Function}
- onDidChangeActiveThemes: (callback) ->
- @emitter.on 'did-change-active-themes', callback
- @emitter.on 'did-reload-all', callback # TODO: Remove once deprecated pre-1.0 APIs are gone
-
- ###
- Section: Accessing Available Themes
- ###
-
- getAvailableNames: ->
- # TODO: Maybe should change to list all the available themes out there?
- @getLoadedNames()
-
- ###
- Section: Accessing Loaded Themes
- ###
-
- # Public: Get an array of all the loaded theme names.
- getLoadedThemeNames: ->
- theme.name for theme in @getLoadedThemes()
-
- # Public: Get an array of all the loaded themes.
- getLoadedThemes: ->
- pack for pack in @packageManager.getLoadedPackages() when pack.isTheme()
-
- ###
- Section: Accessing Active Themes
- ###
-
- # Public: Get an array of all the active theme names.
- getActiveThemeNames: ->
- theme.name for theme in @getActiveThemes()
-
- # Public: Get an array of all the active themes.
- getActiveThemes: ->
- pack for pack in @packageManager.getActivePackages() when pack.isTheme()
-
- activatePackages: -> @activateThemes()
-
- ###
- Section: Managing Enabled Themes
- ###
-
- # Public: Get the enabled theme names from the config.
- #
- # Returns an array of theme names in the order that they should be activated.
- getEnabledThemeNames: ->
- themeNames = atom.config.get('core.themes') ? []
- themeNames = [themeNames] unless _.isArray(themeNames)
- themeNames = themeNames.filter (themeName) ->
- if themeName and typeof themeName is 'string'
- return true if atom.packages.resolvePackagePath(themeName)
- console.warn("Enabled theme '#{themeName}' is not installed.")
- false
-
- # Use a built-in syntax and UI theme any time the configured themes are not
- # available.
- if themeNames.length < 2
- builtInThemeNames = [
- 'atom-dark-syntax'
- 'atom-dark-ui'
- 'atom-light-syntax'
- 'atom-light-ui'
- 'base16-tomorrow-dark-theme'
- 'base16-tomorrow-light-theme'
- 'solarized-dark-syntax'
- 'solarized-light-syntax'
- ]
- themeNames = _.intersection(themeNames, builtInThemeNames)
- if themeNames.length is 0
- themeNames = ['atom-dark-syntax', 'atom-dark-ui']
- else if themeNames.length is 1
- if _.endsWith(themeNames[0], '-ui')
- themeNames.unshift('atom-dark-syntax')
- else
- themeNames.push('atom-dark-ui')
-
- # Reverse so the first (top) theme is loaded after the others. We want
- # the first/top theme to override later themes in the stack.
- themeNames.reverse()
-
- ###
- Section: Private
- ###
-
- # Resolve and apply the stylesheet specified by the path.
- #
- # This supports both CSS and Less stylsheets.
- #
- # * `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) ->
- if fullPath = @resolveStylesheet(stylesheetPath)
- content = @loadStylesheet(fullPath)
- @applyStylesheet(fullPath, content)
- else
- throw new Error("Could not find a file at path '#{stylesheetPath}'")
-
- unwatchUserStylesheet: ->
- @userStylsheetSubscriptions?.dispose()
- @userStylsheetSubscriptions = null
- @userStylesheetFile = null
- @userStyleSheetDisposable?.dispose()
- @userStyleSheetDisposable = null
-
- loadUserStylesheet: ->
- @unwatchUserStylesheet()
-
- userStylesheetPath = atom.styles.getUserStyleSheetPath()
- return unless fs.isFileSync(userStylesheetPath)
-
- try
- @userStylesheetFile = new File(userStylesheetPath)
- @userStylsheetSubscriptions = new CompositeDisposable()
- reloadStylesheet = => @loadUserStylesheet()
- @userStylsheetSubscriptions.add(@userStylesheetFile.onDidChange(reloadStylesheet))
- @userStylsheetSubscriptions.add(@userStylesheetFile.onDidRename(reloadStylesheet))
- @userStylsheetSubscriptions.add(@userStylesheetFile.onDidDelete(reloadStylesheet))
- catch error
- message = """
- Unable to watch path: `#{path.basename(userStylesheetPath)}`. Make sure
- you have permissions to `#{userStylesheetPath}`.
-
- On linux there are currently problems with watch sizes. See
- [this document][watches] for more info.
- [watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path
- """
- atom.notifications.addError(message, dismissable: true)
-
- try
- userStylesheetContents = @loadStylesheet(userStylesheetPath, true)
- catch
- return
-
- @userStyleSheetDisposable = atom.styles.addStyleSheet(userStylesheetContents, sourcePath: userStylesheetPath, priority: 2)
-
- loadBaseStylesheets: ->
- @requireStylesheet('../static/bootstrap')
- @reloadBaseStylesheets()
-
- reloadBaseStylesheets: ->
- @requireStylesheet('../static/atom')
- if nativeStylesheetPath = fs.resolveOnLoadPath(process.platform, ['css', 'less'])
- @requireStylesheet(nativeStylesheetPath)
-
- stylesheetElementForId: (id) ->
- document.head.querySelector("atom-styles style[source-path=\"#{id}\"]")
-
- resolveStylesheet: (stylesheetPath) ->
- if path.extname(stylesheetPath).length > 0
- fs.resolveOnLoadPath(stylesheetPath)
- else
- fs.resolveOnLoadPath(stylesheetPath, ['css', 'less'])
-
- loadStylesheet: (stylesheetPath, importFallbackVariables) ->
- if path.extname(stylesheetPath) is '.less'
- @loadLessStylesheet(stylesheetPath, importFallbackVariables)
- else
- fs.readFileSync(stylesheetPath, 'utf8')
-
- loadLessStylesheet: (lessStylesheetPath, importFallbackVariables=false) ->
- unless @lessCache?
- LessCompileCache = require './less-compile-cache'
- @lessCache = new LessCompileCache({@resourcePath, importPaths: @getImportPaths()})
-
- try
- if importFallbackVariables
- baseVarImports = """
- @import "variables/ui-variables";
- @import "variables/syntax-variables";
- """
- less = fs.readFileSync(lessStylesheetPath, 'utf8')
- @lessCache.cssForFile(lessStylesheetPath, [baseVarImports, less].join('\n'))
- else
- @lessCache.read(lessStylesheetPath)
- catch error
- error.less = true
- if error.line?
- # Adjust line numbers for import fallbacks
- error.line -= 2 if importFallbackVariables
-
- message = "Error compiling Less stylesheet: `#{lessStylesheetPath}`"
- detail = """
- Line number: #{error.line}
- #{error.message}
- """
- else
- message = "Error loading Less stylesheet: `#{lessStylesheetPath}`"
- detail = error.message
-
- atom.notifications.addError(message, {detail, dismissable: true})
- throw error
-
- removeStylesheet: (stylesheetPath) ->
- @styleSheetDisposablesBySourcePath[stylesheetPath]?.dispose()
-
- applyStylesheet: (path, text) ->
- @styleSheetDisposablesBySourcePath[path] = atom.styles.addStyleSheet(text, sourcePath: path)
-
- stringToId: (string) ->
- string.replace(/\\/g, '/')
-
- activateThemes: ->
- deferred = Q.defer()
-
- # atom.config.observe runs the callback once, then on subsequent changes.
- atom.config.observe 'core.themes', =>
- @deactivateThemes()
-
- @refreshLessCache() # Update cache for packages in core.themes config
-
- promises = []
- for themeName in @getEnabledThemeNames()
- if @packageManager.resolvePackagePath(themeName)
- promises.push(@packageManager.activatePackage(themeName))
- else
- console.warn("Failed to activate theme '#{themeName}' because it isn't installed.")
-
- Q.all(promises).then =>
- @addActiveThemeClasses()
- @refreshLessCache() # Update cache again now that @getActiveThemes() is populated
- @loadUserStylesheet()
- @reloadBaseStylesheets()
- @initialLoadComplete = true
- @emit 'reloaded' if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-change-active-themes'
- deferred.resolve()
-
- deferred.promise
-
- deactivateThemes: ->
- @removeActiveThemeClasses()
- @unwatchUserStylesheet()
- @packageManager.deactivatePackage(pack.name) for pack in @getActiveThemes()
- null
-
- isInitialLoadComplete: -> @initialLoadComplete
-
- addActiveThemeClasses: ->
- workspaceElement = atom.views.getView(atom.workspace)
- for pack in @getActiveThemes()
- workspaceElement.classList.add("theme-#{pack.name}")
- return
-
- removeActiveThemeClasses: ->
- workspaceElement = atom.views.getView(atom.workspace)
- for pack in @getActiveThemes()
- workspaceElement.classList.remove("theme-#{pack.name}")
- return
-
- refreshLessCache: ->
- @lessCache?.setImportPaths(@getImportPaths())
-
- getImportPaths: ->
- activeThemes = @getActiveThemes()
- if activeThemes.length > 0
- themePaths = (theme.getStylesheetsPath() for theme in activeThemes when theme)
- else
- themePaths = []
- for themeName in @getEnabledThemeNames()
- if themePath = @packageManager.resolvePackagePath(themeName)
- deprecatedPath = path.join(themePath, 'stylesheets')
- if fs.isDirectorySync(deprecatedPath)
- themePaths.push(deprecatedPath)
- else
- themePaths.push(path.join(themePath, 'styles'))
-
- themePaths.filter (themePath) -> fs.isDirectorySync(themePath)
-
-if Grim.includeDeprecatedAPIs
- EmitterMixin = require('emissary').Emitter
- EmitterMixin.includeInto(ThemeManager)
-
- ThemeManager::on = (eventName) ->
- switch eventName
- when 'reloaded'
- Grim.deprecate 'Use ThemeManager::onDidChangeActiveThemes instead'
- when 'stylesheet-added'
- Grim.deprecate 'Use ThemeManager::onDidAddStylesheet instead'
- when 'stylesheet-removed'
- Grim.deprecate 'Use ThemeManager::onDidRemoveStylesheet instead'
- when 'stylesheet-updated'
- Grim.deprecate 'Use ThemeManager::onDidUpdateStylesheet instead'
- when 'stylesheets-changed'
- Grim.deprecate 'Use ThemeManager::onDidChangeStylesheets instead'
- else
- Grim.deprecate 'ThemeManager::on is deprecated. Use event subscription methods instead.'
- EmitterMixin::on.apply(this, arguments)
-
- ThemeManager::onDidReloadAll = (callback) ->
- Grim.deprecate("Use `::onDidChangeActiveThemes` instead.")
- @onDidChangeActiveThemes(callback)
-
- ThemeManager::onDidAddStylesheet = (callback) ->
- Grim.deprecate("Use atom.styles.onDidAddStyleElement instead")
- @emitter.on 'did-add-stylesheet', callback
-
- ThemeManager::onDidRemoveStylesheet = (callback) ->
- Grim.deprecate("Use atom.styles.onDidRemoveStyleElement instead")
- @emitter.on 'did-remove-stylesheet', callback
-
- ThemeManager::onDidUpdateStylesheet = (callback) ->
- Grim.deprecate("Use atom.styles.onDidUpdateStyleElement instead")
- @emitter.on 'did-update-stylesheet', callback
-
- ThemeManager::onDidChangeStylesheets = (callback) ->
- Grim.deprecate("Use atom.styles.onDidAdd/RemoveStyleElement instead")
- @emitter.on 'did-change-stylesheets', callback
-
- ThemeManager::getUserStylesheetPath = ->
- Grim.deprecate("Call atom.styles.getUserStyleSheetPath() instead")
- atom.styles.getUserStyleSheetPath()
-
- ThemeManager::getLoadedNames = ->
- Grim.deprecate("Use `::getLoadedThemeNames` instead.")
- @getLoadedThemeNames()
-
- ThemeManager::getActiveNames = ->
- Grim.deprecate("Use `::getActiveThemeNames` instead.")
- @getActiveThemeNames()
-
- ThemeManager::setEnabledThemes = (enabledThemeNames) ->
- Grim.deprecate("Use `atom.config.set('core.themes', arrayOfThemeNames)` instead")
- atom.config.set('core.themes', enabledThemeNames)
diff --git a/src/theme-manager.js b/src/theme-manager.js
new file mode 100644
index 00000000000..a25d08937ca
--- /dev/null
+++ b/src/theme-manager.js
@@ -0,0 +1,464 @@
+/* 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));
+ }
+};
diff --git a/src/theme-package.coffee b/src/theme-package.coffee
deleted file mode 100644
index f14750d2fb2..00000000000
--- a/src/theme-package.coffee
+++ /dev/null
@@ -1,31 +0,0 @@
-Q = require 'q'
-Package = require './package'
-
-module.exports =
-class ThemePackage extends Package
- getType: -> 'theme'
-
- getStyleSheetPriority: -> 1
-
- enable: ->
- atom.config.unshiftAtKeyPath('core.themes', @name)
-
- disable: ->
- atom.config.removeAtKeyPath('core.themes', @name)
-
- load: ->
- @loadTime = 0
- this
-
- activate: ->
- return @activationDeferred.promise if @activationDeferred?
-
- @activationDeferred = Q.defer()
- @measure 'activateTime', =>
- try
- @loadStylesheets()
- @activateNow()
- catch error
- @handleError("Failed to activate the #{@name} theme", error)
-
- @activationDeferred.promise
diff --git a/src/theme-package.js b/src/theme-package.js
new file mode 100644
index 00000000000..b9abb9d92f1
--- /dev/null
+++ b/src/theme-package.js
@@ -0,0 +1,57 @@
+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;
+ }
+};
diff --git a/src/title-bar.js b/src/title-bar.js
new file mode 100644
index 00000000000..267c92356d8
--- /dev/null
+++ b/src/title-bar.js
@@ -0,0 +1,53 @@
+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);
+ }
+};
diff --git a/src/token-iterator.coffee b/src/token-iterator.coffee
deleted file mode 100644
index 202b044ba35..00000000000
--- a/src/token-iterator.coffee
+++ /dev/null
@@ -1,83 +0,0 @@
-{SoftTab, HardTab, PairedCharacter, SoftWrapIndent} = require './special-token-symbols'
-
-module.exports =
-class TokenIterator
- constructor: (line) ->
- @reset(line) if line?
-
- reset: (@line) ->
- @index = null
- @bufferStart = @line.startBufferColumn
- @bufferEnd = @bufferStart
- @screenStart = 0
- @screenEnd = 0
- @scopes = @line.openScopes.map (id) -> atom.grammars.scopeForId(id)
- @scopeStarts = @scopes.slice()
- @scopeEnds = []
- this
-
- next: ->
- {tags} = @line
-
- if @index?
- @index++
- @scopeEnds.length = 0
- @scopeStarts.length = 0
- @bufferStart = @bufferEnd
- @screenStart = @screenEnd
- else
- @index = 0
-
- while @index < tags.length
- tag = tags[@index]
- if tag < 0
- if tag % 2 is 0
- @scopeEnds.push(atom.grammars.scopeForId(tag + 1))
- @scopes.pop()
- else
- scope = atom.grammars.scopeForId(tag)
- @scopeStarts.push(scope)
- @scopes.push(scope)
- @index++
- else
- if @isHardTab()
- @screenEnd = @screenStart + tag
- @bufferEnd = @bufferStart + 1
- else if @isSoftWrapIndentation()
- @screenEnd = @screenStart + tag
- @bufferEnd = @bufferStart + 0
- else
- @screenEnd = @screenStart + tag
- @bufferEnd = @bufferStart + tag
- return true
-
- false
-
- getBufferStart: -> @bufferStart
- getBufferEnd: -> @bufferEnd
-
- getScreenStart: -> @screenStart
- getScreenEnd: -> @screenEnd
-
- getScopeStarts: -> @scopeStarts
- getScopeEnds: -> @scopeEnds
-
- getScopes: -> @scopes
-
- getText: ->
- @line.text.substring(@screenStart, @screenEnd)
-
- isSoftTab: ->
- @line.specialTokens[@index] is SoftTab
-
- isHardTab: ->
- @line.specialTokens[@index] is HardTab
-
- isSoftWrapIndentation: ->
- @line.specialTokens[@index] is SoftWrapIndent
-
- isPairedCharacter: ->
- @line.specialTokens[@index] is PairedCharacter
-
- isAtomic: ->
- @isSoftTab() or @isHardTab() or @isSoftWrapIndentation() or @isPairedCharacter()
diff --git a/src/token-iterator.js b/src/token-iterator.js
new file mode 100644
index 00000000000..c9fcfe81e9b
--- /dev/null
+++ b/src/token-iterator.js
@@ -0,0 +1,80 @@
+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;
+ }
+};
diff --git a/src/token.coffee b/src/token.coffee
index 60e8194f884..902f54fa208 100644
--- a/src/token.coffee
+++ b/src/token.coffee
@@ -1,47 +1,25 @@
_ = require 'underscore-plus'
StartDotRegex = /^\.?/
-WhitespaceRegex = /\S/
# Represents a single unit of text as selected by a grammar.
module.exports =
class Token
value: null
- hasPairedCharacter: false
scopes: null
- isAtomic: null
- isHardTab: null
- firstNonWhitespaceIndex: null
- firstTrailingWhitespaceIndex: null
- hasInvisibleCharacters: false
constructor: (properties) ->
- {@value, @scopes, @isAtomic, @isHardTab, @bufferDelta} = properties
- {@hasInvisibleCharacters, @hasPairedCharacter, @isSoftWrapIndentation} = properties
- @firstNonWhitespaceIndex = properties.firstNonWhitespaceIndex ? null
- @firstTrailingWhitespaceIndex = properties.firstTrailingWhitespaceIndex ? null
-
- @screenDelta = @value.length
- @bufferDelta ?= @screenDelta
+ {@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) and !!@isAtomic is !!other.isAtomic
+ @value is other.value and _.isEqual(@scopes, other.scopes)
isBracket: ->
/^meta\.brace\b/.test(_.last(@scopes))
- isOnlyWhitespace: ->
- not WhitespaceRegex.test(@value)
-
matchesScopeSelector: (selector) ->
targetClasses = selector.replace(StartDotRegex, '').split('.')
_.any @scopes, (scope) ->
scopeClasses = scope.split('.')
_.isSubset(targetClasses, scopeClasses)
-
- hasLeadingWhitespace: ->
- @firstNonWhitespaceIndex? and @firstNonWhitespaceIndex > 0
-
- hasTrailingWhitespace: ->
- @firstTrailingWhitespaceIndex? and @firstTrailingWhitespaceIndex < @value.length
diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee
deleted file mode 100644
index 60ebe16f094..00000000000
--- a/src/tokenized-buffer.coffee
+++ /dev/null
@@ -1,506 +0,0 @@
-_ = require 'underscore-plus'
-{CompositeDisposable, Emitter} = require 'event-kit'
-{Point, Range} = require 'text-buffer'
-{ScopeSelector} = require 'first-mate'
-Serializable = require 'serializable'
-Model = require './model'
-TokenizedLine = require './tokenized-line'
-TokenIterator = require './token-iterator'
-Token = require './token'
-ScopeDescriptor = require './scope-descriptor'
-Grim = require 'grim'
-
-module.exports =
-class TokenizedBuffer extends Model
- Serializable.includeInto(this)
-
- grammar: null
- currentGrammarScore: null
- buffer: null
- tabLength: null
- tokenizedLines: null
- chunkSize: 50
- invalidRows: null
- visible: false
- configSettings: null
-
- constructor: ({@buffer, @tabLength, @ignoreInvisibles}) ->
- @emitter = new Emitter
- @disposables = new CompositeDisposable
- @tokenIterator = new TokenIterator
-
- @disposables.add atom.grammars.onDidAddGrammar(@grammarAddedOrUpdated)
- @disposables.add atom.grammars.onDidUpdateGrammar(@grammarAddedOrUpdated)
-
- @disposables.add @buffer.preemptDidChange (e) => @handleBufferChange(e)
- @disposables.add @buffer.onDidChangePath (@bufferPath) => @reloadGrammar()
-
- @reloadGrammar()
-
- destroyed: ->
- @disposables.dispose()
-
- serializeParams: ->
- bufferPath: @buffer.getPath()
- tabLength: @tabLength
- ignoreInvisibles: @ignoreInvisibles
-
- deserializeParams: (params) ->
- params.buffer = atom.project.bufferForPathSync(params.bufferPath)
- params
-
- observeGrammar: (callback) ->
- callback(@grammar)
- @onDidChangeGrammar(callback)
-
- onDidChangeGrammar: (callback) ->
- @emitter.on 'did-change-grammar', callback
-
- onDidChange: (callback) ->
- @emitter.on 'did-change', callback
-
- onDidTokenize: (callback) ->
- @emitter.on 'did-tokenize', callback
-
- grammarAddedOrUpdated: (grammar) =>
- if grammar.injectionSelector?
- @retokenizeLines() if @hasTokenForSelector(grammar.injectionSelector)
- else
- newScore = grammar.getScore(@buffer.getPath(), @buffer.getText())
- @setGrammar(grammar, newScore) if newScore > @currentGrammarScore
-
- setGrammar: (grammar, score) ->
- return if grammar is @grammar
-
- @grammar = grammar
- @rootScopeDescriptor = new ScopeDescriptor(scopes: [@grammar.scopeName])
- @currentGrammarScore = score ? grammar.getScore(@buffer.getPath(), @buffer.getText())
-
- @grammarUpdateDisposable?.dispose()
- @grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines()
- @disposables.add(@grammarUpdateDisposable)
-
- scopeOptions = {scope: @rootScopeDescriptor}
- @configSettings =
- tabLength: atom.config.get('editor.tabLength', scopeOptions)
- invisibles: atom.config.get('editor.invisibles', scopeOptions)
- showInvisibles: atom.config.get('editor.showInvisibles', scopeOptions)
-
- if @configSubscriptions?
- @configSubscriptions.dispose()
- @disposables.remove(@configSubscriptions)
- @configSubscriptions = new CompositeDisposable
- @configSubscriptions.add atom.config.onDidChange 'editor.tabLength', scopeOptions, ({newValue}) =>
- @configSettings.tabLength = newValue
- @retokenizeLines()
- ['invisibles', 'showInvisibles'].forEach (key) =>
- @configSubscriptions.add atom.config.onDidChange "editor.#{key}", scopeOptions, ({newValue}) =>
- oldInvisibles = @getInvisiblesToShow()
- @configSettings[key] = newValue
- @retokenizeLines() unless _.isEqual(@getInvisiblesToShow(), oldInvisibles)
- @disposables.add(@configSubscriptions)
-
- @retokenizeLines()
-
- @emit 'grammar-changed', grammar if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-change-grammar', grammar
-
- reloadGrammar: ->
- if grammar = atom.grammars.selectGrammar(@buffer.getPath(), @buffer.getText())
- @setGrammar(grammar)
- else
- throw new Error("No grammar found for path: #{path}")
-
- hasTokenForSelector: (selector) ->
- for {tokens} in @tokenizedLines
- for token in tokens
- return true if selector.matches(token.scopes)
- false
-
- retokenizeLines: ->
- lastRow = @buffer.getLastRow()
- @tokenizedLines = @buildPlaceholderTokenizedLinesForRows(0, lastRow)
- @invalidRows = []
- @invalidateRow(0)
- @fullyTokenized = false
- event = {start: 0, end: lastRow, delta: 0}
- @emit 'changed', event if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-change', event
-
- setVisible: (@visible) ->
- @tokenizeInBackground() if @visible
-
- getTabLength: ->
- @tabLength ? @configSettings.tabLength
-
- setTabLength: (tabLength) ->
- return if tabLength is @tabLength
-
- @tabLength = tabLength
- @retokenizeLines()
-
- setIgnoreInvisibles: (ignoreInvisibles) ->
- if ignoreInvisibles isnt @ignoreInvisibles
- @ignoreInvisibles = ignoreInvisibles
- if @configSettings.showInvisibles and @configSettings.invisibles?
- @retokenizeLines()
-
- tokenizeInBackground: ->
- return if not @visible or @pendingChunk or not @isAlive()
-
- @pendingChunk = true
- _.defer =>
- @pendingChunk = false
- @tokenizeNextChunk() if @isAlive() and @buffer.isAlive()
-
- tokenizeNextChunk: ->
- # Short circuit null grammar which can just use the placeholder tokens
- if @grammar is atom.grammars.nullGrammar and @firstInvalidRow()?
- @invalidRows = []
- @markTokenizationComplete()
- return
-
- rowsRemaining = @chunkSize
-
- while @firstInvalidRow()? and rowsRemaining > 0
- startRow = @invalidRows.shift()
- lastRow = @getLastRow()
- continue if startRow > lastRow
-
- row = startRow
- loop
- previousStack = @stackForRow(row)
- @tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1), @openScopesForRow(row))
- if --rowsRemaining is 0
- filledRegion = false
- endRow = row
- break
- if row is lastRow or _.isEqual(@stackForRow(row), previousStack)
- filledRegion = true
- endRow = row
- break
- row++
-
- @validateRow(endRow)
- @invalidateRow(endRow + 1) unless filledRegion
-
- [startRow, endRow] = @updateFoldableStatus(startRow, endRow)
-
- event = {start: startRow, end: endRow, delta: 0}
- @emit 'changed', event if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-change', event
-
- if @firstInvalidRow()?
- @tokenizeInBackground()
- else
- @markTokenizationComplete()
-
- markTokenizationComplete: ->
- unless @fullyTokenized
- @emit 'tokenized' if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-tokenize'
- @fullyTokenized = true
-
- firstInvalidRow: ->
- @invalidRows[0]
-
- validateRow: (row) ->
- @invalidRows.shift() while @invalidRows[0] <= row
- return
-
- invalidateRow: (row) ->
- @invalidRows.push(row)
- @invalidRows.sort (a, b) -> a - b
- @tokenizeInBackground()
-
- updateInvalidRows: (start, end, delta) ->
- @invalidRows = @invalidRows.map (row) ->
- if row < start
- row
- else if start <= row <= end
- end + delta + 1
- else if row > end
- row + delta
-
- handleBufferChange: (e) ->
- {oldRange, newRange} = e
- start = oldRange.start.row
- end = oldRange.end.row
- delta = newRange.end.row - oldRange.end.row
-
- @updateInvalidRows(start, end, delta)
- previousEndStack = @stackForRow(end) # used in spill detection below
- newTokenizedLines = @buildTokenizedLinesForRows(start, end + delta, @stackForRow(start - 1), @openScopesForRow(start))
- _.spliceWithArray(@tokenizedLines, start, end - start + 1, newTokenizedLines)
-
- start = @retokenizeWhitespaceRowsIfIndentLevelChanged(start - 1, -1)
- end = @retokenizeWhitespaceRowsIfIndentLevelChanged(newRange.end.row + 1, 1) - delta
-
- newEndStack = @stackForRow(end + delta)
- if newEndStack and not _.isEqual(newEndStack, previousEndStack)
- @invalidateRow(end + delta + 1)
-
- [start, end] = @updateFoldableStatus(start, end + delta)
- end -= delta
-
- event = {start, end, delta, bufferChange: e}
- @emit 'changed', event if Grim.includeDeprecatedAPIs
- @emitter.emit 'did-change', event
-
- retokenizeWhitespaceRowsIfIndentLevelChanged: (row, increment) ->
- line = @tokenizedLines[row]
- if line?.isOnlyWhitespace() and @indentLevelForRow(row) isnt line.indentLevel
- while line?.isOnlyWhitespace()
- @tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1), @openScopesForRow(row))
- row += increment
- line = @tokenizedLines[row]
-
- row - increment
-
- updateFoldableStatus: (startRow, endRow) ->
- scanStartRow = @buffer.previousNonBlankRow(startRow) ? startRow
- scanStartRow-- while scanStartRow > 0 and @tokenizedLineForRow(scanStartRow).isComment()
- scanEndRow = @buffer.nextNonBlankRow(endRow) ? endRow
-
- for row in [scanStartRow..scanEndRow] by 1
- foldable = @isFoldableAtRow(row)
- line = @tokenizedLineForRow(row)
- unless line.foldable is foldable
- line.foldable = foldable
- startRow = Math.min(startRow, row)
- endRow = Math.max(endRow, row)
-
- [startRow, endRow]
-
- isFoldableAtRow: (row) ->
- @isFoldableCodeAtRow(row) or @isFoldableCommentAtRow(row)
-
- # Returns a {Boolean} indicating whether the given buffer row starts
- # a a foldable row range due to the code's indentation patterns.
- isFoldableCodeAtRow: (row) ->
- return false if @buffer.isRowBlank(row) or @tokenizedLineForRow(row).isComment()
- nextRow = @buffer.nextNonBlankRow(row)
- return false unless nextRow?
-
- @indentLevelForRow(nextRow) > @indentLevelForRow(row)
-
- isFoldableCommentAtRow: (row) ->
- previousRow = row - 1
- nextRow = row + 1
- return false if nextRow > @buffer.getLastRow()
-
- (row is 0 or not @tokenizedLineForRow(previousRow).isComment()) and
- @tokenizedLineForRow(row).isComment() and
- @tokenizedLineForRow(nextRow).isComment()
-
- buildTokenizedLinesForRows: (startRow, endRow, startingStack, startingopenScopes) ->
- ruleStack = startingStack
- openScopes = startingopenScopes
- stopTokenizingAt = startRow + @chunkSize
- tokenizedLines = for row in [startRow..endRow]
- if (ruleStack or row is 0) and row < stopTokenizingAt
- tokenizedLine = @buildTokenizedLineForRow(row, ruleStack, openScopes)
- ruleStack = tokenizedLine.ruleStack
- openScopes = @scopesFromTags(openScopes, tokenizedLine.tags)
- else
- tokenizedLine = @buildPlaceholderTokenizedLineForRow(row, openScopes)
- tokenizedLine
-
- if endRow >= stopTokenizingAt
- @invalidateRow(stopTokenizingAt)
- @tokenizeInBackground()
-
- tokenizedLines
-
- buildPlaceholderTokenizedLinesForRows: (startRow, endRow) ->
- @buildPlaceholderTokenizedLineForRow(row) for row in [startRow..endRow]
-
- buildPlaceholderTokenizedLineForRow: (row) ->
- openScopes = [@grammar.startIdForScope(@grammar.scopeName)]
- text = @buffer.lineForRow(row)
- tags = [text.length]
- tabLength = @getTabLength()
- indentLevel = @indentLevelForRow(row)
- lineEnding = @buffer.lineEndingForRow(row)
- new TokenizedLine({openScopes, text, tags, tabLength, indentLevel, invisibles: @getInvisiblesToShow(), lineEnding, @tokenIterator})
-
- buildTokenizedLineForRow: (row, ruleStack, openScopes) ->
- @buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack, openScopes)
-
- buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) ->
- lineEnding = @buffer.lineEndingForRow(row)
- tabLength = @getTabLength()
- indentLevel = @indentLevelForRow(row)
- {tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false)
- new TokenizedLine({openScopes, text, tags, ruleStack, tabLength, lineEnding, indentLevel, invisibles: @getInvisiblesToShow(), @tokenIterator})
-
- getInvisiblesToShow: ->
- if @configSettings.showInvisibles and not @ignoreInvisibles
- @configSettings.invisibles
- else
- null
-
- tokenizedLineForRow: (bufferRow) ->
- @tokenizedLines[bufferRow]
-
- stackForRow: (bufferRow) ->
- @tokenizedLines[bufferRow]?.ruleStack
-
- openScopesForRow: (bufferRow) ->
- if bufferRow > 0
- precedingLine = @tokenizedLines[bufferRow - 1]
- @scopesFromTags(precedingLine.openScopes, precedingLine.tags)
- else
- []
-
- scopesFromTags: (startingScopes, tags) ->
- scopes = startingScopes.slice()
- for tag in tags when tag < 0
- if (tag % 2) is -1
- scopes.push(tag)
- else
- expectedScope = tag + 1
- poppedScope = scopes.pop()
- unless poppedScope is expectedScope
- throw new Error("Encountered an invalid scope end id. Popped #{poppedScope}, expected to pop #{expectedScope}.")
- scopes
-
- indentLevelForRow: (bufferRow) ->
- line = @buffer.lineForRow(bufferRow)
- indentLevel = 0
-
- if line is ''
- nextRow = bufferRow + 1
- lineCount = @getLineCount()
- while nextRow < lineCount
- nextLine = @buffer.lineForRow(nextRow)
- unless nextLine is ''
- indentLevel = Math.ceil(@indentLevelForLine(nextLine))
- break
- nextRow++
-
- previousRow = bufferRow - 1
- while previousRow >= 0
- previousLine = @buffer.lineForRow(previousRow)
- unless previousLine is ''
- indentLevel = Math.max(Math.ceil(@indentLevelForLine(previousLine)), indentLevel)
- break
- previousRow--
-
- indentLevel
- else
- @indentLevelForLine(line)
-
- indentLevelForLine: (line) ->
- if match = line.match(/^[\t ]+/)
- leadingWhitespace = match[0]
- tabCount = leadingWhitespace.match(/\t/g)?.length ? 0
- spaceCount = leadingWhitespace.match(/[ ]/g)?.length ? 0
- tabCount + (spaceCount / @getTabLength())
- else
- 0
-
- scopeDescriptorForPosition: (position) ->
- {row, column} = Point.fromObject(position)
-
- iterator = @tokenizedLines[row].getTokenIterator()
- while iterator.next()
- if iterator.getScreenEnd() > column
- scopes = iterator.getScopes()
- break
-
- # rebuild scope of last token if we iterated off the end
- unless scopes?
- scopes = iterator.getScopes()
- scopes.push(iterator.getScopeEnds().reverse()...)
-
- new ScopeDescriptor({scopes})
-
- tokenForPosition: (position) ->
- {row, column} = Point.fromObject(position)
- @tokenizedLines[row].tokenAtBufferColumn(column)
-
- tokenStartPositionForPosition: (position) ->
- {row, column} = Point.fromObject(position)
- column = @tokenizedLines[row].tokenStartColumnForBufferColumn(column)
- new Point(row, column)
-
- bufferRangeForScopeAtPosition: (selector, position) ->
- selector = new ScopeSelector(selector.replace(/^\./, ''))
- position = Point.fromObject(position)
-
- {openScopes, tags} = @tokenizedLines[position.row]
- scopes = openScopes.map (tag) -> atom.grammars.scopeForId(tag)
-
- startColumn = 0
- for tag, tokenIndex in tags
- if tag < 0
- if tag % 2 is -1
- scopes.push(atom.grammars.scopeForId(tag))
- else
- scopes.pop()
- else
- endColumn = startColumn + tag
- if endColumn > position.column
- break
- else
- startColumn = endColumn
-
- return unless selector.matches(scopes)
-
- startScopes = scopes.slice()
- for startTokenIndex in [(tokenIndex - 1)..0] by -1
- tag = tags[startTokenIndex]
- if tag < 0
- if tag % 2 is -1
- startScopes.pop()
- else
- startScopes.push(atom.grammars.scopeForId(tag))
- else
- break unless selector.matches(startScopes)
- startColumn -= tag
-
- endScopes = scopes.slice()
- for endTokenIndex in [(tokenIndex + 1)...tags.length] by 1
- tag = tags[endTokenIndex]
- if tag < 0
- if tag % 2 is -1
- endScopes.push(atom.grammars.scopeForId(tag))
- else
- endScopes.pop()
- else
- break unless selector.matches(endScopes)
- endColumn += tag
-
- new Range(new Point(position.row, startColumn), new Point(position.row, endColumn))
-
- # Gets the row number of the last line.
- #
- # Returns a {Number}.
- getLastRow: ->
- @buffer.getLastRow()
-
- getLineCount: ->
- @buffer.getLineCount()
-
- logLines: (start=0, end=@buffer.getLastRow()) ->
- for row in [start..end]
- line = @tokenizedLineForRow(row).text
- console.log row, line, line.length
- return
-
-if Grim.includeDeprecatedAPIs
- EmitterMixin = require('emissary').Emitter
-
- TokenizedBuffer::on = (eventName) ->
- switch eventName
- when 'changed'
- Grim.deprecate("Use TokenizedBuffer::onDidChange instead")
- when 'grammar-changed'
- Grim.deprecate("Use TokenizedBuffer::onDidChangeGrammar instead")
- when 'tokenized'
- Grim.deprecate("Use TokenizedBuffer::onDidTokenize instead")
- else
- Grim.deprecate("TokenizedBuffer::on is deprecated. Use event subscription methods instead.")
-
- EmitterMixin::on.apply(this, arguments)
diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee
index 45af81e57d7..abace0fcde3 100644
--- a/src/tokenized-line.coffee
+++ b/src/tokenized-line.coffee
@@ -1,425 +1,43 @@
-_ = require 'underscore-plus'
-{isPairedCharacter} = require './text-utils'
Token = require './token'
-{SoftTab, HardTab, PairedCharacter, SoftWrapIndent} = require './special-token-symbols'
-
-NonWhitespaceRegex = /\S/
-LeadingWhitespaceRegex = /^\s*/
-TrailingWhitespaceRegex = /\s*$/
-RepeatedSpaceRegex = /[ ]/g
CommentScopeRegex = /(\b|\.)comment/
+
idCounter = 1
module.exports =
class TokenizedLine
- endOfLineInvisibles: null
- lineIsWhitespaceOnly: false
- firstNonWhitespaceIndex: 0
- foldable: false
-
constructor: (properties) ->
@id = idCounter++
return unless properties?
- @specialTokens = {}
- {@openScopes, @text, @tags, @lineEnding, @ruleStack, @tokenIterator} = properties
- {@startBufferColumn, @fold, @tabLength, @indentLevel, @invisibles} = properties
-
- @startBufferColumn ?= 0
- @bufferDelta = @text.length
-
- @transformContent()
- @buildEndOfLineInvisibles() if @invisibles? and @lineEnding?
-
- transformContent: ->
- text = ''
- bufferColumn = 0
- screenColumn = 0
- tokenIndex = 0
- tokenOffset = 0
- firstNonWhitespaceColumn = null
- lastNonWhitespaceColumn = null
-
- while bufferColumn < @text.length
- # advance to next token if we've iterated over its length
- if tokenOffset is @tags[tokenIndex]
- tokenIndex++
- tokenOffset = 0
-
- # advance to next token tag
- tokenIndex++ while @tags[tokenIndex] < 0
-
- character = @text[bufferColumn]
-
- # split out unicode surrogate pairs
- if isPairedCharacter(@text, bufferColumn)
- prefix = tokenOffset
- suffix = @tags[tokenIndex] - tokenOffset - 2
- splitTokens = []
- splitTokens.push(prefix) if prefix > 0
- splitTokens.push(2)
- splitTokens.push(suffix) if suffix > 0
-
- @tags.splice(tokenIndex, 1, splitTokens...)
-
- firstNonWhitespaceColumn ?= screenColumn
- lastNonWhitespaceColumn = screenColumn + 1
-
- text += @text.substr(bufferColumn, 2)
- screenColumn += 2
- bufferColumn += 2
-
- tokenIndex++ if prefix > 0
- @specialTokens[tokenIndex] = PairedCharacter
- tokenIndex++
- tokenOffset = 0
-
- # split out leading soft tabs
- else if character is ' '
- if firstNonWhitespaceColumn?
- text += ' '
- else
- if (screenColumn + 1) % @tabLength is 0
- @specialTokens[tokenIndex] = SoftTab
- suffix = @tags[tokenIndex] - @tabLength
- @tags.splice(tokenIndex, 1, @tabLength)
- @tags.splice(tokenIndex + 1, 0, suffix) if suffix > 0
- text += @invisibles?.space ? ' '
-
- screenColumn++
- bufferColumn++
- tokenOffset++
-
- # expand hard tabs to the next tab stop
- else if character is '\t'
- tabLength = @tabLength - (screenColumn % @tabLength)
- if @invisibles?.tab
- text += @invisibles.tab
- else
- text += ' '
- text += ' ' for i in [1...tabLength] by 1
-
- prefix = tokenOffset
- suffix = @tags[tokenIndex] - tokenOffset - 1
- splitTokens = []
- splitTokens.push(prefix) if prefix > 0
- splitTokens.push(tabLength)
- splitTokens.push(suffix) if suffix > 0
-
- @tags.splice(tokenIndex, 1, splitTokens...)
-
- screenColumn += tabLength
- bufferColumn++
-
- tokenIndex++ if prefix > 0
- @specialTokens[tokenIndex] = HardTab
- tokenIndex++
- tokenOffset = 0
-
- # continue past any other character
- else
- firstNonWhitespaceColumn ?= screenColumn
- lastNonWhitespaceColumn = screenColumn
-
- text += character
- screenColumn++
- bufferColumn++
- tokenOffset++
-
- @text = text
-
- @firstNonWhitespaceIndex = firstNonWhitespaceColumn
- if lastNonWhitespaceColumn?
- if lastNonWhitespaceColumn + 1 < @text.length
- @firstTrailingWhitespaceIndex = lastNonWhitespaceColumn + 1
- if @invisibles?.space
- @text =
- @text.substring(0, @firstTrailingWhitespaceIndex) +
- @text.substring(@firstTrailingWhitespaceIndex)
- .replace(RepeatedSpaceRegex, @invisibles.space)
- else
- @lineIsWhitespaceOnly = true
- @firstTrailingWhitespaceIndex = 0
+ {@openScopes, @text, @tags, @ruleStack, @tokenIterator, @grammar, tokens} = properties
+ @cachedTokens = tokens
getTokenIterator: -> @tokenIterator.reset(this)
Object.defineProperty @prototype, 'tokens', get: ->
- iterator = @getTokenIterator()
- tokens = []
-
- while iterator.next()
- properties = {
- value: iterator.getText()
- scopes: iterator.getScopes().slice()
- isAtomic: iterator.isAtomic()
- isHardTab: iterator.isHardTab()
- hasPairedCharacter: iterator.isPairedCharacter()
- isSoftWrapIndentation: iterator.isSoftWrapIndentation()
- }
-
- if iterator.isHardTab()
- properties.bufferDelta = 1
- properties.hasInvisibleCharacters = true if @invisibles?.tab
-
- if iterator.getScreenStart() < @firstNonWhitespaceIndex
- properties.firstNonWhitespaceIndex =
- Math.min(@firstNonWhitespaceIndex, iterator.getScreenEnd()) - iterator.getScreenStart()
- properties.hasInvisibleCharacters = true if @invisibles?.space
-
- if @lineEnding? and iterator.getScreenEnd() > @firstTrailingWhitespaceIndex
- properties.firstTrailingWhitespaceIndex =
- Math.max(0, @firstTrailingWhitespaceIndex - iterator.getScreenStart())
- properties.hasInvisibleCharacters = true if @invisibles?.space
-
- tokens.push(new Token(properties))
-
- tokens
-
- copy: ->
- copy = new TokenizedLine
- copy.tokenIterator = @tokenIterator
- copy.indentLevel = @indentLevel
- copy.openScopes = @openScopes
- copy.text = @text
- copy.tags = @tags
- copy.specialTokens = @specialTokens
- copy.firstNonWhitespaceIndex = @firstNonWhitespaceIndex
- copy.firstTrailingWhitespaceIndex = @firstTrailingWhitespaceIndex
- copy.lineEnding = @lineEnding
- copy.endOfLineInvisibles = @endOfLineInvisibles
- copy.ruleStack = @ruleStack
- copy.startBufferColumn = @startBufferColumn
- copy.fold = @fold
- copy
-
- # This clips a given screen column to a valid column that's within the line
- # and not in the middle of any atomic tokens.
- #
- # column - A {Number} representing the column to clip
- # options - A hash with the key clip. Valid values for this key:
- # 'closest' (default): clip to the closest edge of an atomic token.
- # 'forward': clip to the forward edge.
- # 'backward': clip to the backward edge.
- #
- # Returns a {Number} representing the clipped column.
- clipScreenColumn: (column, options={}) ->
- return 0 if @tags.length is 0
-
- {clip} = options
- column = Math.min(column, @getMaxScreenColumn())
-
- tokenStartColumn = 0
-
- iterator = @getTokenIterator()
- while iterator.next()
- break if iterator.getScreenEnd() > column
-
- if iterator.isSoftWrapIndentation()
- iterator.next() while iterator.isSoftWrapIndentation()
- iterator.getScreenStart()
- else if iterator.isAtomic() and iterator.getScreenStart() < column
- if clip is 'forward'
- iterator.getScreenEnd()
- else if clip is 'backward'
- iterator.getScreenStart()
- else #'closest'
- if column > ((iterator.getScreenStart() + iterator.getScreenEnd()) / 2)
- iterator.getScreenEnd()
- else
- iterator.getScreenStart()
+ if @cachedTokens
+ @cachedTokens
else
- column
+ iterator = @getTokenIterator()
+ tokens = []
- screenColumnForBufferColumn: (targetBufferColumn, options) ->
- iterator = @getTokenIterator()
- while iterator.next()
- tokenBufferStart = iterator.getBufferStart()
- tokenBufferEnd = iterator.getBufferEnd()
- if tokenBufferStart <= targetBufferColumn < tokenBufferEnd
- overshoot = targetBufferColumn - tokenBufferStart
- return Math.min(
- iterator.getScreenStart() + overshoot,
- iterator.getScreenEnd()
- )
- iterator.getScreenEnd()
+ while iterator.next()
+ tokens.push(new Token({
+ value: iterator.getText()
+ scopes: iterator.getScopes().slice()
+ }))
- bufferColumnForScreenColumn: (targetScreenColumn) ->
- iterator = @getTokenIterator()
- while iterator.next()
- tokenScreenStart = iterator.getScreenStart()
- tokenScreenEnd = iterator.getScreenEnd()
- if tokenScreenStart <= targetScreenColumn < tokenScreenEnd
- overshoot = targetScreenColumn - tokenScreenStart
- return Math.min(
- iterator.getBufferStart() + overshoot,
- iterator.getBufferEnd()
- )
- iterator.getBufferEnd()
-
- getMaxScreenColumn: ->
- if @fold
- 0
- else
- @text.length
-
- getMaxBufferColumn: ->
- @startBufferColumn + @bufferDelta
-
- # Given a boundary column, finds the point where this line would wrap.
- #
- # maxColumn - The {Number} where you want soft wrapping to occur
- #
- # Returns a {Number} representing the `line` position where the wrap would take place.
- # Returns `null` if a wrap wouldn't occur.
- findWrapColumn: (maxColumn) ->
- return unless maxColumn?
- return unless @text.length > maxColumn
-
- if /\s/.test(@text[maxColumn])
- # search forward for the start of a word past the boundary
- for column in [maxColumn..@text.length]
- return column if /\S/.test(@text[column])
-
- return @text.length
- else
- # search backward for the start of the word on the boundary
- for column in [maxColumn..@firstNonWhitespaceIndex]
- return column + 1 if /\s/.test(@text[column])
-
- return maxColumn
-
- softWrapAt: (column, hangingIndent) ->
- return [null, this] if column is 0
-
- leftText = @text.substring(0, column)
- rightText = @text.substring(column)
-
- leftTags = []
- rightTags = []
-
- leftSpecialTokens = {}
- rightSpecialTokens = {}
-
- rightOpenScopes = @openScopes.slice()
-
- screenColumn = 0
-
- for tag, index in @tags
- # tag represents a token
- if tag >= 0
- # token ends before the soft wrap column
- if screenColumn + tag <= column
- if specialToken = @specialTokens[index]
- leftSpecialTokens[index] = specialToken
- leftTags.push(tag)
- screenColumn += tag
-
- # token starts before and ends after the split column
- else if screenColumn <= column
- leftSuffix = column - screenColumn
- rightPrefix = screenColumn + tag - column
-
- leftTags.push(leftSuffix) if leftSuffix > 0
-
- softWrapIndent = @indentLevel * @tabLength + (hangingIndent ? 0)
- for i in [0...softWrapIndent] by 1
- rightText = ' ' + rightText
- remainingSoftWrapIndent = softWrapIndent
- while remainingSoftWrapIndent > 0
- indentToken = Math.min(remainingSoftWrapIndent, @tabLength)
- rightSpecialTokens[rightTags.length] = SoftWrapIndent
- rightTags.push(indentToken)
- remainingSoftWrapIndent -= indentToken
-
- rightTags.push(rightPrefix) if rightPrefix > 0
-
- screenColumn += tag
-
- # token is after split column
- else
- if specialToken = @specialTokens[index]
- rightSpecialTokens[rightTags.length] = specialToken
- rightTags.push(tag)
-
- # tag represents the start or end of a scop
- else if (tag % 2) is -1
- if screenColumn < column
- leftTags.push(tag)
- rightOpenScopes.push(tag)
- else
- rightTags.push(tag)
- else
- if screenColumn < column
- leftTags.push(tag)
- rightOpenScopes.pop()
- else
- rightTags.push(tag)
-
- splitBufferColumn = @bufferColumnForScreenColumn(column)
-
- leftFragment = new TokenizedLine
- leftFragment.tokenIterator = @tokenIterator
- leftFragment.openScopes = @openScopes
- leftFragment.text = leftText
- leftFragment.tags = leftTags
- leftFragment.specialTokens = leftSpecialTokens
- leftFragment.startBufferColumn = @startBufferColumn
- leftFragment.bufferDelta = splitBufferColumn - @startBufferColumn
- leftFragment.ruleStack = @ruleStack
- leftFragment.invisibles = @invisibles
- leftFragment.lineEnding = null
- leftFragment.indentLevel = @indentLevel
- leftFragment.tabLength = @tabLength
- leftFragment.firstNonWhitespaceIndex = Math.min(column, @firstNonWhitespaceIndex)
- leftFragment.firstTrailingWhitespaceIndex = Math.min(column, @firstTrailingWhitespaceIndex)
-
- rightFragment = new TokenizedLine
- rightFragment.tokenIterator = @tokenIterator
- rightFragment.openScopes = rightOpenScopes
- rightFragment.text = rightText
- rightFragment.tags = rightTags
- rightFragment.specialTokens = rightSpecialTokens
- rightFragment.startBufferColumn = splitBufferColumn
- rightFragment.bufferDelta = @bufferDelta - splitBufferColumn
- rightFragment.ruleStack = @ruleStack
- rightFragment.invisibles = @invisibles
- rightFragment.lineEnding = @lineEnding
- rightFragment.indentLevel = @indentLevel
- rightFragment.tabLength = @tabLength
- rightFragment.endOfLineInvisibles = @endOfLineInvisibles
- rightFragment.firstNonWhitespaceIndex = Math.max(softWrapIndent, @firstNonWhitespaceIndex - column + softWrapIndent)
- rightFragment.firstTrailingWhitespaceIndex = Math.max(softWrapIndent, @firstTrailingWhitespaceIndex - column + softWrapIndent)
-
- [leftFragment, rightFragment]
-
- isSoftWrapped: ->
- @lineEnding is null
-
- isColumnInsideSoftWrapIndentation: (targetColumn) ->
- targetColumn < @getSoftWrapIndentationDelta()
-
- getSoftWrapIndentationDelta: ->
- delta = 0
- for tag, index in @tags
- if tag >= 0
- if @specialTokens[index] is SoftWrapIndent
- delta += tag
- else
- break
- delta
-
- hasOnlySoftWrapIndentation: ->
- @getSoftWrapIndentationDelta() is @text.length
+ tokens
tokenAtBufferColumn: (bufferColumn) ->
@tokens[@tokenIndexAtBufferColumn(bufferColumn)]
tokenIndexAtBufferColumn: (bufferColumn) ->
- delta = 0
+ column = 0
for token, index in @tokens
- delta += token.bufferDelta
- return index if delta > bufferColumn
+ column += token.value.length
+ return index if column > bufferColumn
index - 1
tokenStartColumnForBufferColumn: (bufferColumn) ->
@@ -430,31 +48,38 @@ class TokenizedLine
delta = nextDelta
delta
- buildEndOfLineInvisibles: ->
- @endOfLineInvisibles = []
- {cr, eol} = @invisibles
-
- switch @lineEnding
- when '\r\n'
- @endOfLineInvisibles.push(cr) if cr
- @endOfLineInvisibles.push(eol) if eol
- when '\n'
- @endOfLineInvisibles.push(eol) if eol
-
isComment: ->
- iterator = @getTokenIterator()
- while iterator.next()
- scopes = iterator.getScopes()
- continue if scopes.length is 1
- continue unless NonWhitespaceRegex.test(iterator.getText())
- for scope in scopes
- return true if CommentScopeRegex.test(scope)
- break
+ 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
- isOnlyWhitespace: ->
- @lineIsWhitespaceOnly
-
tokenAtIndex: (index) ->
@tokens[index]
@@ -462,3 +87,9 @@ class TokenizedLine
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
diff --git a/src/tooltip-manager.coffee b/src/tooltip-manager.coffee
deleted file mode 100644
index ee2054b5a2f..00000000000
--- a/src/tooltip-manager.coffee
+++ /dev/null
@@ -1,106 +0,0 @@
-_ = require 'underscore-plus'
-{Disposable} = require 'event-kit'
-{$} = require './space-pen-extensions'
-
-# Essential: Associates tooltips with HTML elements or selectors.
-#
-# You can get the `TooltipManager` via `atom.tooltips`.
-#
-# ## Examples
-#
-# The essence of displaying a tooltip
-#
-# ```coffee
-# # display it
-# disposable = atom.tooltips.add(div, {title: 'This is a tooltip'})
-#
-# # remove it
-# disposable.dispose()
-# ```
-#
-# In practice there are usually multiple tooltips. So we add them to a
-# CompositeDisposable
-#
-# ```coffee
-# {CompositeDisposable} = require 'atom'
-# subscriptions = new CompositeDisposable
-#
-# div1 = document.createElement('div')
-# div2 = document.createElement('div')
-# subscriptions.add atom.tooltips.add(div1, {title: 'This is a tooltip'})
-# subscriptions.add atom.tooltips.add(div2, {title: 'Another tooltip'})
-#
-# # remove them all
-# subscriptions.dispose()
-# ```
-#
-# You can display a key binding in the tooltip as well with the
-# `keyBindingCommand` option.
-#
-# ```coffee
-# disposable = atom.tooltips.add @caseOptionButton,
-# title: "Match Case"
-# keyBindingCommand: 'find-and-replace:toggle-case-option'
-# keyBindingTarget: @findEditor.element
-# ```
-module.exports =
-class TooltipManager
- defaults:
- delay:
- show: 1000
- hide: 100
- container: 'body'
- html: true
- placement: 'auto top'
- viewportPadding: 2
-
- # Essential: Add a tooltip to the given element.
- #
- # * `target` An `HTMLElement`
- # * `options` See http://getbootstrap.com/javascript/#tooltips for a full list
- # of options. You can also supply the following additional options:
- # * `title` {String} Text in the tip.
- # * `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) ->
- requireBootstrapTooltip()
-
- {keyBindingCommand, keyBindingTarget} = options
-
- if keyBindingCommand?
- bindings = atom.keymaps.findKeyBindings(command: keyBindingCommand, target: keyBindingTarget)
- keystroke = getKeystroke(bindings)
- if options.title? and keystroke?
- options.title += " " + getKeystroke(bindings)
- else if keystroke?
- options.title = getKeystroke(bindings)
-
- $target = $(target)
- $target.tooltip(_.defaults(options, @defaults))
-
- new Disposable ->
- tooltip = $target.data('bs.tooltip')
- if tooltip?
- tooltip.leave(currentTarget: target)
- tooltip.hide()
- $target.tooltip('destroy')
-
-humanizeKeystrokes = (keystroke) ->
- keystrokes = keystroke.split(' ')
- keystrokes = (_.humanizeKeystroke(stroke) for stroke in keystrokes)
- keystrokes.join(' ')
-
-getKeystroke = (bindings) ->
- if bindings?.length
- "#{humanizeKeystrokes(bindings[0].keystrokes)}"
- else
-
-requireBootstrapTooltip = _.once ->
- atom.requireWithGlobals('bootstrap/js/tooltip', {jQuery: $})
diff --git a/src/tooltip-manager.js b/src/tooltip-manager.js
new file mode 100644
index 00000000000..4d08b6b83ac
--- /dev/null
+++ b/src/tooltip-manager.js
@@ -0,0 +1,209 @@
+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
+ )}`;
+ }
+}
diff --git a/src/tooltip.js b/src/tooltip.js
new file mode 100644
index 00000000000..3d59f8036db
--- /dev/null
+++ b/src/tooltip.js
@@ -0,0 +1,691 @@
+'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;
diff --git a/src/tree-indenter.js b/src/tree-indenter.js
new file mode 100644
index 00000000000..c536edd4870
--- /dev/null
+++ b/src/tree-indenter.js
@@ -0,0 +1,155 @@
+// 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;
+ }
+};
diff --git a/src/tree-sitter-grammar.js b/src/tree-sitter-grammar.js
new file mode 100644
index 00000000000..e76b8aed45e
--- /dev/null
+++ b/src/tree-sitter-grammar.js
@@ -0,0 +1,194 @@
+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;
+}
diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js
new file mode 100644
index 00000000000..3ffe4038e2b
--- /dev/null
+++ b/src/tree-sitter-language-mode.js
@@ -0,0 +1,1556 @@
+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;
diff --git a/src/typescript.coffee b/src/typescript.coffee
deleted file mode 100644
index 3a54941f33d..00000000000
--- a/src/typescript.coffee
+++ /dev/null
@@ -1,106 +0,0 @@
-###
-Cache for source code transpiled by TypeScript.
-
-Inspired by https://github.com/atom/atom/blob/7a719d585db96ff7d2977db9067e1d9d4d0adf1a/src/babel.coffee
-###
-
-crypto = require 'crypto'
-fs = require 'fs-plus'
-path = require 'path'
-tss = null # Defer until used
-
-stats =
- hits: 0
- misses: 0
-
-defaultOptions =
- target: 1 # ES5
- module: 'commonjs'
- sourceMap: true
-
-createTypeScriptVersionAndOptionsDigest = (version, options) ->
- shasum = crypto.createHash('sha1')
- # Include the version of typescript in the hash.
- shasum.update('typescript', 'utf8')
- shasum.update('\0', 'utf8')
- shasum.update(version, 'utf8')
- shasum.update('\0', 'utf8')
- shasum.update(JSON.stringify(options))
- shasum.digest('hex')
-
-cacheDir = null
-jsCacheDir = null
-
-getCachePath = (sourceCode) ->
- digest = crypto.createHash('sha1').update(sourceCode, 'utf8').digest('hex')
-
- unless jsCacheDir?
- tssVersion = require('typescript-simple/package.json').version
- jsCacheDir = path.join(cacheDir, createTypeScriptVersionAndOptionsDigest(tssVersion, defaultOptions))
-
- path.join(jsCacheDir, "#{digest}.js")
-
-getCachedJavaScript = (cachePath) ->
- if fs.isFileSync(cachePath)
- try
- cachedJavaScript = fs.readFileSync(cachePath, 'utf8')
- stats.hits++
- return cachedJavaScript
- null
-
-# Returns the TypeScript options that should be used to transpile filePath.
-createOptions = (filePath) ->
- options = filename: filePath
- for key, value of defaultOptions
- options[key] = value
- options
-
-transpile = (sourceCode, filePath, cachePath) ->
- options = createOptions(filePath)
- unless tss?
- {TypeScriptSimple} = require 'typescript-simple'
- tss = new TypeScriptSimple(options, false)
- js = tss.compile(sourceCode, filePath)
- stats.misses++
-
- try
- fs.writeFileSync(cachePath, js)
-
- js
-
-# Function that obeys the contract of an entry in the require.extensions map.
-# Returns the transpiled version of the JavaScript code at filePath, which is
-# either generated on the fly or pulled from cache.
-loadFile = (module, filePath) ->
- sourceCode = fs.readFileSync(filePath, 'utf8')
- cachePath = getCachePath(sourceCode)
- js = getCachedJavaScript(cachePath) ? transpile(sourceCode, filePath, cachePath)
- module._compile(js, filePath)
-
-register = ->
- Object.defineProperty(require.extensions, '.ts', {
- enumerable: true
- writable: false
- value: loadFile
- })
-
-setCacheDirectory = (newCacheDir) ->
- if cacheDir isnt newCacheDir
- cacheDir = newCacheDir
- jsCacheDir = null
-
-module.exports =
- register: register
- setCacheDirectory: setCacheDirectory
- getCacheMisses: -> stats.misses
- getCacheHits: -> stats.hits
-
- # Visible for testing.
- createTypeScriptVersionAndOptionsDigest: createTypeScriptVersionAndOptionsDigest
-
- addPathToCache: (filePath) ->
- return if path.extname(filePath) isnt '.ts'
-
- sourceCode = fs.readFileSync(filePath, 'utf8')
- cachePath = getCachePath(sourceCode)
- transpile(sourceCode, filePath, cachePath)
diff --git a/src/typescript.js b/src/typescript.js
new file mode 100644
index 00000000000..2995224b1b3
--- /dev/null
+++ b/src/typescript.js
@@ -0,0 +1,60 @@
+'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');
+}
diff --git a/src/update-process-env.js b/src/update-process-env.js
new file mode 100644
index 00000000000..21ea4552dcd
--- /dev/null
+++ b/src/update-process-env.js
@@ -0,0 +1,143 @@
+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 };
diff --git a/src/uri-handler-registry.js b/src/uri-handler-registry.js
new file mode 100644
index 00000000000..7b232ccc636
--- /dev/null
+++ b/src/uri-handler-registry.js
@@ -0,0 +1,134 @@
+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;
+ }
+};
diff --git a/src/view-registry.coffee b/src/view-registry.coffee
deleted file mode 100644
index 24c53bd57a7..00000000000
--- a/src/view-registry.coffee
+++ /dev/null
@@ -1,221 +0,0 @@
-{find} = require 'underscore-plus'
-Grim = require 'grim'
-{Disposable} = require 'event-kit'
-
-# 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.
-#
-# 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`.
-#
-# ## Examples
-#
-# ### Getting the workspace element
-#
-# ```coffee
-# workspaceElement = atom.views.getView(atom.workspace)
-# ```
-#
-# ### Getting An Editor Element
-#
-# ```coffee
-# textEditor = atom.workspace.getActiveTextEditor()
-# textEditorElement = atom.views.getView(textEditor)
-# ```
-#
-# ### Getting A Pane Element
-#
-# ```coffee
-# pane = atom.workspace.getActivePane()
-# paneElement = atom.views.getView(pane)
-# ```
-module.exports =
-class ViewRegistry
- documentPollingInterval: 200
- documentUpdateRequested: false
- documentReadInProgress: false
- performDocumentPollAfterUpdate: false
- pollIntervalHandle: null
-
- constructor: ->
- @views = new WeakMap
- @providers = []
- @documentWriters = []
- @documentReaders = []
- @documentPollers = []
-
- # 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
- # modelConstructor: TextEditor
- # viewConstructor: TextEditorElement
- # ```
- #
- # * `providerSpec` {Object} containing the following keys:
- # * `modelConstructor` Constructor {Function} for your model.
- # * `viewConstructor` (Optional) Constructor {Function} for your view. It
- # should be a subclass of `HTMLElement` (that is, your view should be a
- # DOM node) and have a `::setModel()` method which will be called
- # immediately after construction. If you don't supply this property, you
- # must supply the `createView` property with a function that never returns
- # `undefined`.
- # * `createView` (Optional) Factory {Function} that must return a subclass
- # of `HTMLElement` or `undefined`. If this property is not present or the
- # function returns `undefined`, the view provider will fall back to the
- # `viewConstructor` property. If you don't provide this property, you must
- # provider a `viewConstructor` property.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to remove the
- # added provider.
- addViewProvider: (modelConstructor, createView) ->
- if arguments.length is 1
- Grim.deprecate("atom.views.addViewProvider now takes 2 arguments: a model constructor and a createView function. See docs for details.")
- provider = modelConstructor
- else
- provider = {modelConstructor, createView}
-
- @providers.push(provider)
- new Disposable =>
- @providers = @providers.filter (p) -> p isnt provider
-
- # 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.
- #
- # ## Examples
- #
- # ### Getting An Editor Element
- #
- # ```coffee
- # textEditor = atom.workspace.getActiveTextEditor()
- # textEditorElement = atom.views.getView(textEditor)
- # ```
- #
- # ### Getting A Pane Element
- #
- # ```coffee
- # pane = atom.workspace.getActivePane()
- # paneElement = atom.views.getView(pane)
- # ```
- #
- # ### Getting The Workspace Element
- #
- # ```coffee
- # workspaceElement = atom.views.getView(atom.workspace)
- # ```
- #
- # * `object` The object for which you want to retrieve a view. This can be a
- # pane item, a pane, or the workspace itself.
- #
- # Returns a DOM element.
- getView: (object) ->
- return unless object?
-
- if view = @views.get(object)
- view
- else
- view = @createView(object)
- @views.set(object, view)
- view
-
- createView: (object) ->
- if object instanceof HTMLElement
- object
- else if object?.jquery
- object[0]
- else if provider = @findProvider(object)
- element = provider.createView?(object)
- unless element?
- element = new provider.viewConstructor
- element.initialize?(object) ? element.setModel?(object)
- element
- else if viewConstructor = object?.getViewClass?()
- view = new viewConstructor(object)
- view[0]
- else
- throw new Error("Can't create a view for #{object.constructor.name} instance. Please register a view provider.")
-
- findProvider: (object) ->
- find @providers, ({modelConstructor}) -> object instanceof modelConstructor
-
- updateDocument: (fn) ->
- @documentWriters.push(fn)
- @requestDocumentUpdate() unless @documentReadInProgress
- new Disposable =>
- @documentWriters = @documentWriters.filter (writer) -> writer isnt fn
-
- readDocument: (fn) ->
- @documentReaders.push(fn)
- @requestDocumentUpdate()
- new Disposable =>
- @documentReaders = @documentReaders.filter (reader) -> reader isnt fn
-
- pollDocument: (fn) ->
- @startPollingDocument() if @documentPollers.length is 0
- @documentPollers.push(fn)
- new Disposable =>
- @documentPollers = @documentPollers.filter (poller) -> poller isnt fn
- @stopPollingDocument() if @documentPollers.length is 0
-
- pollAfterNextUpdate: ->
- @performDocumentPollAfterUpdate = true
-
- clearDocumentRequests: ->
- @documentReaders = []
- @documentWriters = []
- @documentPollers = []
- @documentUpdateRequested = false
- @stopPollingDocument()
-
- requestDocumentUpdate: ->
- unless @documentUpdateRequested
- @documentUpdateRequested = true
- requestAnimationFrame(@performDocumentUpdate)
-
- performDocumentUpdate: =>
- @documentUpdateRequested = false
- writer() while writer = @documentWriters.shift()
-
- @documentReadInProgress = true
- reader() while reader = @documentReaders.shift()
- @performDocumentPoll() if @performDocumentPollAfterUpdate
- @performDocumentPollAfterUpdate = false
- @documentReadInProgress = false
-
- # process updates requested as a result of reads
- writer() while writer = @documentWriters.shift()
-
- startPollingDocument: ->
- @pollIntervalHandle = window.setInterval(@performDocumentPoll, @documentPollingInterval)
-
- stopPollingDocument: ->
- window.clearInterval(@pollIntervalHandle)
-
- performDocumentPoll: =>
- if @documentUpdateRequested
- @performDocumentPollAfterUpdate = true
- else
- poller() for poller in @documentPollers
- return
diff --git a/src/view-registry.js b/src/view-registry.js
new file mode 100644
index 00000000000..698dbc3251e
--- /dev/null
+++ b/src/view-registry.js
@@ -0,0 +1,287 @@
+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();
+ }
+ }
+};
diff --git a/src/window-bootstrap.coffee b/src/window-bootstrap.coffee
deleted file mode 100644
index 886ba26dc48..00000000000
--- a/src/window-bootstrap.coffee
+++ /dev/null
@@ -1,13 +0,0 @@
-# Like sands through the hourglass, so are the days of our lives.
-require './window'
-
-Atom = require './atom'
-window.atom = Atom.loadOrCreate('editor')
-atom.initialize()
-atom.startEditorWindow()
-
-# Workaround for focus getting cleared upon window creation
-windowFocused = ->
- window.removeEventListener('focus', windowFocused)
- setTimeout (-> document.querySelector('atom-workspace').focus()), 0
-window.addEventListener('focus', windowFocused)
diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee
deleted file mode 100644
index 7d67e87abd1..00000000000
--- a/src/window-event-handler.coffee
+++ /dev/null
@@ -1,201 +0,0 @@
-path = require 'path'
-{$} = require './space-pen-extensions'
-{Disposable} = require 'event-kit'
-ipc = require 'ipc'
-shell = require 'shell'
-{Subscriber} = require 'emissary'
-fs = require 'fs-plus'
-
-# Handles low-level events related to the window.
-module.exports =
-class WindowEventHandler
- Subscriber.includeInto(this)
-
- constructor: ->
- @reloadRequested = false
-
- @subscribe ipc, 'message', (message, detail) ->
- switch message
- when 'open-locations'
- needsProjectPaths = atom.project?.getPaths().length is 0
-
- for {pathToOpen, initialLine, initialColumn} in detail
- if pathToOpen? and needsProjectPaths
- if fs.existsSync(pathToOpen)
- atom.project.addPath(pathToOpen)
- else
- dirToOpen = path.dirname(pathToOpen)
- if fs.existsSync(dirToOpen)
- atom.project.addPath(dirToOpen)
-
- unless fs.isDirectorySync(pathToOpen)
- atom.workspace?.open(pathToOpen, {initialLine, initialColumn})
-
- return
-
- when 'update-available'
- atom.updateAvailable(detail)
-
- # FIXME: Remove this when deprecations are removed
- {releaseVersion} = detail
- detail = [releaseVersion]
- if workspaceElement = atom.views.getView(atom.workspace)
- atom.commands.dispatch workspaceElement, "window:update-available", detail
-
- @subscribe ipc, 'command', (command, args...) ->
- activeElement = document.activeElement
- # Use the workspace element view if body has focus
- if activeElement is document.body and workspaceElement = atom.views.getView(atom.workspace)
- activeElement = workspaceElement
-
- atom.commands.dispatch(activeElement, command, args[0])
-
- @subscribe ipc, 'context-command', (command, args...) ->
- $(atom.contextMenu.activeElement).trigger(command, args...)
-
- @subscribe $(window), 'focus', -> document.body.classList.remove('is-blurred')
-
- @subscribe $(window), 'blur', -> document.body.classList.add('is-blurred')
-
- @subscribe $(window), 'beforeunload', =>
- confirmed = atom.workspace?.confirmClose(windowCloseRequested: true)
- atom.hide() if confirmed and not @reloadRequested and atom.getCurrentWindow().isWebViewFocused()
- @reloadRequested = false
-
- atom.storeDefaultWindowDimensions()
- atom.storeWindowDimensions()
- atom.unloadEditorWindow() if confirmed
-
- confirmed
-
- @subscribe $(window), 'blur', -> atom.storeDefaultWindowDimensions()
-
- @subscribe $(window), 'unload', -> atom.removeEditorWindow()
-
- @subscribeToCommand $(window), 'window:toggle-full-screen', -> atom.toggleFullScreen()
-
- @subscribeToCommand $(window), 'window:close', -> atom.close()
-
- @subscribeToCommand $(window), 'window:reload', =>
- @reloadRequested = true
- atom.reload()
-
- @subscribeToCommand $(window), 'window:toggle-dev-tools', -> atom.toggleDevTools()
-
- if process.platform in ['win32', 'linux']
- @subscribeToCommand $(window), 'window:toggle-menu-bar', ->
- atom.config.set('core.autoHideMenuBar', not atom.config.get('core.autoHideMenuBar'))
-
- @subscribeToCommand $(document), 'core:focus-next', @focusNext
-
- @subscribeToCommand $(document), 'core:focus-previous', @focusPrevious
-
- document.addEventListener 'keydown', @onKeydown
-
- document.addEventListener 'drop', @onDrop
- @subscribe new Disposable =>
- document.removeEventListener('drop', @onDrop)
-
- document.addEventListener 'dragover', @onDragOver
- @subscribe new Disposable =>
- document.removeEventListener('dragover', @onDragOver)
-
- @subscribe $(document), 'click', 'a', @openLink
-
- # Prevent form submits from changing the current window's URL
- @subscribe $(document), 'submit', 'form', (e) -> e.preventDefault()
-
- @subscribe $(document), 'contextmenu', (e) ->
- e.preventDefault()
- atom.contextMenu.showForEvent(e)
-
- @handleNativeKeybindings()
-
- # Wire commands that should be handled by the native menu
- # for elements with the `.native-key-bindings` class.
- handleNativeKeybindings: ->
- menu = null
- bindCommandToAction = (command, action) =>
- @subscribe $(document), command, (event) ->
- if event.target.webkitMatchesSelector('.native-key-bindings')
- menu ?= require('remote').require('menu')
- menu.sendActionToFirstResponder(action)
- true
-
- bindCommandToAction('core:copy', 'copy:')
- bindCommandToAction('core:paste', 'paste:')
- bindCommandToAction('core:undo', 'undo:')
- bindCommandToAction('core:redo', 'redo:')
- bindCommandToAction('core:select-all', 'selectAll:')
-
- onKeydown: (event) ->
- atom.keymaps.handleKeyboardEvent(event)
- event.stopImmediatePropagation()
-
- onDrop: (event) ->
- event.preventDefault()
- event.stopPropagation()
-
- onDragOver: (event) ->
- event.preventDefault()
- event.stopPropagation()
- event.dataTransfer.dropEffect = 'none'
-
- openLink: ({target, currentTarget}) ->
- location = target?.getAttribute('href') or currentTarget?.getAttribute('href')
- if location and location[0] isnt '#' and /^https?:\/\//.test(location)
- shell.openExternal(location)
- false
-
- eachTabIndexedElement: (callback) ->
- for element in $('[tabindex]')
- element = $(element)
- continue if element.isDisabled()
-
- tabIndex = parseInt(element.attr('tabindex'))
- continue unless tabIndex >= 0
-
- callback(element, tabIndex)
- return
-
- focusNext: =>
- focusedTabIndex = parseInt($(':focus').attr('tabindex')) or -Infinity
-
- nextElement = null
- nextTabIndex = Infinity
- lowestElement = null
- lowestTabIndex = Infinity
- @eachTabIndexedElement (element, tabIndex) ->
- if tabIndex < lowestTabIndex
- lowestTabIndex = tabIndex
- lowestElement = element
-
- if focusedTabIndex < tabIndex < nextTabIndex
- nextTabIndex = tabIndex
- nextElement = element
-
- if nextElement?
- nextElement.focus()
- else if lowestElement?
- lowestElement.focus()
-
- focusPrevious: =>
- focusedTabIndex = parseInt($(':focus').attr('tabindex')) or Infinity
-
- previousElement = null
- previousTabIndex = -Infinity
- highestElement = null
- highestTabIndex = -Infinity
- @eachTabIndexedElement (element, tabIndex) ->
- if tabIndex > highestTabIndex
- highestTabIndex = tabIndex
- highestElement = element
-
- if focusedTabIndex > tabIndex > previousTabIndex
- previousTabIndex = tabIndex
- previousElement = element
-
- if previousElement?
- previousElement.focus()
- else if highestElement?
- highestElement.focus()
diff --git a/src/window-event-handler.js b/src/window-event-handler.js
new file mode 100644
index 00000000000..b796defb6d6
--- /dev/null
+++ b/src/window-event-handler.js
@@ -0,0 +1,324 @@
+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);
+ }
+};
diff --git a/src/window.coffee b/src/window.coffee
deleted file mode 100644
index 9554218cace..00000000000
--- a/src/window.coffee
+++ /dev/null
@@ -1,27 +0,0 @@
-# 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 = (description, fn) ->
- start = Date.now()
- value = fn()
- result = Date.now() - start
- console.log description, result
- 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 = (description, fn) ->
- measure description, ->
- console.profile(description)
- value = fn()
- console.profileEnd(description)
- value
diff --git a/src/window.js b/src/window.js
new file mode 100644
index 00000000000..a13a5e64858
--- /dev/null
+++ b/src/window.js
@@ -0,0 +1,30 @@
+// 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;
+ });
+};
diff --git a/src/workspace-center.js b/src/workspace-center.js
new file mode 100644
index 00000000000..f803c124368
--- /dev/null
+++ b/src/workspace-center.js
@@ -0,0 +1,343 @@
+'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();
+ }
+ }
+};
diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee
deleted file mode 100644
index 4f5dd6c5d38..00000000000
--- a/src/workspace-element.coffee
+++ /dev/null
@@ -1,173 +0,0 @@
-ipc = require 'ipc'
-path = require 'path'
-{Disposable, CompositeDisposable} = require 'event-kit'
-Grim = require 'grim'
-scrollbarStyle = require 'scrollbar-style'
-{callAttachHooks} = require 'space-pen'
-WorkspaceView = null
-
-module.exports =
-class WorkspaceElement extends HTMLElement
- globalTextEditorStyleSheet: null
-
- createdCallback: ->
- @subscriptions = new CompositeDisposable
- @initializeGlobalTextEditorStyleSheet()
- @initializeContent()
- @observeScrollbarStyle()
- @observeTextEditorFontConfig()
- @createSpacePenShim() if Grim.includeDeprecatedAPIs
-
- attachedCallback: ->
- callAttachHooks(this) if Grim.includeDeprecatedAPIs
- @focus()
-
- detachedCallback: ->
- @subscriptions.dispose()
- @model.destroy()
-
- initializeGlobalTextEditorStyleSheet: ->
- atom.styles.addStyleSheet('atom-text-editor {}', sourcePath: 'global-text-editor-styles')
- @globalTextEditorStyleSheet = document.head.querySelector('style[source-path="global-text-editor-styles"]').sheet
-
- initializeContent: ->
- @classList.add 'workspace'
- @setAttribute 'tabindex', -1
-
- @verticalAxis = document.createElement('atom-workspace-axis')
- @verticalAxis.classList.add('vertical')
-
- @horizontalAxis = document.createElement('atom-workspace-axis')
- @horizontalAxis.classList.add('horizontal')
- @horizontalAxis.appendChild(@verticalAxis)
-
- @appendChild(@horizontalAxis)
-
- observeScrollbarStyle: ->
- @subscriptions.add scrollbarStyle.observePreferredScrollbarStyle (style) =>
- switch style
- when 'legacy'
- @classList.remove('scrollbars-visible-when-scrolling')
- @classList.add("scrollbars-visible-always")
- when 'overlay'
- @classList.remove('scrollbars-visible-always')
- @classList.add("scrollbars-visible-when-scrolling")
-
- observeTextEditorFontConfig: ->
- @subscriptions.add atom.config.observe 'editor.fontSize', @setTextEditorFontSize.bind(this)
- @subscriptions.add atom.config.observe 'editor.fontFamily', @setTextEditorFontFamily.bind(this)
- @subscriptions.add atom.config.observe 'editor.lineHeight', @setTextEditorLineHeight.bind(this)
-
- createSpacePenShim: ->
- WorkspaceView ?= require './workspace-view'
- @__spacePenView = new WorkspaceView(this)
-
- initialize: (@model) ->
- @paneContainer = atom.views.getView(@model.paneContainer)
- @verticalAxis.appendChild(@paneContainer)
- @addEventListener 'focus', @handleFocus.bind(this)
-
- @panelContainers =
- top: atom.views.getView(@model.panelContainers.top)
- left: atom.views.getView(@model.panelContainers.left)
- right: atom.views.getView(@model.panelContainers.right)
- bottom: atom.views.getView(@model.panelContainers.bottom)
- modal: atom.views.getView(@model.panelContainers.modal)
-
- @horizontalAxis.insertBefore(@panelContainers.left, @verticalAxis)
- @horizontalAxis.appendChild(@panelContainers.right)
-
- @verticalAxis.insertBefore(@panelContainers.top, @paneContainer)
- @verticalAxis.appendChild(@panelContainers.bottom)
-
- @appendChild(@panelContainers.modal)
-
- @__spacePenView.setModel(@model) if Grim.includeDeprecatedAPIs
- this
-
- getModel: -> @model
-
- setTextEditorFontSize: (fontSize) ->
- @updateGlobalEditorStyle('font-size', fontSize + 'px')
-
- setTextEditorFontFamily: (fontFamily) ->
- @updateGlobalEditorStyle('font-family', fontFamily)
-
- setTextEditorLineHeight: (lineHeight) ->
- @updateGlobalEditorStyle('line-height', lineHeight)
-
- updateGlobalEditorStyle: (property, value) ->
- editorRule = @globalTextEditorStyleSheet.cssRules[0]
- editorRule.style[property] = value
- atom.themes.emitter.emit 'did-update-stylesheet', @globalTextEditorStyleSheet
-
- handleFocus: (event) ->
- @model.getActivePane().activate()
-
- focusPaneViewAbove: -> @paneContainer.focusPaneViewAbove()
-
- focusPaneViewBelow: -> @paneContainer.focusPaneViewBelow()
-
- focusPaneViewOnLeft: -> @paneContainer.focusPaneViewOnLeft()
-
- focusPaneViewOnRight: -> @paneContainer.focusPaneViewOnRight()
-
- runPackageSpecs: ->
- if activePath = atom.workspace.getActivePaneItem()?.getPath?()
- [projectPath] = atom.project.relativizePath(activePath)
- else
- [projectPath] = atom.project.getPaths()
- ipc.send('run-package-specs', path.join(projectPath, 'spec')) if projectPath
-
-atom.commands.add 'atom-workspace',
- 'window:increase-font-size': -> @getModel().increaseFontSize()
- 'window:decrease-font-size': -> @getModel().decreaseFontSize()
- 'window:reset-font-size': -> @getModel().resetFontSize()
- 'application:about': -> ipc.send('command', 'application:about')
- 'application:run-all-specs': -> ipc.send('command', 'application:run-all-specs')
- 'application:run-benchmarks': -> ipc.send('command', 'application:run-benchmarks')
- 'application:show-settings': -> ipc.send('command', 'application:show-settings')
- 'application:quit': -> ipc.send('command', 'application:quit')
- 'application:hide': -> ipc.send('command', 'application:hide')
- 'application:hide-other-applications': -> ipc.send('command', 'application:hide-other-applications')
- 'application:install-update': -> ipc.send('command', 'application:install-update')
- 'application:unhide-all-applications': -> ipc.send('command', 'application:unhide-all-applications')
- 'application:new-window': -> ipc.send('command', 'application:new-window')
- 'application:new-file': -> ipc.send('command', 'application:new-file')
- 'application:open': -> ipc.send('command', 'application:open')
- 'application:open-file': -> ipc.send('command', 'application:open-file')
- 'application:open-folder': -> ipc.send('command', 'application:open-folder')
- 'application:open-dev': -> ipc.send('command', 'application:open-dev')
- 'application:open-safe': -> ipc.send('command', 'application:open-safe')
- 'application:open-api-preview': -> ipc.send('command', 'application:open-api-preview')
- 'application:open-dev-api-preview': -> ipc.send('command', 'application:open-dev-api-preview')
- 'application:add-project-folder': -> atom.addProjectFolder()
- 'application:minimize': -> ipc.send('command', 'application:minimize')
- 'application:zoom': -> ipc.send('command', 'application:zoom')
- 'application:bring-all-windows-to-front': -> ipc.send('command', 'application:bring-all-windows-to-front')
- 'application:open-your-config': -> ipc.send('command', 'application:open-your-config')
- 'application:open-your-init-script': -> ipc.send('command', 'application:open-your-init-script')
- 'application:open-your-keymap': -> ipc.send('command', 'application:open-your-keymap')
- 'application:open-your-snippets': -> ipc.send('command', 'application:open-your-snippets')
- 'application:open-your-stylesheet': -> ipc.send('command', 'application:open-your-stylesheet')
- 'application:open-license': -> @getModel().openLicense()
- 'window:run-package-specs': -> @runPackageSpecs()
- '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:save-all': -> @getModel().saveAll()
- 'window:toggle-invisibles': -> atom.config.set("editor.showInvisibles", not atom.config.get("editor.showInvisibles"))
- 'window:log-deprecation-warnings': -> Grim.logDeprecations()
- 'window:toggle-auto-indent': -> atom.config.set("editor.autoIndent", not atom.config.get("editor.autoIndent"))
- 'pane:reopen-closed-item': -> @getModel().reopenItem()
- 'core:close': -> @getModel().destroyActivePaneItemOrEmptyPane()
- 'core:save': -> @getModel().saveActivePaneItem()
- 'core:save-as': -> @getModel().saveActivePaneItemAs()
-
-if process.platform is 'darwin'
- atom.commands.add 'atom-workspace', 'window:install-shell-commands', -> @getModel().installShellCommands()
-
-module.exports = WorkspaceElement = document.registerElement 'atom-workspace', prototype: WorkspaceElement.prototype
diff --git a/src/workspace-element.js b/src/workspace-element.js
new file mode 100644
index 00000000000..9400ef028c6
--- /dev/null
+++ b/src/workspace-element.js
@@ -0,0 +1,486 @@
+'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
+};
diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee
deleted file mode 100644
index 6fe9f6848bd..00000000000
--- a/src/workspace-view.coffee
+++ /dev/null
@@ -1,340 +0,0 @@
-ipc = require 'ipc'
-path = require 'path'
-Q = require 'q'
-_ = require 'underscore-plus'
-Delegator = require 'delegato'
-{deprecate, logDeprecationWarnings} = require 'grim'
-{$, $$, View} = require './space-pen-extensions'
-fs = require 'fs-plus'
-Workspace = require './workspace'
-PaneView = require './pane-view'
-PaneContainerView = require './pane-container-view'
-TextEditor = require './text-editor'
-
-# Deprecated: The top-level view for the entire window. An instance of this class is
-# available via the `atom.workspaceView` global.
-#
-# It is backed by a model object, an instance of {Workspace}, which is available
-# via the `atom.workspace` global or {::getModel}. You should prefer to interact
-# with the model object when possible, but it won't always be possible with the
-# current API.
-#
-# ## Adding Perimeter Panels
-#
-# Use the following methods if possible to attach panels to the perimeter of the
-# workspace rather than manipulating the DOM directly to better insulate you to
-# changes in the workspace markup:
-#
-# * {::prependToTop}
-# * {::appendToTop}
-# * {::prependToBottom}
-# * {::appendToBottom}
-# * {::prependToLeft}
-# * {::appendToLeft}
-# * {::prependToRight}
-# * {::appendToRight}
-#
-# ## Requiring in package specs
-#
-# If you need a `WorkspaceView` instance to test your package, require it via
-# the built-in `atom` module.
-#
-# ```coffee
-# {WorkspaceView} = require 'atom'
-# ```
-#
-# You can assign it to the `atom.workspaceView` global in the spec or just use
-# it as a local, depending on what you're trying to accomplish. Building the
-# `WorkspaceView` is currently expensive, so you should try build a {Workspace}
-# instead if possible.
-module.exports =
-class WorkspaceView extends View
- Delegator.includeInto(this)
-
- @delegatesProperty 'fullScreen', 'destroyedItemURIs', toProperty: 'model'
- @delegatesMethods 'open', 'openSync',
- 'saveActivePaneItem', 'saveActivePaneItemAs', 'saveAll', 'destroyActivePaneItem',
- 'destroyActivePane', 'increaseFontSize', 'decreaseFontSize', toProperty: 'model'
-
- constructor: (@element) ->
- unless @element?
- return atom.views.getView(atom.workspace).__spacePenView
- super
- @deprecateViewEvents()
- @attachedEditorViews = new WeakSet
-
- setModel: (@model) ->
- @horizontal = @find('atom-workspace-axis.horizontal')
- @vertical = @find('atom-workspace-axis.vertical')
- @panes = @find('atom-pane-container').view()
- @subscribe @model.onDidOpen => @trigger 'uri-opened'
-
- beforeRemove: ->
- @model?.destroy()
-
- ###
- Section: Accessing the Workspace Model
- ###
-
- # Essential: Get the underlying model object.
- #
- # Returns a {Workspace}.
- getModel: -> @model
-
- ###
- Section: Accessing Views
- ###
-
- # Essential: Register a function to be called for every current and future
- # editor view in the workspace (only includes {TextEditorView}s that are pane
- # items).
- #
- # * `callback` A {Function} with an {TextEditorView} as its only argument.
- # * `editorView` {TextEditorView}
- #
- # Returns a subscription object with an `.off` method that you can call to
- # unregister the callback.
- eachEditorView: (callback) ->
- for editorView in @getEditorViews()
- @attachedEditorViews.add(editorView)
- callback(editorView)
-
- attachedCallback = (e, editorView) =>
- unless @attachedEditorViews.has(editorView)
- @attachedEditorViews.add(editorView)
- callback(editorView) unless editorView.mini
-
- @on('editor:attached', attachedCallback)
-
- off: => @off('editor:attached', attachedCallback)
-
- # Essential: Register a function to be called for every current and future
- # pane view in the workspace.
- #
- # * `callback` A {Function} with a {PaneView} as its only argument.
- # * `paneView` {PaneView}
- #
- # Returns a subscription object with an `.off` method that you can call to
- # unregister the callback.
- eachPaneView: (callback) ->
- @panes.eachPaneView(callback)
-
- # Essential: Get all existing pane views.
- #
- # Prefer {Workspace::getPanes} if you don't need access to the view objects.
- # Also consider using {::eachPaneView} if you want to register a callback for
- # all current and *future* pane views.
- #
- # Returns an Array of all open {PaneView}s.
- getPaneViews: ->
- @panes.getPaneViews()
-
- # Essential: Get the active pane view.
- #
- # Prefer {Workspace::getActivePane} if you don't actually need access to the
- # view.
- #
- # Returns a {PaneView}.
- getActivePaneView: ->
- @panes.getActivePaneView()
-
- # Essential: Get the view associated with the active pane item.
- #
- # Returns a view.
- getActiveView: ->
- @panes.getActiveView()
-
- ###
- Section: Adding elements to the workspace
- ###
-
- prependToTop: (element) ->
- deprecate 'Please use Workspace::addTopPanel() instead'
- @vertical.prepend(element)
-
- appendToTop: (element) ->
- deprecate 'Please use Workspace::addTopPanel() instead'
- @panes.before(element)
-
- prependToBottom: (element) ->
- deprecate 'Please use Workspace::addBottomPanel() instead'
- @panes.after(element)
-
- appendToBottom: (element) ->
- deprecate 'Please use Workspace::addBottomPanel() instead'
- @vertical.append(element)
-
- prependToLeft: (element) ->
- deprecate 'Please use Workspace::addLeftPanel() instead'
- @horizontal.prepend(element)
-
- appendToLeft: (element) ->
- deprecate 'Please use Workspace::addLeftPanel() instead'
- @vertical.before(element)
-
- prependToRight: (element) ->
- deprecate 'Please use Workspace::addRightPanel() instead'
- @vertical.after(element)
-
- appendToRight: (element) ->
- deprecate 'Please use Workspace::addRightPanel() instead'
- @horizontal.append(element)
-
- ###
- Section: Focusing pane views
- ###
-
- # Focus the previous pane by id.
- focusPreviousPaneView: -> @model.activatePreviousPane()
-
- # Focus the next pane by id.
- focusNextPaneView: -> @model.activateNextPane()
-
- # Focus the pane directly above the active pane.
- focusPaneViewAbove: -> @panes.focusPaneViewAbove()
-
- # Focus the pane directly below the active pane.
- focusPaneViewBelow: -> @panes.focusPaneViewBelow()
-
- # Focus the pane directly to the left of the active pane.
- focusPaneViewOnLeft: -> @panes.focusPaneViewOnLeft()
-
- # Focus the pane directly to the right of the active pane.
- focusPaneViewOnRight: -> @panes.focusPaneViewOnRight()
-
- ###
- Section: Private
- ###
-
- # Prompts to save all unsaved items
- confirmClose: ->
- @model.confirmClose()
-
- # Get all editor views.
- #
- # You should prefer {Workspace::getEditors} unless you absolutely need access
- # to the view objects. Also consider using {::eachEditorView}, which will call
- # a callback for all current and *future* editor views.
- #
- # Returns an {Array} of {TextEditorView}s.
- getEditorViews: ->
- for editorElement in @panes.element.querySelectorAll('atom-pane > .item-views > atom-text-editor')
- $(editorElement).view()
-
- ###
- Section: Deprecated
- ###
-
- deprecateViewEvents: ->
- originalWorkspaceViewOn = @on
-
- @on = (eventName) =>
- switch eventName
- when 'beep'
- deprecate('Use Atom::onDidBeep instead')
- when 'cursor:moved'
- deprecate('Use TextEditor::onDidChangeCursorPosition instead')
- when 'editor:attached'
- deprecate('Use Workspace::onDidAddTextEditor instead')
- when 'editor:detached'
- deprecate('Use TextEditor::onDidDestroy instead')
- when 'editor:will-be-removed'
- deprecate('Use TextEditor::onDidDestroy instead')
- when 'pane:active-item-changed'
- deprecate('Use Pane::onDidChangeActiveItem instead')
- when 'pane:active-item-modified-status-changed'
- deprecate('Use Pane::onDidChangeActiveItem and call onDidChangeModified on the active item instead')
- when 'pane:active-item-title-changed'
- deprecate('Use Pane::onDidChangeActiveItem and call onDidChangeTitle on the active item instead')
- when 'pane:attached'
- deprecate('Use Workspace::onDidAddPane instead')
- when 'pane:became-active'
- deprecate('Use Pane::onDidActivate instead')
- when 'pane:became-inactive'
- deprecate('Use Pane::onDidChangeActive instead')
- when 'pane:item-added'
- deprecate('Use Pane::onDidAddItem instead')
- when 'pane:item-moved'
- deprecate('Use Pane::onDidMoveItem instead')
- when 'pane:item-removed'
- deprecate('Use Pane::onDidRemoveItem instead')
- when 'pane:removed'
- deprecate('Use Pane::onDidDestroy instead')
- when 'pane-container:active-pane-item-changed'
- deprecate('Use Workspace::onDidChangeActivePaneItem instead')
- when 'selection:changed'
- deprecate('Use TextEditor::onDidChangeSelectionRange instead')
- when 'uri-opened'
- deprecate('Use Workspace::onDidOpen instead')
- originalWorkspaceViewOn.apply(this, arguments)
-
- TextEditorView = require './text-editor-view'
- originalEditorViewOn = TextEditorView::on
- TextEditorView::on = (eventName) ->
- switch eventName
- when 'cursor:moved'
- deprecate('Use TextEditor::onDidChangeCursorPosition instead')
- when 'editor:attached'
- deprecate('Use TextEditor::onDidAddTextEditor instead')
- when 'editor:detached'
- deprecate('Use TextEditor::onDidDestroy instead')
- when 'editor:will-be-removed'
- deprecate('Use TextEditor::onDidDestroy instead')
- when 'selection:changed'
- deprecate('Use TextEditor::onDidChangeSelectionRange instead')
- originalEditorViewOn.apply(this, arguments)
-
- originalPaneViewOn = PaneView::on
- PaneView::on = (eventName) ->
- switch eventName
- when 'cursor:moved'
- deprecate('Use TextEditor::onDidChangeCursorPosition instead')
- when 'editor:attached'
- deprecate('Use TextEditor::onDidAddTextEditor instead')
- when 'editor:detached'
- deprecate('Use TextEditor::onDidDestroy instead')
- when 'editor:will-be-removed'
- deprecate('Use TextEditor::onDidDestroy instead')
- when 'pane:active-item-changed'
- deprecate('Use Pane::onDidChangeActiveItem instead')
- when 'pane:active-item-modified-status-changed'
- deprecate('Use Pane::onDidChangeActiveItem and call onDidChangeModified on the active item instead')
- when 'pane:active-item-title-changed'
- deprecate('Use Pane::onDidChangeActiveItem and call onDidChangeTitle on the active item instead')
- when 'pane:attached'
- deprecate('Use Workspace::onDidAddPane instead')
- when 'pane:became-active'
- deprecate('Use Pane::onDidActivate instead')
- when 'pane:became-inactive'
- deprecate('Use Pane::onDidChangeActive instead')
- when 'pane:item-added'
- deprecate('Use Pane::onDidAddItem instead')
- when 'pane:item-moved'
- deprecate('Use Pane::onDidMoveItem instead')
- when 'pane:item-removed'
- deprecate('Use Pane::onDidRemoveItem instead')
- when 'pane:removed'
- deprecate('Use Pane::onDidDestroy instead')
- when 'selection:changed'
- deprecate('Use TextEditor::onDidChangeSelectionRange instead')
- originalPaneViewOn.apply(this, arguments)
-
- # Deprecated
- eachPane: (callback) ->
- deprecate("Use WorkspaceView::eachPaneView instead")
- @eachPaneView(callback)
-
- # Deprecated
- getPanes: ->
- deprecate("Use WorkspaceView::getPaneViews instead")
- @getPaneViews()
-
- # Deprecated
- getActivePane: ->
- deprecate("Use WorkspaceView::getActivePaneView instead")
- @getActivePaneView()
-
- # Deprecated: Call {Workspace::getActivePaneItem} instead.
- getActivePaneItem: ->
- deprecate("Use Workspace::getActivePaneItem instead")
- @model.getActivePaneItem()
diff --git a/src/workspace.coffee b/src/workspace.coffee
deleted file mode 100644
index 0e6fb7e406b..00000000000
--- a/src/workspace.coffee
+++ /dev/null
@@ -1,970 +0,0 @@
-{includeDeprecatedAPIs, deprecate} = require 'grim'
-_ = require 'underscore-plus'
-path = require 'path'
-{join} = path
-Q = require 'q'
-Serializable = require 'serializable'
-{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
-Grim = require 'grim'
-fs = require 'fs-plus'
-Model = require './model'
-TextEditor = require './text-editor'
-PaneContainer = require './pane-container'
-Pane = require './pane'
-Panel = require './panel'
-PanelElement = require './panel-element'
-PanelContainer = require './panel-container'
-PanelContainerElement = require './panel-container-element'
-WorkspaceElement = require './workspace-element'
-Task = require './task'
-
-# 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, you'll need to use the
-# {WorkspaceView} class for now until we establish APIs at the model layer.
-#
-# * `editor` {TextEditor} the new editor
-#
-module.exports =
-class Workspace extends Model
- atom.deserializers.add(this)
- Serializable.includeInto(this)
-
- constructor: (params) ->
- super
-
- unless Grim.includeDeprecatedAPIs
- @paneContainer = params?.paneContainer
- @fullScreen = params?.fullScreen ? false
- @destroyedItemURIs = params?.destroyedItemURIs ? []
-
- @emitter = new Emitter
- @openers = []
-
- @paneContainer ?= new PaneContainer()
- @paneContainer.onDidDestroyPaneItem(@didDestroyPaneItem)
-
- @panelContainers =
- top: new PanelContainer({location: 'top'})
- left: new PanelContainer({location: 'left'})
- right: new PanelContainer({location: 'right'})
- bottom: new PanelContainer({location: 'bottom'})
- modal: new PanelContainer({location: 'modal'})
-
- @subscribeToActiveItem()
-
- @addOpener (filePath) ->
- switch filePath
- when 'atom://.atom/stylesheet'
- atom.project.open(atom.styles.getUserStyleSheetPath())
- when 'atom://.atom/keymap'
- atom.project.open(atom.keymaps.getUserKeymapPath())
- when 'atom://.atom/config'
- atom.project.open(atom.config.getUserConfigPath())
- when 'atom://.atom/init-script'
- atom.project.open(atom.getUserInitScriptPath())
-
- atom.views.addViewProvider Workspace, (model) ->
- new WorkspaceElement().initialize(model)
-
- atom.views.addViewProvider PanelContainer, (model) ->
- new PanelContainerElement().initialize(model)
-
- atom.views.addViewProvider Panel, (model) ->
- new PanelElement().initialize(model)
-
- @subscribeToFontSize()
-
- # Called by the Serializable mixin during deserialization
- deserializeParams: (params) ->
- for packageName in params.packagesWithActiveGrammars ? []
- atom.packages.getLoadedPackage(packageName)?.loadGrammarsSync()
-
- params.paneContainer = PaneContainer.deserialize(params.paneContainer)
- params
-
- # Called by the Serializable mixin during serialization.
- serializeParams: ->
- paneContainer: @paneContainer.serialize()
- fullScreen: atom.isFullScreen()
- packagesWithActiveGrammars: @getPackageNamesWithActiveGrammars()
-
- getPackageNamesWithActiveGrammars: ->
- packageNames = []
- addGrammar = ({includedGrammarScopes, packageName}={}) ->
- return unless packageName
- # Prevent cycles
- return if packageNames.indexOf(packageName) isnt -1
-
- packageNames.push(packageName)
- for scopeName in includedGrammarScopes ? []
- addGrammar(atom.grammars.grammarForScopeName(scopeName))
- return
-
- editors = @getTextEditors()
- addGrammar(editor.getGrammar()) for editor in editors
-
- if editors.length > 0
- for grammar in atom.grammars.getGrammars() when grammar.injectionSelector
- addGrammar(grammar)
-
- _.uniq(packageNames)
-
- editorAdded: (editor) ->
- @emit 'editor-created', editor if includeDeprecatedAPIs
-
- installShellCommands: ->
- require('./command-installer').installShellCommandsInteractively()
-
- subscribeToActiveItem: ->
- @updateWindowTitle()
- @updateDocumentEdited()
- atom.project.onDidChangePaths @updateWindowTitle
-
- @observeActivePaneItem (item) =>
- @updateWindowTitle()
- @updateDocumentEdited()
-
- @activeItemSubscriptions?.dispose()
- @activeItemSubscriptions = new CompositeDisposable
-
- if typeof item?.onDidChangeTitle is 'function'
- titleSubscription = item.onDidChangeTitle(@updateWindowTitle)
- else if typeof item?.on is 'function'
- titleSubscription = item.on('title-changed', @updateWindowTitle)
- unless typeof titleSubscription?.dispose is 'function'
- titleSubscription = new Disposable => item.off('title-changed', @updateWindowTitle)
-
- if typeof item?.onDidChangeModified is 'function'
- modifiedSubscription = item.onDidChangeModified(@updateDocumentEdited)
- else if typeof item?.on? is 'function'
- modifiedSubscription = item.on('modified-status-changed', @updateDocumentEdited)
- unless typeof modifiedSubscription?.dispose is 'function'
- modifiedSubscription = new Disposable => item.off('modified-status-changed', @updateDocumentEdited)
-
- @activeItemSubscriptions.add(titleSubscription) if titleSubscription?
- @activeItemSubscriptions.add(modifiedSubscription) if modifiedSubscription?
-
- # Updates the application's title and proxy icon based on whichever file is
- # open.
- updateWindowTitle: =>
- appName = 'Atom'
- projectPaths = atom.project?.getPaths() ? []
- if item = @getActivePaneItem()
- itemPath = item.getPath?()
- itemTitle = item.getTitle?()
- projectPath = _.find projectPaths, (projectPath) ->
- itemPath is projectPath or itemPath?.startsWith(projectPath + path.sep)
- itemTitle ?= "untitled"
- projectPath ?= projectPaths[0]
-
- if item? and projectPath?
- document.title = "#{itemTitle} - #{projectPath} - #{appName}"
- atom.setRepresentedFilename(itemPath ? projectPath)
- else if projectPath?
- document.title = "#{projectPath} - #{appName}"
- atom.setRepresentedFilename(projectPath)
- else
- document.title = "#{itemTitle} - #{appName}"
- atom.setRepresentedFilename("")
-
- # On OS X, fades the application window's proxy icon when the current file
- # has been modified.
- updateDocumentEdited: =>
- modified = @getActivePaneItem()?.isModified?() ? false
- atom.setDocumentEdited(modified)
-
- ###
- Section: Event Subscription
- ###
-
- # 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` 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) ->
- callback(textEditor) for textEditor in @getTextEditors()
- @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) -> @paneContainer.observePaneItems(callback)
-
- # Essential: Invoke the given callback when the active pane item changes.
- #
- # * `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) -> @paneContainer.onDidChangeActivePaneItem(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) -> @paneContainer.observeActivePaneItem(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) ->
- @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) -> @paneContainer.onDidAddPane(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) -> @paneContainer.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) -> @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) -> @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) -> @paneContainer.observeActivePane(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) -> @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) -> @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) -> @paneContainer.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) ->
- @onDidAddPaneItem ({item, pane, index}) ->
- callback({textEditor: item, pane, index}) if item instanceof TextEditor
-
- ###
- 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' or 'right'. 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.
- # * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on
- # containing pane. Defaults to `true`.
- # * `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`.
- #
- # Returns a promise that resolves to the {TextEditor} for the file URI.
- open: (uri, options={}) ->
- searchAllPanes = options.searchAllPanes
- split = options.split
- uri = atom.project.resolvePath(uri)
-
- pane = @paneContainer.paneForURI(uri) if searchAllPanes
- pane ?= switch split
- when 'left'
- @getActivePane().findLeftmostSibling()
- when 'right'
- @getActivePane().findOrCreateRightmostSibling()
- else
- @getActivePane()
-
- @openURIInPane(uri, pane, options)
-
- # Open Atom's license in the active pane.
- openLicense: ->
- @open(join(atom.getLoadSettings().resourcePath, '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`.
- openSync: (uri='', options={}) ->
- # TODO: Remove deprecated changeFocus option
- if includeDeprecatedAPIs and options.changeFocus?
- deprecate("The `changeFocus` option has been renamed to `activatePane`")
- options.activatePane = options.changeFocus
- delete options.changeFocus
-
- {initialLine, initialColumn} = options
- activatePane = options.activatePane ? true
-
- uri = atom.project.resolvePath(uri)
- item = @getActivePane().itemForURI(uri)
- if uri
- item ?= opener(uri, options) for opener in @getOpeners() when not item
- item ?= atom.project.openSync(uri, {initialLine, initialColumn})
-
- @getActivePane().activateItem(item)
- @itemOpened(item)
- @getActivePane().activate() if activatePane
- item
-
- openURIInPane: (uri, pane, options={}) ->
- # TODO: Remove deprecated changeFocus option
- if includeDeprecatedAPIs and options.changeFocus?
- deprecate("The `changeFocus` option has been renamed to `activatePane`")
- options.activatePane = options.changeFocus
- delete options.changeFocus
-
- activatePane = options.activatePane ? true
-
- if uri?
- item = pane.itemForURI(uri)
- item ?= opener(uri, options) for opener in @getOpeners() when not item
-
- try
- item ?= atom.project.open(uri, options)
- catch error
- switch error.code
- when 'EFILETOOLARGE'
- atom.notifications.addWarning("#{error.message} Large file support is being tracked at [atom/atom#307](https://github.com/atom/atom/issues/307).")
- when 'EACCES'
- atom.notifications.addWarning("Permission denied '#{error.path}'")
- when 'EPERM', 'EBUSY'
- atom.notifications.addWarning("Unable to open '#{error.path}'", detail: error.message)
- else
- throw error
- return Q()
-
- Q(item)
- .then (item) =>
- if not pane
- pane = new Pane(items: [item])
- @paneContainer.root = pane
- @itemOpened(item)
- pane.activateItem(item)
- pane.activate() if activatePane
- if options.initialLine? or options.initialColumn?
- item.setCursorBufferPosition?([options.initialLine, options.initialColumn])
- index = pane.getActiveItemIndex()
- @emit "uri-opened" if includeDeprecatedAPIs
- @emitter.emit 'did-open', {uri, pane, item, index}
- item
-
- # 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: ->
- if uri = @destroyedItemURIs.pop()
- @open(uri)
- else
- Q()
-
- # Public: Register an opener for a uri.
- #
- # An {TextEditor} will be used if no openers return 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.
- addOpener: (opener) ->
- if includeDeprecatedAPIs
- packageName = @getCallingPackageName()
-
- wrappedOpener = (uri, options) ->
- item = opener(uri, options)
- if item? and typeof item.getUri is 'function' and typeof item.getURI isnt 'function'
- Grim.deprecate("Pane item with class `#{item.constructor.name}` should implement `::getURI` instead of `::getUri`.", {packageName})
- if item? and typeof item.on is 'function' and typeof item.onDidChangeTitle isnt 'function'
- Grim.deprecate("If you would like your pane item with class `#{item.constructor.name}` to support title change behavior, please implement a `::onDidChangeTitle()` method. `::on` methods for items are no longer supported. If not, ignore this message.", {packageName})
- if item? and typeof item.on is 'function' and typeof item.onDidChangeModified isnt 'function'
- Grim.deprecate("If you would like your pane item with class `#{item.constructor.name}` to support modified behavior, please implement a `::onDidChangeModified()` method. If not, ignore this message. `::on` methods for items are no longer supported.", {packageName})
- item
-
- @openers.push(wrappedOpener)
- new Disposable => _.remove(@openers, wrappedOpener)
- else
- @openers.push(opener)
- new Disposable => _.remove(@openers, opener)
-
- getOpeners: ->
- @openers
-
- ###
- Section: Pane Items
- ###
-
- # Essential: Get all pane items in the workspace.
- #
- # Returns an {Array} of items.
- getPaneItems: ->
- @paneContainer.getPaneItems()
-
- # Essential: Get the active {Pane}'s active item.
- #
- # Returns an pane item {Object}.
- getActivePaneItem: ->
- @paneContainer.getActivePaneItem()
-
- # Essential: Get all text editors in the workspace.
- #
- # Returns an {Array} of {TextEditor}s.
- getTextEditors: ->
- @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: ->
- activeItem = @getActivePaneItem()
- activeItem if activeItem instanceof TextEditor
-
- # Save all pane items.
- saveAll: ->
- @paneContainer.saveAll()
-
- confirmClose: (options) ->
- @paneContainer.confirmClose(options)
-
- # 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: ->
- @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: ->
- @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: ->
- @getActivePane().destroyActiveItem()
-
- ###
- Section: Panes
- ###
-
- # Extended: Get all panes in the workspace.
- #
- # Returns an {Array} of {Pane}s.
- getPanes: ->
- @paneContainer.getPanes()
-
- # Extended: Get the active {Pane}.
- #
- # Returns a {Pane}.
- getActivePane: ->
- @paneContainer.getActivePane()
-
- # Extended: Make the next pane active.
- activateNextPane: ->
- @paneContainer.activateNextPane()
-
- # Extended: Make the previous pane active.
- activatePreviousPane: ->
- @paneContainer.activatePreviousPane()
-
- # Extended: Get the first {Pane} with an item for the given URI.
- #
- # * `uri` {String} uri
- #
- # Returns a {Pane} or `undefined` if no pane exists for the given URI.
- paneForURI: (uri) ->
- @paneContainer.paneForURI(uri)
-
- # Extended: Get the {Pane} containing the given item.
- #
- # * `item` Item the returned pane contains.
- #
- # Returns a {Pane} or `undefined` if no pane exists for the given item.
- paneForItem: (item) ->
- @paneContainer.paneForItem(item)
-
- # Destroy (close) the active pane.
- destroyActivePane: ->
- @getActivePane()?.destroy()
-
- # Destroy the active pane item or the active pane if it is empty.
- destroyActivePaneItemOrEmptyPane: ->
- if @getActivePaneItem()? then @destroyActivePaneItem() else @destroyActivePane()
-
- # Increase the editor font size by 1px.
- increaseFontSize: ->
- atom.config.set("editor.fontSize", atom.config.get("editor.fontSize") + 1)
-
- # Decrease the editor font size by 1px.
- decreaseFontSize: ->
- fontSize = atom.config.get("editor.fontSize")
- atom.config.set("editor.fontSize", fontSize - 1) if fontSize > 1
-
- # Restore to the window's original editor font size.
- resetFontSize: ->
- if @originalFontSize
- atom.config.set("editor.fontSize", @originalFontSize)
-
- subscribeToFontSize: ->
- atom.config.onDidChange 'editor.fontSize', ({oldValue}) =>
- @originalFontSize ?= oldValue
-
- # Removes the item's uri from the list of potential items to reopen.
- itemOpened: (item) ->
- if typeof item.getURI is 'function'
- uri = item.getURI()
- else if typeof item.getUri is 'function'
- uri = item.getUri()
-
- if uri?
- _.remove(@destroyedItemURIs, uri)
-
- # Adds the destroyed item's uri to the list of items to reopen.
- didDestroyPaneItem: ({item}) =>
- if typeof item.getURI is 'function'
- uri = item.getURI()
- else if typeof item.getUri is 'function'
- uri = item.getUri()
-
- if uri?
- @destroyedItemURIs.push(uri)
-
- # Called by Model superclass when destroyed
- destroyed: ->
- @paneContainer.destroy()
- @activeItemSubscriptions?.dispose()
-
-
- ###
- 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: ->
- @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) ->
- @addPanel('bottom', options)
-
- # Essential: Get an {Array} of all the panel items to the left of the editor window.
- getLeftPanels: ->
- @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) ->
- @addPanel('left', options)
-
- # Essential: Get an {Array} of all the panel items to the right of the editor window.
- getRightPanels: ->
- @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) ->
- @addPanel('right', options)
-
- # Essential: Get an {Array} of all the panel items at the top of the editor window.
- getTopPanels: ->
- @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) ->
- @addPanel('top', options)
-
- # Essential: Get an {Array} of all the modal panel items
- getModalPanels: ->
- @getPanels('modal')
-
- # Essential: Adds a panel item as a modal dialog.
- #
- # * `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}
- addModalPanel: (options={}) ->
- @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 location, container of @panelContainers
- panel = container.panelForItem(item)
- return panel if panel?
- null
-
- getPanels: (location) ->
- @panelContainers[location].getPanels()
-
- addPanel: (location, options) ->
- options ?= {}
- @panelContainers[location].addPanel(new Panel(options))
-
- ###
- Section: Searching and Replacing
- ###
-
- # Public: Performs a search across all the files in the workspace.
- #
- # * `regex` {RegExp} to search with.
- # * `options` (optional) {Object} (default: {})
- # * `paths` An {Array} of glob patterns to search within
- # * `iterator` {Function} callback on each file found
- #
- # Returns a `Promise`.
- scan: (regex, options={}, iterator) ->
- if _.isFunction(options)
- iterator = options
- options = {}
-
- deferred = Q.defer()
-
- searchOptions =
- ignoreCase: regex.ignoreCase
- inclusions: options.paths
- includeHidden: true
- excludeVcsIgnores: atom.config.get('core.excludeVcsIgnoredPaths')
- exclusions: atom.config.get('core.ignoredNames')
- follow: atom.config.get('core.followSymlinks')
-
- task = Task.once require.resolve('./scan-handler'), atom.project.getPaths(), regex.source, searchOptions, ->
- deferred.resolve()
-
- task.on 'scan:result-found', (result) ->
- iterator(result) unless atom.project.isPathModified(result.filePath)
-
- task.on 'scan:file-error', (error) ->
- iterator(null, error)
-
- if _.isFunction(options.onPathsSearched)
- task.on 'scan:paths-searched', (numberOfPathsSearched) ->
- options.onPathsSearched(numberOfPathsSearched)
-
- for buffer in atom.project.getBuffers() when buffer.isModified()
- filePath = buffer.getPath()
- continue unless atom.project.contains(filePath)
- matches = []
- buffer.scan regex, (match) -> matches.push match
- iterator {filePath, matches} if matches.length > 0
-
- promise = deferred.promise
- promise.cancel = ->
- task.terminate()
- deferred.resolve('cancelled')
- promise
-
- # Public: Performs a replace across all the specified files in the project.
- #
- # * `regex` A {RegExp} to search with.
- # * `replacementText` Text to replace all matches of regex with
- # * `filePaths` List 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) ->
- deferred = Q.defer()
-
- openPaths = (buffer.getPath() for buffer in atom.project.getBuffers())
- outOfProcessPaths = _.difference(filePaths, openPaths)
-
- inProcessFinished = not openPaths.length
- outOfProcessFinished = not outOfProcessPaths.length
- checkFinished = ->
- deferred.resolve() if outOfProcessFinished and inProcessFinished
-
- unless outOfProcessFinished.length
- flags = 'g'
- flags += 'i' if regex.ignoreCase
-
- 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 in atom.project.getBuffers()
- continue unless buffer.getPath() in filePaths
- replacements = buffer.replace(regex, replacementText, iterator)
- iterator({filePath: buffer.getPath(), replacements}) if replacements
-
- inProcessFinished = true
- checkFinished()
-
- deferred.promise
-
-if includeDeprecatedAPIs
- Workspace.properties
- paneContainer: null
- fullScreen: false
- destroyedItemURIs: -> []
-
- Object.defineProperty Workspace::, 'activePaneItem',
- get: ->
- Grim.deprecate "Use ::getActivePaneItem() instead of the ::activePaneItem property"
- @getActivePaneItem()
-
- Object.defineProperty Workspace::, 'activePane',
- get: ->
- Grim.deprecate "Use ::getActivePane() instead of the ::activePane property"
- @getActivePane()
-
- StackTraceParser = require 'stacktrace-parser'
-
- Workspace::getCallingPackageName = ->
- error = new Error
- Error.captureStackTrace(error)
- stack = StackTraceParser.parse(error.stack)
-
- packagePaths = @getPackagePathsByPackageName()
-
- for i in [0...stack.length]
- stackFramePath = stack[i].file
-
- # Empty when it was run from the dev console
- return unless stackFramePath
-
- for packageName, packagePath of packagePaths
- continue if stackFramePath is 'node.js'
- relativePath = path.relative(packagePath, stackFramePath)
- return packageName unless /^\.\./.test(relativePath)
- return
-
- Workspace::getPackagePathsByPackageName = ->
- packagePathsByPackageName = {}
- for pack in atom.packages.getLoadedPackages()
- packagePath = pack.path
- if packagePath.indexOf('.atom/dev/packages') > -1 or packagePath.indexOf('.atom/packages') > -1
- packagePath = fs.realpathSync(packagePath)
- packagePathsByPackageName[pack.name] = packagePath
- packagePathsByPackageName
-
- Workspace::eachEditor = (callback) ->
- deprecate("Use Workspace::observeTextEditors instead")
-
- callback(editor) for editor in @getEditors()
- @subscribe this, 'editor-created', (editor) -> callback(editor)
-
- Workspace::getEditors = ->
- deprecate("Use Workspace::getTextEditors instead")
-
- editors = []
- for pane in @paneContainer.getPanes()
- editors.push(item) for item in pane.getItems() when item instanceof TextEditor
-
- editors
-
- Workspace::on = (eventName) ->
- switch eventName
- when 'editor-created'
- deprecate("Use Workspace::onDidAddTextEditor or Workspace::observeTextEditors instead.")
- when 'uri-opened'
- deprecate("Use Workspace::onDidOpen or Workspace::onDidAddPaneItem instead. https://atom.io/docs/api/latest/Workspace#instance-onDidOpen")
- else
- deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.")
-
- super
-
- Workspace::reopenItemSync = ->
- deprecate("Use Workspace::reopenItem instead")
- if uri = @destroyedItemURIs.pop()
- @openSync(uri)
-
- Workspace::registerOpener = (opener) ->
- Grim.deprecate("Call Workspace::addOpener instead")
- @addOpener(opener)
-
- Workspace::unregisterOpener = (opener) ->
- Grim.deprecate("Call .dispose() on the Disposable returned from ::addOpener instead")
- _.remove(@openers, opener)
-
- Workspace::getActiveEditor = ->
- Grim.deprecate "Call ::getActiveTextEditor instead"
- @getActivePane()?.getActiveEditor()
-
- Workspace::paneForUri = (uri) ->
- deprecate("Use ::paneForURI instead.")
- @paneForURI(uri)
diff --git a/src/workspace.js b/src/workspace.js
new file mode 100644
index 00000000000..3b040c3b064
--- /dev/null
+++ b/src/workspace.js
@@ -0,0 +1,2271 @@
+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();
+ }
+ }
+ }
+};
diff --git a/static/atom-ui/README.md b/static/atom-ui/README.md
new file mode 100644
index 00000000000..b3c02c7dfd1
--- /dev/null
+++ b/static/atom-ui/README.md
@@ -0,0 +1,13 @@
+# :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:
diff --git a/static/atom-ui/_index.less b/static/atom-ui/_index.less
new file mode 100644
index 00000000000..4a1db024b66
--- /dev/null
+++ b/static/atom-ui/_index.less
@@ -0,0 +1,35 @@
+// 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";
diff --git a/static/atom-ui/styles/badges.less b/static/atom-ui/styles/badges.less
new file mode 100644
index 00000000000..048113d89b0
--- /dev/null
+++ b/static/atom-ui/styles/badges.less
@@ -0,0 +1,64 @@
+@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;
+ }
+
+}
diff --git a/static/atom-ui/styles/button-groups.less b/static/atom-ui/styles/button-groups.less
new file mode 100644
index 00000000000..430522071fe
--- /dev/null
+++ b/static/atom-ui/styles/button-groups.less
@@ -0,0 +1,187 @@
+@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;
+ }
+}
diff --git a/static/atom-ui/styles/buttons.less b/static/atom-ui/styles/buttons.less
new file mode 100644
index 00000000000..5f5c35d9b25
--- /dev/null
+++ b/static/atom-ui/styles/buttons.less
@@ -0,0 +1,274 @@
+@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;
+ }
+}
diff --git a/static/atom-ui/styles/git-status.less b/static/atom-ui/styles/git-status.less
new file mode 100644
index 00000000000..ead7ed73dd3
--- /dev/null
+++ b/static/atom-ui/styles/git-status.less
@@ -0,0 +1,13 @@
+@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; }
+}
diff --git a/static/icons.less b/static/atom-ui/styles/icons.less
similarity index 89%
rename from static/icons.less
rename to static/atom-ui/styles/icons.less
index 4fe56a8c1b1..993ba28f8c3 100644
--- a/static/icons.less
+++ b/static/atom-ui/styles/icons.less
@@ -4,8 +4,7 @@
margin-right: @component-icon-padding;
}
-a.icon,
-button.icon {
+a.icon {
text-decoration: none;
color: @text-color;
&:hover{
diff --git a/static/atom-ui/styles/inputs.less b/static/atom-ui/styles/inputs.less
new file mode 100644
index 00000000000..591de55db0f
--- /dev/null
+++ b/static/atom-ui/styles/inputs.less
@@ -0,0 +1,383 @@
+@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);
+ }
+ }
+}
diff --git a/static/atom-ui/styles/layout.less b/static/atom-ui/styles/layout.less
new file mode 100644
index 00000000000..83560fab966
--- /dev/null
+++ b/static/atom-ui/styles/layout.less
@@ -0,0 +1,85 @@
+@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();
+}
diff --git a/static/atom-ui/styles/lists.less b/static/atom-ui/styles/lists.less
new file mode 100644
index 00000000000..d8a32d9190a
--- /dev/null
+++ b/static/atom-ui/styles/lists.less
@@ -0,0 +1,291 @@
+@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;
+ }
+ }
+ }
+}
diff --git a/static/atom-ui/styles/loading.less b/static/atom-ui/styles/loading.less
new file mode 100644
index 00000000000..dff5b178228
--- /dev/null
+++ b/static/atom-ui/styles/loading.less
@@ -0,0 +1,21 @@
+//
+// 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); }
diff --git a/static/atom-ui/styles/messages.less b/static/atom-ui/styles/messages.less
new file mode 100644
index 00000000000..a679c92cb4f
--- /dev/null
+++ b/static/atom-ui/styles/messages.less
@@ -0,0 +1,41 @@
+@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%;
+ }
+ }
+}
diff --git a/static/atom-ui/styles/mixins/mixins.less b/static/atom-ui/styles/mixins/mixins.less
new file mode 100644
index 00000000000..afd8d51369c
--- /dev/null
+++ b/static/atom-ui/styles/mixins/mixins.less
@@ -0,0 +1,88 @@
+@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;
+}
diff --git a/static/atom-ui/styles/modals.less b/static/atom-ui/styles/modals.less
new file mode 100644
index 00000000000..8857f3a68c9
--- /dev/null
+++ b/static/atom-ui/styles/modals.less
@@ -0,0 +1,83 @@
+@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;
+}
diff --git a/static/atom-ui/styles/panels.less b/static/atom-ui/styles/panels.less
new file mode 100644
index 00000000000..72bfc4bb834
--- /dev/null
+++ b/static/atom-ui/styles/panels.less
@@ -0,0 +1,38 @@
+@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;
+ }
+}
diff --git a/static/atom-ui/styles/private/README.md b/static/atom-ui/styles/private/README.md
new file mode 100644
index 00000000000..a383db44881
--- /dev/null
+++ b/static/atom-ui/styles/private/README.md
@@ -0,0 +1,5 @@
+# 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.
diff --git a/static/atom-ui/styles/private/alerts.less b/static/atom-ui/styles/private/alerts.less
new file mode 100644
index 00000000000..cc32a8c8c8d
--- /dev/null
+++ b/static/atom-ui/styles/private/alerts.less
@@ -0,0 +1,114 @@
+@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);
+}
diff --git a/static/atom-ui/styles/private/close.less b/static/atom-ui/styles/private/close.less
new file mode 100644
index 00000000000..9befb2e9d7a
--- /dev/null
+++ b/static/atom-ui/styles/private/close.less
@@ -0,0 +1,38 @@
+//
+// 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;
+ }
+}
diff --git a/static/atom-ui/styles/private/code.less b/static/atom-ui/styles/private/code.less
new file mode 100644
index 00000000000..a657ca70ee6
--- /dev/null
+++ b/static/atom-ui/styles/private/code.less
@@ -0,0 +1,77 @@
+@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;
+}
diff --git a/static/atom-ui/styles/private/forms.less b/static/atom-ui/styles/private/forms.less
new file mode 100644
index 00000000000..c0172bb0d5c
--- /dev/null
+++ b/static/atom-ui/styles/private/forms.less
@@ -0,0 +1,705 @@
+@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 `