Merge branch 'master' of https://github.com/atom/atom into b3-failing-seed

This commit is contained in:
Linus Eriksson
2018-01-19 19:08:21 +01:00
116 changed files with 9928 additions and 6823 deletions

17
.pairs
View File

@@ -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

View File

@@ -1,3 +1,8 @@
language: python
python:
- "2.7.13"
git:
depth: 10

View File

@@ -40,7 +40,7 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
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]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/4/

View File

@@ -36,7 +36,7 @@ This project and everyone participating in it is governed by the [Atom Code of C
## 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.](http://blog.atom.io/2016/04/19/managing-the-deluge-of-atom-issues.html) You'll get faster results by using the resources below.
> **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.
@@ -45,7 +45,7 @@ We have an official message board with a detailed FAQ and where the community ch
If chat is more your speed, you can join the Atom and Electron Slack team:
* [Join the Atom and Electron Slack Team](http://atom-slack.herokuapp.com/)
* [Join the Atom and Electron Slack Team](https://atom-slack.herokuapp.com/)
* Even though Slack is a chat service, sometimes it takes several hours for community members to respond — please be patient!
* Use the `#atom` channel for general questions or discussion about Atom
* Use the `#electron` channel for questions about Electron
@@ -65,7 +65,7 @@ Atom is intentionally very modular. Nearly every non-editor UI element you inter
![atom-packages](https://cloud.githubusercontent.com/assets/69169/10472281/84fc9792-71d3-11e5-9fd1-19da717df079.png)
To get a sense for the packages that are bundled with Atom, you can go to Settings > Packages within Atom and take a look at the Core Packages section.
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:
@@ -80,8 +80,8 @@ Here's a list of the big ones:
* [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 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 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.
* [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 to publishing packages.
* [atom.io](https://github.com/atom/atom.io) - the repository for feedback on the [Atom.io website](https://atom.io) and the [Atom.io package API](https://github.com/atom/atom/blob/master/docs/apm-rest-api.md) used by [apm](https://github.com/atom/apm).
@@ -119,10 +119,10 @@ Before creating bug reports, please check [this list](#before-submitting-a-bug-r
#### Before Submitting A Bug Report
* **Check the [debugging guide](http://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](http://flight-manual.atom.io/hacking-atom/sections/debugging/#update-to-the-latest-version), if the problem happens when you run Atom in [safe mode](http://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](http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-atom-and-package-settings).
* **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 [FAQs on the forum](https://discuss.atom.io/c/faq)** 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/issues?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.
* **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?
@@ -135,15 +135,15 @@ Explain the problem and include additional details to help maintainers reproduce
* **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](http://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.
* **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](http://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](http://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 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](http://flight-manual.atom.io/hacking-atom/sections/debugging/#diagnose-runtime-performance-problems-with-the-dev-tools-cpu-profiler)?**
* **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.
@@ -155,7 +155,7 @@ Include details about your configuration and environment:
* **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](http://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 [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?
@@ -167,10 +167,10 @@ Before creating enhancement suggestions, please check [this list](#before-submit
#### Before Submitting An Enhancement Suggestion
* **Check the [debugging guide](http://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](http://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](http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-atom-and-package-settings).
* **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/issues?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.
* **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?
@@ -180,7 +180,7 @@ Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com
* **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](http://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.
* **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).
@@ -195,11 +195,11 @@ Unsure where to begin contributing to Atom? You can start by looking through the
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](http://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).
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](http://flight-manual.atom.io):
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]
@@ -210,10 +210,10 @@ Atom Core and all packages can be developed locally. For instructions on how to
* Do not include issue numbers in the PR title
* Include screenshots and animated GIFs in your pull request whenever possible.
* Follow the [JavaScript](#javascript-styleguide) and [CoffeeScript](#coffeescript-styleguide) styleguides.
* Include thoughtfully-worded, well-structured [Jasmine](http://jasmine.github.io/) specs in the `./spec` folder. Run them using `atom --test spec`. See the [Specs Styleguide](#specs-styleguide) below.
* Include thoughtfully-worded, well-structured [Jasmine](https://jasmine.github.io/) specs in the `./spec` folder. Run them using `atom --test spec`. See the [Specs Styleguide](#specs-styleguide) below.
* Document new code based on the [Documentation Styleguide](#documentation-styleguide)
* End all files with a newline
* [Avoid platform-dependent code](http://flight-manual.atom.io/hacking-atom/sections/cross-platform-compatibility/)
* [Avoid platform-dependent code](https://flight-manual.atom.io/hacking-atom/sections/cross-platform-compatibility/)
* Place requires in the following order:
* Built in Node Modules (such as `path`)
* Built in Atom and Electron Modules (such as `atom`, `remote`)
@@ -230,7 +230,7 @@ Atom Core and all packages can be developed locally. For instructions on how to
* Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
* Limit the first line to 72 characters or less
* Reference issues and pull requests liberally after the first line
* When only changing documentation, include `[ci skip]` in the commit description
* 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
@@ -250,7 +250,7 @@ Atom Core and all packages can be developed locally. For instructions on how to
### JavaScript Styleguide
All JavaScript must adhere to [JavaScript Standard Style](http://standardjs.com/).
All JavaScript must adhere to [JavaScript Standard Style](https://standardjs.com/).
* Prefer the object spread operator (`{...anotherObj}`) to `Object.assign()`
* Inline `export`s with expressions whenever possible
@@ -292,7 +292,7 @@ All JavaScript must adhere to [JavaScript Standard Style](http://standardjs.com/
### Specs Styleguide
- Include thoughtfully-worded, well-structured [Jasmine](http://jasmine.github.io/) specs in the `./spec` folder.
- 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.
@@ -337,7 +337,7 @@ disablePackage: (name, options, callback) ->
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/issues?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/issues?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.
[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 have a label from every group or that an issue can't have more than one label from the same group.
@@ -369,7 +369,7 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and
| `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](http://flight-manual.atom.io/)). |
| `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. |
@@ -405,94 +405,94 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and
| `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/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aenhancement
[search-atom-org-label-enhancement]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aenhancement
[search-atom-repo-label-bug]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Abug
[search-atom-org-label-bug]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Abug
[search-atom-repo-label-question]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aquestion
[search-atom-org-label-question]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aquestion
[search-atom-repo-label-feedback]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Afeedback
[search-atom-org-label-feedback]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Afeedback
[search-atom-repo-label-help-wanted]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ahelp-wanted
[search-atom-org-label-help-wanted]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ahelp-wanted
[search-atom-repo-label-beginner]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Abeginner
[search-atom-org-label-beginner]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Abeginner
[search-atom-repo-label-more-information-needed]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Amore-information-needed
[search-atom-org-label-more-information-needed]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Amore-information-needed
[search-atom-repo-label-needs-reproduction]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aneeds-reproduction
[search-atom-org-label-needs-reproduction]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aneeds-reproduction
[search-atom-repo-label-triage-help-needed]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Atriage-help-needed
[search-atom-org-label-triage-help-needed]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Atriage-help-needed
[search-atom-repo-label-windows]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Awindows
[search-atom-org-label-windows]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Awindows
[search-atom-repo-label-linux]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Alinux
[search-atom-org-label-linux]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Alinux
[search-atom-repo-label-mac]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Amac
[search-atom-org-label-mac]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Amac
[search-atom-repo-label-documentation]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Adocumentation
[search-atom-org-label-documentation]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Adocumentation
[search-atom-repo-label-performance]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aperformance
[search-atom-org-label-performance]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aperformance
[search-atom-repo-label-security]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Asecurity
[search-atom-org-label-security]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Asecurity
[search-atom-repo-label-ui]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aui
[search-atom-org-label-ui]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aui
[search-atom-repo-label-api]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aapi
[search-atom-org-label-api]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aapi
[search-atom-repo-label-crash]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Acrash
[search-atom-org-label-crash]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Acrash
[search-atom-repo-label-auto-indent]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aauto-indent
[search-atom-org-label-auto-indent]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aauto-indent
[search-atom-repo-label-encoding]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aencoding
[search-atom-org-label-encoding]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aencoding
[search-atom-repo-label-network]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Anetwork
[search-atom-org-label-network]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Anetwork
[search-atom-repo-label-uncaught-exception]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Auncaught-exception
[search-atom-org-label-uncaught-exception]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Auncaught-exception
[search-atom-repo-label-git]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Agit
[search-atom-org-label-git]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Agit
[search-atom-repo-label-blocked]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ablocked
[search-atom-org-label-blocked]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ablocked
[search-atom-repo-label-duplicate]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aduplicate
[search-atom-org-label-duplicate]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aduplicate
[search-atom-repo-label-wontfix]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Awontfix
[search-atom-org-label-wontfix]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Awontfix
[search-atom-repo-label-invalid]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ainvalid
[search-atom-org-label-invalid]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ainvalid
[search-atom-repo-label-package-idea]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Apackage-idea
[search-atom-org-label-package-idea]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Apackage-idea
[search-atom-repo-label-wrong-repo]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Awrong-repo
[search-atom-org-label-wrong-repo]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Awrong-repo
[search-atom-repo-label-editor-rendering]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aeditor-rendering
[search-atom-org-label-editor-rendering]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aeditor-rendering
[search-atom-repo-label-build-error]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Abuild-error
[search-atom-org-label-build-error]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Abuild-error
[search-atom-repo-label-error-from-pathwatcher]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aerror-from-pathwatcher
[search-atom-org-label-error-from-pathwatcher]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aerror-from-pathwatcher
[search-atom-repo-label-error-from-save]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aerror-from-save
[search-atom-org-label-error-from-save]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aerror-from-save
[search-atom-repo-label-error-from-open]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aerror-from-open
[search-atom-org-label-error-from-open]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aerror-from-open
[search-atom-repo-label-installer]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ainstaller
[search-atom-org-label-installer]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ainstaller
[search-atom-repo-label-auto-updater]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aauto-updater
[search-atom-org-label-auto-updater]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aauto-updater
[search-atom-repo-label-deprecation-help]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Adeprecation-help
[search-atom-org-label-deprecation-help]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Adeprecation-help
[search-atom-repo-label-electron]: https://github.com/issues?q=is%3Aissue+repo%3Aatom%2Fatom+is%3Aopen+label%3Aelectron
[search-atom-org-label-electron]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aelectron
[search-atom-repo-label-work-in-progress]: https://github.com/pulls?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Awork-in-progress
[search-atom-org-label-work-in-progress]: https://github.com/pulls?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Awork-in-progress
[search-atom-repo-label-needs-review]: https://github.com/pulls?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Aneeds-review
[search-atom-org-label-needs-review]: https://github.com/pulls?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Aneeds-review
[search-atom-repo-label-under-review]: https://github.com/pulls?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Aunder-review
[search-atom-org-label-under-review]: https://github.com/pulls?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Aunder-review
[search-atom-repo-label-requires-changes]: https://github.com/pulls?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Arequires-changes
[search-atom-org-label-requires-changes]: https://github.com/pulls?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Arequires-changes
[search-atom-repo-label-needs-testing]: https://github.com/pulls?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Aneeds-testing
[search-atom-org-label-needs-testing]: https://github.com/pulls?q=is%3Aopen+is%3Apr+user%3Aatom+label%3Aneeds-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/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3Abeginner+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc
[help-wanted]:https://github.com/issues?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc+-label%3Abeginner
[contributing-to-official-atom-packages]:http://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/
[hacking-on-atom-core]: http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/
[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/

View File

@@ -9,8 +9,8 @@ Do you want to ask a question? Are you looking for support? The Atom message boa
### Prerequisites
* [ ] Put an X between the brackets on this line if you have done all of the following:
* Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode
* Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/
* Reproduced the problem in Safe Mode: https://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode
* Followed all applicable steps in the debugging guide: https://flight-manual.atom.io/hacking-atom/sections/debugging/
* Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq
* Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom
* Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages

View File

@@ -1,4 +1,4 @@
Copyright (c) 2011-2017 GitHub Inc.
Copyright (c) 2011-2018 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

View File

@@ -27,6 +27,20 @@ We must be able to understand the design of your change from this description. I
<!-- What are the possible side-effects or negative impacts of the code change? -->
### Verification Process
<!--
What process did you follow to verify that your change has the desired effects?
- How did you verify that all new functionality works as expected?
- How did you verify that all changed functionality works as expected?
- How did you verify that the change has not introduced any regressions?
Describe the actions you performed (e.g., buttons you clicked, text you typed, commands you ran, etc.), and describe the results you observed.
-->
### Applicable Issues
<!-- Enter any applicable Issues here -->

View File

@@ -33,7 +33,7 @@ Atom will automatically update when a new release is available.
### Windows
Download the latest [Atom installer](https://github.com/atom/atom/releases/latest). AtomSetup.exe is 32-bit, AtomSetup-x64.exe for 64-bit systems.
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.
@@ -42,27 +42,11 @@ The `.zip` version will not automatically update.
Using [Chocolatey](https://chocolatey.org)? Run `cinst Atom` to install the latest version of Atom.
### Debian based (Debian, Ubuntu, Linux Mint)
### Linux
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.
The Linux version does not currently automatically update so you will need to
repeat these steps to upgrade to future releases.
### RPM based (Red Hat, openSUSE, Fedora, CentOS)
Atom is only available for 64-bit Linux systems.
1. Download `atom.x86_64.rpm` from the [Atom releases page](https://github.com/atom/atom/releases/latest).
2. Run `sudo rpm -i atom.x86_64.rpm` on the downloaded package.
3. Launch Atom using the installed `atom` command.
The Linux version does not currently automatically update so you will need to
repeat these steps to upgrade to future releases.
Configure your distribution's package manager to install and update Atom by following the [Linux installation instructions](https://flight-manual.atom.io/getting-started/sections/installing-atom/#platform-linux) in the Flight Manual. You will also find instructions on how to install Atom's official Linux packages without using a package repository, though you will not get automatic updates after installing Atom this way.
### Archive extraction
@@ -82,9 +66,9 @@ repeat these steps to upgrade to future releases.
## Building
* [FreeBSD](./docs/build-instructions/freebsd.md)
* [Linux](http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-linux)
* [macOS](http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-mac)
* [Windows](http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-windows)
* [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)
## License

View File

@@ -2,10 +2,10 @@
If you're looking for support for Atom there are a lot of options, check out:
* User Documentation &mdash; [The Atom Flight Manual](http://flight-manual.atom.io)
* User Documentation &mdash; [The Atom Flight Manual](https://flight-manual.atom.io)
* Developer Documentation &mdash; [Atom API Documentation](https://atom.io/docs/api/latest)
* FAQ &mdash; [The Atom FAQ on Discuss](https://discuss.atom.io/c/faq)
* Message Board &mdash; [Discuss, the official Atom and Electron message board](https://discuss.atom.io)
* Chat &mdash; [Join the Atom Slack team](http://atom-slack.herokuapp.com/)
* Chat &mdash; [Join the Atom Slack team](https://atom-slack.herokuapp.com/)
On Discuss and in the Atom Slack team, there are a bunch of helpful community members that should be willing to point you in the right direction.

View File

@@ -6,6 +6,6 @@
"url": "https://github.com/atom/atom.git"
},
"dependencies": {
"atom-package-manager": "1.18.10"
"atom-package-manager": "1.19.0"
}
}

View File

@@ -43,13 +43,19 @@ build_script:
- SET SQUIRREL_TEMP=C:\tmp
- IF [%TASK%]==[installer] (
IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] (
ECHO Building on release branch - Creating production artifacts &&
script\build.cmd --code-sign --compress-artifacts --create-windows-installer
) ELSE (
ECHO Skipping installer and Atom build on non-release branch
IF [%APPVEYOR_REPO_BRANCH%]==[master] IF NOT DEFINED APPVEYOR_PULL_REQUEST_NUMBER (
ECHO Building on master branch - Creating signed zips &&
script\build.cmd --code-sign --compress-artifacts
) ELSE (
ECHO Skipping installer build for non-release/non-master branch
)
)
) ELSE (
ECHO Skipping installer build on non-installer build matrix row &&
script\build.cmd --code-sign --compress-artifacts
ECHO Test build only - Not creating artifacts &&
script\build.cmd
)
test_script:

34
atom.sh
View File

@@ -9,11 +9,17 @@ else
exit 1
fi
if [ "$(basename $0)" == 'atom-beta' ]; then
BETA_VERSION=true
else
BETA_VERSION=
fi
case $(basename $0) in
atom-beta)
CHANNEL=beta
;;
atom-dev)
CHANNEL=dev
;;
*)
CHANNEL=stable
;;
esac
export ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT=true
@@ -67,7 +73,7 @@ if [ $OS == 'Mac' ]; then
ATOM_APP_NAME="$(basename "$ATOM_APP")"
fi
if [ -n "$BETA_VERSION" ]; then
if [ "$CHANNEL" == 'beta' ]; then
ATOM_EXECUTABLE_NAME="Atom Beta"
else
ATOM_EXECUTABLE_NAME="Atom"
@@ -101,11 +107,17 @@ elif [ $OS == 'Linux' ]; then
SCRIPT=$(readlink -f "$0")
USR_DIRECTORY=$(readlink -f $(dirname $SCRIPT)/..)
if [ -n "$BETA_VERSION" ]; then
ATOM_PATH="$USR_DIRECTORY/share/atom-beta/atom"
else
ATOM_PATH="$USR_DIRECTORY/share/atom/atom"
fi
case $CHANNEL in
beta)
ATOM_PATH="$USR_DIRECTORY/share/atom-beta/atom"
;;
dev)
ATOM_PATH="$USR_DIRECTORY/share/atom-dev/atom"
;;
*)
ATOM_PATH="$USR_DIRECTORY/share/atom/atom"
;;
esac
ATOM_HOME="${ATOM_HOME:-$HOME/.atom}"
mkdir -p "$ATOM_HOME"

View File

@@ -1,11 +1,9 @@
/** @babel */
const Chart = require('chart.js')
const glob = require('glob')
const fs = require('fs-plus')
const path = require('path')
import Chart from 'chart.js'
import glob from 'glob'
import fs from 'fs-plus'
import path from 'path'
export default async function ({test, benchmarkPaths}) {
module.exports = async ({test, benchmarkPaths}) => {
document.body.style.backgroundColor = '#ffffff'
document.body.style.overflow = 'auto'

View File

@@ -1,6 +1,4 @@
/** @babel */
import {TextEditor, TextBuffer} from 'atom'
const {TextEditor, TextBuffer} = require('atom')
const MIN_SIZE_IN_KB = 0 * 1024
const MAX_SIZE_IN_KB = 10 * 1024
@@ -8,7 +6,7 @@ const SIZE_STEP_IN_KB = 1024
const LINE_TEXT = 'Lorem ipsum dolor sit amet\n'
const TEXT = LINE_TEXT.repeat(Math.ceil(MAX_SIZE_IN_KB * 1024 / LINE_TEXT.length))
export default async function ({test}) {
module.exports = async ({test}) => {
const data = []
document.body.appendChild(atom.workspace.getElement())
@@ -27,6 +25,7 @@ export default async function ({test}) {
let t0 = window.performance.now()
const buffer = new TextBuffer({text})
const editor = new TextEditor({buffer, autoHeight: false, largeFileMode: true})
atom.grammars.autoAssignLanguageMode(buffer)
atom.workspace.getActivePane().activateItem(editor)
let t1 = window.performance.now()

View File

@@ -1,8 +1,6 @@
/** @babel */
import path from 'path'
import fs from 'fs'
import {TextEditor, TextBuffer} from 'atom'
const path = require('path')
const fs = require('fs')
const {TextEditor, TextBuffer} = require('atom')
const SIZES_IN_KB = [
512,
@@ -12,7 +10,7 @@ const SIZES_IN_KB = [
const REPEATED_TEXT = fs.readFileSync(path.join(__dirname, '..', 'spec', 'fixtures', 'sample.js'), 'utf8').replace(/\n/g, '')
const TEXT = REPEATED_TEXT.repeat(Math.ceil(SIZES_IN_KB[SIZES_IN_KB.length - 1] * 1024 / REPEATED_TEXT.length))
export default async function ({test}) {
module.exports = async ({test}) => {
const data = []
const workspaceElement = atom.workspace.getElement()
@@ -34,7 +32,7 @@ export default async function ({test}) {
let t0 = window.performance.now()
const buffer = new TextBuffer({text})
const editor = new TextEditor({buffer, autoHeight: false, largeFileMode: true})
editor.setGrammar(atom.grammars.grammarForScopeName('source.js'))
atom.grammars.assignLanguageMode(buffer, 'source.js')
atom.workspace.getActivePane().activateItem(editor)
let t1 = window.performance.now()

View File

@@ -2,20 +2,20 @@
![Atom](https://cloud.githubusercontent.com/assets/72919/2874231/3af1db48-d3dd-11e3-98dc-6066f8bc766f.png)
Most of the Atom user and developer documentation is contained in the [Atom Flight Manual](https://github.com/atom/flight-manual.atom.io) repository.
In this directory you can only find very specific build and API level documentation. Some of this may eventually move to the Flight Manual as well.
Most of the Atom user and developer documentation is contained in the [Atom Flight Manual](https://github.com/atom/flight-manual.atom.io).
## Build documentation
Instructions for building Atom on various platforms from source.
* [macOS](./build-instructions/macOS.md)
* [Windows](./build-instructions/windows.md)
* [Linux](./build-instructions/linux.md)
* [FreeBSD](./build-instructions/freebsd.md)
* Moved to [the Flight Manual](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/)
* Linux
* macOS
* Windows
## Other documentation here
## Other documentation
* [apm REST API](./apm-rest-api.md)
* [Tips for contributing to packages](./contributing-to-packages.md)
[Native Profiling on macOS](./native-profiling.md)
The other documentation that was listed here previously has been moved to [the Flight Manual](https://flight-manual.atom.io).

View File

@@ -1,285 +1,3 @@
# Atom.io package and update API
This guide describes the web API used by [apm](https://github.com/atom/apm) and
Atom. The vast majority of use cases are met by the `apm` command-line tool,
which does other useful things like incrementing your version in `package.json`
and making sure you have pushed your git tag. In fact, Atom itself shells out to
`apm` rather than hitting the API directly. If you're curious about how Atom
uses `apm`, see the [PackageManager class](https://github.com/atom/settings-view/blob/master/lib/package-manager.coffee)
in the `settings-view` package.
*This API should be considered pre-release and is subject to change (though significant breaking changes are unlikely).*
### Authorization
For calls to the API that require authentication, provide a valid token from your
[Atom.io account page](https://atom.io/account) in the `Authorization` header.
### Media type
All requests that take parameters require `application/json`.
# API Resources
## Packages
### Listing packages
#### GET /api/packages
Parameters:
- **page** (optional)
- **sort** (optional) - One of `downloads`, `created_at`, `updated_at`, `stars`. Defaults to `downloads`
- **direction** (optional) - `asc` or `desc`. Defaults to `desc`. `stars` can only be ordered `desc`
Returns a list of all packages in the following format:
```json
[
{
"releases": {
"latest": "0.6.0"
},
"name": "thedaniel-test-package",
"repository": {
"type": "git",
"url": "https://github.com/thedaniel/test-package"
}
},
...
]
```
Results are paginated 30 at a time, and links to the next and last pages are
provided in the `Link` header:
```
Link: <https://www.atom.io/api/packages?page=1>; rel="self",
<https://www.atom.io/api/packages?page=41>; rel="last",
<https://www.atom.io/api/packages?page=2>; rel="next"
```
By default, results are sorted by download count, descending.
### Searching packages
#### GET /api/packages/search
Parameters:
- **q** (required) - Search query
- **page** (optional)
- **sort** (optional) - One of `downloads`, `created_at`, `updated_at`, `stars`. Defaults to the relevance of the search query.
- **direction** (optional) - `asc` or `desc`. Defaults to `desc`.
Returns results in the same format as [listing packages](#listing-packages).
### Showing package details
#### GET /api/packages/:package_name
Returns package details and versions for a single package
Parameters:
- **engine** (optional) - Only show packages with versions compatible with this
Atom version. Must be valid [SemVer](http://semver.org).
Returns:
```json
{
"releases": {
"latest": "0.6.0"
},
"name": "thedaniel-test-package",
"repository": {
"type": "git",
"url": "https://github.com/thedaniel/test-package"
},
"versions": [
(see single version output below)
...,
]
}
```
### Creating a package
#### POST /api/packages
Create a new package; requires authentication.
The name and version will be fetched from the `package.json`
file in the specified repository. The authenticating user *must* have access
to the indicated repository.
Parameters:
- **repository** - String. The repository containing the plugin, in the form "owner/repo"
Returns:
- **201** - Successfully created, returns created package.
- **400** - Repository is inaccessible, nonexistent, not an atom package. Possible
error messages include:
- That repo does not exist, isn't an atom package, or atombot does not have access
- The package.json at owner/repo isn't valid
- **409** - A package by that name already exists
### Deleting a package
#### DELETE /api/packages/:package_name
Delete a package; requires authentication.
Returns:
- **204** - Success
- **400** - Repository is inaccessible
- **401** - Unauthorized
### Renaming a package
Packages are renamed by publishing a new version with the name changed in `package.json`
See [Creating a new package version](#creating-a-new-package-version) for details.
Requests made to the previous name will forward to the new name.
### Package Versions
#### GET /api/packages/:package_name/versions/:version_name
Returns `package.json` with `dist` key added for e.g. tarball download:
```json
{
"bugs": {
"url": "https://github.com/thedaniel/test-package/issues"
},
"dependencies": {
"async": "~0.2.6",
"pegjs": "~0.7.0",
"season": "~0.13.0"
},
"description": "Expand snippets matching the current prefix with `tab`.",
"dist": {
"tarball": "https://codeload.github.com/..."
},
"engines": {
"atom": "*"
},
"main": "./lib/snippets",
"name": "thedaniel-test-package",
"publishConfig": {
"registry": "https://...",
},
"repository": {
"type": "git",
"url": "https://github.com/thedaniel/test-package.git"
},
"version": "0.6.0"
}
```
### Creating a new package version
#### POST /api/packages/:package_name/versions
Creates a new package version from a git tag; requires authentication. If `rename`
is not `true`, the `name` field in `package.json` *must* match the current package
name.
#### Parameters
- **tag** - A git tag for the version you'd like to create. It's important to note
that the version name will not be taken from the tag, but from the `version`
key in the `package.json` file at that ref. The authenticating user *must* have
access to the package repository.
- **rename** - Boolean indicating whether this version contains a new name for the package.
#### Returns
- **201** - Successfully created. Returns created version.
- **400** - Git tag not found / Repository inaccessible / package.json invalid
- **409** - Version exists
### Deleting a version
#### DELETE /api/packages/:package_name/versions/:version_name
Deletes a package version; requires authentication.
Note that a version cannot be republished with a different tag if it is deleted.
If you need to delete the latest version of a package for e.g. security reasons,
you'll need to increment the version when republishing.
Returns 204 No Content
## Stars
### Listing user stars
#### GET /api/users/:login/stars
List a user's starred packages.
Return value is similar to **GET /api/packages**
#### GET /api/stars
List the authenticated user's starred packages; requires authentication.
Return value is similar to **GET /api/packages**
### Starring a package
#### POST /api/packages/:name/star
Star a package; requires authentication.
Returns a package.
### Unstarring a package
#### DELETE /api/packages/:name/star
Unstar a package; requires authentication.
Returns 204 No Content.
### Listing a package's stargazers
#### GET /api/packages/:name/stargazers
List the users that have starred a package.
Returns a list of user objects:
```json
[
{"login":"aperson"},
{"login":"anotherperson"},
]
```
## Atom updates
### Listing Atom updates
#### GET /api/updates
Atom update feed, following the format expected by [Squirrel](https://github.com/Squirrel/).
Returns:
```json
{
"name": "0.96.0",
"notes": "[HTML release notes]",
"pub_date": "2014-05-19T15:52:06.000Z",
"url": "https://www.atom.io/api/updates/download"
}
```
The information that was here has been moved to [a permanent home inside Atom's Flight Manual.](https://flight-manual.atom.io/atom-server-side-apis/)

View File

@@ -114,4 +114,4 @@
| [TODO](https://github.com/atom/language-todo) | [![macOS Build Status](https://travis-ci.org/atom/language-todo.svg?branch=master)](https://travis-ci.org/atom/language-todo) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/gcgb9m7h146lv6qp/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-todo/branch/master) |
| [TOML](https://github.com/atom/language-toml) | [![macOS Build Status](https://travis-ci.org/atom/language-toml.svg?branch=master)](https://travis-ci.org/atom/language-toml) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/kohao3fjyk6xv0sc/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-toml/branch/master) |
| [XML](https://github.com/atom/language-xml) | [![macOS Build Status](https://travis-ci.org/atom/language-xml.svg?branch=master)](https://travis-ci.org/atom/language-xml) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/m5f6rn74a6h3q5uq/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-xml/branch/master) |
| [YAML](https://github/atom/language-yaml) | [![macOS Build Status](https://travis-ci.org/atom/language-yaml.svg?branch=master)](https://travis-ci.org/atom/language-yaml) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/eaa4ql7kipgphc2n/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-yaml/branch/master) |
| [YAML](https://github.com/atom/language-yaml) | [![macOS Build Status](https://travis-ci.org/atom/language-yaml.svg?branch=master)](https://travis-ci.org/atom/language-yaml) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/eaa4ql7kipgphc2n/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-yaml/branch/master) |

View File

@@ -1 +1 @@
See the [Hacking on Atom Core](http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-linux) section in the [Atom Flight Manual](http://flight-manual.atom.io).
See the [Hacking on Atom Core](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-linux) section in the [Atom Flight Manual](https://flight-manual.atom.io).

View File

@@ -1 +1 @@
See the [Hacking on Atom Core](http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-mac) section in the [Atom Flight Manual](http://flight-manual.atom.io).
See the [Hacking on Atom Core](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-mac) section in the [Atom Flight Manual](https://flight-manual.atom.io).

View File

@@ -1 +1 @@
See the [Hacking on Atom Core](http://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-windows) section in the [Atom Flight Manual](http://flight-manual.atom.io).
See the [Hacking on Atom Core](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/#platform-windows) section in the [Atom Flight Manual](https://flight-manual.atom.io).

View File

@@ -1 +1 @@
See http://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/
See https://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/

View File

@@ -133,6 +133,8 @@
'cmd-ctrl-left': 'editor:move-selection-left'
'cmd-ctrl-right': 'editor:move-selection-right'
'cmd-shift-V': 'editor:paste-without-reformatting'
'alt-up': 'editor:select-larger-syntax-node'
'alt-down': 'editor:select-smaller-syntax-node'
# Emacs
'alt-f': 'editor:move-to-end-of-word'

View File

@@ -1,7 +1,7 @@
{
"name": "atom",
"productName": "Atom",
"version": "1.24.0-dev",
"version": "1.25.0-dev",
"description": "A hackable text editor for the 21st Century.",
"main": "./src/main-process/main.js",
"repository": {
@@ -12,13 +12,13 @@
"url": "https://github.com/atom/atom/issues"
},
"license": "MIT",
"electronVersion": "1.6.15",
"electronVersion": "1.7.10",
"dependencies": {
"@atom/nsfw": "^1.0.18",
"@atom/source-map-support": "^0.3.4",
"async": "0.2.6",
"atom-keymap": "8.2.8",
"atom-select-list": "^0.1.0",
"atom-select-list": "^0.7.0",
"atom-ui": "0.4.1",
"babel-core": "5.8.38",
"cached-run-in-this-context": "0.4.1",
@@ -38,7 +38,7 @@
"fs-plus": "^3.0.1",
"fstream": "0.1.24",
"fuzzaldrin": "^2.1",
"git-utils": "5.1.0",
"git-utils": "5.2.1",
"glob": "^7.1.1",
"grim": "1.5.0",
"jasmine-json": "~0.0",
@@ -70,103 +70,104 @@
"service-hub": "^0.7.4",
"sinon": "1.17.4",
"temp": "^0.8.3",
"text-buffer": "13.8.3",
"text-buffer": "13.11.5",
"tree-sitter": "^0.8.6",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"winreg": "^1.2.1",
"yargs": "^3.23.0"
},
"packageDependencies": {
"atom-dark-syntax": "0.28.0",
"atom-dark-ui": "0.53.0",
"atom-dark-syntax": "0.29.0",
"atom-dark-ui": "0.53.1",
"atom-light-syntax": "0.29.0",
"atom-light-ui": "0.46.0",
"atom-light-ui": "0.46.1",
"base16-tomorrow-dark-theme": "1.5.0",
"base16-tomorrow-light-theme": "1.5.0",
"one-dark-ui": "1.10.8",
"one-light-ui": "1.10.8",
"one-dark-syntax": "1.8.0",
"one-light-syntax": "1.8.0",
"solarized-dark-syntax": "1.1.2",
"solarized-light-syntax": "1.1.2",
"about": "1.7.8",
"archive-view": "0.64.1",
"autocomplete-atom-api": "0.10.5",
"autocomplete-css": "0.17.4",
"autocomplete-html": "0.8.3",
"autocomplete-plus": "2.37.3",
"autocomplete-snippets": "1.11.2",
"autoflow": "0.29.0",
"one-dark-ui": "1.10.10",
"one-light-ui": "1.10.10",
"one-dark-syntax": "1.8.2",
"one-light-syntax": "1.8.2",
"solarized-dark-syntax": "1.1.4",
"solarized-light-syntax": "1.1.4",
"about": "1.8.0",
"archive-view": "0.64.2",
"autocomplete-atom-api": "0.10.6",
"autocomplete-css": "0.17.5",
"autocomplete-html": "0.8.4",
"autocomplete-plus": "2.40.2",
"autocomplete-snippets": "1.12.0",
"autoflow": "0.29.3",
"autosave": "0.24.6",
"background-tips": "0.27.1",
"bookmarks": "0.44.4",
"bracket-matcher": "0.88.0",
"command-palette": "0.42.0",
"bookmarks": "0.45.1",
"bracket-matcher": "0.89.0",
"command-palette": "0.43.0",
"dalek": "0.2.1",
"deprecation-cop": "0.56.9",
"dev-live-reload": "0.47.1",
"encoding-selector": "0.23.7",
"exception-reporting": "0.41.5",
"find-and-replace": "0.214.0",
"fuzzy-finder": "1.7.3",
"github": "0.8.2",
"git-diff": "1.3.6",
"dev-live-reload": "0.48.1",
"encoding-selector": "0.23.8",
"exception-reporting": "0.42.0",
"find-and-replace": "0.215.5",
"fuzzy-finder": "1.7.5",
"github": "0.9.1",
"git-diff": "1.3.9",
"go-to-line": "0.32.1",
"grammar-selector": "0.49.8",
"grammar-selector": "0.49.9",
"image-view": "0.62.4",
"incompatible-packages": "0.27.3",
"keybinding-resolver": "0.38.1",
"line-ending-selector": "0.7.4",
"link": "0.31.3",
"markdown-preview": "0.159.18",
"line-ending-selector": "0.7.5",
"link": "0.31.4",
"markdown-preview": "0.159.20",
"metrics": "1.2.6",
"notifications": "0.69.2",
"open-on-github": "1.3.0",
"package-generator": "1.1.1",
"settings-view": "0.253.0",
"snippets": "1.1.9",
"spell-check": "0.72.3",
"notifications": "0.70.2",
"open-on-github": "1.3.1",
"package-generator": "1.3.0",
"settings-view": "0.254.0",
"snippets": "1.3.0",
"spell-check": "0.72.7",
"status-bar": "1.8.15",
"styleguide": "0.49.9",
"symbols-view": "0.118.1",
"styleguide": "0.49.10",
"symbols-view": "0.118.2",
"tabs": "0.109.1",
"timecop": "0.36.2",
"tree-view": "0.221.3",
"update-package-dependencies": "0.13.0",
"welcome": "0.36.5",
"update-package-dependencies": "0.13.1",
"welcome": "0.36.6",
"whitespace": "0.37.5",
"wrap-guide": "0.40.2",
"language-c": "0.58.1",
"language-clojure": "0.22.4",
"wrap-guide": "0.40.3",
"language-c": "0.59.1",
"language-clojure": "0.22.6",
"language-coffee-script": "0.49.3",
"language-csharp": "0.14.3",
"language-css": "0.42.7",
"language-gfm": "0.90.2",
"language-csharp": "0.14.4",
"language-css": "0.42.9",
"language-gfm": "0.90.3",
"language-git": "0.19.1",
"language-go": "0.44.3",
"language-html": "0.48.2",
"language-go": "0.45.0",
"language-html": "0.48.6",
"language-hyperlink": "0.16.3",
"language-java": "0.27.6",
"language-javascript": "0.127.6",
"language-javascript": "0.128.1",
"language-json": "0.19.1",
"language-less": "0.33.0",
"language-less": "0.34.2",
"language-make": "0.22.3",
"language-mustache": "0.14.4",
"language-objective-c": "0.15.1",
"language-perl": "0.38.1",
"language-php": "0.42.2",
"language-php": "0.43.0",
"language-property-list": "0.9.1",
"language-python": "0.45.5",
"language-python": "0.47.0",
"language-ruby": "0.71.4",
"language-ruby-on-rails": "0.25.2",
"language-sass": "0.61.1",
"language-shellscript": "0.25.4",
"language-ruby-on-rails": "0.25.3",
"language-sass": "0.61.4",
"language-shellscript": "0.26.0",
"language-source": "0.9.0",
"language-sql": "0.25.8",
"language-sql": "0.25.9",
"language-text": "0.7.3",
"language-todo": "0.29.3",
"language-toml": "0.18.1",
"language-typescript": "0.2.3",
"language-typescript": "0.3.0",
"language-xml": "0.35.2",
"language-yaml": "0.31.1"
},

View File

@@ -28,6 +28,7 @@ const argv = yargs
const checkChromedriverVersion = require('./lib/check-chromedriver-version')
const cleanOutputDirectory = require('./lib/clean-output-directory')
const cleanPackageLock = require('./lib/clean-package-lock')
const codeSignOnMac = require('./lib/code-sign-on-mac')
const codeSignOnWindows = require('./lib/code-sign-on-windows')
const compressArtifacts = require('./lib/compress-artifacts')
@@ -58,6 +59,7 @@ const CONFIG = require('./config')
let binariesPromise = Promise.resolve()
if (!argv.existingBinaries) {
cleanPackageLock()
checkChromedriverVersion()
cleanOutputDirectory()
copyAssets()

View File

@@ -1,6 +0,0 @@
@ECHO OFF
IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp
SET SQUIRREL_TEMP=C:\sqtemp
del script\package-lock.json /q
del apm\package-lock.json /q
script\build.cmd --existing-binaries --code-sign --create-windows-installer

View File

@@ -0,0 +1,18 @@
// This module exports a function that deletes all `package-lock.json` files that do
// not exist under a `node_modules` directory.
'use strict'
const CONFIG = require('../config')
const fs = require('fs-extra')
const glob = require('glob')
const path = require('path')
module.exports = function () {
console.log('Deleting problematic package-lock.json files')
let paths = glob.sync(path.join(CONFIG.repositoryRootPath, '**', 'package-lock.json'), {ignore: path.join('**', 'node_modules', '**')})
for (let path of paths) {
fs.unlinkSync(path)
}
}

View File

@@ -46,6 +46,7 @@ module.exports = function (packagedAppPath) {
relativePath === path.join('..', 'node_modules', 'less', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'less', 'lib', 'less', 'fs.js') ||
relativePath === path.join('..', 'node_modules', 'less', 'lib', 'less-node', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'lodash.isequal', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'node-fetch', 'lib', 'fetch-error.js') ||
relativePath === path.join('..', 'node_modules', 'superstring', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'oniguruma', 'src', 'oniguruma.js') ||
@@ -57,7 +58,9 @@ module.exports = function (packagedAppPath) {
relativePath === path.join('..', 'node_modules', 'spelling-manager', 'node_modules', 'natural', 'lib', 'natural', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'tar', 'tar.js') ||
relativePath === path.join('..', 'node_modules', 'temp', 'lib', 'temp.js') ||
relativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js')
relativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') ||
relativePath === path.join('..', 'node_modules', 'tree-sitter', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'winreg', 'lib', 'registry.js')
)
}
}).then((snapshotScript) => {
@@ -85,7 +88,7 @@ module.exports = function (packagedAppPath) {
console.log(`Generating startup blob at "${generatedStartupBlobPath}"`)
childProcess.execFileSync(
path.join(CONFIG.repositoryRootPath, 'script', 'node_modules', 'electron-mksnapshot', 'bin', 'mksnapshot'),
[snapshotScriptPath, '--startup_blob', generatedStartupBlobPath]
['--no-use_ic', snapshotScriptPath, '--startup_blob', generatedStartupBlobPath]
)
let startupBlobDestinationPath

View File

@@ -4,30 +4,54 @@ const fs = require('fs-extra')
const handleTilde = require('./handle-tilde')
const path = require('path')
const template = require('lodash.template')
const startCase = require('lodash.startcase')
const execSync = require('child_process').execSync
const CONFIG = require('../config')
function install (installationDirPath, packagedAppFileName, packagedAppPath) {
if (fs.existsSync(installationDirPath)) {
console.log(`Removing previously installed "${packagedAppFileName}" at "${installationDirPath}"`)
fs.removeSync(installationDirPath)
}
console.log(`Installing "${packagedAppFileName}" at "${installationDirPath}"`)
fs.copySync(packagedAppPath, installationDirPath)
}
/**
* Finds the path to the base directory of the icon default icon theme
* This follows the freedesktop Icon Theme Specification:
* https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#install_icons
* and the XDG Base Directory Specification:
* https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables
*/
function findBaseIconThemeDirPath () {
const defaultBaseIconThemeDir = '/usr/share/icons/hicolor'
const dataDirsString = process.env.XDG_DATA_DIRS
if (dataDirsString) {
const dataDirs = dataDirsString.split(path.delimiter)
if (dataDirs.includes('/usr/share/') || dataDirs.includes('/usr/share')) {
return defaultBaseIconThemeDir
} else {
return path.join(dataDirs[0], 'icons', 'hicolor')
}
} else {
return defaultBaseIconThemeDir
}
}
module.exports = function (packagedAppPath, installDir) {
const packagedAppFileName = path.basename(packagedAppPath)
if (process.platform === 'darwin') {
const installPrefix = installDir !== '' ? handleTilde(installDir) : path.join(path.sep, 'Applications')
const installationDirPath = path.join(installPrefix, packagedAppFileName)
if (fs.existsSync(installationDirPath)) {
console.log(`Removing previously installed "${packagedAppFileName}" at "${installationDirPath}"`)
fs.removeSync(installationDirPath)
}
console.log(`Installing "${packagedAppPath}" at "${installationDirPath}"`)
fs.copySync(packagedAppPath, installationDirPath)
install(installationDirPath, packagedAppFileName, packagedAppPath)
} else if (process.platform === 'win32') {
const installPrefix = installDir !== '' ? installDir : process.env.LOCALAPPDATA
const installationDirPath = path.join(installPrefix, packagedAppFileName, 'app-dev')
try {
if (fs.existsSync(installationDirPath)) {
console.log(`Removing previously installed "${packagedAppFileName}" at "${installationDirPath}"`)
fs.removeSync(installationDirPath)
}
console.log(`Installing "${packagedAppPath}" at "${installationDirPath}"`)
fs.copySync(packagedAppPath, installationDirPath)
install(installationDirPath, packagedAppFileName, packagedAppPath)
} catch (e) {
console.log(`Administrator elevation required to install into "${installationDirPath}"`)
const fsAdmin = require('fs-admin')
@@ -38,59 +62,92 @@ module.exports = function (packagedAppPath, installDir) {
})
}
} else {
const atomExecutableName = CONFIG.channel === 'beta' ? 'atom-beta' : 'atom'
const apmExecutableName = CONFIG.channel === 'beta' ? 'apm-beta' : 'apm'
const appName = CONFIG.channel === 'beta' ? 'Atom Beta' : 'Atom'
const atomExecutableName = CONFIG.channel === 'stable' ? 'atom' : 'atom-' + CONFIG.channel
const apmExecutableName = CONFIG.channel === 'stable' ? 'apm' : 'apm-' + CONFIG.channel
const appName = CONFIG.channel === 'stable' ? 'Atom' : startCase('Atom ' + CONFIG.channel)
const appDescription = CONFIG.appMetadata.description
const prefixDirPath = installDir !== '' ? handleTilde(installDir) : path.join('/usr', 'local')
const shareDirPath = path.join(prefixDirPath, 'share')
const installationDirPath = path.join(shareDirPath, atomExecutableName)
const applicationsDirPath = path.join(shareDirPath, 'applications')
const desktopEntryPath = path.join(applicationsDirPath, `${atomExecutableName}.desktop`)
const binDirPath = path.join(prefixDirPath, 'bin')
const atomBinDestinationPath = path.join(binDirPath, atomExecutableName)
const apmBinDestinationPath = path.join(binDirPath, apmExecutableName)
fs.mkdirpSync(applicationsDirPath)
fs.mkdirpSync(binDirPath)
if (fs.existsSync(installationDirPath)) {
console.log(`Removing previously installed "${packagedAppFileName}" at "${installationDirPath}"`)
fs.removeSync(installationDirPath)
}
console.log(`Installing "${packagedAppFileName}" at "${installationDirPath}"`)
fs.copySync(packagedAppPath, installationDirPath)
install(installationDirPath, packagedAppFileName, packagedAppPath)
if (fs.existsSync(desktopEntryPath)) {
console.log(`Removing existing desktop entry file at "${desktopEntryPath}"`)
fs.removeSync(desktopEntryPath)
}
console.log(`Writing desktop entry file at "${desktopEntryPath}"`)
const iconPath = path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'png', '1024.png')
const desktopEntryTemplate = fs.readFileSync(path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.desktop.in'))
const desktopEntryContents = template(desktopEntryTemplate)({
appName,
appFileName: atomExecutableName,
description: appDescription,
installDir: prefixDirPath,
iconPath
})
fs.writeFileSync(desktopEntryPath, desktopEntryContents)
{ // Install icons
const baseIconThemeDirPath = findBaseIconThemeDirPath()
const fullIconName = atomExecutableName + '.png'
if (fs.existsSync(atomBinDestinationPath)) {
console.log(`Removing existing executable at "${atomBinDestinationPath}"`)
fs.removeSync(atomBinDestinationPath)
}
console.log(`Copying atom.sh to "${atomBinDestinationPath}"`)
fs.copySync(path.join(CONFIG.repositoryRootPath, 'atom.sh'), atomBinDestinationPath)
let existingIconsFound = false
fs.readdirSync(baseIconThemeDirPath).forEach(size => {
const iconPath = path.join(baseIconThemeDirPath, size, 'apps', fullIconName)
if (fs.existsSync(iconPath)) {
if (!existingIconsFound) {
console.log(`Removing existing icons from "${baseIconThemeDirPath}"`)
}
existingIconsFound = true
fs.removeSync(iconPath)
}
})
try {
fs.lstatSync(apmBinDestinationPath)
console.log(`Removing existing executable at "${apmBinDestinationPath}"`)
fs.removeSync(apmBinDestinationPath)
} catch (e) { }
console.log(`Symlinking apm to "${apmBinDestinationPath}"`)
fs.symlinkSync(path.join('..', 'share', atomExecutableName, 'resources', 'app', 'apm', 'node_modules', '.bin', 'apm'), apmBinDestinationPath)
console.log(`Installing icons at "${baseIconThemeDirPath}"`)
const appIconsPath = path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'png')
fs.readdirSync(appIconsPath).forEach(imageName => {
if (/\.png$/.test(imageName)) {
const size = path.basename(imageName, '.png')
const iconPath = path.join(appIconsPath, imageName)
fs.copySync(iconPath, path.join(baseIconThemeDirPath, `${size}x${size}`, 'apps', fullIconName))
}
})
console.log(`Updating icon cache for "${baseIconThemeDirPath}"`)
try {
execSync(`gtk-update-icon-cache ${baseIconThemeDirPath} --force`)
} catch (e) {}
}
{ // Install xdg desktop file
const desktopEntryPath = path.join(applicationsDirPath, `${atomExecutableName}.desktop`)
if (fs.existsSync(desktopEntryPath)) {
console.log(`Removing existing desktop entry file at "${desktopEntryPath}"`)
fs.removeSync(desktopEntryPath)
}
console.log(`Writing desktop entry file at "${desktopEntryPath}"`)
const desktopEntryTemplate = fs.readFileSync(path.join(CONFIG.repositoryRootPath, 'resources', 'linux', 'atom.desktop.in'))
const desktopEntryContents = template(desktopEntryTemplate)({
appName,
appFileName: atomExecutableName,
description: appDescription,
installDir: prefixDirPath,
iconPath: atomExecutableName
})
fs.writeFileSync(desktopEntryPath, desktopEntryContents)
}
{ // Add atom executable to the PATH
const atomBinDestinationPath = path.join(binDirPath, atomExecutableName)
if (fs.existsSync(atomBinDestinationPath)) {
console.log(`Removing existing executable at "${atomBinDestinationPath}"`)
fs.removeSync(atomBinDestinationPath)
}
console.log(`Copying atom.sh to "${atomBinDestinationPath}"`)
fs.copySync(path.join(CONFIG.repositoryRootPath, 'atom.sh'), atomBinDestinationPath)
}
{ // Link apm executable to the PATH
const apmBinDestinationPath = path.join(binDirPath, apmExecutableName)
try {
fs.lstatSync(apmBinDestinationPath)
console.log(`Removing existing executable at "${apmBinDestinationPath}"`)
fs.removeSync(apmBinDestinationPath)
} catch (e) { }
console.log(`Symlinking apm to "${apmBinDestinationPath}"`)
fs.symlinkSync(path.join('..', 'share', atomExecutableName, 'resources', 'app', 'apm', 'node_modules', '.bin', 'apm'), apmBinDestinationPath)
}
console.log(`Changing permissions to 755 for "${installationDirPath}"`)
fs.chmodSync(installationDirPath, '755')

View File

@@ -8,17 +8,18 @@
"colors": "1.1.2",
"csslint": "1.0.2",
"donna": "1.0.16",
"electron-chromedriver": "~1.6",
"electron-chromedriver": "~1.7",
"electron-link": "0.1.2",
"electron-mksnapshot": "~1.6",
"electron-mksnapshot": "~1.7",
"electron-packager": "7.3.0",
"electron-winstaller": "2.6.3",
"electron-winstaller": "2.6.4",
"fs-admin": "^0.1.5",
"fs-extra": "0.30.0",
"glob": "7.0.3",
"joanna": "0.0.9",
"joanna": "0.0.10",
"klaw-sync": "^1.1.2",
"legal-eagle": "0.14.0",
"lodash.startcase": "4.4.0",
"lodash.template": "4.4.0",
"minidump": "0.9.0",
"mkdirp": "0.5.1",

View File

@@ -3,6 +3,7 @@
'use strict'
require('colors')
const argv = require('yargs').argv
const assert = require('assert')
const async = require('async')
const childProcess = require('child_process')
@@ -150,17 +151,26 @@ function runBenchmarkTests (callback) {
let testSuitesToRun = testSuitesForPlatform(process.platform)
function testSuitesForPlatform (platform) {
let suites = [];
switch (platform) {
case 'darwin':
return [runCoreMainProcessTests, runCoreRenderProcessTests, runBenchmarkTests].concat(packageTestSuites)
suites = [runCoreMainProcessTests, runCoreRenderProcessTests, runBenchmarkTests].concat(packageTestSuites)
break
case 'win32':
return (process.arch === 'x64') ? [runCoreMainProcessTests, runCoreRenderProcessTests] : [runCoreMainProcessTests]
suites = (process.arch === 'x64') ? [runCoreMainProcessTests, runCoreRenderProcessTests] : [runCoreMainProcessTests]
break
case 'linux':
return [runCoreMainProcessTests]
suites = [runCoreMainProcessTests]
break
default:
console.log(`Unrecognized platform: ${platform}`)
return []
}
if (argv.skipMainProcessTests) {
suites = suites.filter(suite => suite !== runCoreMainProcessTests);
}
return suites;
}
async.series(testSuitesToRun, function (err, exitCodes) {

View File

@@ -1,5 +1,6 @@
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
const {it, fit, ffit, beforeEach, afterEach, conditionPromise} = require('./async-spec-helpers')
const _ = require('underscore-plus')
const fs = require('fs')
const path = require('path')
const temp = require('temp').track()
const AtomEnvironment = require('../src/atom-environment')
@@ -301,8 +302,9 @@ describe('AtomEnvironment', () => {
})
it('serializes the text editor registry', async () => {
await atom.packages.activatePackage('language-text')
const editor = await atom.workspace.open('sample.js')
atom.textEditors.setGrammarOverride(editor, 'text.plain')
expect(atom.grammars.assignLanguageMode(editor, 'text.plain')).toBe(true)
const atom2 = new AtomEnvironment({
applicationDelegate: atom.applicationDelegate,
@@ -318,7 +320,9 @@ describe('AtomEnvironment', () => {
atom2.initialize({document, window})
await atom2.deserialize(atom.serialize())
expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain')
await atom2.packages.activatePackage('language-text')
const editor2 = atom2.workspace.getActiveTextEditor()
expect(editor2.getBuffer().getLanguageMode().getLanguageId()).toBe('text.plain')
atom2.destroy()
})
@@ -468,15 +472,28 @@ describe('AtomEnvironment', () => {
await atom.workspace.open()
})
it('automatically restores the saved state into the current environment', () => {
const state = {}
spyOn(atom.workspace, 'open')
spyOn(atom, 'restoreStateIntoThisEnvironment')
it('automatically restores the saved state into the current environment', async () => {
const projectPath = temp.mkdirSync()
const filePath1 = path.join(projectPath, 'file-1')
const filePath2 = path.join(projectPath, 'file-2')
const filePath3 = path.join(projectPath, 'file-3')
fs.writeFileSync(filePath1, 'abc')
fs.writeFileSync(filePath2, 'def')
fs.writeFileSync(filePath3, 'ghi')
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.restoreStateIntoThisEnvironment).toHaveBeenCalledWith(state)
expect(atom.workspace.open.callCount).toBe(1)
expect(atom.workspace.open).toHaveBeenCalledWith(__filename)
const env1 = new AtomEnvironment({applicationDelegate: atom.applicationDelegate})
env1.project.setPaths([projectPath])
await env1.workspace.open(filePath1)
await env1.workspace.open(filePath2)
await env1.workspace.open(filePath3)
const env1State = env1.serialize()
env1.destroy()
const env2 = new AtomEnvironment({applicationDelegate: atom.applicationDelegate})
await env2.attemptRestoreProjectStateForPaths(env1State, [projectPath], [filePath2])
const restoredURIs = env2.workspace.getPaneItems().map(p => p.getURI())
expect(restoredURIs).toEqual([filePath1, filePath2, filePath3])
env2.destroy()
})
describe('when a dock has a non-text editor', () => {
@@ -515,27 +532,31 @@ describe('AtomEnvironment', () => {
})
})
it('prompts the user to restore the state in a new window, discarding it and adding folder to current window', () => {
spyOn(atom, 'confirm').andReturn(1)
it('prompts the user to restore the state in a new window, discarding it and adding folder to current window', async () => {
jasmine.useRealClock()
spyOn(atom, 'confirm').andCallFake((options, callback) => callback(1))
spyOn(atom.project, 'addPath')
spyOn(atom.workspace, 'open')
const state = Symbol()
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).toHaveBeenCalled()
expect(atom.project.addPath.callCount).toBe(1)
await conditionPromise(() => atom.project.addPath.callCount === 1)
expect(atom.project.addPath).toHaveBeenCalledWith(__dirname)
expect(atom.workspace.open.callCount).toBe(1)
expect(atom.workspace.open).toHaveBeenCalledWith(__filename)
})
it('prompts the user to restore the state in a new window, opening a new window', () => {
spyOn(atom, 'confirm').andReturn(0)
it('prompts the user to restore the state in a new window, opening a new window', async () => {
jasmine.useRealClock()
spyOn(atom, 'confirm').andCallFake((options, callback) => callback(0))
spyOn(atom, 'open')
const state = Symbol()
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
expect(atom.confirm).toHaveBeenCalled()
await conditionPromise(() => atom.open.callCount === 1)
expect(atom.open).toHaveBeenCalledWith({
pathsToOpen: [__dirname, __filename],
newWindow: true,
@@ -589,7 +610,7 @@ describe('AtomEnvironment', () => {
const promise = new Promise((r) => { resolve = r })
envLoaded = () => {
resolve()
promise
return promise
}
atomEnvironment = new AtomEnvironment({
applicationDelegate: atom.applicationDelegate,

View File

@@ -9,34 +9,41 @@ ipcHelpers = require '../src/ipc-helpers'
formatStackTrace = (spec, message='', stackTrace) ->
return stackTrace unless stackTrace
# at ... (.../jasmine.js:1:2)
jasminePattern = /^\s*at\s+.*\(?.*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/
firstJasmineLinePattern = /^\s*at [/\\].*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/
# at jasmine.Something... (.../jasmine.js:1:2)
firstJasmineLinePattern = /^\s*at\s+jasmine\.[A-Z][^\s]*\s+\(?.*[/\\]jasmine(-[^/\\]*)?\.js:\d+:\d+\)?\s*$/
lines = []
for line in stackTrace.split('\n')
lines.push(line) unless jasminePattern.test(line)
break if firstJasmineLinePattern.test(line)
lines.push(line) unless jasminePattern.test(line)
# Remove first line of stack when it is the same as the error message
errorMatch = lines[0]?.match(/^Error: (.*)/)
lines.shift() if message.trim() is errorMatch?[1]?.trim()
for line, index in lines
# Remove prefix of lines matching: at jasmine.Spec.<anonymous> (path:1:2)
prefixMatch = line.match(/at jasmine\.Spec\.<anonymous> \(([^)]+)\)/)
line = "at #{prefixMatch[1]}" if prefixMatch
lines = lines.map (line) ->
# Only format actual stacktrace lines
if /^\s*at\s/.test(line)
# Needs to occur before path relativization
if process.platform is 'win32' and /file:\/\/\//.test(line)
# file:///C:/some/file -> C:\some\file
line = line.replace('file:///', '').replace(///#{path.posix.sep}///g, path.win32.sep)
# Relativize locations to spec directory
if process.platform is 'win32'
line = line.replace('file:///', '').replace(///#{path.posix.sep}///g, path.win32.sep)
line = line.replace("at #{spec.specDirectory}#{path.sep}", 'at ')
lines[index] = line.replace("(#{spec.specDirectory}#{path.sep}", '(') # at step (path:1:2)
line = line.trim()
# at jasmine.Spec.<anonymous> (path:1:2) -> at path:1:2
.replace(/^at jasmine\.Spec\.<anonymous> \(([^)]+)\)/, 'at $1')
# at it (path:1:2) -> at path:1:2
.replace(/^at f*it \(([^)]+)\)/, 'at $1')
# at spec/file-test.js -> at file-test.js
.replace(spec.specDirectory + path.sep, '')
return line
lines = lines.map (line) -> line.trim()
lines.join('\n').trim()
module.exports =
class AtomReporter
constructor: ->
@element = document.createElement('div')
@element.classList.add('spec-reporter-container')

View File

@@ -35,9 +35,9 @@ describe('CommandInstaller on #darwin', () => {
installer.installShellCommandsInteractively()
expect(appDelegate.confirm).toHaveBeenCalledWith({
expect(appDelegate.confirm.mostRecentCall.args[0]).toEqual({
message: 'Failed to install shell commands',
detailedMessage: 'an error'
detail: 'an error'
})
appDelegate.confirm.reset()
@@ -46,9 +46,9 @@ describe('CommandInstaller on #darwin', () => {
installer.installShellCommandsInteractively()
expect(appDelegate.confirm).toHaveBeenCalledWith({
expect(appDelegate.confirm.mostRecentCall.args[0]).toEqual({
message: 'Failed to install shell commands',
detailedMessage: 'another error'
detail: 'another error'
})
})
@@ -61,9 +61,9 @@ describe('CommandInstaller on #darwin', () => {
installer.installShellCommandsInteractively()
expect(appDelegate.confirm).toHaveBeenCalledWith({
expect(appDelegate.confirm.mostRecentCall.args[0]).toEqual({
message: 'Commands installed.',
detailedMessage: 'The shell commands `atom` and `apm` are installed.'
detail: 'The shell commands `atom` and `apm` are installed.'
})
})

View File

@@ -1,5 +1,6 @@
const CommandRegistry = require('../src/command-registry');
const _ = require('underscore-plus');
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers');
describe("CommandRegistry", () => {
let registry, parent, child, grandchild;
@@ -357,12 +358,41 @@ describe("CommandRegistry", () => {
expect(called).toBe(true);
});
it("returns a boolean indicating whether any listeners matched the command", () => {
it("returns a promise if any listeners matched the command", () => {
registry.add('.grandchild', 'command', () => {});
expect(registry.dispatch(grandchild, 'command')).toBe(true);
expect(registry.dispatch(grandchild, 'bogus')).toBe(false);
expect(registry.dispatch(parent, 'command')).toBe(false);
expect(registry.dispatch(grandchild, 'command').constructor.name).toBe("Promise");
expect(registry.dispatch(grandchild, 'bogus')).toBe(null);
expect(registry.dispatch(parent, 'command')).toBe(null);
});
it("returns a promise that resolves when the listeners resolve", async () => {
jasmine.useRealClock();
registry.add('.grandchild', 'command', () => 1);
registry.add('.grandchild', 'command', () => Promise.resolve(2));
registry.add('.grandchild', 'command', () => new Promise((resolve) => {
setTimeout(() => { resolve(3); }, 1);
}));
const values = await registry.dispatch(grandchild, 'command');
expect(values).toEqual([3, 2, 1]);
});
it("returns a promise that rejects when a listener is rejected", async () => {
jasmine.useRealClock();
registry.add('.grandchild', 'command', () => 1);
registry.add('.grandchild', 'command', () => Promise.resolve(2));
registry.add('.grandchild', 'command', () => new Promise((resolve, reject) => {
setTimeout(() => { reject(3); }, 1);
}));
let value;
try {
value = await registry.dispatch(grandchild, 'command');
} catch (err) {
value = err;
}
expect(value).toBe(3);
});
});

View File

@@ -106,6 +106,15 @@ describe "Config", ->
atom.config.set("foo.bar.baz", 1, scopeSelector: ".source.coffee", source: "some-package")
expect(atom.config.get("foo.bar.baz", scope: [".source.coffee"])).toBe 100
describe "when the first component of the scope descriptor matches a legacy scope alias", ->
it "falls back to properties defined for the legacy scope if no value is found for the original scope descriptor", ->
atom.config.addLegacyScopeAlias('javascript', '.source.js')
atom.config.set('foo', 100, scopeSelector: '.source.js')
atom.config.set('foo', 200, scopeSelector: 'javascript for_statement')
expect(atom.config.get('foo', scope: ['javascript', 'for_statement', 'identifier'])).toBe(200)
expect(atom.config.get('foo', scope: ['javascript', 'function', 'identifier'])).toBe(100)
describe ".getAll(keyPath, {scope, sources, excludeSources})", ->
it "reads all of the values for a given key-path", ->
expect(atom.config.set("foo", 41)).toBe true
@@ -130,6 +139,20 @@ describe "Config", ->
{scopeSelector: '*', value: 40}
]
describe "when the first component of the scope descriptor matches a legacy scope alias", ->
it "includes the values defined for the legacy scope", ->
atom.config.addLegacyScopeAlias('javascript', '.source.js')
expect(atom.config.set('foo', 41)).toBe true
expect(atom.config.set('foo', 42, scopeSelector: 'javascript')).toBe true
expect(atom.config.set('foo', 43, scopeSelector: '.source.js')).toBe true
expect(atom.config.getAll('foo', scope: ['javascript'])).toEqual([
{scopeSelector: 'javascript', value: 42},
{scopeSelector: '.js.source', value: 43},
{scopeSelector: '*', value: 41}
])
describe ".set(keyPath, value, {source, scopeSelector})", ->
it "allows a key path's value to be written", ->
expect(atom.config.set("foo.bar.baz", 42)).toBe true

View File

@@ -201,11 +201,7 @@ describe('Dock', () => {
const dockElement = atom.workspace.getBottomDock().getElement()
dockElement.querySelector('.atom-dock-resize-handle').dispatchEvent(new MouseEvent('mousedown', {detail: 2}))
expect(dockElement.offsetHeight).toBe(0)
// There should still be a hoverable, absolutely-positioned element so users can reveal the
// toggle affordance even when fullscreened.
expect(dockElement.querySelector('.atom-dock-inner').offsetHeight).toBe(1)
expect(dockElement.querySelector('.atom-dock-inner').offsetHeight).toBe(0)
// The content should be masked away.
expect(dockElement.querySelector('.atom-dock-mask').offsetHeight).toBe(0)
})

View File

@@ -0,0 +1 @@
exports.isFakeTreeSitterParser = true

View File

@@ -0,0 +1,14 @@
name: 'Some Language'
id: 'some-language'
type: 'tree-sitter'
parser: './fake-parser'
fileTypes: [
'somelang'
]
scopes:
'class > identifier': 'entity.name.type.class'

View File

@@ -13,6 +13,14 @@ describe('GitRepositoryProvider', () => {
provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm)
})
afterEach(() => {
if (provider) {
Object.keys(provider.pathToRepository).forEach(key => {
provider.pathToRepository[key].destroy()
})
}
})
describe('.repositoryForDirectory(directory)', () => {
describe('when specified a Directory with a Git repository', () => {
it('resolves with a GitRepository', async () => {

View File

@@ -366,6 +366,7 @@ describe('GitRepository', () => {
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
grammarRegistry: atom.grammars,
applicationDelegate: atom.applicationDelegate
})
await project2.deserialize(atom.project.serialize({isUnloading: false}))

View File

@@ -0,0 +1,497 @@
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
const dedent = require('dedent')
const path = require('path')
const fs = require('fs-plus')
const temp = require('temp').track()
const TextBuffer = require('text-buffer')
const GrammarRegistry = require('../src/grammar-registry')
const TreeSitterGrammar = require('../src/tree-sitter-grammar')
const FirstMate = require('first-mate')
describe('GrammarRegistry', () => {
let grammarRegistry
beforeEach(() => {
grammarRegistry = new GrammarRegistry({config: atom.config})
})
describe('.assignLanguageMode(buffer, languageId)', () => {
it('assigns to the buffer a language mode with the given language id', async () => {
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-css/grammars/css.cson'))
const buffer = new TextBuffer()
expect(grammarRegistry.assignLanguageMode(buffer, 'source.js')).toBe(true)
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
// Returns true if we found the grammar, even if it didn't change
expect(grammarRegistry.assignLanguageMode(buffer, 'source.js')).toBe(true)
// Language names are not case-sensitive
expect(grammarRegistry.assignLanguageMode(buffer, 'source.css')).toBe(true)
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css')
// Returns false if no language is found
expect(grammarRegistry.assignLanguageMode(buffer, 'blub')).toBe(false)
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css')
})
describe('when no languageId is passed', () => {
it('makes the buffer use the null grammar', () => {
grammarRegistry.loadGrammarSync(require.resolve('language-css/grammars/css.cson'))
const buffer = new TextBuffer()
expect(grammarRegistry.assignLanguageMode(buffer, 'source.css')).toBe(true)
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css')
expect(grammarRegistry.assignLanguageMode(buffer, null)).toBe(true)
expect(buffer.getLanguageMode().getLanguageId()).toBe('text.plain.null-grammar')
})
})
})
describe('.grammarForId(languageId)', () => {
it('converts the language id to a text-mate language id when `core.useTreeSitterParsers` is false', () => {
atom.config.set('core.useTreeSitterParsers', false)
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson'))
const grammar = grammarRegistry.grammarForId('javascript')
expect(grammar instanceof FirstMate.Grammar).toBe(true)
expect(grammar.scopeName).toBe('source.js')
grammarRegistry.removeGrammar(grammar)
expect(grammarRegistry.grammarForId('javascript')).toBe(undefined)
})
it('converts the language id to a tree-sitter language id when `core.useTreeSitterParsers` is true', () => {
atom.config.set('core.useTreeSitterParsers', true)
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson'))
const grammar = grammarRegistry.grammarForId('source.js')
expect(grammar instanceof TreeSitterGrammar).toBe(true)
expect(grammar.id).toBe('javascript')
grammarRegistry.removeGrammar(grammar)
expect(grammarRegistry.grammarForId('source.js') instanceof FirstMate.Grammar).toBe(true)
})
})
describe('.autoAssignLanguageMode(buffer)', () => {
it('assigns to the buffer a language mode based on the best available grammar', () => {
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-css/grammars/css.cson'))
const buffer = new TextBuffer()
buffer.setPath('foo.js')
expect(grammarRegistry.assignLanguageMode(buffer, 'source.css')).toBe(true)
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css')
grammarRegistry.autoAssignLanguageMode(buffer)
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
})
})
describe('.maintainLanguageMode(buffer)', () => {
it('assigns a grammar to the buffer based on its path', async () => {
const buffer = new TextBuffer()
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-c/grammars/c.cson'))
buffer.setPath('test.js')
grammarRegistry.maintainLanguageMode(buffer)
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
buffer.setPath('test.c')
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.c')
})
it('updates the buffer\'s grammar when a more appropriate text-mate grammar is added for its path', async () => {
atom.config.set('core.useTreeSitterParsers', false)
const buffer = new TextBuffer()
expect(buffer.getLanguageMode().getLanguageId()).toBe(null)
buffer.setPath('test.js')
grammarRegistry.maintainLanguageMode(buffer)
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson'))
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
})
it('updates the buffer\'s grammar when a more appropriate tree-sitter grammar is added for its path', async () => {
atom.config.set('core.useTreeSitterParsers', true)
const buffer = new TextBuffer()
expect(buffer.getLanguageMode().getLanguageId()).toBe(null)
buffer.setPath('test.js')
grammarRegistry.maintainLanguageMode(buffer)
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson'))
expect(buffer.getLanguageMode().getLanguageId()).toBe('javascript')
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
expect(buffer.getLanguageMode().getLanguageId()).toBe('javascript')
})
it('can be overridden by calling .assignLanguageMode', () => {
const buffer = new TextBuffer()
buffer.setPath('test.js')
grammarRegistry.maintainLanguageMode(buffer)
grammarRegistry.loadGrammarSync(require.resolve('language-css/grammars/css.cson'))
expect(grammarRegistry.assignLanguageMode(buffer, 'source.css')).toBe(true)
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css')
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css')
})
it('returns a disposable that can be used to stop the registry from updating the buffer', async () => {
const buffer = new TextBuffer()
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
const previousSubscriptionCount = buffer.emitter.getTotalListenerCount()
const disposable = grammarRegistry.maintainLanguageMode(buffer)
expect(buffer.emitter.getTotalListenerCount()).toBeGreaterThan(previousSubscriptionCount)
expect(retainedBufferCount(grammarRegistry)).toBe(1)
buffer.setPath('test.js')
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
buffer.setPath('test.txt')
expect(buffer.getLanguageMode().getLanguageId()).toBe('text.plain.null-grammar')
disposable.dispose()
expect(buffer.emitter.getTotalListenerCount()).toBe(previousSubscriptionCount)
expect(retainedBufferCount(grammarRegistry)).toBe(0)
buffer.setPath('test.js')
expect(buffer.getLanguageMode().getLanguageId()).toBe('text.plain.null-grammar')
expect(retainedBufferCount(grammarRegistry)).toBe(0)
})
it('doesn\'t do anything when called a second time with the same buffer', async () => {
const buffer = new TextBuffer()
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
const disposable1 = grammarRegistry.maintainLanguageMode(buffer)
const disposable2 = grammarRegistry.maintainLanguageMode(buffer)
buffer.setPath('test.js')
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
disposable2.dispose()
buffer.setPath('test.txt')
expect(buffer.getLanguageMode().getLanguageId()).toBe('text.plain.null-grammar')
disposable1.dispose()
buffer.setPath('test.js')
expect(buffer.getLanguageMode().getLanguageId()).toBe('text.plain.null-grammar')
})
it('does not retain the buffer after the buffer is destroyed', () => {
const buffer = new TextBuffer()
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
const disposable = grammarRegistry.maintainLanguageMode(buffer)
expect(retainedBufferCount(grammarRegistry)).toBe(1)
expect(subscriptionCount(grammarRegistry)).toBe(2)
buffer.destroy()
expect(retainedBufferCount(grammarRegistry)).toBe(0)
expect(subscriptionCount(grammarRegistry)).toBe(0)
expect(buffer.emitter.getTotalListenerCount()).toBe(0)
disposable.dispose()
expect(retainedBufferCount(grammarRegistry)).toBe(0)
expect(subscriptionCount(grammarRegistry)).toBe(0)
})
it('does not retain the buffer when the grammar registry is destroyed', () => {
const buffer = new TextBuffer()
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
const disposable = grammarRegistry.maintainLanguageMode(buffer)
expect(retainedBufferCount(grammarRegistry)).toBe(1)
expect(subscriptionCount(grammarRegistry)).toBe(2)
grammarRegistry.clear()
expect(retainedBufferCount(grammarRegistry)).toBe(0)
expect(subscriptionCount(grammarRegistry)).toBe(0)
expect(buffer.emitter.getTotalListenerCount()).toBe(0)
})
})
describe('.selectGrammar(filePath)', () => {
it('always returns a grammar', () => {
const registry = new GrammarRegistry({config: atom.config})
expect(registry.selectGrammar().scopeName).toBe('text.plain.null-grammar')
})
it('selects the text.plain grammar over the null grammar', async () => {
await atom.packages.activatePackage('language-text')
expect(atom.grammars.selectGrammar('test.txt').scopeName).toBe('text.plain')
})
it('selects a grammar based on the file path case insensitively', async () => {
await atom.packages.activatePackage('language-coffee-script')
expect(atom.grammars.selectGrammar('/tmp/source.coffee').scopeName).toBe('source.coffee')
expect(atom.grammars.selectGrammar('/tmp/source.COFFEE').scopeName).toBe('source.coffee')
})
describe('on Windows', () => {
let originalPlatform
beforeEach(() => {
originalPlatform = process.platform
Object.defineProperty(process, 'platform', {value: 'win32'})
})
afterEach(() => {
Object.defineProperty(process, 'platform', {value: originalPlatform})
})
it('normalizes back slashes to forward slashes when matching the fileTypes', async () => {
await atom.packages.activatePackage('language-git')
expect(atom.grammars.selectGrammar('something\\.git\\config').scopeName).toBe('source.git-config')
})
})
it("can use the filePath to load the correct grammar based on the grammar's filetype", async () => {
await atom.packages.activatePackage('language-git')
await atom.packages.activatePackage('language-javascript')
await atom.packages.activatePackage('language-ruby')
expect(atom.grammars.selectGrammar('file.js').name).toBe('JavaScript') // based on extension (.js)
expect(atom.grammars.selectGrammar(path.join(temp.dir, '.git', 'config')).name).toBe('Git Config') // based on end of the path (.git/config)
expect(atom.grammars.selectGrammar('Rakefile').name).toBe('Ruby') // based on the file's basename (Rakefile)
expect(atom.grammars.selectGrammar('curb').name).toBe('Null Grammar')
expect(atom.grammars.selectGrammar('/hu.git/config').name).toBe('Null Grammar')
})
describe('when the grammar has a contentRegExp field', () => {
it('favors grammars whose contentRegExp matches a prefix of the file\'s content', () => {
atom.grammars.addGrammar({
id: 'javascript-1',
fileTypes: ['js']
})
atom.grammars.addGrammar({
id: 'flow-javascript',
contentRegExp: new RegExp('//.*@flow'),
fileTypes: ['js']
})
atom.grammars.addGrammar({
id: 'javascript-2',
fileTypes: ['js']
})
const selectedGrammar = atom.grammars.selectGrammar('test.js', dedent`
// Copyright EvilCorp
// @flow
module.exports = function () { return 1 + 1 }
`)
expect(selectedGrammar.id).toBe('flow-javascript')
})
})
it("uses the filePath's shebang line if the grammar cannot be determined by the extension or basename", async () => {
await atom.packages.activatePackage('language-javascript')
await atom.packages.activatePackage('language-ruby')
const filePath = require.resolve('./fixtures/shebang')
expect(atom.grammars.selectGrammar(filePath).name).toBe('Ruby')
})
it('uses the number of newlines in the first line regex to determine the number of lines to test against', async () => {
await atom.packages.activatePackage('language-property-list')
await atom.packages.activatePackage('language-coffee-script')
let fileContent = 'first-line\n<html>'
expect(atom.grammars.selectGrammar('dummy.coffee', fileContent).name).toBe('CoffeeScript')
fileContent = '<?xml version="1.0" encoding="UTF-8"?>'
expect(atom.grammars.selectGrammar('grammar.tmLanguage', fileContent).name).toBe('Null Grammar')
fileContent += '\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
expect(atom.grammars.selectGrammar('grammar.tmLanguage', fileContent).name).toBe('Property List (XML)')
})
it("doesn't read the file when the file contents are specified", async () => {
await atom.packages.activatePackage('language-ruby')
const filePath = require.resolve('./fixtures/shebang')
const filePathContents = fs.readFileSync(filePath, 'utf8')
spyOn(fs, 'read').andCallThrough()
expect(atom.grammars.selectGrammar(filePath, filePathContents).name).toBe('Ruby')
expect(fs.read).not.toHaveBeenCalled()
})
describe('when multiple grammars have matching fileTypes', () => {
it('selects the grammar with the longest fileType match', () => {
const grammarPath1 = temp.path({suffix: '.json'})
fs.writeFileSync(grammarPath1, JSON.stringify({
name: 'test1',
scopeName: 'source1',
fileTypes: ['test']
}))
const grammar1 = atom.grammars.loadGrammarSync(grammarPath1)
expect(atom.grammars.selectGrammar('more.test', '')).toBe(grammar1)
fs.removeSync(grammarPath1)
const grammarPath2 = temp.path({suffix: '.json'})
fs.writeFileSync(grammarPath2, JSON.stringify({
name: 'test2',
scopeName: 'source2',
fileTypes: ['test', 'more.test']
}))
const grammar2 = atom.grammars.loadGrammarSync(grammarPath2)
expect(atom.grammars.selectGrammar('more.test', '')).toBe(grammar2)
return fs.removeSync(grammarPath2)
})
})
it('favors non-bundled packages when breaking scoring ties', async () => {
await atom.packages.activatePackage('language-ruby')
await atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'packages', 'package-with-rb-filetype'))
atom.grammars.grammarForScopeName('source.ruby').bundledPackage = true
atom.grammars.grammarForScopeName('test.rb').bundledPackage = false
expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env ruby').scopeName).toBe('source.ruby')
expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env testruby').scopeName).toBe('test.rb')
expect(atom.grammars.selectGrammar('test.rb').scopeName).toBe('test.rb')
})
describe('when there is no file path', () => {
it('does not throw an exception (regression)', () => {
expect(() => atom.grammars.selectGrammar(null, '#!/usr/bin/ruby')).not.toThrow()
expect(() => atom.grammars.selectGrammar(null, '')).not.toThrow()
expect(() => atom.grammars.selectGrammar(null, null)).not.toThrow()
})
})
describe('when the user has custom grammar file types', () => {
it('considers the custom file types as well as those defined in the grammar', async () => {
await atom.packages.activatePackage('language-ruby')
atom.config.set('core.customFileTypes', {'source.ruby': ['Cheffile']})
expect(atom.grammars.selectGrammar('build/Cheffile', 'cookbook "postgres"').scopeName).toBe('source.ruby')
})
it('favors user-defined file types over built-in ones of equal length', async () => {
await atom.packages.activatePackage('language-ruby')
await atom.packages.activatePackage('language-coffee-script')
atom.config.set('core.customFileTypes', {
'source.coffee': ['Rakefile'],
'source.ruby': ['Cakefile']
})
expect(atom.grammars.selectGrammar('Rakefile', '').scopeName).toBe('source.coffee')
expect(atom.grammars.selectGrammar('Cakefile', '').scopeName).toBe('source.ruby')
})
it('favors user-defined file types over grammars with matching first-line-regexps', async () => {
await atom.packages.activatePackage('language-ruby')
await atom.packages.activatePackage('language-javascript')
atom.config.set('core.customFileTypes', {'source.ruby': ['bootstrap']})
expect(atom.grammars.selectGrammar('bootstrap', '#!/usr/bin/env node').scopeName).toBe('source.ruby')
})
})
it('favors a grammar with a matching file type over one with m matching first line pattern', async () => {
await atom.packages.activatePackage('language-ruby')
await atom.packages.activatePackage('language-javascript')
expect(atom.grammars.selectGrammar('foo.rb', '#!/usr/bin/env node').scopeName).toBe('source.ruby')
})
describe('tree-sitter vs text-mate', () => {
it('favors a text-mate grammar over a tree-sitter grammar when `core.useTreeSitterParsers` is false', () => {
atom.config.set('core.useTreeSitterParsers', false)
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson'))
const grammar = grammarRegistry.selectGrammar('test.js')
expect(grammar.scopeName).toBe('source.js')
expect(grammar instanceof FirstMate.Grammar).toBe(true)
})
it('favors a tree-sitter grammar over a text-mate grammar when `core.useTreeSitterParsers` is true', () => {
atom.config.set('core.useTreeSitterParsers', true)
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson'))
const grammar = grammarRegistry.selectGrammar('test.js')
expect(grammar.id).toBe('javascript')
expect(grammar instanceof TreeSitterGrammar).toBe(true)
})
})
})
describe('.removeGrammar(grammar)', () => {
it("removes the grammar, so it won't be returned by selectGrammar", async () => {
await atom.packages.activatePackage('language-css')
const grammar = atom.grammars.selectGrammar('foo.css')
atom.grammars.removeGrammar(grammar)
expect(atom.grammars.selectGrammar('foo.css').name).not.toBe(grammar.name)
})
})
describe('serialization', () => {
it('persists editors\' grammar overrides', async () => {
const buffer1 = new TextBuffer()
const buffer2 = new TextBuffer()
grammarRegistry.loadGrammarSync(require.resolve('language-c/grammars/c.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-html/grammars/html.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
grammarRegistry.maintainLanguageMode(buffer1)
grammarRegistry.maintainLanguageMode(buffer2)
grammarRegistry.assignLanguageMode(buffer1, 'source.c')
grammarRegistry.assignLanguageMode(buffer2, 'source.js')
const buffer1Copy = await TextBuffer.deserialize(buffer1.serialize())
const buffer2Copy = await TextBuffer.deserialize(buffer2.serialize())
const grammarRegistryCopy = new GrammarRegistry({config: atom.config})
grammarRegistryCopy.deserialize(JSON.parse(JSON.stringify(grammarRegistry.serialize())))
grammarRegistryCopy.loadGrammarSync(require.resolve('language-c/grammars/c.cson'))
grammarRegistryCopy.loadGrammarSync(require.resolve('language-html/grammars/html.cson'))
expect(buffer1Copy.getLanguageMode().getLanguageId()).toBe(null)
expect(buffer2Copy.getLanguageMode().getLanguageId()).toBe(null)
grammarRegistryCopy.maintainLanguageMode(buffer1Copy)
grammarRegistryCopy.maintainLanguageMode(buffer2Copy)
expect(buffer1Copy.getLanguageMode().getLanguageId()).toBe('source.c')
expect(buffer2Copy.getLanguageMode().getLanguageId()).toBe(null)
grammarRegistryCopy.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
expect(buffer1Copy.getLanguageMode().getLanguageId()).toBe('source.c')
expect(buffer2Copy.getLanguageMode().getLanguageId()).toBe('source.js')
})
})
})
function retainedBufferCount (grammarRegistry) {
return grammarRegistry.grammarScoresByBuffer.size
}
function subscriptionCount (grammarRegistry) {
return grammarRegistry.subscriptions.disposables.size
}

View File

@@ -1,182 +0,0 @@
path = require 'path'
fs = require 'fs-plus'
temp = require('temp').track()
GrammarRegistry = require '../src/grammar-registry'
Grim = require 'grim'
describe "the `grammars` global", ->
beforeEach ->
waitsForPromise ->
atom.packages.activatePackage('language-text')
waitsForPromise ->
atom.packages.activatePackage('language-javascript')
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
waitsForPromise ->
atom.packages.activatePackage('language-ruby')
waitsForPromise ->
atom.packages.activatePackage('language-git')
afterEach ->
waitsForPromise ->
atom.packages.deactivatePackages()
runs ->
atom.packages.unloadPackages()
try
temp.cleanupSync()
describe ".selectGrammar(filePath)", ->
it "always returns a grammar", ->
registry = new GrammarRegistry(config: atom.config)
expect(registry.selectGrammar().scopeName).toBe 'text.plain.null-grammar'
it "selects the text.plain grammar over the null grammar", ->
expect(atom.grammars.selectGrammar('test.txt').scopeName).toBe 'text.plain'
it "selects a grammar based on the file path case insensitively", ->
expect(atom.grammars.selectGrammar('/tmp/source.coffee').scopeName).toBe 'source.coffee'
expect(atom.grammars.selectGrammar('/tmp/source.COFFEE').scopeName).toBe 'source.coffee'
describe "on Windows", ->
originalPlatform = null
beforeEach ->
originalPlatform = process.platform
Object.defineProperty process, 'platform', value: 'win32'
afterEach ->
Object.defineProperty process, 'platform', value: originalPlatform
it "normalizes back slashes to forward slashes when matching the fileTypes", ->
expect(atom.grammars.selectGrammar('something\\.git\\config').scopeName).toBe 'source.git-config'
it "can use the filePath to load the correct grammar based on the grammar's filetype", ->
waitsForPromise ->
atom.packages.activatePackage('language-git')
runs ->
expect(atom.grammars.selectGrammar("file.js").name).toBe "JavaScript" # based on extension (.js)
expect(atom.grammars.selectGrammar(path.join(temp.dir, '.git', 'config')).name).toBe "Git Config" # based on end of the path (.git/config)
expect(atom.grammars.selectGrammar("Rakefile").name).toBe "Ruby" # based on the file's basename (Rakefile)
expect(atom.grammars.selectGrammar("curb").name).toBe "Null Grammar"
expect(atom.grammars.selectGrammar("/hu.git/config").name).toBe "Null Grammar"
it "uses the filePath's shebang line if the grammar cannot be determined by the extension or basename", ->
filePath = require.resolve("./fixtures/shebang")
expect(atom.grammars.selectGrammar(filePath).name).toBe "Ruby"
it "uses the number of newlines in the first line regex to determine the number of lines to test against", ->
waitsForPromise ->
atom.packages.activatePackage('language-property-list')
runs ->
fileContent = "first-line\n<html>"
expect(atom.grammars.selectGrammar("dummy.coffee", fileContent).name).toBe "CoffeeScript"
fileContent = '<?xml version="1.0" encoding="UTF-8"?>'
expect(atom.grammars.selectGrammar("grammar.tmLanguage", fileContent).name).toBe "Null Grammar"
fileContent += '\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
expect(atom.grammars.selectGrammar("grammar.tmLanguage", fileContent).name).toBe "Property List (XML)"
it "doesn't read the file when the file contents are specified", ->
filePath = require.resolve("./fixtures/shebang")
filePathContents = fs.readFileSync(filePath, 'utf8')
spyOn(fs, 'read').andCallThrough()
expect(atom.grammars.selectGrammar(filePath, filePathContents).name).toBe "Ruby"
expect(fs.read).not.toHaveBeenCalled()
describe "when multiple grammars have matching fileTypes", ->
it "selects the grammar with the longest fileType match", ->
grammarPath1 = temp.path(suffix: '.json')
fs.writeFileSync grammarPath1, JSON.stringify(
name: 'test1'
scopeName: 'source1'
fileTypes: ['test']
)
grammar1 = atom.grammars.loadGrammarSync(grammarPath1)
expect(atom.grammars.selectGrammar('more.test', '')).toBe grammar1
fs.removeSync(grammarPath1)
grammarPath2 = temp.path(suffix: '.json')
fs.writeFileSync grammarPath2, JSON.stringify(
name: 'test2'
scopeName: 'source2'
fileTypes: ['test', 'more.test']
)
grammar2 = atom.grammars.loadGrammarSync(grammarPath2)
expect(atom.grammars.selectGrammar('more.test', '')).toBe grammar2
fs.removeSync(grammarPath2)
it "favors non-bundled packages when breaking scoring ties", ->
waitsForPromise ->
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'packages', 'package-with-rb-filetype'))
runs ->
atom.grammars.grammarForScopeName('source.ruby').bundledPackage = true
atom.grammars.grammarForScopeName('test.rb').bundledPackage = false
expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env ruby').scopeName).toBe 'source.ruby'
expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env testruby').scopeName).toBe 'test.rb'
expect(atom.grammars.selectGrammar('test.rb').scopeName).toBe 'test.rb'
describe "when there is no file path", ->
it "does not throw an exception (regression)", ->
expect(-> atom.grammars.selectGrammar(null, '#!/usr/bin/ruby')).not.toThrow()
expect(-> atom.grammars.selectGrammar(null, '')).not.toThrow()
expect(-> atom.grammars.selectGrammar(null, null)).not.toThrow()
describe "when the user has custom grammar file types", ->
it "considers the custom file types as well as those defined in the grammar", ->
atom.config.set('core.customFileTypes', 'source.ruby': ['Cheffile'])
expect(atom.grammars.selectGrammar('build/Cheffile', 'cookbook "postgres"').scopeName).toBe 'source.ruby'
it "favors user-defined file types over built-in ones of equal length", ->
atom.config.set('core.customFileTypes',
'source.coffee': ['Rakefile'],
'source.ruby': ['Cakefile']
)
expect(atom.grammars.selectGrammar('Rakefile', '').scopeName).toBe 'source.coffee'
expect(atom.grammars.selectGrammar('Cakefile', '').scopeName).toBe 'source.ruby'
it "favors user-defined file types over grammars with matching first-line-regexps", ->
atom.config.set('core.customFileTypes', 'source.ruby': ['bootstrap'])
expect(atom.grammars.selectGrammar('bootstrap', '#!/usr/bin/env node').scopeName).toBe 'source.ruby'
describe "when there is a grammar with a first line pattern, the file type of the file is known, but from a different grammar", ->
it "favors file type over the matching pattern", ->
expect(atom.grammars.selectGrammar('foo.rb', '#!/usr/bin/env node').scopeName).toBe 'source.ruby'
describe ".removeGrammar(grammar)", ->
it "removes the grammar, so it won't be returned by selectGrammar", ->
grammar = atom.grammars.selectGrammar('foo.js')
atom.grammars.removeGrammar(grammar)
expect(atom.grammars.selectGrammar('foo.js').name).not.toBe grammar.name
describe "grammar overrides", ->
it "logs deprecations and uses the TextEditorRegistry", ->
editor = null
waitsForPromise ->
atom.workspace.open('sample.js').then (e) -> editor = e
runs ->
spyOn(Grim, 'deprecate')
atom.grammars.setGrammarOverrideForPath(editor.getPath(), 'source.ruby')
expect(Grim.deprecate.callCount).toBe 1
expect(editor.getGrammar().name).toBe 'Ruby'
expect(atom.grammars.grammarOverrideForPath(editor.getPath())).toBe('source.ruby')
expect(Grim.deprecate.callCount).toBe 2
atom.grammars.clearGrammarOverrideForPath(editor.getPath(), 'source.ruby')
expect(Grim.deprecate.callCount).toBe 3
expect(editor.getGrammar().name).toBe 'JavaScript'
expect(atom.grammars.grammarOverrideForPath(editor.getPath())).toBe(undefined)
expect(Grim.deprecate.callCount).toBe 4

View File

@@ -1,10 +1,8 @@
/** @babel */
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers'
import {Emitter, Disposable, CompositeDisposable} from 'event-kit'
import {HistoryManager, HistoryProject} from '../src/history-manager'
import StateStore from '../src/state-store'
const {HistoryManager, HistoryProject} = require('../src/history-manager')
const StateStore = require('../src/state-store')
describe("HistoryManager", () => {
let historyManager, commandRegistry, project, stateStore
@@ -182,11 +180,26 @@ describe("HistoryManager", () => {
})
})
describe("saveState" ,() => {
describe("saveState", () => {
let savedHistory
beforeEach(() => {
// historyManager.saveState is spied on globally to prevent specs from
// modifying the shared project history. Since these tests depend on
// saveState, we unspy it but in turn spy on the state store instead
// so that no data is actually stored to it.
jasmine.unspy(historyManager, 'saveState')
spyOn(historyManager.stateStore, 'save').andCallFake((name, history) => {
savedHistory = history
return Promise.resolve()
})
})
it("saves the state", async () => {
await historyManager.addProject(["/save/state"])
await historyManager.saveState()
const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry})
spyOn(historyManager2.stateStore, 'load').andCallFake(name => Promise.resolve(savedHistory))
await historyManager2.loadState()
expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state'])
})

View File

@@ -1,14 +1,13 @@
/** @babel */
import season from 'season'
import dedent from 'dedent'
import electron from 'electron'
import fs from 'fs-plus'
import path from 'path'
import sinon from 'sinon'
import AtomApplication from '../../src/main-process/atom-application'
import parseCommandLine from '../../src/main-process/parse-command-line'
import {timeoutPromise, conditionPromise, emitterEventPromise} from '../async-spec-helpers'
const temp = require('temp').track()
const season = require('season')
const dedent = require('dedent')
const electron = require('electron')
const fs = require('fs-plus')
const path = require('path')
const sinon = require('sinon')
const AtomApplication = require('../../src/main-process/atom-application')
const parseCommandLine = require('../../src/main-process/parse-command-line')
const {timeoutPromise, conditionPromise, emitterEventPromise} = require('../async-spec-helpers')
const ATOM_RESOURCE_PATH = path.resolve(__dirname, '..', '..')
@@ -17,7 +16,7 @@ describe('AtomApplication', function () {
let originalAppQuit, originalShowMessageBox, originalAtomHome, atomApplicationsToDestroy
beforeEach(function () {
beforeEach(() => {
originalAppQuit = electron.app.quit
originalShowMessageBox = electron.dialog.showMessageBox
mockElectronAppQuit()
@@ -34,7 +33,7 @@ describe('AtomApplication', function () {
atomApplicationsToDestroy = []
})
afterEach(async function () {
afterEach(async () => {
process.env.ATOM_HOME = originalAtomHome
for (let atomApplication of atomApplicationsToDestroy) {
await atomApplication.destroy()
@@ -44,8 +43,8 @@ describe('AtomApplication', function () {
electron.dialog.showMessageBox = originalShowMessageBox
})
describe('launch', function () {
it('can open to a specific line number of a file', async function () {
describe('launch', () => {
it('can open to a specific line number of a file', async () => {
const filePath = path.join(makeTempDir(), 'new-file')
fs.writeFileSync(filePath, '1\n2\n3\n4\n')
const atomApplication = buildAtomApplication()
@@ -53,8 +52,8 @@ describe('AtomApplication', function () {
const window = atomApplication.launch(parseCommandLine([filePath + ':3']))
await focusWindow(window)
const cursorRow = await evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (textEditor) {
const cursorRow = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(textEditor => {
sendBackToMainProcess(textEditor.getCursorBufferPosition().row)
})
})
@@ -62,7 +61,7 @@ describe('AtomApplication', function () {
assert.equal(cursorRow, 2)
})
it('can open to a specific line and column of a file', async function () {
it('can open to a specific line and column of a file', async () => {
const filePath = path.join(makeTempDir(), 'new-file')
fs.writeFileSync(filePath, '1\n2\n3\n4\n')
const atomApplication = buildAtomApplication()
@@ -70,8 +69,8 @@ describe('AtomApplication', function () {
const window = atomApplication.launch(parseCommandLine([filePath + ':2:2']))
await focusWindow(window)
const cursorPosition = await evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (textEditor) {
const cursorPosition = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(textEditor => {
sendBackToMainProcess(textEditor.getCursorBufferPosition())
})
})
@@ -79,7 +78,7 @@ describe('AtomApplication', function () {
assert.deepEqual(cursorPosition, {row: 1, column: 1})
})
it('removes all trailing whitespace and colons from the specified path', async function () {
it('removes all trailing whitespace and colons from the specified path', async () => {
let filePath = path.join(makeTempDir(), 'new-file')
fs.writeFileSync(filePath, '1\n2\n3\n4\n')
const atomApplication = buildAtomApplication()
@@ -87,8 +86,8 @@ describe('AtomApplication', function () {
const window = atomApplication.launch(parseCommandLine([filePath + ':: ']))
await focusWindow(window)
const openedPath = await evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (textEditor) {
const openedPath = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(textEditor => {
sendBackToMainProcess(textEditor.getPath())
})
})
@@ -97,7 +96,7 @@ describe('AtomApplication', function () {
})
if (process.platform === 'darwin' || process.platform === 'win32') {
it('positions new windows at an offset distance from the previous window', async function () {
it('positions new windows at an offset distance from the previous window', async () => {
const atomApplication = buildAtomApplication()
const window1 = atomApplication.launch(parseCommandLine([makeTempDir()]))
@@ -115,7 +114,7 @@ describe('AtomApplication', function () {
})
}
it('reuses existing windows when opening paths, but not directories', async function () {
it('reuses existing windows when opening paths, but not directories', async () => {
const dirAPath = makeTempDir("a")
const dirBPath = makeTempDir("b")
const dirCPath = makeTempDir("c")
@@ -127,8 +126,8 @@ describe('AtomApplication', function () {
await emitterEventPromise(window1, 'window:locations-opened')
await focusWindow(window1)
let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (textEditor) {
let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(textEditor => {
sendBackToMainProcess(textEditor.getPath())
})
})
@@ -139,8 +138,8 @@ describe('AtomApplication', function () {
const reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath]))
assert.equal(reusedWindow, window1)
assert.deepEqual(atomApplication.getAllWindows(), [window1])
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) {
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
const subscription = atom.workspace.onDidChangeActivePaneItem(textEditor => {
sendBackToMainProcess(textEditor.getPath())
subscription.dispose()
})
@@ -156,7 +155,7 @@ describe('AtomApplication', function () {
assert.deepEqual(await getTreeViewRootDirectories(window2), [dirCPath])
})
it('adds folders to existing windows when the --add option is used', async function () {
it('adds folders to existing windows when the --add option is used', async () => {
const dirAPath = makeTempDir("a")
const dirBPath = makeTempDir("b")
const dirCPath = makeTempDir("c")
@@ -167,8 +166,8 @@ describe('AtomApplication', function () {
const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'new-file')]))
await focusWindow(window1)
let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (textEditor) {
let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(textEditor => {
sendBackToMainProcess(textEditor.getPath())
})
})
@@ -179,8 +178,8 @@ describe('AtomApplication', function () {
let reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath, '--add']))
assert.equal(reusedWindow, window1)
assert.deepEqual(atomApplication.getAllWindows(), [window1])
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) {
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
const subscription = atom.workspace.onDidChangeActivePaneItem(textEditor => {
sendBackToMainProcess(textEditor.getPath())
subscription.dispose()
})
@@ -198,14 +197,14 @@ describe('AtomApplication', function () {
assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirCPath, dirBPath])
})
it('persists window state based on the project directories', async function () {
it('persists window state based on the project directories', async () => {
const tempDirPath = makeTempDir()
const atomApplication = buildAtomApplication()
const nonExistentFilePath = path.join(tempDirPath, 'new-file')
const window1 = atomApplication.launch(parseCommandLine([nonExistentFilePath]))
await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (textEditor) {
await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(textEditor => {
textEditor.insertText('Hello World!')
sendBackToMainProcess(null)
})
@@ -217,7 +216,7 @@ describe('AtomApplication', function () {
// Restore unsaved state when opening the directory itself
const window2 = atomApplication.launch(parseCommandLine([tempDirPath]))
await window2.loadedPromise
const window2Text = await evalInWebContents(window2.browserWindow.webContents, function (sendBackToMainProcess) {
const window2Text = await evalInWebContents(window2.browserWindow.webContents, sendBackToMainProcess => {
const textEditor = atom.workspace.getActiveTextEditor()
textEditor.moveToBottom()
textEditor.insertText(' How are you?')
@@ -231,13 +230,13 @@ describe('AtomApplication', function () {
// Restore unsaved state when opening a path to a non-existent file in the directory
const window3 = atomApplication.launch(parseCommandLine([path.join(tempDirPath, 'another-non-existent-file')]))
await window3.loadedPromise
const window3Texts = await evalInWebContents(window3.browserWindow.webContents, function (sendBackToMainProcess, nonExistentFilePath) {
const window3Texts = await evalInWebContents(window3.browserWindow.webContents, (sendBackToMainProcess, nonExistentFilePath) => {
sendBackToMainProcess(atom.workspace.getTextEditors().map(editor => editor.getText()))
})
assert.include(window3Texts, 'Hello World! How are you?')
})
it('shows all directories in the tree view when multiple directory paths are passed to Atom', async function () {
it('shows all directories in the tree view when multiple directory paths are passed to Atom', async () => {
const dirAPath = makeTempDir("a")
const dirBPath = makeTempDir("b")
const dirBSubdirPath = path.join(dirBPath, 'c')
@@ -250,7 +249,7 @@ describe('AtomApplication', function () {
assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirBPath])
})
it('reuses windows with no project paths to open directories', async function () {
it('reuses windows with no project paths to open directories', async () => {
const tempDirPath = makeTempDir()
const atomApplication = buildAtomApplication()
const window1 = atomApplication.launch(parseCommandLine([]))
@@ -261,18 +260,18 @@ describe('AtomApplication', function () {
await conditionPromise(async () => (await getTreeViewRootDirectories(reusedWindow)).length > 0)
})
it('opens a new window with a single untitled buffer when launched with no path, even if windows already exist', async function () {
it('opens a new window with a single untitled buffer when launched with no path, even if windows already exist', async () => {
const atomApplication = buildAtomApplication()
const window1 = atomApplication.launch(parseCommandLine([]))
await focusWindow(window1)
const window1EditorTitle = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const window1EditorTitle = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle())
})
assert.equal(window1EditorTitle, 'untitled')
const window2 = atomApplication.openWithOptions(parseCommandLine([]))
await focusWindow(window2)
const window2EditorTitle = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const window2EditorTitle = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle())
})
assert.equal(window2EditorTitle, 'untitled')
@@ -280,7 +279,7 @@ describe('AtomApplication', function () {
assert.deepEqual(atomApplication.getAllWindows(), [window2, window1])
})
it('does not open an empty editor when opened with no path if the core.openEmptyEditorOnStart config setting is false', async function () {
it('does not open an empty editor when opened with no path if the core.openEmptyEditorOnStart config setting is false', async () => {
const configPath = path.join(process.env.ATOM_HOME, 'config.cson')
const config = season.readFileSync(configPath)
if (!config['*'].core) config['*'].core = {}
@@ -294,19 +293,19 @@ describe('AtomApplication', function () {
// wait a bit just to make sure we don't pass due to querying the render process before it loads
await timeoutPromise(1000)
const itemCount = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const itemCount = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
sendBackToMainProcess(atom.workspace.getActivePane().getItems().length)
})
assert.equal(itemCount, 0)
})
it('opens an empty text editor and loads its parent directory in the tree-view when launched with a new file path', async function () {
it('opens an empty text editor and loads its parent directory in the tree-view when launched with a new file path', async () => {
const atomApplication = buildAtomApplication()
const newFilePath = path.join(makeTempDir(), 'new-file')
const window = atomApplication.launch(parseCommandLine([newFilePath]))
await focusWindow(window)
const {editorTitle, editorText} = await evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (editor) {
const {editorTitle, editorText} = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(editor => {
sendBackToMainProcess({editorTitle: editor.getTitle(), editorText: editor.getText()})
})
})
@@ -315,7 +314,7 @@ describe('AtomApplication', function () {
assert.deepEqual(await getTreeViewRootDirectories(window), [path.dirname(newFilePath)])
})
it('adds a remote directory to the project when launched with a remote directory', async function () {
it('adds a remote directory to the project when launched with a remote directory', async () => {
const packagePath = path.join(__dirname, '..', 'fixtures', 'packages', 'package-with-directory-provider')
const packagesDirPath = path.join(process.env.ATOM_HOME, 'packages')
fs.mkdirSync(packagesDirPath)
@@ -338,13 +337,13 @@ describe('AtomApplication', function () {
assert.deepEqual(directories, [{type: 'FakeRemoteDirectory', path: remotePath}])
function getProjectDirectories () {
return evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) {
return evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
sendBackToMainProcess(atom.project.getDirectories().map(d => ({ type: d.constructor.name, path: d.getPath() })))
})
}
})
it('reopens any previously opened windows when launched with no path', async function () {
it('reopens any previously opened windows when launched with no path', async () => {
if (process.platform === 'win32') return; // Test is too flakey on Windows
const tempDirPath1 = makeTempDir()
@@ -352,11 +351,9 @@ describe('AtomApplication', function () {
const atomApplication1 = buildAtomApplication()
const app1Window1 = atomApplication1.launch(parseCommandLine([tempDirPath1]))
await emitterEventPromise(app1Window1, 'window:locations-opened')
const app1Window2 = atomApplication1.launch(parseCommandLine([tempDirPath2]))
await Promise.all([
emitterEventPromise(app1Window1, 'window:locations-opened'),
emitterEventPromise(app1Window2, 'window:locations-opened')
])
await emitterEventPromise(app1Window2, 'window:locations-opened')
await Promise.all([
app1Window1.prepareToUnload(),
@@ -374,7 +371,7 @@ describe('AtomApplication', function () {
assert.deepEqual(await getTreeViewRootDirectories(app2Window2), [tempDirPath2])
})
it('does not reopen any previously opened windows when launched with no path and `core.restorePreviousWindowsOnStart` is no', async function () {
it('does not reopen any previously opened windows when launched with no path and `core.restorePreviousWindowsOnStart` is no', async () => {
const atomApplication1 = buildAtomApplication()
const app1Window1 = atomApplication1.launch(parseCommandLine([makeTempDir()]))
await focusWindow(app1Window1)
@@ -393,30 +390,136 @@ describe('AtomApplication', function () {
assert.deepEqual(app2Window.representedDirectoryPaths, [])
})
describe('when closing the last window', function () {
describe('when the `--wait` flag is passed', () => {
let killedPids, atomApplication, onDidKillProcess
beforeEach(() => {
killedPids = []
onDidKillProcess = null
atomApplication = buildAtomApplication({
killProcess (pid) {
killedPids.push(pid)
if (onDidKillProcess) onDidKillProcess()
}
})
})
it('kills the specified pid after a newly-opened window is closed', async () => {
const window1 = atomApplication.launch(parseCommandLine(['--wait', '--pid', '101']))
await focusWindow(window1)
const [window2] = atomApplication.launch(parseCommandLine(['--new-window', '--wait', '--pid', '102']))
await focusWindow(window2)
assert.deepEqual(killedPids, [])
let processKillPromise = new Promise(resolve => { onDidKillProcess = resolve })
window1.close()
await processKillPromise
assert.deepEqual(killedPids, [101])
processKillPromise = new Promise(resolve => { onDidKillProcess = resolve })
window2.close()
await processKillPromise
assert.deepEqual(killedPids, [101, 102])
})
it('kills the specified pid after a newly-opened file in an existing window is closed', async () => {
const window = atomApplication.launch(parseCommandLine(['--wait', '--pid', '101']))
await focusWindow(window)
const filePath1 = temp.openSync('test').path
const filePath2 = temp.openSync('test').path
fs.writeFileSync(filePath1, 'File 1')
fs.writeFileSync(filePath2, 'File 2')
const reusedWindow = atomApplication.launch(parseCommandLine(['--wait', '--pid', '102', filePath1, filePath2]))
assert.equal(reusedWindow, window)
const activeEditorPath = await evalInWebContents(window.browserWindow.webContents, send => {
const subscription = atom.workspace.onDidChangeActivePaneItem(editor => {
send(editor.getPath())
subscription.dispose()
})
})
assert([filePath1, filePath2].includes(activeEditorPath))
assert.deepEqual(killedPids, [])
await evalInWebContents(window.browserWindow.webContents, send => {
atom.workspace.getActivePaneItem().destroy()
send()
})
await timeoutPromise(100)
assert.deepEqual(killedPids, [])
let processKillPromise = new Promise(resolve => { onDidKillProcess = resolve })
await evalInWebContents(window.browserWindow.webContents, send => {
atom.workspace.getActivePaneItem().destroy()
send()
})
await processKillPromise
assert.deepEqual(killedPids, [102])
processKillPromise = new Promise(resolve => { onDidKillProcess = resolve })
window.close()
await processKillPromise
assert.deepEqual(killedPids, [102, 101])
})
it('kills the specified pid after a newly-opened directory in an existing window is closed', async () => {
const window = atomApplication.launch(parseCommandLine([]))
await focusWindow(window)
const dirPath1 = makeTempDir()
const reusedWindow = atomApplication.launch(parseCommandLine(['--wait', '--pid', '101', dirPath1]))
assert.equal(reusedWindow, window)
assert.deepEqual(await getTreeViewRootDirectories(window), [dirPath1])
assert.deepEqual(killedPids, [])
const dirPath2 = makeTempDir()
await evalInWebContents(window.browserWindow.webContents, (send, dirPath1, dirPath2) => {
atom.project.setPaths([dirPath1, dirPath2])
send()
}, dirPath1, dirPath2)
await timeoutPromise(100)
assert.deepEqual(killedPids, [])
let processKillPromise = new Promise(resolve => { onDidKillProcess = resolve })
await evalInWebContents(window.browserWindow.webContents, (send, dirPath2) => {
atom.project.setPaths([dirPath2])
send()
}, dirPath2)
await processKillPromise
assert.deepEqual(killedPids, [101])
})
})
describe('when closing the last window', () => {
if (process.platform === 'linux' || process.platform === 'win32') {
it('quits the application', async function () {
it('quits the application', async () => {
const atomApplication = buildAtomApplication()
const window = atomApplication.launch(parseCommandLine([path.join(makeTempDir("a"), 'file-a')]))
await focusWindow(window)
window.close()
await window.closedPromise
assert(electron.app.hasQuitted())
await atomApplication.lastBeforeQuitPromise
assert(electron.app.didQuit())
})
} else if (process.platform === 'darwin') {
it('leaves the application open', async function () {
it('leaves the application open', async () => {
const atomApplication = buildAtomApplication()
const window = atomApplication.launch(parseCommandLine([path.join(makeTempDir("a"), 'file-a')]))
await focusWindow(window)
window.close()
await window.closedPromise
assert(!electron.app.hasQuitted())
await timeoutPromise(1000)
assert(!electron.app.didQuit())
})
}
})
describe('when adding or removing project folders', function () {
it('stores the window state immediately', async function () {
describe('when adding or removing project folders', () => {
it('stores the window state immediately', async () => {
const dirA = makeTempDir()
const dirB = makeTempDir()
@@ -443,8 +546,8 @@ describe('AtomApplication', function () {
})
})
describe('when opening atom:// URLs', function () {
it('loads the urlMain file in a new window', async function () {
describe('when opening atom:// URLs', () => {
it('loads the urlMain file in a new window', async () => {
const packagePath = path.join(__dirname, '..', 'fixtures', 'packages', 'package-with-url-main')
const packagesDirPath = path.join(process.env.ATOM_HOME, 'packages')
fs.mkdirSync(packagesDirPath)
@@ -456,7 +559,7 @@ describe('AtomApplication', function () {
let windows = atomApplication.launch(launchOptions)
await windows[0].loadedPromise
let reached = await evalInWebContents(windows[0].browserWindow.webContents, function (sendBackToMainProcess) {
let reached = await evalInWebContents(windows[0].browserWindow.webContents, sendBackToMainProcess => {
sendBackToMainProcess(global.reachedUrlMain)
})
assert.equal(reached, true);
@@ -490,7 +593,7 @@ describe('AtomApplication', function () {
})
})
it('waits until all the windows have saved their state before quitting', async function () {
it('waits until all the windows have saved their state before quitting', async () => {
const dirAPath = makeTempDir("a")
const dirBPath = makeTempDir("b")
const atomApplication = buildAtomApplication()
@@ -499,9 +602,12 @@ describe('AtomApplication', function () {
const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')]))
await focusWindow(window2)
electron.app.quit()
assert(!electron.app.hasQuitted())
await new Promise(process.nextTick)
assert(!electron.app.didQuit())
await Promise.all([window1.lastPrepareToUnloadPromise, window2.lastPrepareToUnloadPromise])
assert(electron.app.hasQuitted())
await new Promise(process.nextTick)
assert(electron.app.didQuit())
})
it('prevents quitting if user cancels when prompted to save an item', async () => {
@@ -509,30 +615,30 @@ describe('AtomApplication', function () {
const window1 = atomApplication.launch(parseCommandLine([]))
const window2 = atomApplication.launch(parseCommandLine([]))
await Promise.all([window1.loadedPromise, window2.loadedPromise])
await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.getActiveTextEditor().insertText('unsaved text')
sendBackToMainProcess()
})
// Choosing "Cancel"
mockElectronShowMessageBox({choice: 1})
mockElectronShowMessageBox({response: 1})
electron.app.quit()
await atomApplication.lastBeforeQuitPromise
assert(!electron.app.hasQuitted())
assert(!electron.app.didQuit())
assert.equal(electron.app.quit.callCount, 1) // Ensure choosing "Cancel" doesn't try to quit the electron app more than once (regression)
// Choosing "Don't save"
mockElectronShowMessageBox({choice: 2})
mockElectronShowMessageBox({response: 2})
electron.app.quit()
await atomApplication.lastBeforeQuitPromise
assert(electron.app.hasQuitted())
assert(electron.app.didQuit())
})
function buildAtomApplication () {
const atomApplication = new AtomApplication({
function buildAtomApplication (params = {}) {
const atomApplication = new AtomApplication(Object.assign({
resourcePath: ATOM_RESOURCE_PATH,
atomHomeDirPath: process.env.ATOM_HOME
})
atomHomeDirPath: process.env.ATOM_HOME,
}, params))
atomApplicationsToDestroy.push(atomApplication)
return atomApplication
}
@@ -544,40 +650,34 @@ describe('AtomApplication', function () {
}
function mockElectronAppQuit () {
let quitted = false
electron.app.quit = function () {
if (electron.app.quit.callCount) {
electron.app.quit.callCount++
} else {
electron.app.quit.callCount = 1
}
let didQuit = false
let shouldQuit = true
electron.app.emit('before-quit', {preventDefault: () => { shouldQuit = false }})
if (shouldQuit) {
quitted = true
}
}
electron.app.hasQuitted = function () {
return quitted
electron.app.quit = function () {
this.quit.callCount++
let defaultPrevented = false
this.emit('before-quit', {preventDefault() { defaultPrevented = true }})
if (!defaultPrevented) didQuit = true
}
electron.app.quit.callCount = 0
electron.app.didQuit = () => didQuit
}
function mockElectronShowMessageBox ({choice}) {
electron.dialog.showMessageBox = function () {
return choice
function mockElectronShowMessageBox ({response}) {
electron.dialog.showMessageBox = (window, options, callback) => {
callback(response)
}
}
function makeTempDir (name) {
const temp = require('temp').track()
return fs.realpathSync(temp.mkdirSync(name))
}
let channelIdCounter = 0
function evalInWebContents (webContents, source, ...args) {
const channelId = 'eval-result-' + channelIdCounter++
return new Promise(function (resolve) {
return new Promise(resolve => {
electron.ipcMain.on(channelId, receiveResult)
function receiveResult (event, result) {
@@ -589,13 +689,13 @@ describe('AtomApplication', function () {
function sendBackToMainProcess (result) {
require('electron').ipcRenderer.send('${channelId}', result)
}
(${source})(sendBackToMainProcess)
(${source})(sendBackToMainProcess, ${args.map(JSON.stringify).join(', ')})
`)
})
}
function getTreeViewRootDirectories (atomWindow) {
return evalInWebContents(atomWindow.browserWindow.webContents, function (sendBackToMainProcess) {
return evalInWebContents(atomWindow.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.getLeftDock().observeActivePaneItem((treeView) => {
if (treeView) {
sendBackToMainProcess(
@@ -609,8 +709,8 @@ describe('AtomApplication', function () {
}
function clearElectronSession () {
return new Promise(function (resolve) {
electron.session.defaultSession.clearStorageData(function () {
return new Promise(resolve => {
electron.session.defaultSession.clearStorageData(() => {
// Resolve promise on next tick, otherwise the process stalls. This
// might be a bug in Electron, but it's probably fixed on the newer
// versions.

View File

@@ -66,4 +66,27 @@ describe('NotificationManager', () => {
expect(notification.getType()).toBe('success')
})
})
describe('clearing notifications', () => {
it('clears the notifications when ::clear has been called', () => {
manager.addSuccess('success')
expect(manager.getNotifications().length).toBe(1)
manager.clear()
expect(manager.getNotifications().length).toBe(0)
})
describe('adding events', () => {
let clearSpy
beforeEach(() => {
clearSpy = jasmine.createSpy()
manager.onDidClearNotifications(clearSpy)
})
it('emits an event when the notifications have been cleared', () => {
manager.clear()
expect(clearSpy).toHaveBeenCalled()
})
})
})
})

View File

@@ -1030,6 +1030,13 @@ describe('PackageManager', () => {
expect(atom.grammars.selectGrammar('a.alot').name).toBe('Alot')
expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Alittle')
})
it('loads any tree-sitter grammars defined in the package', async () => {
await atom.packages.activatePackage('package-with-tree-sitter-grammar')
const grammar = atom.grammars.selectGrammar('test.somelang')
expect(grammar.name).toBe('Some Language')
expect(grammar.languageModule.isFakeTreeSitterParser).toBe(true)
})
})
describe('scoped-property loading', () => {

View File

@@ -5,7 +5,7 @@ describe('PaneContainer', () => {
let confirm, params
beforeEach(() => {
confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0)
confirm = spyOn(atom.applicationDelegate, 'confirm').andCallFake((options, callback) => callback(0))
params = {
location: 'center',
config: atom.config,
@@ -280,14 +280,14 @@ describe('PaneContainer', () => {
})
it('returns true if the user saves all modified files when prompted', async () => {
confirm.andReturn(0)
confirm.andCallFake((options, callback) => callback(0))
const saved = await container.confirmClose()
expect(confirm).toHaveBeenCalled()
expect(saved).toBeTruthy()
})
it('returns false if the user cancels saving any modified file', async () => {
confirm.andReturn(1)
confirm.andCallFake((options, callback) => callback(1))
const saved = await container.confirmClose()
expect(confirm).toHaveBeenCalled()
expect(saved).toBeFalsy()

View File

@@ -3,7 +3,7 @@ const {Emitter} = require('event-kit')
const Grim = require('grim')
const Pane = require('../src/pane')
const PaneContainer = require('../src/pane-container')
const {it, fit, ffit, fffit, beforeEach, timeoutPromise} = require('./async-spec-helpers')
const {it, fit, ffit, fffit, beforeEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers')
describe('Pane', () => {
let confirm, showSaveDialog, deserializerDisposable
@@ -564,7 +564,7 @@ describe('Pane', () => {
describe('when the item has a uri', () => {
it('saves the item before destroying it', async () => {
itemURI = 'test'
confirm.andReturn(0)
confirm.andCallFake((options, callback) => callback(0))
const success = await pane.destroyItem(item1)
expect(item1.save).toHaveBeenCalled()
@@ -576,13 +576,17 @@ describe('Pane', () => {
describe('when the item has no uri', () => {
it('presents a save-as dialog, then saves the item with the given uri before removing and destroying it', async () => {
jasmine.useRealClock()
itemURI = null
showSaveDialog.andReturn('/selected/path')
confirm.andReturn(0)
showSaveDialog.andCallFake((options, callback) => callback('/selected/path'))
confirm.andCallFake((options, callback) => callback(0))
const success = await pane.destroyItem(item1)
expect(showSaveDialog).toHaveBeenCalledWith({})
expect(showSaveDialog.mostRecentCall.args[0]).toEqual({})
await conditionPromise(() => item1.saveAs.callCount === 1)
expect(item1.saveAs).toHaveBeenCalledWith('/selected/path')
expect(pane.getItems().includes(item1)).toBe(false)
expect(item1.isDestroyed()).toBe(true)
@@ -593,7 +597,7 @@ describe('Pane', () => {
describe("if the [Don't Save] option is selected", () => {
it('removes and destroys the item without saving it', async () => {
confirm.andReturn(2)
confirm.andCallFake((options, callback) => callback(2))
const success = await pane.destroyItem(item1)
expect(item1.save).not.toHaveBeenCalled()
@@ -605,7 +609,7 @@ describe('Pane', () => {
describe('if the [Cancel] option is selected', () => {
it('does not save, remove, or destroy the item', async () => {
confirm.andReturn(1)
confirm.andCallFake((options, callback) => callback(1))
const success = await pane.destroyItem(item1)
expect(item1.save).not.toHaveBeenCalled()
@@ -735,7 +739,7 @@ describe('Pane', () => {
beforeEach(() => {
pane = new Pane(paneParams({items: [new Item('A')]}))
showSaveDialog.andReturn('/selected/path')
showSaveDialog.andCallFake((options, callback) => callback('/selected/path'))
})
describe('when the active item has a uri', () => {
@@ -764,7 +768,7 @@ describe('Pane', () => {
it('opens a save dialog and saves the current item as the selected path', async () => {
pane.getActiveItem().saveAs = jasmine.createSpy('saveAs')
await pane.saveActiveItem()
expect(showSaveDialog).toHaveBeenCalledWith({})
expect(showSaveDialog.mostRecentCall.args[0]).toEqual({})
expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith('/selected/path')
})
})
@@ -779,7 +783,7 @@ describe('Pane', () => {
it('does nothing if the user cancels choosing a path', async () => {
pane.getActiveItem().saveAs = jasmine.createSpy('saveAs')
showSaveDialog.andReturn(undefined)
showSaveDialog.andCallFake((options, callback) => callback(undefined))
await pane.saveActiveItem()
expect(pane.getActiveItem().saveAs).not.toHaveBeenCalled()
})
@@ -835,15 +839,19 @@ describe('Pane', () => {
beforeEach(() => {
pane = new Pane(paneParams({items: [new Item('A')]}))
showSaveDialog.andReturn('/selected/path')
showSaveDialog.andCallFake((options, callback) => callback('/selected/path'))
})
describe('when the current item has a saveAs method', () => {
it('opens the save dialog and calls saveAs on the item with the selected path', () => {
it('opens the save dialog and calls saveAs on the item with the selected path', async () => {
jasmine.useRealClock()
pane.getActiveItem().path = __filename
pane.getActiveItem().saveAs = jasmine.createSpy('saveAs')
pane.saveActiveItemAs()
expect(showSaveDialog).toHaveBeenCalledWith({defaultPath: __filename})
expect(showSaveDialog.mostRecentCall.args[0]).toEqual({defaultPath: __filename})
await conditionPromise(() => pane.getActiveItem().saveAs.callCount === 1)
expect(pane.getActiveItem().saveAs).toHaveBeenCalledWith('/selected/path')
})
})
@@ -1210,7 +1218,7 @@ describe('Pane', () => {
item1.getURI = () => '/test/path'
item1.save = jasmine.createSpy('save')
confirm.andReturn(0)
confirm.andCallFake((options, callback) => callback(0))
await pane.close()
expect(confirm).toHaveBeenCalled()
expect(item1.save).toHaveBeenCalled()
@@ -1225,7 +1233,7 @@ describe('Pane', () => {
item1.getURI = () => '/test/path'
item1.save = jasmine.createSpy('save')
confirm.andReturn(1)
confirm.andCallFake((options, callback) => callback(1))
await pane.close()
expect(confirm).toHaveBeenCalled()
@@ -1240,8 +1248,8 @@ describe('Pane', () => {
item1.shouldPromptToSave = () => true
item1.saveAs = jasmine.createSpy('saveAs')
confirm.andReturn(0)
showSaveDialog.andReturn(undefined)
confirm.andCallFake((options, callback) => callback(0))
showSaveDialog.andCallFake((options, callback) => callback(undefined))
await pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
@@ -1270,12 +1278,12 @@ describe('Pane', () => {
it('does not destroy the pane if save fails and user clicks cancel', async () => {
let confirmations = 0
confirm.andCallFake(() => {
confirm.andCallFake((options, callback) => {
confirmations++
if (confirmations === 1) {
return 0 // click save
callback(0) // click save
} else {
return 1
callback(1)
}
}) // click cancel
@@ -1290,17 +1298,17 @@ describe('Pane', () => {
item1.saveAs = jasmine.createSpy('saveAs').andReturn(true)
let confirmations = 0
confirm.andCallFake(() => {
confirm.andCallFake((options, callback) => {
confirmations++
return 0
callback(0)
}) // save and then save as
showSaveDialog.andReturn('new/path')
showSaveDialog.andCallFake((options, callback) => callback('new/path'))
await pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(confirmations).toBe(2)
expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalledWith({})
expect(atom.applicationDelegate.showSaveDialog.mostRecentCall.args[0]).toEqual({})
expect(item1.save).toHaveBeenCalled()
expect(item1.saveAs).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe(true)
@@ -1315,20 +1323,21 @@ describe('Pane', () => {
})
let confirmations = 0
confirm.andCallFake(() => {
confirm.andCallFake((options, callback) => {
confirmations++
if (confirmations < 3) {
return 0 // save, save as, save as
callback(0) // save, save as, save as
} else {
callback(2) // don't save
}
return 2
}) // don't save
})
showSaveDialog.andReturn('new/path')
showSaveDialog.andCallFake((options, callback) => callback('new/path'))
await pane.close()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(confirmations).toBe(3)
expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalledWith({})
expect(atom.applicationDelegate.showSaveDialog.mostRecentCall.args[0]).toEqual({})
expect(item1.save).toHaveBeenCalled()
expect(item1.saveAs).toHaveBeenCalled()
expect(pane.isDestroyed()).toBe(true)

View File

@@ -35,7 +35,12 @@ describe('Project', () => {
})
it("does not deserialize paths to directories that don't exist", () => {
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject = new Project({
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
grammarRegistry: atom.grammars
})
const state = atom.project.serialize()
state.paths.push('/directory/that/does/not/exist')
@@ -55,7 +60,12 @@ describe('Project', () => {
const childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child')
fs.mkdirSync(childPath)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject = new Project({
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
grammarRegistry: atom.grammars
})
atom.project.setPaths([childPath])
const state = atom.project.serialize()
@@ -80,7 +90,12 @@ describe('Project', () => {
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject = new Project({
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
grammarRegistry: atom.grammars
})
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -93,7 +108,12 @@ describe('Project', () => {
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject = new Project({
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
grammarRegistry: atom.grammars
})
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -113,7 +133,12 @@ describe('Project', () => {
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
fs.mkdirSync(pathToOpen)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject = new Project({
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
grammarRegistry: atom.grammars
})
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -131,7 +156,12 @@ describe('Project', () => {
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
fs.chmodSync(pathToOpen, '000')
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject = new Project({
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
grammarRegistry: atom.grammars
})
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -148,7 +178,12 @@ describe('Project', () => {
runs(() => {
expect(atom.project.getBuffers().length).toBe(1)
fs.unlinkSync(pathToOpen)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject = new Project({
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
grammarRegistry: atom.grammars
})
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -165,7 +200,12 @@ describe('Project', () => {
atom.workspace.getActiveTextEditor().setText('unsaved\n')
expect(atom.project.getBuffers().length).toBe(1)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
deserializedProject = new Project({
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
grammarRegistry: atom.grammars
})
})
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -189,7 +229,12 @@ describe('Project', () => {
layerA = bufferA.addMarkerLayer({persistent: true})
markerA = layerA.markPosition([0, 3])
bufferA.append('!')
notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
notQuittingProject = new Project({
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
grammarRegistry: atom.grammars
})
})
waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false})))
@@ -197,7 +242,12 @@ describe('Project', () => {
runs(() => {
expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).toBeUndefined()
expect(notQuittingProject.getBuffers()[0].undo()).toBe(false)
quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
quittingProject = new Project({
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm,
grammarRegistry: atom.grammars
})
})
waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true})))
@@ -209,7 +259,7 @@ describe('Project', () => {
})
})
describe('when an editor is saved and the project has no path', () =>
describe('when an editor is saved and the project has no path', () => {
it("sets the project's path to the saved file's parent directory", () => {
const tempFile = temp.openSync().path
atom.project.setPaths([])
@@ -222,7 +272,7 @@ describe('Project', () => {
runs(() => expect(atom.project.getPaths()[0]).toBe(path.dirname(tempFile)))
})
)
})
describe('before and after saving a buffer', () => {
let buffer
@@ -422,7 +472,7 @@ describe('Project', () => {
atom.project.onDidAddBuffer(newBufferHandler)
})
describe("when given an absolute path that isn't currently open", () =>
describe("when given an absolute path that isn't currently open", () => {
it("returns a new edit session for the given path and emits 'buffer-created'", () => {
let editor = null
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
@@ -432,9 +482,9 @@ describe('Project', () => {
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
})
})
)
})
describe("when given a relative path that isn't currently opened", () =>
describe("when given a relative path that isn't currently opened", () => {
it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", () => {
let editor = null
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
@@ -444,9 +494,9 @@ describe('Project', () => {
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
})
})
)
})
describe('when passed the path to a buffer that is currently opened', () =>
describe('when passed the path to a buffer that is currently opened', () => {
it('returns a new edit session containing currently opened buffer', () => {
let editor = null
@@ -465,9 +515,9 @@ describe('Project', () => {
})
)
})
)
})
describe('when not passed a path', () =>
describe('when not passed a path', () => {
it("returns a new edit session and emits 'buffer-created'", () => {
let editor = null
waitsForPromise(() => atom.workspace.open().then(o => { editor = o }))
@@ -477,7 +527,7 @@ describe('Project', () => {
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
})
})
)
})
})
describe('.bufferForPath(path)', () => {
@@ -537,7 +587,7 @@ describe('Project', () => {
})
describe('.repositoryForDirectory(directory)', () => {
it('resolves to null when the directory does not have a repository', () =>
it('resolves to null when the directory does not have a repository', () => {
waitsForPromise(() => {
const directory = new Directory('/tmp')
return atom.project.repositoryForDirectory(directory).then((result) => {
@@ -546,9 +596,9 @@ describe('Project', () => {
expect(atom.project.repositoryPromisesByPath.size).toBe(0)
})
})
)
})
it('resolves to a GitRepository and is cached when the given directory is a Git repo', () =>
it('resolves to a GitRepository and is cached when the given directory is a Git repo', () => {
waitsForPromise(() => {
const directory = new Directory(path.join(__dirname, '..'))
const promise = atom.project.repositoryForDirectory(directory)
@@ -561,7 +611,7 @@ describe('Project', () => {
expect(atom.project.repositoryForDirectory(directory)).toBe(promise)
})
})
)
})
it('creates a new repository if a previous one with the same directory had been destroyed', () => {
let repository = null
@@ -582,14 +632,14 @@ describe('Project', () => {
})
describe('.setPaths(paths, options)', () => {
describe('when path is a file', () =>
describe('when path is a file', () => {
it("sets its path to the file's parent directory and updates the root directory", () => {
const filePath = require.resolve('./fixtures/dir/a')
atom.project.setPaths([filePath])
expect(atom.project.getPaths()[0]).toEqual(path.dirname(filePath))
expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(filePath))
})
)
})
describe('when path is a directory', () => {
it('assigns the directories and repositories', () => {
@@ -636,13 +686,13 @@ describe('Project', () => {
})
})
describe('when no paths are given', () =>
describe('when no paths are given', () => {
it('clears its path', () => {
atom.project.setPaths([])
expect(atom.project.getPaths()).toEqual([])
expect(atom.project.getDirectories()).toEqual([])
})
)
})
it('normalizes the path to remove consecutive slashes, ., and .. segments', () => {
atom.project.setPaths([`${require.resolve('./fixtures/dir/a')}${path.sep}b${path.sep}${path.sep}..`])
@@ -693,9 +743,9 @@ describe('Project', () => {
expect(atom.project.getPaths()).toEqual(previousPaths)
})
it('optionally throws on non-existent directories', () =>
it('optionally throws on non-existent directories', () => {
expect(() => atom.project.addPath('/this-definitely/does-not-exist', {mustExist: true})).toThrow()
)
})
})
describe('.removePath(path)', () => {
@@ -813,7 +863,7 @@ describe('Project', () => {
})
})
describe('.onDidAddBuffer()', () =>
describe('.onDidAddBuffer()', () => {
it('invokes the callback with added text buffers', () => {
const buffers = []
const added = []
@@ -838,9 +888,9 @@ describe('Project', () => {
expect(added).toEqual([buffers[1]])
})
})
)
})
describe('.observeBuffers()', () =>
describe('.observeBuffers()', () => {
it('invokes the observer with current and future text buffers', () => {
const buffers = []
const observed = []
@@ -872,7 +922,7 @@ describe('Project', () => {
expect(observed).toEqual(buffers)
})
})
)
})
describe('.relativize(path)', () => {
it('returns the path, relative to whichever root directory it is inside of', () => {
@@ -906,21 +956,21 @@ describe('Project', () => {
expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')])
})
describe("when the given path isn't inside of any of the project's path", () =>
describe("when the given path isn't inside of any of the project's path", () => {
it('returns null for the root path, and the given path unchanged', () => {
const randomPath = path.join('some', 'random', 'path')
expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath])
})
)
})
describe('when the given path is a URL', () =>
describe('when the given path is a URL', () => {
it('returns null for the root path, and the given path unchanged', () => {
const url = 'http://the-path'
expect(atom.project.relativizePath(url)).toEqual([null, url])
})
)
})
describe('when the given path is inside more than one root folder', () =>
describe('when the given path is inside more than one root folder', () => {
it('uses the root folder that is closest to the given path', () => {
atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir'))
@@ -933,10 +983,10 @@ describe('Project', () => {
path.join('somewhere', 'something.txt')
])
})
)
})
})
describe('.contains(path)', () =>
describe('.contains(path)', () => {
it('returns whether or not the given path is in one of the root directories', () => {
const rootPath = atom.project.getPaths()[0]
const childPath = path.join(rootPath, 'some', 'child', 'directory')
@@ -945,11 +995,11 @@ describe('Project', () => {
const randomPath = path.join('some', 'random', 'path')
expect(atom.project.contains(randomPath)).toBe(false)
})
)
})
describe('.resolvePath(uri)', () =>
describe('.resolvePath(uri)', () => {
it('normalizes disk drive letter in passed path on #win32', () => {
expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')
})
)
})
})

View File

@@ -7,10 +7,11 @@ fs = require 'fs-plus'
Grim = require 'grim'
pathwatcher = require 'pathwatcher'
FindParentDir = require 'find-parent-dir'
{CompositeDisposable} = require 'event-kit'
TextEditor = require '../src/text-editor'
TextEditorElement = require '../src/text-editor-element'
TokenizedBuffer = require '../src/tokenized-buffer'
TextMateLanguageMode = require '../src/text-mate-language-mode'
clipboard = require '../src/safe-clipboard'
jasmineStyle = document.createElement('style')
@@ -61,6 +62,9 @@ else
specProjectPath = require('os').tmpdir()
beforeEach ->
# Do not clobber recent project history
spyOn(Object.getPrototypeOf(atom.history), 'saveState').andReturn(Promise.resolve())
atom.project.setPaths([specProjectPath])
window.resetTimeouts()
@@ -96,8 +100,21 @@ beforeEach ->
spyOn(TextEditor.prototype, "shouldPromptToSave").andReturn false
# make tokenization synchronous
TokenizedBuffer.prototype.chunkSize = Infinity
spyOn(TokenizedBuffer.prototype, "tokenizeInBackground").andCallFake -> @tokenizeNextChunk()
TextMateLanguageMode.prototype.chunkSize = Infinity
spyOn(TextMateLanguageMode.prototype, "tokenizeInBackground").andCallFake -> @tokenizeNextChunk()
# Without this spy, TextEditor.onDidTokenize callbacks would not be called
# after the buffer's language mode changed, because by the time the editor
# called its new language mode's onDidTokenize method, the language mode
# would already be fully tokenized.
spyOn(TextEditor.prototype, "onDidTokenize").andCallFake (callback) ->
new CompositeDisposable(
@emitter.on("did-tokenize", callback),
@onDidChangeGrammar =>
languageMode = @buffer.getLanguageMode()
if languageMode.tokenizeInBackground?.originalValue
callback()
)
clipboardContent = 'initial clipboard content'
spyOn(clipboard, 'writeText').andCallFake (text) -> clipboardContent = text

View File

@@ -0,0 +1,77 @@
const SyntaxScopeMap = require('../src/syntax-scope-map')
describe('SyntaxScopeMap', () => {
it('can match immediate child selectors', () => {
const map = new SyntaxScopeMap({
'a > b > c': 'x',
'b > c': 'y',
'c': 'z'
})
expect(map.get(['a', 'b', 'c'], [0, 0, 0])).toBe('x')
expect(map.get(['d', 'b', 'c'], [0, 0, 0])).toBe('y')
expect(map.get(['d', 'e', 'c'], [0, 0, 0])).toBe('z')
expect(map.get(['e', 'c'], [0, 0, 0])).toBe('z')
expect(map.get(['c'], [0, 0, 0])).toBe('z')
expect(map.get(['d'], [0, 0, 0])).toBe(undefined)
})
it('can match :nth-child pseudo-selectors on leaves', () => {
const map = new SyntaxScopeMap({
'a > b': 'w',
'a > b:nth-child(1)': 'x',
'b': 'y',
'b:nth-child(2)': 'z'
})
expect(map.get(['a', 'b'], [0, 0])).toBe('w')
expect(map.get(['a', 'b'], [0, 1])).toBe('x')
expect(map.get(['a', 'b'], [0, 2])).toBe('w')
expect(map.get(['b'], [0])).toBe('y')
expect(map.get(['b'], [1])).toBe('y')
expect(map.get(['b'], [2])).toBe('z')
})
it('can match :nth-child pseudo-selectors on interior nodes', () => {
const map = new SyntaxScopeMap({
'b:nth-child(1) > c': 'w',
'a > b > c': 'x',
'a > b:nth-child(2) > c': 'y'
})
expect(map.get(['b', 'c'], [0, 0])).toBe(undefined)
expect(map.get(['b', 'c'], [1, 0])).toBe('w')
expect(map.get(['a', 'b', 'c'], [1, 0, 0])).toBe('x')
expect(map.get(['a', 'b', 'c'], [1, 2, 0])).toBe('y')
})
it('allows anonymous tokens to be referred to by their string value', () => {
const map = new SyntaxScopeMap({
'"b"': 'w',
'a > "b"': 'x',
'a > "b":nth-child(1)': 'y'
})
expect(map.get(['b'], [0], true)).toBe(undefined)
expect(map.get(['b'], [0], false)).toBe('w')
expect(map.get(['a', 'b'], [0, 0], false)).toBe('x')
expect(map.get(['a', 'b'], [0, 1], false)).toBe('y')
})
it('supports the wildcard selector', () => {
const map = new SyntaxScopeMap({
'*': 'w',
'a > *': 'x',
'a > *:nth-child(1)': 'y',
'a > *:nth-child(1) > b': 'z'
})
expect(map.get(['b'], [0])).toBe('w')
expect(map.get(['c'], [0])).toBe('w')
expect(map.get(['a', 'b'], [0, 0])).toBe('x')
expect(map.get(['a', 'b'], [0, 1])).toBe('y')
expect(map.get(['a', 'c'], [0, 1])).toBe('y')
expect(map.get(['a', 'c', 'b'], [0, 1, 1])).toBe('z')
expect(map.get(['a', 'c', 'b'], [0, 2, 1])).toBe('w')
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -70,20 +70,47 @@ describe('TextEditorElement', () => {
expect(element.getModel().isLineNumberGutterVisible()).toBe(false)
})
it("honors the 'readonly' attribute", async function() {
jasmineContent.innerHTML = "<atom-text-editor readonly>"
const element = jasmineContent.firstChild
expect(element.getComponent().isInputEnabled()).toBe(false)
element.removeAttribute('readonly')
expect(element.getComponent().isInputEnabled()).toBe(true)
element.setAttribute('readonly', true)
expect(element.getComponent().isInputEnabled()).toBe(false)
})
it('honors the text content', () => {
jasmineContent.innerHTML = '<atom-text-editor>testing</atom-text-editor>'
const element = jasmineContent.firstChild
expect(element.getModel().getText()).toBe('testing')
})
describe('tabIndex', () => {
it('uses a default value of -1', () => {
jasmineContent.innerHTML = '<atom-text-editor />'
const element = jasmineContent.firstChild
expect(element.tabIndex).toBe(-1)
expect(element.querySelector('input').tabIndex).toBe(-1)
})
it('uses the custom value when given', () => {
jasmineContent.innerHTML = '<atom-text-editor tabIndex="42" />'
const element = jasmineContent.firstChild
expect(element.tabIndex).toBe(-1)
expect(element.querySelector('input').tabIndex).toBe(42)
})
})
describe('when the model is assigned', () =>
it("adds the 'mini' attribute if .isMini() returns true on the model", function (done) {
it("adds the 'mini' attribute if .isMini() returns true on the model", async () => {
const element = buildTextEditorElement()
element.getModel().update({mini: true})
atom.views.getNextUpdatePromise().then(() => {
expect(element.hasAttribute('mini')).toBe(true)
done()
})
await atom.views.getNextUpdatePromise()
expect(element.hasAttribute('mini')).toBe(true)
})
)
@@ -268,12 +295,11 @@ describe('TextEditorElement', () => {
})
)
describe('::setUpdatedSynchronously', () =>
describe('::setUpdatedSynchronously', () => {
it('controls whether the text editor is updated synchronously', () => {
spyOn(window, 'requestAnimationFrame').andCallFake(fn => fn())
const element = buildTextEditorElement()
jasmine.attachToDOM(element)
expect(element.isUpdatedSynchronously()).toBe(false)
@@ -288,7 +314,7 @@ describe('TextEditorElement', () => {
expect(window.requestAnimationFrame).not.toHaveBeenCalled()
expect(element.textContent).toContain('goodbye')
})
)
})
describe('::getDefaultCharacterWidth', () => {
it('returns 0 before the element is attached', () => {

View File

@@ -1,10 +1,8 @@
/** @babel */
import TextEditorRegistry from '../src/text-editor-registry'
import TextEditor from '../src/text-editor'
import TextBuffer from 'text-buffer'
import {it, fit, ffit, fffit} from './async-spec-helpers'
import dedent from 'dedent'
const TextEditorRegistry = require('../src/text-editor-registry')
const TextEditor = require('../src/text-editor')
const TextBuffer = require('text-buffer')
const {it, fit, ffit, fffit} = require('./async-spec-helpers')
const dedent = require('dedent')
describe('TextEditorRegistry', function () {
let registry, editor, initialPackageActivation
@@ -20,6 +18,7 @@ describe('TextEditorRegistry', function () {
})
editor = new TextEditor({autoHeight: false})
expect(atom.grammars.assignLanguageMode(editor, 'text.plain.null-grammar')).toBe(true)
})
afterEach(function () {
@@ -71,128 +70,17 @@ describe('TextEditorRegistry', function () {
atom.config.set('editor.tabLength', 8, {scope: '.source.js'})
const editor = registry.build({buffer: new TextBuffer({filePath: 'test.js'})})
expect(editor.getGrammar().name).toBe("JavaScript")
expect(editor.getTabLength()).toBe(8)
})
})
describe('.maintainGrammar', function () {
it('assigns a grammar to the editor based on its path', async function () {
await atom.packages.activatePackage('language-javascript')
await atom.packages.activatePackage('language-c')
editor.getBuffer().setPath('test.js')
registry.maintainGrammar(editor)
expect(editor.getGrammar().name).toBe('JavaScript')
editor.getBuffer().setPath('test.c')
expect(editor.getGrammar().name).toBe('C')
})
it('updates the editor\'s grammar when a more appropriate grammar is added for its path', async function () {
expect(editor.getGrammar().name).toBe('Null Grammar')
editor.getBuffer().setPath('test.js')
registry.maintainGrammar(editor)
await atom.packages.activatePackage('language-javascript')
expect(editor.getGrammar().name).toBe('JavaScript')
})
it('returns a disposable that can be used to stop the registry from updating the editor', async function () {
await atom.packages.activatePackage('language-javascript')
const previousSubscriptionCount = getSubscriptionCount(editor)
const disposable = registry.maintainGrammar(editor)
expect(getSubscriptionCount(editor)).toBeGreaterThan(previousSubscriptionCount)
expect(registry.editorsWithMaintainedGrammar.size).toBe(1)
editor.getBuffer().setPath('test.js')
expect(editor.getGrammar().name).toBe('JavaScript')
editor.getBuffer().setPath('test.txt')
expect(editor.getGrammar().name).toBe('Null Grammar')
disposable.dispose()
expect(getSubscriptionCount(editor)).toBe(previousSubscriptionCount)
expect(registry.editorsWithMaintainedGrammar.size).toBe(0)
editor.getBuffer().setPath('test.js')
expect(editor.getGrammar().name).toBe('Null Grammar')
expect(retainedEditorCount(registry)).toBe(0)
})
describe('when called twice with a given editor', function () {
it('does nothing the second time', async function () {
await atom.packages.activatePackage('language-javascript')
const disposable1 = registry.maintainGrammar(editor)
const disposable2 = registry.maintainGrammar(editor)
editor.getBuffer().setPath('test.js')
expect(editor.getGrammar().name).toBe('JavaScript')
disposable2.dispose()
editor.getBuffer().setPath('test.txt')
expect(editor.getGrammar().name).toBe('Null Grammar')
disposable1.dispose()
editor.getBuffer().setPath('test.js')
expect(editor.getGrammar().name).toBe('Null Grammar')
})
})
})
describe('.setGrammarOverride', function () {
it('sets the editor\'s grammar and does not update it based on other criteria', async function () {
await atom.packages.activatePackage('language-c')
await atom.packages.activatePackage('language-javascript')
registry.maintainGrammar(editor)
editor.getBuffer().setPath('file-1.js')
expect(editor.getGrammar().name).toBe('JavaScript')
registry.setGrammarOverride(editor, 'source.c')
expect(editor.getGrammar().name).toBe('C')
editor.getBuffer().setPath('file-3.rb')
await atom.packages.activatePackage('language-ruby')
expect(editor.getGrammar().name).toBe('C')
editor.getBuffer().setPath('file-1.js')
expect(editor.getGrammar().name).toBe('C')
})
})
describe('.clearGrammarOverride', function () {
it('resumes setting the grammar based on its path and content', async function () {
await atom.packages.activatePackage('language-c')
await atom.packages.activatePackage('language-javascript')
registry.maintainGrammar(editor)
editor.getBuffer().setPath('file-1.js')
expect(editor.getGrammar().name).toBe('JavaScript')
registry.setGrammarOverride(editor, 'source.c')
expect(registry.getGrammarOverride(editor)).toBe('source.c')
expect(editor.getGrammar().name).toBe('C')
registry.clearGrammarOverride(editor)
expect(editor.getGrammar().name).toBe('JavaScript')
editor.getBuffer().setPath('file-3.rb')
await atom.packages.activatePackage('language-ruby')
expect(editor.getGrammar().name).toBe('Ruby')
expect(registry.getGrammarOverride(editor)).toBe(undefined)
})
})
describe('.maintainConfig(editor)', function () {
it('does not update the editor when config settings change for unrelated scope selectors', async function () {
await atom.packages.activatePackage('language-javascript')
const editor2 = new TextEditor()
editor2.setGrammar(atom.grammars.selectGrammar('test.js'))
atom.grammars.assignLanguageMode(editor2, 'source.js')
registry.maintainConfig(editor)
registry.maintainConfig(editor2)
@@ -254,18 +142,57 @@ describe('TextEditorRegistry', function () {
atom.config.set('core.fileEncoding', 'utf16le', {scopeSelector: '.source.js'})
expect(editor.getEncoding()).toBe('utf8')
editor.setGrammar(atom.grammars.grammarForScopeName('source.js'))
atom.grammars.assignLanguageMode(editor, 'source.js')
await initialPackageActivation
expect(editor.getEncoding()).toBe('utf16le')
atom.config.set('core.fileEncoding', 'utf16be', {scopeSelector: '.source.js'})
expect(editor.getEncoding()).toBe('utf16be')
editor.setGrammar(atom.grammars.selectGrammar('test.txt'))
atom.grammars.assignLanguageMode(editor, 'text.plain.null-grammar')
await initialPackageActivation
expect(editor.getEncoding()).toBe('utf8')
})
it('preserves editor settings that haven\'t changed between previous and current language modes', async function () {
await atom.packages.activatePackage('language-javascript')
registry.maintainConfig(editor)
await initialPackageActivation
expect(editor.getEncoding()).toBe('utf8')
editor.setEncoding('utf16le')
expect(editor.getEncoding()).toBe('utf16le')
expect(editor.isSoftWrapped()).toBe(false)
editor.setSoftWrapped(true)
expect(editor.isSoftWrapped()).toBe(true)
atom.grammars.assignLanguageMode(editor, 'source.js')
await initialPackageActivation
expect(editor.getEncoding()).toBe('utf16le')
expect(editor.isSoftWrapped()).toBe(true)
})
it('updates editor settings that have changed between previous and current language modes', async function () {
await atom.packages.activatePackage('language-javascript')
registry.maintainConfig(editor)
await initialPackageActivation
expect(editor.getEncoding()).toBe('utf8')
atom.config.set('core.fileEncoding', 'utf16be', {scopeSelector: '.text.plain.null-grammar'})
atom.config.set('core.fileEncoding', 'utf16le', {scopeSelector: '.source.js'})
expect(editor.getEncoding()).toBe('utf16be')
editor.setEncoding('utf8')
expect(editor.getEncoding()).toBe('utf8')
atom.grammars.assignLanguageMode(editor, 'source.js')
await initialPackageActivation
expect(editor.getEncoding()).toBe('utf16le')
})
it('returns a disposable that can be used to stop the registry from updating the editor\'s config', async function () {
await atom.packages.activatePackage('language-javascript')
@@ -331,7 +258,7 @@ describe('TextEditorRegistry', function () {
describe('when the "tabType" config setting is "auto"', function () {
it('enables or disables soft tabs based on the editor\'s content', async function () {
await atom.packages.activatePackage('language-javascript')
editor.setGrammar(atom.grammars.selectGrammar('test.js'))
atom.grammars.assignLanguageMode(editor, 'source.js')
atom.config.set('editor.tabType', 'auto')
registry.maintainConfig(editor)
@@ -342,7 +269,7 @@ describe('TextEditorRegistry', function () {
hello;
}
`)
editor.tokenizedBuffer.retokenizeLines()
editor.getBuffer().getLanguageMode().retokenizeLines()
expect(editor.getSoftTabs()).toBe(true)
editor.setText(dedent`
@@ -350,7 +277,7 @@ describe('TextEditorRegistry', function () {
hello;
}
`)
editor.tokenizedBuffer.retokenizeLines()
editor.getBuffer().getLanguageMode().retokenizeLines()
expect(editor.getSoftTabs()).toBe(false)
editor.setText(dedent`
@@ -361,7 +288,7 @@ describe('TextEditorRegistry', function () {
${'\t'}hello;
}
` + editor.getText())
editor.tokenizedBuffer.retokenizeLines()
editor.getBuffer().getLanguageMode().retokenizeLines()
expect(editor.getSoftTabs()).toBe(false)
editor.setText(dedent`
@@ -374,7 +301,7 @@ describe('TextEditorRegistry', function () {
}
`)
editor.tokenizedBuffer.retokenizeLines()
editor.getBuffer().getLanguageMode().retokenizeLines()
expect(editor.getSoftTabs()).toBe(false)
editor.setText(dedent`
@@ -386,7 +313,7 @@ describe('TextEditorRegistry', function () {
hello;
}
`)
editor.tokenizedBuffer.retokenizeLines()
editor.getBuffer().getLanguageMode().retokenizeLines()
expect(editor.getSoftTabs()).toBe(true)
})
})
@@ -624,19 +551,6 @@ describe('TextEditorRegistry', function () {
expect(editor.getUndoGroupingInterval()).toBe(300)
})
it('sets the non-word characters based on the config', async function () {
editor.update({nonWordCharacters: '()'})
expect(editor.getNonWordCharacters()).toBe('()')
atom.config.set('editor.nonWordCharacters', '(){}')
registry.maintainConfig(editor)
await initialPackageActivation
expect(editor.getNonWordCharacters()).toBe('(){}')
atom.config.set('editor.nonWordCharacters', '(){}[]')
expect(editor.getNonWordCharacters()).toBe('(){}[]')
})
it('sets the scroll sensitivity based on the config', async function () {
editor.update({scrollSensitivity: 50})
expect(editor.getScrollSensitivity()).toBe(50)
@@ -650,21 +564,6 @@ describe('TextEditorRegistry', function () {
expect(editor.getScrollSensitivity()).toBe(70)
})
it('gives the editor a scoped-settings delegate based on the config', async function () {
atom.config.set('editor.nonWordCharacters', '()')
atom.config.set('editor.nonWordCharacters', '(){}', {scopeSelector: '.a.b .c.d'})
atom.config.set('editor.nonWordCharacters', '(){}[]', {scopeSelector: '.e.f *'})
registry.maintainConfig(editor)
await initialPackageActivation
let delegate = editor.getScopedSettingsDelegate()
expect(delegate.getNonWordCharacters(['a.b', 'c.d'])).toBe('(){}')
expect(delegate.getNonWordCharacters(['e.f', 'g.h'])).toBe('(){}[]')
expect(delegate.getNonWordCharacters(['i.j'])).toBe('()')
})
describe('when called twice with a given editor', function () {
it('does nothing the second time', async function () {
editor.update({scrollSensitivity: 50})
@@ -686,46 +585,6 @@ describe('TextEditorRegistry', function () {
})
})
})
describe('serialization', function () {
it('persists editors\' grammar overrides', async function () {
const editor2 = new TextEditor()
await atom.packages.activatePackage('language-c')
await atom.packages.activatePackage('language-html')
await atom.packages.activatePackage('language-javascript')
registry.maintainGrammar(editor)
registry.maintainGrammar(editor2)
registry.setGrammarOverride(editor, 'source.c')
registry.setGrammarOverride(editor2, 'source.js')
await atom.packages.deactivatePackage('language-javascript')
const editorCopy = TextEditor.deserialize(editor.serialize(), atom)
const editor2Copy = TextEditor.deserialize(editor2.serialize(), atom)
const registryCopy = new TextEditorRegistry({
assert: atom.assert,
config: atom.config,
grammarRegistry: atom.grammars,
packageManager: {deferredActivationHooks: null}
})
registryCopy.deserialize(JSON.parse(JSON.stringify(registry.serialize())))
expect(editorCopy.getGrammar().name).toBe('Null Grammar')
expect(editor2Copy.getGrammar().name).toBe('Null Grammar')
registryCopy.maintainGrammar(editorCopy)
registryCopy.maintainGrammar(editor2Copy)
expect(editorCopy.getGrammar().name).toBe('C')
expect(editor2Copy.getGrammar().name).toBe('Null Grammar')
await atom.packages.activatePackage('language-javascript')
expect(editorCopy.getGrammar().name).toBe('C')
expect(editor2Copy.getGrammar().name).toBe('JavaScript')
})
})
})
function getSubscriptionCount (editor) {

View File

@@ -7,6 +7,7 @@ const dedent = require('dedent')
const clipboard = require('../src/safe-clipboard')
const TextEditor = require('../src/text-editor')
const TextBuffer = require('text-buffer')
const TextMateLanguageMode = require('../src/text-mate-language-mode')
describe('TextEditor', () => {
let buffer, editor, lineLengths
@@ -19,6 +20,17 @@ describe('TextEditor', () => {
await atom.packages.activatePackage('language-javascript')
})
it('generates unique ids for each editor', async () => {
// Deserialized editors are initialized with the serialized id. We can
// initialize an editor with what we expect to be the next id:
const deserialized = new TextEditor({id: editor.id+1})
expect(deserialized.id).toEqual(editor.id+1)
// The id generator should skip the id used up by the deserialized one:
const fresh = new TextEditor()
expect(fresh.id).toNotEqual(deserialized.id)
})
describe('when the editor is deserialized', () => {
it('restores selections and folds based on markers in the buffer', async () => {
editor.setSelectedBufferRange([[1, 2], [3, 4]])
@@ -85,22 +97,6 @@ describe('TextEditor', () => {
})
})
describe('when the editor is constructed with the largeFileMode option set to true', () => {
it("loads the editor but doesn't tokenize", async () => {
editor = await atom.workspace.openTextFile('sample.js', {largeFileMode: true})
buffer = editor.getBuffer()
expect(editor.lineTextForScreenRow(0)).toBe(buffer.lineForRow(0))
expect(editor.tokensForScreenRow(0).length).toBe(1)
expect(editor.tokensForScreenRow(1).length).toBe(2) // soft tab
expect(editor.lineTextForScreenRow(12)).toBe(buffer.lineForRow(12))
expect(editor.getCursorScreenPosition()).toEqual([0, 0])
editor.insertText('hey"')
expect(editor.tokensForScreenRow(0).length).toBe(1)
expect(editor.tokensForScreenRow(1).length).toBe(2)
})
})
describe('.copy()', () => {
it('returns a different editor with the same initial state', () => {
expect(editor.getAutoHeight()).toBeFalsy()
@@ -115,12 +111,12 @@ describe('TextEditor', () => {
editor.update({showCursorOnSelection: false})
editor.setSelectedBufferRange([[1, 2], [3, 4]])
editor.addSelectionForBufferRange([[5, 6], [7, 8]], {reversed: true})
editor.foldBufferRow(4)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
editor.setScrollTopRow(3)
expect(editor.getScrollTopRow()).toBe(3)
editor.setScrollLeftColumn(4)
expect(editor.getScrollLeftColumn()).toBe(4)
editor.foldBufferRow(4)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
const editor2 = editor.copy()
const element2 = editor2.getElement()
@@ -1356,7 +1352,7 @@ describe('TextEditor', () => {
})
it('will limit paragraph range to comments', () => {
editor.setGrammar(atom.grammars.grammarForScopeName('source.js'))
atom.grammars.assignLanguageMode(editor.getBuffer(), 'source.js')
editor.setText(dedent`
var quicksort = function () {
/* Single line comment block */
@@ -2081,14 +2077,13 @@ describe('TextEditor', () => {
expect(scopeDescriptors[0].getScopesArray()).toEqual(['source.js'])
expect(scopeDescriptors[1].getScopesArray()).toEqual(['source.js', 'string.quoted.single.js'])
editor.setScopedSettingsDelegate({
getNonWordCharacters (scopes) {
const result = '/\()"\':,.;<>~!@#$%^&*|+=[]{}`?'
if (scopes.some(scope => scope.startsWith('string'))) {
return result
} else {
return result + '-'
}
spyOn(editor.getBuffer().getLanguageMode(), 'getNonWordCharacters').andCallFake(function (position) {
const result = '/\()"\':,.;<>~!@#$%^&*|+=[]{}`?'
const scopes = this.scopeDescriptorForPosition(position).getScopesArray()
if (scopes.some(scope => scope.startsWith('string'))) {
return result
} else {
return result + '-'
}
})
@@ -2381,6 +2376,19 @@ describe('TextEditor', () => {
])
})
})
it('does not create a new selection if it would be fully contained within another selection', () => {
editor.setText('abc\ndef\nghi\njkl\nmno')
editor.setCursorBufferPosition([0, 1])
let addedSelectionCount = 0
editor.onDidAddSelection(() => { addedSelectionCount++ })
editor.addSelectionBelow()
editor.addSelectionBelow()
editor.addSelectionBelow()
expect(addedSelectionCount).toBe(3)
})
})
describe('.addSelectionAbove()', () => {
@@ -2503,6 +2511,19 @@ describe('TextEditor', () => {
])
})
})
it('does not create a new selection if it would be fully contained within another selection', () => {
editor.setText('abc\ndef\nghi\njkl\nmno')
editor.setCursorBufferPosition([4, 1])
let addedSelectionCount = 0
editor.onDidAddSelection(() => { addedSelectionCount++ })
editor.addSelectionAbove()
editor.addSelectionAbove()
editor.addSelectionAbove()
expect(addedSelectionCount).toBe(3)
})
})
describe('.splitSelectionsIntoLines()', () => {
@@ -3512,13 +3533,16 @@ describe('TextEditor', () => {
})
describe("when the undo option is set to 'skip'", () => {
beforeEach(() => editor.setSelectedBufferRange([[1, 2], [1, 2]]))
it('does not undo the skipped operation', () => {
let range = editor.insertText('x')
range = editor.insertText('y', {undo: 'skip'})
it('groups the change with the previous change for purposes of undo and redo', () => {
editor.setSelectedBufferRanges([
[[0, 0], [0, 0]],
[[1, 0], [1, 0]]
])
editor.insertText('x')
editor.insertText('y', {undo: 'skip'})
editor.undo()
expect(buffer.lineForRow(1)).toBe(' yvar sort = function(items) {')
expect(buffer.lineForRow(0)).toBe('var quicksort = function () {')
expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {')
})
})
})
@@ -3694,7 +3718,7 @@ describe('TextEditor', () => {
describe('when a newline is appended with a trailing closing tag behind the cursor (e.g. by pressing enter in the middel of a line)', () => {
it('indents the new line to the correct level when editor.autoIndent is true and using a curly-bracket language', () => {
editor.update({autoIndent: true})
editor.setGrammar(atom.grammars.selectGrammar('file.js'))
atom.grammars.assignLanguageMode(editor, 'source.js')
editor.setText('var test = () => {\n return true;};')
editor.setCursorBufferPosition([1, 14])
editor.insertNewline()
@@ -3703,7 +3727,7 @@ describe('TextEditor', () => {
})
it('indents the new line to the current level when editor.autoIndent is true and no increaseIndentPattern is specified', () => {
editor.setGrammar(atom.grammars.selectGrammar('file'))
atom.grammars.assignLanguageMode(editor, null)
editor.update({autoIndent: true})
editor.setText(' if true')
editor.setCursorBufferPosition([0, 8])
@@ -3716,7 +3740,7 @@ describe('TextEditor', () => {
it('indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language', async () => {
await atom.packages.activatePackage('language-coffee-script')
editor.update({autoIndent: true})
editor.setGrammar(atom.grammars.selectGrammar('file.coffee'))
atom.grammars.assignLanguageMode(editor, 'source.coffee')
editor.setText('if true\n return trueelse\n return false')
editor.setCursorBufferPosition([1, 13])
editor.insertNewline()
@@ -3730,7 +3754,7 @@ describe('TextEditor', () => {
it('indents the new line to the correct level when editor.autoIndent is true', async () => {
await atom.packages.activatePackage('language-go')
editor.update({autoIndent: true})
editor.setGrammar(atom.grammars.selectGrammar('file.go'))
atom.grammars.assignLanguageMode(editor, 'source.go')
editor.setText('fmt.Printf("some%s",\n "thing")')
editor.setCursorBufferPosition([1, 10])
editor.insertNewline()
@@ -5403,6 +5427,34 @@ describe('TextEditor', () => {
expect(buffer.getLineCount()).toBe(count - 2)
})
it("restores cursor position for multiple cursors", () => {
const line = '0123456789'.repeat(8)
editor.setText((line + '\n').repeat(5))
editor.setCursorScreenPosition([0, 5])
editor.addCursorAtScreenPosition([2, 8])
editor.deleteLine()
const cursors = editor.getCursors()
expect(cursors.length).toBe(2)
expect(cursors[0].getScreenPosition()).toEqual([0, 5])
expect(cursors[1].getScreenPosition()).toEqual([1, 8])
})
it("restores cursor position for multiple selections", () => {
const line = '0123456789'.repeat(8)
editor.setText((line + '\n').repeat(5))
editor.setSelectedBufferRanges([
[[0, 5], [0, 8]],
[[2, 4], [2, 15]]
])
editor.deleteLine()
const cursors = editor.getCursors()
expect(cursors.length).toBe(2)
expect(cursors[0].getScreenPosition()).toEqual([0, 5])
expect(cursors[1].getScreenPosition()).toEqual([1, 4])
})
it('deletes a line only once when multiple selections are on the same line', () => {
const line1 = buffer.lineForRow(1)
const count = buffer.getLineCount()
@@ -5622,21 +5674,30 @@ describe('TextEditor', () => {
})
})
describe('when a better-matched grammar is added to syntax', () => {
it('switches to the better-matched grammar and re-tokenizes the buffer', async () => {
editor.destroy()
describe('when the buffer\'s language mode changes', () => {
it('notifies onDidTokenize observers when retokenization is finished', async () => {
// Exercise the full `tokenizeInBackground` code path, which bails out early if
// `.setVisible` has not been called with `true`.
jasmine.unspy(TextMateLanguageMode.prototype, 'tokenizeInBackground')
jasmine.attachToDOM(editor.getElement())
const jsGrammar = atom.grammars.selectGrammar('a.js')
atom.grammars.removeGrammar(jsGrammar)
const events = []
editor.onDidTokenize(event => events.push(event))
editor = await atom.workspace.open('sample.js', {autoIndent: false})
await atom.packages.activatePackage('language-c')
expect(atom.grammars.assignLanguageMode(editor.getBuffer(), 'source.c')).toBe(true)
advanceClock(1)
expect(events.length).toBe(1)
})
expect(editor.getGrammar()).toBe(atom.grammars.nullGrammar)
expect(editor.tokensForScreenRow(0).length).toBe(1)
it('notifies onDidChangeGrammar observers', async () => {
const events = []
editor.onDidChangeGrammar(grammar => events.push(grammar))
atom.grammars.addGrammar(jsGrammar)
expect(editor.getGrammar()).toBe(jsGrammar)
expect(editor.tokensForScreenRow(0).length).toBeGreaterThan(1)
await atom.packages.activatePackage('language-c')
expect(atom.grammars.assignLanguageMode(editor.getBuffer(), 'source.c')).toBe(true)
expect(events.length).toBe(1)
expect(events[0].name).toBe('C')
})
})
@@ -6630,17 +6691,6 @@ describe('TextEditor', () => {
})
})
describe('when the editor is constructed with the grammar option set', () => {
beforeEach(async () => {
await atom.packages.activatePackage('language-coffee-script')
})
it('sets the grammar', () => {
editor = new TextEditor({grammar: atom.grammars.grammarForScopeName('source.coffee')})
expect(editor.getGrammar().name).toBe('CoffeeScript')
})
})
describe('softWrapAtPreferredLineLength', () => {
it('soft wraps the editor at the preferred line length unless the editor is narrower or the editor is mini', () => {
editor.update({
@@ -6701,6 +6751,7 @@ describe('TextEditor', () => {
beforeEach(async () => {
editor = await atom.workspace.open('sample.js')
jasmine.unspy(editor, 'shouldPromptToSave')
spyOn(atom.stateStore, 'isConnected').andReturn(true)
})
it('returns true when buffer has unsaved changes', () => {
@@ -6828,7 +6879,7 @@ describe('TextEditor', () => {
})
it('does nothing for empty lines and null grammar', () => {
editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar'))
atom.grammars.assignLanguageMode(editor, null)
editor.setCursorBufferPosition([10, 0])
editor.toggleLineCommentsInSelection()
expect(editor.lineTextForBufferRow(10)).toBe('')
@@ -7028,19 +7079,15 @@ describe('TextEditor', () => {
})
describe('.unfoldAll()', () => {
it('unfolds every folded line and autoscrolls', async () => {
it('unfolds every folded line', async () => {
editor = await atom.workspace.open('sample.js', {autoIndent: false})
const autoscrollEvents = []
editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event))
const initialScreenLineCount = editor.getScreenLineCount()
editor.foldBufferRow(0)
editor.foldBufferRow(1)
expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount)
expect(autoscrollEvents.length).toBe(1)
editor.unfoldAll()
expect(editor.getScreenLineCount()).toBe(initialScreenLineCount)
expect(autoscrollEvents.length).toBe(2)
})
it('unfolds every folded line with comments', async () => {
@@ -7058,11 +7105,8 @@ describe('TextEditor', () => {
describe('.foldAll()', () => {
it('folds every foldable line', async () => {
editor = await atom.workspace.open('sample.js', {autoIndent: false})
const autoscrollEvents = []
editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event))
editor.foldAll()
expect(autoscrollEvents.length).toBe(1)
const [fold1, fold2, fold3] = editor.unfoldAll()
expect([fold1.start.row, fold1.end.row]).toEqual([0, 12])
expect([fold2.start.row, fold2.end.row]).toEqual([1, 9])
@@ -7093,11 +7137,7 @@ describe('TextEditor', () => {
describe('when bufferRow can be folded', () => {
it('creates a fold based on the syntactic region starting at the given row', () => {
const autoscrollEvents = []
editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event))
editor.foldBufferRow(1)
expect(autoscrollEvents.length).toBe(1)
const [fold] = editor.unfoldAll()
expect([fold.start.row, fold.end.row]).toEqual([1, 9])
})
@@ -7144,14 +7184,10 @@ describe('TextEditor', () => {
describe('.foldCurrentRow()', () => {
it('creates a fold at the location of the last cursor', async () => {
editor = await atom.workspace.open()
editor.setText('\nif (x) {\n y()\n}')
editor.setCursorBufferPosition([1, 0])
expect(editor.getScreenLineCount()).toBe(4)
const autoscrollEvents = []
editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event))
editor.foldCurrentRow()
expect(autoscrollEvents.length).toBe(1)
expect(editor.getScreenLineCount()).toBe(3)
})
@@ -7168,26 +7204,21 @@ describe('TextEditor', () => {
describe('.foldAllAtIndentLevel(indentLevel)', () => {
it('folds blocks of text at the given indentation level', async () => {
editor = await atom.workspace.open('sample.js', {autoIndent: false})
const autoscrollEvents = []
editor.onDidRequestAutoscroll(event => autoscrollEvents.push(event))
editor.foldAllAtIndentLevel(0)
expect(editor.lineTextForScreenRow(0)).toBe(`var quicksort = function () {${editor.displayLayer.foldCharacter}`)
expect(editor.getLastScreenRow()).toBe(0)
expect(autoscrollEvents.length).toBe(1)
editor.foldAllAtIndentLevel(1)
expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {')
expect(editor.lineTextForScreenRow(1)).toBe(` var sort = function(items) {${editor.displayLayer.foldCharacter}`)
expect(editor.getLastScreenRow()).toBe(4)
expect(autoscrollEvents.length).toBe(2)
editor.foldAllAtIndentLevel(2)
expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {')
expect(editor.lineTextForScreenRow(1)).toBe(' var sort = function(items) {')
expect(editor.lineTextForScreenRow(2)).toBe(' if (items.length <= 1) return items;')
expect(editor.getLastScreenRow()).toBe(9)
expect(autoscrollEvents.length).toBe(3)
})
it('folds every foldable range at a given indentLevel', async () => {

File diff suppressed because it is too large Load Diff

View File

@@ -424,12 +424,12 @@ h2 {
waitsForPromise(() => atom.themes.activateThemes())
})
it('uses the default dark UI and syntax themes and logs a warning', function () {
it('uses the default one-dark UI and syntax themes and logs a warning', function () {
const activeThemeNames = atom.themes.getActiveThemeNames()
expect(console.warn.callCount).toBe(2)
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-dark-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')
expect(activeThemeNames).toContain('one-dark-ui')
expect(activeThemeNames).toContain('one-dark-syntax')
})
})
@@ -459,8 +459,8 @@ h2 {
it('uses the default dark UI and syntax themes', function () {
const activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-dark-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')
expect(activeThemeNames).toContain('one-dark-ui')
expect(activeThemeNames).toContain('one-dark-syntax')
})
})
@@ -471,10 +471,10 @@ h2 {
waitsForPromise(() => atom.themes.activateThemes())
})
it('uses the default dark UI theme', function () {
it('uses the default one-dark UI theme', function () {
const activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-dark-ui')
expect(activeThemeNames).toContain('one-dark-ui')
expect(activeThemeNames).toContain('atom-light-syntax')
})
})
@@ -486,11 +486,11 @@ h2 {
waitsForPromise(() => atom.themes.activateThemes())
})
it('uses the default dark syntax theme', function () {
it('uses the default one-dark syntax theme', function () {
const activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-light-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')
expect(activeThemeNames).toContain('one-dark-syntax')
})
})
})

View File

@@ -1,43 +0,0 @@
const TextBuffer = require('text-buffer')
const TokenizedBuffer = require('../src/tokenized-buffer')
describe('TokenIterator', () =>
it('correctly terminates scopes at the beginning of the line (regression)', () => {
const grammar = atom.grammars.createGrammar('test', {
'scopeName': 'text.broken',
'name': 'Broken grammar',
'patterns': [
{
'begin': 'start',
'end': '(?=end)',
'name': 'blue.broken'
},
{
'match': '.',
'name': 'yellow.broken'
}
]
})
const buffer = new TextBuffer({text: `\
start x
end x
x\
`})
const tokenizedBuffer = new TokenizedBuffer({
buffer,
config: atom.config,
grammarRegistry: atom.grammars,
packageManager: atom.packages,
assert: atom.assert
})
tokenizedBuffer.setGrammar(grammar)
const tokenIterator = tokenizedBuffer.tokenizedLines[1].getTokenIterator()
tokenIterator.next()
expect(tokenIterator.getBufferStart()).toBe(0)
expect(tokenIterator.getScopeEnds()).toEqual([])
expect(tokenIterator.getScopeStarts()).toEqual(['text.broken', 'yellow.broken'])
})
)

View File

@@ -1,110 +0,0 @@
/** @babel */
import TokenizedBufferIterator from '../src/tokenized-buffer-iterator'
import {Point} from 'text-buffer'
describe('TokenizedBufferIterator', () => {
describe('seek(position)', function () {
it('seeks to the leftmost tag boundary greater than or equal to the given position and returns the containing tags', function () {
const tokenizedBuffer = {
tokenizedLineForRow (row) {
if (row === 0) {
return {
tags: [-1, -2, -3, -4, -5, 3, -3, -4, -6, -5, 4, -6, -3, -4],
text: 'foo bar',
openScopes: []
}
} else {
return null
}
}
}
const iterator = new TokenizedBufferIterator(tokenizedBuffer)
expect(iterator.seek(Point(0, 0))).toEqual([])
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseScopeIds()).toEqual([])
expect(iterator.getOpenScopeIds()).toEqual([257])
iterator.moveToSuccessor()
expect(iterator.getCloseScopeIds()).toEqual([257])
expect(iterator.getOpenScopeIds()).toEqual([259])
expect(iterator.seek(Point(0, 1))).toEqual([261])
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.getCloseScopeIds()).toEqual([])
expect(iterator.getOpenScopeIds()).toEqual([259])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.getCloseScopeIds()).toEqual([259, 261])
expect(iterator.getOpenScopeIds()).toEqual([261])
expect(iterator.seek(Point(0, 3))).toEqual([261])
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.getCloseScopeIds()).toEqual([])
expect(iterator.getOpenScopeIds()).toEqual([259])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.getCloseScopeIds()).toEqual([259, 261])
expect(iterator.getOpenScopeIds()).toEqual([261])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 7))
expect(iterator.getCloseScopeIds()).toEqual([261])
expect(iterator.getOpenScopeIds()).toEqual([259])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 7))
expect(iterator.getCloseScopeIds()).toEqual([259])
expect(iterator.getOpenScopeIds()).toEqual([])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(1, 0))
expect(iterator.getCloseScopeIds()).toEqual([])
expect(iterator.getOpenScopeIds()).toEqual([])
expect(iterator.seek(Point(0, 5))).toEqual([261])
expect(iterator.getPosition()).toEqual(Point(0, 7))
expect(iterator.getCloseScopeIds()).toEqual([261])
expect(iterator.getOpenScopeIds()).toEqual([259])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 7))
expect(iterator.getCloseScopeIds()).toEqual([259])
expect(iterator.getOpenScopeIds()).toEqual([])
})
})
describe('moveToSuccessor()', function () {
it('reports two boundaries at the same position when tags close, open, then close again without a non-negative integer separating them (regression)', () => {
const tokenizedBuffer = {
tokenizedLineForRow () {
return {
tags: [-1, -2, -1, -2],
text: '',
openScopes: []
}
}
}
const iterator = new TokenizedBufferIterator(tokenizedBuffer)
iterator.seek(Point(0, 0))
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseScopeIds()).toEqual([])
expect(iterator.getOpenScopeIds()).toEqual([257])
iterator.moveToSuccessor()
expect(iterator.getPosition()).toEqual(Point(0, 0))
expect(iterator.getCloseScopeIds()).toEqual([257])
expect(iterator.getOpenScopeIds()).toEqual([257])
iterator.moveToSuccessor()
expect(iterator.getCloseScopeIds()).toEqual([257])
expect(iterator.getOpenScopeIds()).toEqual([])
})
})
})

View File

@@ -1,904 +0,0 @@
const NullGrammar = require('../src/null-grammar')
const TokenizedBuffer = require('../src/tokenized-buffer')
const TextBuffer = require('text-buffer')
const {Point, Range} = TextBuffer
const _ = require('underscore-plus')
const dedent = require('dedent')
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
const {ScopedSettingsDelegate} = require('../src/text-editor-registry')
describe('TokenizedBuffer', () => {
let tokenizedBuffer, buffer
beforeEach(async () => {
// enable async tokenization
TokenizedBuffer.prototype.chunkSize = 5
jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground')
await atom.packages.activatePackage('language-javascript')
})
afterEach(() => {
buffer && buffer.destroy()
tokenizedBuffer && tokenizedBuffer.destroy()
})
function startTokenizing (tokenizedBuffer) {
tokenizedBuffer.setVisible(true)
}
function fullyTokenize (tokenizedBuffer) {
tokenizedBuffer.setVisible(true)
while (tokenizedBuffer.firstInvalidRow() != null) {
advanceClock()
}
}
describe('serialization', () => {
describe('when the underlying buffer has a path', () => {
beforeEach(async () => {
buffer = atom.project.bufferForPathSync('sample.js')
await atom.packages.activatePackage('language-coffee-script')
})
it('deserializes it searching among the buffers in the current project', () => {
const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
})
})
describe('when the underlying buffer has no path', () => {
beforeEach(() => buffer = atom.project.bufferForPathSync(null))
it('deserializes it searching among the buffers in the current project', () => {
const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
})
})
})
describe('tokenizing', () => {
describe('when the buffer is destroyed', () => {
beforeEach(() => {
buffer = atom.project.bufferForPathSync('sample.js')
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
startTokenizing(tokenizedBuffer)
})
it('stops tokenization', () => {
tokenizedBuffer.destroy()
spyOn(tokenizedBuffer, 'tokenizeNextChunk')
advanceClock()
expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled()
})
})
describe('when the buffer contains soft-tabs', () => {
beforeEach(() => {
buffer = atom.project.bufferForPathSync('sample.js')
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
startTokenizing(tokenizedBuffer)
})
afterEach(() => {
tokenizedBuffer.destroy()
buffer.release()
})
describe('on construction', () =>
it('tokenizes lines chunk at a time in the background', () => {
const line0 = tokenizedBuffer.tokenizedLines[0]
expect(line0).toBeUndefined()
const line11 = tokenizedBuffer.tokenizedLines[11]
expect(line11).toBeUndefined()
// tokenize chunk 1
advanceClock()
expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy()
expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy()
expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined()
// tokenize chunk 2
advanceClock()
expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy()
expect(tokenizedBuffer.tokenizedLines[9].ruleStack != null).toBeTruthy()
expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined()
// tokenize last chunk
advanceClock()
expect(tokenizedBuffer.tokenizedLines[10].ruleStack != null).toBeTruthy()
expect(tokenizedBuffer.tokenizedLines[12].ruleStack != null).toBeTruthy()
})
)
describe('when the buffer is partially tokenized', () => {
beforeEach(() => {
// tokenize chunk 1 only
advanceClock()
})
describe('when there is a buffer change inside the tokenized region', () => {
describe('when lines are added', () => {
it('pushes the invalid rows down', () => {
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
buffer.insert([1, 0], '\n\n')
expect(tokenizedBuffer.firstInvalidRow()).toBe(7)
})
})
describe('when lines are removed', () => {
it('pulls the invalid rows up', () => {
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
buffer.delete([[1, 0], [3, 0]])
expect(tokenizedBuffer.firstInvalidRow()).toBe(2)
})
})
describe('when the change invalidates all the lines before the current invalid region', () => {
it('retokenizes the invalidated lines and continues into the valid region', () => {
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
buffer.insert([2, 0], '/*')
expect(tokenizedBuffer.firstInvalidRow()).toBe(3)
advanceClock()
expect(tokenizedBuffer.firstInvalidRow()).toBe(8)
})
})
})
describe('when there is a buffer change surrounding an invalid row', () => {
it('pushes the invalid row to the end of the change', () => {
buffer.setTextInRange([[4, 0], [6, 0]], '\n\n\n')
expect(tokenizedBuffer.firstInvalidRow()).toBe(8)
})
})
describe('when there is a buffer change inside an invalid region', () => {
it('does not attempt to tokenize the lines in the change, and preserves the existing invalid row', () => {
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
buffer.setTextInRange([[6, 0], [7, 0]], '\n\n\n')
expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined()
expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined()
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
})
})
})
describe('when the buffer is fully tokenized', () => {
beforeEach(() => fullyTokenize(tokenizedBuffer))
describe('when there is a buffer change that is smaller than the chunk size', () => {
describe('when lines are updated, but none are added or removed', () => {
it('updates tokens to reflect the change', () => {
buffer.setTextInRange([[0, 0], [2, 0]], 'foo()\n7\n')
expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual({value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js']})
expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: '7', scopes: ['source.js', 'constant.numeric.decimal.js']})
// line 2 is unchanged
expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']})
})
describe('when the change invalidates the tokenization of subsequent lines', () => {
it('schedules the invalidated lines to be tokenized in the background', () => {
buffer.insert([5, 30], '/* */')
buffer.insert([2, 0], '/*')
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js'])
advanceClock()
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
})
})
it('resumes highlighting with the state of the previous line', () => {
buffer.insert([0, 0], '/*')
buffer.insert([5, 0], '*/')
buffer.insert([1, 0], 'var ')
expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
})
})
describe('when lines are both updated and removed', () => {
it('updates tokens to reflect the change', () => {
buffer.setTextInRange([[1, 0], [3, 0]], 'foo()')
// previous line 0 remains
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({value: 'var', scopes: ['source.js', 'storage.type.var.js']})
// previous line 3 should be combined with input to form line 1
expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']})
// lines below deleted regions should be shifted upward
expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'while', scopes: ['source.js', 'keyword.control.js']})
expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']})
expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual({value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']})
})
})
describe('when the change invalidates the tokenization of subsequent lines', () => {
it('schedules the invalidated lines to be tokenized in the background', () => {
buffer.insert([5, 30], '/* */')
buffer.setTextInRange([[2, 0], [3, 0]], '/*')
expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'])
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js'])
advanceClock()
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
})
})
describe('when lines are both updated and inserted', () => {
it('updates tokens to reflect the change', () => {
buffer.setTextInRange([[1, 0], [2, 0]], 'foo()\nbar()\nbaz()\nquux()')
// previous line 0 remains
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ value: 'var', scopes: ['source.js', 'storage.type.var.js']})
// 3 new lines inserted
expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
expect(tokenizedBuffer.tokenizedLines[2].tokens[0]).toEqual({value: 'bar', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
expect(tokenizedBuffer.tokenizedLines[3].tokens[0]).toEqual({value: 'baz', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
// previous line 2 is joined with quux() on line 4
expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual({value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']})
// previous line 3 is pushed down to become line 5
expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']})
})
})
describe('when the change invalidates the tokenization of subsequent lines', () => {
it('schedules the invalidated lines to be tokenized in the background', () => {
buffer.insert([5, 30], '/* */')
buffer.insert([2, 0], '/*\nabcde\nabcder')
expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'])
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js'])
advanceClock() // tokenize invalidated lines in background
expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe(['source.js', 'comment.block.js'])
})
})
})
describe('when there is an insertion that is larger than the chunk size', () =>
it('tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background', () => {
const commentBlock = _.multiplyString('// a comment\n', tokenizedBuffer.chunkSize + 2)
buffer.insert([0, 0], commentBlock)
expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy()
expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy()
expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined()
advanceClock()
expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy()
expect(tokenizedBuffer.tokenizedLines[6].ruleStack != null).toBeTruthy()
})
)
it('does not break out soft tabs across a scope boundary', async () => {
await atom.packages.activatePackage('language-gfm')
tokenizedBuffer.setTabLength(4)
tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md'))
buffer.setText(' <![]()\n ')
fullyTokenize(tokenizedBuffer)
let length = 0
for (let tag of tokenizedBuffer.tokenizedLines[1].tags) {
if (tag > 0) length += tag
}
expect(length).toBe(4)
})
})
})
describe('when the buffer contains hard-tabs', () => {
beforeEach(async () => {
atom.packages.activatePackage('language-coffee-script')
buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee')
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2})
startTokenizing(tokenizedBuffer)
})
afterEach(() => {
tokenizedBuffer.destroy()
buffer.release()
})
describe('when the buffer is fully tokenized', () => {
beforeEach(() => fullyTokenize(tokenizedBuffer))
})
})
describe('when tokenization completes', () => {
it('emits the `tokenized` event', async () => {
const editor = await atom.workspace.open('sample.js')
const tokenizedHandler = jasmine.createSpy('tokenized handler')
editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
fullyTokenize(editor.tokenizedBuffer)
expect(tokenizedHandler.callCount).toBe(1)
})
it("doesn't re-emit the `tokenized` event when it is re-tokenized", async () => {
const editor = await atom.workspace.open('sample.js')
fullyTokenize(editor.tokenizedBuffer)
const tokenizedHandler = jasmine.createSpy('tokenized handler')
editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
editor.getBuffer().insert([0, 0], "'")
fullyTokenize(editor.tokenizedBuffer)
expect(tokenizedHandler).not.toHaveBeenCalled()
})
})
describe('when the grammar is updated because a grammar it includes is activated', async () => {
it('re-emits the `tokenized` event', async () => {
const editor = await atom.workspace.open('coffee.coffee')
const tokenizedHandler = jasmine.createSpy('tokenized handler')
editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
fullyTokenize(editor.tokenizedBuffer)
tokenizedHandler.reset()
await atom.packages.activatePackage('language-coffee-script')
fullyTokenize(editor.tokenizedBuffer)
expect(tokenizedHandler.callCount).toBe(1)
})
it('retokenizes the buffer', async () => {
await atom.packages.activatePackage('language-ruby-on-rails')
await atom.packages.activatePackage('language-ruby')
buffer = atom.project.bufferForPathSync()
buffer.setText("<div class='name'><%= User.find(2).full_name %></div>")
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2})
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({
value: "<div class='name'>",
scopes: ['text.html.ruby']
})
await atom.packages.activatePackage('language-html')
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({
value: '<',
scopes: ['text.html.ruby', 'meta.tag.block.div.html', 'punctuation.definition.tag.begin.html']
})
})
})
describe('when the buffer is configured with the null grammar', () => {
it('does not actually tokenize using the grammar', () => {
spyOn(NullGrammar, 'tokenizeLine').andCallThrough()
buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar')
buffer.setText('a\nb\nc')
tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2})
const tokenizeCallback = jasmine.createSpy('onDidTokenize')
tokenizedBuffer.onDidTokenize(tokenizeCallback)
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined()
expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined()
expect(tokenizeCallback.callCount).toBe(0)
expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled()
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined()
expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined()
expect(tokenizeCallback.callCount).toBe(0)
expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled()
})
})
})
describe('.tokenForPosition(position)', () => {
afterEach(() => {
tokenizedBuffer.destroy()
buffer.release()
})
it('returns the correct token (regression)', () => {
buffer = atom.project.bufferForPathSync('sample.js')
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual(['source.js'])
expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual(['source.js'])
expect(tokenizedBuffer.tokenForPosition([1, 2]).scopes).toEqual(['source.js', 'storage.type.var.js'])
})
})
describe('.bufferRangeForScopeAtPosition(selector, position)', () => {
beforeEach(() => {
buffer = atom.project.bufferForPathSync('sample.js')
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
fullyTokenize(tokenizedBuffer)
})
describe('when the selector does not match the token at the position', () =>
it('returns a falsy value', () => expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.bogus', [0, 1])).toBeUndefined())
)
describe('when the selector matches a single token at the position', () => {
it('returns the range covered by the token', () => {
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 1])).toEqual([[0, 0], [0, 3]])
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 3])).toEqual([[0, 0], [0, 3]])
})
})
describe('when the selector matches a run of multiple tokens at the position', () => {
it('returns the range covered by all contiguous tokens (within a single line)', () => {
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual([[1, 6], [1, 28]])
})
})
})
describe('.tokenizedLineForRow(row)', () => {
it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => {
buffer = atom.project.bufferForPathSync('sample.js')
const grammar = atom.grammars.grammarForScopeName('source.js')
tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
const line0 = buffer.lineForRow(0)
const jsScopeStartId = grammar.startIdForScope(grammar.scopeName)
const jsScopeEndId = grammar.endIdForScope(grammar.scopeName)
startTokenizing(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([jsScopeStartId, line0.length, jsScopeEndId])
advanceClock(1)
expect(tokenizedBuffer.tokenizedLines[0]).not.toBeUndefined()
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).not.toEqual([jsScopeStartId, line0.length, jsScopeEndId])
const nullScopeStartId = NullGrammar.startIdForScope(NullGrammar.scopeName)
const nullScopeEndId = NullGrammar.endIdForScope(NullGrammar.scopeName)
tokenizedBuffer.setGrammar(NullGrammar)
startTokenizing(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
advanceClock(1)
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
})
it('returns undefined if the requested row is outside the buffer range', () => {
buffer = atom.project.bufferForPathSync('sample.js')
const grammar = atom.grammars.grammarForScopeName('source.js')
tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
fullyTokenize(tokenizedBuffer)
expect(tokenizedBuffer.tokenizedLineForRow(999)).toBeUndefined()
})
})
describe('text decoration layer API', () => {
describe('iterator', () => {
it('iterates over the syntactic scope boundaries', () => {
buffer = new TextBuffer({text: 'var foo = 1 /*\nhello*/var bar = 2\n'})
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
fullyTokenize(tokenizedBuffer)
const iterator = tokenizedBuffer.buildIterator()
iterator.seek(Point(0, 0))
const expectedBoundaries = [
{position: Point(0, 0), closeTags: [], openTags: ['syntax--source syntax--js', 'syntax--storage syntax--type syntax--var syntax--js']},
{position: Point(0, 3), closeTags: ['syntax--storage syntax--type syntax--var syntax--js'], openTags: []},
{position: Point(0, 8), closeTags: [], openTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js']},
{position: Point(0, 9), closeTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js'], openTags: []},
{position: Point(0, 10), closeTags: [], openTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js']},
{position: Point(0, 11), closeTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js'], openTags: []},
{position: Point(0, 12), closeTags: [], openTags: ['syntax--comment syntax--block syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js']},
{position: Point(0, 14), closeTags: ['syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js'], openTags: []},
{position: Point(1, 5), closeTags: [], openTags: ['syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js']},
{position: Point(1, 7), closeTags: ['syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js', 'syntax--comment syntax--block syntax--js'], openTags: ['syntax--storage syntax--type syntax--var syntax--js']},
{position: Point(1, 10), closeTags: ['syntax--storage syntax--type syntax--var syntax--js'], openTags: []},
{position: Point(1, 15), closeTags: [], openTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js']},
{position: Point(1, 16), closeTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js'], openTags: []},
{position: Point(1, 17), closeTags: [], openTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js']},
{position: Point(1, 18), closeTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js'], openTags: []}
]
while (true) {
const boundary = {
position: iterator.getPosition(),
closeTags: iterator.getCloseScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId)),
openTags: iterator.getOpenScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))
}
expect(boundary).toEqual(expectedBoundaries.shift())
if (!iterator.moveToSuccessor()) { break }
}
expect(iterator.seek(Point(0, 1)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
'syntax--source syntax--js',
'syntax--storage syntax--type syntax--var syntax--js'
])
expect(iterator.getPosition()).toEqual(Point(0, 3))
expect(iterator.seek(Point(0, 8)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
'syntax--source syntax--js'
])
expect(iterator.getPosition()).toEqual(Point(0, 8))
expect(iterator.seek(Point(1, 0)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
'syntax--source syntax--js',
'syntax--comment syntax--block syntax--js'
])
expect(iterator.getPosition()).toEqual(Point(1, 0))
expect(iterator.seek(Point(1, 18)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
'syntax--source syntax--js',
'syntax--constant syntax--numeric syntax--decimal syntax--js'
])
expect(iterator.getPosition()).toEqual(Point(1, 18))
expect(iterator.seek(Point(2, 0)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
'syntax--source syntax--js'
])
iterator.moveToSuccessor()
}) // ensure we don't infinitely loop (regression test)
it('does not report columns beyond the length of the line', async () => {
await atom.packages.activatePackage('language-coffee-script')
buffer = new TextBuffer({text: '# hello\n# world'})
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2})
fullyTokenize(tokenizedBuffer)
const iterator = tokenizedBuffer.buildIterator()
iterator.seek(Point(0, 0))
iterator.moveToSuccessor()
iterator.moveToSuccessor()
expect(iterator.getPosition().column).toBe(7)
iterator.moveToSuccessor()
expect(iterator.getPosition().column).toBe(0)
iterator.seek(Point(0, 7))
expect(iterator.getPosition().column).toBe(7)
iterator.seek(Point(0, 8))
expect(iterator.getPosition().column).toBe(7)
})
it('correctly terminates scopes at the beginning of the line (regression)', () => {
const grammar = atom.grammars.createGrammar('test', {
'scopeName': 'text.broken',
'name': 'Broken grammar',
'patterns': [
{'begin': 'start', 'end': '(?=end)', 'name': 'blue.broken'},
{'match': '.', 'name': 'yellow.broken'}
]
})
buffer = new TextBuffer({text: 'start x\nend x\nx'})
tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
fullyTokenize(tokenizedBuffer)
const iterator = tokenizedBuffer.buildIterator()
iterator.seek(Point(1, 0))
expect(iterator.getPosition()).toEqual([1, 0])
expect(iterator.getCloseScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual(['syntax--blue syntax--broken'])
expect(iterator.getOpenScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual(['syntax--yellow syntax--broken'])
})
})
})
describe('.suggestedIndentForBufferRow', () => {
let editor
describe('javascript', () => {
beforeEach(async () => {
editor = await atom.workspace.open('sample.js', {autoIndent: false})
await atom.packages.activatePackage('language-javascript')
})
it('bases indentation off of the previous non-blank line', () => {
expect(editor.suggestedIndentForBufferRow(0)).toBe(0)
expect(editor.suggestedIndentForBufferRow(1)).toBe(1)
expect(editor.suggestedIndentForBufferRow(2)).toBe(2)
expect(editor.suggestedIndentForBufferRow(5)).toBe(3)
expect(editor.suggestedIndentForBufferRow(7)).toBe(2)
expect(editor.suggestedIndentForBufferRow(9)).toBe(1)
expect(editor.suggestedIndentForBufferRow(11)).toBe(1)
})
it('does not take invisibles into account', () => {
editor.update({showInvisibles: true})
expect(editor.suggestedIndentForBufferRow(0)).toBe(0)
expect(editor.suggestedIndentForBufferRow(1)).toBe(1)
expect(editor.suggestedIndentForBufferRow(2)).toBe(2)
expect(editor.suggestedIndentForBufferRow(5)).toBe(3)
expect(editor.suggestedIndentForBufferRow(7)).toBe(2)
expect(editor.suggestedIndentForBufferRow(9)).toBe(1)
expect(editor.suggestedIndentForBufferRow(11)).toBe(1)
})
})
describe('css', () => {
beforeEach(async () => {
editor = await atom.workspace.open('css.css', {autoIndent: true})
await atom.packages.activatePackage('language-source')
await atom.packages.activatePackage('language-css')
})
it('does not return negative values (regression)', () => {
editor.setText('.test {\npadding: 0;\n}')
expect(editor.suggestedIndentForBufferRow(2)).toBe(0)
})
})
})
describe('.isFoldableAtRow(row)', () => {
beforeEach(() => {
buffer = atom.project.bufferForPathSync('sample.js')
buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n')
buffer.insert([0, 0], '// multi-line\n// comment\n// block\n')
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
fullyTokenize(tokenizedBuffer)
})
it('includes the first line of multi-line comments', () => {
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true)
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) // because of indent
expect(tokenizedBuffer.isFoldableAtRow(13)).toBe(true)
expect(tokenizedBuffer.isFoldableAtRow(14)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(15)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(16)).toBe(false)
buffer.insert([0, Infinity], '\n')
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(true)
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(false)
buffer.undo()
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true)
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true)
}) // because of indent
it('includes non-comment lines that precede an increase in indentation', () => {
buffer.insert([2, 0], ' ') // commented lines preceding an indent aren't foldable
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true)
expect(tokenizedBuffer.isFoldableAtRow(4)).toBe(true)
expect(tokenizedBuffer.isFoldableAtRow(5)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true)
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
buffer.insert([7, 0], ' ')
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true)
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
buffer.undo()
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true)
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
buffer.insert([7, 0], ' \n x\n')
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true)
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
buffer.insert([9, 0], ' ')
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true)
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false)
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
})
})
describe('.getFoldableRangesAtIndentLevel', () => {
it('returns the ranges that can be folded at the given indent level', () => {
buffer = new TextBuffer(dedent `
if (a) {
b();
if (c) {
d()
if (e) {
f()
}
g()
}
h()
}
i()
if (j) {
k()
}
`)
tokenizedBuffer = new TokenizedBuffer({buffer})
expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2))).toBe(dedent `
if (a) {⋯
}
i()
if (j) {⋯
}
`)
expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2))).toBe(dedent `
if (a) {
b();
if (c) {⋯
}
h()
}
i()
if (j) {
k()
}
`)
expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2))).toBe(dedent `
if (a) {
b();
if (c) {
d()
if (e) {⋯
}
g()
}
h()
}
i()
if (j) {
k()
}
`)
})
})
describe('.getFoldableRanges', () => {
it('returns the ranges that can be folded', () => {
buffer = new TextBuffer(dedent `
if (a) {
b();
if (c) {
d()
if (e) {
f()
}
g()
}
h()
}
i()
if (j) {
k()
}
`)
tokenizedBuffer = new TokenizedBuffer({buffer})
expect(tokenizedBuffer.getFoldableRanges(2).map(r => r.toString())).toEqual([
...tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2),
...tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2),
...tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2),
].sort((a, b) => (a.start.row - b.start.row) || (a.end.row - b.end.row)).map(r => r.toString()))
})
})
describe('.getFoldableRangeContainingPoint', () => {
it('returns the range for the smallest fold that contains the given range', () => {
buffer = new TextBuffer(dedent `
if (a) {
b();
if (c) {
d()
if (e) {
f()
}
g()
}
h()
}
i()
if (j) {
k()
}
`)
tokenizedBuffer = new TokenizedBuffer({buffer})
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 5), 2)).toBeNull()
let range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 10), 2)
expect(simulateFold([range])).toBe(dedent `
if (a) {⋯
}
i()
if (j) {
k()
}
`)
range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity), 2)
expect(simulateFold([range])).toBe(dedent `
if (a) {⋯
}
i()
if (j) {
k()
}
`)
range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, 20), 2)
expect(simulateFold([range])).toBe(dedent `
if (a) {
b();
if (c) {⋯
}
h()
}
i()
if (j) {
k()
}
`)
})
it('works for coffee-script', async () => {
const editor = await atom.workspace.open('coffee.coffee')
await atom.packages.activatePackage('language-coffee-script')
buffer = editor.buffer
tokenizedBuffer = editor.tokenizedBuffer
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [20, Infinity]])
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [17, Infinity]])
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [17, Infinity]])
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(19, Infinity))).toEqual([[19, Infinity], [20, Infinity]])
})
it('works for javascript', async () => {
const editor = await atom.workspace.open('sample.js')
await atom.packages.activatePackage('language-javascript')
buffer = editor.buffer
tokenizedBuffer = editor.tokenizedBuffer
expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [12, Infinity]])
expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [9, Infinity]])
expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [9, Infinity]])
expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(4, Infinity))).toEqual([[4, Infinity], [7, Infinity]])
})
})
function simulateFold (ranges) {
buffer.transact(() => {
for (const range of ranges.reverse()) {
buffer.setTextInRange(range, '⋯')
}
})
let text = buffer.getText()
buffer.undo()
return text
}
})

View File

@@ -0,0 +1,560 @@
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
const dedent = require('dedent')
const TextBuffer = require('text-buffer')
const {Point} = TextBuffer
const TextEditor = require('../src/text-editor')
const TreeSitterGrammar = require('../src/tree-sitter-grammar')
const TreeSitterLanguageMode = require('../src/tree-sitter-language-mode')
const cGrammarPath = require.resolve('language-c/grammars/tree-sitter-c.cson')
const pythonGrammarPath = require.resolve('language-python/grammars/tree-sitter-python.cson')
const jsGrammarPath = require.resolve('language-javascript/grammars/tree-sitter-javascript.cson')
describe('TreeSitterLanguageMode', () => {
let editor, buffer
beforeEach(async () => {
editor = await atom.workspace.open('')
buffer = editor.getBuffer()
})
describe('highlighting', () => {
it('applies the most specific scope mapping to each node in the syntax tree', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {
'program': 'source',
'call_expression > identifier': 'function',
'property_identifier': 'property',
'call_expression > member_expression > property_identifier': 'method'
}
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText('aa.bbb = cc(d.eee());')
expectTokensToEqual(editor, [[
{text: 'aa.', scopes: ['source']},
{text: 'bbb', scopes: ['source', 'property']},
{text: ' = ', scopes: ['source']},
{text: 'cc', scopes: ['source', 'function']},
{text: '(d.', scopes: ['source']},
{text: 'eee', scopes: ['source', 'method']},
{text: '());', scopes: ['source']}
]])
})
it('can start or end multiple scopes at the same position', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {
'program': 'source',
'call_expression': 'call',
'member_expression': 'member',
'identifier': 'variable',
'"("': 'open-paren',
'")"': 'close-paren',
}
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText('a = bb.ccc();')
expectTokensToEqual(editor, [[
{text: 'a', scopes: ['source', 'variable']},
{text: ' = ', scopes: ['source']},
{text: 'bb', scopes: ['source', 'call', 'member', 'variable']},
{text: '.ccc', scopes: ['source', 'call', 'member']},
{text: '(', scopes: ['source', 'call', 'open-paren']},
{text: ')', scopes: ['source', 'call', 'close-paren']},
{text: ';', scopes: ['source']}
]])
})
it('can resume highlighting on a line that starts with whitespace', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {
'call_expression > member_expression > property_identifier': 'function',
'property_identifier': 'member',
'identifier': 'variable'
}
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText('a\n .b();')
expectTokensToEqual(editor, [
[
{text: 'a', scopes: ['variable']},
],
[
{text: ' ', scopes: ['whitespace']},
{text: '.', scopes: []},
{text: 'b', scopes: ['function']},
{text: '();', scopes: []}
]
])
})
it('correctly skips over tokens with zero size', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-c',
scopes: {
'primitive_type': 'type',
'identifier': 'variable',
}
})
const languageMode = new TreeSitterLanguageMode({buffer, grammar})
buffer.setLanguageMode(languageMode)
buffer.setText('int main() {\n int a\n int b;\n}');
editor.screenLineForScreenRow(0)
expect(
languageMode.document.rootNode.descendantForPosition(Point(1, 2), Point(1, 6)).toString()
).toBe('(declaration (primitive_type) (identifier) (MISSING))')
expectTokensToEqual(editor, [
[
{text: 'int', scopes: ['type']},
{text: ' ', scopes: []},
{text: 'main', scopes: ['variable']},
{text: '() {', scopes: []}
],
[
{text: ' ', scopes: ['whitespace']},
{text: 'int', scopes: ['type']},
{text: ' ', scopes: []},
{text: 'a', scopes: ['variable']}
],
[
{text: ' ', scopes: ['whitespace']},
{text: 'int', scopes: ['type']},
{text: ' ', scopes: []},
{text: 'b', scopes: ['variable']},
{text: ';', scopes: []}
],
[
{text: '}', scopes: []}
]
])
})
})
describe('folding', () => {
beforeEach(() => {
editor.displayLayer.reset({foldCharacter: '…'})
})
it('can fold nodes that start and end with specified tokens', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
folds: [
{
start: {type: '{', index: 0},
end: {type: '}', index: -1}
},
{
start: {type: '(', index: 0},
end: {type: ')', index: -1}
}
]
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
module.exports =
class A {
getB (c,
d,
e) {
return this.f(g)
}
}
`)
editor.screenLineForScreenRow(0)
expect(editor.isFoldableAtBufferRow(0)).toBe(false)
expect(editor.isFoldableAtBufferRow(1)).toBe(true)
expect(editor.isFoldableAtBufferRow(2)).toBe(true)
expect(editor.isFoldableAtBufferRow(3)).toBe(false)
expect(editor.isFoldableAtBufferRow(4)).toBe(true)
expect(editor.isFoldableAtBufferRow(5)).toBe(false)
editor.foldBufferRow(2)
expect(getDisplayText(editor)).toBe(dedent `
module.exports =
class A {
getB (…) {
return this.f(g)
}
}
`)
editor.foldBufferRow(4)
expect(getDisplayText(editor)).toBe(dedent `
module.exports =
class A {
getB (…) {…}
}
`)
})
it('can fold nodes of specified types', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
folds: [
// Start the fold after the first child (the opening tag) and end it at the last child
// (the closing tag).
{
type: 'jsx_element',
start: {index: 0},
end: {index: -1}
},
// End the fold at the *second* to last child of the self-closing tag: the `/`.
{
type: 'jsx_self_closing_element',
start: {index: 1},
end: {index: -2}
}
]
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
const element1 = <Element
className='submit'
id='something' />
const element2 = <Element>
<span>hello</span>
<span>world</span>
</Element>
`)
editor.screenLineForScreenRow(0)
expect(editor.isFoldableAtBufferRow(0)).toBe(true)
expect(editor.isFoldableAtBufferRow(1)).toBe(false)
expect(editor.isFoldableAtBufferRow(2)).toBe(false)
expect(editor.isFoldableAtBufferRow(3)).toBe(false)
expect(editor.isFoldableAtBufferRow(4)).toBe(true)
expect(editor.isFoldableAtBufferRow(5)).toBe(false)
editor.foldBufferRow(0)
expect(getDisplayText(editor)).toBe(dedent `
const element1 = <Element…/>
const element2 = <Element>
<span>hello</span>
<span>world</span>
</Element>
`)
editor.foldBufferRow(4)
expect(getDisplayText(editor)).toBe(dedent `
const element1 = <Element…/>
const element2 = <Element>…
</Element>
`)
})
it('can fold entire nodes when no start or end parameters are specified', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
folds: [
// By default, for a node with no children, folds are started at the *end* of the first
// line of a node, and ended at the *beginning* of the last line.
{type: 'comment'}
]
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
/**
* Important
*/
const x = 1 /*
Also important
*/
`)
editor.screenLineForScreenRow(0)
expect(editor.isFoldableAtBufferRow(0)).toBe(true)
expect(editor.isFoldableAtBufferRow(1)).toBe(false)
expect(editor.isFoldableAtBufferRow(2)).toBe(false)
expect(editor.isFoldableAtBufferRow(3)).toBe(true)
expect(editor.isFoldableAtBufferRow(4)).toBe(false)
editor.foldBufferRow(0)
expect(getDisplayText(editor)).toBe(dedent `
/**… */
const x = 1 /*
Also important
*/
`)
editor.foldBufferRow(3)
expect(getDisplayText(editor)).toBe(dedent `
/**… */
const x = 1 /*…*/
`)
})
it('tries each folding strategy for a given node in the order specified', () => {
const grammar = new TreeSitterGrammar(atom.grammars, cGrammarPath, {
parser: 'tree-sitter-c',
folds: [
// If the #ifdef has an `#else` clause, then end the fold there.
{
type: ['preproc_ifdef', 'preproc_elif'],
start: {index: 1},
end: {type: ['preproc_else', 'preproc_elif']}
},
// Otherwise, end the fold at the last child - the `#endif`.
{
type: 'preproc_ifdef',
start: {index: 1},
end: {index: -1}
},
// When folding an `#else` clause, the fold extends to the end of the clause.
{
type: 'preproc_else',
start: {index: 0}
}
]
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
#ifndef FOO_H_
#define FOO_H_
#ifdef _WIN32
#include <windows.h>
const char *path_separator = "\\";
#elif defined MACOS
#include <carbon.h>
const char *path_separator = "/";
#else
#include <dirent.h>
const char *path_separator = "/";
#endif
#endif
`)
editor.screenLineForScreenRow(0)
editor.foldBufferRow(3)
expect(getDisplayText(editor)).toBe(dedent `
#ifndef FOO_H_
#define FOO_H_
#ifdef _WIN32…
#elif defined MACOS
#include <carbon.h>
const char *path_separator = "/";
#else
#include <dirent.h>
const char *path_separator = "/";
#endif
#endif
`)
editor.foldBufferRow(8)
expect(getDisplayText(editor)).toBe(dedent `
#ifndef FOO_H_
#define FOO_H_
#ifdef _WIN32…
#elif defined MACOS…
#else
#include <dirent.h>
const char *path_separator = "/";
#endif
#endif
`)
editor.foldBufferRow(0)
expect(getDisplayText(editor)).toBe(dedent `
#ifndef FOO_H_…
#endif
`)
editor.foldAllAtIndentLevel(1)
expect(getDisplayText(editor)).toBe(dedent `
#ifndef FOO_H_
#define FOO_H_
#ifdef _WIN32…
#elif defined MACOS…
#else…
#endif
#endif
`)
})
describe('when folding a node that ends with a line break', () => {
it('ends the fold at the end of the previous line', () => {
const grammar = new TreeSitterGrammar(atom.grammars, pythonGrammarPath, {
parser: 'tree-sitter-python',
folds: [
{
type: 'function_definition',
start: {type: ':'}
}
]
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
def ab():
print 'a'
print 'b'
def cd():
print 'c'
print 'd'
`)
editor.screenLineForScreenRow(0)
editor.foldBufferRow(0)
expect(getDisplayText(editor)).toBe(dedent `
def ab():…
def cd():
print 'c'
print 'd'
`)
})
})
})
describe('.scopeDescriptorForPosition', () => {
it('returns a scope descriptor representing the given position in the syntax tree', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
id: 'javascript',
parser: 'tree-sitter-javascript'
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText('foo({bar: baz});')
editor.screenLineForScreenRow(0)
expect(editor.scopeDescriptorForBufferPosition({row: 0, column: 6}).getScopesArray()).toEqual([
'javascript',
'program',
'expression_statement',
'call_expression',
'arguments',
'object',
'pair',
'property_identifier'
])
})
})
describe('TextEditor.selectLargerSyntaxNode and .selectSmallerSyntaxNode', () => {
it('expands and contract the selection based on the syntax tree', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {'program': 'source'}
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
function a (b, c, d) {
eee.f()
g()
}
`)
editor.screenLineForScreenRow(0)
editor.setCursorBufferPosition([1, 3])
editor.selectLargerSyntaxNode()
expect(editor.getSelectedText()).toBe('eee')
editor.selectLargerSyntaxNode()
expect(editor.getSelectedText()).toBe('eee.f')
editor.selectLargerSyntaxNode()
expect(editor.getSelectedText()).toBe('eee.f()')
editor.selectLargerSyntaxNode()
expect(editor.getSelectedText()).toBe('{\n eee.f()\n g()\n}')
editor.selectLargerSyntaxNode()
expect(editor.getSelectedText()).toBe('function a (b, c, d) {\n eee.f()\n g()\n}')
editor.selectSmallerSyntaxNode()
expect(editor.getSelectedText()).toBe('{\n eee.f()\n g()\n}')
editor.selectSmallerSyntaxNode()
expect(editor.getSelectedText()).toBe('eee.f()')
editor.selectSmallerSyntaxNode()
expect(editor.getSelectedText()).toBe('eee.f')
editor.selectSmallerSyntaxNode()
expect(editor.getSelectedText()).toBe('eee')
editor.selectSmallerSyntaxNode()
expect(editor.getSelectedBufferRange()).toEqual([[1, 3], [1, 3]])
})
})
})
function getDisplayText (editor) {
return editor.displayLayer.getText()
}
function expectTokensToEqual (editor, expectedTokenLines) {
const lastRow = editor.getLastScreenRow()
// Assert that the correct tokens are returned regardless of which row
// the highlighting iterator starts on.
for (let startRow = 0; startRow <= lastRow; startRow++) {
editor.displayLayer.clearSpatialIndex()
editor.displayLayer.getScreenLines(startRow, Infinity)
const tokenLines = []
for (let row = startRow; row <= lastRow; row++) {
tokenLines[row] = editor.tokensForScreenRow(row).map(({text, scopes}) => ({
text,
scopes: scopes.map(scope => scope
.split(' ')
.map(className => className.slice('syntax--'.length))
.join(' '))
}))
}
for (let row = startRow; row <= lastRow; row++) {
const tokenLine = tokenLines[row]
const expectedTokenLine = expectedTokenLines[row]
expect(tokenLine.length).toEqual(expectedTokenLine.length)
for (let i = 0; i < tokenLine.length; i++) {
expect(tokenLine[i]).toEqual(expectedTokenLine[i], `Token ${i}, startRow: ${startRow}`)
}
}
}
}

View File

@@ -44,7 +44,15 @@ describe('WindowEventHandler', () => {
})
)
})
describe('resize event', () =>
it('calls storeWindowDimensions', () => {
spyOn(atom, 'storeWindowDimensions')
window.dispatchEvent(new CustomEvent('resize'))
expect(atom.storeWindowDimensions).toHaveBeenCalled()
})
)
describe('window:close event', () =>
it('closes the window', () => {
spyOn(atom, 'close')

View File

@@ -561,8 +561,6 @@ describe('WorkspaceElement', () => {
expectToggleButtonHidden(rightDock)
expectToggleButtonHidden(bottomDock)
workspaceElement.paneContainer.dispatchEvent(new MouseEvent('mouseleave'))
// --- Right Dock ---
// Mouse over where the toggle button would be if the dock were hovered
@@ -591,7 +589,7 @@ describe('WorkspaceElement', () => {
// Mouse to edge of the window
moveMouse({clientX: 575, clientY: 150})
expectToggleButtonHidden(rightDock)
moveMouse({clientX: 600, clientY: 150})
moveMouse({clientX: 598, clientY: 150})
expectToggleButtonVisible(rightDock, 'icon-chevron-left')
// Click the toggle button again
@@ -627,7 +625,7 @@ describe('WorkspaceElement', () => {
// Mouse to edge of the window
moveMouse({clientX: 25, clientY: 150})
expectToggleButtonHidden(leftDock)
moveMouse({clientX: 0, clientY: 150})
moveMouse({clientX: 2, clientY: 150})
expectToggleButtonVisible(leftDock, 'icon-chevron-right')
// Click the toggle button again
@@ -663,7 +661,7 @@ describe('WorkspaceElement', () => {
// Mouse to edge of the window
moveMouse({clientX: 300, clientY: 290})
expectToggleButtonHidden(leftDock)
moveMouse({clientX: 300, clientY: 300})
moveMouse({clientX: 300, clientY: 299})
expectToggleButtonVisible(bottomDock, 'icon-chevron-up')
// Click the toggle button again

View File

@@ -1,5 +1,7 @@
const path = require('path')
const temp = require('temp').track()
const dedent = require('dedent')
const TextBuffer = require('text-buffer')
const TextEditor = require('../src/text-editor')
const Workspace = require('../src/workspace')
const Project = require('../src/project')
@@ -8,7 +10,7 @@ const _ = require('underscore-plus')
const fstream = require('fstream')
const fs = require('fs-plus')
const AtomEnvironment = require('../src/atom-environment')
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise} = require('./async-spec-helpers')
describe('Workspace', () => {
let workspace
@@ -43,7 +45,8 @@ describe('Workspace', () => {
notificationManager: atom.notifications,
packageManager: atom.packages,
confirm: atom.confirm.bind(atom),
applicationDelegate: atom.applicationDelegate
applicationDelegate: atom.applicationDelegate,
grammarRegistry: atom.grammars
})
return atom.project.deserialize(projectState).then(() => {
workspace = atom.workspace = new Workspace({
@@ -656,59 +659,42 @@ describe('Workspace', () => {
})
})
describe('when the file is over 2MB', () => {
it('opens the editor with largeFileMode: true', () => {
spyOn(fs, 'getSizeSync').andReturn(2 * 1048577) // 2MB
let editor = null
waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e }))
runs(() => expect(editor.largeFileMode).toBe(true))
})
})
describe('when the file is over user-defined limit', () => {
const shouldPromptForFileOfSize = (size, shouldPrompt) => {
describe('when the file size is over the limit defined in `core.warnOnLargeFileLimit`', () => {
const shouldPromptForFileOfSize = async (size, shouldPrompt) => {
spyOn(fs, 'getSizeSync').andReturn(size * 1048577)
atom.applicationDelegate.confirm.andCallFake(() => selectedButtonIndex)
atom.applicationDelegate.confirm()
var selectedButtonIndex = 1 // cancel
let editor = null
waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e }))
let selectedButtonIndex = 1 // cancel
atom.applicationDelegate.confirm.andCallFake((options, callback) => callback(selectedButtonIndex))
let editor = await workspace.open('sample.js')
if (shouldPrompt) {
runs(() => {
expect(editor).toBeUndefined()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(editor).toBeUndefined()
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
atom.applicationDelegate.confirm.reset()
selectedButtonIndex = 0
}) // open the file
atom.applicationDelegate.confirm.reset()
selectedButtonIndex = 0 // open the file
waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e }))
editor = await workspace.open('sample.js')
runs(() => {
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
expect(editor.largeFileMode).toBe(true)
})
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
} else {
runs(() => expect(editor).not.toBeUndefined())
expect(editor).not.toBeUndefined()
}
}
it('prompts the user to make sure they want to open a file this big', () => {
it('prompts before opening the file', async () => {
atom.config.set('core.warnOnLargeFileLimit', 20)
shouldPromptForFileOfSize(20, true)
await shouldPromptForFileOfSize(20, true)
})
it("doesn't prompt on files below the limit", () => {
it("doesn't prompt on files below the limit", async () => {
atom.config.set('core.warnOnLargeFileLimit', 30)
shouldPromptForFileOfSize(20, false)
await shouldPromptForFileOfSize(20, false)
})
it('prompts for smaller files with a lower limit', () => {
it('prompts for smaller files with a lower limit', async () => {
atom.config.set('core.warnOnLargeFileLimit', 5)
shouldPromptForFileOfSize(10, true)
await shouldPromptForFileOfSize(10, true)
})
})
@@ -943,6 +929,18 @@ describe('Workspace', () => {
})
})
})
describe('when opening an editor with a buffer that isn\'t part of the project', () => {
it('adds the buffer to the project', async () => {
const buffer = new TextBuffer()
const editor = new TextEditor({buffer})
await atom.workspace.open(editor)
expect(atom.project.getBuffers().map(buffer => buffer.id)).toContain(buffer.id)
expect(buffer.getLanguageMode().getLanguageId()).toBe('text.plain.null-grammar')
})
})
})
describe('finding items in the workspace', () => {
@@ -1217,8 +1215,8 @@ describe('Workspace', () => {
})
})
describe('::onDidStopChangingActivePaneItem()', function () {
it('invokes observers when the active item of the active pane stops changing', function () {
describe('::onDidStopChangingActivePaneItem()', () => {
it('invokes observers when the active item of the active pane stops changing', () => {
const pane1 = atom.workspace.getCenter().getActivePane()
const pane2 = pane1.splitRight({items: [document.createElement('div'), document.createElement('div')]});
atom.workspace.getLeftDock().getActivePane().addItem(document.createElement('div'))
@@ -1237,29 +1235,22 @@ describe('Workspace', () => {
})
describe('the grammar-used hook', () => {
it('fires when opening a file or changing the grammar of an open file', () => {
let editor = null
let javascriptGrammarUsed = false
let coffeescriptGrammarUsed = false
it('fires when opening a file or changing the grammar of an open file', async () => {
let resolveJavascriptGrammarUsed, resolveCoffeeScriptGrammarUsed
const javascriptGrammarUsed = new Promise(resolve => { resolveJavascriptGrammarUsed = resolve })
const coffeescriptGrammarUsed = new Promise(resolve => { resolveCoffeeScriptGrammarUsed = resolve })
atom.packages.triggerDeferredActivationHooks()
atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', resolveJavascriptGrammarUsed)
atom.packages.onDidTriggerActivationHook('language-coffee-script:grammar-used', resolveCoffeeScriptGrammarUsed)
runs(() => {
atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', () => { javascriptGrammarUsed = true })
atom.packages.onDidTriggerActivationHook('language-coffee-script:grammar-used', () => { coffeescriptGrammarUsed = true })
})
const editor = await atom.workspace.open('sample.js', {autoIndent: false})
await atom.packages.activatePackage('language-javascript')
await javascriptGrammarUsed
waitsForPromise(() => atom.workspace.open('sample.js', {autoIndent: false}).then(o => { editor = o }))
waitsForPromise(() => atom.packages.activatePackage('language-javascript'))
waitsFor(() => javascriptGrammarUsed)
waitsForPromise(() => atom.packages.activatePackage('language-coffee-script'))
runs(() => editor.setGrammar(atom.grammars.selectGrammar('.coffee')))
waitsFor(() => coffeescriptGrammarUsed)
await atom.packages.activatePackage('language-coffee-script')
atom.grammars.assignLanguageMode(editor, 'source.coffee')
await coffeescriptGrammarUsed
})
})
@@ -1382,7 +1373,7 @@ describe('Workspace', () => {
describe('::getActiveTextEditor()', () => {
describe("when the workspace center's active pane item is a text editor", () => {
describe('when the workspace center has focus', function () {
describe('when the workspace center has focus', () => {
it('returns the text editor', () => {
const workspaceCenter = workspace.getCenter()
const editor = new TextEditor()
@@ -1393,7 +1384,7 @@ describe('Workspace', () => {
})
})
describe('when a dock has focus', function () {
describe('when a dock has focus', () => {
it('returns the text editor', () => {
const workspaceCenter = workspace.getCenter()
const editor = new TextEditor()
@@ -1521,34 +1512,27 @@ describe('Workspace', () => {
})
describe('when an editor is destroyed', () => {
it('removes the editor', () => {
let editor = null
waitsForPromise(() => workspace.open('a').then(e => { editor = e }))
runs(() => {
expect(workspace.getTextEditors()).toHaveLength(1)
editor.destroy()
expect(workspace.getTextEditors()).toHaveLength(0)
})
it('removes the editor', async () => {
const editor = await workspace.open('a')
expect(workspace.getTextEditors()).toHaveLength(1)
editor.destroy()
expect(workspace.getTextEditors()).toHaveLength(0)
})
})
describe('when an editor is copied because its pane is split', () => {
it('sets up the new editor to be configured by the text editor registry', () => {
waitsForPromise(() => atom.packages.activatePackage('language-javascript'))
it('sets up the new editor to be configured by the text editor registry', async () => {
await atom.packages.activatePackage('language-javascript')
waitsForPromise(() =>
workspace.open('a').then(editor => {
atom.textEditors.setGrammarOverride(editor, 'source.js')
expect(editor.getGrammar().name).toBe('JavaScript')
const editor = await workspace.open('a')
workspace.getActivePane().splitRight({copyActiveItem: true})
const newEditor = workspace.getActiveTextEditor()
expect(newEditor).not.toBe(editor)
expect(newEditor.getGrammar().name).toBe('JavaScript')
})
)
atom.grammars.assignLanguageMode(editor, 'source.js')
expect(editor.getGrammar().name).toBe('JavaScript')
workspace.getActivePane().splitRight({copyActiveItem: true})
const newEditor = workspace.getActiveTextEditor()
expect(newEditor).not.toBe(editor)
expect(newEditor.getGrammar().name).toBe('JavaScript')
})
})
@@ -1561,11 +1545,10 @@ describe('Workspace', () => {
waitsForPromise(() => atom.workspace.open('sample.coffee'))
runs(function () {
atom.workspace.getActiveTextEditor().setText(`\
i = /test/; #FIXME\
`
)
runs(() => {
atom.workspace.getActiveTextEditor().setText(dedent `
i = /test/; #FIXME\
`)
const atom2 = new AtomEnvironment({applicationDelegate: atom.applicationDelegate})
atom2.initialize({
@@ -2789,7 +2772,7 @@ i = /test/; #FIXME\
})
describe('grammar activation', () => {
it('notifies the workspace of which grammar is used', () => {
it('notifies the workspace of which grammar is used', async () => {
atom.packages.triggerDeferredActivationHooks()
const javascriptGrammarUsed = jasmine.createSpy('js grammar used')
@@ -2800,52 +2783,51 @@ i = /test/; #FIXME\
atom.packages.onDidTriggerActivationHook('language-ruby:grammar-used', rubyGrammarUsed)
atom.packages.onDidTriggerActivationHook('language-c:grammar-used', cGrammarUsed)
waitsForPromise(() => atom.packages.activatePackage('language-ruby'))
waitsForPromise(() => atom.packages.activatePackage('language-javascript'))
waitsForPromise(() => atom.packages.activatePackage('language-c'))
waitsForPromise(() => atom.workspace.open('sample-with-comments.js'))
await atom.packages.activatePackage('language-ruby')
await atom.packages.activatePackage('language-javascript')
await atom.packages.activatePackage('language-c')
await atom.workspace.open('sample-with-comments.js')
runs(() => {
// Hooks are triggered when opening new editors
expect(javascriptGrammarUsed).toHaveBeenCalled()
// Hooks are triggered when opening new editors
expect(javascriptGrammarUsed).toHaveBeenCalled()
// Hooks are triggered when changing existing editors grammars
atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.c'))
expect(cGrammarUsed).toHaveBeenCalled()
// Hooks are triggered when changing existing editors grammars
atom.grammars.assignLanguageMode(atom.workspace.getActiveTextEditor(), 'source.c')
expect(cGrammarUsed).toHaveBeenCalled()
// Hooks are triggered when editors are added in other ways.
atom.workspace.getActivePane().splitRight({copyActiveItem: true})
atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.ruby'))
expect(rubyGrammarUsed).toHaveBeenCalled()
})
// Hooks are triggered when editors are added in other ways.
atom.workspace.getActivePane().splitRight({copyActiveItem: true})
atom.grammars.assignLanguageMode(atom.workspace.getActiveTextEditor(), 'source.ruby')
expect(rubyGrammarUsed).toHaveBeenCalled()
})
})
describe('.checkoutHeadRevision()', () => {
let editor = null
beforeEach(() => {
beforeEach(async () => {
jasmine.useRealClock()
atom.config.set('editor.confirmCheckoutHeadRevision', false)
waitsForPromise(() => atom.workspace.open('sample-with-comments.js').then(o => { editor = o }))
editor = await atom.workspace.open('sample-with-comments.js')
})
it('reverts to the version of its file checked into the project repository', () => {
it('reverts to the version of its file checked into the project repository', async () => {
editor.setCursorBufferPosition([0, 0])
editor.insertText('---\n')
expect(editor.lineTextForBufferRow(0)).toBe('---')
waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor))
atom.workspace.checkoutHeadRevision(editor)
runs(() => expect(editor.lineTextForBufferRow(0)).toBe(''))
await conditionPromise(() => editor.lineTextForBufferRow(0) === '')
})
describe("when there's no repository for the editor's file", () => {
it("doesn't do anything", () => {
it("doesn't do anything", async () => {
editor = new TextEditor()
editor.setText('stuff')
atom.workspace.checkoutHeadRevision(editor)
waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor))
atom.workspace.checkoutHeadRevision(editor)
})
})
})
@@ -2894,4 +2876,6 @@ i = /test/; #FIXME\
})
})
const escapeStringRegex = str => str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
function escapeStringRegex (string) {
return string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
}

View File

@@ -1,294 +0,0 @@
_ = require 'underscore-plus'
{ipcRenderer, remote, shell} = require 'electron'
ipcHelpers = require './ipc-helpers'
{Disposable} = require 'event-kit'
getWindowLoadSettings = require './get-window-load-settings'
module.exports =
class ApplicationDelegate
getWindowLoadSettings: -> getWindowLoadSettings()
open: (params) ->
ipcRenderer.send('open', params)
pickFolder: (callback) ->
responseChannel = "atom-pick-folder-response"
ipcRenderer.on responseChannel, (event, path) ->
ipcRenderer.removeAllListeners(responseChannel)
callback(path)
ipcRenderer.send("pick-folder", responseChannel)
getCurrentWindow: ->
remote.getCurrentWindow()
closeWindow: ->
ipcHelpers.call('window-method', 'close')
getTemporaryWindowState: ->
ipcHelpers.call('get-temporary-window-state').then (stateJSON) -> JSON.parse(stateJSON)
setTemporaryWindowState: (state) ->
ipcHelpers.call('set-temporary-window-state', JSON.stringify(state))
getWindowSize: ->
[width, height] = remote.getCurrentWindow().getSize()
{width, height}
setWindowSize: (width, height) ->
ipcHelpers.call('set-window-size', width, height)
getWindowPosition: ->
[x, y] = remote.getCurrentWindow().getPosition()
{x, y}
setWindowPosition: (x, y) ->
ipcHelpers.call('set-window-position', x, y)
centerWindow: ->
ipcHelpers.call('center-window')
focusWindow: ->
ipcHelpers.call('focus-window')
showWindow: ->
ipcHelpers.call('show-window')
hideWindow: ->
ipcHelpers.call('hide-window')
reloadWindow: ->
ipcHelpers.call('window-method', 'reload')
restartApplication: ->
ipcRenderer.send("restart-application")
minimizeWindow: ->
ipcHelpers.call('window-method', 'minimize')
isWindowMaximized: ->
remote.getCurrentWindow().isMaximized()
maximizeWindow: ->
ipcHelpers.call('window-method', 'maximize')
unmaximizeWindow: ->
ipcHelpers.call('window-method', 'unmaximize')
isWindowFullScreen: ->
remote.getCurrentWindow().isFullScreen()
setWindowFullScreen: (fullScreen=false) ->
ipcHelpers.call('window-method', 'setFullScreen', fullScreen)
onDidEnterFullScreen: (callback) ->
ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback)
onDidLeaveFullScreen: (callback) ->
ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback)
openWindowDevTools: ->
# Defer DevTools interaction to the next tick, because using them during
# event handling causes some wrong input events to be triggered on
# `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
new Promise(process.nextTick).then(-> ipcHelpers.call('window-method', 'openDevTools'))
closeWindowDevTools: ->
# Defer DevTools interaction to the next tick, because using them during
# event handling causes some wrong input events to be triggered on
# `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
new Promise(process.nextTick).then(-> ipcHelpers.call('window-method', 'closeDevTools'))
toggleWindowDevTools: ->
# Defer DevTools interaction to the next tick, because using them during
# event handling causes some wrong input events to be triggered on
# `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
new Promise(process.nextTick).then(-> ipcHelpers.call('window-method', 'toggleDevTools'))
executeJavaScriptInWindowDevTools: (code) ->
ipcRenderer.send("execute-javascript-in-dev-tools", code)
setWindowDocumentEdited: (edited) ->
ipcHelpers.call('window-method', 'setDocumentEdited', edited)
setRepresentedFilename: (filename) ->
ipcHelpers.call('window-method', 'setRepresentedFilename', filename)
addRecentDocument: (filename) ->
ipcRenderer.send("add-recent-document", filename)
setRepresentedDirectoryPaths: (paths) ->
ipcHelpers.call('window-method', 'setRepresentedDirectoryPaths', paths)
setAutoHideWindowMenuBar: (autoHide) ->
ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide)
setWindowMenuBarVisibility: (visible) ->
remote.getCurrentWindow().setMenuBarVisibility(visible)
getPrimaryDisplayWorkAreaSize: ->
remote.screen.getPrimaryDisplay().workAreaSize
getUserDefault: (key, type) ->
remote.systemPreferences.getUserDefault(key, type)
confirm: ({message, detailedMessage, buttons}) ->
buttons ?= {}
if _.isArray(buttons)
buttonLabels = buttons
else
buttonLabels = Object.keys(buttons)
chosen = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info'
message: message
detail: detailedMessage
buttons: buttonLabels
normalizeAccessKeys: true
})
if _.isArray(buttons)
chosen
else
callback = buttons[buttonLabels[chosen]]
callback?()
showMessageDialog: (params) ->
showSaveDialog: (params) ->
if typeof params is 'string'
params = {defaultPath: params}
@getCurrentWindow().showSaveDialog(params)
playBeepSound: ->
shell.beep()
onDidOpenLocations: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'open-locations'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onUpdateAvailable: (callback) ->
outerCallback = (event, message, detail) ->
# TODO: Yes, this is strange that `onUpdateAvailable` is listening for
# `did-begin-downloading-update`. We currently have no mechanism to know
# if there is an update, so begin of downloading is a good proxy.
callback(detail) if message is 'did-begin-downloading-update'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onDidBeginDownloadingUpdate: (callback) ->
@onUpdateAvailable(callback)
onDidBeginCheckingForUpdate: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'checking-for-update'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onDidCompleteDownloadingUpdate: (callback) ->
outerCallback = (event, message, detail) ->
# TODO: We could rename this event to `did-complete-downloading-update`
callback(detail) if message is 'update-available'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onUpdateNotAvailable: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'update-not-available'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onUpdateError: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'update-error'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onApplicationMenuCommand: (callback) ->
outerCallback = (event, args...) ->
callback(args...)
ipcRenderer.on('command', outerCallback)
new Disposable ->
ipcRenderer.removeListener('command', outerCallback)
onContextMenuCommand: (callback) ->
outerCallback = (event, args...) ->
callback(args...)
ipcRenderer.on('context-command', outerCallback)
new Disposable ->
ipcRenderer.removeListener('context-command', outerCallback)
onURIMessage: (callback) ->
outerCallback = (event, args...) ->
callback(args...)
ipcRenderer.on('uri-message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('uri-message', outerCallback)
onDidRequestUnload: (callback) ->
outerCallback = (event, message) ->
callback(event).then (shouldUnload) ->
ipcRenderer.send('did-prepare-to-unload', shouldUnload)
ipcRenderer.on('prepare-to-unload', outerCallback)
new Disposable ->
ipcRenderer.removeListener('prepare-to-unload', outerCallback)
onDidChangeHistoryManager: (callback) ->
outerCallback = (event, message) ->
callback(event)
ipcRenderer.on('did-change-history-manager', outerCallback)
new Disposable ->
ipcRenderer.removeListener('did-change-history-manager', outerCallback)
didChangeHistoryManager: ->
ipcRenderer.send('did-change-history-manager')
openExternal: (url) ->
shell.openExternal(url)
checkForUpdate: ->
ipcRenderer.send('command', 'application:check-for-update')
restartAndInstallUpdate: ->
ipcRenderer.send('command', 'application:install-update')
getAutoUpdateManagerState: ->
ipcRenderer.sendSync('get-auto-update-manager-state')
getAutoUpdateManagerErrorMessage: ->
ipcRenderer.sendSync('get-auto-update-manager-error')
emitWillSavePath: (path) ->
ipcRenderer.sendSync('will-save-path', path)
emitDidSavePath: (path) ->
ipcRenderer.sendSync('did-save-path', path)
resolveProxy: (requestId, url) ->
ipcRenderer.send('resolve-proxy', requestId, url)
onDidResolveProxy: (callback) ->
outerCallback = (event, requestId, proxy) ->
callback(requestId, proxy)
ipcRenderer.on('did-resolve-proxy', outerCallback)
new Disposable ->
ipcRenderer.removeListener('did-resolve-proxy', outerCallback)

374
src/application-delegate.js Normal file
View File

@@ -0,0 +1,374 @@
const {ipcRenderer, remote, shell} = require('electron')
const ipcHelpers = require('./ipc-helpers')
const {Disposable} = require('event-kit')
const getWindowLoadSettings = require('./get-window-load-settings')
module.exports =
class ApplicationDelegate {
getWindowLoadSettings () { return getWindowLoadSettings() }
open (params) {
return ipcRenderer.send('open', params)
}
pickFolder (callback) {
const responseChannel = 'atom-pick-folder-response'
ipcRenderer.on(responseChannel, function (event, path) {
ipcRenderer.removeAllListeners(responseChannel)
return callback(path)
})
return ipcRenderer.send('pick-folder', responseChannel)
}
getCurrentWindow () {
return remote.getCurrentWindow()
}
closeWindow () {
return ipcHelpers.call('window-method', 'close')
}
async getTemporaryWindowState () {
const stateJSON = await ipcHelpers.call('get-temporary-window-state')
return JSON.parse(stateJSON)
}
setTemporaryWindowState (state) {
return ipcHelpers.call('set-temporary-window-state', JSON.stringify(state))
}
getWindowSize () {
const [width, height] = Array.from(remote.getCurrentWindow().getSize())
return {width, height}
}
setWindowSize (width, height) {
return ipcHelpers.call('set-window-size', width, height)
}
getWindowPosition () {
const [x, y] = Array.from(remote.getCurrentWindow().getPosition())
return {x, y}
}
setWindowPosition (x, y) {
return ipcHelpers.call('set-window-position', x, y)
}
centerWindow () {
return ipcHelpers.call('center-window')
}
focusWindow () {
return ipcHelpers.call('focus-window')
}
showWindow () {
return ipcHelpers.call('show-window')
}
hideWindow () {
return ipcHelpers.call('hide-window')
}
reloadWindow () {
return ipcHelpers.call('window-method', 'reload')
}
restartApplication () {
return ipcRenderer.send('restart-application')
}
minimizeWindow () {
return ipcHelpers.call('window-method', 'minimize')
}
isWindowMaximized () {
return remote.getCurrentWindow().isMaximized()
}
maximizeWindow () {
return ipcHelpers.call('window-method', 'maximize')
}
unmaximizeWindow () {
return ipcHelpers.call('window-method', 'unmaximize')
}
isWindowFullScreen () {
return remote.getCurrentWindow().isFullScreen()
}
setWindowFullScreen (fullScreen = false) {
return ipcHelpers.call('window-method', 'setFullScreen', fullScreen)
}
onDidEnterFullScreen (callback) {
return ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback)
}
onDidLeaveFullScreen (callback) {
return ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback)
}
async openWindowDevTools () {
// Defer DevTools interaction to the next tick, because using them during
// event handling causes some wrong input events to be triggered on
// `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
await new Promise(process.nextTick)
return ipcHelpers.call('window-method', 'openDevTools')
}
async closeWindowDevTools () {
// Defer DevTools interaction to the next tick, because using them during
// event handling causes some wrong input events to be triggered on
// `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
await new Promise(process.nextTick)
return ipcHelpers.call('window-method', 'closeDevTools')
}
async toggleWindowDevTools () {
// Defer DevTools interaction to the next tick, because using them during
// event handling causes some wrong input events to be triggered on
// `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
await new Promise(process.nextTick)
return ipcHelpers.call('window-method', 'toggleDevTools')
}
executeJavaScriptInWindowDevTools (code) {
return ipcRenderer.send('execute-javascript-in-dev-tools', code)
}
didClosePathWithWaitSession (path) {
return ipcHelpers.call('window-method', 'didClosePathWithWaitSession', path)
}
setWindowDocumentEdited (edited) {
return ipcHelpers.call('window-method', 'setDocumentEdited', edited)
}
setRepresentedFilename (filename) {
return ipcHelpers.call('window-method', 'setRepresentedFilename', filename)
}
addRecentDocument (filename) {
return ipcRenderer.send('add-recent-document', filename)
}
setRepresentedDirectoryPaths (paths) {
return ipcHelpers.call('window-method', 'setRepresentedDirectoryPaths', paths)
}
setAutoHideWindowMenuBar (autoHide) {
return ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide)
}
setWindowMenuBarVisibility (visible) {
return remote.getCurrentWindow().setMenuBarVisibility(visible)
}
getPrimaryDisplayWorkAreaSize () {
return remote.screen.getPrimaryDisplay().workAreaSize
}
getUserDefault (key, type) {
return remote.systemPreferences.getUserDefault(key, type)
}
confirm (options, callback) {
if (typeof callback === 'function') {
// Async version: pass options directly to Electron but set sane defaults
options = Object.assign({type: 'info', normalizeAccessKeys: true}, options)
remote.dialog.showMessageBox(remote.getCurrentWindow(), options, callback)
} else {
// Legacy sync version: options can only have `message`,
// `detailedMessage` (optional), and buttons array or object (optional)
let {message, detailedMessage, buttons} = options
let buttonLabels
if (!buttons) buttons = {}
if (Array.isArray(buttons)) {
buttonLabels = buttons
} else {
buttonLabels = Object.keys(buttons)
}
const chosen = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message,
detail: detailedMessage,
buttons: buttonLabels,
normalizeAccessKeys: true
})
if (Array.isArray(buttons)) {
return chosen
} else {
const callback = buttons[buttonLabels[chosen]]
if (typeof callback === 'function') callback()
}
}
}
showMessageDialog (params) {}
showSaveDialog (options, callback) {
if (typeof callback === 'function') {
// Async
this.getCurrentWindow().showSaveDialog(options, callback)
} else {
// Sync
if (typeof params === 'string') {
options = {defaultPath: options}
}
return this.getCurrentWindow().showSaveDialog(options)
}
}
playBeepSound () {
return shell.beep()
}
onDidOpenLocations (callback) {
const outerCallback = (event, message, detail) => {
if (message === 'open-locations') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
}
onUpdateAvailable (callback) {
const outerCallback = (event, message, detail) => {
// TODO: Yes, this is strange that `onUpdateAvailable` is listening for
// `did-begin-downloading-update`. We currently have no mechanism to know
// if there is an update, so begin of downloading is a good proxy.
if (message === 'did-begin-downloading-update') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
}
onDidBeginDownloadingUpdate (callback) {
return this.onUpdateAvailable(callback)
}
onDidBeginCheckingForUpdate (callback) {
const outerCallback = (event, message, detail) => {
if (message === 'checking-for-update') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
}
onDidCompleteDownloadingUpdate (callback) {
const outerCallback = (event, message, detail) => {
// TODO: We could rename this event to `did-complete-downloading-update`
if (message === 'update-available') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
}
onUpdateNotAvailable (callback) {
const outerCallback = (event, message, detail) => {
if (message === 'update-not-available') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
}
onUpdateError (callback) {
const outerCallback = (event, message, detail) => {
if (message === 'update-error') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
}
onApplicationMenuCommand (handler) {
const outerCallback = (event, ...args) => handler(...args)
ipcRenderer.on('command', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('command', outerCallback))
}
onContextMenuCommand (handler) {
const outerCallback = (event, ...args) => handler(...args)
ipcRenderer.on('context-command', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('context-command', outerCallback))
}
onURIMessage (handler) {
const outerCallback = (event, ...args) => handler(...args)
ipcRenderer.on('uri-message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('uri-message', outerCallback))
}
onDidRequestUnload (callback) {
const outerCallback = async (event, message) => {
const shouldUnload = await callback(event)
ipcRenderer.send('did-prepare-to-unload', shouldUnload)
}
ipcRenderer.on('prepare-to-unload', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('prepare-to-unload', outerCallback))
}
onDidChangeHistoryManager (callback) {
const outerCallback = (event, message) => callback(event)
ipcRenderer.on('did-change-history-manager', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('did-change-history-manager', outerCallback))
}
didChangeHistoryManager () {
return ipcRenderer.send('did-change-history-manager')
}
openExternal (url) {
return shell.openExternal(url)
}
checkForUpdate () {
return ipcRenderer.send('command', 'application:check-for-update')
}
restartAndInstallUpdate () {
return ipcRenderer.send('command', 'application:install-update')
}
getAutoUpdateManagerState () {
return ipcRenderer.sendSync('get-auto-update-manager-state')
}
getAutoUpdateManagerErrorMessage () {
return ipcRenderer.sendSync('get-auto-update-manager-error')
}
emitWillSavePath (path) {
return ipcRenderer.sendSync('will-save-path', path)
}
emitDidSavePath (path) {
return ipcRenderer.sendSync('did-save-path', path)
}
resolveProxy (requestId, url) {
return ipcRenderer.send('resolve-proxy', requestId, url)
}
onDidResolveProxy (callback) {
const outerCallback = (event, requestId, proxy) => callback(requestId, proxy)
ipcRenderer.on('did-resolve-proxy', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('did-resolve-proxy', outerCallback))
}
}

View File

@@ -51,13 +51,15 @@ let nextId = 0
//
// An instance of this class is always available as the `atom` global.
class AtomEnvironment {
/*
Section: Construction and Destruction
Section: Properties
*/
// Call .loadOrCreate instead
constructor (params = {}) {
this.id = (params.id != null) ? params.id : nextId++
// Public: A {Clipboard} instance
this.clipboard = params.clipboard
this.updateProcessEnv = params.updateProcessEnv || updateProcessEnv
this.enablePersistence = params.enablePersistence
@@ -68,26 +70,44 @@ class AtomEnvironment {
this.loadTime = null
this.emitter = new Emitter()
this.disposables = new CompositeDisposable()
this.pathsWithWaitSessions = new Set()
// Public: A {DeserializerManager} instance
this.deserializers = new DeserializerManager(this)
this.deserializeTimings = {}
// Public: A {ViewRegistry} instance
this.views = new ViewRegistry(this)
TextEditor.setScheduler(this.views)
// Public: A {NotificationManager} instance
this.notifications = new NotificationManager()
this.stateStore = new StateStore('AtomEnvironments', 1)
// Public: A {Config} instance
this.config = new Config({
notificationManager: this.notifications,
enablePersistence: this.enablePersistence
})
this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)})
// Public: A {KeymapManager} instance
this.keymaps = new KeymapManager({notificationManager: this.notifications})
// Public: A {TooltipManager} instance
this.tooltips = new TooltipManager({keymapManager: this.keymaps, viewRegistry: this.views})
// Public: A {CommandRegistry} instance
this.commands = new CommandRegistry()
this.uriHandlerRegistry = new URIHandlerRegistry()
// Public: A {GrammarRegistry} instance
this.grammars = new GrammarRegistry({config: this.config})
// Public: A {StyleManager} instance
this.styles = new StyleManager()
// Public: A {PackageManager} instance
this.packages = new PackageManager({
config: this.config,
styleManager: this.styles,
@@ -99,6 +119,8 @@ class AtomEnvironment {
viewRegistry: this.views,
uriHandlerRegistry: this.uriHandlerRegistry
})
// Public: A {ThemeManager} instance
this.themes = new ThemeManager({
packageManager: this.packages,
config: this.config,
@@ -106,16 +128,29 @@ class AtomEnvironment {
notificationManager: this.notifications,
viewRegistry: this.views
})
// Public: A {MenuManager} instance
this.menu = new MenuManager({keymapManager: this.keymaps, packageManager: this.packages})
// Public: A {ContextMenuManager} instance
this.contextMenu = new ContextMenuManager({keymapManager: this.keymaps})
this.packages.setMenuManager(this.menu)
this.packages.setContextMenuManager(this.contextMenu)
this.packages.setThemeManager(this.themes)
this.project = new Project({notificationManager: this.notifications, packageManager: this.packages, config: this.config, applicationDelegate: this.applicationDelegate})
// Public: A {Project} instance
this.project = new Project({
notificationManager: this.notifications,
packageManager: this.packages,
grammarRegistry: this.grammars,
config: this.config,
applicationDelegate: this.applicationDelegate
})
this.commandInstaller = new CommandInstaller(this.applicationDelegate)
this.protocolHandlerInstaller = new ProtocolHandlerInstaller()
// Public: A {TextEditorRegistry} instance
this.textEditors = new TextEditorRegistry({
config: this.config,
grammarRegistry: this.grammars,
@@ -123,6 +158,7 @@ class AtomEnvironment {
packageManager: this.packages
})
// Public: A {Workspace} instance
this.workspace = new Workspace({
config: this.config,
project: this.project,
@@ -152,7 +188,9 @@ class AtomEnvironment {
this.windowEventHandler = new WindowEventHandler({atomEnvironment: this, applicationDelegate: this.applicationDelegate})
// Public: A {HistoryManager} instance
this.history = new HistoryManager({project: this.project, commands: this.commands, stateStore: this.stateStore})
// Keep instances of HistoryManager in sync
this.disposables.add(this.history.onDidChangeProjects(event => {
if (!event.reloaded) this.applicationDelegate.didChangeHistoryManager()
@@ -201,12 +239,13 @@ class AtomEnvironment {
this.themes.initialize({configDirPath: this.configDirPath, resourcePath, safeMode, devMode})
this.commandInstaller.initialize(this.getVersion())
this.protocolHandlerInstaller.initialize(this.config, this.notifications)
this.uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this))
this.autoUpdater.initialize()
this.config.load()
this.protocolHandlerInstaller.initialize(this.config, this.notifications)
this.themes.loadBaseStylesheets()
this.initialStyleElements = this.styles.getSnapshot()
if (params.onlyLoadBaseStyleSheets) this.themes.initialLoadComplete = true
@@ -321,6 +360,7 @@ class AtomEnvironment {
this.grammars.clear()
this.textEditors.clear()
this.views.clear()
this.pathsWithWaitSessions.clear()
}
destroy () {
@@ -333,7 +373,7 @@ class AtomEnvironment {
if (this.project) this.project.destroy()
this.project = null
this.commands.clear()
this.stylesElement.remove()
if (this.stylesElement) this.stylesElement.remove()
this.config.unobserveUserConfig()
this.autoUpdater.destroy()
this.uriHandlerRegistry.destroy()
@@ -784,7 +824,22 @@ class AtomEnvironment {
this.document.body.appendChild(this.workspace.getElement())
if (this.backgroundStylesheet) this.backgroundStylesheet.remove()
this.watchProjectPaths()
let previousProjectPaths = this.project.getPaths()
this.disposables.add(this.project.onDidChangePaths(newPaths => {
for (let path of previousProjectPaths) {
if (this.pathsWithWaitSessions.has(path) && !newPaths.includes(path)) {
this.applicationDelegate.didClosePathWithWaitSession(path)
}
}
previousProjectPaths = newPaths
this.applicationDelegate.setRepresentedDirectoryPaths(newPaths)
}))
this.disposables.add(this.workspace.onDidDestroyPaneItem(({item}) => {
const path = item.getPath && item.getPath()
if (this.pathsWithWaitSessions.has(path)) {
this.applicationDelegate.didClosePathWithWaitSession(path)
}
}))
this.packages.activate()
this.keymaps.loadUserKeymap()
@@ -815,10 +870,9 @@ class AtomEnvironment {
project: this.project.serialize(options),
workspace: this.workspace.serialize(),
packageStates: this.packages.serialize(),
grammars: {grammarOverridesByPath: this.grammars.grammarOverridesByPath},
grammars: this.grammars.serialize(),
fullScreen: this.isFullScreen(),
windowDimensions: this.windowDimensions,
textEditors: this.textEditors.serialize()
windowDimensions: this.windowDimensions
}
}
@@ -911,29 +965,63 @@ class AtomEnvironment {
// Essential: A flexible way to open a dialog akin to an alert dialog.
//
// While both async and sync versions are provided, it is recommended to use the async version
// such that the renderer process is not blocked while the dialog box is open.
//
// The async version accepts the same options as Electron's `dialog.showMessageBox`.
// For convenience, it sets `type` to `'info'` and `normalizeAccessKeys` to `true` by default.
//
// If the dialog is closed (via `Esc` key or `X` in the top corner) without selecting a button
// the first button will be clicked unless a "Cancel" or "No" button is provided.
//
// ## Examples
//
// ```coffee
// atom.confirm
// message: 'How you feeling?'
// detailedMessage: 'Be honest.'
// buttons:
// Good: -> window.alert('good to hear')
// Bad: -> window.alert('bummer')
// ```js
// // Async version (recommended)
// atom.confirm({
// message: 'How you feeling?',
// detail: 'Be honest.',
// buttons: ['Good', 'Bad']
// }, response => {
// if (response === 0) {
// window.alert('good to hear')
// } else {
// window.alert('bummer')
// }
// })
//
// ```js
// // Legacy sync version
// const chosen = atom.confirm({
// message: 'How you feeling?',
// detailedMessage: 'Be honest.',
// buttons: {
// Good: () => window.alert('good to hear'),
// Bad: () => window.alert('bummer')
// }
// })
// ```
//
// * `options` An {Object} with the following keys:
// * `options` An options {Object}. If the callback argument is also supplied, see the documentation at
// https://electronjs.org/docs/api/dialog#dialogshowmessageboxbrowserwindow-options-callback for the list of
// available options. Otherwise, only the following keys are accepted:
// * `message` The {String} message to display.
// * `detailedMessage` (optional) The {String} detailed message to display.
// * `buttons` (optional) Either an array of strings or an object where keys are
// button names and the values are callbacks to invoke when clicked.
// * `buttons` (optional) Either an {Array} of {String}s or an {Object} where keys are
// button names and the values are callback {Function}s to invoke when clicked.
// * `callback` (optional) A {Function} that will be called with the index of the chosen option.
// If a callback is supplied, the dialog will be non-blocking. This argument is recommended.
//
// Returns the chosen button index {Number} if the buttons option is an array or the return value of the callback if the buttons option is an object.
confirm (params = {}) {
return this.applicationDelegate.confirm(params)
// Returns the chosen button index {Number} if the buttons option is an array
// or the return value of the callback if the buttons option is an object.
// If a callback function is supplied, returns `undefined`.
confirm (options = {}, callback) {
if (callback) {
// Async: no return value
this.applicationDelegate.confirm(options, callback)
} else {
return this.applicationDelegate.confirm(options)
}
}
/*
@@ -988,13 +1076,6 @@ class AtomEnvironment {
return this.themes.load()
}
// Notify the browser project of the window's current project path
watchProjectPaths () {
this.disposables.add(this.project.onDidChangePaths(() => {
this.applicationDelegate.setRepresentedDirectoryPaths(this.project.getPaths())
}))
}
setDocumentEdited (edited) {
if (typeof this.applicationDelegate.setWindowDocumentEdited === 'function') {
this.applicationDelegate.setWindowDocumentEdited(edited)
@@ -1008,8 +1089,10 @@ class AtomEnvironment {
}
addProjectFolder () {
this.pickFolder((selectedPaths = []) => {
this.addToProject(selectedPaths)
return new Promise((resolve) => {
this.pickFolder((selectedPaths) => {
this.addToProject(selectedPaths || []).then(resolve)
})
})
}
@@ -1022,7 +1105,7 @@ class AtomEnvironment {
}
}
attemptRestoreProjectStateForPaths (state, projectPaths, filesToOpen = []) {
async attemptRestoreProjectStateForPaths (state, projectPaths, filesToOpen = []) {
const center = this.workspace.getCenter()
const windowIsUnused = () => {
for (let container of this.workspace.getPaneContainers()) {
@@ -1038,33 +1121,41 @@ class AtomEnvironment {
}
if (windowIsUnused()) {
this.restoreStateIntoThisEnvironment(state)
await this.restoreStateIntoThisEnvironment(state)
return Promise.all(filesToOpen.map(file => this.workspace.open(file)))
} else {
let resolveDiscardStatePromise = null
const discardStatePromise = new Promise((resolve) => {
resolveDiscardStatePromise = resolve
})
const nouns = projectPaths.length === 1 ? 'folder' : 'folders'
const choice = this.confirm({
this.confirm({
message: 'Previous automatically-saved project state detected',
detailedMessage: `There is previously saved state for the selected ${nouns}. ` +
detail: `There is previously saved state for the selected ${nouns}. ` +
`Would you like to add the ${nouns} to this window, permanently discarding the saved state, ` +
`or open the ${nouns} in a new window, restoring the saved state?`,
buttons: [
'&Open in new window and recover state',
'&Add to this window and discard state'
]})
if (choice === 0) {
this.open({
pathsToOpen: projectPaths.concat(filesToOpen),
newWindow: true,
devMode: this.inDevMode(),
safeMode: this.inSafeMode()
})
return Promise.resolve(null)
} else if (choice === 1) {
for (let selectedPath of projectPaths) {
this.project.addPath(selectedPath)
]
}, response => {
if (response === 0) {
this.open({
pathsToOpen: projectPaths.concat(filesToOpen),
newWindow: true,
devMode: this.inDevMode(),
safeMode: this.inSafeMode()
})
resolveDiscardStatePromise(Promise.resolve(null))
} else if (response === 1) {
for (let selectedPath of projectPaths) {
this.project.addPath(selectedPath)
}
resolveDiscardStatePromise(Promise.all(filesToOpen.map(file => this.workspace.open(file))))
}
return Promise.all(filesToOpen.map(file => this.workspace.open(file)))
}
})
return discardStatePromise
}
}
@@ -1076,12 +1167,11 @@ class AtomEnvironment {
return this.deserialize(state)
}
showSaveDialog (callback) {
callback(this.showSaveDialogSync())
}
showSaveDialogSync (options = {}) {
this.applicationDelegate.showSaveDialog(options)
deprecate(`atom.showSaveDialogSync is deprecated and will be removed soon.
Please, implement ::saveAs and ::getSaveDialogOptions instead for pane items
or use Pane::saveItemAs for programmatic saving.`)
return this.applicationDelegate.showSaveDialog(options)
}
async saveState (options, storageKey) {
@@ -1112,11 +1202,6 @@ class AtomEnvironment {
async deserialize (state) {
if (!state) return Promise.resolve()
const grammarOverridesByPath = state.grammars && state.grammars.grammarOverridesByPath
if (grammarOverridesByPath) {
this.grammars.grammarOverridesByPath = grammarOverridesByPath
}
this.setFullScreen(state.fullScreen)
const missingProjectPaths = []
@@ -1141,7 +1226,7 @@ class AtomEnvironment {
this.deserializeTimings.project = Date.now() - startTime
if (state.textEditors) this.textEditors.deserialize(state.textEditors)
if (state.grammars) this.grammars.deserialize(state.grammars)
startTime = Date.now()
if (state.workspace) this.workspace.deserialize(state.workspace, this.deserializers)
@@ -1266,8 +1351,9 @@ class AtomEnvironment {
}
}
for (var {pathToOpen, initialLine, initialColumn, forceAddToWindow} of locations) {
if (pathToOpen && (needsProjectPaths || forceAddToWindow)) {
for (const location of locations) {
const {pathToOpen} = location
if (pathToOpen && (needsProjectPaths || location.forceAddToWindow)) {
if (fs.existsSync(pathToOpen)) {
pushFolderToOpen(this.project.getDirectoryForProjectPath(pathToOpen).getPath())
} else if (fs.existsSync(path.dirname(pathToOpen))) {
@@ -1278,8 +1364,10 @@ class AtomEnvironment {
}
if (!fs.isDirectorySync(pathToOpen)) {
fileLocationsToOpen.push({pathToOpen, initialLine, initialColumn})
fileLocationsToOpen.push(location)
}
if (location.hasWaitSession) this.pathsWithWaitSessions.add(pathToOpen)
}
let restoredState = false
@@ -1300,7 +1388,7 @@ class AtomEnvironment {
if (!restoredState) {
const fileOpenPromises = []
for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) {
for (const {pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) {
fileOpenPromises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn}))
}
await Promise.all(fileOpenPromises)

View File

@@ -89,6 +89,10 @@ export default class Color {
return this.alpha === 1 ? this.toHexString() : this.toRGBAString()
}
toString () {
return this.toRGBAString()
}
isEqual (color) {
if (this === color) {
return true

View File

@@ -23,8 +23,8 @@ class CommandInstaller {
const showErrorDialog = (error) => {
this.applicationDelegate.confirm({
message: 'Failed to install shell commands',
detailedMessage: error.message
})
detail: error.message
}, () => {})
}
this.installAtomCommand(true, error => {
@@ -33,8 +33,8 @@ class CommandInstaller {
if (error) return showErrorDialog(error)
this.applicationDelegate.confirm({
message: 'Commands installed.',
detailedMessage: 'The shell commands `atom` and `apm` are installed.'
})
detail: 'The shell commands `atom` and `apm` are installed.'
}, () => {})
})
})
}

View File

@@ -309,7 +309,7 @@ module.exports = class CommandRegistry {
handleCommandEvent (event) {
let propagationStopped = false
let immediatePropagationStopped = false
let matched = false
let matched = []
let currentTarget = event.target
const dispatchedEvent = new CustomEvent(event.type, {
@@ -373,10 +373,6 @@ module.exports = class CommandRegistry {
listeners = selectorBasedListeners.concat(listeners)
}
if (listeners.length > 0) {
matched = true
}
// Call inline listeners first in reverse registration order,
// and selector-based listeners by specificity and reverse
// registration order.
@@ -385,7 +381,7 @@ module.exports = class CommandRegistry {
if (immediatePropagationStopped) {
break
}
listener.didDispatch.call(currentTarget, dispatchedEvent)
matched.push(listener.didDispatch.call(currentTarget, dispatchedEvent))
}
if (currentTarget === window) {
@@ -399,7 +395,7 @@ module.exports = class CommandRegistry {
this.emitter.emit('did-dispatch', dispatchedEvent)
return matched
return (matched.length > 0 ? Promise.all(matched) : null)
}
commandRegistered (commandName) {

View File

@@ -342,6 +342,11 @@ const configSchema = {
description: 'Emulated with Atom events'
}
]
},
useTreeSitterParsers: {
type: 'boolean',
default: false,
description: 'Use the new Tree-sitter parsing system for supported languages'
}
}
},

View File

@@ -423,6 +423,7 @@ class Config
@configFileHasErrors = false
@transactDepth = 0
@pendingOperations = []
@legacyScopeAliases = {}
@requestLoad = _.debounce =>
@loadUserConfig()
@@ -599,11 +600,22 @@ class Config
# * `value` The value for the key-path
getAll: (keyPath, options) ->
{scope} = options if options?
result = []
if scope?
scopeDescriptor = ScopeDescriptor.fromObject(scope)
result = result.concat @scopedSettingsStore.getAll(scopeDescriptor.getScopeChain(), keyPath, options)
result = @scopedSettingsStore.getAll(
scopeDescriptor.getScopeChain(),
keyPath,
options
)
if legacyScopeDescriptor = @getLegacyScopeDescriptor(scopeDescriptor)
result.push(@scopedSettingsStore.getAll(
legacyScopeDescriptor.getScopeChain(),
keyPath,
options
)...)
else
result = []
if globalValue = @getRawValue(keyPath, options)
result.push(scopeSelector: '*', value: globalValue)
@@ -762,6 +774,12 @@ class Config
finally
@endTransaction()
addLegacyScopeAlias: (languageId, legacyScopeName) ->
@legacyScopeAliases[languageId] = legacyScopeName
removeLegacyScopeAlias: (languageId) ->
delete @legacyScopeAliases[languageId]
###
Section: Internal methods used by core
###
@@ -1145,7 +1163,20 @@ class Config
getRawScopedValue: (scopeDescriptor, keyPath, options) ->
scopeDescriptor = ScopeDescriptor.fromObject(scopeDescriptor)
@scopedSettingsStore.getPropertyValue(scopeDescriptor.getScopeChain(), keyPath, options)
result = @scopedSettingsStore.getPropertyValue(
scopeDescriptor.getScopeChain(),
keyPath,
options
)
if result?
result
else if legacyScopeDescriptor = @getLegacyScopeDescriptor(scopeDescriptor)
@scopedSettingsStore.getPropertyValue(
legacyScopeDescriptor.getScopeChain(),
keyPath,
options
)
observeScopedKeyPath: (scope, keyPath, callback) ->
callback(@get(keyPath, {scope}))
@@ -1160,6 +1191,13 @@ class Config
oldValue = newValue
callback(event)
getLegacyScopeDescriptor: (scopeDescriptor) ->
legacyAlias = @legacyScopeAliases[scopeDescriptor.scopes[0]]
if legacyAlias
scopes = scopeDescriptor.scopes.slice()
scopes[0] = legacyAlias
new ScopeDescriptor({scopes})
# Base schema enforcers. These will coerce raw input into the specified type,
# and will throw an error when the value cannot be coerced. Throwing the error
# will indicate that the value should not be set.

View File

@@ -1,9 +1,16 @@
// Converts a query string parameter for a line or column number
// to a zero-based line or column number for the Atom API.
function getLineColNumber (numStr) {
const num = parseInt(numStr || 0, 10)
return Math.max(num - 1, 0)
}
function openFile (atom, {query}) {
const {filename, line, column} = query
atom.workspace.open(filename, {
initialLine: parseInt(line || 0, 10),
initialColumn: parseInt(column || 0, 10),
initialLine: getLineColNumber(line),
initialColumn: getLineColNumber(column),
searchAllPanes: true
})
}

View File

@@ -705,7 +705,7 @@ class Cursor extends Model {
*/
getNonWordCharacters () {
return this.editor.getNonWordCharacters(this.getScopeDescriptor().getScopesArray())
return this.editor.getNonWordCharacters(this.getBufferPosition())
}
changePosition (options, fn) {

View File

@@ -34,7 +34,7 @@ export default class DeserializerManager {
// common approach is to register a *constructor* as the deserializer for its
// instances by adding a `.deserialize()` class method. When your method is
// called, it will be passed serialized state as the first argument and the
// {Atom} environment object as the second argument, which is useful if you
// {AtomEnvironment} object as the second argument, which is useful if you
// wish to avoid referencing the `atom` global.
add (...deserializers) {
for (let i = 0; i < deserializers.length; i++) {

View File

@@ -327,12 +327,15 @@ module.exports = class Dock {
// Include all panels that are closer to the edge than the dock in our calculations.
switch (this.location) {
case 'right':
if (!this.isVisible()) bounds.left = bounds.right - 2
bounds.right = Number.POSITIVE_INFINITY
break
case 'bottom':
if (!this.isVisible()) bounds.top = bounds.bottom - 1
bounds.bottom = Number.POSITIVE_INFINITY
break
case 'left':
if (!this.isVisible()) bounds.right = bounds.left + 2
bounds.left = Number.NEGATIVE_INFINITY
break
}

View File

@@ -1,28 +1,165 @@
const _ = require('underscore-plus')
const Grim = require('grim')
const CSON = require('season')
const FirstMate = require('first-mate')
const {Disposable, CompositeDisposable} = require('event-kit')
const TextMateLanguageMode = require('./text-mate-language-mode')
const TreeSitterLanguageMode = require('./tree-sitter-language-mode')
const TreeSitterGrammar = require('./tree-sitter-grammar')
const Token = require('./token')
const fs = require('fs-plus')
const Grim = require('grim')
const {Point, Range} = require('text-buffer')
const PathSplitRegex = new RegExp('[/.]')
const GRAMMAR_TYPE_BONUS = 1000
const PATH_SPLIT_REGEX = new RegExp('[/.]')
// Extended: Syntax class holding the grammars used for tokenizing.
// Extended: This class holds the grammars used for tokenizing.
//
// An instance of this class is always available as the `atom.grammars` global.
//
// The Syntax class also contains properties for things such as the
// language-specific comment regexes. See {::getProperty} for more details.
module.exports =
class GrammarRegistry extends FirstMate.GrammarRegistry {
class GrammarRegistry {
constructor ({config} = {}) {
super({maxTokensPerLine: 100, maxLineLength: 1000})
this.config = config
this.subscriptions = new CompositeDisposable()
this.textmateRegistry = new FirstMate.GrammarRegistry({maxTokensPerLine: 100, maxLineLength: 1000})
this.clear()
}
clear () {
this.textmateRegistry.clear()
this.treeSitterGrammarsById = {}
if (this.subscriptions) this.subscriptions.dispose()
this.subscriptions = new CompositeDisposable()
this.languageOverridesByBufferId = new Map()
this.grammarScoresByBuffer = new Map()
this.textMateScopeNamesByTreeSitterLanguageId = new Map()
this.treeSitterLanguageIdsByTextMateScopeName = new Map()
const grammarAddedOrUpdated = this.grammarAddedOrUpdated.bind(this)
this.textmateRegistry.onDidAddGrammar(grammarAddedOrUpdated)
this.textmateRegistry.onDidUpdateGrammar(grammarAddedOrUpdated)
}
serialize () {
const languageOverridesByBufferId = {}
this.languageOverridesByBufferId.forEach((languageId, bufferId) => {
languageOverridesByBufferId[bufferId] = languageId
})
return {languageOverridesByBufferId}
}
deserialize (params) {
for (const bufferId in params.languageOverridesByBufferId || {}) {
this.languageOverridesByBufferId.set(
bufferId,
params.languageOverridesByBufferId[bufferId]
)
}
}
createToken (value, scopes) {
return new Token({value, scopes})
}
// Extended: set a {TextBuffer}'s language mode based on its path and content,
// and continue to update its language mode as grammars are added or updated, or
// the buffer's file path changes.
//
// * `buffer` The {TextBuffer} whose language mode will be maintained.
//
// Returns a {Disposable} that can be used to stop updating the buffer's
// language mode.
maintainLanguageMode (buffer) {
this.grammarScoresByBuffer.set(buffer, null)
const languageOverride = this.languageOverridesByBufferId.get(buffer.id)
if (languageOverride) {
this.assignLanguageMode(buffer, languageOverride)
} else {
this.autoAssignLanguageMode(buffer)
}
const pathChangeSubscription = buffer.onDidChangePath(() => {
this.grammarScoresByBuffer.delete(buffer)
if (!this.languageOverridesByBufferId.has(buffer.id)) {
this.autoAssignLanguageMode(buffer)
}
})
const destroySubscription = buffer.onDidDestroy(() => {
this.grammarScoresByBuffer.delete(buffer)
this.languageOverridesByBufferId.delete(buffer.id)
this.subscriptions.remove(destroySubscription)
this.subscriptions.remove(pathChangeSubscription)
})
this.subscriptions.add(pathChangeSubscription, destroySubscription)
return new Disposable(() => {
destroySubscription.dispose()
pathChangeSubscription.dispose()
this.subscriptions.remove(pathChangeSubscription)
this.subscriptions.remove(destroySubscription)
this.grammarScoresByBuffer.delete(buffer)
this.languageOverridesByBufferId.delete(buffer.id)
})
}
// Extended: Force a {TextBuffer} to use a different grammar than the
// one that would otherwise be selected for it.
//
// * `buffer` The {TextBuffer} whose grammar will be set.
// * `languageId` The {String} id of the desired language.
//
// Returns a {Boolean} that indicates whether the language was successfully
// found.
assignLanguageMode (buffer, languageId) {
if (buffer.getBuffer) buffer = buffer.getBuffer()
languageId = this.normalizeLanguageId(languageId)
let grammar = null
if (languageId != null) {
grammar = this.grammarForId(languageId)
if (!grammar) return false
this.languageOverridesByBufferId.set(buffer.id, languageId)
} else {
this.languageOverridesByBufferId.set(buffer.id, null)
grammar = this.textmateRegistry.nullGrammar
}
this.grammarScoresByBuffer.set(buffer, null)
if (grammar.scopeName !== buffer.getLanguageMode().getLanguageId()) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
}
return true
}
// Extended: Remove any language mode override that has been set for the
// given {TextBuffer}. This will assign to the buffer the best language
// mode available.
//
// * `buffer` The {TextBuffer}.
autoAssignLanguageMode (buffer) {
const result = this.selectGrammarWithScore(
buffer.getPath(),
getGrammarSelectionContent(buffer)
)
this.languageOverridesByBufferId.delete(buffer.id)
this.grammarScoresByBuffer.set(buffer, result.score)
if (result.grammar.scopeName !== buffer.getLanguageMode().getLanguageId()) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(result.grammar, buffer))
}
}
languageModeForGrammarAndBuffer (grammar, buffer) {
if (grammar instanceof TreeSitterGrammar) {
return new TreeSitterLanguageMode({grammar, buffer, config: this.config})
} else {
return new TextMateLanguageMode({grammar, buffer, config: this.config})
}
}
// Extended: Select a grammar for the given file path and file contents.
//
// This picks the best match by checking the file path and contents against
@@ -39,39 +176,44 @@ class GrammarRegistry extends FirstMate.GrammarRegistry {
selectGrammarWithScore (filePath, fileContents) {
let bestMatch = null
let highestScore = -Infinity
for (let grammar of this.grammars) {
this.forEachGrammar(grammar => {
const score = this.getGrammarScore(grammar, filePath, fileContents)
if ((score > highestScore) || (bestMatch == null)) {
if (score > highestScore || bestMatch == null) {
bestMatch = grammar
highestScore = score
}
}
})
return {grammar: bestMatch, score: highestScore}
}
// Extended: Returns a {Number} representing how well the grammar matches the
// `filePath` and `contents`.
getGrammarScore (grammar, filePath, contents) {
if ((contents == null) && fs.isFileSync(filePath)) {
if (contents == null && fs.isFileSync(filePath)) {
contents = fs.readFileSync(filePath, 'utf8')
}
let score = this.getGrammarPathScore(grammar, filePath)
if ((score > 0) && !grammar.bundledPackage) {
if (score > 0 && !grammar.bundledPackage) {
score += 0.125
}
if (this.grammarMatchesContents(grammar, contents)) {
score += 0.25
}
if (score > 0 && this.isGrammarPreferredType(grammar)) {
score += GRAMMAR_TYPE_BONUS
}
return score
}
getGrammarPathScore (grammar, filePath) {
if (!filePath) { return -1 }
if (!filePath) return -1
if (process.platform === 'win32') { filePath = filePath.replace(/\\/g, '/') }
const pathComponents = filePath.toLowerCase().split(PathSplitRegex)
let pathScore = -1
const pathComponents = filePath.toLowerCase().split(PATH_SPLIT_REGEX)
let pathScore = 0
let customFileTypes
if (this.config.get('core.customFileTypes')) {
@@ -85,7 +227,7 @@ class GrammarRegistry extends FirstMate.GrammarRegistry {
for (let i = 0; i < fileTypes.length; i++) {
const fileType = fileTypes[i]
const fileTypeComponents = fileType.toLowerCase().split(PathSplitRegex)
const fileTypeComponents = fileType.toLowerCase().split(PATH_SPLIT_REGEX)
const pathSuffix = pathComponents.slice(-fileTypeComponents.length)
if (_.isEqual(pathSuffix, fileTypeComponents)) {
pathScore = Math.max(pathScore, fileType.length)
@@ -99,25 +241,48 @@ class GrammarRegistry extends FirstMate.GrammarRegistry {
}
grammarMatchesContents (grammar, contents) {
if ((contents == null) || (grammar.firstLineRegex == null)) { return false }
if (contents == null) return false
let escaped = false
let numberOfNewlinesInRegex = 0
for (let character of grammar.firstLineRegex.source) {
switch (character) {
case '\\':
escaped = !escaped
break
case 'n':
if (escaped) { numberOfNewlinesInRegex++ }
escaped = false
break
default:
escaped = false
if (grammar.contentRegExp) { // TreeSitter grammars
return grammar.contentRegExp.test(contents)
} else if (grammar.firstLineRegex) { // FirstMate grammars
let escaped = false
let numberOfNewlinesInRegex = 0
for (let character of grammar.firstLineRegex.source) {
switch (character) {
case '\\':
escaped = !escaped
break
case 'n':
if (escaped) { numberOfNewlinesInRegex++ }
escaped = false
break
default:
escaped = false
}
}
const lines = contents.split('\n')
return grammar.firstLineRegex.testSync(lines.slice(0, numberOfNewlinesInRegex + 1).join('\n'))
} else {
return false
}
const lines = contents.split('\n')
return grammar.firstLineRegex.testSync(lines.slice(0, numberOfNewlinesInRegex + 1).join('\n'))
}
forEachGrammar (callback) {
this.textmateRegistry.grammars.forEach(callback)
for (let grammarId in this.treeSitterGrammarsById) {
callback(this.treeSitterGrammarsById[grammarId])
}
}
grammarForId (languageId) {
languageId = this.normalizeLanguageId(languageId)
return (
this.textmateRegistry.grammarForScopeName(languageId) ||
this.treeSitterGrammarsById[languageId]
)
}
// Deprecated: Get the grammar override for the given file path.
@@ -126,46 +291,228 @@ class GrammarRegistry extends FirstMate.GrammarRegistry {
//
// Returns a {String} such as `"source.js"`.
grammarOverrideForPath (filePath) {
Grim.deprecate('Use atom.textEditors.getGrammarOverride(editor) instead')
const editor = getEditorForPath(filePath)
if (editor) {
return atom.textEditors.getGrammarOverride(editor)
}
Grim.deprecate('Use buffer.getLanguageMode().getLanguageId() instead')
const buffer = atom.project.findBufferForPath(filePath)
if (buffer) return this.languageOverridesByBufferId.get(buffer.id)
}
// Deprecated: Set the grammar override for the given file path.
//
// * `filePath` A non-empty {String} file path.
// * `scopeName` A {String} such as `"source.js"`.
// * `languageId` A {String} such as `"source.js"`.
//
// Returns undefined.
setGrammarOverrideForPath (filePath, scopeName) {
Grim.deprecate('Use atom.textEditors.setGrammarOverride(editor, scopeName) instead')
const editor = getEditorForPath(filePath)
if (editor) {
atom.textEditors.setGrammarOverride(editor, scopeName)
setGrammarOverrideForPath (filePath, languageId) {
Grim.deprecate('Use atom.grammars.assignLanguageMode(buffer, languageId) instead')
const buffer = atom.project.findBufferForPath(filePath)
if (buffer) {
const grammar = this.grammarForScopeName(languageId)
if (grammar) this.languageOverridesByBufferId.set(buffer.id, grammar.name)
}
}
// Deprecated: Remove the grammar override for the given file path.
// Remove the grammar override for the given file path.
//
// * `filePath` A {String} file path.
//
// Returns undefined.
clearGrammarOverrideForPath (filePath) {
Grim.deprecate('Use atom.textEditors.clearGrammarOverride(editor) instead')
Grim.deprecate('Use atom.grammars.autoAssignLanguageMode(buffer) instead')
const buffer = atom.project.findBufferForPath(filePath)
if (buffer) this.languageOverridesByBufferId.delete(buffer.id)
}
const editor = getEditorForPath(filePath)
if (editor) {
atom.textEditors.clearGrammarOverride(editor)
grammarAddedOrUpdated (grammar) {
if (grammar.scopeName && !grammar.id) grammar.id = grammar.scopeName
this.grammarScoresByBuffer.forEach((score, buffer) => {
const languageMode = buffer.getLanguageMode()
if (grammar.injectionSelector) {
if (languageMode.hasTokenForSelector(grammar.injectionSelector)) {
languageMode.retokenizeLines()
}
return
}
const languageOverride = this.languageOverridesByBufferId.get(buffer.id)
if ((grammar.id === buffer.getLanguageMode().getLanguageId() ||
grammar.id === languageOverride)) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
} else if (!languageOverride) {
const score = this.getGrammarScore(grammar, buffer.getPath(), getGrammarSelectionContent(buffer))
const currentScore = this.grammarScoresByBuffer.get(buffer)
if (currentScore == null || score > currentScore) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
this.grammarScoresByBuffer.set(buffer, score)
}
}
})
}
// Extended: Invoke the given callback when a grammar is added to the registry.
//
// * `callback` {Function} to call when a grammar is added.
// * `grammar` {Grammar} that was added.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidAddGrammar (callback) {
return this.textmateRegistry.onDidAddGrammar(callback)
}
// Extended: Invoke the given callback when a grammar is updated due to a grammar
// it depends on being added or removed from the registry.
//
// * `callback` {Function} to call when a grammar is updated.
// * `grammar` {Grammar} that was updated.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidUpdateGrammar (callback) {
return this.textmateRegistry.onDidUpdateGrammar(callback)
}
get nullGrammar () {
return this.textmateRegistry.nullGrammar
}
get grammars () {
return this.textmateRegistry.grammars
}
decodeTokens () {
return this.textmateRegistry.decodeTokens.apply(this.textmateRegistry, arguments)
}
grammarForScopeName (scopeName) {
return this.grammarForId(scopeName)
}
addGrammar (grammar) {
if (grammar instanceof TreeSitterGrammar) {
this.treeSitterGrammarsById[grammar.id] = grammar
if (grammar.legacyScopeName) {
this.config.addLegacyScopeAlias(grammar.id, grammar.legacyScopeName)
this.textMateScopeNamesByTreeSitterLanguageId.set(grammar.id, grammar.legacyScopeName)
this.treeSitterLanguageIdsByTextMateScopeName.set(grammar.legacyScopeName, grammar.id)
}
this.grammarAddedOrUpdated(grammar)
return new Disposable(() => this.removeGrammar(grammar))
} else {
return this.textmateRegistry.addGrammar(grammar)
}
}
removeGrammar (grammar) {
if (grammar instanceof TreeSitterGrammar) {
delete this.treeSitterGrammarsById[grammar.id]
if (grammar.legacyScopeName) {
this.config.removeLegacyScopeAlias(grammar.id)
this.textMateScopeNamesByTreeSitterLanguageId.delete(grammar.id)
this.treeSitterLanguageIdsByTextMateScopeName.delete(grammar.legacyScopeName)
}
} else {
return this.textmateRegistry.removeGrammar(grammar)
}
}
removeGrammarForScopeName (scopeName) {
return this.textmateRegistry.removeGrammarForScopeName(scopeName)
}
// Extended: Read a grammar asynchronously and add it to the registry.
//
// * `grammarPath` A {String} absolute file path to a grammar file.
// * `callback` A {Function} to call when loaded with the following arguments:
// * `error` An {Error}, may be null.
// * `grammar` A {Grammar} or null if an error occured.
loadGrammar (grammarPath, callback) {
this.readGrammar(grammarPath, (error, grammar) => {
if (error) return callback(error)
this.addGrammar(grammar)
callback(grammar)
})
}
// Extended: Read a grammar synchronously and add it to this registry.
//
// * `grammarPath` A {String} absolute file path to a grammar file.
//
// Returns a {Grammar}.
loadGrammarSync (grammarPath) {
const grammar = this.readGrammarSync(grammarPath)
this.addGrammar(grammar)
return grammar
}
// Extended: Read a grammar asynchronously but don't add it to the registry.
//
// * `grammarPath` A {String} absolute file path to a grammar file.
// * `callback` A {Function} to call when read with the following arguments:
// * `error` An {Error}, may be null.
// * `grammar` A {Grammar} or null if an error occured.
//
// Returns undefined.
readGrammar (grammarPath, callback) {
if (!callback) callback = () => {}
CSON.readFile(grammarPath, (error, params = {}) => {
if (error) return callback(error)
try {
callback(null, this.createGrammar(grammarPath, params))
} catch (error) {
callback(error)
}
})
}
// Extended: Read a grammar synchronously but don't add it to the registry.
//
// * `grammarPath` A {String} absolute file path to a grammar file.
//
// Returns a {Grammar}.
readGrammarSync (grammarPath) {
return this.createGrammar(grammarPath, CSON.readFileSync(grammarPath) || {})
}
createGrammar (grammarPath, params) {
if (params.type === 'tree-sitter') {
return new TreeSitterGrammar(this, grammarPath, params)
} else {
if (typeof params.scopeName !== 'string' || params.scopeName.length === 0) {
throw new Error(`Grammar missing required scopeName property: ${grammarPath}`)
}
return this.textmateRegistry.createGrammar(grammarPath, params)
}
}
// Extended: Get all the grammars in this registry.
//
// Returns a non-empty {Array} of {Grammar} instances.
getGrammars () {
return this.textmateRegistry.getGrammars()
}
scopeForId (id) {
return this.textmateRegistry.scopeForId(id)
}
isGrammarPreferredType (grammar) {
return this.config.get('core.useTreeSitterParsers')
? grammar instanceof TreeSitterGrammar
: grammar instanceof FirstMate.Grammar
}
normalizeLanguageId (languageId) {
if (this.config.get('core.useTreeSitterParsers')) {
return this.treeSitterLanguageIdsByTextMateScopeName.get(languageId) || languageId
} else {
return this.textMateScopeNamesByTreeSitterLanguageId.get(languageId) || languageId
}
}
}
function getEditorForPath (filePath) {
if (filePath != null) {
return atom.workspace.getTextEditors().find(editor => editor.getPath() === filePath)
}
function getGrammarSelectionContent (buffer) {
return buffer.getTextInRange(Range(
Point(0, 0),
buffer.positionForCharacterIndex(1024)
))
}

View File

@@ -50,8 +50,8 @@ export class HistoryManager {
return this.emitter.on('did-change-projects', callback)
}
didChangeProjects (args) {
this.emitter.emit('did-change-projects', args || { reloaded: false })
didChangeProjects (args = {reloaded: false}) {
this.emitter.emit('did-change-projects', args)
}
async addProject (paths, lastOpened) {
@@ -93,7 +93,7 @@ export class HistoryManager {
}
async loadState () {
let history = await this.stateStore.load('history-manager')
const history = await this.stateStore.load('history-manager')
if (history && history.projects) {
this.projects = history.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened)))
this.didChangeProjects({reloaded: true})

View File

@@ -67,6 +67,7 @@ global.atom = new AtomEnvironment({
enablePersistence: true
})
TextEditor.setScheduler(global.atom.views)
global.atom.preloadPackages()
# Like sands through the hourglass, so are the days of our lives.

View File

@@ -82,6 +82,7 @@ module.exports = ({blobStore}) ->
params.onlyLoadBaseStyleSheets = true unless params.hasOwnProperty("onlyLoadBaseStyleSheets")
atomEnvironment = new AtomEnvironment(params)
atomEnvironment.initialize(params)
TextEditor.setScheduler(atomEnvironment.views)
atomEnvironment
promise = testRunner({

View File

@@ -1,161 +0,0 @@
{app, Menu} = require 'electron'
_ = require 'underscore-plus'
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) ->
@windowTemplates = new WeakMap()
@setActiveTemplate(@getDefaultTemplate())
@autoUpdateManager.on 'state-changed', (state) => @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) ->
@translateTemplate(template, keystrokesByCommand)
@substituteVersion(template)
@windowTemplates.set(window, template)
@setActiveTemplate(template) if window is @lastFocusedWindow
setActiveTemplate: (template) ->
unless _.isEqual(template, @activeTemplate)
@activeTemplate = template
@menu = Menu.buildFromTemplate(_.deepClone(template))
Menu.setApplicationMenu(@menu)
@showUpdateMenuItem(@autoUpdateManager.getState())
# Register a BrowserWindow with this application menu.
addWindow: (window) ->
@lastFocusedWindow ?= window
focusHandler = =>
@lastFocusedWindow = window
if template = @windowTemplates.get(window)
@setActiveTemplate(template)
window.on 'focus', focusHandler
window.once 'closed', =>
@lastFocusedWindow = null if window is @lastFocusedWindow
@windowTemplates.delete(window)
window.removeListener 'focus', focusHandler
@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) ->
items = []
for index, item of menu.items or {}
items.push(item)
items = items.concat(@flattenMenuItems(item.submenu)) if item.submenu
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) ->
items = []
for item in template
items.push(item)
items = items.concat(@flattenMenuTemplate(item.submenu)) if item.submenu
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 item in @flattenMenuItems(@menu)
item.enabled = enable if item.metadata?.windowSpecific
return
# Replaces VERSION with the current version.
substituteVersion: (template) ->
if (item = _.find(@flattenMenuTemplate(template), ({label}) -> label is 'VERSION'))
item.label = "Version #{@version}"
# Sets the proper visible state the update menu items
showUpdateMenuItem: (state) ->
checkForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Check for Update')
checkingForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Checking for Update')
downloadingUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Downloading Update')
installUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Restart and Install Update')
return unless checkForUpdateItem? and checkingForUpdateItem? and downloadingUpdateItem? and installUpdateItem?
checkForUpdateItem.visible = false
checkingForUpdateItem.visible = false
downloadingUpdateItem.visible = false
installUpdateItem.visible = false
switch state
when 'idle', 'error', 'no-update-available'
checkForUpdateItem.visible = true
when 'checking'
checkingForUpdateItem.visible = true
when 'downloading'
downloadingUpdateItem.visible = true
when 'update-available'
installUpdateItem.visible = true
# Default list of menu items.
#
# Returns an Array of menu item Objects.
getDefaultTemplate: ->
[
label: "Atom"
submenu: [
{label: "Check for Update", metadata: {autoUpdate: true}}
{label: 'Reload', accelerator: 'Command+R', click: => @focusedWindow()?.reload()}
{label: 'Close Window', accelerator: 'Command+Shift+W', click: => @focusedWindow()?.close()}
{label: 'Toggle Dev Tools', accelerator: 'Command+Alt+I', click: => @focusedWindow()?.toggleDevTools()}
{label: 'Quit', accelerator: 'Command+Q', click: -> app.quit()}
]
]
focusedWindow: ->
_.find global.atomApplication.getAllWindows(), (atomWindow) -> atomWindow.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) =>
item.metadata ?= {}
if item.command
item.accelerator = @acceleratorForCommand(item.command, keystrokesByCommand)
item.click = -> global.atomApplication.sendCommand(item.command, item.commandDetail)
item.metadata.windowSpecific = true unless /^application:/.test(item.command, item.commandDetail)
@translateTemplate(item.submenu, keystrokesByCommand) if item.submenu
template
# Determine the accelerator for a given command.
#
# command - The name of the command.
# keystrokesByCommand - An Object where the keys are commands and the values
# are Arrays containing the keystroke.
#
# Returns a String containing the keystroke in a format that can be interpreted
# by Electron to provide nice icons where available.
acceleratorForCommand: (command, keystrokesByCommand) ->
firstKeystroke = keystrokesByCommand[command]?[0]
MenuHelpers.acceleratorForKeystroke(firstKeystroke)

View File

@@ -0,0 +1,225 @@
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(({label}) => label === 'Check for Update')
const checkingForUpdateItem = items.find(({label}) => label === 'Checking for Update')
const downloadingUpdateItem = items.find(({label}) => label === 'Downloading Update')
const installUpdateItem = items.find(({label}) => label === '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',
submenu: [
{
label: 'Check for Update',
metadata: {autoUpdate: true}
},
{
label: 'Reload',
accelerator: 'Command+R',
click: () => {
const window = this.focusedWindow()
if (window) window.reload()
}
},
{
label: 'Close Window',
accelerator: 'Command+Shift+W',
click: () => {
const window = this.focusedWindow()
if (window) window.close()
}
},
{
label: 'Toggle Dev Tools',
accelerator: 'Command+Alt+I',
click: () => {
const window = this.focusedWindow()
if (window) window.toggleDevTools()
}
},
{
label: '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) {
item.accelerator = this.acceleratorForCommand(item.command, keystrokesByCommand)
item.click = () => global.atomApplication.sendCommand(item.command, item.commandDetail)
if (!/^application:/.test(item.command, item.commandDetail)) {
item.metadata.windowSpecific = true
}
}
if (item.submenu) this.translateTemplate(item.submenu, keystrokesByCommand)
})
return template
}
// Determine the accelerator for a given command.
//
// command - The name of the command.
// keystrokesByCommand - An Object where the keys are commands and the values
// are Arrays containing the keystroke.
//
// Returns a String containing the keystroke in a format that can be interpreted
// by Electron to provide nice icons where available.
acceleratorForCommand (command, keystrokesByCommand) {
const firstKeystroke = keystrokesByCommand[command] && keystrokesByCommand[command][0]
return MenuHelpers.acceleratorForKeystroke(firstKeystroke)
}
}

View File

@@ -1,917 +0,0 @@
AtomWindow = require './atom-window'
ApplicationMenu = require './application-menu'
AtomProtocolHandler = require './atom-protocol-handler'
AutoUpdateManager = require './auto-update-manager'
StorageFolder = require '../storage-folder'
Config = require '../config'
FileRecoveryService = require './file-recovery-service'
ipcHelpers = require '../ipc-helpers'
{BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron'
{CompositeDisposable, Disposable} = require 'event-kit'
fs = require 'fs-plus'
path = require 'path'
os = require 'os'
net = require 'net'
url = require 'url'
{EventEmitter} = require 'events'
_ = require 'underscore-plus'
FindParentDir = null
Resolve = null
ConfigSchema = require '../config-schema'
LocationSuffixRegExp = /(:\d+)(:\d+)?$/
# 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
Object.assign @prototype, EventEmitter.prototype
# Public: The entry point into the Atom application.
@open: (options) ->
unless options.socketPath?
if process.platform is 'win32'
userNameSafe = new Buffer(process.env.USERNAME).toString('base64')
options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{userNameSafe}-#{process.arch}-sock"
else
options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.env.USER}.sock")
# 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 atom-shell, before it's fixed we check the existence of socketPath to
# speedup startup.
if (process.platform isnt 'win32' and not fs.existsSync options.socketPath) or options.test or options.benchmark or options.benchmarkTest
new AtomApplication(options).initialize(options)
return
client = net.connect {path: options.socketPath}, ->
client.write JSON.stringify(options), ->
client.end()
app.quit()
client.on 'error', -> new AtomApplication(options).initialize(options)
windows: null
applicationMenu: null
atomProtocolHandler: null
resourcePath: null
version: null
quitting: false
exit: (status) -> app.exit(status)
constructor: (options) ->
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, @logFile, @userDataDir} = options
@socketPath = null if options.test or options.benchmark or options.benchmarkTest
@pidsToOpenWindows = {}
@windowStack = new WindowStack()
@config = new Config({enablePersistence: true})
@config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)}
ConfigSchema.projectHome = {
type: 'string',
default: path.join(fs.getHomeDirectory(), 'github'),
description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.'
}
@config.initialize({configDirPath: process.env.ATOM_HOME, @resourcePath, projectHomeSchema: ConfigSchema.projectHome})
@config.load()
@fileRecoveryService = new FileRecoveryService(path.join(process.env.ATOM_HOME, "recovery"))
@storageFolder = new StorageFolder(process.env.ATOM_HOME)
@autoUpdateManager = new AutoUpdateManager(
@version,
options.test or options.benchmark or options.benchmarkTest,
@config
)
@disposable = new CompositeDisposable
@handleEvents()
# 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.
initialize: (options) ->
global.atomApplication = this
# DEPRECATED: This can be removed at some point (added in 1.13)
# It converts `useCustomTitleBar: true` to `titleBar: "custom"`
if process.platform is 'darwin' and @config.get('core.useCustomTitleBar')
@config.unset('core.useCustomTitleBar')
@config.set('core.titleBar', 'custom')
@config.onDidChange 'core.titleBar', @promptForRestart.bind(this)
process.nextTick => @autoUpdateManager.initialize()
@applicationMenu = new ApplicationMenu(@version, @autoUpdateManager)
@atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode)
@listenForArgumentsFromNewProcess()
@setupDockMenu()
@launch(options)
destroy: ->
windowsClosePromises = @getAllWindows().map (window) ->
window.close()
window.closedPromise
Promise.all(windowsClosePromises).then(=> @disposable.dispose())
launch: (options) ->
if options.test or options.benchmark or options.benchmarkTest
@openWithOptions(options)
else if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0
if @config.get('core.restorePreviousWindowsOnStart') is 'always'
@loadState(_.deepClone(options))
@openWithOptions(options)
else
@loadState(options) or @openPath(options)
openWithOptions: (options) ->
{
initialPaths, pathsToOpen, executedFrom, urlsToOpen, benchmark,
benchmarkTest, test, pidToKillWhenClosed, devMode, safeMode, newWindow,
logFile, profileStartup, timeout, clearWindowState, addToLastWindow, env
} = options
app.focus()
if test
@runTests({
headless: true, devMode, @resourcePath, executedFrom, pathsToOpen,
logFile, timeout, env
})
else if benchmark or benchmarkTest
@runBenchmarks({headless: true, test: benchmarkTest, @resourcePath, executedFrom, pathsToOpen, timeout, env})
else if pathsToOpen.length > 0
@openPaths({
initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow,
devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env
})
else if urlsToOpen.length > 0
for urlToOpen in urlsToOpen
@openUrl({urlToOpen, devMode, safeMode, env})
else
# Always open a editor window if this is the first instance of Atom.
@openPath({
initialPaths, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup,
clearWindowState, addToLastWindow, env
})
# Public: Removes the {AtomWindow} from the global window list.
removeWindow: (window) ->
@windowStack.removeWindow(window)
if @getAllWindows().length is 0
@applicationMenu?.enableWindowSpecificItems(false)
if process.platform in ['win32', 'linux']
app.quit()
return
@saveState(true) unless window.isSpec
# Public: Adds the {AtomWindow} to the global window list.
addWindow: (window) ->
@windowStack.addWindow(window)
@applicationMenu?.addWindow(window.browserWindow)
window.once 'window:loaded', =>
@autoUpdateManager?.emitUpdateAvailableEvent(window)
unless window.isSpec
focusHandler = => @windowStack.touch(window)
blurHandler = => @saveState(false)
window.browserWindow.on 'focus', focusHandler
window.browserWindow.on 'blur', blurHandler
window.browserWindow.once 'closed', =>
@windowStack.removeWindow(window)
window.browserWindow.removeListener 'focus', focusHandler
window.browserWindow.removeListener 'blur', blurHandler
window.browserWindow.webContents.once 'did-finish-load', => @saveState(false)
getAllWindows: =>
@windowStack.all().slice()
getLastFocusedWindow: (predicate) =>
@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.
listenForArgumentsFromNewProcess: ->
return unless @socketPath?
@deleteSocketFile()
server = net.createServer (connection) =>
data = ''
connection.on 'data', (chunk) ->
data = data + chunk
connection.on 'end', =>
options = JSON.parse(data)
@openWithOptions(options)
server.listen @socketPath
server.on 'error', (error) -> console.error 'Application server failed', error
deleteSocketFile: ->
return if process.platform is 'win32' or not @socketPath?
if fs.existsSync(@socketPath)
try
fs.unlinkSync(@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.
throw error unless error.code is 'ENOENT'
# Registers basic application commands, non-idempotent.
handleEvents: ->
getLoadSettings = =>
devMode: @focusedWindow()?.devMode
safeMode: @focusedWindow()?.safeMode
@on 'application:quit', -> app.quit()
@on 'application:new-window', -> @openPath(getLoadSettings())
@on 'application:new-file', -> (@focusedWindow() ? this).openPath()
@on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true)
@on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true)
@on 'application:inspect', ({x, y, atomWindow}) ->
atomWindow ?= @focusedWindow()
atomWindow?.browserWindow.inspectElement(x, y)
@on 'application:open-documentation', -> shell.openExternal('http://flight-manual.atom.io/')
@on 'application:open-discussions', -> shell.openExternal('https://discuss.atom.io')
@on 'application:open-faq', -> shell.openExternal('https://atom.io/faq')
@on 'application:open-terms-of-use', -> shell.openExternal('https://atom.io/terms')
@on 'application:report-issue', -> shell.openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#reporting-bugs')
@on 'application:search-issues', -> shell.openExternal('https://github.com/search?q=+is%3Aissue+user%3Aatom')
@on 'application:install-update', =>
@quitting = true
@autoUpdateManager.install()
@on 'application:check-for-update', => @autoUpdateManager.check()
if process.platform is 'darwin'
@on 'application:bring-all-windows-to-front', -> Menu.sendActionToFirstResponder('arrangeInFront:')
@on 'application:hide', -> Menu.sendActionToFirstResponder('hide:')
@on 'application:hide-other-applications', -> Menu.sendActionToFirstResponder('hideOtherApplications:')
@on 'application:minimize', -> Menu.sendActionToFirstResponder('performMiniaturize:')
@on 'application:unhide-all-applications', -> Menu.sendActionToFirstResponder('unhideAllApplications:')
@on 'application:zoom', -> Menu.sendActionToFirstResponder('zoom:')
else
@on 'application:minimize', -> @focusedWindow()?.minimize()
@on 'application:zoom', -> @focusedWindow()?.maximize()
@openPathOnEvent('application:about', 'atom://about')
@openPathOnEvent('application:show-settings', 'atom://config')
@openPathOnEvent('application:open-your-config', 'atom://.atom/config')
@openPathOnEvent('application:open-your-init-script', 'atom://.atom/init-script')
@openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap')
@openPathOnEvent('application:open-your-snippets', 'atom://.atom/snippets')
@openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet')
@openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
@disposable.add ipcHelpers.on app, 'before-quit', (event) =>
resolveBeforeQuitPromise = null
@lastBeforeQuitPromise = new Promise((resolve) -> resolveBeforeQuitPromise = resolve)
if @quitting
resolveBeforeQuitPromise()
else
event.preventDefault()
@quitting = true
windowUnloadPromises = @getAllWindows().map((window) -> window.prepareToUnload())
Promise.all(windowUnloadPromises).then((windowUnloadedResults) ->
didUnloadAllWindows = windowUnloadedResults.every((didUnloadWindow) -> didUnloadWindow)
app.quit() if didUnloadAllWindows
resolveBeforeQuitPromise()
)
@disposable.add ipcHelpers.on app, 'will-quit', =>
@killAllProcesses()
@deleteSocketFile()
@disposable.add ipcHelpers.on app, 'open-file', (event, pathToOpen) =>
event.preventDefault()
@openPath({pathToOpen})
@disposable.add ipcHelpers.on app, 'open-url', (event, urlToOpen) =>
event.preventDefault()
@openUrl({urlToOpen, @devMode, @safeMode})
@disposable.add ipcHelpers.on app, 'activate', (event, hasVisibleWindows) =>
unless hasVisibleWindows
event?.preventDefault()
@emit('application:new-window')
@disposable.add ipcHelpers.on ipcMain, 'restart-application', =>
@restart()
@disposable.add ipcHelpers.on ipcMain, 'resolve-proxy', (event, requestId, url) ->
event.sender.session.resolveProxy url, (proxy) ->
unless event.sender.isDestroyed()
event.sender.send('did-resolve-proxy', requestId, proxy)
@disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) =>
for atomWindow in @getAllWindows()
webContents = atomWindow.browserWindow.webContents
if webContents isnt event.sender
webContents.send('did-change-history-manager')
# A request from the associated render process to open a new render process.
@disposable.add ipcHelpers.on ipcMain, 'open', (event, options) =>
window = @atomWindowForEvent(event)
if options?
if typeof options.pathsToOpen is 'string'
options.pathsToOpen = [options.pathsToOpen]
if options.pathsToOpen?.length > 0
options.window = window
@openPaths(options)
else
new AtomWindow(this, @fileRecoveryService, options)
else
@promptForPathToOpen('all', {window})
@disposable.add ipcHelpers.on ipcMain, 'update-application-menu', (event, template, keystrokesByCommand) =>
win = BrowserWindow.fromWebContents(event.sender)
@applicationMenu?.update(win, template, keystrokesByCommand)
@disposable.add ipcHelpers.on ipcMain, 'run-package-specs', (event, packageSpecPath) =>
@runTests({resourcePath: @devResourcePath, pathsToOpen: [packageSpecPath], headless: false})
@disposable.add ipcHelpers.on ipcMain, 'run-benchmarks', (event, benchmarksPath) =>
@runBenchmarks({resourcePath: @devResourcePath, pathsToOpen: [benchmarksPath], headless: false, test: false})
@disposable.add ipcHelpers.on ipcMain, 'command', (event, command) =>
@emit(command)
@disposable.add ipcHelpers.on ipcMain, 'open-command', (event, command, args...) =>
defaultPath = args[0] if args.length > 0
switch command
when 'application:open' then @promptForPathToOpen('all', getLoadSettings(), defaultPath)
when 'application:open-file' then @promptForPathToOpen('file', getLoadSettings(), defaultPath)
when 'application:open-folder' then @promptForPathToOpen('folder', getLoadSettings(), defaultPath)
else console.log "Invalid open-command received: " + command
@disposable.add ipcHelpers.on ipcMain, 'window-command', (event, command, args...) ->
win = BrowserWindow.fromWebContents(event.sender)
win.emit(command, args...)
@disposable.add ipcHelpers.respondTo 'window-method', (browserWindow, method, args...) =>
@atomWindowForBrowserWindow(browserWindow)?[method](args...)
@disposable.add ipcHelpers.on ipcMain, 'pick-folder', (event, responseChannel) =>
@promptForPath "folder", (selectedPaths) ->
event.sender.send(responseChannel, selectedPaths)
@disposable.add ipcHelpers.respondTo 'set-window-size', (win, width, height) ->
win.setSize(width, height)
@disposable.add ipcHelpers.respondTo 'set-window-position', (win, x, y) ->
win.setPosition(x, y)
@disposable.add ipcHelpers.respondTo 'center-window', (win) ->
win.center()
@disposable.add ipcHelpers.respondTo 'focus-window', (win) ->
win.focus()
@disposable.add ipcHelpers.respondTo 'show-window', (win) ->
win.show()
@disposable.add ipcHelpers.respondTo 'hide-window', (win) ->
win.hide()
@disposable.add ipcHelpers.respondTo 'get-temporary-window-state', (win) ->
win.temporaryState
@disposable.add ipcHelpers.respondTo 'set-temporary-window-state', (win, state) ->
win.temporaryState = state
clipboard = require '../safe-clipboard'
@disposable.add ipcHelpers.on ipcMain, 'write-text-to-selection-clipboard', (event, selectedText) ->
clipboard.writeText(selectedText, 'selection')
@disposable.add ipcHelpers.on ipcMain, 'write-to-stdout', (event, output) ->
process.stdout.write(output)
@disposable.add ipcHelpers.on ipcMain, 'write-to-stderr', (event, output) ->
process.stderr.write(output)
@disposable.add ipcHelpers.on ipcMain, 'add-recent-document', (event, filename) ->
app.addRecentDocument(filename)
@disposable.add ipcHelpers.on ipcMain, 'execute-javascript-in-dev-tools', (event, code) ->
event.sender.devToolsWebContents?.executeJavaScript(code)
@disposable.add ipcHelpers.on ipcMain, 'get-auto-update-manager-state', (event) =>
event.returnValue = @autoUpdateManager.getState()
@disposable.add ipcHelpers.on ipcMain, 'get-auto-update-manager-error', (event) =>
event.returnValue = @autoUpdateManager.getErrorMessage()
@disposable.add ipcHelpers.on ipcMain, 'will-save-path', (event, path) =>
@fileRecoveryService.willSavePath(@atomWindowForEvent(event), path)
event.returnValue = true
@disposable.add ipcHelpers.on ipcMain, 'did-save-path', (event, path) =>
@fileRecoveryService.didSavePath(@atomWindowForEvent(event), path)
event.returnValue = true
@disposable.add ipcHelpers.on ipcMain, 'did-change-paths', =>
@saveState(false)
@disposable.add(@disableZoomOnDisplayChange())
setupDockMenu: ->
if process.platform is 'darwin'
dockMenu = Menu.buildFromTemplate [
{label: 'New Window', click: => @emit('application:new-window')}
]
app.dock.setMenu dockMenu
# 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...) ->
unless @emit(command, args...)
focusedWindow = @focusedWindow()
if focusedWindow?
focusedWindow.sendCommand(command, args...)
else
@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...) ->
unless @emit(command, args...)
if atomWindow?
atomWindow.sendCommand(command, args...)
else
@sendCommandToFirstResponder(command)
# Translates the command into macOS action and sends it to application's first
# responder.
sendCommandToFirstResponder: (command) ->
return false unless process.platform is 'darwin'
switch command
when 'core:undo' then Menu.sendActionToFirstResponder('undo:')
when 'core:redo' then Menu.sendActionToFirstResponder('redo:')
when 'core:copy' then Menu.sendActionToFirstResponder('copy:')
when 'core:cut' then Menu.sendActionToFirstResponder('cut:')
when 'core:paste' then Menu.sendActionToFirstResponder('paste:')
when 'core:select-all' then Menu.sendActionToFirstResponder('selectAll:')
else return false
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) ->
@on eventName, ->
if window = @focusedWindow()
window.openPath(pathToOpen)
else
@openPath({pathToOpen})
# Returns the {AtomWindow} for the given paths.
windowForPaths: (pathsToOpen, devMode) ->
_.find @getAllWindows(), (atomWindow) ->
atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen)
# Returns the {AtomWindow} for the given ipcMain event.
atomWindowForEvent: ({sender}) ->
@atomWindowForBrowserWindow(BrowserWindow.fromWebContents(sender))
atomWindowForBrowserWindow: (browserWindow) ->
@getAllWindows().find((atomWindow) -> atomWindow.browserWindow is browserWindow)
# Public: Returns the currently focused {AtomWindow} or undefined if none.
focusedWindow: ->
_.find @getAllWindows(), (atomWindow) -> atomWindow.isFocused()
# Get the platform-specific window offset for new windows.
getWindowOffsetForCurrentPlatform: ->
offsetByPlatform =
darwin: 22
win32: 26
offsetByPlatform[process.platform] ? 0
# Get the dimensions for opening a new window by cascading as appropriate to
# the platform.
getDimensionsForNewWindow: ->
return if (@focusedWindow() ? @getLastFocusedWindow())?.isMaximized()
dimensions = (@focusedWindow() ? @getLastFocusedWindow())?.getDimensions()
offset = @getWindowOffsetForCurrentPlatform()
if dimensions? and offset?
dimensions.x += offset
dimensions.y += offset
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: ({initialPaths, pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env} = {}) ->
@openPaths({initialPaths, 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
# :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.
openPaths: ({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState, addToLastWindow, env}={}) ->
if not pathsToOpen? or pathsToOpen.length is 0
return
env = process.env unless env?
devMode = Boolean(devMode)
safeMode = Boolean(safeMode)
clearWindowState = Boolean(clearWindowState)
locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom, addToLastWindow) for pathToOpen in pathsToOpen)
pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen)
unless pidToKillWhenClosed or newWindow
existingWindow = @windowForPaths(pathsToOpen, devMode)
stats = (fs.statSyncNoException(pathToOpen) for pathToOpen in pathsToOpen)
unless existingWindow?
if currentWindow = window ? @getLastFocusedWindow()
existingWindow = currentWindow if (
addToLastWindow or
currentWindow.devMode is devMode and
(
stats.every((stat) -> stat.isFile?()) or
stats.some((stat) -> stat.isDirectory?() and not currentWindow.hasProjectPath())
)
)
if existingWindow?
openedWindow = existingWindow
openedWindow.openLocations(locationsToOpen)
if openedWindow.isMinimized()
openedWindow.restore()
else
openedWindow.focus()
openedWindow.replaceEnvironment(env)
else
if devMode
try
windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window'))
resourcePath = @devResourcePath
windowInitializationScript ?= require.resolve('../initialize-application-window')
resourcePath ?= @resourcePath
windowDimensions ?= @getDimensionsForNewWindow()
openedWindow = new AtomWindow(this, @fileRecoveryService, {initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env})
openedWindow.focus()
@windowStack.addWindow(openedWindow)
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
openedWindow.browserWindow.once 'closed', =>
@killProcessForWindow(openedWindow)
openedWindow
# Kill all processes associated with opened windows.
killAllProcesses: ->
@killProcess(pid) for pid of @pidsToOpenWindows
return
# Kill process associated with the given opened window.
killProcessForWindow: (openedWindow) ->
for pid, trackedWindow of @pidsToOpenWindows
@killProcess(pid) if trackedWindow is openedWindow
return
# Kill the process with the given pid.
killProcess: (pid) ->
try
parsedPid = parseInt(pid)
process.kill(parsedPid) if isFinite(parsedPid)
catch error
if error.code isnt 'ESRCH'
console.log("Killing process #{pid} failed: #{error.code ? error.message}")
delete @pidsToOpenWindows[pid]
saveState: (allowEmpty=false) ->
return if @quitting
states = []
for window in @getAllWindows()
unless window.isSpec
states.push({initialPaths: window.representedDirectoryPaths})
states.reverse()
if states.length > 0 or allowEmpty
@storageFolder.storeSync('application.json', states)
@emit('application:did-save-state')
loadState: (options) ->
if (@config.get('core.restorePreviousWindowsOnStart') in ['yes', 'always']) and (states = @storageFolder.load('application.json'))?.length > 0
for state in states
@openWithOptions(Object.assign(options, {
initialPaths: state.initialPaths
pathsToOpen: state.initialPaths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
urlsToOpen: []
devMode: @devMode
safeMode: @safeMode
}))
else
null
# 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}) ->
parsedUrl = url.parse(urlToOpen, true)
return unless parsedUrl.protocol is "atom:"
pack = @findPackageWithName(parsedUrl.host, devMode)
if pack?.urlMain
@openPackageUrlMain(parsedUrl.host, pack.urlMain, urlToOpen, devMode, safeMode, env)
else
@openPackageUriHandler(urlToOpen, parsedUrl, devMode, safeMode, env)
openPackageUriHandler: (url, parsedUrl, devMode, safeMode, env) ->
bestWindow = null
if parsedUrl.host is 'core'
predicate = require('../core-uri-handlers').windowPredicate(parsedUrl)
bestWindow = @getLastFocusedWindow (win) ->
not win.isSpecWindow() and predicate(win)
bestWindow ?= @getLastFocusedWindow (win) -> not win.isSpecWindow()
if bestWindow?
bestWindow.sendURIMessage url
bestWindow.focus()
else
resourcePath = @resourcePath
if devMode
try
windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window'))
resourcePath = @devResourcePath
windowInitializationScript ?= require.resolve('../initialize-application-window')
windowDimensions = @getDimensionsForNewWindow()
win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env})
@windowStack.addWindow(win)
win.on 'window:loaded', ->
win.sendURIMessage url
findPackageWithName: (packageName, devMode) ->
_.find @getPackageManager(devMode).getAvailablePackageMetadata(), ({name}) -> name is packageName
openPackageUrlMain: (packageName, packageUrlMain, urlToOpen, devMode, safeMode, env) ->
packagePath = @getPackageManager(devMode).resolvePackagePath(packageName)
windowInitializationScript = path.resolve(packagePath, packageUrlMain)
windowDimensions = @getDimensionsForNewWindow()
new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env})
getPackageManager: (devMode) ->
unless @packages?
PackageManager = require '../package-manager'
@packages = new PackageManager({})
@packages.initialize
configDirPath: process.env.ATOM_HOME
devMode: devMode
resourcePath: @resourcePath
@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}) ->
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
resourcePath = @resourcePath
timeoutInSeconds = Number.parseFloat(timeout)
unless Number.isNaN(timeoutInSeconds)
timeoutHandler = ->
console.log "The test suite has timed out because it has been running for more than #{timeoutInSeconds} seconds."
process.exit(124) # Use the same exit code as the UNIX timeout util.
setTimeout(timeoutHandler, timeoutInSeconds * 1000)
try
windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-test-window'))
catch error
windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window'))
testPaths = []
if pathsToOpen?
for pathToOpen in pathsToOpen
testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen)))
if testPaths.length is 0
process.stderr.write 'Error: Specify at least one test path\n\n'
process.exit(1)
legacyTestRunnerPath = @resolveLegacyTestRunnerPath()
testRunnerPath = @resolveTestRunnerPath(testPaths[0])
devMode = true
isSpec = true
safeMode ?= false
new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env})
runBenchmarks: ({headless, test, resourcePath, executedFrom, pathsToOpen, env}) ->
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
resourcePath = @resourcePath
try
windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-benchmark-window'))
catch error
windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-benchmark-window'))
benchmarkPaths = []
if pathsToOpen?
for pathToOpen in pathsToOpen
benchmarkPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen)))
if benchmarkPaths.length is 0
process.stderr.write 'Error: Specify at least one benchmark path.\n\n'
process.exit(1)
devMode = true
isSpec = true
safeMode = false
new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, test, isSpec, devMode, benchmarkPaths, safeMode, env})
resolveTestRunnerPath: (testPath) ->
FindParentDir ?= require 'find-parent-dir'
if packageRoot = FindParentDir.sync(testPath, 'package.json')
packageMetadata = require(path.join(packageRoot, 'package.json'))
if packageMetadata.atomTestRunner
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)
@resolveLegacyTestRunnerPath()
resolveLegacyTestRunnerPath: ->
try
require.resolve(path.resolve(@devResourcePath, 'spec', 'jasmine-test-runner'))
catch error
require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner'))
locationForPathToOpen: (pathToOpen, executedFrom='', forceAddToWindow) ->
return {pathToOpen} unless pathToOpen
pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
match = pathToOpen.match(LocationSuffixRegExp)
if match?
pathToOpen = pathToOpen.slice(0, -match[0].length)
initialLine = Math.max(0, parseInt(match[1].slice(1)) - 1) if match[1]
initialColumn = Math.max(0, parseInt(match[2].slice(1)) - 1) if match[2]
else
initialLine = initialColumn = null
unless url.parse(pathToOpen).protocol?
pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen))
{pathToOpen, initialLine, initialColumn, forceAddToWindow}
# 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 a selected file path.
# :path - An optional String which controls the default path to which the
# file dialog opens.
promptForPathToOpen: (type, {devMode, safeMode, window}, path=null) ->
@promptForPath type, ((pathsToOpen) =>
@openPaths({pathsToOpen, devMode, safeMode, window})), path
promptForPath: (type, callback, path) ->
properties =
switch type
when 'file' then ['openFile']
when 'folder' then ['openDirectory']
when 'all' then ['openFile', 'openDirectory']
else throw new Error("#{type} is an invalid type for promptForPath")
# Show the open dialog as child window on Windows and Linux, and as
# independent dialog on macOS. This matches most native apps.
parentWindow =
if process.platform is 'darwin'
null
else
BrowserWindow.getFocusedWindow()
openOptions =
properties: properties.concat(['multiSelections', 'createDirectory'])
title: switch type
when 'file' then 'Open File'
when 'folder' then 'Open Folder'
else 'Open'
# File dialog defaults to project directory of currently active editor
if path?
openOptions.defaultPath = path
dialog.showOpenDialog(parentWindow, openOptions, callback)
promptForRestart: ->
chosen = 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 chosen is 0
@restart()
restart: ->
args = []
args.push("--safe") if @safeMode
args.push("--log-file=#{@logFile}") if @logFile?
args.push("--socket-path=#{@socketPath}") if @socketPath?
args.push("--user-data-dir=#{@userDataDir}") if @userDataDir?
if @devMode
args.push('--dev')
args.push("--resource-path=#{@resourcePath}")
app.relaunch({args})
app.quit()
disableZoomOnDisplayChange: ->
outerCallback = =>
for window in @getAllWindows()
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', outerCallback)
screen.on('display-removed', outerCallback)
new Disposable ->
screen.removeListener('display-added', outerCallback)
screen.removeListener('display-removed', outerCallback)
class WindowStack
constructor: (@windows = []) ->
addWindow: (window) =>
@removeWindow(window)
@windows.unshift(window)
touch: (window) =>
@addWindow(window)
removeWindow: (window) =>
currentIndex = @windows.indexOf(window)
@windows.splice(currentIndex, 1) if currentIndex > -1
getLastFocusedWindow: (predicate) =>
predicate ?= (win) -> true
@windows.find(predicate)
all: =>
@windows

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +0,0 @@
{protocol} = require 'electron'
fs = require 'fs'
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) ->
@loadPaths = []
unless safeMode
@loadPaths.push(path.join(process.env.ATOM_HOME, 'dev', 'packages'))
@loadPaths.push(path.join(process.env.ATOM_HOME, 'packages'))
@loadPaths.push(path.join(resourcePath, 'node_modules'))
@registerAtomProtocol()
# Creates the 'atom' custom protocol handler.
registerAtomProtocol: ->
protocol.registerFileProtocol 'atom', (request, callback) =>
relativePath = path.normalize(request.url.substr(7))
if relativePath.indexOf('assets/') is 0
assetsPath = path.join(process.env.ATOM_HOME, relativePath)
filePath = assetsPath if fs.statSyncNoException(assetsPath).isFile?()
unless filePath
for loadPath in @loadPaths
filePath = path.join(loadPath, relativePath)
break if fs.statSyncNoException(filePath).isFile?()
callback(filePath)

View File

@@ -0,0 +1,54 @@
const {protocol} = require('electron')
const fs = require('fs')
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(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)
})
}
}

View File

@@ -1,323 +0,0 @@
{BrowserWindow, app, dialog, ipcMain} = require 'electron'
path = require 'path'
fs = require 'fs'
url = require 'url'
{EventEmitter} = require 'events'
module.exports =
class AtomWindow
Object.assign @prototype, EventEmitter.prototype
@iconPath: path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
@includeShellLoadTime: true
browserWindow: null
loaded: null
isSpec: null
constructor: (@atomApplication, @fileRecoveryService, settings={}) ->
{@resourcePath, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
locationsToOpen ?= [{pathToOpen}] if pathToOpen
locationsToOpen ?= []
@loadedPromise = new Promise((@resolveLoadedPromise) =>)
@closedPromise = new Promise((@resolveClosedPromise) =>)
options =
show: false
title: 'Atom'
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: not @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'
# 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 is 'linux'
options.icon = @constructor.iconPath
if @shouldAddCustomTitleBar()
options.titleBarStyle = 'hidden'
if @shouldAddCustomInsetTitleBar()
options.titleBarStyle = 'hidden-inset'
if @shouldHideTitleBar()
options.frame = false
@browserWindow = new BrowserWindow(options)
@handleEvents()
@loadSettings = Object.assign({}, settings)
@loadSettings.appVersion = app.getVersion()
@loadSettings.resourcePath = @resourcePath
@loadSettings.devMode ?= false
@loadSettings.safeMode ?= false
@loadSettings.atomHome = process.env.ATOM_HOME
@loadSettings.clearWindowState ?= false
@loadSettings.initialPaths ?=
for {pathToOpen} in locationsToOpen when pathToOpen
stat = fs.statSyncNoException(pathToOpen) or null
if stat?.isDirectory()
pathToOpen
else
parentDirectory = path.dirname(pathToOpen)
if stat?.isFile() or fs.existsSync(parentDirectory)
parentDirectory
else
pathToOpen
@loadSettings.initialPaths.sort()
# Only send to the first non-spec window created
if @constructor.includeShellLoadTime and not @isSpec
@constructor.includeShellLoadTime = false
@loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
@representedDirectoryPaths = @loadSettings.initialPaths
@env = @loadSettings.env if @loadSettings.env?
@browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings)
@browserWindow.on 'window:loaded', =>
@disableZoom()
@emit 'window:loaded'
@resolveLoadedPromise()
@browserWindow.on 'window:locations-opened', =>
@emit 'window:locations-opened'
@browserWindow.on 'enter-full-screen', =>
@browserWindow.webContents.send('did-enter-full-screen')
@browserWindow.on 'leave-full-screen', =>
@browserWindow.webContents.send('did-leave-full-screen')
@browserWindow.loadURL url.format
protocol: 'file'
pathname: "#{@resourcePath}/static/index.html"
slashes: true
@browserWindow.showSaveDialog = @showSaveDialog.bind(this)
@browserWindow.focusOnWebView() if @isSpec
@browserWindow.temporaryState = {windowDimensions} if windowDimensions?
hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?)
@openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow()
@atomApplication.addWindow(this)
hasProjectPath: -> @representedDirectoryPaths.length > 0
setupContextMenu: ->
ContextMenu = require './context-menu'
@browserWindow.on 'context-menu', (menuTemplate) =>
new ContextMenu(menuTemplate, this)
containsPaths: (paths) ->
for pathToCheck in paths
return false unless @containsPath(pathToCheck)
true
containsPath: (pathToCheck) ->
@representedDirectoryPaths.some (projectPath) ->
if not projectPath
false
else if not pathToCheck
false
else if pathToCheck is projectPath
true
else if fs.statSyncNoException(pathToCheck).isDirectory?()
false
else if pathToCheck.indexOf(path.join(projectPath, path.sep)) is 0
true
else
false
handleEvents: ->
@browserWindow.on 'close', (event) =>
unless @atomApplication.quitting or @unloading
event.preventDefault()
@unloading = true
@atomApplication.saveState(false)
@prepareToUnload().then (result) =>
@close() if result
@browserWindow.on 'closed', =>
@fileRecoveryService.didCloseWindow(this)
@atomApplication.removeWindow(this)
@resolveClosedPromise()
@browserWindow.on 'unresponsive', =>
return if @isSpec
chosen = dialog.showMessageBox @browserWindow,
type: 'warning'
buttons: ['Force Close', 'Keep Waiting']
message: 'Editor is not responding'
detail: 'The editor is not responding. Would you like to force close it or just keep waiting?'
@browserWindow.destroy() if chosen is 0
@browserWindow.webContents.on 'crashed', =>
if @headless
console.log "Renderer process crashed, exiting"
@atomApplication.exit(100)
return
@fileRecoveryService.didCrashWindow(this)
chosen = dialog.showMessageBox @browserWindow,
type: 'warning'
buttons: ['Close Window', 'Reload', 'Keep It Open']
message: 'The editor has crashed'
detail: 'Please report this issue to https://github.com/atom/atom'
switch chosen
when 0 then @browserWindow.destroy()
when 1 then @browserWindow.reload()
@browserWindow.webContents.on 'will-navigate', (event, url) =>
unless url is @browserWindow.webContents.getURL()
event.preventDefault()
@setupContextMenu()
if @isSpec
# Spec window's web view should always have focus
@browserWindow.on 'blur', =>
@browserWindow.focusOnWebView()
prepareToUnload: ->
if @isSpecWindow()
return Promise.resolve(true)
@lastPrepareToUnloadPromise = new Promise (resolve) =>
callback = (event, result) =>
if BrowserWindow.fromWebContents(event.sender) is @browserWindow
ipcMain.removeListener('did-prepare-to-unload', callback)
unless result
@unloading = false
@atomApplication.quitting = false
resolve(result)
ipcMain.on('did-prepare-to-unload', callback)
@browserWindow.webContents.send('prepare-to-unload')
openPath: (pathToOpen, initialLine, initialColumn) ->
@openLocations([{pathToOpen, initialLine, initialColumn}])
openLocations: (locationsToOpen) ->
@loadedPromise.then => @sendMessage 'open-locations', locationsToOpen
replaceEnvironment: (env) ->
@browserWindow.webContents.send 'environment', env
sendMessage: (message, detail) ->
@browserWindow.webContents.send 'message', message, detail
sendCommand: (command, args...) ->
if @isSpecWindow()
unless @atomApplication.sendCommandToFirstResponder(command)
switch command
when 'window:reload' then @reload()
when 'window:toggle-dev-tools' then @toggleDevTools()
when 'window:close' then @close()
else if @isWebViewFocused()
@sendCommandToBrowserWindow(command, args...)
else
unless @atomApplication.sendCommandToFirstResponder(command)
@sendCommandToBrowserWindow(command, args...)
sendURIMessage: (uri) ->
@browserWindow.webContents.send 'uri-message', uri
sendCommandToBrowserWindow: (command, args...) ->
action = if args[0]?.contextCommand then 'context-command' else 'command'
@browserWindow.webContents.send action, command, args...
getDimensions: ->
[x, y] = @browserWindow.getPosition()
[width, height] = @browserWindow.getSize()
{x, y, width, height}
shouldAddCustomTitleBar: ->
not @isSpec and
process.platform is 'darwin' and
@atomApplication.config.get('core.titleBar') is 'custom'
shouldAddCustomInsetTitleBar: ->
not @isSpec and
process.platform is 'darwin' and
@atomApplication.config.get('core.titleBar') is 'custom-inset'
shouldHideTitleBar: ->
not @isSpec and
process.platform is 'darwin' and
@atomApplication.config.get('core.titleBar') is 'hidden'
close: -> @browserWindow.close()
focus: -> @browserWindow.focus()
minimize: -> @browserWindow.minimize()
maximize: -> @browserWindow.maximize()
unmaximize: -> @browserWindow.unmaximize()
restore: -> @browserWindow.restore()
setFullScreen: (fullScreen) -> @browserWindow.setFullScreen(fullScreen)
setAutoHideMenuBar: (autoHideMenuBar) -> @browserWindow.setAutoHideMenuBar(autoHideMenuBar)
handlesAtomCommands: ->
not @isSpecWindow() and @isWebViewFocused()
isFocused: -> @browserWindow.isFocused()
isMaximized: -> @browserWindow.isMaximized()
isMinimized: -> @browserWindow.isMinimized()
isWebViewFocused: -> @browserWindow.isWebViewFocused()
isSpecWindow: -> @isSpec
reload: ->
@loadedPromise = new Promise((@resolveLoadedPromise) =>)
@prepareToUnload().then (result) =>
@browserWindow.reload() if result
@loadedPromise
showSaveDialog: (params) ->
params = Object.assign({
title: 'Save File',
defaultPath: @representedDirectoryPaths[0]
}, params)
dialog.showSaveDialog(@browserWindow, params)
toggleDevTools: -> @browserWindow.toggleDevTools()
openDevTools: -> @browserWindow.openDevTools()
closeDevTools: -> @browserWindow.closeDevTools()
setDocumentEdited: (documentEdited) -> @browserWindow.setDocumentEdited(documentEdited)
setRepresentedFilename: (representedFilename) -> @browserWindow.setRepresentedFilename(representedFilename)
setRepresentedDirectoryPaths: (@representedDirectoryPaths) ->
@representedDirectoryPaths.sort()
@loadSettings.initialPaths = @representedDirectoryPaths
@browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings)
@atomApplication.saveState()
copy: -> @browserWindow.copy()
disableZoom: ->
@browserWindow.webContents.setVisualZoomLevelLimits(1, 1)

View File

@@ -0,0 +1,432 @@
const {BrowserWindow, app, dialog, ipcMain} = require('electron')
const path = require('path')
const fs = require('fs')
const url = require('url')
const {EventEmitter} = require('events')
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 = {}) {
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
let {pathToOpen, locationsToOpen} = settings
if (!locationsToOpen && pathToOpen) locationsToOpen = [{pathToOpen}]
if (!locationsToOpen) locationsToOpen = []
this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve })
this.closedPromise = new Promise(resolve => { this.resolveClosedPromise = resolve })
const options = {
show: false,
title: 'Atom',
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'
}
}
// 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 = ICON_PATH
if (this.shouldAddCustomTitleBar()) options.titleBarStyle = 'hidden'
if (this.shouldAddCustomInsetTitleBar()) options.titleBarStyle = 'hidden-inset'
if (this.shouldHideTitleBar()) options.frame = false
this.browserWindow = new BrowserWindow(options)
this.handleEvents()
this.loadSettings = Object.assign({}, settings)
this.loadSettings.appVersion = app.getVersion()
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
if (!this.loadSettings.initialPaths) {
this.loadSettings.initialPaths = []
for (const {pathToOpen} of locationsToOpen) {
if (!pathToOpen) continue
const stat = fs.statSyncNoException(pathToOpen) || null
if (stat && stat.isDirectory()) {
this.loadSettings.initialPaths.push(pathToOpen)
} else {
const parentDirectory = path.dirname(pathToOpen)
if ((stat && stat.isFile()) || fs.existsSync(parentDirectory)) {
this.loadSettings.initialPaths.push(parentDirectory)
} else {
this.loadSettings.initialPaths.push(pathToOpen)
}
}
}
}
this.loadSettings.initialPaths.sort()
// 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
}
}
this.representedDirectoryPaths = this.loadSettings.initialPaths
if (!this.loadSettings.env) this.env = this.loadSettings.env
this.browserWindow.loadSettingsJSON = JSON.stringify(this.loadSettings)
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)
}
hasProjectPath () {
return this.representedDirectoryPaths.length > 0
}
setupContextMenu () {
const ContextMenu = require('./context-menu')
this.browserWindow.on('context-menu', menuTemplate => {
return new ContextMenu(menuTemplate, this)
})
}
containsPaths (paths) {
return paths.every(p => this.containsPath(p))
}
containsPath (pathToCheck) {
if (!pathToCheck) return false
const stat = fs.statSyncNoException(pathToCheck)
if (stat && stat.isDirectory()) return false
return this.representedDirectoryPaths.some(projectPath =>
pathToCheck === projectPath || pathToCheck.startsWith(path.join(projectPath, path.sep))
)
}
handleEvents () {
this.browserWindow.on('close', async event => {
if (!this.atomApplication.quitting && !this.unloading) {
event.preventDefault()
this.unloading = true
this.atomApplication.saveState(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', () => {
if (this.isSpec) return
const chosen = dialog.showMessageBox(this.browserWindow, {
type: 'warning',
buttons: ['Force Close', 'Keep Waiting'],
message: 'Editor is not responding',
detail:
'The editor is not responding. Would you like to force close it or just keep waiting?'
})
if (chosen === 0) this.browserWindow.destroy()
})
this.browserWindow.webContents.on('crashed', () => {
if (this.headless) {
console.log('Renderer process crashed, exiting')
this.atomApplication.exit(100)
return
}
this.fileRecoveryService.didCrashWindow(this)
const chosen = dialog.showMessageBox(this.browserWindow, {
type: 'warning',
buttons: ['Close Window', 'Reload', 'Keep It Open'],
message: 'The editor has crashed',
detail: 'Please report this issue to https://github.com/atom/atom'
})
switch (chosen) {
case 0: return this.browserWindow.destroy()
case 1: return this.browserWindow.reload()
}
})
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) {
await this.loadedPromise
this.sendMessage('open-locations', locationsToOpen)
}
replaceEnvironment (env) {
this.browserWindow.webContents.send('environment', env)
}
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}
}
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 &&
process.platform === 'darwin' &&
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.representedDirectoryPaths[0]
}, options)
if (typeof callback === 'function') {
// Async
dialog.showSaveDialog(this.browserWindow, options, callback)
} else {
// Sync
return dialog.showSaveDialog(this.browserWindow, options)
}
}
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)
}
setRepresentedDirectoryPaths (representedDirectoryPaths) {
this.representedDirectoryPaths = representedDirectoryPaths
this.representedDirectoryPaths.sort()
this.loadSettings.initialPaths = this.representedDirectoryPaths
this.browserWindow.loadSettingsJSON = JSON.stringify(this.loadSettings)
return this.atomApplication.saveState()
}
didClosePathWithWaitSession (path) {
this.atomApplication.windowDidClosePathWithWaitSession(this, path)
}
copy () {
return this.browserWindow.copy()
}
disableZoom () {
return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1)
}
}

View File

@@ -5,7 +5,7 @@ class ContextMenu
constructor: (template, @atomWindow) ->
template = @createClickHandlers(template)
menu = Menu.buildFromTemplate(template)
menu.popup(@atomWindow.browserWindow)
menu.popup(@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

View File

@@ -27,6 +27,15 @@ class NotificationManager {
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
*/
@@ -200,7 +209,9 @@ class NotificationManager {
Section: Managing Notifications
*/
// Public: Clear all the notifications.
clear () {
this.notifications = []
this.emitter.emit('did-clear-notifications')
}
}

View File

@@ -1,848 +0,0 @@
path = require 'path'
_ = require 'underscore-plus'
async = require 'async'
CSON = require 'season'
fs = require 'fs-plus'
{Emitter, CompositeDisposable} = require 'event-kit'
CompileCache = require './compile-cache'
ModuleCache = require './module-cache'
ScopedProperties = require './scoped-properties'
BufferedProcess = require './buffered-process'
# Extended: Loads and activates a package's main module and resources such as
# stylesheets, keymaps, grammar, editor properties, and menus.
module.exports =
class Package
keymaps: null
menus: null
stylesheets: null
stylesheetDisposables: null
grammars: null
settings: null
mainModulePath: null
resolvedMainModulePath: false
mainModule: null
mainInitialized: false
mainActivated: false
###
Section: Construction
###
constructor: (params) ->
{
@path, @metadata, @bundledPackage, @preloadedPackage, @packageManager, @config, @styleManager, @commandRegistry,
@keymapManager, @notificationManager, @grammarRegistry, @themeManager,
@menuManager, @contextMenuManager, @deserializerManager, @viewRegistry
} = params
@emitter = new Emitter
@metadata ?= @packageManager.loadPackageMetadata(@path)
@bundledPackage ?= @packageManager.isBundledPackagePath(@path)
@name = @metadata?.name ? params.name ? path.basename(@path)
@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: ->
@config.removeAtKeyPath('core.disabledPackages', @name)
disable: ->
@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
preload: ->
@loadKeymaps()
@loadMenus()
@registerDeserializerMethods()
@activateCoreStartupServices()
@registerURIHandler()
@configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata()
@requireMainModule()
@settingsPromise = @loadSettings()
@activationDisposables = new CompositeDisposable
@activateKeymaps()
@activateMenus()
settings.activate() for settings in @settings
@settingsActivated = true
finishLoading: ->
@measure 'loadTime', =>
@path = path.join(@packageManager.resourcePath, @path)
ModuleCache.add(@path, @metadata)
@loadStylesheets()
# Unfortunately some packages are accessing `@mainModulePath`, so we need
# to compute that variable eagerly also for preloaded packages.
@getMainModulePath()
load: ->
@measure 'loadTime', =>
try
ModuleCache.add(@path, @metadata)
@loadKeymaps()
@loadMenus()
@loadStylesheets()
@registerDeserializerMethods()
@activateCoreStartupServices()
@registerURIHandler()
@registerTranspilerConfig()
@configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata()
@settingsPromise = @loadSettings()
if @shouldRequireMainModuleOnLoad() and not @mainModule?
@requireMainModule()
catch error
@handleError("Failed to load the #{@name} package", error)
this
unload: ->
@unregisterTranspilerConfig()
shouldRequireMainModuleOnLoad: ->
not (
@metadata.deserializers? or
@metadata.viewProviders? or
@metadata.configSchema? or
@activationShouldBeDeferred() or
localStorage.getItem(@getCanDeferMainModuleRequireStorageKey()) is 'true'
)
reset: ->
@stylesheets = []
@keymaps = []
@menus = []
@grammars = []
@settings = []
@mainInitialized = false
@mainActivated = false
initializeIfNeeded: ->
return if @mainInitialized
@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.
@requireMainModule() unless @mainModule?
@mainModule.initialize?(@packageManager.getPackageState(@name) ? {})
@mainInitialized = true
catch error
@handleError("Failed to initialize the #{@name} package", error)
return
activate: ->
@grammarsPromise ?= @loadGrammars()
@activationPromise ?=
new Promise (resolve, reject) =>
@resolveActivationPromise = resolve
@measure 'activateTime', =>
try
@activateResources()
if @activationShouldBeDeferred()
@subscribeToDeferredActivation()
else
@activateNow()
catch error
@handleError("Failed to activate the #{@name} package", error)
Promise.all([@grammarsPromise, @settingsPromise, @activationPromise])
activateNow: ->
try
@requireMainModule() unless @mainModule?
@configSchemaRegisteredOnActivate = @registerConfigSchemaFromMainModule()
@registerViewProviders()
@activateStylesheets()
if @mainModule? and not @mainActivated
@initializeIfNeeded()
@mainModule.activateConfig?()
@mainModule.activate?(@packageManager.getPackageState(@name) ? {})
@mainActivated = true
@activateServices()
@activationCommandSubscriptions?.dispose()
@activationHookSubscriptions?.dispose()
catch error
@handleError("Failed to activate the #{@name} package", error)
@resolveActivationPromise?()
registerConfigSchemaFromMetadata: ->
if configSchema = @metadata.configSchema
@config.setSchema @name, {type: 'object', properties: configSchema}
true
else
false
registerConfigSchemaFromMainModule: ->
if @mainModule? and not @configSchemaRegisteredOnLoad
if @mainModule.config? and typeof @mainModule.config is 'object'
@config.setSchema @name, {type: 'object', properties: @mainModule.config}
return true
false
# TODO: Remove. Settings view calls this method currently.
activateConfig: ->
return if @configSchemaRegisteredOnLoad
@requireMainModule()
@registerConfigSchemaFromMainModule()
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(
@styleManager.addStyleSheet(
source,
{
sourcePath,
priority,
context,
skipDeprecatedSelectorsTransformation: @bundledPackage
}
)
)
@stylesheetsActivated = true
activateResources: ->
@activationDisposables ?= new CompositeDisposable
keymapIsDisabled = _.include(@config.get("core.packagesWithKeymapsDisabled") ? [], @name)
if keymapIsDisabled
@deactivateKeymaps()
else unless @keymapActivated
@activateKeymaps()
unless @menusActivated
@activateMenus()
unless @grammarsActivated
grammar.activate() for grammar in @grammars
@grammarsActivated = true
unless @settingsActivated
settings.activate() for settings in @settings
@settingsActivated = true
activateKeymaps: ->
return if @keymapActivated
@keymapDisposables = new CompositeDisposable()
validateSelectors = not @preloadedPackage
@keymapDisposables.add(@keymapManager.add(keymapPath, map, 0, validateSelectors)) for [keymapPath, map] in @keymaps
@menuManager.update()
@keymapActivated = true
deactivateKeymaps: ->
return if not @keymapActivated
@keymapDisposables?.dispose()
@menuManager.update()
@keymapActivated = false
hasKeymaps: ->
for [path, map] in @keymaps
if map.length > 0
return true
false
activateMenus: ->
validateSelectors = not @preloadedPackage
for [menuPath, map] in @menus when map['context-menu']?
try
itemsBySelector = map['context-menu']
@activationDisposables.add(@contextMenuManager.add(itemsBySelector, validateSelectors))
catch error
if error.code is 'EBADSELECTOR'
error.message += " in #{menuPath}"
error.stack += "\n at #{menuPath}:1:1"
throw error
for [menuPath, map] in @menus when map['menu']?
@activationDisposables.add(@menuManager.add(map['menu']))
@menusActivated = 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 @packageManager.serviceHub.provide(name, servicesByVersion)
for name, {versions} of @metadata.consumedServices
for version, methodName of versions
if typeof @mainModule[methodName] is 'function'
@activationDisposables.add @packageManager.serviceHub.consume(name, version, @mainModule[methodName].bind(@mainModule))
return
registerURIHandler: ->
handlerConfig = @getURIHandler()
if methodName = handlerConfig?.method
@uriHandlerSubscription = @packageManager.registerURIHandlerForPackage @name, (args...) =>
@handleURI(methodName, args)
unregisterURIHandler: ->
@uriHandlerSubscription?.dispose()
handleURI: (methodName, args) ->
@activate().then => @mainModule[methodName]?.apply(@mainModule, args)
@activateNow() unless @mainActivated
registerTranspilerConfig: ->
if @metadata.atomTranspilers
CompileCache.addTranspilerConfigForPath(@path, @name, @metadata, @metadata.atomTranspilers)
unregisterTranspilerConfig: ->
if @metadata.atomTranspilers
CompileCache.removeTranspilerConfigForPath(@path)
loadKeymaps: ->
if @bundledPackage and @packageManager.packagesCache[@name]?
@keymaps = (["core:#{keymapPath}", keymapObject] for keymapPath, keymapObject of @packageManager.packagesCache[@name].keymaps)
else
@keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath, allowDuplicateKeys: false) ? {}]
return
loadMenus: ->
if @bundledPackage and @packageManager.packagesCache[@name]?
@menus = (["core:#{menuPath}", menuObject] for menuPath, menuObject of @packageManager.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, @themeManager.loadStylesheet(stylesheetPath, true)]
registerDeserializerMethods: ->
if @metadata.deserializers?
Object.keys(@metadata.deserializers).forEach (deserializerName) =>
methodName = @metadata.deserializers[deserializerName]
@deserializerManager.add
name: deserializerName,
deserialize: (state, atomEnvironment) =>
@registerViewProviders()
@requireMainModule()
@initializeIfNeeded()
@mainModule[methodName](state, atomEnvironment)
return
activateCoreStartupServices: ->
if directoryProviderService = @metadata.providedServices?['atom.directory-provider']
@requireMainModule()
servicesByVersion = {}
for version, methodName of directoryProviderService.versions
if typeof @mainModule[methodName] is 'function'
servicesByVersion[version] = @mainModule[methodName]()
@packageManager.serviceHub.provide('atom.directory-provider', servicesByVersion)
registerViewProviders: ->
if @metadata.viewProviders? and not @registeredViewProviders
@requireMainModule()
@metadata.viewProviders.forEach (methodName) =>
@viewRegistry.addViewProvider (model) =>
@initializeIfNeeded()
@mainModule[methodName](model)
@registeredViewProviders = true
getStylesheetsPath: ->
path.join(@path, 'styles')
getStylesheetPaths: ->
if @bundledPackage and @packageManager.packagesCache[@name]?.styleSheetPaths?
styleSheetPaths = @packageManager.packagesCache[@name].styleSheetPaths
styleSheetPaths.map (styleSheetPath) => path.join(@path, styleSheetPath)
else
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
if @preloadedPackage and @packageManager.packagesCache[@name]?
grammarPaths = @packageManager.packagesCache[@name].grammarPaths
else
grammarPaths = fs.listSync(path.join(@path, 'grammars'), ['json', 'cson'])
for grammarPath in grammarPaths
if @preloadedPackage and @packageManager.packagesCache[@name]?
grammarPath = path.resolve(@packageManager.resourcePath, grammarPath)
try
grammar = @grammarRegistry.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 Promise.resolve() if @grammarsLoaded
loadGrammar = (grammarPath, callback) =>
if @preloadedPackage
grammarPath = path.resolve(@packageManager.resourcePath, grammarPath)
@grammarRegistry.readGrammar grammarPath, (error, grammar) =>
if error?
detail = "#{error.message} in #{grammarPath}"
stack = "#{error.stack}\n at #{grammarPath}:1:1"
@notificationManager.addFatalError("Failed to load a #{@name} package grammar", {stack, detail, packageName: @name, dismissable: true})
else
grammar.packageName = @name
grammar.bundledPackage = @bundledPackage
@grammars.push(grammar)
grammar.activate() if @grammarsActivated
callback()
new Promise (resolve) =>
if @preloadedPackage and @packageManager.packagesCache[@name]?
grammarPaths = @packageManager.packagesCache[@name].grammarPaths
async.each grammarPaths, loadGrammar, -> resolve()
else
grammarsDirPath = path.join(@path, 'grammars')
fs.exists grammarsDirPath, (grammarsDirExists) ->
return resolve() unless grammarsDirExists
fs.list grammarsDirPath, ['json', 'cson'], (error, grammarPaths=[]) ->
async.each grammarPaths, loadGrammar, -> resolve()
loadSettings: ->
@settings = []
loadSettingsFile = (settingsPath, callback) =>
ScopedProperties.load settingsPath, @config, (error, settings) =>
if error?
detail = "#{error.message} in #{settingsPath}"
stack = "#{error.stack}\n at #{settingsPath}:1:1"
@notificationManager.addFatalError("Failed to load the #{@name} package settings", {stack, detail, packageName: @name, dismissable: true})
else
@settings.push(settings)
settings.activate() if @settingsActivated
callback()
new Promise (resolve) =>
if @preloadedPackage and @packageManager.packagesCache[@name]?
for settingsPath, scopedProperties of @packageManager.packagesCache[@name].settings
settings = new ScopedProperties("core:#{settingsPath}", scopedProperties ? {}, @config)
@settings.push(settings)
settings.activate() if @settingsActivated
resolve()
else
settingsDirPath = path.join(@path, 'settings')
fs.exists settingsDirPath, (settingsDirExists) ->
return resolve() unless settingsDirExists
fs.list settingsDirPath, ['json', 'cson'], (error, settingsPaths=[]) ->
async.each settingsPaths, loadSettingsFile, -> resolve()
serialize: ->
if @mainActivated
try
@mainModule?.serialize?()
catch e
console.error "Error serializing package '#{@name}'", e.stack
deactivate: ->
@activationPromise = null
@resolveActivationPromise = null
@activationCommandSubscriptions?.dispose()
@activationHookSubscriptions?.dispose()
@configSchemaRegisteredOnActivate = false
@unregisterURIHandler()
@deactivateResources()
@deactivateKeymaps()
unless @mainActivated
@emitter.emit 'did-deactivate'
return
try
deactivationResult = @mainModule?.deactivate?()
catch e
console.error "Error deactivating package '#{@name}'", e.stack
# We support then-able async promises as well as sync ones from deactivate
if typeof deactivationResult?.then is 'function'
deactivationResult.then => @afterDeactivation()
else
@afterDeactivation()
afterDeactivation: ->
try
@mainModule?.deactivateConfig?()
catch e
console.error "Error deactivating package '#{@name}'", e.stack
@mainActivated = false
@mainInitialized = false
@emitter.emit 'did-deactivate'
deactivateResources: ->
grammar.deactivate() for grammar in @grammars
settings.deactivate() for settings in @settings
@stylesheetDisposables?.dispose()
@activationDisposables?.dispose()
@keymapDisposables?.dispose()
@stylesheetsActivated = false
@grammarsActivated = false
@settingsActivated = false
@menusActivated = false
reloadStylesheets: ->
try
@loadStylesheets()
catch error
@handleError("Failed to reload the #{@name} package stylesheets", error)
@stylesheetDisposables?.dispose()
@stylesheetDisposables = new CompositeDisposable
@stylesheetsActivated = false
@activateStylesheets()
requireMainModule: ->
if @bundledPackage and @packageManager.packagesCache[@name]?
if @packageManager.packagesCache[@name].main?
@mainModule = require(@packageManager.packagesCache[@name].main)
else if @mainModuleRequired
@mainModule
else if not @isCompatible()
console.warn """
Failed to require the main module of '#{@name}' because it requires one or more incompatible native modules (#{_.pluck(@incompatibleModules, 'name').join(', ')}).
Run `apm rebuild` in the package directory and restart Atom to resolve.
"""
return
else
mainModulePath = @getMainModulePath()
if fs.isFileSync(mainModulePath)
@mainModuleRequired = true
previousViewProviderCount = @viewRegistry.getViewProviderCount()
previousDeserializerCount = @deserializerManager.getDeserializerCount()
@mainModule = require(mainModulePath)
if (@viewRegistry.getViewProviderCount() is previousViewProviderCount and
@deserializerManager.getDeserializerCount() is previousDeserializerCount)
localStorage.setItem(@getCanDeferMainModuleRequireStorageKey(), 'true')
getMainModulePath: ->
return @mainModulePath if @resolvedMainModulePath
@resolvedMainModulePath = true
if @bundledPackage and @packageManager.packagesCache[@name]?
if @packageManager.packagesCache[@name].main
@mainModulePath = path.resolve(@packageManager.resourcePath, 'static', @packageManager.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, ["", CompileCache.supportedExtensions...])
activationShouldBeDeferred: ->
@hasActivationCommands() or @hasActivationHooks() or @hasDeferredURIHandler()
hasActivationHooks: ->
@getActivationHooks()?.length > 0
hasActivationCommands: ->
for selector, commands of @getActivationCommands()
return true if commands.length > 0
false
hasDeferredURIHandler: ->
@getURIHandler() and @getURIHandler().deferActivation isnt false
subscribeToDeferredActivation: ->
@subscribeToActivationCommands()
@subscribeToActivationHooks()
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 @commandRegistry.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 @commandRegistry.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...)
@activationCommands
subscribeToActivationHooks: ->
@activationHookSubscriptions = new CompositeDisposable
for hook in @getActivationHooks()
do (hook) =>
@activationHookSubscriptions.add(@packageManager.onDidTriggerActivationHook(hook, => @activateNow())) if hook? and _.isString(hook) and hook.trim().length > 0
return
getActivationHooks: ->
return @activationHooks if @metadata? and @activationHooks?
@activationHooks = []
if @metadata.activationHooks?
if _.isArray(@metadata.activationHooks)
@activationHooks.push(@metadata.activationHooks...)
else if _.isString(@metadata.activationHooks)
@activationHooks.push(@metadata.activationHooks)
@activationHooks = _.uniq(@activationHooks)
getURIHandler: ->
@metadata?.uriHandler
# 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.
#
# First try to get this information from
# @metadata._atomModuleCache.extensions. If @metadata._atomModuleCache doesn't
# exist, recurse through all dependencies.
getNativeModuleDependencyPaths: ->
nativeModulePaths = []
if @metadata._atomModuleCache?
relativeNativeModuleBindingPaths = @metadata._atomModuleCache.extensions?['.node'] ? []
for relativeNativeModuleBindingPath in relativeNativeModuleBindingPaths
nativeModulePath = path.join(@path, relativeNativeModuleBindingPath, '..', '..', '..')
nativeModulePaths.push(nativeModulePath)
return 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
###
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: ->
return @compatible if @compatible?
if @preloadedPackage
# Preloaded packages are always considered compatible
@compatible = true
else if @getMainModulePath()
@incompatibleModules = @getIncompatibleNativeModules()
@compatible = @incompatibleModules.length is 0 and not @getBuildFailureOutput()?
else
@compatible = true
# 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: ->
new Promise (resolve) =>
@runRebuildProcess (result) =>
if result.code is 0
global.localStorage.removeItem(@getBuildFailureOutputStorageKey())
else
@compatible = false
global.localStorage.setItem(@getBuildFailureOutputStorageKey(), result.stderr)
global.localStorage.setItem(@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: ->
global.localStorage.getItem(@getBuildFailureOutputStorageKey())
runRebuildProcess: (callback) ->
stderr = ''
stdout = ''
new BufferedProcess({
command: @packageManager.getApmPath()
args: ['rebuild', '--no-color']
options: {cwd: @path}
stderr: (output) -> stderr += output
stdout: (output) -> stdout += output
exit: (code) -> callback({code, stdout, stderr})
})
getBuildFailureOutputStorageKey: ->
"installed-packages:#{@name}:#{@metadata.version}:build-error"
getIncompatibleNativeModulesStorageKey: ->
electronVersion = process.versions.electron
"installed-packages:#{@name}:#{@metadata.version}:electron-#{electronVersion}:incompatible-native-modules"
getCanDeferMainModuleRequireStorageKey: ->
"installed-packages:#{@name}:#{@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 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: ->
unless @packageManager.devMode
try
if arrayAsString = global.localStorage.getItem(@getIncompatibleNativeModulesStorageKey())
return JSON.parse(arrayAsString)
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(@getIncompatibleNativeModulesStorageKey(), JSON.stringify(incompatibleNativeModules))
incompatibleNativeModules
handleError: (message, error) ->
if atom.inSpecMode()
throw 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
@notificationManager.addFatalError(message, {stack, detail, packageName: @name, dismissable: true})

1107
src/package.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,12 @@ class PaneResizeHandleElement extends HTMLElement
@addEventListener 'mousedown', @resizeStarted.bind(this)
attachedCallback: ->
@isHorizontal = @parentElement.classList.contains("horizontal")
@classList.add if @isHorizontal then 'horizontal' else 'vertical'
# 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 @parentElement
@isHorizontal = @parentElement.classList.contains("horizontal")
@classList.add if @isHorizontal then 'horizontal' else 'vertical'
detachedCallback: ->
@resizeStopped()

View File

@@ -790,57 +790,53 @@ class Pane {
}
promptToSaveItem (item, options = {}) {
if (typeof item.shouldPromptToSave !== 'function' || !item.shouldPromptToSave(options)) {
return Promise.resolve(true)
}
let uri
if (typeof item.getURI === 'function') {
uri = item.getURI()
} else if (typeof item.getUri === 'function') {
uri = item.getUri()
} else {
return Promise.resolve(true)
}
const title = (typeof item.getTitle === 'function' && item.getTitle()) || uri
const saveDialog = (saveButtonText, saveFn, message) => {
const chosen = this.applicationDelegate.confirm({
message,
detailedMessage: 'Your changes will be lost if you close this item without saving.',
buttons: [saveButtonText, 'Cancel', "&Don't Save"]}
)
switch (chosen) {
case 0:
return new Promise(resolve => {
return saveFn(item, error => {
if (error instanceof SaveCancelledError) {
resolve(false)
} else if (error) {
saveDialog(
'Save as',
this.saveItemAs,
`'${title}' could not be saved.\nError: ${this.getMessageForErrorCode(error.code)}`
).then(resolve)
} else {
resolve(true)
}
})
})
case 1:
return Promise.resolve(false)
case 2:
return Promise.resolve(true)
return new Promise((resolve, reject) => {
if (typeof item.shouldPromptToSave !== 'function' || !item.shouldPromptToSave(options)) {
return resolve(true)
}
}
return saveDialog(
'Save',
this.saveItem,
`'${title}' has changes, do you want to save them?`
)
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.
@@ -908,7 +904,7 @@ class Pane {
// after the item is successfully saved, or with the error if it failed.
// The return value will be that of `nextAction` or `undefined` if it was not
// provided
saveItemAs (item, nextAction) {
async saveItemAs (item, nextAction) {
if (!item) return
if (typeof item.saveAs !== 'function') return
@@ -919,22 +915,34 @@ class Pane {
const itemPath = item.getPath()
if (itemPath && !saveOptions.defaultPath) saveOptions.defaultPath = itemPath
const newItemPath = this.applicationDelegate.showSaveDialog(saveOptions)
if (newItemPath) {
return promisify(() => item.saveAs(newItemPath))
.then(() => {
if (nextAction) nextAction()
})
.catch(error => {
if (nextAction) {
nextAction(error)
} else {
this.handleSaveError(error, item)
}
})
} else if (nextAction) {
return nextAction(new SaveCancelledError('Save Cancelled'))
}
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 await saveDialogPromise
}
// Public: Save all items.

View File

@@ -422,7 +422,7 @@ class PathWatcher {
// 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 it events.
// of the underlying OS resources that feed its events.
//
// PathWatchers acquired through `watchPath` are already started.
//
@@ -533,7 +533,7 @@ class PathWatcher {
}
}
// Extended: Unsubscribe all subscribers from filesystem events. Native resources will be release asynchronously,
// 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()) {

View File

@@ -2,7 +2,7 @@ const path = require('path')
const _ = require('underscore-plus')
const fs = require('fs-plus')
const {Emitter, Disposable} = require('event-kit')
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
const TextBuffer = require('text-buffer')
const {watchPath} = require('./path-watcher')
@@ -19,10 +19,12 @@ class Project extends Model {
Section: Construction and Destruction
*/
constructor ({notificationManager, packageManager, config, applicationDelegate}) {
constructor ({notificationManager, packageManager, config, applicationDelegate, grammarRegistry}) {
super()
this.notificationManager = notificationManager
this.applicationDelegate = applicationDelegate
this.grammarRegistry = grammarRegistry
this.emitter = new Emitter()
this.buffers = []
this.rootDirectories = []
@@ -35,6 +37,7 @@ class Project extends Model {
this.watcherPromisesByPath = {}
this.retiredBufferIDs = new Set()
this.retiredBufferPaths = new Set()
this.subscriptions = new CompositeDisposable()
this.consumeServices(packageManager)
}
@@ -54,6 +57,9 @@ class Project extends Model {
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()
}
@@ -104,6 +110,7 @@ class Project extends Model {
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})
@@ -211,7 +218,7 @@ class Project extends Model {
//
// 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:
// {Array} of {GitRepository} objects:
// ```
// Promise.all(atom.project.getDirectories().map(
// atom.project.repositoryForDirectory.bind(atom.project)))
@@ -222,10 +229,10 @@ class Project extends Model {
// Public: Get the repository for a given directory asynchronously.
//
// * `directory` {Directory} for which to get a {Repository}.
// * `directory` {Directory} for which to get a {GitRepository}.
//
// Returns a {Promise} that resolves with either:
// * {Repository} if a repository can be created for the given directory
// * {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()
@@ -654,11 +661,8 @@ class Project extends Model {
}
addBuffer (buffer, options = {}) {
return this.addBufferAtIndex(buffer, this.buffers.length, options)
}
addBufferAtIndex (buffer, index, options = {}) {
this.buffers.splice(index, 0, buffer)
this.buffers.push(buffer)
this.subscriptions.add(this.grammarRegistry.maintainLanguageMode(buffer))
this.subscribeToBuffer(buffer)
this.emitter.emit('did-add-buffer', buffer)
return buffer

View File

@@ -12,13 +12,13 @@ class ProtocolHandlerInstaller {
}
isDefaultProtocolClient () {
return remote.app.isDefaultProtocolClient('atom', process.execPath, ['--uri-handler'])
return remote.app.isDefaultProtocolClient('atom', process.execPath, ['--uri-handler', '--'])
}
setAsDefaultProtocolClient () {
// This Electron API is only available on Windows and macOS. There might be some
// hacks to make it work on Linux; see https://github.com/electron/electron/issues/6440
return this.isSupported() && remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--uri-handler'])
return this.isSupported() && remote.app.setAsDefaultProtocolClient('atom', process.execPath, ['--uri-handler', '--'])
}
initialize (config, notifications) {
@@ -26,19 +26,28 @@ class ProtocolHandlerInstaller {
return
}
if (!this.isDefaultProtocolClient()) {
const behaviorWhenNotProtocolClient = config.get(SETTING)
switch (behaviorWhenNotProtocolClient) {
case PROMPT:
const behaviorWhenNotProtocolClient = config.get(SETTING)
switch (behaviorWhenNotProtocolClient) {
case PROMPT:
if (!this.isDefaultProtocolClient()) {
this.promptToBecomeProtocolClient(config, notifications)
break
case ALWAYS:
}
break
case ALWAYS:
if (!this.isDefaultProtocolClient()) {
this.setAsDefaultProtocolClient()
break
case NEVER:
default:
// Do nothing
}
}
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
}
}
@@ -63,7 +72,7 @@ class ProtocolHandlerInstaller {
notification = notifications.addInfo('Register as default atom:// URI handler?', {
dismissable: true,
icon: 'link',
description: 'Atom is not currently set as the defaut handler for atom:// URIs. Would you like Atom to handle ' +
description: 'Atom is not currently set as the default handler for atom:// URIs. Would you like Atom to handle ' +
'atom:// URIs?',
buttons: [
{

View File

@@ -160,6 +160,8 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
'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
)
@@ -219,18 +221,40 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
'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-current-row': ->
@foldCurrentRow()
@scrollToCursorPosition()
'editor:unfold-current-row': ->
@unfoldCurrentRow()
@scrollToCursorPosition()
'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: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:copy-path': -> copyPathToClipboard(this, project, clipboard, false)
'editor:copy-project-path': -> copyPathToClipboard(this, project, clipboard, true)

View File

@@ -2,7 +2,7 @@
# 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}
# 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
@@ -39,11 +39,17 @@ class ScopeDescriptor
getScopesArray: -> @scopes
getScopeChain: ->
@scopes
.map (scope) ->
scope = ".#{scope}" unless scope[0] is '.'
scope
.join(' ')
# For backward compatibility, prefix TextMate-style scope names with
# leading dots (e.g. 'source.js' -> '.source.js').
if @scopes[0]?.includes('.')
result = ''
for scope, i in @scopes
result += ' ' if i > 0
result += '.' if scope[0] isnt '.'
result += scope
result
else
@scopes.join(' ')
toString: ->
@getScopeChain()

Some files were not shown because too many files have changed in this diff Show More