mirror of
https://github.com/atom/atom.git
synced 2026-04-06 03:02:13 -04:00
Merge branch 'master' of https://github.com/atom/atom into b3-failing-seed
This commit is contained in:
17
.pairs
17
.pairs
@@ -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
|
||||
@@ -1,3 +1,8 @@
|
||||
language: python
|
||||
|
||||
python:
|
||||
- "2.7.13"
|
||||
|
||||
git:
|
||||
depth: 10
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
228
CONTRIBUTING.md
228
CONTRIBUTING.md
@@ -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
|
||||
|
||||

|
||||
|
||||
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/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
28
README.md
28
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
If you're looking for support for Atom there are a lot of options, check out:
|
||||
|
||||
* User Documentation — [The Atom Flight Manual](http://flight-manual.atom.io)
|
||||
* User Documentation — [The Atom Flight Manual](https://flight-manual.atom.io)
|
||||
* Developer Documentation — [Atom API Documentation](https://atom.io/docs/api/latest)
|
||||
* FAQ — [The Atom FAQ on Discuss](https://discuss.atom.io/c/faq)
|
||||
* Message Board — [Discuss, the official Atom and Electron message board](https://discuss.atom.io)
|
||||
* Chat — [Join the Atom Slack team](http://atom-slack.herokuapp.com/)
|
||||
* Chat — [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.
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"url": "https://github.com/atom/atom.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"atom-package-manager": "1.18.10"
|
||||
"atom-package-manager": "1.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
12
appveyor.yml
12
appveyor.yml
@@ -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
34
atom.sh
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||

|
||||
|
||||
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).
|
||||
|
||||
@@ -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/)
|
||||
@@ -114,4 +114,4 @@
|
||||
| [TODO](https://github.com/atom/language-todo) | [](https://travis-ci.org/atom/language-todo) | [](https://ci.appveyor.com/project/Atom/language-todo/branch/master) |
|
||||
| [TOML](https://github.com/atom/language-toml) | [](https://travis-ci.org/atom/language-toml) | [](https://ci.appveyor.com/project/Atom/language-toml/branch/master) |
|
||||
| [XML](https://github.com/atom/language-xml) | [](https://travis-ci.org/atom/language-xml) | [](https://ci.appveyor.com/project/Atom/language-xml/branch/master) |
|
||||
| [YAML](https://github/atom/language-yaml) | [](https://travis-ci.org/atom/language-yaml) | [](https://ci.appveyor.com/project/Atom/language-yaml/branch/master) |
|
||||
| [YAML](https://github.com/atom/language-yaml) | [](https://travis-ci.org/atom/language-yaml) | [](https://ci.appveyor.com/project/Atom/language-yaml/branch/master) |
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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'
|
||||
|
||||
127
package.json
127
package.json
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
18
script/lib/clean-package-lock.js
Normal file
18
script/lib/clean-package-lock.js
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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",
|
||||
|
||||
18
script/test
18
script/test
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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.'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
1
spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/fake-parser.js
vendored
Normal file
1
spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/fake-parser.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
exports.isFakeTreeSitterParser = true
|
||||
14
spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/some-language.cson
vendored
Normal file
14
spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/some-language.cson
vendored
Normal 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'
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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}))
|
||||
|
||||
497
spec/grammar-registry-spec.js
Normal file
497
spec/grammar-registry-spec.js
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'])
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
77
spec/syntax-scope-map-spec.js
Normal file
77
spec/syntax-scope-map-spec.js
Normal 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
@@ -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', () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
1040
spec/text-mate-language-mode-spec.js
Normal file
1040
spec/text-mate-language-mode-spec.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'])
|
||||
})
|
||||
)
|
||||
@@ -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([])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
560
spec/tree-sitter-language-mode-spec.js
Normal file
560
spec/tree-sitter-language-mode-spec.js
Normal 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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, '\\$&')
|
||||
}
|
||||
|
||||
@@ -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
374
src/application-delegate.js
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.'
|
||||
}, () => {})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
225
src/main-process/application-menu.js
Normal file
225
src/main-process/application-menu.js
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
1376
src/main-process/atom-application.js
Normal file
1376
src/main-process/atom-application.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
54
src/main-process/atom-protocol-handler.js
Normal file
54
src/main-process/atom-protocol-handler.js
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
432
src/main-process/atom-window.js
Normal file
432
src/main-process/atom-window.js
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
1107
src/package.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
|
||||
140
src/pane.js
140
src/pane.js
@@ -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.
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user