diff --git a/.gitignore b/.gitignore
index 6eec21c2a..bce6c56d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,4 @@ debug.log
docs/output
docs/includes
spec/fixtures/evil-files/
+out/
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 444ce0b4c..c7d7eeb14 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,24 +1,46 @@
-# Contributor Code of Conduct
+# Contributor Covenant Code of Conduct
-As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
+## Our Pledge
-We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
-- The use of sexualized language or imagery
-- Personal attacks
-- Trolling or insulting/derogatory comments
-- Public or private harassment
-- Publishing other's private information, such as physical or electronic addresses, without explicit permission
-- Other unethical or unprofessional conduct
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
-By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
+## Scope
-This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
-Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at [atom@github.com](mailto:atom@github.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident.
+## Enforcement
-This Code of Conduct is adapted from the Contributor Covenant, version 1.3.0, available from http://contributor-covenant.org/version/1/3/0/
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [atom@github.com](mailto:atom@github.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ada420a40..ff48740b2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -50,7 +50,7 @@ To get a sense for the packages that are bundled with Atom, you can go to Settin
Here's a list of the big ones:
-* [atom/atom](https://github.com/atom/atom) - Atom Core! The core editor component is responsible for basic text editing (e.g. cursors, selections, scrolling), text indentation, wrapping, and folding, text rendering, editor rendering, file system operations (e.g. saving), and installation and auto-updating. You should also use this repository for feedback related to the [core API](https://atom.io/docs/api/latest/Notification) and for large, overarching design proposals.
+* [atom/atom](https://github.com/atom/atom) - Atom Core! The core editor component is responsible for basic text editing (e.g. cursors, selections, scrolling), text indentation, wrapping, and folding, text rendering, editor rendering, file system operations (e.g. saving), and installation and auto-updating. You should also use this repository for feedback related to the [core API](https://atom.io/docs/api/latest) and for large, overarching design proposals.
* [tree-view](https://github.com/atom/tree-view) - file and directory listing on the left of the UI.
* [fuzzy-finder](https://github.com/atom/fuzzy-finder) - the quick file opener.
* [find-and-replace](https://github.com/atom/find-and-replace) - all search and replace functionality.
@@ -62,7 +62,7 @@ Here's a list of the big ones:
* [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](https://github.com/atom/solarized-dark). You should use these packages for reporting issues that appear in many languages, but disappear if you change to another syntax theme.
+* [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.
* [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).
@@ -82,7 +82,7 @@ Before creating bug reports, please check [this list](#before-submitting-a-bug-r
#### Before Submitting A Bug Report
-* **Check the [debugging guide](https://atom.io/docs/latest/hacking-atom-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://atom.io/docs/latest/hacking-atom-debugging#update-to-the-latest-version), if the problem happens when you run Atom in [safe mode](https://atom.io/docs/latest/hacking-atom-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://atom.io/docs/latest/hacking-atom-debugging#check-atom-and-package-settings).
+* **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 [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, add a comment to the existing issue instead of opening a new one.
@@ -100,13 +100,13 @@ Explain the problem and include additional details to help maintainers reproduce
* **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 OSX 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 OSX, 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**, include a [CPU profile capture and a screenshot](https://atom.io/docs/latest/hacking-atom-debugging#diagnose-performance-problems-with-the-dev-tools-cpu-profiler) with your report.
+* **If the problem is related to performance**, include a [CPU profile capture and a screenshot](http://flight-manual.atom.io/hacking-atom/sections/debugging/#diagnose-performance-problems-with-the-dev-tools-cpu-profiler) with your report.
* **If the Chrome's developer tools pane is shown without you triggering it**, that normally means that an exception was thrown. The Console tab will include an entry for the exception. Expand the exception so that the stack trace is visible, and provide the full exception and stack trace in a [code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines) and as a screenshot.
* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
Provide more context by answering these questions:
-* **Can you reproduce the problem in [safe mode](https://atom.io/docs/latest/hacking-atom-debugging#check-if-the-problem-shows-up-in-safe-mode)?**
+* **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)?**
* **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.
@@ -118,7 +118,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](https://atom.io/docs/latest/using-atom-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](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 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?
@@ -166,7 +166,7 @@ Before creating enhancement suggestions, please check [this list](#before-submit
#### Before Submitting An Enhancement Suggestion
-* **Check the [debugging guide](https://atom.io/docs/latest/hacking-atom-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://atom.io/docs/latest/hacking-atom-debugging#update-to-the-latest-version) and if you can get the desired behavior by changing [Atom's or packages' config settings](https://atom.io/docs/latest/hacking-atom-debugging#check-atom-and-package-settings).
+* **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 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.
@@ -365,7 +365,7 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and
| `blocked` | [search][search-atom-repo-label-blocked] | [search][search-atom-org-label-blocked] | Issues blocked on other issues. |
| `duplicate` | [search][search-atom-repo-label-duplicate] | [search][search-atom-org-label-duplicate] | Issues which are duplicates of other issues, i.e. they have been reported before. |
| `wontfix` | [search][search-atom-repo-label-wontfix] | [search][search-atom-org-label-wontfix] | The Atom core team has decided not to fix these issues for now, either because they're working as intended or for some other reason. |
-| `invalid` | [search][search-atom-repo-label-invalid] | [search][search-atom-org-label-invalid] | Issues which are't valid (e.g. user errors). |
+| `invalid` | [search][search-atom-repo-label-invalid] | [search][search-atom-org-label-invalid] | Issues which aren't valid (e.g. user errors). |
| `package-idea` | [search][search-atom-repo-label-package-idea] | [search][search-atom-org-label-package-idea] | Feature request which might be good candidates for new packages, instead of extending Atom or core Atom packages. |
| `wrong-repo` | [search][search-atom-repo-label-wrong-repo] | [search][search-atom-org-label-wrong-repo] | Issues reported on the wrong repository (e.g. a bug related to the [Settings View package](https://github.com/atom/settings-view) was reported on [Atom core](https://github.com/atom/atom)). |
@@ -376,7 +376,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 OSX. |
-| `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/Atom) and the [flight manual](https://atom.io/docs/latest/)). |
+| `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/)). |
| `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. |
@@ -406,10 +406,6 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and
| Label name | `atom/atom` :mag_right: | `atom`‑org :mag_right: | Description |
| --- | --- | --- | --- |
-| `in-progress` | [search][search-atom-repo-label-in-progress] | [search][search-atom-org-label-in-progress] | Tasks which the Atom core team is working on currently. |
-| `on-deck` | [search][search-atom-repo-label-on-deck] | [search][search-atom-org-label-on-deck] | Tasks which the Atom core team plans to work on next. |
-| `shipping` | [search][search-atom-repo-label-shipping] | [search][search-atom-org-label-shipping] | Tasks which the Atom core team completed and will be released in one of the next releases. |
-| `post-1.0-roadmap` | [search][search-atom-repo-label-post-1.0-roadmap] | [search][search-atom-org-label-post-1.0-roadmap] | The Atom core team's roadmap post version 1.0.0. |
| `atom` | [search][search-atom-repo-label-atom] | [search][search-atom-org-label-atom] | Topics discussed for prioritization at the next meeting of Atom core team members. |
#### Pull Request Labels
@@ -498,14 +494,6 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and
[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-on-deck]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aon-deck
-[search-atom-org-label-on-deck]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aon-deck
-[search-atom-repo-label-in-progress]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ain-progress
-[search-atom-org-label-in-progress]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ain-progress
-[search-atom-repo-label-shipping]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Ashipping
-[search-atom-org-label-shipping]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Ashipping
-[search-atom-repo-label-post-1.0-roadmap]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Apost-1.0-roadmap
-[search-atom-org-label-post-1.0-roadmap]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Apost-1.0-roadmap
[search-atom-repo-label-atom]: https://github.com/issues?q=is%3Aopen+is%3Aissue+repo%3Aatom%2Fatom+label%3Aatom
[search-atom-org-label-atom]: https://github.com/issues?q=is%3Aopen+is%3Aissue+user%3Aatom+label%3Aatom
[search-atom-repo-label-work-in-progress]: https://github.com/pulls?q=is%3Aopen+is%3Apr+repo%3Aatom%2Fatom+label%3Awork-in-progress
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
new file mode 100644
index 000000000..5b3ea42f4
--- /dev/null
+++ b/ISSUE_TEMPLATE.md
@@ -0,0 +1,28 @@
+### Prerequisites
+
+* [ ] Can you reproduce the problem in [safe mode](http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-if-the-problem-shows-up-in-safe-mode)?
+* [ ] Are you running the [latest version of Atom](http://flight-manual.atom.io/hacking-atom/sections/debugging/#update-to-the-latest-version)?
+* [ ] Did you check the [debugging guide](http://flight-manual.atom.io/hacking-atom/sections/debugging/)?
+* [ ] Did you check the [FAQs on Discuss](https://discuss.atom.io/c/faq)?
+* [ ] Are you reporting to the [correct repository](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#atom-and-packages)?
+* [ ] Did you [perform a cursory search](https://github.com/issues?q=is%3Aissue+user%3Aatom+-repo%3Aatom%2Felectron) to see if your bug or enhancement is already reported?
+
+For more information on how to write a good [bug report](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#how-do-i-submit-a-good-bug-report) or [enhancement request](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#how-do-i-submit-a-good-enhancement-suggestion), see the `CONTRIBUTING` guide.
+
+### Description
+
+[Description of the bug or feature]
+
+### Steps to Reproduce
+
+1. [First Step]
+2. [Second Step]
+3. [and so on...]
+
+**Expected behavior:** [What you expected to happen]
+
+**Actual behavior:** [What actually happened]
+
+### Versions
+
+You can get this information from executing `atom --version` and `apm --version` at the command line. Also, please include the OS and what version of the OS you're running.
diff --git a/README.md b/README.md
index dcda2146c..20e940689 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,22 @@ Currently only a 64-bit version is available.
The Linux version does not currently automatically update so you will need to
repeat these steps to upgrade to future releases.
+### Archive extraction
+
+An archive is available for people who don't want to install `atom` as root.
+
+This version enables you to install multiple Atom versions in parallel. It has been built on Ubuntu 64-bit,
+but should be compatible with other Linux distributions.
+
+1. Install dependencies (on Ubuntu): `sudo apt install git gconf2 gconf-service libgtk2.0-0 libudev1 libgcrypt20
+libnotify4 libxtst6 libnss3 python gvfs-bin xdg-utils libcap2`
+2. Download `atom-amd64.tar.gz` from the [Atom releases page](https://github.com/atom/atom/releases/latest).
+3. Run `tar xf atom-amd64.tar.gz` in the directory where you want to extract the Atom folder.
+4. Launch Atom using the installed `atom` command from the newly extracted directory.
+
+The Linux version does not currently automatically update so you will need to
+repeat these steps to upgrade to future releases.
+
## Building
* [Linux](docs/build-instructions/linux.md)
diff --git a/apm/package.json b/apm/package.json
index 2e6b0b8ea..d4fcc851a 100644
--- a/apm/package.json
+++ b/apm/package.json
@@ -6,6 +6,6 @@
"url": "https://github.com/atom/atom.git"
},
"dependencies": {
- "atom-package-manager": "1.6.0"
+ "atom-package-manager": "1.10.0"
}
}
diff --git a/atom.sh b/atom.sh
index ef8dbcdc4..b68716bf4 100755
--- a/atom.sh
+++ b/atom.sh
@@ -4,8 +4,6 @@ if [ "$(uname)" == 'Darwin' ]; then
OS='Mac'
elif [ "$(expr substr $(uname -s) 1 5)" == 'Linux' ]; then
OS='Linux'
-elif [ "$(expr substr $(uname -s) 1 10)" == 'MINGW32_NT' ]; then
- OS='Cygwin'
else
echo "Your platform ($(uname -a)) is not supported."
exit 1
diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee
index a34e1fbd4..16e0ed5d6 100644
--- a/build/Gruntfile.coffee
+++ b/build/Gruntfile.coffee
@@ -34,23 +34,10 @@ module.exports = (grunt) ->
grunt.file.setBase(path.resolve('..'))
# Options
+ [defaultChannel, releaseBranch] = getDefaultChannelAndReleaseBranch(packageJson.version)
installDir = grunt.option('install-dir')
- buildDir = grunt.option('build-dir')
- buildDir ?= 'out'
- buildDir = path.resolve(buildDir)
-
- channel = grunt.option('channel')
- releasableBranches = ['stable', 'beta']
- if process.env.APPVEYOR and not process.env.APPVEYOR_PULL_REQUEST_NUMBER
- channel ?= process.env.APPVEYOR_REPO_BRANCH if process.env.APPVEYOR_REPO_BRANCH in releasableBranches
-
- if process.env.TRAVIS and not process.env.TRAVIS_PULL_REQUEST
- channel ?= process.env.TRAVIS_BRANCH if process.env.TRAVIS_BRANCH in releasableBranches
-
- if process.env.JANKY_BRANCH
- channel ?= process.env.JANKY_BRANCH if process.env.JANKY_BRANCH in releasableBranches
-
- channel ?= 'dev'
+ buildDir = path.resolve(grunt.option('build-dir') ? 'out')
+ channel = grunt.option('channel') ? defaultChannel
metadata = packageJson
appName = packageJson.productName
@@ -70,7 +57,7 @@ module.exports = (grunt) ->
homeDir = process.env.USERPROFILE
contentsDir = shellAppDir
appDir = path.join(shellAppDir, 'resources', 'app')
- installDir ?= path.join(process.env.ProgramFiles, appName)
+ installDir ?= path.join(process.env.LOCALAPPDATA, appName, 'app-dev')
killCommand = 'taskkill /F /IM atom.exe'
else if process.platform is 'darwin'
homeDir = process.env.HOME
@@ -189,7 +176,7 @@ module.exports = (grunt) ->
pkg: grunt.file.readJSON('package.json')
atom: {
- appName, channel, metadata,
+ appName, channel, metadata, releaseBranch,
appFileName, apmFileName,
appDir, buildDir, contentsDir, installDir, shellAppDir, symbolsDir,
}
@@ -298,6 +285,7 @@ module.exports = (grunt) ->
ciTasks.push('dump-symbols') if process.platform is 'darwin'
ciTasks.push('set-version', 'check-licenses', 'lint', 'generate-asar')
ciTasks.push('mkdeb') if process.platform is 'linux'
+ ciTasks.push('mktar') if process.platform is 'linux'
ciTasks.push('codesign:exe') if process.platform is 'win32' and not process.env.CI
ciTasks.push('create-windows-installer:installer') if process.platform is 'win32'
ciTasks.push('test') if process.platform is 'darwin'
@@ -310,3 +298,21 @@ module.exports = (grunt) ->
unless process.platform is 'linux' or grunt.option('no-install')
defaultTasks.push 'install'
grunt.registerTask('default', defaultTasks)
+ grunt.registerTask('build-and-sign', ['download-electron', 'download-electron-chromedriver', 'build', 'set-version', 'generate-asar', 'codesign:app', 'install'])
+
+getDefaultChannelAndReleaseBranch = (version) ->
+ if version.match(/dev/) or isBuildingPR()
+ channel = 'dev'
+ releaseBranch = null
+ else
+ if version.match(/beta/)
+ channel = 'beta'
+ else
+ channel = 'stable'
+
+ minorVersion = version.match(/^\d\.\d/)[0]
+ releaseBranch = "#{minorVersion}-releases"
+ [channel, releaseBranch]
+
+isBuildingPR = ->
+ process.env.APPVEYOR_PULL_REQUEST_NUMBER? or process.env.TRAVIS_PULL_REQUEST?
diff --git a/build/certs/AtomDevTestSignKey.p12 b/build/certs/AtomDevTestSignKey.p12
new file mode 100644
index 000000000..a93e9a9f0
Binary files /dev/null and b/build/certs/AtomDevTestSignKey.p12 differ
diff --git a/build/package.json b/build/package.json
index 6c41d4355..2f8d88d8a 100644
--- a/build/package.json
+++ b/build/package.json
@@ -25,7 +25,7 @@
"grunt-contrib-less": "~0.8.0",
"grunt-cson": "0.16.0",
"grunt-download-electron": "^2.1.1",
- "grunt-electron-installer": "1.0.6",
+ "grunt-electron-installer": "1.2.2",
"grunt-lesslint": "0.17.0",
"grunt-peg": "~1.1.0",
"grunt-shell": "~0.3.1",
diff --git a/build/tasks/build-task.coffee b/build/tasks/build-task.coffee
index 9164f8dab..ff61d2ba8 100644
--- a/build/tasks/build-task.coffee
+++ b/build/tasks/build-task.coffee
@@ -54,9 +54,9 @@ module.exports = (grunt) ->
# so that it doesn't becomes larger than it needs to be.
ignoredPaths = [
path.join('git-utils', 'deps')
- path.join('nodegit', 'vendor')
- path.join('nodegit', 'node_modules', 'node-pre-gyp')
- path.join('nodegit', 'node_modules', '.bin')
+ path.join('ohnogit', 'node_modules', 'nodegit', 'vendor')
+ path.join('ohnogit', 'node_modules', 'nodegit', 'node_modules', 'node-pre-gyp')
+ path.join('ohnogit', 'node_modules', 'nodegit', 'node_modules', '.bin')
path.join('oniguruma', 'deps')
path.join('less', 'dist')
path.join('bootstrap', 'docs')
@@ -122,9 +122,9 @@ module.exports = (grunt) ->
# Ignore *.cc and *.h files from native modules
ignoredPaths.push "#{_.escapeRegExp(path.join('ctags', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('git-utils', 'src') + path.sep)}.*\\.(cc|h)*"
- ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'src') + path.sep)}.*\\.(cc|h)?"
- ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'generate') + path.sep)}.*\\.(cc|h)?"
- ignoredPaths.push "#{_.escapeRegExp(path.join('nodegit', 'include') + path.sep)}.*\\.(cc|h)?"
+ ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'src') + path.sep)}.*\\.(cc|h)?"
+ ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'generate') + path.sep)}.*\\.(cc|h)?"
+ ignoredPaths.push "#{_.escapeRegExp(path.join('ohnogit', 'node_modules', 'nodegit', 'include') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('keytar', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('nslog', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('oniguruma', 'src') + path.sep)}.*\\.(cc|h)*"
@@ -133,7 +133,6 @@ module.exports = (grunt) ->
ignoredPaths.push "#{_.escapeRegExp(path.join('scrollbar-style', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('spellchecker', 'src') + path.sep)}.*\\.(cc|h)*"
ignoredPaths.push "#{_.escapeRegExp(path.join('cached-run-in-this-context', 'src') + path.sep)}.*\\.(cc|h)?"
- ignoredPaths.push "#{_.escapeRegExp(path.join('marker-index', 'src') + path.sep)}.*\\.(cc|h)?"
ignoredPaths.push "#{_.escapeRegExp(path.join('keyboard-layout', 'src') + path.sep)}.*\\.(cc|h|mm)*"
# Ignore build files
diff --git a/build/tasks/codesign-task.coffee b/build/tasks/codesign-task.coffee
index 6c99795c0..559d41bbf 100644
--- a/build/tasks/codesign-task.coffee
+++ b/build/tasks/codesign-task.coffee
@@ -1,39 +1,71 @@
path = require 'path'
+fs = require 'fs'
+request = require 'request'
module.exports = (grunt) ->
{spawn} = require('./task-helpers')(grunt)
- grunt.registerTask 'codesign:exe', 'Codesign atom.exe and Update.exe', ->
+ signUsingWindowsSDK = (exeToSign, callback) ->
+ {WIN_P12KEY_PASSWORD, WIN_P12KEY_URL} = process.env
+ if WIN_P12KEY_URL?
+ grunt.log.ok("Obtaining signing key")
+ downloadedKeyFile = path.resolve(__dirname, 'DownloadedSignKey.p12')
+ downloadFile WIN_P12KEY_URL, downloadedKeyFile, (done) ->
+ signUsingWindowsSDKTool exeToSign, downloadedKeyFile, WIN_P12KEY_PASSWORD, (done) ->
+ fs.unlinkSync(downloadedKeyFile)
+ callback()
+ else
+ signUsingWindowsSDKTool exeToSign, path.resolve(__dirname, '..', 'certs', 'AtomDevTestSignKey.p12'), 'password', callback
+
+ signUsingWindowsSDKTool = (exeToSign, keyFilePath, password, callback) ->
+ grunt.log.ok("Signing #{exeToSign}")
+ args = ['sign', '/v', '/p', password, '/f', keyFilePath, exeToSign]
+ spawn {cmd: 'C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v7.1A\\bin\\signtool.exe', args: args}, callback
+
+ signUsingJanky = (exeToSign, callback) ->
+ spawn {cmd: process.env.JANKY_SIGNTOOL, args: [exeToSign]}, callback
+
+ signWindowsExecutable = if process.env.JANKY_SIGNTOOL then signUsingJanky else signUsingWindowsSDK
+
+ grunt.registerTask 'codesign:exe', 'CodeSign Atom.exe and Update.exe', ->
done = @async()
spawn {cmd: 'taskkill', args: ['/F', '/IM', 'atom.exe']}, ->
- cmd = process.env.JANKY_SIGNTOOL ? 'signtool'
atomExePath = path.join(grunt.config.get('atom.shellAppDir'), 'atom.exe')
- spawn {cmd, args: [atomExePath]}, (error) ->
+ signWindowsExecutable atomExePath, (error) ->
return done(error) if error?
updateExePath = path.resolve(__dirname, '..', 'node_modules', 'grunt-electron-installer', 'vendor', 'Update.exe')
- spawn {cmd, args: [updateExePath]}, (error) -> done(error)
+ signWindowsExecutable updateExePath, (error) -> done(error)
- grunt.registerTask 'codesign:installer', 'Codesign AtomSetup.exe', ->
+ grunt.registerTask 'codesign:installer', 'CodeSign AtomSetup.exe', ->
done = @async()
- cmd = process.env.JANKY_SIGNTOOL ? 'signtool'
atomSetupExePath = path.resolve(grunt.config.get('atom.buildDir'), 'installer', 'AtomSetup.exe')
- spawn {cmd, args: [atomSetupExePath]}, (error) -> done(error)
+ signWindowsExecutable atomSetupExePath, (error) -> done(error)
- grunt.registerTask 'codesign:app', 'Codesign Atom.app', ->
+ grunt.registerTask 'codesign:app', 'CodeSign Atom.app', ->
done = @async()
unlockKeychain (error) ->
return done(error) if error?
- cmd = 'codesign'
args = ['--deep', '--force', '--verbose', '--sign', 'Developer ID Application: GitHub', grunt.config.get('atom.shellAppDir')]
- spawn {cmd, args}, (error) -> done(error)
+ spawn {cmd: 'codesign', args: args}, (error) -> done(error)
unlockKeychain = (callback) ->
return callback() unless process.env.XCODE_KEYCHAIN
- cmd = 'security'
{XCODE_KEYCHAIN_PASSWORD, XCODE_KEYCHAIN} = process.env
args = ['unlock-keychain', '-p', XCODE_KEYCHAIN_PASSWORD, XCODE_KEYCHAIN]
- spawn {cmd, args}, (error) -> callback(error)
+ spawn {cmd: 'security', args: args}, (error) -> callback(error)
+
+ downloadFile = (sourceUrl, targetPath, callback) ->
+ options = {
+ url: sourceUrl
+ headers: {
+ 'User-Agent': 'Atom Signing Key build task',
+ 'Accept': 'application/vnd.github.VERSION.raw'
+ }
+ }
+ request(options)
+ .pipe(fs.createWriteStream(targetPath))
+ .on('finish', callback)
diff --git a/build/tasks/install-task.coffee b/build/tasks/install-task.coffee
index 2d9054385..19fd3d383 100644
--- a/build/tasks/install-task.coffee
+++ b/build/tasks/install-task.coffee
@@ -16,10 +16,22 @@ module.exports = (grunt) ->
{description} = grunt.config.get('atom.metadata')
if process.platform is 'win32'
- runas ?= require 'runas'
- copyFolder = path.resolve 'script', 'copy-folder.cmd'
- if runas('cmd', ['/c', copyFolder, shellAppDir, installDir], admin: true) isnt 0
- grunt.log.error("Failed to copy #{shellAppDir} to #{installDir}")
+ done = @async()
+ fs.access(installDir, fs.W_OK, (err) ->
+ adminRequired = true if err
+ if adminRequired
+ grunt.log.ok("User does not have write access to #{installDir}, elevating to admin")
+ runas ?= require 'runas'
+ copyFolder = path.resolve 'script', 'copy-folder.cmd'
+
+ if runas('cmd', ['/c', copyFolder, shellAppDir, installDir], admin: adminRequired) isnt 0
+ grunt.log.error("Failed to copy #{shellAppDir} to #{installDir}")
+ else
+ grunt.log.ok("Installed into #{installDir}")
+
+ done()
+ )
+
else if process.platform is 'darwin'
rm installDir
mkdir path.dirname(installDir)
diff --git a/build/tasks/license-overrides.coffee b/build/tasks/license-overrides.coffee
index aa136eb37..be56934a2 100644
--- a/build/tasks/license-overrides.coffee
+++ b/build/tasks/license-overrides.coffee
@@ -123,10 +123,10 @@ module.exports =
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
- 'tweetnacl@0.13.2':
+ 'tweetnacl@0.14.3':
repository: 'https://github.com/dchest/tweetnacl-js'
license: 'Public Domain'
- source: 'https://github.com/dchest/tweetnacl-js/blob/2f328394f74d83564634fb89ea2798caa3a4edb9/README.md says public domain.'
+ source: 'https://github.com/dchest/tweetnacl-js/blob/1042c9c65dc8f1dcb9e981d962d7dbbcf58f1fdc/COPYING.txt says public domain.'
'json-schema@0.2.2':
repository: 'https://github.com/kriszyp/json-schema'
license: 'BSD'
diff --git a/build/tasks/mktar-task.coffee b/build/tasks/mktar-task.coffee
new file mode 100644
index 000000000..f5edbb4be
--- /dev/null
+++ b/build/tasks/mktar-task.coffee
@@ -0,0 +1,30 @@
+path = require 'path'
+
+module.exports = (grunt) ->
+ {spawn, fillTemplate} = require('./task-helpers')(grunt)
+
+ grunt.registerTask 'mktar', 'Create an archive', ->
+ done = @async()
+
+ appFileName = grunt.config.get('atom.appFileName')
+ buildDir = grunt.config.get('atom.buildDir')
+ shellAppDir = grunt.config.get('atom.shellAppDir')
+ {version, description} = grunt.config.get('atom.metadata')
+
+ if process.arch is 'ia32'
+ arch = 'i386'
+ else if process.arch is 'x64'
+ arch = 'amd64'
+ else
+ return done("Unsupported arch #{process.arch}")
+
+ iconPath = path.join(shellAppDir, 'resources', 'app.asar.unpacked', 'resources', 'atom.png')
+
+ cmd = path.join('script', 'mktar')
+ args = [appFileName, version, arch, iconPath, buildDir]
+ spawn {cmd, args}, (error) ->
+ if error?
+ done(error)
+ else
+ grunt.log.ok "Created " + path.join(buildDir, "#{appFileName}-#{version}-#{arch}.tar.gz")
+ done()
diff --git a/build/tasks/publish-build-task.coffee b/build/tasks/publish-build-task.coffee
index 4f8df6336..0a18c9c23 100644
--- a/build/tasks/publish-build-task.coffee
+++ b/build/tasks/publish-build-task.coffee
@@ -31,14 +31,9 @@ module.exports = (gruntObject) ->
cp path.join(docsOutputDir, 'api.json'), path.join(buildDir, 'atom-api.json')
grunt.registerTask 'upload-assets', 'Upload the assets to a GitHub release', ->
- channel = grunt.config.get('atom.channel')
- switch channel
- when 'stable'
- isPrerelease = false
- when 'beta'
- isPrerelease = true
- else
- return
+ releaseBranch = grunt.config.get('atom.releaseBranch')
+ isPrerelease = grunt.config.get('atom.channel') is 'beta'
+ return unless releaseBranch?
doneCallback = @async()
startTime = Date.now()
@@ -55,7 +50,7 @@ module.exports = (gruntObject) ->
zipAssets buildDir, assets, (error) ->
return done(error) if error?
- getAtomDraftRelease isPrerelease, channel, (error, release) ->
+ getAtomDraftRelease isPrerelease, releaseBranch, (error, release) ->
return done(error) if error?
assetNames = (asset.assetName for asset in assets)
deleteExistingAssets release, assetNames, (error) ->
@@ -79,7 +74,7 @@ getAssets = ->
]
when 'win32'
assets = [{assetName: 'atom-windows.zip', sourcePath: appName}]
- for squirrelAsset in ['AtomSetup.exe', 'RELEASES', "atom-#{version}-full.nupkg", "atom-#{version}-delta.nupkg"]
+ for squirrelAsset in ['AtomSetup.exe', 'AtomSetup.msi', 'RELEASES', "atom-#{version}-full.nupkg", "atom-#{version}-delta.nupkg"]
cp path.join(buildDir, 'installer', squirrelAsset), path.join(buildDir, squirrelAsset)
assets.push({assetName: squirrelAsset, sourcePath: assetName})
assets
@@ -90,13 +85,13 @@ getAssets = ->
arch = 'amd64'
# Check for a Debian build
- sourcePath = "#{buildDir}/#{appFileName}-#{version}-#{arch}.deb"
+ sourcePath = path.join(buildDir, "#{appFileName}-#{version}-#{arch}.deb")
assetName = "atom-#{arch}.deb"
# Check for a Fedora build
unless fs.isFileSync(sourcePath)
rpmName = fs.readdirSync("#{buildDir}/rpm")[0]
- sourcePath = "#{buildDir}/rpm/#{rpmName}"
+ sourcePath = path.join(buildDir, "rpm", rpmName)
if process.arch is 'ia32'
arch = 'i386'
else
@@ -104,10 +99,17 @@ getAssets = ->
assetName = "atom.#{arch}.rpm"
cp sourcePath, path.join(buildDir, assetName)
+ assets = [{assetName, sourcePath}]
- [
- {assetName, sourcePath}
- ]
+ # Check for an archive build on a debian build machine.
+ # We could provide a Fedora version if some libraries are not compatible
+ sourcePath = path.join(buildDir, "#{appFileName}-#{version}-#{arch}.tar.gz")
+ if fs.isFileSync(sourcePath)
+ assetName = "atom-#{arch}.tar.gz"
+ cp sourcePath, path.join(buildDir, assetName)
+ assets.push({assetName, sourcePath})
+
+ assets
logError = (message, error, details) ->
grunt.log.error(message)
diff --git a/build/tasks/set-version-task.coffee b/build/tasks/set-version-task.coffee
index fc2382476..c7a29b584 100644
--- a/build/tasks/set-version-task.coffee
+++ b/build/tasks/set-version-task.coffee
@@ -5,9 +5,7 @@ module.exports = (grunt) ->
{spawn} = require('./task-helpers')(grunt)
getVersion = (callback) ->
- releasableBranches = ['stable', 'beta']
- channel = grunt.config.get('atom.channel')
- shouldUseCommitHash = if channel in releasableBranches then false else true
+ shouldUseCommitHash = grunt.config.get('atom.channel') is 'dev'
inRepository = fs.existsSync(path.resolve(__dirname, '..', '..', '.git'))
{version} = require(path.join(grunt.config.get('atom.appDir'), 'package.json'))
if shouldUseCommitHash and inRepository
diff --git a/build/tasks/spec-task.coffee b/build/tasks/spec-task.coffee
index 892c92696..c1067231c 100644
--- a/build/tasks/spec-task.coffee
+++ b/build/tasks/spec-task.coffee
@@ -1,5 +1,6 @@
fs = require 'fs'
path = require 'path'
+temp = require('temp').track()
_ = require 'underscore-plus'
async = require 'async'
@@ -16,20 +17,6 @@ module.exports = (grunt) ->
packageSpecQueue = null
- logDeprecations = (label, {stderr}={}) ->
- return unless process.env.JANKY_SHA1 or process.env.CI
- stderr ?= ''
- deprecatedStart = stderr.indexOf('Calls to deprecated functions')
- return if deprecatedStart is -1
-
- grunt.log.error(label)
- stderr = stderr.substring(deprecatedStart)
- stderr = stderr.replace(/^\s*\[[^\]]+\]\s+/gm, '')
- stderr = stderr.replace(/source: .*$/gm, '')
- stderr = stderr.replace(/^"/gm, '')
- stderr = stderr.replace(/",\s*$/gm, '')
- grunt.log.error(stderr)
-
getAppPath = ->
contentsDir = grunt.config.get('atom.contentsDir')
switch process.platform
@@ -56,14 +43,14 @@ module.exports = (grunt) ->
args: ['--test', "--resource-path=#{resourcePath}", path.join(packagePath, 'spec')]
opts:
cwd: packagePath
- env: _.extend({}, process.env, ATOM_PATH: rootDir)
+ env: _.extend({}, process.env, ELECTRON_ENABLE_LOGGING: true, ATOM_PATH: rootDir)
else if process.platform is 'win32'
options =
cmd: process.env.comspec
args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", "--log-file=ci.log", path.join(packagePath, 'spec')]
opts:
cwd: packagePath
- env: _.extend({}, process.env, ATOM_PATH: rootDir)
+ env: _.extend({}, process.env, ELECTRON_ENABLE_LOGGING: true, ATOM_PATH: rootDir)
grunt.log.ok "Launching #{path.basename(packagePath)} specs."
spawn options, (error, results, code) ->
@@ -73,7 +60,6 @@ module.exports = (grunt) ->
fs.unlinkSync(path.join(packagePath, 'ci.log'))
failedPackages.push path.basename(packagePath) if error
- logDeprecations("#{path.basename(packagePath)} Specs", results)
callback()
modulesDirectory = path.resolve('node_modules')
@@ -94,20 +80,18 @@ module.exports = (grunt) ->
if process.platform in ['darwin', 'linux']
options =
cmd: appPath
- args: ['--test', "--resource-path=#{resourcePath}", coreSpecsPath]
+ args: ['--test', "--resource-path=#{resourcePath}", coreSpecsPath, "--user-data-dir=#{temp.mkdirSync('atom-user-data-dir')}"]
opts:
- env: _.extend({}, process.env,
- ATOM_INTEGRATION_TESTS_ENABLED: true
- )
+ env: _.extend({}, process.env, {ELECTRON_ENABLE_LOGGING: true, ATOM_INTEGRATION_TESTS_ENABLED: true})
+ stdio: 'inherit'
else if process.platform is 'win32'
options =
cmd: process.env.comspec
args: ['/c', appPath, '--test', "--resource-path=#{resourcePath}", '--log-file=ci.log', coreSpecsPath]
opts:
- env: _.extend({}, process.env,
- ATOM_INTEGRATION_TESTS_ENABLED: true
- )
+ env: _.extend({}, process.env, {ELECTRON_ENABLE_LOGGING: true, ATOM_INTEGRATION_TESTS_ENABLED: true})
+ stdio: 'inherit'
grunt.log.ok "Launching core specs."
spawn options, (error, results, code) ->
@@ -117,7 +101,6 @@ module.exports = (grunt) ->
else
# TODO: Restore concurrency on Windows
packageSpecQueue?.concurrency = concurrency
- logDeprecations('Core Specs', results)
callback(null, error)
diff --git a/build/tasks/task-helpers.coffee b/build/tasks/task-helpers.coffee
index d24cdec77..b42b4dd15 100644
--- a/build/tasks/task-helpers.coffee
+++ b/build/tasks/task-helpers.coffee
@@ -52,8 +52,10 @@ module.exports = (grunt) ->
stderr = []
error = null
proc = childProcess.spawn(options.cmd, options.args, options.opts)
- proc.stdout.on 'data', (data) -> stdout.push(data.toString())
- proc.stderr.on 'data', (data) -> stderr.push(data.toString())
+ if proc.stdout?
+ proc.stdout.on 'data', (data) -> stdout.push(data.toString())
+ if proc.stderr?
+ proc.stderr.on 'data', (data) -> stderr.push(data.toString())
proc.on 'error', (processError) -> error ?= processError
proc.on 'close', (exitCode, signal) ->
error ?= new Error(signal) if exitCode isnt 0
diff --git a/circle.yml b/circle.yml
new file mode 100644
index 000000000..a55900cca
--- /dev/null
+++ b/circle.yml
@@ -0,0 +1,4 @@
+general:
+ branches:
+ only:
+ - io-circle-ci
diff --git a/docs/build-instructions/linux.md b/docs/build-instructions/linux.md
index fc2f73ee7..126604c49 100644
--- a/docs/build-instructions/linux.md
+++ b/docs/build-instructions/linux.md
@@ -6,8 +6,8 @@ Ubuntu LTS 12.04 64-bit is the recommended platform.
* OS with 64-bit or 32-bit architecture
* C++ toolchain
- * [Git](http://git-scm.com/)
- * [Node.js](http://nodejs.org/download/) (0.10.x or above)
+ * [Git](https://git-scm.com/)
+ * [Node.js](https://nodejs.org/en/download/) (0.10.x or above)
* [npm](https://www.npmjs.com/) v1.4.x or above (automatically bundled with Node.js)
* `npm -v` to check the version.
* `npm config set python /usr/bin/python2 -g` to ensure that gyp uses python2.
@@ -64,7 +64,7 @@ If you have problems with permissions don't forget to prefix with `sudo`
script/build
```
- This will create the atom application at `$TMPDIR/atom-build/Atom`.
+ This will create the atom application at `out/Atom`.
4. Install the `atom` and `apm` commands to `/usr/local/bin` by executing:
@@ -74,18 +74,24 @@ If you have problems with permissions don't forget to prefix with `sudo`
To use the newly installed Atom, quit and restart all running Atom instances.
-5. *Optionally*, you may generate distributable packages of Atom at `$TMPDIR/atom-build`. Currently, `.deb` and `.rpm` package types are supported. To create a `.deb` package run:
+5. *Optionally*, you may generate distributable packages of Atom at `out`. Currently, `.deb` and `.rpm` package types are supported, as well as a `.tar.gz` archive. To create a `.deb` package run:
```sh
script/grunt mkdeb
```
- To create an `.rpm` package run
+ To create a `.rpm` package run
```sh
script/grunt mkrpm
```
+ To create a `.tar.gz` archive run
+
+ ```sh
+ script/grunt mktar
+ ```
+
## Advanced Options
### Custom build directory
diff --git a/docs/build-instructions/os-x.md b/docs/build-instructions/os-x.md
index 76da080c9..d9e15808b 100644
--- a/docs/build-instructions/os-x.md
+++ b/docs/build-instructions/os-x.md
@@ -3,7 +3,7 @@
## Requirements
* OS X 10.8 or later
- * [Node.js](http://nodejs.org/download/) (0.10.x or above)
+ * [Node.js](https://nodejs.org/en/download/) (0.10.x or above)
* Command Line Tools for [Xcode](https://developer.apple.com/xcode/downloads/) (run `xcode-select --install` to install)
## Instructions
diff --git a/docs/build-instructions/windows.md b/docs/build-instructions/windows.md
index fdd55fdde..d0e101ba0 100644
--- a/docs/build-instructions/windows.md
+++ b/docs/build-instructions/windows.md
@@ -3,7 +3,7 @@
## Requirements
### General
- * [Node.js](http://nodejs.org/en/download/) v4.x
+ * [Node.js](https://nodejs.org/en/download/) v4.x
* [Python](https://www.python.org/downloads/) v2.7.x
* The python.exe must be available at `%SystemDrive%\Python27\python.exe`.
If it is installed elsewhere, you can create a symbolic link to the
@@ -14,29 +14,27 @@
You can use either:
- * [Visual Studio 2013 Update 5](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Express or better) on Windows 7, 8 or 10
- * [Visual Studio 2015](http://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Community or better) with Windows 8 or 10
+ * [Visual Studio 2013 Update 5](https://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Express or better) on Windows 7, 8 or 10
+ * [Visual Studio 2015](https://www.visualstudio.com/en-us/downloads/download-visual-studio-vs) (Community or better) with Windows 8 or 10
Whichever version you use, ensure that:
* The default installation folder is chosen so the build tools can find it
* Visual C++ support is installed
- * You set the `GYP_MSVS_VERSION` environment variable to the Visual Studio version (`2013` or `2015`), e.g. , e.g. ``[Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", "2015", "User")`` in PowerShell or set it in Windows advanced system settings control panel.
- * The git command is in your path
+ * A `git` command is in your path
+ * If you have both VS2013 and VS2015 installed set the `GYP_MSVS_VERSION` environment variable to the Visual Studio version (`2013` or `2015`) you wish to use, e.g. ``[Environment]::SetEnvironmentVariable("GYP_MSVS_VERSION", "2015", "User")`` in PowerShell or set it in Windows advanced system settings control panel.
## Instructions
You can run these commands using Command Prompt, PowerShell or Git Shell via [GitHub Desktop](https://desktop.github.com/). These instructions will assume the use of Bash from Git Shell - if you are using Command Prompt use a backslash instead: i.e. `script\build`.
-**VS2015 + Git Shell users** should note that the default path supplied with Git Shell includes reference to an older version of msbuild that will fail. It is recommended you use a PowerShell window that has git in the path at this time.
-
```bash
cd C:\
git clone https://github.com/atom/atom/
cd atom
script/build
```
-This will create the Atom application in the `Program Files` folder.
+This will create the Atom application in the `out\Atom` folder as well as copy it to a subfolder of your user profile (e.g. `c:\Users\Bob`) called `AppData\Local\atom\app-dev`.
### `script/build` Options
* `--install-dir` - Creates the final built application in this directory. Example (trailing slash is optional):
@@ -47,6 +45,7 @@ This will create the Atom application in the `Program Files` folder.
```bash
./script/build --build-dir Z:\Some\Temporary\Directory\
```
+ * `--no-install` - Skips the installation task after building.
* `--verbose` - Verbose mode. A lot more information output.
## Do I have to use GitHub Desktop?
@@ -63,37 +62,34 @@ If none of this works, do install Github Desktop and use its Git Shell as it mak
### Common Errors
* `node is not recognized`
-
* If you just installed Node.js, you'll need to restart your PowerShell/Command Prompt/Git Shell before the node
command is available on your Path.
-* `script/build` outputs only the Node.js and Python versions before returning
+* `msbuild.exe failed with exit code: 1`
+ * Ensure you have Visual C++ support installed. Go into Add/Remove Programs, select Visual Studio and press Modify and then check the Visual C++ box.
+* `script/build` stops with no error or warning shortly after displaying the versions of node, npm and Python
+ * Make sure that the path where you have checked out Atom does not include a space. e.g. use `c:\atom` and not `c:\my stuff\atom`
+
+* `script/build` outputs only the Node.js and Python versions before returning
* Try moving the repository to `C:\atom`. Most likely, the path is too long.
See [issue #2200](https://github.com/atom/atom/issues/2200).
* `error MSB4025: The project file could not be loaded. Invalid character in the given encoding.`
-
* This can occur because your home directory (`%USERPROFILE%`) has non-ASCII
characters in it. This is a bug in [gyp](https://code.google.com/p/gyp/)
which is used to build native Node.js modules and there is no known workaround.
* https://github.com/TooTallNate/node-gyp/issues/297
* https://code.google.com/p/gyp/issues/detail?id=393
-* `script/build` stops at installing runas with `Failed at the runas@x.y.z install script.`
+* `'node_modules\.bin\npm' is not recognized as an internal or external command, operable program or batch file.`
+ * This occurs if the previous build left things in a bad state. Run `script\clean` and then `script\build` again.
+* `script/build` stops at installing runas with `Failed at the runas@x.y.z install script.`
* See the next item.
* `error MSB8020: The build tools for Visual Studio 201? (Platform Toolset = 'v1?0') cannot be found.`
-
- * If you're building Atom with Visual Studio 2013 or above make sure the `GYP_MSVS_VERSION` environment variable is set, and then re-run `script/build` after a clean:
-
- ```bash
- $env:GYP_MSVS_VERSION='2013' # '2015' if using Visual Studio 2015, and so on
- script/clean
- script/build
- ```
- * If you are using Visual Studio 2013 or above and the build fails with some other error message this environment variable might still be required and ensure you have Visual C++ language support installed.
+ * If you're building Atom with Visual Studio 2013 try setting the `GYP_MSVS_VERSION` environment variable to 2013 and then `script/clean` followed by `script/build` (re-open your command prompt or Powershell window if you set it using the GUI)
* Other `node-gyp` errors on first build attempt, even though the right Node.js and Python versions are installed.
* Do try the build command one more time, as experience shows it often works on second try in many of these cases.
diff --git a/docs/native-profiling.md b/docs/native-profiling.md
new file mode 100644
index 000000000..f9f9160a5
--- /dev/null
+++ b/docs/native-profiling.md
@@ -0,0 +1,18 @@
+# Profiling the Atom Render Process on OS X with Instruments
+
+
+
+* Determine the version of Electron for your version of Atom.
+ * Open the dev tools with `alt-cmd-i`
+ * Evaluate `process.versions.electron` in the console.
+* Based on this version, download the appropriate Electron symbols from the [releases](https://github.com/atom/electron/releases) page.
+ * The file name should look like `electron-v0.X.Y-darwin-x64-dsym.zip`.
+ * Decompress these symbols in your `~/Downloads` directory.
+* Now create a time profile in Instruments.
+ * Open `Instruments.app`.
+ * Select `Time Profiler`
+ * In Atom, determine the pid to attach to by evaluating `process.pid` in the dev tools console.
+ * Attach to this pid via the menu at the upper left corner of the Instruments profiler.
+ * Click record, do your thing.
+ * Click stop.
+ * The symbols should have been automatically located by Instruments (via Spotlight or something?), giving you a readable profile.
diff --git a/dot-atom/keymap.cson b/dot-atom/keymap.cson
index 10ad345d4..fd7c4f96e 100644
--- a/dot-atom/keymap.cson
+++ b/dot-atom/keymap.cson
@@ -18,15 +18,15 @@
# 'ctrl-p': 'core:move-down'
#
# You can find more information about keymaps in these guides:
-# * https://atom.io/docs/latest/using-atom-basic-customization#customizing-key-bindings
-# * https://atom.io/docs/latest/behind-atom-keymaps-in-depth
+# * http://flight-manual.atom.io/using-atom/sections/basic-customization/#_customizing_keybindings
+# * http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth/
#
# If you're having trouble with your keybindings not working, try the
# Keybinding Resolver: `Cmd+.` on OS X and `Ctrl+.` on other platforms. See the
# Debugging Guide for more information:
-# * https://atom.io/docs/latest/hacking-atom-debugging#check-the-keybindings
+# * http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-the-keybindings
#
# This file uses CoffeeScript Object Notation (CSON).
# If you are unfamiliar with CSON, you can read more about it in the
# Atom Flight Manual:
-# https://atom.io/docs/latest/using-atom-basic-customization#cson
+# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson
diff --git a/dot-atom/snippets.cson b/dot-atom/snippets.cson
index eb8f1b22a..cd66bba04 100644
--- a/dot-atom/snippets.cson
+++ b/dot-atom/snippets.cson
@@ -18,4 +18,4 @@
# This file uses CoffeeScript Object Notation (CSON).
# If you are unfamiliar with CSON, you can read more about it in the
# Atom Flight Manual:
-# https://atom.io/docs/latest/using-atom-basic-customization#cson
+# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson
diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson
index 4859f9b67..953fb0c48 100644
--- a/keymaps/darwin.cson
+++ b/keymaps/darwin.cson
@@ -73,8 +73,10 @@
'cmd-alt-right': 'pane:show-next-item'
'ctrl-pageup': 'pane:show-previous-item'
'ctrl-pagedown': 'pane:show-next-item'
- 'ctrl-tab': 'pane:show-next-item'
- 'ctrl-shift-tab': 'pane:show-previous-item'
+ 'ctrl-tab': 'pane:show-next-recently-used-item'
+ 'ctrl-tab ^ctrl': 'pane:move-active-item-to-top-of-stack'
+ 'ctrl-shift-tab': 'pane:show-previous-recently-used-item'
+ 'ctrl-shift-tab ^ctrl': 'pane:move-active-item-to-top-of-stack'
'cmd-=': 'window:increase-font-size'
'cmd-+': 'window:increase-font-size'
'cmd--': 'window:decrease-font-size'
@@ -128,6 +130,8 @@
# Atom Specific
'ctrl-W': 'editor:select-word'
+ 'cmd-ctrl-left': 'editor:move-selection-left'
+ 'cmd-ctrl-right': 'editor:move-selection-right'
# Sublime Parity
'cmd-a': 'core:select-all'
diff --git a/keymaps/linux.cson b/keymaps/linux.cson
index 9ddb760e2..1f78739a9 100644
--- a/keymaps/linux.cson
+++ b/keymaps/linux.cson
@@ -14,6 +14,8 @@
'ctrl-shift-pageup': 'pane:move-item-left'
'ctrl-shift-pagedown': 'pane:move-item-right'
'f11': 'window:toggle-full-screen'
+ 'alt-shift-left': 'editor:move-selection-left'
+ 'alt-shift-right': 'editor:move-selection-right'
# Sublime Parity
'ctrl-,': 'application:show-settings'
@@ -25,6 +27,7 @@
'ctrl-n': 'application:new-file'
'ctrl-s': 'core:save'
'ctrl-S': 'core:save-as'
+ 'ctrl-f4': 'core:close'
'ctrl-w': 'core:close'
'ctrl-z': 'core:undo'
'ctrl-y': 'core:redo'
@@ -46,8 +49,10 @@
'pagedown': 'core:page-down'
'backspace': 'core:backspace'
'shift-backspace': 'core:backspace'
- 'ctrl-tab': 'pane:show-next-item'
- 'ctrl-shift-tab': 'pane:show-previous-item'
+ 'ctrl-tab': 'pane:show-next-recently-used-item'
+ 'ctrl-tab ^ctrl': 'pane:move-active-item-to-top-of-stack'
+ 'ctrl-shift-tab': 'pane:show-previous-recently-used-item'
+ 'ctrl-shift-tab ^ctrl': 'pane:move-active-item-to-top-of-stack'
'ctrl-pageup': 'pane:show-previous-item'
'ctrl-pagedown': 'pane:show-next-item'
'ctrl-up': 'core:move-up'
diff --git a/keymaps/win32.cson b/keymaps/win32.cson
index e4703bac8..10450ec18 100644
--- a/keymaps/win32.cson
+++ b/keymaps/win32.cson
@@ -20,6 +20,8 @@
'ctrl-shift-left': 'pane:move-item-left'
'ctrl-shift-right': 'pane:move-item-right'
'f11': 'window:toggle-full-screen'
+ 'alt-shift-left': 'editor:move-selection-left'
+ 'alt-shift-right': 'editor:move-selection-right'
# Sublime Parity
'ctrl-,': 'application:show-settings'
@@ -52,8 +54,10 @@
'pagedown': 'core:page-down'
'backspace': 'core:backspace'
'shift-backspace': 'core:backspace'
- 'ctrl-tab': 'pane:show-next-item'
- 'ctrl-shift-tab': 'pane:show-previous-item'
+ 'ctrl-tab': 'pane:show-next-recently-used-item'
+ 'ctrl-tab ^ctrl': 'pane:move-active-item-to-top-of-stack'
+ 'ctrl-shift-tab': 'pane:show-previous-recently-used-item'
+ 'ctrl-shift-tab ^ctrl': 'pane:move-active-item-to-top-of-stack'
'ctrl-pageup': 'pane:show-previous-item'
'ctrl-pagedown': 'pane:show-next-item'
'ctrl-shift-up': 'core:move-up'
diff --git a/menus/darwin.cson b/menus/darwin.cson
index a2636887d..bb3ce0acf 100644
--- a/menus/darwin.cson
+++ b/menus/darwin.cson
@@ -75,6 +75,13 @@
{ label: 'Join Lines', command: 'editor:join-lines' }
]
}
+ {
+ label: 'Columns',
+ submenu: [
+ { label: 'Move Selection Left', command: 'editor:move-selection-left' }
+ { label: 'Move Selection Right', command: 'editor:move-selection-right' }
+ ]
+ }
{
label: 'Text',
submenu: [
@@ -200,7 +207,6 @@
submenu: [
{ label: 'Terms of Use', command: 'application:open-terms-of-use' }
{ label: 'Documentation', command: 'application:open-documentation' }
- { label: 'Roadmap', command: 'application:open-roadmap' }
{ label: 'Frequently Asked Questions', command: 'application:open-faq' }
{ type: 'separator' }
{ label: 'Community Discussions', command: 'application:open-discussions' }
@@ -222,10 +228,10 @@
{label: 'Delete', command: 'core:delete'}
{label: 'Select All', command: 'core:select-all'}
{type: 'separator'}
- {label: 'Split Up', command: 'pane:split-up'}
- {label: 'Split Down', command: 'pane:split-down'}
- {label: 'Split Left', command: 'pane:split-left'}
- {label: 'Split Right', command: 'pane:split-right'}
+ {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'}
+ {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'}
+ {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'}
+ {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'}
{label: 'Close Pane', command: 'pane:close'}
{type: 'separator'}
]
diff --git a/menus/linux.cson b/menus/linux.cson
index 1276748d8..b84fc8053 100644
--- a/menus/linux.cson
+++ b/menus/linux.cson
@@ -48,6 +48,13 @@
{ label: '&Join Lines', command: 'editor:join-lines' }
]
}
+ {
+ label: 'Columns',
+ submenu: [
+ { label: 'Move Selection &Left', command: 'editor:move-selection-left' }
+ { label: 'Move Selection &Right', command: 'editor:move-selection-right' }
+ ]
+ }
{
label: 'Text',
submenu: [
@@ -174,7 +181,6 @@
{ label: "VERSION", enabled: false }
{ type: 'separator' }
{ label: '&Documentation', command: 'application:open-documentation' }
- { label: 'Roadmap', command: 'application:open-roadmap' }
{ label: 'Frequently Asked Questions', command: 'application:open-faq' }
{ type: 'separator' }
{ label: 'Community Discussions', command: 'application:open-discussions' }
@@ -198,10 +204,10 @@
{label: 'Delete', command: 'core:delete'}
{label: 'Select All', command: 'core:select-all'}
{type: 'separator'}
- {label: 'Split Up', command: 'pane:split-up'}
- {label: 'Split Down', command: 'pane:split-down'}
- {label: 'Split Left', command: 'pane:split-left'}
- {label: 'Split Right', command: 'pane:split-right'}
+ {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'}
+ {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'}
+ {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'}
+ {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'}
{label: 'Close Pane', command: 'pane:close'}
{type: 'separator'}
]
diff --git a/menus/win32.cson b/menus/win32.cson
index a7d41b28f..323db5d18 100644
--- a/menus/win32.cson
+++ b/menus/win32.cson
@@ -56,6 +56,13 @@
{ label: '&Join Lines', command: 'editor:join-lines' }
]
}
+ {
+ label: 'Columns',
+ submenu: [
+ { label: 'Move Selection &Left', command: 'editor:move-selection-left' }
+ { label: 'Move Selection &Right', command: 'editor:move-selection-right' }
+ ]
+ }
{
label: 'Text',
submenu: [
@@ -177,7 +184,6 @@
{ label: 'Downloading Update', enabled: false, visible: false}
{ type: 'separator' }
{ label: '&Documentation', command: 'application:open-documentation' }
- { label: 'Roadmap', command: 'application:open-roadmap' }
{ label: 'Frequently Asked Questions', command: 'application:open-faq' }
{ type: 'separator' }
{ label: 'Community Discussions', command: 'application:open-discussions' }
@@ -201,10 +207,10 @@
{label: 'Delete', command: 'core:delete'}
{label: 'Select All', command: 'core:select-all'}
{type: 'separator'}
- {label: 'Split Up', command: 'pane:split-up'}
- {label: 'Split Down', command: 'pane:split-down'}
- {label: 'Split Left', command: 'pane:split-left'}
- {label: 'Split Right', command: 'pane:split-right'}
+ {label: 'Split Up', command: 'pane:split-up-and-copy-active-item'}
+ {label: 'Split Down', command: 'pane:split-down-and-copy-active-item'}
+ {label: 'Split Left', command: 'pane:split-left-and-copy-active-item'}
+ {label: 'Split Right', command: 'pane:split-right-and-copy-active-item'}
{label: 'Close Pane', command: 'pane:close'}
{type: 'separator'}
]
diff --git a/package.json b/package.json
index 6dc272858..596b8c36e 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "atom",
"productName": "Atom",
- "version": "1.7.0-dev",
+ "version": "1.9.0-dev",
"description": "A hackable text editor for the 21st Century.",
"main": "./src/browser/main.js",
"repository": {
@@ -12,16 +12,17 @@
"url": "https://github.com/atom/atom/issues"
},
"license": "MIT",
- "electronVersion": "0.34.5",
+ "electronVersion": "0.36.12",
"dependencies": {
"async": "0.2.6",
- "atom-keymap": "^6.2.0",
+ "atom-keymap": "6.3.2",
"babel-core": "^5.8.21",
"bootstrap": "^3.3.4",
"cached-run-in-this-context": "0.4.1",
"clear-cut": "^2.0.1",
"coffee-script": "1.8.0",
"color": "^0.7.3",
+ "devtron": "1.1.0",
"event-kit": "^1.5.0",
"find-parent-dir": "^0.3.0",
"first-mate": "^5.1.1",
@@ -36,12 +37,12 @@
"key-path-helpers": "^0.4.0",
"less-cache": "0.23",
"line-top-index": "0.2.0",
- "marked": "^0.3.4",
- "nodegit": "0.9.0",
+ "marked": "^0.3.5",
"normalize-package-data": "^2.0.0",
"nslog": "^3",
+ "ohnogit": "0.0.11",
"oniguruma": "^5",
- "pathwatcher": "~6.2",
+ "pathwatcher": "~6.5",
"property-accessors": "^1.1.3",
"random-words": "0.0.1",
"resolve": "^1.1.6",
@@ -54,7 +55,7 @@
"service-hub": "^0.7.0",
"source-map-support": "^0.3.2",
"temp": "0.8.1",
- "text-buffer": "8.2.1",
+ "text-buffer": "9.1.0",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"yargs": "^3.23.0"
@@ -66,89 +67,89 @@
"atom-light-ui": "0.43.0",
"base16-tomorrow-dark-theme": "1.1.0",
"base16-tomorrow-light-theme": "1.1.1",
- "one-dark-ui": "1.1.9",
- "one-light-ui": "1.1.9",
+ "one-dark-ui": "1.3.1",
+ "one-light-ui": "1.3.1",
"one-dark-syntax": "1.2.0",
"one-light-syntax": "1.2.0",
- "solarized-dark-syntax": "1.0.0",
- "solarized-light-syntax": "1.0.0",
- "about": "1.3.0",
- "archive-view": "0.61.0",
+ "solarized-dark-syntax": "1.0.2",
+ "solarized-light-syntax": "1.0.2",
+ "about": "1.5.2",
+ "archive-view": "0.61.1",
"autocomplete-atom-api": "0.10.0",
- "autocomplete-css": "0.11.0",
+ "autocomplete-css": "0.11.1",
"autocomplete-html": "0.7.2",
- "autocomplete-plus": "2.25.0",
- "autocomplete-snippets": "1.10.0",
+ "autocomplete-plus": "2.31.0",
+ "autocomplete-snippets": "1.11.0",
"autoflow": "0.27.0",
- "autosave": "0.23.0",
+ "autosave": "0.23.1",
"background-tips": "0.26.0",
- "bookmarks": "0.38.2",
- "bracket-matcher": "0.79.0",
+ "bookmarks": "0.41.0",
+ "bracket-matcher": "0.82.1",
"command-palette": "0.38.0",
- "deprecation-cop": "0.54.0",
+ "deprecation-cop": "0.54.1",
"dev-live-reload": "0.47.0",
- "encoding-selector": "0.21.0",
- "exception-reporting": "0.37.0",
- "find-and-replace": "0.197.1",
- "fuzzy-finder": "0.94.0",
- "git-diff": "0.57.0",
+ "encoding-selector": "0.22.0",
+ "exception-reporting": "0.38.1",
+ "fuzzy-finder": "1.2.0",
+ "git-diff": "1.0.1",
+ "find-and-replace": "0.198.0",
"go-to-line": "0.30.0",
- "grammar-selector": "0.48.0",
- "image-view": "0.56.0",
- "incompatible-packages": "0.25.0",
- "keybinding-resolver": "0.33.0",
- "line-ending-selector": "0.3.0",
- "link": "0.31.0",
- "markdown-preview": "0.157.2",
+ "grammar-selector": "0.48.1",
+ "image-view": "0.57.0",
+ "incompatible-packages": "0.26.1",
+ "keybinding-resolver": "0.35.0",
+ "line-ending-selector": "0.5.0",
+ "link": "0.31.1",
+ "markdown-preview": "0.158.0",
"metrics": "0.53.1",
- "notifications": "0.62.1",
- "open-on-github": "0.41.0",
- "package-generator": "0.41.0",
- "settings-view": "0.232.3",
- "snippets": "1.0.1",
- "spell-check": "0.65.0",
- "status-bar": "0.83.0",
- "styleguide": "0.45.1",
- "symbols-view": "0.111.0",
- "tabs": "0.90.0",
- "timecop": "0.33.0",
- "tree-view": "0.201.0",
+ "notifications": "0.64.1",
+ "open-on-github": "1.1.0",
+ "package-generator": "1.0.0",
+ "settings-view": "0.237.0",
+ "snippets": "1.0.2",
+ "spell-check": "0.67.1",
+ "status-bar": "1.2.6",
+ "styleguide": "0.45.2",
+ "symbols-view": "0.113.0",
+ "tabs": "0.95.0",
+ "timecop": "0.33.1",
+ "tree-view": "0.207.0",
"update-package-dependencies": "0.10.0",
- "welcome": "0.33.0",
- "whitespace": "0.32.1",
+ "welcome": "0.34.0",
+ "whitespace": "0.32.2",
"wrap-guide": "0.38.1",
- "language-c": "0.51.1",
- "language-clojure": "0.19.1",
- "language-coffee-script": "0.46.1",
- "language-csharp": "0.11.0",
- "language-css": "0.36.0",
- "language-gfm": "0.84.0",
- "language-git": "0.12.1",
+ "language-c": "0.52.0",
+ "language-clojure": "0.20.0",
+ "language-coffee-script": "0.47.0",
+ "language-csharp": "0.12.1",
+ "language-css": "0.36.1",
+ "language-gfm": "0.86.0",
+ "language-git": "0.13.0",
"language-go": "0.42.0",
- "language-html": "0.44.0",
+ "language-html": "0.44.1",
"language-hyperlink": "0.16.0",
- "language-java": "0.17.0",
+ "language-java": "0.19.0",
"language-javascript": "0.110.0",
- "language-json": "0.17.4",
- "language-less": "0.29.0",
- "language-make": "0.21.0",
+ "language-json": "0.18.0",
+ "language-less": "0.29.3",
+ "language-make": "0.22.0",
"language-mustache": "0.13.0",
"language-objective-c": "0.15.1",
- "language-perl": "0.32.0",
+ "language-perl": "0.35.0",
"language-php": "0.37.0",
"language-property-list": "0.8.0",
- "language-python": "0.43.0",
- "language-ruby": "0.68.1",
+ "language-python": "0.44.0",
+ "language-ruby": "0.68.5",
"language-ruby-on-rails": "0.25.0",
- "language-sass": "0.45.0",
- "language-shellscript": "0.21.0",
+ "language-sass": "0.52.0",
+ "language-shellscript": "0.22.2",
"language-source": "0.9.0",
- "language-sql": "0.20.0",
- "language-text": "0.7.0",
+ "language-sql": "0.21.0",
+ "language-text": "0.7.1",
"language-todo": "0.27.0",
"language-toml": "0.18.0",
- "language-xml": "0.34.3",
- "language-yaml": "0.25.1"
+ "language-xml": "0.34.6",
+ "language-yaml": "0.26.0"
},
"private": true,
"scripts": {
@@ -173,7 +174,8 @@
"runs",
"spyOn",
"waitsFor",
- "waitsForPromise"
+ "waitsForPromise",
+ "indexedDB"
]
}
}
diff --git a/resources/win/atom.cmd b/resources/win/atom.cmd
index c9bfdd5ba..73c4ddb01 100644
--- a/resources/win/atom.cmd
+++ b/resources/win/atom.cmd
@@ -2,6 +2,7 @@
SET EXPECT_OUTPUT=
SET WAIT=
+SET PSARGS=%*
FOR %%a IN (%*) DO (
IF /I "%%a"=="-f" SET EXPECT_OUTPUT=YES
@@ -22,31 +23,14 @@ FOR %%a IN (%*) DO (
)
)
-rem Getting the process ID in cmd of the current cmd process: http://superuser.com/questions/881789/identify-and-kill-batch-script-started-before
-set T=%TEMP%\atomCmdProcessId-%time::=%.tmp
-wmic process where (Name="WMIC.exe" AND CommandLine LIKE "%%%TIME%%%") get ParentProcessId /value | find "ParentProcessId" >%T%
-set /P A=<%T%
-set PID=%A:~16%
-del %T%
-
IF "%EXPECT_OUTPUT%"=="YES" (
SET ELECTRON_ENABLE_LOGGING=YES
IF "%WAIT%"=="YES" (
- "%~dp0\..\..\atom.exe" --pid=%PID% %*
- rem If the wait flag is set, don't exit this process until Atom tells it to.
- goto waitLoop
- )
- ELSE (
+ powershell -noexit "Start-Process -FilePath \"%~dp0\..\..\atom.exe\" -ArgumentList \"--pid=$pid $env:PSARGS\" ; wait-event"
+ exit 0
+ ) ELSE (
"%~dp0\..\..\atom.exe" %*
)
) ELSE (
"%~dp0\..\app\apm\bin\node.exe" "%~dp0\atom.js" %*
)
-
-goto end
-
-:waitLoop
- sleep 1
- goto waitLoop
-
-:end
diff --git a/resources/win/atom.sh b/resources/win/atom.sh
index 0eaf193c0..99dd90bea 100644
--- a/resources/win/atom.sh
+++ b/resources/win/atom.sh
@@ -1,49 +1,5 @@
#!/bin/sh
-
-while getopts ":fhtvw-:" opt; do
- case "$opt" in
- -)
- case "${OPTARG}" in
- wait)
- WAIT=1
- ;;
- help|version)
- REDIRECT_STDERR=1
- EXPECT_OUTPUT=1
- ;;
- foreground|test)
- EXPECT_OUTPUT=1
- ;;
- esac
- ;;
- w)
- WAIT=1
- ;;
- h|v)
- REDIRECT_STDERR=1
- EXPECT_OUTPUT=1
- ;;
- f|t)
- EXPECT_OUTPUT=1
- ;;
- esac
-done
-
-directory=$(dirname "$0")
-
-WINPS=`ps | grep -i $$`
-PID=`echo $WINPS | cut -d' ' -f 4`
-
-if [ $EXPECT_OUTPUT ]; then
- export ELECTRON_ENABLE_LOGGING=1
- "$directory/../../atom.exe" --executed-from="$(pwd)" --pid=$PID "$@"
-else
- "$directory/../app/apm/bin/node.exe" "$directory/atom.js" "$@"
-fi
-
-# If the wait flag is set, don't exit this process until Atom tells it to.
-if [ $WAIT ]; then
- while true; do
- sleep 1
- done
-fi
+pushd "$(dirname "$0")" > /dev/null
+ATOMCMD=""$(pwd -W)"/atom.cmd"
+popd > /dev/null
+cmd.exe //c "$ATOMCMD" "$@"
diff --git a/script/build b/script/build
index a40a02e13..ca5569d0f 100755
--- a/script/build
+++ b/script/build
@@ -2,9 +2,24 @@
var cp = require('./utils/child-process-wrapper.js');
var runGrunt = require('./utils/run-grunt.js');
var path = require('path');
+var fs = require('fs');
process.chdir(path.dirname(__dirname));
+if (process.platform === 'win32') {
+ process.env['PATH'] = process.env['PATH']
+ .split(';')
+ .filter(function(p) {
+ if (fs.existsSync(path.resolve(p, 'msbuild.exe'))) {
+ console.log('Excluding "' + p + '" from PATH to avoid msbuild.exe mismatch')
+ return false;
+ } else {
+ return true;
+ }
+ })
+ .join(';');
+}
+
cp.safeExec('node script/bootstrap', function() {
// build/node_modules/.bin/grunt "$@"
var args = process.argv.slice(2);
diff --git a/script/clean b/script/clean
index fd0aa5bfa..cc4933f95 100755
--- a/script/clean
+++ b/script/clean
@@ -1,21 +1,21 @@
#!/usr/bin/env node
-var cp = require('./utils/child-process-wrapper.js');
+var childProcess = require('./utils/child-process-wrapper.js');
var fs = require('fs');
var path = require('path');
var os = require('os');
-var removeCommand = process.platform === 'win32' ? 'rmdir /S /Q ' : 'rm -rf ';
+var isWindows = process.platform === 'win32';
var productName = require('../package.json').productName;
process.chdir(path.dirname(__dirname));
-var home = process.env[(process.platform === 'win32') ? 'USERPROFILE' : 'HOME'];
+var home = process.env[isWindows ? 'USERPROFILE' : 'HOME'];
var tmpdir = os.tmpdir();
// Windows: Use START as a way to ignore error if Atom.exe isnt running
-var killatom = process.platform === 'win32' ? 'START taskkill /F /IM ' + productName + '.exe' : 'pkill -9 ' + productName + ' || true';
+var killAtomCommand = isWindows ? 'START taskkill /F /IM ' + productName + '.exe' : 'pkill -9 ' + productName + ' || true';
+//childProcess.safeExec(killAtomCommand);
-var commands = [
- killatom,
+var pathsToRemove = [
[__dirname, '..', 'node_modules'],
[__dirname, '..', 'build', 'node_modules'],
[__dirname, '..', 'apm', 'node_modules'],
@@ -31,20 +31,33 @@ var commands = [
[home, '.atom', 'electron'],
[tmpdir, 'atom-build'],
[tmpdir, 'atom-cached-atom-shells'],
-];
-var run = function() {
- var next = commands.shift();
- if (!next)
- process.exit(0);
+].map(function(pathSegments) {
+ return path.resolve.apply(null, pathSegments);
+});
- if (Array.isArray(next)) {
- var pathToRemove = path.resolve.apply(path.resolve, next);
- if (fs.existsSync(pathToRemove))
- next = removeCommand + pathToRemove;
- else
- return run();
+pathsToRemove.forEach(function(pathToRemove) {
+ if (fs.existsSync(pathToRemove)) {
+ removePath(pathToRemove);
}
+});
- cp.safeExec(next, run);
+function removePath(pathToRemove) {
+ if (isWindows) {
+ removePathOnWindows(pathToRemove);
+ } else {
+ childProcess.safeExec('rm -rf ' + pathToRemove);
+ }
+}
+
+// Windows has a 260-char path limit for rmdir etc. Just recursively delete in Node.
+function removePathOnWindows(folderPath) {
+ fs.readdirSync(folderPath).forEach(function(entry, index) {
+ var entryPath = path.join(folderPath, entry);
+ if (fs.lstatSync(entryPath).isDirectory()) {
+ removePathOnWindows(entryPath);
+ } else {
+ fs.unlinkSync(entryPath);
+ }
+ });
+ fs.rmdirSync(folderPath);
};
-run();
diff --git a/script/mktar b/script/mktar
new file mode 100755
index 000000000..986063f9a
--- /dev/null
+++ b/script/mktar
@@ -0,0 +1,39 @@
+#!/bin/bash
+# mktar name version arch icon-path build-root-path
+
+set -e
+
+SCRIPT=`readlink -f "$0"`
+ROOT=`readlink -f $(dirname $SCRIPT)/..`
+cd $ROOT
+
+NAME="$1"
+VERSION="$2"
+ARCH="$3"
+ICON_FILE="$4"
+BUILD_ROOT_PATH="$5"
+FILE_MODE=755
+
+TAR_PATH=$BUILD_ROOT_PATH
+ATOM_PATH="$BUILD_ROOT_PATH/Atom"
+
+TARGET_ROOT="`mktemp -d`"
+chmod $FILE_MODE "$TARGET_ROOT"
+NAME_IN_TAR="$NAME-$VERSION-$ARCH"
+TARGET="$TARGET_ROOT/$NAME_IN_TAR"
+
+# Copy executable and resources
+cp -a "$ATOM_PATH" "$TARGET"
+
+# Copy icon file
+cp "$ICON_FILE" "$TARGET/$NAME.png"
+
+# Remove executable bit from .node files
+find "$TARGET" -type f -name "*.node" -exec chmod a-x {} \;
+
+# Create the archive
+pushd "$TARGET_ROOT"
+tar caf "$TAR_PATH/$NAME_IN_TAR.tar.gz" "$NAME_IN_TAR"
+popd
+
+rm -rf "$TARGET_ROOT"
diff --git a/script/utils/child-process-wrapper.js b/script/utils/child-process-wrapper.js
index 2afc57934..55e6733d1 100644
--- a/script/utils/child-process-wrapper.js
+++ b/script/utils/child-process-wrapper.js
@@ -17,7 +17,7 @@ exports.safeExec = function(command, options, callback) {
var child = childProcess.exec(command, options, function(error, stdout, stderr) {
if (error)
process.exit(error.code || 1);
- else
+ else if (callback)
callback(null);
});
child.stderr.pipe(process.stderr);
diff --git a/spec/async-spec-helpers.coffee b/spec/async-spec-helpers.coffee
index 5f8e03ca3..6ed8a5a2b 100644
--- a/spec/async-spec-helpers.coffee
+++ b/spec/async-spec-helpers.coffee
@@ -19,7 +19,9 @@ exports.afterEach = (fn) ->
waitsForPromise = (fn) ->
promise = fn()
- waitsFor 'spec promise to resolve', 30000, (done) ->
+ # This timeout is 3 minutes. We need to bump it back down once we fix backgrounding
+ # of the renderer process on CI. See https://github.com/atom/electron/issues/4317
+ waitsFor 'spec promise to resolve', 3 * 60 * 1000, (done) ->
promise.then(
done,
(error) ->
diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee
index b5b975112..39c22a33d 100644
--- a/spec/atom-environment-spec.coffee
+++ b/spec/atom-environment-spec.coffee
@@ -4,6 +4,7 @@ temp = require 'temp'
Package = require '../src/package'
ThemeManager = require '../src/theme-manager'
AtomEnvironment = require '../src/atom-environment'
+StorageFolder = require '../src/storage-folder'
describe "AtomEnvironment", ->
describe 'window sizing methods', ->
@@ -27,8 +28,10 @@ describe "AtomEnvironment", ->
atom.setSize(originalSize.width, originalSize.height)
it 'sets the size of the window, and can retrieve the size just set', ->
- atom.setSize(100, 400)
- expect(atom.getSize()).toEqual width: 100, height: 400
+ newWidth = originalSize.width + 12
+ newHeight = originalSize.height + 23
+ atom.setSize(newWidth, newHeight)
+ expect(atom.getSize()).toEqual width: newWidth, height: newHeight
describe ".isReleasedVersion()", ->
it "returns false if the version is a SHA and true otherwise", ->
@@ -152,6 +155,8 @@ describe "AtomEnvironment", ->
atom.enablePersistence = false
it "selects the state based on the current project paths", ->
+ jasmine.useRealClock()
+
[dir1, dir2] = [temp.mkdirSync("dir1-"), temp.mkdirSync("dir2-")]
loadSettings = _.extend atom.getLoadSettings(),
@@ -159,20 +164,100 @@ describe "AtomEnvironment", ->
windowState: null
spyOn(atom, 'getLoadSettings').andCallFake -> loadSettings
- spyOn(atom.getStorageFolder(), 'getPath').andReturn(temp.mkdirSync("storage-dir-"))
+ spyOn(atom, 'serialize').andReturn({stuff: 'cool'})
- atom.state.stuff = "cool"
atom.project.setPaths([dir1, dir2])
- atom.saveStateSync()
+ # State persistence will fail if other Atom instances are running
+ waitsForPromise ->
+ atom.stateStore.connect().then (isConnected) ->
+ expect(isConnected).toBe true
- atom.state = {}
- atom.loadStateSync()
- expect(atom.state.stuff).toBeUndefined()
+ waitsForPromise ->
+ atom.saveState().then ->
+ atom.loadState().then (state) ->
+ expect(state).toBeFalsy()
- loadSettings.initialPaths = [dir2, dir1]
- atom.state = {}
- atom.loadStateSync()
- expect(atom.state.stuff).toBe("cool")
+ waitsForPromise ->
+ loadSettings.initialPaths = [dir2, dir1]
+ atom.loadState().then (state) ->
+ expect(state).toEqual({stuff: 'cool'})
+
+ it "loads state from the storage folder when it can't be found in atom.stateStore", ->
+ jasmine.useRealClock()
+
+ storageFolderState = {foo: 1, bar: 2}
+ serializedState = {someState: 42}
+ loadSettings = _.extend(atom.getLoadSettings(), {initialPaths: [temp.mkdirSync("project-directory")]})
+ spyOn(atom, 'getLoadSettings').andReturn(loadSettings)
+ spyOn(atom, 'serialize').andReturn(serializedState)
+ spyOn(atom, 'getStorageFolder').andReturn(new StorageFolder(temp.mkdirSync("config-directory")))
+ atom.project.setPaths(atom.getLoadSettings().initialPaths)
+
+ waitsForPromise ->
+ atom.stateStore.connect()
+
+ runs ->
+ atom.getStorageFolder().storeSync(atom.getStateKey(loadSettings.initialPaths), storageFolderState)
+
+ waitsForPromise ->
+ atom.loadState().then (state) -> expect(state).toEqual(storageFolderState)
+
+ waitsForPromise ->
+ atom.saveState()
+
+ waitsForPromise ->
+ atom.loadState().then (state) -> expect(state).toEqual(serializedState)
+
+ it "saves state when the CPU is idle after a keydown or mousedown event", ->
+ spyOn(atom, 'saveState')
+ idleCallbacks = []
+ spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback)
+
+ keydown = new KeyboardEvent('keydown')
+ atom.document.dispatchEvent(keydown)
+ advanceClock atom.saveStateDebounceInterval
+ idleCallbacks.shift()()
+ expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
+ expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
+
+ atom.saveState.reset()
+ mousedown = new MouseEvent('mousedown')
+ atom.document.dispatchEvent(mousedown)
+ advanceClock atom.saveStateDebounceInterval
+ idleCallbacks.shift()()
+ expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
+ expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
+
+ it "saves state immediately when unloading the editor window, ignoring pending and successive mousedown/keydown events", ->
+ spyOn(atom, 'saveState')
+ idleCallbacks = []
+ spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback)
+
+ mousedown = new MouseEvent('mousedown')
+ atom.document.dispatchEvent(mousedown)
+ atom.unloadEditorWindow()
+ expect(atom.saveState).toHaveBeenCalledWith({isUnloading: true})
+ expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: false})
+
+ atom.saveState.reset()
+ advanceClock atom.saveStateDebounceInterval
+ idleCallbacks.shift()()
+ expect(atom.saveState).not.toHaveBeenCalled()
+
+ atom.saveState.reset()
+ mousedown = new MouseEvent('mousedown')
+ atom.document.dispatchEvent(mousedown)
+ advanceClock atom.saveStateDebounceInterval
+ idleCallbacks.shift()()
+ expect(atom.saveState).not.toHaveBeenCalled()
+
+ it "serializes the project state with all the options supplied in saveState", ->
+ spyOn(atom.project, 'serialize').andReturn({foo: 42})
+
+ waitsForPromise -> atom.saveState({anyOption: 'any option'})
+ runs ->
+ expect(atom.project.serialize.calls.length).toBe(1)
+ expect(atom.project.serialize.mostRecentCall.args[0]).toEqual({anyOption: 'any option'})
describe "openInitialEmptyEditorIfNecessary", ->
describe "when there are no paths set", ->
@@ -230,23 +315,6 @@ describe "AtomEnvironment", ->
atomEnvironment.destroy()
- it "saves the serialized state of the window so it can be deserialized after reload", ->
- atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, window, document})
- spyOn(atomEnvironment, 'saveStateSync')
-
- workspaceState = atomEnvironment.workspace.serialize()
- grammarsState = {grammarOverridesByPath: atomEnvironment.grammars.grammarOverridesByPath}
- projectState = atomEnvironment.project.serialize()
-
- atomEnvironment.unloadEditorWindow()
-
- expect(atomEnvironment.state.workspace).toEqual workspaceState
- expect(atomEnvironment.state.grammars).toEqual grammarsState
- expect(atomEnvironment.state.project).toEqual projectState
- expect(atomEnvironment.saveStateSync).toHaveBeenCalled()
-
- atomEnvironment.destroy()
-
describe "::destroy()", ->
it "does not throw exceptions when unsubscribing from ipc events (regression)", ->
configDirPath = temp.mkdirSync()
@@ -258,6 +326,7 @@ describe "AtomEnvironment", ->
}
atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate, window, document: fakeDocument})
spyOn(atomEnvironment.packages, 'getAvailablePackagePaths').andReturn []
+ spyOn(atomEnvironment, 'displayWindow').andReturn Promise.resolve()
atomEnvironment.startEditorWindow()
atomEnvironment.unloadEditorWindow()
atomEnvironment.destroy()
@@ -273,6 +342,14 @@ describe "AtomEnvironment", ->
atom.openLocations([{pathToOpen}])
expect(atom.project.getPaths()[0]).toBe __dirname
+ describe "then a second path is opened with forceAddToWindow", ->
+ it "adds the second path to the project's paths", ->
+ firstPathToOpen = __dirname
+ secondPathToOpen = path.resolve(__dirname, './fixtures')
+ atom.openLocations([{pathToOpen: firstPathToOpen}])
+ atom.openLocations([{pathToOpen: secondPathToOpen, forceAddToWindow: true}])
+ expect(atom.project.getPaths()).toEqual([firstPathToOpen, secondPathToOpen])
+
describe "when the opened path does not exist but its parent directory does", ->
it "adds the parent directory to the project paths", ->
pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt')
@@ -309,7 +386,7 @@ describe "AtomEnvironment", ->
updateAvailableHandler = jasmine.createSpy("update-available-handler")
subscription = atom.onUpdateAvailable updateAvailableHandler
- autoUpdater = require('remote').require('auto-updater')
+ autoUpdater = require('electron').remote.require('auto-updater')
autoUpdater.emit 'update-downloaded', null, "notes", "version"
waitsFor ->
@@ -318,3 +395,18 @@ describe "AtomEnvironment", ->
runs ->
{releaseVersion} = updateAvailableHandler.mostRecentCall.args[0]
expect(releaseVersion).toBe 'version'
+
+ describe "::getReleaseChannel()", ->
+ [version] = []
+ beforeEach ->
+ spyOn(atom, 'getVersion').andCallFake -> version
+
+ it "returns the correct channel based on the version number", ->
+ version = '1.5.6'
+ expect(atom.getReleaseChannel()).toBe 'stable'
+
+ version = '1.5.0-beta10'
+ expect(atom.getReleaseChannel()).toBe 'beta'
+
+ version = '1.7.0-dev-5340c91'
+ expect(atom.getReleaseChannel()).toBe 'dev'
diff --git a/spec/atom-reporter.coffee b/spec/atom-reporter.coffee
index 150eb9a4a..dcda4633b 100644
--- a/spec/atom-reporter.coffee
+++ b/spec/atom-reporter.coffee
@@ -172,7 +172,7 @@ class AtomReporter
listen document, 'click', '.stack-trace', (event) ->
event.currentTarget.classList.toggle('expanded')
- @reloadButton.addEventListener('click', -> require('ipc').send('call-window-method', 'restart'))
+ @reloadButton.addEventListener('click', -> require('electron').ipcRenderer.send('call-window-method', 'restart'))
updateSpecCounts: ->
if @skippedCount
diff --git a/spec/auto-update-manager-spec.js b/spec/auto-update-manager-spec.js
new file mode 100644
index 000000000..91b52192c
--- /dev/null
+++ b/spec/auto-update-manager-spec.js
@@ -0,0 +1,125 @@
+'use babel'
+
+import AutoUpdateManager from '../src/auto-update-manager'
+import {remote} from 'electron'
+const electronAutoUpdater = remote.require('electron').autoUpdater
+
+describe('AutoUpdateManager (renderer)', () => {
+ let autoUpdateManager
+
+ beforeEach(() => {
+ autoUpdateManager = new AutoUpdateManager({
+ applicationDelegate: atom.applicationDelegate
+ })
+ })
+
+ afterEach(() => {
+ autoUpdateManager.destroy()
+ })
+
+ describe('::onDidBeginCheckingForUpdate', () => {
+ it('subscribes to "did-begin-checking-for-update" event', () => {
+ const spy = jasmine.createSpy('spy')
+ autoUpdateManager.onDidBeginCheckingForUpdate(spy)
+ electronAutoUpdater.emit('checking-for-update')
+ waitsFor(() => {
+ return spy.callCount === 1
+ })
+ })
+ })
+
+ describe('::onDidBeginDownloadingUpdate', () => {
+ it('subscribes to "did-begin-downloading-update" event', () => {
+ const spy = jasmine.createSpy('spy')
+ autoUpdateManager.onDidBeginDownloadingUpdate(spy)
+ electronAutoUpdater.emit('update-available')
+ waitsFor(() => {
+ return spy.callCount === 1
+ })
+ })
+ })
+
+ describe('::onDidCompleteDownloadingUpdate', () => {
+ it('subscribes to "did-complete-downloading-update" event', () => {
+ const spy = jasmine.createSpy('spy')
+ autoUpdateManager.onDidCompleteDownloadingUpdate(spy)
+ electronAutoUpdater.emit('update-downloaded', null, null, '1.2.3')
+ waitsFor(() => {
+ return spy.callCount === 1
+ })
+ runs(() => {
+ expect(spy.mostRecentCall.args[0].releaseVersion).toBe('1.2.3')
+ })
+ })
+ })
+
+ describe('::onUpdateNotAvailable', () => {
+ it('subscribes to "update-not-available" event', () => {
+ const spy = jasmine.createSpy('spy')
+ autoUpdateManager.onUpdateNotAvailable(spy)
+ electronAutoUpdater.emit('update-not-available')
+ waitsFor(() => {
+ return spy.callCount === 1
+ })
+ })
+ })
+
+ describe('::onUpdateError', () => {
+ it('subscribes to "update-error" event', () => {
+ const spy = jasmine.createSpy('spy')
+ autoUpdateManager.onUpdateError(spy)
+ electronAutoUpdater.emit('error', {}, 'an error message')
+ waitsFor(() => spy.callCount === 1)
+ runs(() => expect(autoUpdateManager.getErrorMessage()).toBe('an error message'))
+ })
+ })
+
+ describe('::platformSupportsUpdates', () => {
+ let state, releaseChannel
+ it('returns true on OS X and Windows when in stable', () => {
+ spyOn(autoUpdateManager, 'getState').andCallFake(() => state)
+ spyOn(atom, 'getReleaseChannel').andCallFake(() => releaseChannel)
+
+ state = 'idle'
+ releaseChannel = 'stable'
+ expect(autoUpdateManager.platformSupportsUpdates()).toBe(true)
+
+ state = 'idle'
+ releaseChannel = 'dev'
+ expect(autoUpdateManager.platformSupportsUpdates()).toBe(false)
+
+ state = 'unsupported'
+ releaseChannel = 'stable'
+ expect(autoUpdateManager.platformSupportsUpdates()).toBe(false)
+
+ state = 'unsupported'
+ releaseChannel = 'dev'
+ expect(autoUpdateManager.platformSupportsUpdates()).toBe(false)
+ })
+ })
+
+ describe('::destroy', () => {
+ it('unsubscribes from all events', () => {
+ const spy = jasmine.createSpy('spy')
+ const doneIndicator = jasmine.createSpy('spy')
+ atom.applicationDelegate.onUpdateNotAvailable(doneIndicator)
+ autoUpdateManager.onDidBeginCheckingForUpdate(spy)
+ autoUpdateManager.onDidBeginDownloadingUpdate(spy)
+ autoUpdateManager.onDidCompleteDownloadingUpdate(spy)
+ autoUpdateManager.onUpdateNotAvailable(spy)
+ autoUpdateManager.destroy()
+ electronAutoUpdater.emit('checking-for-update')
+ electronAutoUpdater.emit('update-available')
+ electronAutoUpdater.emit('update-downloaded', null, null, '1.2.3')
+ electronAutoUpdater.emit('update-not-available')
+
+ waitsFor(() => {
+ return doneIndicator.callCount === 1
+ })
+
+ runs(() => {
+ expect(spy.callCount).toBe(0)
+ })
+ })
+ })
+})
diff --git a/spec/babel-spec.coffee b/spec/babel-spec.coffee
index 02f0583ee..e95b000cb 100644
--- a/spec/babel-spec.coffee
+++ b/spec/babel-spec.coffee
@@ -15,7 +15,6 @@ describe "Babel transpiler support", ->
CompileCache.setCacheDirectory(temp.mkdirSync('compile-cache'))
for cacheKey in Object.keys(require.cache)
if cacheKey.startsWith(path.join(__dirname, 'fixtures', 'babel'))
- console.log('deleting', cacheKey)
delete require.cache[cacheKey]
afterEach ->
diff --git a/spec/buffered-process-spec.coffee b/spec/buffered-process-spec.coffee
index 04cff0b6d..84d8b0440 100644
--- a/spec/buffered-process-spec.coffee
+++ b/spec/buffered-process-spec.coffee
@@ -1,5 +1,6 @@
ChildProcess = require 'child_process'
path = require 'path'
+fs = require 'fs-plus'
BufferedProcess = require '../src/buffered-process'
describe "BufferedProcess", ->
@@ -15,20 +16,20 @@ describe "BufferedProcess", ->
describe "when there is an error handler specified", ->
describe "when an error event is emitted by the process", ->
it "calls the error handler and does not throw an exception", ->
- process = new BufferedProcess
- command: 'bad-command-nope'
+ bufferedProcess = new BufferedProcess
+ command: 'bad-command-nope1'
args: ['nothing']
- options: {}
+ options: {shell: false}
errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle()
- process.onWillThrowError(errorSpy)
+ bufferedProcess.onWillThrowError(errorSpy)
waitsFor -> errorSpy.callCount > 0
runs ->
expect(window.onerror).not.toHaveBeenCalled()
expect(errorSpy).toHaveBeenCalled()
- expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope ENOENT'
+ expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope1 ENOENT'
describe "when an error is thrown spawning the process", ->
it "calls the error handler and does not throw an exception", ->
@@ -37,13 +38,13 @@ describe "BufferedProcess", ->
error.code = 'EAGAIN'
throw error
- process = new BufferedProcess
+ bufferedProcess = new BufferedProcess
command: 'ls'
args: []
options: {}
errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle()
- process.onWillThrowError(errorSpy)
+ bufferedProcess.onWillThrowError(errorSpy)
waitsFor -> errorSpy.callCount > 0
@@ -53,55 +54,24 @@ describe "BufferedProcess", ->
expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'Something is really wrong'
describe "when there is not an error handler specified", ->
- it "calls the error handler and does not throw an exception", ->
- process = new BufferedProcess
- command: 'bad-command-nope'
+ it "does throw an exception", ->
+ new BufferedProcess
+ command: 'bad-command-nope2'
args: ['nothing']
- options: {}
+ options: {shell: false}
waitsFor -> window.onerror.callCount > 0
runs ->
expect(window.onerror).toHaveBeenCalled()
- expect(window.onerror.mostRecentCall.args[0]).toContain 'Failed to spawn command `bad-command-nope`'
+ expect(window.onerror.mostRecentCall.args[0]).toContain 'Failed to spawn command `bad-command-nope2`'
expect(window.onerror.mostRecentCall.args[4].name).toBe 'BufferedProcessError'
- describe "on Windows", ->
- originalPlatform = null
-
- beforeEach ->
- # Prevent any commands from actually running and affecting the host
- originalSpawn = ChildProcess.spawn
- spyOn(ChildProcess, 'spawn').andCallFake ->
- # Just spawn something that won't actually modify the host
- if originalPlatform is 'win32'
- originalSpawn('dir')
- else
- originalSpawn('ls')
-
- originalPlatform = process.platform
- Object.defineProperty process, 'platform', value: 'win32'
-
- afterEach ->
- Object.defineProperty process, 'platform', value: originalPlatform
-
- describe "when the explorer command is spawned on Windows", ->
- it "doesn't quote arguments of the form /root,C...", ->
- new BufferedProcess({command: 'explorer.exe', args: ['/root,C:\\foo']})
- expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '"explorer.exe /root,C:\\foo"'
-
- it "spawns the command using a cmd.exe wrapper", ->
- new BufferedProcess({command: 'dir'})
- expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'cmd.exe'
- expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s'
- expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/c'
- expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '"dir"'
-
- it "calls the specified stdout, stderr, and exit callbacks ", ->
+ it "calls the specified stdout, stderr, and exit callbacks", ->
stdout = ''
stderr = ''
exitCallback = jasmine.createSpy('exit callback')
- process = new BufferedProcess
+ new BufferedProcess
command: atom.packages.getApmPath()
args: ['-h']
options: {}
@@ -114,3 +84,52 @@ describe "BufferedProcess", ->
runs ->
expect(stderr).toContain 'apm - Atom Package Manager'
expect(stdout).toEqual ''
+
+ it "calls the specified stdout callback with whole lines", ->
+ exitCallback = jasmine.createSpy('exit callback')
+ loremPath = require.resolve("./fixtures/lorem.txt")
+ content = fs.readFileSync(loremPath).toString()
+ baseContent = content.split('\n')
+ stdout = ''
+ allLinesEndWithNewline = true
+ new BufferedProcess
+ command: if process.platform is 'win32' then 'type' else 'cat'
+ args: [loremPath]
+ options: {}
+ stdout: (lines) ->
+ endsWithNewline = (lines.charAt lines.length - 1) is '\n'
+ if not endsWithNewline then allLinesEndWithNewline = false
+ stdout += lines
+ exit: exitCallback
+
+ waitsFor -> exitCallback.callCount is 1
+
+ runs ->
+ expect(allLinesEndWithNewline).toBeTrue
+ expect(stdout).toBe content
+
+ describe "on Windows", ->
+ originalPlatform = null
+
+ beforeEach ->
+ # Prevent any commands from actually running and affecting the host
+ originalSpawn = ChildProcess.spawn
+ spyOn(ChildProcess, 'spawn')
+ originalPlatform = process.platform
+ Object.defineProperty process, 'platform', value: 'win32'
+
+ afterEach ->
+ Object.defineProperty process, 'platform', value: originalPlatform
+
+ describe "when the explorer command is spawned on Windows", ->
+ it "doesn't quote arguments of the form /root,C...", ->
+ new BufferedProcess({command: 'explorer.exe', args: ['/root,C:\\foo']})
+ expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"explorer.exe /root,C:\\foo"'
+
+ it "spawns the command using a cmd.exe wrapper when options.shell is undefined", ->
+ new BufferedProcess({command: 'dir'})
+ expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'cmd.exe'
+ expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s'
+ expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/d'
+ expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '/c'
+ expect(ChildProcess.spawn.argsForCall[0][1][3]).toBe '"dir"'
diff --git a/spec/command-registry-spec.coffee b/spec/command-registry-spec.coffee
index ecdd42fd6..aaf044b1d 100644
--- a/spec/command-registry-spec.coffee
+++ b/spec/command-registry-spec.coffee
@@ -74,6 +74,13 @@ describe "CommandRegistry", ->
grandchild.dispatchEvent(new CustomEvent('command', bubbles: true))
expect(calls).toEqual ['.foo.bar', '.bar', '.foo']
+ it "orders inline listeners by reverse registration order", ->
+ calls = []
+ registry.add child, 'command', -> calls.push('child1')
+ registry.add child, 'command', -> calls.push('child2')
+ child.dispatchEvent(new CustomEvent('command', bubbles: true))
+ expect(calls).toEqual ['child2', 'child1']
+
it "stops bubbling through ancestors when .stopPropagation() is called on the event", ->
calls = []
diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee
index c7485ea65..acd9b112b 100644
--- a/spec/config-spec.coffee
+++ b/spec/config-spec.coffee
@@ -1621,6 +1621,16 @@ describe "Config", ->
expect(color.toHexString()).toBe '#ff0000'
expect(color.toRGBAString()).toBe 'rgba(255, 0, 0, 1)'
+ color.red = 11
+ color.green = 11
+ color.blue = 124
+ color.alpha = 1
+ atom.config.set('foo.bar.aColor', color)
+
+ color = atom.config.get('foo.bar.aColor')
+ expect(color.toHexString()).toBe '#0b0b7c'
+ expect(color.toRGBAString()).toBe 'rgba(11, 11, 124, 1)'
+
it 'coerces various types to a color object', ->
atom.config.set('foo.bar.aColor', 'red')
expect(atom.config.get('foo.bar.aColor')).toEqual {red: 255, green: 0, blue: 0, alpha: 1}
diff --git a/spec/decoration-manager-spec.coffee b/spec/decoration-manager-spec.coffee
new file mode 100644
index 000000000..c428df8cf
--- /dev/null
+++ b/spec/decoration-manager-spec.coffee
@@ -0,0 +1,85 @@
+DecorationManager = require '../src/decoration-manager'
+_ = require 'underscore-plus'
+
+describe "DecorationManager", ->
+ [decorationManager, buffer, defaultMarkerLayer] = []
+
+ beforeEach ->
+ buffer = atom.project.bufferForPathSync('sample.js')
+ displayLayer = buffer.addDisplayLayer()
+ defaultMarkerLayer = displayLayer.addMarkerLayer()
+ decorationManager = new DecorationManager(displayLayer, defaultMarkerLayer)
+
+ waitsForPromise ->
+ atom.packages.activatePackage('language-javascript')
+
+ afterEach ->
+ decorationManager.destroy()
+ buffer.release()
+
+ describe "decorations", ->
+ [marker, decoration, decorationProperties] = []
+ beforeEach ->
+ marker = defaultMarkerLayer.markBufferRange([[2, 13], [3, 15]])
+ decorationProperties = {type: 'line-number', class: 'one'}
+ decoration = decorationManager.decorateMarker(marker, decorationProperties)
+
+ it "can add decorations associated with markers and remove them", ->
+ expect(decoration).toBeDefined()
+ expect(decoration.getProperties()).toBe decorationProperties
+ expect(decorationManager.decorationForId(decoration.id)).toBe decoration
+ expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id][0]).toBe decoration
+
+ decoration.destroy()
+ expect(decorationManager.decorationsForScreenRowRange(2, 3)[marker.id]).not.toBeDefined()
+ expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined()
+
+ it "will not fail if the decoration is removed twice", ->
+ decoration.destroy()
+ decoration.destroy()
+ expect(decorationManager.decorationForId(decoration.id)).not.toBeDefined()
+
+ it "does not allow destroyed markers to be decorated", ->
+ marker.destroy()
+ expect(->
+ decorationManager.decorateMarker(marker, {type: 'overlay', item: document.createElement('div')})
+ ).toThrow("Cannot decorate a destroyed marker")
+ expect(decorationManager.getOverlayDecorations()).toEqual []
+
+ describe "when a decoration is updated via Decoration::update()", ->
+ it "emits an 'updated' event containing the new and old params", ->
+ decoration.onDidChangeProperties updatedSpy = jasmine.createSpy()
+ decoration.setProperties type: 'line-number', class: 'two'
+
+ {oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0]
+ expect(oldProperties).toEqual decorationProperties
+ expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'}
+
+ describe "::getDecorations(properties)", ->
+ it "returns decorations matching the given optional properties", ->
+ expect(decorationManager.getDecorations()).toEqual [decoration]
+ expect(decorationManager.getDecorations(class: 'two').length).toEqual 0
+ expect(decorationManager.getDecorations(class: 'one').length).toEqual 1
+
+ describe "::decorateMarker", ->
+ describe "when decorating gutters", ->
+ [marker] = []
+
+ beforeEach ->
+ marker = defaultMarkerLayer.markBufferRange([[1, 0], [1, 0]])
+
+ it "creates a decoration that is both of 'line-number' and 'gutter' type when called with the 'line-number' type", ->
+ decorationProperties = {type: 'line-number', class: 'one'}
+ decoration = decorationManager.decorateMarker(marker, decorationProperties)
+ expect(decoration.isType('line-number')).toBe true
+ expect(decoration.isType('gutter')).toBe true
+ expect(decoration.getProperties().gutterName).toBe 'line-number'
+ expect(decoration.getProperties().class).toBe 'one'
+
+ it "creates a decoration that is only of 'gutter' type if called with the 'gutter' type and a 'gutterName'", ->
+ decorationProperties = {type: 'gutter', gutterName: 'test-gutter', class: 'one'}
+ decoration = decorationManager.decorateMarker(marker, decorationProperties)
+ expect(decoration.isType('gutter')).toBe true
+ expect(decoration.isType('line-number')).toBe false
+ expect(decoration.getProperties().gutterName).toBe 'test-gutter'
+ expect(decoration.getProperties().class).toBe 'one'
diff --git a/spec/display-buffer-spec.coffee b/spec/display-buffer-spec.coffee
deleted file mode 100644
index 0246008a4..000000000
--- a/spec/display-buffer-spec.coffee
+++ /dev/null
@@ -1,1312 +0,0 @@
-DisplayBuffer = require '../src/display-buffer'
-_ = require 'underscore-plus'
-
-describe "DisplayBuffer", ->
- [displayBuffer, buffer, changeHandler, tabLength] = []
- beforeEach ->
- tabLength = 2
-
- buffer = atom.project.bufferForPathSync('sample.js')
- displayBuffer = new DisplayBuffer({
- buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars,
- packageManager: atom.packages, assert: ->
- })
- changeHandler = jasmine.createSpy 'changeHandler'
- displayBuffer.onDidChange changeHandler
-
- waitsForPromise ->
- atom.packages.activatePackage('language-javascript')
-
- afterEach ->
- displayBuffer.destroy()
- buffer.release()
-
- describe "::copy()", ->
- it "creates a new DisplayBuffer with the same initial state", ->
- marker1 = displayBuffer.markBufferRange([[1, 2], [3, 4]], id: 1)
- marker2 = displayBuffer.markBufferRange([[2, 3], [4, 5]], reversed: true, id: 2)
- marker3 = displayBuffer.markBufferPosition([5, 6], id: 3)
- displayBuffer.createFold(3, 5)
-
- displayBuffer2 = displayBuffer.copy()
- expect(displayBuffer2.id).not.toBe displayBuffer.id
- expect(displayBuffer2.buffer).toBe displayBuffer.buffer
- expect(displayBuffer2.getTabLength()).toBe displayBuffer.getTabLength()
-
- expect(displayBuffer2.getMarkerCount()).toEqual displayBuffer.getMarkerCount()
- expect(displayBuffer2.findMarker(id: 1)).toEqual marker1
- expect(displayBuffer2.findMarker(id: 2)).toEqual marker2
- expect(displayBuffer2.findMarker(id: 3)).toEqual marker3
- expect(displayBuffer2.isFoldedAtBufferRow(3)).toBeTruthy()
-
- # can diverge from origin
- displayBuffer2.unfoldBufferRow(3)
- expect(displayBuffer2.isFoldedAtBufferRow(3)).not.toBe displayBuffer.isFoldedAtBufferRow(3)
-
- describe "when the buffer changes", ->
- it "renders line numbers correctly", ->
- originalLineCount = displayBuffer.getLineCount()
- oneHundredLines = [0..100].join("\n")
- buffer.insert([0, 0], oneHundredLines)
- expect(displayBuffer.getLineCount()).toBe 100 + originalLineCount
-
- it "updates the display buffer prior to invoking change handlers registered on the buffer", ->
- buffer.onDidChange -> expect(displayBuffer2.tokenizedLineForScreenRow(0).text).toBe "testing"
- displayBuffer2 = new DisplayBuffer({
- buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars,
- packageManager: atom.packages, assert: ->
- })
- buffer.setText("testing")
-
- describe "soft wrapping", ->
- beforeEach ->
- displayBuffer.setEditorWidthInChars(50)
- displayBuffer.setSoftWrapped(true)
- displayBuffer.setDefaultCharWidth(1)
- changeHandler.reset()
-
- describe "rendering of soft-wrapped lines", ->
- describe "when there are double width characters", ->
- it "takes them into account when finding the soft wrap column", ->
- buffer.setText("私たちのフ是一个地方,数千名学生12345业余爱们的板作为hello world this is a pretty long latin line")
- displayBuffer.setDefaultCharWidth(1, 5, 0, 0)
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe("私たちのフ是一个地方")
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe(",数千名学生12345业余爱")
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe("们的板作为hello world this is a ")
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe("pretty long latin line")
-
- describe "when there are half width characters", ->
- it "takes them into account when finding the soft wrap column", ->
- displayBuffer.setDefaultCharWidth(1, 0, 5, 0)
- buffer.setText("abcᆰᆱᆲネヌネノハヒフヒフヌᄡ○○○hello world this is a pretty long line")
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe("abcᆰᆱᆲネヌネノハヒ")
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe("フヒフヌᄡ○○○hello ")
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe("world this is a pretty long line")
-
- describe "when there are korean characters", ->
- it "takes them into account when finding the soft wrap column", ->
- displayBuffer.setDefaultCharWidth(1, 0, 0, 10)
- buffer.setText("1234세계를향한대화,유니코제10회유니코드국제")
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe("1234세계를향")
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe("한대화,유")
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe("니코제10회")
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe("유니코드국")
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe("제")
-
- describe "when editor.softWrapAtPreferredLineLength is set", ->
- it "uses the preferred line length as the soft wrap column when it is less than the configured soft wrap column", ->
- atom.config.set('editor.preferredLineLength', 100)
- atom.config.set('editor.softWrapAtPreferredLineLength', true)
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' return '
-
- atom.config.set('editor.preferredLineLength', 5)
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' fun'
-
- atom.config.set('editor.softWrapAtPreferredLineLength', false)
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' return '
-
- describe "when editor width is negative", ->
- it "does not hang while wrapping", ->
- displayBuffer.setDefaultCharWidth(1)
- displayBuffer.setWidth(-1)
-
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe " "
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe " var sort = function(items) {"
-
- describe "when the line is shorter than the max line length", ->
- it "renders the line unchanged", ->
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe buffer.lineForRow(0)
-
- describe "when the line is empty", ->
- it "renders the empty line", ->
- expect(displayBuffer.tokenizedLineForScreenRow(13).text).toBe ''
-
- describe "when there is a non-whitespace character at the max length boundary", ->
- describe "when there is whitespace before the boundary", ->
- it "wraps the line at the end of the first whitespace preceding the boundary", ->
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' return '
- expect(displayBuffer.tokenizedLineForScreenRow(11).text).toBe ' sort(left).concat(pivot).concat(sort(right));'
-
- it "wraps the line at the first CJK character before the boundary", ->
- displayBuffer.setEditorWidthInChars(10)
-
- buffer.setTextInRange([[0, 0], [1, 0]], 'abcd efg유私フ业余爱\n')
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe 'abcd efg유私'
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe 'フ业余爱'
-
- buffer.setTextInRange([[0, 0], [1, 0]], 'abcd ef유gef业余爱\n')
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe 'abcd ef유'
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe 'gef业余爱'
-
- describe "when there is no whitespace before the boundary", ->
- it "wraps the line at the first CJK character before the boundary", ->
- buffer.setTextInRange([[0, 0], [1, 0]], '私たちのabcdefghij\n')
- displayBuffer.setEditorWidthInChars(10)
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe '私たちの'
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe 'abcdefghij'
-
- it "wraps the line exactly at the boundary when no CJK character is found, since there's no more graceful place to wrap it", ->
- buffer.setTextInRange([[0, 0], [1, 0]], 'abcdefghijklmnopqrstuvwxyz\n')
- displayBuffer.setEditorWidthInChars(10)
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe 'abcdefghij'
- expect(displayBuffer.tokenizedLineForScreenRow(0).bufferDelta).toBe 'abcdefghij'.length
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe 'klmnopqrst'
- expect(displayBuffer.tokenizedLineForScreenRow(1).bufferDelta).toBe 'klmnopqrst'.length
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe 'uvwxyz'
- expect(displayBuffer.tokenizedLineForScreenRow(2).bufferDelta).toBe 'uvwxyz'.length
-
- it "closes all scopes at the wrap boundary", ->
- displayBuffer.setEditorWidthInChars(10)
- buffer.setText("`aaa${1+2}aaa`")
- iterator = displayBuffer.tokenizedLineForScreenRow(1).getTokenIterator()
- scopes = iterator.getScopes()
- expect(scopes[scopes.length - 1]).not.toBe 'punctuation.section.embedded.js'
-
- describe "when there is a whitespace character at the max length boundary", ->
- it "wraps the line at the first non-whitespace character following the boundary", ->
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe ' var pivot = items.shift(), current, left = [], '
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe ' right = [];'
-
- describe "when the only whitespace characters are at the beginning of the line", ->
- beforeEach ->
- displayBuffer.setEditorWidthInChars(10)
-
- it "wraps the line at the max length when indented with tabs", ->
- buffer.setTextInRange([[0, 0], [1, 0]], '\t\tabcdefghijklmnopqrstuvwxyz')
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe ' abcdef'
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe ' ghijkl'
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe ' mnopqr'
-
- it "wraps the line at the max length when indented with spaces", ->
- buffer.setTextInRange([[0, 0], [1, 0]], ' abcdefghijklmnopqrstuvwxyz')
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe ' abcdef'
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe ' ghijkl'
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe ' mnopqr'
-
- describe "when there are hard tabs", ->
- beforeEach ->
- buffer.setText(buffer.getText().replace(new RegExp(' ', 'g'), '\t'))
-
- it "correctly tokenizes the hard tabs", ->
- expect(displayBuffer.tokenizedLineForScreenRow(3).tokens[0].isHardTab).toBeTruthy()
- expect(displayBuffer.tokenizedLineForScreenRow(3).tokens[1].isHardTab).toBeTruthy()
-
- describe "when a line is wrapped", ->
- it "breaks soft-wrap indentation into a token for each indentation level to support indent guides", ->
- tokenizedLine = displayBuffer.tokenizedLineForScreenRow(4)
-
- expect(tokenizedLine.tokens[0].value).toBe(" ")
- expect(tokenizedLine.tokens[0].isSoftWrapIndentation).toBeTruthy()
-
- expect(tokenizedLine.tokens[1].value).toBe(" ")
- expect(tokenizedLine.tokens[1].isSoftWrapIndentation).toBeTruthy()
-
- expect(tokenizedLine.tokens[2].isSoftWrapIndentation).toBeFalsy()
-
- describe "when editor.softWrapHangingIndent is set", ->
- beforeEach ->
- atom.config.set('editor.softWrapHangingIndent', 3)
-
- it "further indents wrapped lines", ->
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe " return "
- expect(displayBuffer.tokenizedLineForScreenRow(11).text).toBe " sort(left).concat(pivot).concat(sort(right)"
- expect(displayBuffer.tokenizedLineForScreenRow(12).text).toBe " );"
-
- it "includes hanging indent when breaking soft-wrap indentation into tokens", ->
- tokenizedLine = displayBuffer.tokenizedLineForScreenRow(4)
-
- expect(tokenizedLine.tokens[0].value).toBe(" ")
- expect(tokenizedLine.tokens[0].isSoftWrapIndentation).toBeTruthy()
-
- expect(tokenizedLine.tokens[1].value).toBe(" ")
- expect(tokenizedLine.tokens[1].isSoftWrapIndentation).toBeTruthy()
-
- expect(tokenizedLine.tokens[2].value).toBe(" ") # hanging indent
- expect(tokenizedLine.tokens[2].isSoftWrapIndentation).toBeTruthy()
-
- expect(tokenizedLine.tokens[3].value).toBe(" ") # odd space
- expect(tokenizedLine.tokens[3].isSoftWrapIndentation).toBeTruthy()
-
- expect(tokenizedLine.tokens[4].isSoftWrapIndentation).toBeFalsy()
-
- describe "when the buffer changes", ->
- describe "when buffer lines are updated", ->
- describe "when whitespace is added after the max line length", ->
- it "adds whitespace to the end of the current line and wraps an empty line", ->
- fiftyCharacters = _.multiplyString("x", 50)
- buffer.setText(fiftyCharacters)
- buffer.insert([0, 51], " ")
-
- describe "when the update makes a soft-wrapped line shorter than the max line length", ->
- it "rewraps the line and emits a change event", ->
- buffer.delete([[6, 24], [6, 42]])
- expect(displayBuffer.tokenizedLineForScreenRow(7).text).toBe ' current < pivot ? : right.push(current);'
- expect(displayBuffer.tokenizedLineForScreenRow(8).text).toBe ' }'
-
- expect(changeHandler).toHaveBeenCalled()
- [[event]]= changeHandler.argsForCall
-
- expect(event).toEqual(start: 7, end: 8, screenDelta: -1, bufferDelta: 0)
-
- describe "when the update causes a line to soft wrap an additional time", ->
- it "rewraps the line and emits a change event", ->
- buffer.insert([6, 28], '1234567890')
- expect(displayBuffer.tokenizedLineForScreenRow(7).text).toBe ' current < pivot ? '
- expect(displayBuffer.tokenizedLineForScreenRow(8).text).toBe ' left1234567890.push(current) : '
- expect(displayBuffer.tokenizedLineForScreenRow(9).text).toBe ' right.push(current);'
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe ' }'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, screenDelta: 1, bufferDelta: 0)
-
- describe "when buffer lines are inserted", ->
- it "inserts / updates wrapped lines and emits a change event", ->
- buffer.insert([6, 21], '1234567890 abcdefghij 1234567890\nabcdefghij')
- expect(displayBuffer.tokenizedLineForScreenRow(7).text).toBe ' current < pivot1234567890 abcdefghij '
- expect(displayBuffer.tokenizedLineForScreenRow(8).text).toBe ' 1234567890'
- expect(displayBuffer.tokenizedLineForScreenRow(9).text).toBe 'abcdefghij ? left.push(current) : '
- expect(displayBuffer.tokenizedLineForScreenRow(10).text).toBe 'right.push(current);'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, screenDelta: 2, bufferDelta: 1)
-
- describe "when buffer lines are removed", ->
- it "removes lines and emits a change event", ->
- buffer.setTextInRange([[3, 21], [7, 5]], ';')
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe ' var pivot = items;'
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe ' return '
- expect(displayBuffer.tokenizedLineForScreenRow(5).text).toBe ' sort(left).concat(pivot).concat(sort(right));'
- expect(displayBuffer.tokenizedLineForScreenRow(6).text).toBe ' };'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 3, end: 9, screenDelta: -6, bufferDelta: -4)
-
- describe "when a newline is inserted, deleted, and re-inserted at the end of a wrapped line (regression)", ->
- it "correctly renders the original wrapped line", ->
- buffer = atom.project.buildBufferSync(null, '')
- displayBuffer = new DisplayBuffer({
- buffer, tabLength, editorWidthInChars: 30, config: atom.config,
- grammarRegistry: atom.grammars, packageManager: atom.packages, assert: ->
- })
- displayBuffer.setDefaultCharWidth(1)
- displayBuffer.setSoftWrapped(true)
-
- buffer.insert([0, 0], "the quick brown fox jumps over the lazy dog.")
- buffer.insert([0, Infinity], '\n')
- buffer.delete([[0, Infinity], [1, 0]])
- buffer.insert([0, Infinity], '\n')
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "the quick brown fox jumps over "
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "the lazy dog."
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe ""
-
- describe "position translation", ->
- it "translates positions accounting for wrapped lines", ->
- # before any wrapped lines
- expect(displayBuffer.screenPositionForBufferPosition([0, 5])).toEqual([0, 5])
- expect(displayBuffer.bufferPositionForScreenPosition([0, 5])).toEqual([0, 5])
- expect(displayBuffer.screenPositionForBufferPosition([0, 29])).toEqual([0, 29])
- expect(displayBuffer.bufferPositionForScreenPosition([0, 29])).toEqual([0, 29])
-
- # on a wrapped line
- expect(displayBuffer.screenPositionForBufferPosition([3, 5])).toEqual([3, 5])
- expect(displayBuffer.bufferPositionForScreenPosition([3, 5])).toEqual([3, 5])
- expect(displayBuffer.screenPositionForBufferPosition([3, 50])).toEqual([3, 50])
- expect(displayBuffer.screenPositionForBufferPosition([3, 51])).toEqual([3, 50])
- expect(displayBuffer.bufferPositionForScreenPosition([4, 0])).toEqual([3, 50])
- expect(displayBuffer.bufferPositionForScreenPosition([3, 50])).toEqual([3, 50])
- expect(displayBuffer.screenPositionForBufferPosition([3, 62])).toEqual([4, 15])
- expect(displayBuffer.bufferPositionForScreenPosition([4, 11])).toEqual([3, 58])
-
- # following a wrapped line
- expect(displayBuffer.screenPositionForBufferPosition([4, 5])).toEqual([5, 5])
- expect(displayBuffer.bufferPositionForScreenPosition([5, 5])).toEqual([4, 5])
-
- # clip screen position inputs before translating
- expect(displayBuffer.bufferPositionForScreenPosition([-5, -5])).toEqual([0, 0])
- expect(displayBuffer.bufferPositionForScreenPosition([Infinity, Infinity])).toEqual([12, 2])
- expect(displayBuffer.bufferPositionForScreenPosition([3, -5])).toEqual([3, 0])
- expect(displayBuffer.bufferPositionForScreenPosition([3, Infinity])).toEqual([3, 50])
-
- describe ".setEditorWidthInChars(length)", ->
- it "changes the length at which lines are wrapped and emits a change event for all screen lines", ->
- tokensText = (tokens) ->
- _.pluck(tokens, 'value').join('')
-
- displayBuffer.setEditorWidthInChars(40)
- expect(tokensText displayBuffer.tokenizedLineForScreenRow(4).tokens).toBe ' left = [], right = [];'
- expect(tokensText displayBuffer.tokenizedLineForScreenRow(5).tokens).toBe ' while(items.length > 0) {'
- expect(tokensText displayBuffer.tokenizedLineForScreenRow(12).tokens).toBe ' sort(left).concat(pivot).concat(sort'
- expect(changeHandler).toHaveBeenCalledWith(start: 0, end: 15, screenDelta: 3, bufferDelta: 0)
-
- it "only allows positive widths to be assigned", ->
- displayBuffer.setEditorWidthInChars(0)
- expect(displayBuffer.editorWidthInChars).not.toBe 0
- displayBuffer.setEditorWidthInChars(-1)
- expect(displayBuffer.editorWidthInChars).not.toBe -1
-
- describe "primitive folding", ->
- beforeEach ->
- displayBuffer.destroy()
- buffer.release()
- buffer = atom.project.bufferForPathSync('two-hundred.txt')
- displayBuffer = new DisplayBuffer({
- buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars,
- packageManager: atom.packages, assert: ->
- })
- displayBuffer.onDidChange changeHandler
-
- describe "when folds are created and destroyed", ->
- describe "when a fold spans multiple lines", ->
- it "replaces the lines spanned by the fold with a placeholder that references the fold object", ->
- fold = displayBuffer.createFold(4, 7)
- expect(fold).toBeDefined()
-
- [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5)
- expect(line4.fold).toBe fold
- expect(line4.text).toMatch /^4-+/
- expect(line5.text).toBe '8'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 7, screenDelta: -3, bufferDelta: 0)
- changeHandler.reset()
-
- fold.destroy()
- [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5)
- expect(line4.fold).toBeUndefined()
- expect(line4.text).toMatch /^4-+/
- expect(line5.text).toBe '5'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 4, screenDelta: 3, bufferDelta: 0)
-
- describe "when a fold spans a single line", ->
- it "renders a fold placeholder for the folded line but does not skip any lines", ->
- fold = displayBuffer.createFold(4, 4)
-
- [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5)
- expect(line4.fold).toBe fold
- expect(line4.text).toMatch /^4-+/
- expect(line5.text).toBe '5'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 4, screenDelta: 0, bufferDelta: 0)
-
- # Line numbers don't actually change, but it's not worth the complexity to have this
- # be false for single line folds since they are so rare
- changeHandler.reset()
-
- fold.destroy()
-
- [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5)
- expect(line4.fold).toBeUndefined()
- expect(line4.text).toMatch /^4-+/
- expect(line5.text).toBe '5'
-
- expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 4, screenDelta: 0, bufferDelta: 0)
-
- describe "when a fold is nested within another fold", ->
- it "does not render the placeholder for the inner fold until the outer fold is destroyed", ->
- innerFold = displayBuffer.createFold(6, 7)
- outerFold = displayBuffer.createFold(4, 8)
-
- [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5)
- expect(line4.fold).toBe outerFold
- expect(line4.text).toMatch /4-+/
- expect(line5.text).toMatch /9-+/
-
- outerFold.destroy()
- [line4, line5, line6, line7] = displayBuffer.tokenizedLinesForScreenRows(4, 7)
- expect(line4.fold).toBeUndefined()
- expect(line4.text).toMatch /^4-+/
- expect(line5.text).toBe '5'
- expect(line6.fold).toBe innerFold
- expect(line6.text).toBe '6'
- expect(line7.text).toBe '8'
-
- it "allows the outer fold to start at the same location as the inner fold", ->
- innerFold = displayBuffer.createFold(4, 6)
- outerFold = displayBuffer.createFold(4, 8)
-
- [line4, line5] = displayBuffer.tokenizedLinesForScreenRows(4, 5)
- expect(line4.fold).toBe outerFold
- expect(line4.text).toMatch /4-+/
- expect(line5.text).toMatch /9-+/
-
- describe "when creating a fold where one already exists", ->
- it "returns existing fold and does't create new fold", ->
- fold = displayBuffer.createFold(0, 10)
- expect(displayBuffer.foldsMarkerLayer.getMarkers().length).toBe 1
-
- newFold = displayBuffer.createFold(0, 10)
- expect(newFold).toBe fold
- expect(displayBuffer.foldsMarkerLayer.getMarkers().length).toBe 1
-
- describe "when a fold is created inside an existing folded region", ->
- it "creates/destroys the fold, but does not trigger change event", ->
- outerFold = displayBuffer.createFold(0, 10)
- changeHandler.reset()
-
- innerFold = displayBuffer.createFold(2, 5)
- expect(changeHandler).not.toHaveBeenCalled()
- [line0, line1] = displayBuffer.tokenizedLinesForScreenRows(0, 1)
- expect(line0.fold).toBe outerFold
- expect(line1.fold).toBeUndefined()
-
- changeHandler.reset()
- innerFold.destroy()
- expect(changeHandler).not.toHaveBeenCalled()
- [line0, line1] = displayBuffer.tokenizedLinesForScreenRows(0, 1)
- expect(line0.fold).toBe outerFold
- expect(line1.fold).toBeUndefined()
-
- describe "when a fold ends where another fold begins", ->
- it "continues to hide the lines inside the second fold", ->
- fold2 = displayBuffer.createFold(4, 9)
- fold1 = displayBuffer.createFold(0, 4)
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toMatch /^0/
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toMatch /^10/
-
- describe "when there is another display buffer pointing to the same buffer", ->
- it "does not consider folds to be nested inside of folds from the other display buffer", ->
- otherDisplayBuffer = new DisplayBuffer({
- buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars,
- packageManager: atom.packages, assert: ->
- })
- otherDisplayBuffer.createFold(1, 5)
-
- displayBuffer.createFold(2, 4)
- expect(otherDisplayBuffer.foldsStartingAtBufferRow(2).length).toBe 0
-
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '2'
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe '5'
-
- describe "when the buffer changes", ->
- [fold1, fold2] = []
- beforeEach ->
- fold1 = displayBuffer.createFold(2, 4)
- fold2 = displayBuffer.createFold(6, 8)
- changeHandler.reset()
-
- describe "when the old range surrounds a fold", ->
- beforeEach ->
- buffer.setTextInRange([[1, 0], [5, 1]], 'party!')
-
- it "removes the fold and replaces the selection with the new text", ->
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "0"
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "party!"
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 1, end: 3, screenDelta: -2, bufferDelta: -4)
-
- describe "when the changes is subsequently undone", ->
- xit "restores destroyed folds", ->
- buffer.undo()
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '2'
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe '5'
-
- describe "when the old range surrounds two nested folds", ->
- it "removes both folds and replaces the selection with the new text", ->
- displayBuffer.createFold(2, 9)
- changeHandler.reset()
-
- buffer.setTextInRange([[1, 0], [10, 0]], 'goodbye')
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "0"
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "goodbye10"
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe "11"
-
- expect(changeHandler).toHaveBeenCalledWith(start: 1, end: 3, screenDelta: -2, bufferDelta: -9)
-
- describe "when multiple changes happen above the fold", ->
- it "repositions folds correctly", ->
- buffer.delete([[1, 1], [2, 0]])
- buffer.insert([0, 1], "\nnew")
-
- expect(fold1.getStartRow()).toBe 2
- expect(fold1.getEndRow()).toBe 4
-
- describe "when the old range precedes lines with a fold", ->
- describe "when the new range precedes lines with a fold", ->
- it "updates the buffer and re-positions subsequent folds", ->
- buffer.setTextInRange([[0, 0], [1, 1]], 'abc')
-
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "abc"
- expect(displayBuffer.tokenizedLineForScreenRow(1).fold).toBe fold1
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe "5"
- expect(displayBuffer.tokenizedLineForScreenRow(3).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 0, end: 1, screenDelta: -1, bufferDelta: -1)
- changeHandler.reset()
-
- fold1.destroy()
- expect(displayBuffer.tokenizedLineForScreenRow(0).text).toBe "abc"
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "2"
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch /^4-+/
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe "5"
- expect(displayBuffer.tokenizedLineForScreenRow(5).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(6).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 1, end: 1, screenDelta: 2, bufferDelta: 0)
-
- describe "when the old range straddles the beginning of a fold", ->
- it "destroys the fold", ->
- buffer.setTextInRange([[1, 1], [3, 0]], "a\nb\nc\nd\n")
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe '1a'
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe 'b'
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBeUndefined()
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe 'c'
-
- describe "when the old range follows a fold", ->
- it "re-positions the screen ranges for the change event based on the preceding fold", ->
- buffer.setTextInRange([[10, 0], [11, 0]], 'abc')
-
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "1"
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe "5"
- expect(displayBuffer.tokenizedLineForScreenRow(4).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(5).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 6, end: 7, screenDelta: -1, bufferDelta: -1)
-
- describe "when the old range is inside a fold", ->
- describe "when the end of the new range precedes the end of the fold", ->
- it "updates the fold and ensures the change is present when the fold is destroyed", ->
- buffer.insert([3, 0], '\n')
- expect(fold1.getStartRow()).toBe 2
- expect(fold1.getEndRow()).toBe 5
-
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "1"
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe "2"
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch "5"
- expect(displayBuffer.tokenizedLineForScreenRow(4).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(5).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 2, end: 2, screenDelta: 0, bufferDelta: 1)
-
- describe "when the end of the new range exceeds the end of the fold", ->
- it "expands the fold to contain all the inserted lines", ->
- buffer.setTextInRange([[3, 0], [4, 0]], 'a\nb\nc\nd\n')
- expect(fold1.getStartRow()).toBe 2
- expect(fold1.getEndRow()).toBe 7
-
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "1"
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe "2"
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch "5"
- expect(displayBuffer.tokenizedLineForScreenRow(4).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(5).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 2, end: 2, screenDelta: 0, bufferDelta: 3)
-
- describe "when the old range straddles the end of the fold", ->
- describe "when the end of the new range precedes the end of the fold", ->
- it "destroys the fold", ->
- fold2.destroy()
- buffer.setTextInRange([[3, 0], [6, 0]], 'a\n')
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '2'
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBeUndefined()
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toBe 'a'
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe '6'
-
- describe "when the old range is contained to a single line in-between two folds", ->
- it "re-renders the line with the placeholder and re-positions the second fold", ->
- buffer.insert([5, 0], 'abc\n')
-
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe "1"
- expect(displayBuffer.tokenizedLineForScreenRow(2).fold).toBe fold1
- expect(displayBuffer.tokenizedLineForScreenRow(3).text).toMatch "abc"
- expect(displayBuffer.tokenizedLineForScreenRow(4).text).toBe "5"
- expect(displayBuffer.tokenizedLineForScreenRow(5).fold).toBe fold2
- expect(displayBuffer.tokenizedLineForScreenRow(6).text).toMatch /^9-+/
-
- expect(changeHandler).toHaveBeenCalledWith(start: 3, end: 3, screenDelta: 1, bufferDelta: 1)
-
- describe "when the change starts at the beginning of a fold but does not extend to the end (regression)", ->
- it "preserves a proper mapping between buffer and screen coordinates", ->
- expect(displayBuffer.screenPositionForBufferPosition([8, 0])).toEqual [4, 0]
- buffer.setTextInRange([[2, 0], [3, 0]], "\n")
- expect(displayBuffer.screenPositionForBufferPosition([8, 0])).toEqual [4, 0]
-
- describe "position translation", ->
- it "translates positions to account for folded lines and characters and the placeholder", ->
- fold = displayBuffer.createFold(4, 7)
-
- # preceding fold: identity
- expect(displayBuffer.screenPositionForBufferPosition([3, 0])).toEqual [3, 0]
- expect(displayBuffer.screenPositionForBufferPosition([4, 0])).toEqual [4, 0]
-
- expect(displayBuffer.bufferPositionForScreenPosition([3, 0])).toEqual [3, 0]
- expect(displayBuffer.bufferPositionForScreenPosition([4, 0])).toEqual [4, 0]
-
- # inside of fold: translate to the start of the fold
- expect(displayBuffer.screenPositionForBufferPosition([4, 35])).toEqual [4, 0]
- expect(displayBuffer.screenPositionForBufferPosition([5, 5])).toEqual [4, 0]
-
- # following fold
- expect(displayBuffer.screenPositionForBufferPosition([8, 0])).toEqual [5, 0]
- expect(displayBuffer.screenPositionForBufferPosition([11, 2])).toEqual [8, 2]
-
- expect(displayBuffer.bufferPositionForScreenPosition([5, 0])).toEqual [8, 0]
- expect(displayBuffer.bufferPositionForScreenPosition([9, 2])).toEqual [12, 2]
-
- # clip screen positions before translating
- expect(displayBuffer.bufferPositionForScreenPosition([-5, -5])).toEqual([0, 0])
- expect(displayBuffer.bufferPositionForScreenPosition([Infinity, Infinity])).toEqual([200, 0])
-
- # after fold is destroyed
- fold.destroy()
-
- expect(displayBuffer.screenPositionForBufferPosition([8, 0])).toEqual [8, 0]
- expect(displayBuffer.screenPositionForBufferPosition([11, 2])).toEqual [11, 2]
-
- expect(displayBuffer.bufferPositionForScreenPosition([5, 0])).toEqual [5, 0]
- expect(displayBuffer.bufferPositionForScreenPosition([9, 2])).toEqual [9, 2]
-
- describe ".unfoldBufferRow(row)", ->
- it "destroys all folds containing the given row", ->
- displayBuffer.createFold(2, 4)
- displayBuffer.createFold(2, 6)
- displayBuffer.createFold(7, 8)
- displayBuffer.createFold(1, 9)
- displayBuffer.createFold(11, 12)
-
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe '1'
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '10'
-
- displayBuffer.unfoldBufferRow(2)
- expect(displayBuffer.tokenizedLineForScreenRow(1).text).toBe '1'
- expect(displayBuffer.tokenizedLineForScreenRow(2).text).toBe '2'
- expect(displayBuffer.tokenizedLineForScreenRow(7).fold).toBeDefined()
- expect(displayBuffer.tokenizedLineForScreenRow(8).text).toMatch /^9-+/
- expect(displayBuffer.tokenizedLineForScreenRow(10).fold).toBeDefined()
-
- describe ".outermostFoldsInBufferRowRange(startRow, endRow)", ->
- it "returns the outermost folds entirely contained in the given row range, exclusive of end row", ->
- fold1 = displayBuffer.createFold(4, 7)
- fold2 = displayBuffer.createFold(5, 6)
- fold3 = displayBuffer.createFold(11, 15)
- fold4 = displayBuffer.createFold(12, 13)
- fold5 = displayBuffer.createFold(16, 17)
-
- expect(displayBuffer.outermostFoldsInBufferRowRange(3, 18)).toEqual [fold1, fold3, fold5]
- expect(displayBuffer.outermostFoldsInBufferRowRange(5, 16)).toEqual [fold3]
-
- describe "::clipScreenPosition(screenPosition, wrapBeyondNewlines: false, wrapAtSoftNewlines: false, clip: 'closest')", ->
- beforeEach ->
- tabLength = 4
-
- displayBuffer.setDefaultCharWidth(1)
- displayBuffer.setTabLength(tabLength)
- displayBuffer.setSoftWrapped(true)
- displayBuffer.setEditorWidthInChars(50)
-
- it "allows valid positions", ->
- expect(displayBuffer.clipScreenPosition([4, 5])).toEqual [4, 5]
- expect(displayBuffer.clipScreenPosition([4, 11])).toEqual [4, 11]
-
- it "disallows negative positions", ->
- expect(displayBuffer.clipScreenPosition([-1, -1])).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([-1, 10])).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, -1])).toEqual [0, 0]
-
- it "disallows positions beyond the last row", ->
- expect(displayBuffer.clipScreenPosition([1000, 0])).toEqual [15, 2]
- expect(displayBuffer.clipScreenPosition([1000, 1000])).toEqual [15, 2]
-
- describe "when wrapBeyondNewlines is false (the default)", ->
- it "wraps positions beyond the end of hard newlines to the end of the line", ->
- expect(displayBuffer.clipScreenPosition([1, 10000])).toEqual [1, 30]
- expect(displayBuffer.clipScreenPosition([4, 30])).toEqual [4, 15]
- expect(displayBuffer.clipScreenPosition([4, 1000])).toEqual [4, 15]
-
- describe "when wrapBeyondNewlines is true", ->
- it "wraps positions past the end of hard newlines to the next line", ->
- expect(displayBuffer.clipScreenPosition([0, 29], wrapBeyondNewlines: true)).toEqual [0, 29]
- expect(displayBuffer.clipScreenPosition([0, 30], wrapBeyondNewlines: true)).toEqual [1, 0]
- expect(displayBuffer.clipScreenPosition([0, 1000], wrapBeyondNewlines: true)).toEqual [1, 0]
-
- it "wraps positions in the middle of fold lines to the next screen line", ->
- displayBuffer.createFold(3, 5)
- expect(displayBuffer.clipScreenPosition([3, 5], wrapBeyondNewlines: true)).toEqual [4, 0]
-
- describe "when skipSoftWrapIndentation is false (the default)", ->
- it "wraps positions at the end of previous soft-wrapped line", ->
- expect(displayBuffer.clipScreenPosition([4, 0])).toEqual [3, 50]
- expect(displayBuffer.clipScreenPosition([4, 1])).toEqual [3, 50]
- expect(displayBuffer.clipScreenPosition([4, 3])).toEqual [3, 50]
-
- describe "when skipSoftWrapIndentation is true", ->
- it "clips positions to the beginning of the line", ->
- expect(displayBuffer.clipScreenPosition([4, 0], skipSoftWrapIndentation: true)).toEqual [4, 4]
- expect(displayBuffer.clipScreenPosition([4, 1], skipSoftWrapIndentation: true)).toEqual [4, 4]
- expect(displayBuffer.clipScreenPosition([4, 3], skipSoftWrapIndentation: true)).toEqual [4, 4]
-
- describe "when wrapAtSoftNewlines is false (the default)", ->
- it "clips positions at the end of soft-wrapped lines to the character preceding the end of the line", ->
- expect(displayBuffer.clipScreenPosition([3, 50])).toEqual [3, 50]
- expect(displayBuffer.clipScreenPosition([3, 51])).toEqual [3, 50]
- expect(displayBuffer.clipScreenPosition([3, 58])).toEqual [3, 50]
- expect(displayBuffer.clipScreenPosition([3, 1000])).toEqual [3, 50]
-
- describe "when wrapAtSoftNewlines is true", ->
- it "wraps positions at the end of soft-wrapped lines to the next screen line", ->
- expect(displayBuffer.clipScreenPosition([3, 50], wrapAtSoftNewlines: true)).toEqual [3, 50]
- expect(displayBuffer.clipScreenPosition([3, 51], wrapAtSoftNewlines: true)).toEqual [4, 4]
- expect(displayBuffer.clipScreenPosition([3, 58], wrapAtSoftNewlines: true)).toEqual [4, 4]
- expect(displayBuffer.clipScreenPosition([3, 1000], wrapAtSoftNewlines: true)).toEqual [4, 4]
-
- describe "when clip is 'closest' (the default)", ->
- it "clips screen positions in the middle of atomic tab characters to the closest edge of the character", ->
- buffer.insert([0, 0], '\t')
- expect(displayBuffer.clipScreenPosition([0, 0])).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, 1])).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, 2])).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, tabLength-1])).toEqual [0, tabLength]
- expect(displayBuffer.clipScreenPosition([0, tabLength])).toEqual [0, tabLength]
-
- describe "when clip is 'backward'", ->
- it "clips screen positions in the middle of atomic tab characters to the beginning of the character", ->
- buffer.insert([0, 0], '\t')
- expect(displayBuffer.clipScreenPosition([0, 0], clip: 'backward')).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, tabLength-1], clip: 'backward')).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, tabLength], clip: 'backward')).toEqual [0, tabLength]
-
- describe "when clip is 'forward'", ->
- it "clips screen positions in the middle of atomic tab characters to the end of the character", ->
- buffer.insert([0, 0], '\t')
- expect(displayBuffer.clipScreenPosition([0, 0], clip: 'forward')).toEqual [0, 0]
- expect(displayBuffer.clipScreenPosition([0, 1], clip: 'forward')).toEqual [0, tabLength]
- expect(displayBuffer.clipScreenPosition([0, tabLength], clip: 'forward')).toEqual [0, tabLength]
-
- describe "::screenPositionForBufferPosition(bufferPosition, options)", ->
- it "clips the specified buffer position", ->
- expect(displayBuffer.screenPositionForBufferPosition([0, 2])).toEqual [0, 2]
- expect(displayBuffer.screenPositionForBufferPosition([0, 100000])).toEqual [0, 29]
- expect(displayBuffer.screenPositionForBufferPosition([100000, 0])).toEqual [12, 2]
- expect(displayBuffer.screenPositionForBufferPosition([100000, 100000])).toEqual [12, 2]
-
- it "clips to the (left or right) edge of an atomic token without simply rounding up", ->
- tabLength = 4
- displayBuffer.setTabLength(tabLength)
-
- buffer.insert([0, 0], '\t')
- expect(displayBuffer.screenPositionForBufferPosition([0, 0])).toEqual [0, 0]
- expect(displayBuffer.screenPositionForBufferPosition([0, 1])).toEqual [0, tabLength]
-
- it "clips to the edge closest to the given position when it's inside a soft tab", ->
- tabLength = 4
- displayBuffer.setTabLength(tabLength)
-
- buffer.insert([0, 0], ' ')
- expect(displayBuffer.screenPositionForBufferPosition([0, 0])).toEqual [0, 0]
- expect(displayBuffer.screenPositionForBufferPosition([0, 1])).toEqual [0, 0]
- expect(displayBuffer.screenPositionForBufferPosition([0, 2])).toEqual [0, 0]
- expect(displayBuffer.screenPositionForBufferPosition([0, 3])).toEqual [0, 4]
- expect(displayBuffer.screenPositionForBufferPosition([0, 4])).toEqual [0, 4]
-
- describe "position translation in the presence of hard tabs", ->
- it "correctly translates positions on either side of a tab", ->
- buffer.setText('\t')
- expect(displayBuffer.screenPositionForBufferPosition([0, 1])).toEqual [0, 2]
- expect(displayBuffer.bufferPositionForScreenPosition([0, 2])).toEqual [0, 1]
-
- it "correctly translates positions on soft wrapped lines containing tabs", ->
- buffer.setText('\t\taa bb cc dd ee ff gg')
- displayBuffer.setSoftWrapped(true)
- displayBuffer.setDefaultCharWidth(1)
- displayBuffer.setEditorWidthInChars(10)
- expect(displayBuffer.screenPositionForBufferPosition([0, 10], wrapAtSoftNewlines: true)).toEqual [1, 4]
- expect(displayBuffer.bufferPositionForScreenPosition([1, 0])).toEqual [0, 9]
-
- describe "::getMaxLineLength()", ->
- it "returns the length of the longest screen line", ->
- expect(displayBuffer.getMaxLineLength()).toBe 65
- buffer.delete([[6, 0], [6, 65]])
- expect(displayBuffer.getMaxLineLength()).toBe 62
-
- it "correctly updates the location of the longest screen line when changes occur", ->
- expect(displayBuffer.getLongestScreenRow()).toBe 6
- buffer.delete([[3, 0], [5, 0]])
- expect(displayBuffer.getLongestScreenRow()).toBe 4
-
- buffer.delete([[4, 0], [5, 0]])
- expect(displayBuffer.getLongestScreenRow()).toBe 5
- expect(displayBuffer.getMaxLineLength()).toBe 56
-
- buffer.delete([[6, 0], [8, 0]])
- expect(displayBuffer.getLongestScreenRow()).toBe 5
- expect(displayBuffer.getMaxLineLength()).toBe 56
-
- describe "::destroy()", ->
- it "unsubscribes all display buffer markers from their underlying buffer marker (regression)", ->
- marker = displayBuffer.markBufferPosition([12, 2])
- displayBuffer.destroy()
- expect( -> buffer.insert([12, 2], '\n')).not.toThrow()
-
- describe "markers", ->
- beforeEach ->
- displayBuffer.createFold(4, 7)
-
- describe "marker creation and manipulation", ->
- it "allows markers to be created in terms of both screen and buffer coordinates", ->
- marker1 = displayBuffer.markScreenRange([[5, 4], [5, 10]])
- marker2 = displayBuffer.markBufferRange([[8, 4], [8, 10]])
- expect(marker1.getBufferRange()).toEqual [[8, 4], [8, 10]]
- expect(marker2.getScreenRange()).toEqual [[5, 4], [5, 10]]
-
- it "emits a 'marker-created' event on the DisplayBuffer whenever a marker is created", ->
- displayBuffer.onDidCreateMarker markerCreatedHandler = jasmine.createSpy("markerCreatedHandler")
-
- marker1 = displayBuffer.markScreenRange([[5, 4], [5, 10]])
- expect(markerCreatedHandler).toHaveBeenCalledWith(marker1)
- markerCreatedHandler.reset()
-
- marker2 = buffer.markRange([[5, 4], [5, 10]])
- expect(markerCreatedHandler).toHaveBeenCalledWith(displayBuffer.getMarker(marker2.id))
-
- it "allows marker head and tail positions to be manipulated in both screen and buffer coordinates", ->
- marker = displayBuffer.markScreenRange([[5, 4], [5, 10]])
- marker.setHeadScreenPosition([5, 4])
- marker.setTailBufferPosition([5, 4])
- expect(marker.isReversed()).toBeFalsy()
- expect(marker.getBufferRange()).toEqual [[5, 4], [8, 4]]
- marker.setHeadBufferPosition([5, 4])
- marker.setTailScreenPosition([5, 4])
- expect(marker.isReversed()).toBeTruthy()
- expect(marker.getBufferRange()).toEqual [[5, 4], [8, 4]]
-
- it "returns whether a position changed when it is assigned", ->
- marker = displayBuffer.markScreenRange([[0, 0], [0, 0]])
- expect(marker.setHeadScreenPosition([5, 4])).toBeTruthy()
- expect(marker.setHeadScreenPosition([5, 4])).toBeFalsy()
- expect(marker.setHeadBufferPosition([1, 0])).toBeTruthy()
- expect(marker.setHeadBufferPosition([1, 0])).toBeFalsy()
- expect(marker.setTailScreenPosition([5, 4])).toBeTruthy()
- expect(marker.setTailScreenPosition([5, 4])).toBeFalsy()
- expect(marker.setTailBufferPosition([1, 0])).toBeTruthy()
- expect(marker.setTailBufferPosition([1, 0])).toBeFalsy()
-
- describe "marker change events", ->
- [markerChangedHandler, marker] = []
-
- beforeEach ->
- marker = displayBuffer.addMarkerLayer(maintainHistory: true).markScreenRange([[5, 4], [5, 10]])
- marker.onDidChange markerChangedHandler = jasmine.createSpy("markerChangedHandler")
-
- it "triggers the 'changed' event whenever the markers head's screen position changes in the buffer or on screen", ->
- marker.setHeadScreenPosition([8, 20])
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [5, 10]
- oldHeadBufferPosition: [8, 10]
- newHeadScreenPosition: [8, 20]
- newHeadBufferPosition: [11, 20]
- oldTailScreenPosition: [5, 4]
- oldTailBufferPosition: [8, 4]
- newTailScreenPosition: [5, 4]
- newTailBufferPosition: [8, 4]
- textChanged: false
- isValid: true
- }
- markerChangedHandler.reset()
-
- buffer.insert([11, 0], '...')
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [8, 20]
- oldHeadBufferPosition: [11, 20]
- newHeadScreenPosition: [8, 23]
- newHeadBufferPosition: [11, 23]
- oldTailScreenPosition: [5, 4]
- oldTailBufferPosition: [8, 4]
- newTailScreenPosition: [5, 4]
- newTailBufferPosition: [8, 4]
- textChanged: true
- isValid: true
- }
- markerChangedHandler.reset()
-
- displayBuffer.unfoldBufferRow(4)
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [8, 23]
- oldHeadBufferPosition: [11, 23]
- newHeadScreenPosition: [11, 23]
- newHeadBufferPosition: [11, 23]
- oldTailScreenPosition: [5, 4]
- oldTailBufferPosition: [8, 4]
- newTailScreenPosition: [8, 4]
- newTailBufferPosition: [8, 4]
- textChanged: false
- isValid: true
- }
- markerChangedHandler.reset()
-
- displayBuffer.createFold(4, 7)
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [11, 23]
- oldHeadBufferPosition: [11, 23]
- newHeadScreenPosition: [8, 23]
- newHeadBufferPosition: [11, 23]
- oldTailScreenPosition: [8, 4]
- oldTailBufferPosition: [8, 4]
- newTailScreenPosition: [5, 4]
- newTailBufferPosition: [8, 4]
- textChanged: false
- isValid: true
- }
-
- it "triggers the 'changed' event whenever the marker tail's position changes in the buffer or on screen", ->
- marker.setTailScreenPosition([8, 20])
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [5, 10]
- oldHeadBufferPosition: [8, 10]
- newHeadScreenPosition: [5, 10]
- newHeadBufferPosition: [8, 10]
- oldTailScreenPosition: [5, 4]
- oldTailBufferPosition: [8, 4]
- newTailScreenPosition: [8, 20]
- newTailBufferPosition: [11, 20]
- textChanged: false
- isValid: true
- }
- markerChangedHandler.reset()
-
- buffer.insert([11, 0], '...')
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [5, 10]
- oldHeadBufferPosition: [8, 10]
- newHeadScreenPosition: [5, 10]
- newHeadBufferPosition: [8, 10]
- oldTailScreenPosition: [8, 20]
- oldTailBufferPosition: [11, 20]
- newTailScreenPosition: [8, 23]
- newTailBufferPosition: [11, 23]
- textChanged: true
- isValid: true
- }
-
- it "triggers the 'changed' event whenever the marker is invalidated or revalidated", ->
- buffer.deleteRow(8)
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [5, 10]
- oldHeadBufferPosition: [8, 10]
- newHeadScreenPosition: [5, 0]
- newHeadBufferPosition: [8, 0]
- oldTailScreenPosition: [5, 4]
- oldTailBufferPosition: [8, 4]
- newTailScreenPosition: [5, 0]
- newTailBufferPosition: [8, 0]
- textChanged: true
- isValid: false
- }
-
- markerChangedHandler.reset()
- buffer.undo()
-
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(markerChangedHandler.argsForCall[0][0]).toEqual {
- oldHeadScreenPosition: [5, 0]
- oldHeadBufferPosition: [8, 0]
- newHeadScreenPosition: [5, 10]
- newHeadBufferPosition: [8, 10]
- oldTailScreenPosition: [5, 0]
- oldTailBufferPosition: [8, 0]
- newTailScreenPosition: [5, 4]
- newTailBufferPosition: [8, 4]
- textChanged: true
- isValid: true
- }
-
- it "does not call the callback for screen changes that don't change the position of the marker", ->
- displayBuffer.createFold(10, 11)
- expect(markerChangedHandler).not.toHaveBeenCalled()
-
- it "updates markers before emitting buffer change events, but does not notify their observers until the change event", ->
- marker2 = displayBuffer.addMarkerLayer(maintainHistory: true).markBufferRange([[8, 1], [8, 1]])
- marker2.onDidChange marker2ChangedHandler = jasmine.createSpy("marker2ChangedHandler")
- displayBuffer.onDidChange changeHandler = jasmine.createSpy("changeHandler").andCallFake -> onDisplayBufferChange()
-
- # New change ----
-
- onDisplayBufferChange = ->
- # calls change handler first
- expect(markerChangedHandler).not.toHaveBeenCalled()
- expect(marker2ChangedHandler).not.toHaveBeenCalled()
- # but still updates the markers
- expect(marker.getScreenRange()).toEqual [[5, 7], [5, 13]]
- expect(marker.getHeadScreenPosition()).toEqual [5, 13]
- expect(marker.getTailScreenPosition()).toEqual [5, 7]
- expect(marker2.isValid()).toBeFalsy()
-
- buffer.setTextInRange([[8, 0], [8, 2]], ".....")
- expect(changeHandler).toHaveBeenCalled()
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(marker2ChangedHandler).toHaveBeenCalled()
-
- # Undo change ----
-
- changeHandler.reset()
- markerChangedHandler.reset()
- marker2ChangedHandler.reset()
-
- marker3 = displayBuffer.markBufferRange([[8, 1], [8, 2]])
- marker3.onDidChange marker3ChangedHandler = jasmine.createSpy("marker3ChangedHandler")
-
- onDisplayBufferChange = ->
- # calls change handler first
- expect(markerChangedHandler).not.toHaveBeenCalled()
- expect(marker2ChangedHandler).not.toHaveBeenCalled()
- expect(marker3ChangedHandler).not.toHaveBeenCalled()
-
- # markers positions are updated based on the text change
- expect(marker.getScreenRange()).toEqual [[5, 4], [5, 10]]
- expect(marker.getHeadScreenPosition()).toEqual [5, 10]
- expect(marker.getTailScreenPosition()).toEqual [5, 4]
-
- buffer.undo()
- expect(changeHandler).toHaveBeenCalled()
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(marker2ChangedHandler).toHaveBeenCalled()
- expect(marker3ChangedHandler).toHaveBeenCalled()
- expect(marker2.isValid()).toBeTruthy()
- expect(marker3.isValid()).toBeFalsy()
-
- # Redo change ----
-
- changeHandler.reset()
- markerChangedHandler.reset()
- marker2ChangedHandler.reset()
- marker3ChangedHandler.reset()
-
- onDisplayBufferChange = ->
- # calls change handler first
- expect(markerChangedHandler).not.toHaveBeenCalled()
- expect(marker2ChangedHandler).not.toHaveBeenCalled()
- expect(marker3ChangedHandler).not.toHaveBeenCalled()
-
- # markers positions are updated based on the text change
- expect(marker.getScreenRange()).toEqual [[5, 7], [5, 13]]
- expect(marker.getHeadScreenPosition()).toEqual [5, 13]
- expect(marker.getTailScreenPosition()).toEqual [5, 7]
-
- # but marker snapshots are not restored until the end of the undo.
- expect(marker2.isValid()).toBeFalsy()
- expect(marker3.isValid()).toBeFalsy()
-
- buffer.redo()
- expect(changeHandler).toHaveBeenCalled()
- expect(markerChangedHandler).toHaveBeenCalled()
- expect(marker2ChangedHandler).toHaveBeenCalled()
- expect(marker3ChangedHandler).toHaveBeenCalled()
-
- it "updates the position of markers before emitting change events that aren't caused by a buffer change", ->
- displayBuffer.onDidChange changeHandler = jasmine.createSpy("changeHandler").andCallFake ->
- # calls change handler first
- expect(markerChangedHandler).not.toHaveBeenCalled()
- # but still updates the markers
- expect(marker.getScreenRange()).toEqual [[8, 4], [8, 10]]
- expect(marker.getHeadScreenPosition()).toEqual [8, 10]
- expect(marker.getTailScreenPosition()).toEqual [8, 4]
-
- displayBuffer.unfoldBufferRow(4)
-
- expect(changeHandler).toHaveBeenCalled()
- expect(markerChangedHandler).toHaveBeenCalled()
-
- it "emits the correct events when markers are mutated inside event listeners", ->
- marker.onDidChange ->
- if marker.getHeadScreenPosition().isEqual([5, 9])
- marker.setHeadScreenPosition([5, 8])
-
- marker.setHeadScreenPosition([5, 9])
-
- headChanges = for [event] in markerChangedHandler.argsForCall
- {old: event.oldHeadScreenPosition, new: event.newHeadScreenPosition}
-
- expect(headChanges).toEqual [
- {old: [5, 10], new: [5, 9]}
- {old: [5, 9], new: [5, 8]}
- ]
-
- describe "::findMarkers(attributes)", ->
- it "allows the startBufferRow and endBufferRow to be specified", ->
- marker1 = displayBuffer.markBufferRange([[0, 0], [3, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[0, 0], [5, 0]], class: 'a')
- marker3 = displayBuffer.markBufferRange([[9, 0], [10, 0]], class: 'b')
-
- expect(displayBuffer.findMarkers(class: 'a', startBufferRow: 0)).toEqual [marker2, marker1]
- expect(displayBuffer.findMarkers(class: 'a', startBufferRow: 0, endBufferRow: 3)).toEqual [marker1]
- expect(displayBuffer.findMarkers(endBufferRow: 10)).toEqual [marker3]
-
- it "allows the startScreenRow and endScreenRow to be specified", ->
- marker1 = displayBuffer.markBufferRange([[6, 0], [7, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[9, 0], [10, 0]], class: 'a')
- displayBuffer.createFold(4, 7)
- expect(displayBuffer.findMarkers(class: 'a', startScreenRow: 6, endScreenRow: 7)).toEqual [marker2]
-
- it "allows intersectsBufferRowRange to be specified", ->
- marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a')
- displayBuffer.createFold(4, 7)
- expect(displayBuffer.findMarkers(class: 'a', intersectsBufferRowRange: [5, 6])).toEqual [marker1]
-
- it "allows intersectsScreenRowRange to be specified", ->
- marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a')
- displayBuffer.createFold(4, 7)
- expect(displayBuffer.findMarkers(class: 'a', intersectsScreenRowRange: [5, 10])).toEqual [marker2]
-
- it "allows containedInScreenRange to be specified", ->
- marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a')
- displayBuffer.createFold(4, 7)
- expect(displayBuffer.findMarkers(class: 'a', containedInScreenRange: [[5, 0], [7, 0]])).toEqual [marker2]
-
- it "allows intersectsBufferRange to be specified", ->
- marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a')
- displayBuffer.createFold(4, 7)
- expect(displayBuffer.findMarkers(class: 'a', intersectsBufferRange: [[5, 0], [6, 0]])).toEqual [marker1]
-
- it "allows intersectsScreenRange to be specified", ->
- marker1 = displayBuffer.markBufferRange([[5, 0], [5, 0]], class: 'a')
- marker2 = displayBuffer.markBufferRange([[8, 0], [8, 0]], class: 'a')
- displayBuffer.createFold(4, 7)
- expect(displayBuffer.findMarkers(class: 'a', intersectsScreenRange: [[5, 0], [10, 0]])).toEqual [marker2]
-
- describe "marker destruction", ->
- it "allows markers to be destroyed", ->
- marker = displayBuffer.markScreenRange([[5, 4], [5, 10]])
- marker.destroy()
- expect(marker.isValid()).toBeFalsy()
- expect(displayBuffer.getMarker(marker.id)).toBeUndefined()
-
- it "notifies ::onDidDestroy observers when markers are destroyed", ->
- destroyedHandler = jasmine.createSpy("destroyedHandler")
- marker = displayBuffer.markScreenRange([[5, 4], [5, 10]])
- marker.onDidDestroy destroyedHandler
- marker.destroy()
- expect(destroyedHandler).toHaveBeenCalled()
- destroyedHandler.reset()
-
- marker2 = displayBuffer.markScreenRange([[5, 4], [5, 10]])
- marker2.onDidDestroy destroyedHandler
- buffer.getMarker(marker2.id).destroy()
- expect(destroyedHandler).toHaveBeenCalled()
-
- describe "Marker::copy(attributes)", ->
- it "creates a copy of the marker with the given attributes merged in", ->
- initialMarkerCount = displayBuffer.getMarkerCount()
- marker1 = displayBuffer.markScreenRange([[5, 4], [5, 10]], a: 1, b: 2)
- expect(displayBuffer.getMarkerCount()).toBe initialMarkerCount + 1
-
- marker2 = marker1.copy(b: 3)
- expect(marker2.getBufferRange()).toEqual marker1.getBufferRange()
- expect(displayBuffer.getMarkerCount()).toBe initialMarkerCount + 2
- expect(marker1.getProperties()).toEqual a: 1, b: 2
- expect(marker2.getProperties()).toEqual a: 1, b: 3
-
- describe 'when there are multiple DisplayBuffers for a buffer', ->
- describe 'when a marker is created', ->
- it 'the second display buffer will not emit a marker-created event when the marker has been deleted in the first marker-created event', ->
- displayBuffer2 = new DisplayBuffer({
- buffer, tabLength, config: atom.config, grammarRegistry: atom.grammars,
- packageManager: atom.packages, assert: ->
- })
- displayBuffer.onDidCreateMarker markerCreated1 = jasmine.createSpy().andCallFake (marker) -> marker.destroy()
- displayBuffer2.onDidCreateMarker markerCreated2 = jasmine.createSpy()
-
- displayBuffer.markBufferRange([[0, 0], [1, 5]], {})
-
- expect(markerCreated1).toHaveBeenCalled()
- expect(markerCreated2).not.toHaveBeenCalled()
-
- describe "decorations", ->
- [marker, decoration, decorationProperties] = []
- beforeEach ->
- marker = displayBuffer.markBufferRange([[2, 13], [3, 15]])
- decorationProperties = {type: 'line-number', class: 'one'}
- decoration = displayBuffer.decorateMarker(marker, decorationProperties)
-
- it "can add decorations associated with markers and remove them", ->
- expect(decoration).toBeDefined()
- expect(decoration.getProperties()).toBe decorationProperties
- expect(displayBuffer.decorationForId(decoration.id)).toBe decoration
- expect(displayBuffer.decorationsForScreenRowRange(2, 3)[marker.id][0]).toBe decoration
-
- decoration.destroy()
- expect(displayBuffer.decorationsForScreenRowRange(2, 3)[marker.id]).not.toBeDefined()
- expect(displayBuffer.decorationForId(decoration.id)).not.toBeDefined()
-
- it "will not fail if the decoration is removed twice", ->
- decoration.destroy()
- decoration.destroy()
- expect(displayBuffer.decorationForId(decoration.id)).not.toBeDefined()
-
- it "does not allow destroyed markers to be decorated", ->
- marker.destroy()
- expect(->
- displayBuffer.decorateMarker(marker, {type: 'overlay', item: document.createElement('div')})
- ).toThrow("Cannot decorate a destroyed marker")
- expect(displayBuffer.getOverlayDecorations()).toEqual []
-
- describe "when a decoration is updated via Decoration::update()", ->
- it "emits an 'updated' event containing the new and old params", ->
- decoration.onDidChangeProperties updatedSpy = jasmine.createSpy()
- decoration.setProperties type: 'line-number', class: 'two'
-
- {oldProperties, newProperties} = updatedSpy.mostRecentCall.args[0]
- expect(oldProperties).toEqual decorationProperties
- expect(newProperties).toEqual {type: 'line-number', gutterName: 'line-number', class: 'two'}
-
- describe "::getDecorations(properties)", ->
- it "returns decorations matching the given optional properties", ->
- expect(displayBuffer.getDecorations()).toEqual [decoration]
- expect(displayBuffer.getDecorations(class: 'two').length).toEqual 0
- expect(displayBuffer.getDecorations(class: 'one').length).toEqual 1
-
- describe "::scrollToScreenPosition(position, [options])", ->
- it "triggers ::onDidRequestAutoscroll with the logical coordinates along with the options", ->
- scrollSpy = jasmine.createSpy("::onDidRequestAutoscroll")
- displayBuffer.onDidRequestAutoscroll(scrollSpy)
-
- displayBuffer.scrollToScreenPosition([8, 20])
- displayBuffer.scrollToScreenPosition([8, 20], center: true)
- displayBuffer.scrollToScreenPosition([8, 20], center: false, reversed: true)
-
- expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {})
- expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: true})
- expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true})
-
- describe "::decorateMarker", ->
- describe "when decorating gutters", ->
- [marker] = []
-
- beforeEach ->
- marker = displayBuffer.markBufferRange([[1, 0], [1, 0]])
-
- it "creates a decoration that is both of 'line-number' and 'gutter' type when called with the 'line-number' type", ->
- decorationProperties = {type: 'line-number', class: 'one'}
- decoration = displayBuffer.decorateMarker(marker, decorationProperties)
- expect(decoration.isType('line-number')).toBe true
- expect(decoration.isType('gutter')).toBe true
- expect(decoration.getProperties().gutterName).toBe 'line-number'
- expect(decoration.getProperties().class).toBe 'one'
-
- it "creates a decoration that is only of 'gutter' type if called with the 'gutter' type and a 'gutterName'", ->
- decorationProperties = {type: 'gutter', gutterName: 'test-gutter', class: 'one'}
- decoration = displayBuffer.decorateMarker(marker, decorationProperties)
- expect(decoration.isType('gutter')).toBe true
- expect(decoration.isType('line-number')).toBe false
- expect(decoration.getProperties().gutterName).toBe 'test-gutter'
- expect(decoration.getProperties().class).toBe 'one'
diff --git a/spec/environment-helpers-spec.js b/spec/environment-helpers-spec.js
new file mode 100644
index 000000000..20ec15d9f
--- /dev/null
+++ b/spec/environment-helpers-spec.js
@@ -0,0 +1,160 @@
+'use babel'
+/* eslint-env jasmine */
+
+import child_process from 'child_process'
+import environmentHelpers from '../src/environment-helpers'
+import os from 'os'
+
+describe('Environment handling', () => {
+ let originalEnv
+ let options
+
+ beforeEach(() => {
+ originalEnv = process.env
+ delete process._originalEnv
+ options = {
+ platform: process.platform,
+ env: Object.assign({}, process.env)
+ }
+ })
+
+ afterEach(() => {
+ process.env = originalEnv
+ delete process._originalEnv
+ })
+
+ describe('on OSX, when PWD is not set', () => {
+ beforeEach(() => {
+ options.platform = 'darwin'
+ })
+
+ describe('needsPatching', () => {
+ it('returns true if PWD is unset', () => {
+ delete options.env.PWD
+ expect(environmentHelpers.needsPatching(options)).toBe(true)
+ options.env.PWD = undefined
+ expect(environmentHelpers.needsPatching(options)).toBe(true)
+ options.env.PWD = null
+ expect(environmentHelpers.needsPatching(options)).toBe(true)
+ options.env.PWD = false
+ expect(environmentHelpers.needsPatching(options)).toBe(true)
+ })
+
+ it('returns false if PWD is set', () => {
+ options.env.PWD = 'xterm'
+ expect(environmentHelpers.needsPatching(options)).toBe(false)
+ })
+ })
+
+ describe('normalize', () => {
+ it('changes process.env if PWD is unset', () => {
+ if (process.platform === 'win32') {
+ return
+ }
+ delete options.env.PWD
+ environmentHelpers.normalize(options)
+ expect(process._originalEnv).toBeDefined()
+ expect(process._originalEnv).toBeTruthy()
+ expect(process.env).toBeDefined()
+ expect(process.env).toBeTruthy()
+ expect(process.env.PWD).toBeDefined()
+ expect(process.env.PWD).toBeTruthy()
+ expect(process.env.PATH).toBeDefined()
+ expect(process.env.PATH).toBeTruthy()
+ expect(process.env.ATOM_HOME).toBeDefined()
+ expect(process.env.ATOM_HOME).toBeTruthy()
+ })
+ })
+ })
+
+ describe('on a platform other than OSX', () => {
+ beforeEach(() => {
+ options.platform = 'penguin'
+ })
+
+ describe('needsPatching', () => {
+ it('returns false if PWD is set or unset', () => {
+ delete options.env.PWD
+ expect(environmentHelpers.needsPatching(options)).toBe(false)
+ options.env.PWD = undefined
+ expect(environmentHelpers.needsPatching(options)).toBe(false)
+ options.env.PWD = null
+ expect(environmentHelpers.needsPatching(options)).toBe(false)
+ options.env.PWD = false
+ expect(environmentHelpers.needsPatching(options)).toBe(false)
+ options.env.PWD = '/'
+ expect(environmentHelpers.needsPatching(options)).toBe(false)
+ })
+
+ it('returns false for linux', () => {
+ options.platform = 'linux'
+ options.PWD = '/'
+ expect(environmentHelpers.needsPatching(options)).toBe(false)
+ })
+
+ it('returns false for windows', () => {
+ options.platform = 'win32'
+ options.PWD = 'c:\\'
+ expect(environmentHelpers.needsPatching(options)).toBe(false)
+ })
+ })
+
+ describe('normalize', () => {
+ it('does not change the environment', () => {
+ if (process.platform === 'win32') {
+ return
+ }
+ delete options.env.PWD
+ environmentHelpers.normalize(options)
+ expect(process._originalEnv).toBeUndefined()
+ expect(process.env).toBeDefined()
+ expect(process.env).toBeTruthy()
+ expect(process.env.PATH).toBeDefined()
+ expect(process.env.PATH).toBeTruthy()
+ expect(process.env.PWD).toBeUndefined()
+ expect(process.env.PATH).toBe(originalEnv.PATH)
+ expect(process.env.ATOM_HOME).toBeDefined()
+ expect(process.env.ATOM_HOME).toBeTruthy()
+ })
+ })
+ })
+
+ describe('getFromShell', () => {
+ describe('when things are configured properly', () => {
+ beforeEach(() => {
+ spyOn(child_process, 'spawnSync').andReturn({
+ stdout: 'FOO=BAR' + os.EOL + 'TERM=xterm-something' + os.EOL +
+ 'PATH=/usr/bin:/bin:/usr/sbin:/sbin:/crazy/path'
+ })
+ })
+
+ it('returns an object containing the information from the user\'s shell environment', () => {
+ let env = environmentHelpers.getFromShell()
+ expect(env.FOO).toEqual('BAR')
+ expect(env.TERM).toEqual('xterm-something')
+ expect(env.PATH).toEqual('/usr/bin:/bin:/usr/sbin:/sbin:/crazy/path')
+ })
+ })
+
+ describe('when an error occurs launching the shell', () => {
+ beforeEach(() => {
+ spyOn(child_process, 'spawnSync').andReturn({
+ error: new Error('testing when an error occurs')
+ })
+ })
+
+ it('returns undefined', () => {
+ expect(environmentHelpers.getFromShell()).toBeUndefined()
+ })
+
+ it('leaves the environment as-is when normalize() is called', () => {
+ options.platform = 'darwin'
+ delete options.env.PWD
+ expect(environmentHelpers.needsPatching(options)).toBe(true)
+ environmentHelpers.normalize(options)
+ expect(process.env).toBeDefined()
+ expect(process._originalEnv).toBeUndefined()
+ })
+ })
+ })
+})
diff --git a/spec/fake-lines-yardstick.coffee b/spec/fake-lines-yardstick.coffee
index 38716ab3e..c3396ff9f 100644
--- a/spec/fake-lines-yardstick.coffee
+++ b/spec/fake-lines-yardstick.coffee
@@ -1,8 +1,10 @@
{Point} = require 'text-buffer'
+{isPairedCharacter} = require '../src/text-utils'
module.exports =
class FakeLinesYardstick
constructor: (@model, @lineTopIndex) ->
+ {@displayLayer} = @model
@characterWidthsByScope = {}
getScopedCharacterWidth: (scopeNames, char) ->
@@ -24,31 +26,38 @@ class FakeLinesYardstick
targetRow = screenPosition.row
targetColumn = screenPosition.column
- baseCharacterWidth = @model.getDefaultCharWidth()
top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow)
left = 0
column = 0
- iterator = @model.tokenizedLineForScreenRow(targetRow).getTokenIterator()
- while iterator.next()
- characterWidths = @getScopedCharacterWidths(iterator.getScopes())
+ scopes = []
+ startIndex = 0
+ {tagCodes, lineText} = @model.screenLineForScreenRow(targetRow)
+ for tagCode in tagCodes
+ if @displayLayer.isOpenTagCode(tagCode)
+ scopes.push(@displayLayer.tagForCode(tagCode))
+ else if @displayLayer.isCloseTagCode(tagCode)
+ scopes.splice(scopes.lastIndexOf(@displayLayer.tagForCode(tagCode)), 1)
+ else
+ text = lineText.substr(startIndex, tagCode)
+ startIndex += tagCode
+ characterWidths = @getScopedCharacterWidths(scopes)
- valueIndex = 0
- text = iterator.getText()
- while valueIndex < text.length
- if iterator.isPairedCharacter()
- char = text
- charLength = 2
- valueIndex += 2
- else
- char = text[valueIndex]
- charLength = 1
- valueIndex++
+ valueIndex = 0
+ while valueIndex < text.length
+ if isPairedCharacter(text, valueIndex)
+ char = text[valueIndex...valueIndex + 2]
+ charLength = 2
+ valueIndex += 2
+ else
+ char = text[valueIndex]
+ charLength = 1
+ valueIndex++
- break if column is targetColumn
+ break if column is targetColumn
- left += characterWidths[char] ? baseCharacterWidth unless char is '\0'
- column += charLength
+ left += characterWidths[char] ? @model.getDefaultCharWidth() unless char is '\0'
+ column += charLength
{top, left}
diff --git a/spec/fixtures/git/repo-with-submodules/git.git/config b/spec/fixtures/git/repo-with-submodules/git.git/config
index ab57cc5f1..323ba7d9b 100644
--- a/spec/fixtures/git/repo-with-submodules/git.git/config
+++ b/spec/fixtures/git/repo-with-submodules/git.git/config
@@ -5,6 +5,12 @@
logallrefupdates = true
ignorecase = true
precomposeunicode = true
+[branch "master"]
+ remote = origin
+ merge = refs/heads/master
+[remote "origin"]
+ url = git@github.com:atom/some-repo-i-guess.git
+ fetch = +refs/heads/*:refs/remotes/origin/*
[submodule "jstips"]
url = https://github.com/loverajoel/jstips
[submodule "You-Dont-Need-jQuery"]
diff --git a/spec/fixtures/git/repo-with-submodules/git.git/refs/remotes/origin/master b/spec/fixtures/git/repo-with-submodules/git.git/refs/remotes/origin/master
new file mode 100644
index 000000000..3507a23dc
--- /dev/null
+++ b/spec/fixtures/git/repo-with-submodules/git.git/refs/remotes/origin/master
@@ -0,0 +1 @@
+d2b0ad9cbc6f6c4372e8956e5cc5af771b2342e5
diff --git a/spec/fixtures/lorem.txt b/spec/fixtures/lorem.txt
new file mode 100644
index 000000000..be8db8ab8
--- /dev/null
+++ b/spec/fixtures/lorem.txt
@@ -0,0 +1,3 @@
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur ultricies nulla id nibh aliquam, vitae euismod ipsum scelerisque. Vestibulum vulputate facilisis nisi, eu rhoncus turpis pretium ut. Curabitur facilisis urna in diam efficitur, vel maximus tellus consectetur. Suspendisse pulvinar felis sed metus tristique, a posuere dui suscipit. Ut vehicula, tellus ac blandit consequat, libero dui hendrerit elit, non pretium metus odio sed dolor. Vivamus quis volutpat ipsum. In convallis magna nec nunc tristique malesuada. Sed sed hendrerit lacus. Etiam arcu dui, consequat vel neque vitae, iaculis egestas justo. Donec lacinia odio nulla, condimentum porta erat accumsan at. Nunc vulputate nulla vel nunc fermentum egestas.
+Duis ultricies libero elit, nec facilisis mi rhoncus ornare. Aliquam aliquet libero vitae arcu porttitor mattis. Vestibulum ultricies consectetur arcu, non gravida magna eleifend vel. Phasellus varius mattis ultricies. Vestibulum placerat lacus non consectetur fringilla. Duis congue, arcu iaculis vehicula hendrerit, purus odio faucibus ipsum, et fermentum massa tellus euismod nulla. Vivamus pellentesque blandit massa, sit amet hendrerit turpis congue eu. Suspendisse diam dui, vestibulum nec semper varius, maximus eu nunc. Vivamus facilisis pulvinar viverra. Praesent luctus lectus id est porttitor volutpat. Suspendisse est augue, mattis a tincidunt id, condimentum in turpis. Curabitur at erat commodo orci interdum tincidunt. Sed sodales elit odio, a placerat ipsum luctus nec. Sed maximus, justo ut pharetra pellentesque, orci mi faucibus enim, quis viverra arcu dui sed nisl. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent quis velit libero.
+Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus a rutrum tortor. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce bibendum odio et neque vestibulum rutrum. Vestibulum commodo, nibh non sodales lobortis, dui ex consectetur leo, a finibus libero lectus ac diam. Etiam dui nunc, bibendum a tempor vel, vestibulum lacinia neque. Mauris consectetur odio sit amet maximus pretium. Sed rutrum nunc at ante ullamcorper fermentum. Proin at quam a mauris pellentesque viverra. Nunc pretium pulvinar ipsum. Vestibulum eu nibh ut ex gravida tempus. Praesent ut elit ut ligula tristique dapibus ut sit amet leo. Proin non molestie erat.
diff --git a/spec/fixtures/packages/package-with-deserializers/deserializer-1.js b/spec/fixtures/packages/package-with-deserializers/deserializer-1.js
deleted file mode 100644
index f4d7a1488..000000000
--- a/spec/fixtures/packages/package-with-deserializers/deserializer-1.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = function (state) {
- return {
- wasDeserializedBy: 'Deserializer1',
- state: state
- }
-}
diff --git a/spec/fixtures/packages/package-with-deserializers/deserializer-2.js b/spec/fixtures/packages/package-with-deserializers/deserializer-2.js
deleted file mode 100644
index 3099d2b15..000000000
--- a/spec/fixtures/packages/package-with-deserializers/deserializer-2.js
+++ /dev/null
@@ -1,6 +0,0 @@
-module.exports = function (state) {
- return {
- wasDeserializedBy: 'Deserializer2',
- state: state
- }
-}
diff --git a/spec/fixtures/packages/package-with-deserializers/index.js b/spec/fixtures/packages/package-with-deserializers/index.js
index 19bba5ecb..b9be23854 100644
--- a/spec/fixtures/packages/package-with-deserializers/index.js
+++ b/spec/fixtures/packages/package-with-deserializers/index.js
@@ -1,3 +1,17 @@
module.exports = {
- activate: function() {}
+ activate () {},
+
+ deserializeMethod1 (state) {
+ return {
+ wasDeserializedBy: 'deserializeMethod1',
+ state: state
+ }
+ },
+
+ deserializeMethod2 (state) {
+ return {
+ wasDeserializedBy: 'deserializeMethod2',
+ state: state
+ }
+ }
}
diff --git a/spec/fixtures/packages/package-with-deserializers/package.json b/spec/fixtures/packages/package-with-deserializers/package.json
index daa5776bf..bae0776a6 100644
--- a/spec/fixtures/packages/package-with-deserializers/package.json
+++ b/spec/fixtures/packages/package-with-deserializers/package.json
@@ -3,7 +3,7 @@
"version": "1.0.0",
"main": "./index",
"deserializers": {
- "Deserializer1": "./deserializer-1.js",
- "Deserializer2": "./deserializer-2.js"
+ "Deserializer1": "deserializeMethod1",
+ "Deserializer2": "deserializeMethod2"
}
}
diff --git a/spec/fixtures/packages/package-with-prefixed-and-suffixed-repo-url/package.json b/spec/fixtures/packages/package-with-prefixed-and-suffixed-repo-url/package.json
new file mode 100644
index 000000000..ce57f7501
--- /dev/null
+++ b/spec/fixtures/packages/package-with-prefixed-and-suffixed-repo-url/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "package-with-a-git-prefixed-git-repo-url",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/example/repo.git"
+ },
+ "_id": "this is here to simulate the URL being already normalized by npm. we still need to stript git+ from the beginning and .git from the end."
+}
diff --git a/spec/fixtures/packages/package-with-view-providers/deserializer.js b/spec/fixtures/packages/package-with-view-providers/deserializer.js
deleted file mode 100644
index 334e7b2ab..000000000
--- a/spec/fixtures/packages/package-with-view-providers/deserializer.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = function (state) {
- return {state: state}
-}
diff --git a/spec/fixtures/packages/package-with-view-providers/index.js b/spec/fixtures/packages/package-with-view-providers/index.js
index 19bba5ecb..66e62171d 100644
--- a/spec/fixtures/packages/package-with-view-providers/index.js
+++ b/spec/fixtures/packages/package-with-view-providers/index.js
@@ -1,3 +1,25 @@
+'use strict'
+
module.exports = {
- activate: function() {}
+ activate () {},
+
+ theDeserializerMethod (state) {
+ return {state: state}
+ },
+
+ viewProviderMethod1 (model) {
+ if (model.worksWithViewProvider1) {
+ let element = document.createElement('div')
+ element.dataset['createdBy'] = 'view-provider-1'
+ return element
+ }
+ },
+
+ viewProviderMethod2 (model) {
+ if (model.worksWithViewProvider2) {
+ let element = document.createElement('div')
+ element.dataset['createdBy'] = 'view-provider-2'
+ return element
+ }
+ }
}
diff --git a/spec/fixtures/packages/package-with-view-providers/package.json b/spec/fixtures/packages/package-with-view-providers/package.json
index f67477280..eb5c80025 100644
--- a/spec/fixtures/packages/package-with-view-providers/package.json
+++ b/spec/fixtures/packages/package-with-view-providers/package.json
@@ -3,10 +3,10 @@
"main": "./index",
"version": "1.0.0",
"deserializers": {
- "DeserializerFromPackageWithViewProviders": "./deserializer"
+ "DeserializerFromPackageWithViewProviders": "theDeserializerMethod"
},
"viewProviders": [
- "./view-provider-1",
- "./view-provider-2"
+ "viewProviderMethod1",
+ "viewProviderMethod2"
]
}
diff --git a/spec/fixtures/packages/package-with-view-providers/view-provider-1.js b/spec/fixtures/packages/package-with-view-providers/view-provider-1.js
deleted file mode 100644
index e4f0dcc0b..000000000
--- a/spec/fixtures/packages/package-with-view-providers/view-provider-1.js
+++ /dev/null
@@ -1,9 +0,0 @@
-'use strict'
-
-module.exports = function (model) {
- if (model.worksWithViewProvider1) {
- let element = document.createElement('div')
- element.dataset['createdBy'] = 'view-provider-1'
- return element
- }
-}
diff --git a/spec/fixtures/packages/package-with-view-providers/view-provider-2.js b/spec/fixtures/packages/package-with-view-providers/view-provider-2.js
deleted file mode 100644
index a3b58a3aa..000000000
--- a/spec/fixtures/packages/package-with-view-providers/view-provider-2.js
+++ /dev/null
@@ -1,9 +0,0 @@
-'use strict'
-
-module.exports = function (model) {
- if (model.worksWithViewProvider2) {
- let element = document.createElement('div')
- element.dataset['createdBy'] = 'view-provider-2'
- return element
- }
-}
diff --git a/spec/fixtures/sample-with-comments.js b/spec/fixtures/sample-with-comments.js
index c10d42232..b40ddc890 100644
--- a/spec/fixtures/sample-with-comments.js
+++ b/spec/fixtures/sample-with-comments.js
@@ -9,12 +9,23 @@ var quicksort = function () {
// Wowza
if (items.length <= 1) return items;
var pivot = items.shift(), current, left = [], right = [];
+ /*
+ This is a multiline comment block with
+ an empty line inside of it.
+
+ Awesome.
+ */
while(items.length > 0) {
current = items.shift();
current < pivot ? left.push(current) : right.push(current);
}
+ // This is a collection of
+ // single line comments
+
+ // ...with an empty line
+ // among it, geez!
return sort(left).concat(pivot).concat(sort(right));
};
// this is a single-line comment
return sort(Array.apply(this, arguments));
-};
\ No newline at end of file
+};
diff --git a/spec/fixtures/shebang b/spec/fixtures/shebang
index f15429b13..f343f6833 100644
--- a/spec/fixtures/shebang
+++ b/spec/fixtures/shebang
@@ -1,3 +1,3 @@
#!/usr/bin/ruby
-puts "America – fuck yeah!"
\ No newline at end of file
+puts "Atom fixture test"
diff --git a/spec/git-repository-async-spec.js b/spec/git-repository-async-spec.js
index dfc3d5803..fcb528819 100644
--- a/spec/git-repository-async-spec.js
+++ b/spec/git-repository-async-spec.js
@@ -3,7 +3,6 @@
import fs from 'fs-plus'
import path from 'path'
import temp from 'temp'
-import Git from 'nodegit'
import {it, beforeEach, afterEach} from './async-spec-helpers'
@@ -47,7 +46,7 @@ describe('GitRepositoryAsync', () => {
let threw = false
try {
- await repo.repoPromise
+ await repo.getRepo()
} catch (e) {
threw = true
}
@@ -56,6 +55,14 @@ describe('GitRepositoryAsync', () => {
})
})
+ describe('openedPath', () => {
+ it('is the path passed to .open', () => {
+ const workingDirPath = copyRepository()
+ repo = GitRepositoryAsync.open(workingDirPath)
+ expect(repo.openedPath).toBe(workingDirPath)
+ })
+ })
+
describe('.getRepo()', () => {
beforeEach(() => {
const workingDirectory = copySubmoduleRepository()
@@ -64,19 +71,19 @@ describe('GitRepositoryAsync', () => {
})
it('returns the repository when not given a path', async () => {
- const nodeGitRepo1 = await repo.repoPromise
+ const nodeGitRepo1 = await repo.getRepo()
const nodeGitRepo2 = await repo.getRepo()
expect(nodeGitRepo1.workdir()).toBe(nodeGitRepo2.workdir())
})
it('returns the repository when given a non-submodule path', async () => {
- const nodeGitRepo1 = await repo.repoPromise
+ const nodeGitRepo1 = await repo.getRepo()
const nodeGitRepo2 = await repo.getRepo('README')
expect(nodeGitRepo1.workdir()).toBe(nodeGitRepo2.workdir())
})
it('returns the submodule repository when given a submodule path', async () => {
- const nodeGitRepo1 = await repo.repoPromise
+ const nodeGitRepo1 = await repo.getRepo()
const nodeGitRepo2 = await repo.getRepo('jstips')
expect(nodeGitRepo1.workdir()).not.toBe(nodeGitRepo2.workdir())
@@ -103,7 +110,7 @@ describe('GitRepositoryAsync', () => {
it('returns the repository path for a repository path', async () => {
repo = openFixture('master.git')
const repoPath = await repo.getPath()
- expect(repoPath).toBe(path.join(__dirname, 'fixtures', 'git', 'master.git'))
+ expect(repoPath).toEqualPath(path.join(__dirname, 'fixtures', 'git', 'master.git'))
})
})
@@ -230,9 +237,7 @@ describe('GitRepositoryAsync', () => {
})
})
- // @joshaber: Disabling for now. There seems to be some race with path
- // subscriptions leading to intermittent test failures, e.g.: https://travis-ci.org/atom/atom/jobs/102702554
- xdescribe('.checkoutHeadForEditor(editor)', () => {
+ describe('.checkoutHeadForEditor(editor)', () => {
let filePath
let editor
@@ -305,7 +310,7 @@ describe('GitRepositoryAsync', () => {
await repo.getPathStatus(filePath)
expect(statusHandler.callCount).toBe(1)
- const status = Git.Status.STATUS.WT_MODIFIED
+ const status = GitRepositoryAsync.Git.Status.STATUS.WT_MODIFIED
expect(statusHandler.argsForCall[0][0]).toEqual({path: filePath, pathStatus: status})
fs.writeFileSync(filePath, 'abc')
@@ -338,10 +343,10 @@ describe('GitRepositoryAsync', () => {
})
describe('.refreshStatus()', () => {
- let newPath, modifiedPath, cleanPath
+ let newPath, modifiedPath, cleanPath, workingDirectory
beforeEach(() => {
- const workingDirectory = copyRepository()
+ workingDirectory = copyRepository()
repo = GitRepositoryAsync.open(workingDirectory)
modifiedPath = path.join(workingDirectory, 'file.txt')
newPath = path.join(workingDirectory, 'untracked.txt')
@@ -362,7 +367,7 @@ describe('GitRepositoryAsync', () => {
describe('in a repository with submodules', () => {
beforeEach(() => {
- const workingDirectory = copySubmoduleRepository()
+ workingDirectory = copySubmoduleRepository()
repo = GitRepositoryAsync.open(workingDirectory)
modifiedPath = path.join(workingDirectory, 'jstips', 'README.md')
newPath = path.join(workingDirectory, 'You-Dont-Need-jQuery', 'untracked.txt')
@@ -380,6 +385,86 @@ describe('GitRepositoryAsync', () => {
expect(repo.isStatusModified(await repo.getCachedPathStatus(modifiedPath))).toBe(true)
})
})
+
+ it('caches the proper statuses when a subdir is open', async () => {
+ const subDir = path.join(workingDirectory, 'dir')
+ fs.mkdirSync(subDir)
+
+ const filePath = path.join(subDir, 'b.txt')
+ fs.writeFileSync(filePath, '')
+
+ atom.project.setPaths([subDir])
+
+ await atom.workspace.open('b.txt')
+
+ const repo = atom.project.getRepositories()[0].async
+
+ await repo.refreshStatus()
+
+ const status = await repo.getCachedPathStatus(filePath)
+ expect(repo.isStatusModified(status)).toBe(false)
+ expect(repo.isStatusNew(status)).toBe(false)
+ })
+
+ it('caches the proper statuses when multiple project are open', async () => {
+ const otherWorkingDirectory = copyRepository()
+
+ atom.project.setPaths([workingDirectory, otherWorkingDirectory])
+
+ await atom.workspace.open('b.txt')
+
+ const repo = atom.project.getRepositories()[0].async
+
+ await repo.refreshStatus()
+
+ const subDir = path.join(workingDirectory, 'dir')
+ fs.mkdirSync(subDir)
+
+ const filePath = path.join(subDir, 'b.txt')
+ fs.writeFileSync(filePath, 'some content!')
+
+ const status = await repo.getCachedPathStatus(filePath)
+ expect(repo.isStatusModified(status)).toBe(true)
+ expect(repo.isStatusNew(status)).toBe(false)
+ })
+
+ it('emits did-change-statuses if the status changes', async () => {
+ const someNewPath = path.join(workingDirectory, 'MyNewJSFramework.md')
+ fs.writeFileSync(someNewPath, '')
+
+ const statusHandler = jasmine.createSpy('statusHandler')
+ repo.onDidChangeStatuses(statusHandler)
+
+ await repo.refreshStatus()
+
+ waitsFor('the onDidChangeStatuses handler to be called', () => statusHandler.callCount > 0)
+ })
+
+ it('emits did-change-statuses if the branch changes', async () => {
+ const statusHandler = jasmine.createSpy('statusHandler')
+ repo.onDidChangeStatuses(statusHandler)
+
+ repo._refreshBranch = jasmine.createSpy('_refreshBranch').andCallFake(() => {
+ return Promise.resolve(true)
+ })
+
+ await repo.refreshStatus()
+
+ waitsFor('the onDidChangeStatuses handler to be called', () => statusHandler.callCount > 0)
+ })
+
+ it('emits did-change-statuses if the ahead/behind changes', async () => {
+ const statusHandler = jasmine.createSpy('statusHandler')
+ repo.onDidChangeStatuses(statusHandler)
+
+ repo._refreshAheadBehindCount = jasmine.createSpy('_refreshAheadBehindCount').andCallFake(() => {
+ return Promise.resolve(true)
+ })
+
+ await repo.refreshStatus()
+
+ waitsFor('the onDidChangeStatuses handler to be called', () => statusHandler.callCount > 0)
+ })
})
describe('.isProjectAtRoot()', () => {
@@ -499,7 +584,7 @@ describe('GitRepositoryAsync', () => {
await atom.workspace.open('file.txt')
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
- project2.deserialize(atom.project.serialize(), atom.deserializers)
+ project2.deserialize(atom.project.serialize({isUnloading: true}))
const repo = project2.getRepositories()[0].async
waitsForPromise(() => repo.refreshStatus())
@@ -548,6 +633,14 @@ describe('GitRepositoryAsync', () => {
const relativizedPath = repo.relativize(`${workdir}/a/b.txt`, workdir)
expect(relativizedPath).toBe('a/b.txt')
})
+
+ it('preserves file case', () => {
+ repo.isCaseInsensitive = true
+
+ const workdir = '/tmp/foo/bar/baz/'
+ const relativizedPath = repo.relativize(`${workdir}a/README.txt`, workdir)
+ expect(relativizedPath).toBe('a/README.txt')
+ })
})
describe('.getShortHead(path)', () => {
@@ -626,7 +719,7 @@ describe('GitRepositoryAsync', () => {
repo = GitRepositoryAsync.open(workingDirectory)
})
- it('returns 0, 0 for a branch with no upstream', async () => {
+ it('returns 1, 0 for a branch which is ahead by 1', async () => {
await repo.refreshStatus()
const {ahead, behind} = await repo.getCachedUpstreamAheadBehindCount('You-Dont-Need-jQuery')
@@ -792,4 +885,34 @@ describe('GitRepositoryAsync', () => {
})
})
})
+
+ describe('.getOriginURL()', () => {
+ beforeEach(() => {
+ const workingDirectory = copyRepository('repo-with-submodules')
+ repo = GitRepositoryAsync.open(workingDirectory)
+ })
+
+ it('returns the origin URL', async () => {
+ const url = await repo.getOriginURL()
+ expect(url).toBe('git@github.com:atom/some-repo-i-guess.git')
+ })
+ })
+
+ describe('.getUpstreamBranch()', () => {
+ it('returns null when there is no upstream branch', async () => {
+ const workingDirectory = copyRepository()
+ repo = GitRepositoryAsync.open(workingDirectory)
+
+ const upstream = await repo.getUpstreamBranch()
+ expect(upstream).toBe(null)
+ })
+
+ it('returns the upstream branch', async () => {
+ const workingDirectory = copyRepository('repo-with-submodules')
+ repo = GitRepositoryAsync.open(workingDirectory)
+
+ const upstream = await repo.getUpstreamBranch()
+ expect(upstream).toBe('refs/remotes/origin/master')
+ })
+ })
})
diff --git a/spec/git-spec.coffee b/spec/git-spec.coffee
index e6b5d4df6..82e371146 100644
--- a/spec/git-spec.coffee
+++ b/spec/git-spec.coffee
@@ -33,7 +33,7 @@ describe "GitRepository", ->
waitsForPromise ->
repo.async.getPath().then(onSuccess)
runs ->
- expect(onSuccess.mostRecentCall.args[0]).toBe(repoPath)
+ expect(onSuccess.mostRecentCall.args[0]).toEqualPath(repoPath)
describe "new GitRepository(path)", ->
it "throws an exception when no repository is found", ->
@@ -259,6 +259,46 @@ describe "GitRepository", ->
expect(repo.isStatusModified(status)).toBe false
expect(repo.isStatusNew(status)).toBe false
+ it 'caches the proper statuses when multiple project are open', ->
+ otherWorkingDirectory = copyRepository()
+
+ atom.project.setPaths([workingDirectory, otherWorkingDirectory])
+
+ waitsForPromise ->
+ atom.workspace.open('b.txt')
+
+ statusHandler = null
+ runs ->
+ repo = atom.project.getRepositories()[0]
+
+ statusHandler = jasmine.createSpy('statusHandler')
+ repo.onDidChangeStatuses statusHandler
+ repo.refreshStatus()
+
+ waitsFor ->
+ statusHandler.callCount > 0
+
+ runs ->
+ subDir = path.join(workingDirectory, 'dir')
+ fs.mkdirSync(subDir)
+
+ filePath = path.join(subDir, 'b.txt')
+ fs.writeFileSync(filePath, '')
+
+ status = repo.getCachedPathStatus(filePath)
+ expect(repo.isStatusModified(status)).toBe true
+ expect(repo.isStatusNew(status)).toBe false
+
+ it 'caches statuses that were looked up synchronously', ->
+ originalContent = 'undefined'
+ fs.writeFileSync(modifiedPath, 'making this path modified')
+ repo.getPathStatus('file.txt')
+
+ fs.writeFileSync(modifiedPath, originalContent)
+ waitsForPromise -> repo.refreshStatus()
+ runs ->
+ expect(repo.isStatusModified(repo.getCachedPathStatus(modifiedPath))).toBeFalsy()
+
describe "buffer events", ->
[editor] = []
@@ -317,7 +357,7 @@ describe "GitRepository", ->
runs ->
project2 = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
- project2.deserialize(atom.project.serialize(), atom.deserializers)
+ project2.deserialize(atom.project.serialize({isUnloading: false}))
buffer = project2.getBuffers()[0]
waitsFor ->
diff --git a/spec/integration/helpers/start-atom.coffee b/spec/integration/helpers/start-atom.coffee
index 3c1016ad2..2939dd6ab 100644
--- a/spec/integration/helpers/start-atom.coffee
+++ b/spec/integration/helpers/start-atom.coffee
@@ -15,6 +15,8 @@ ChromedriverPort = 9515
ChromedriverURLBase = "/wd/hub"
ChromedriverStatusURL = "http://localhost:#{ChromedriverPort}#{ChromedriverURLBase}/status"
+userDataDir = temp.mkdirSync('atom-user-data-dir')
+
chromeDriverUp = (done) ->
checkStatus = ->
http
@@ -48,7 +50,7 @@ buildAtomClient = (args, env) ->
"atom-env=#{map(env, (value, key) -> "#{key}=#{value}").join(" ")}"
"dev"
"safe"
- "user-data-dir=#{temp.mkdirSync('atom-user-data-dir')}"
+ "user-data-dir=#{userDataDir}"
"socket-path=#{SocketPath}"
])
@@ -124,7 +126,7 @@ buildAtomClient = (args, env) ->
.addCommand "simulateQuit", (done) ->
@execute -> atom.unloadEditorWindow()
- .execute -> require("remote").require("app").emit("before-quit")
+ .execute -> require("electron").remote.app.emit("before-quit")
.call(done)
module.exports = (args, env, fn) ->
diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee
index 6e8a7f55a..7c2503e69 100644
--- a/spec/integration/startup-spec.coffee
+++ b/spec/integration/startup-spec.coffee
@@ -28,13 +28,12 @@ describe "Starting Atom", ->
it "opens the parent directory and creates an empty text editor", ->
runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) ->
client
- .waitForPaneItemCount(1, 1000)
-
.treeViewRootDirectories()
.then ({value}) -> expect(value).toEqual([tempDirPath])
.waitForExist("atom-text-editor", 5000)
.then (exists) -> expect(exists).toBe true
+ .waitForPaneItemCount(1, 1000)
.click("atom-text-editor")
.keys("Hello!")
.execute -> atom.workspace.getActiveTextEditor().getText()
@@ -124,6 +123,34 @@ describe "Starting Atom", ->
.waitForPaneItemCount(0, 1000)
.treeViewRootDirectories()
.then ({value}) -> expect(value).toEqual([otherTempDirPath])
+ describe "when using the -a, --add option", ->
+ it "reuses that window and add the folder to project paths", ->
+ fourthTempDir = temp.mkdirSync("a-fourth-dir")
+ fourthTempFilePath = path.join(fourthTempDir, "a-file")
+ fs.writeFileSync(fourthTempFilePath, "4 - This file was already here.")
+
+ fifthTempDir = temp.mkdirSync("a-fifth-dir")
+ fifthTempFilePath = path.join(fifthTempDir, "a-file")
+ fs.writeFileSync(fifthTempFilePath, "5 - This file was already here.")
+
+ runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) ->
+ client
+ .waitForPaneItemCount(1, 5000)
+
+ # Opening another file reuses the same window and add parent dir to
+ # project paths.
+ .startAnotherAtom(['-a', fourthTempFilePath], ATOM_HOME: atomHome)
+ .waitForPaneItemCount(2, 5000)
+ .waitForWindowCount(1, 1000)
+ .treeViewRootDirectories()
+ .then ({value}) -> expect(value).toEqual([tempDirPath, fourthTempDir])
+ .execute -> atom.workspace.getActiveTextEditor().getText()
+ .then ({value: text}) -> expect(text).toBe "4 - This file was already here."
+
+ # Opening another directory resuses the same window and add the folder to project paths.
+ .startAnotherAtom(['--add', fifthTempDir], ATOM_HOME: atomHome)
+ .treeViewRootDirectories()
+ .then ({value}) -> expect(value).toEqual([tempDirPath, fourthTempDir, fifthTempDir])
it "opens the new window offset from the other window", ->
runAtom [path.join(tempDirPath, "new-file")], {ATOM_HOME: atomHome}, (client) ->
@@ -153,6 +180,8 @@ describe "Starting Atom", ->
.waitForPaneItemCount(0, 3000)
.execute -> atom.workspace.open()
.waitForPaneItemCount(1, 3000)
+ .keys("Hello!")
+ .waitUntil((-> Promise.resolve(false)), 1100)
runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) ->
client
@@ -239,6 +268,36 @@ describe "Starting Atom", ->
[otherTempDirPath]
].sort()
+ it "doesn't reopen any previously opened windows if restorePreviousWindowsOnStart is disabled", ->
+ runAtom [tempDirPath], {ATOM_HOME: atomHome}, (client) ->
+ client
+ .waitForExist("atom-workspace")
+ .waitForNewWindow(->
+ @startAnotherAtom([otherTempDirPath], ATOM_HOME: atomHome)
+ , 5000)
+ .waitForExist("atom-workspace")
+
+ configPath = path.join(atomHome, 'config.cson')
+ config = CSON.readFileSync(configPath)
+ config['*'].core = {restorePreviousWindowsOnStart: false}
+ CSON.writeFileSync(configPath, config)
+
+ runAtom [], {ATOM_HOME: atomHome}, (client) ->
+ windowProjectPaths = []
+
+ client
+ .waitForWindowCount(1, 10000)
+ .then ({value: windowHandles}) ->
+ @window(windowHandles[0])
+ .waitForExist("atom-workspace")
+ .treeViewRootDirectories()
+ .then ({value: directories}) -> windowProjectPaths.push(directories)
+
+ .call ->
+ expect(windowProjectPaths).toEqual [
+ []
+ ]
+
describe "opening a remote directory", ->
it "opens the parent directory and creates an empty text editor", ->
remoteDirectory = 'remote://server:3437/some/directory/path'
diff --git a/spec/jasmine-test-runner.coffee b/spec/jasmine-test-runner.coffee
index 62b42d0a9..dd8386f5d 100644
--- a/spec/jasmine-test-runner.coffee
+++ b/spec/jasmine-test-runner.coffee
@@ -1,7 +1,8 @@
+Grim = require 'grim'
_ = require 'underscore-plus'
fs = require 'fs-plus'
path = require 'path'
-ipc = require 'ipc'
+{ipcRenderer} = require 'electron'
module.exports = ({logFile, headless, testPaths, buildAtomEnvironment}) ->
window[key] = value for key, value of require '../vendor/jasmine'
@@ -88,7 +89,7 @@ buildTerminalReporter = (logFile, resolveWithExitCode) ->
if logStream?
fs.writeSync(logStream, str)
else
- ipc.send 'write-to-stderr', str
+ ipcRenderer.send 'write-to-stderr', str
{TerminalReporter} = require 'jasmine-tagged'
new TerminalReporter
@@ -96,13 +97,10 @@ buildTerminalReporter = (logFile, resolveWithExitCode) ->
log(str)
onComplete: (runner) ->
fs.closeSync(logStream) if logStream?
- if process.env.JANKY_SHA1 or process.env.CI
- grim = require 'grim'
-
- if grim.getDeprecationsLength() > 0
- grim.logDeprecations()
- resolveWithExitCode(1)
- return
+ if Grim.getDeprecationsLength() > 0
+ Grim.logDeprecations()
+ resolveWithExitCode(1)
+ return
if runner.results().failedCount > 0
resolveWithExitCode(1)
diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee
index 7ea4a1ae9..d8f545abe 100644
--- a/spec/language-mode-spec.coffee
+++ b/spec/language-mode-spec.coffee
@@ -334,66 +334,56 @@ describe "LanguageMode", ->
it "folds every foldable line", ->
languageMode.foldAll()
- fold1 = editor.tokenizedLineForScreenRow(0).fold
- expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 12]
- fold1.destroy()
-
- fold2 = editor.tokenizedLineForScreenRow(1).fold
- expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [1, 9]
- fold2.destroy()
-
- fold3 = editor.tokenizedLineForScreenRow(4).fold
- expect([fold3.getStartRow(), fold3.getEndRow()]).toEqual [4, 7]
+ [fold1, fold2, fold3] = languageMode.unfoldAll()
+ expect([fold1.start.row, fold1.end.row]).toEqual [0, 12]
+ expect([fold2.start.row, fold2.end.row]).toEqual [1, 9]
+ expect([fold3.start.row, fold3.end.row]).toEqual [4, 7]
describe ".foldBufferRow(bufferRow)", ->
describe "when bufferRow can be folded", ->
it "creates a fold based on the syntactic region starting at the given row", ->
languageMode.foldBufferRow(1)
- fold = editor.tokenizedLineForScreenRow(1).fold
- expect(fold.getStartRow()).toBe 1
- expect(fold.getEndRow()).toBe 9
+ [fold] = languageMode.unfoldAll()
+ expect([fold.start.row, fold.end.row]).toEqual [1, 9]
describe "when bufferRow can't be folded", ->
it "searches upward for the first row that begins a syntatic region containing the given buffer row (and folds it)", ->
languageMode.foldBufferRow(8)
- fold = editor.tokenizedLineForScreenRow(1).fold
- expect(fold.getStartRow()).toBe 1
- expect(fold.getEndRow()).toBe 9
+ [fold] = languageMode.unfoldAll()
+ expect([fold.start.row, fold.end.row]).toEqual [1, 9]
describe "when the bufferRow is already folded", ->
it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", ->
languageMode.foldBufferRow(2)
- expect(editor.tokenizedLineForScreenRow(1).fold).toBeDefined()
- expect(editor.tokenizedLineForScreenRow(0).fold).not.toBeDefined()
+ expect(editor.isFoldedAtBufferRow(0)).toBe(false)
+ expect(editor.isFoldedAtBufferRow(1)).toBe(true)
languageMode.foldBufferRow(1)
- expect(editor.tokenizedLineForScreenRow(0).fold).toBeDefined()
+ expect(editor.isFoldedAtBufferRow(0)).toBe(true)
describe "when the bufferRow is in a multi-line comment", ->
it "searches upward and downward for surrounding comment lines and folds them as a single fold", ->
buffer.insert([1, 0], " //this is a comment\n // and\n //more docs\n\n//second comment")
languageMode.foldBufferRow(1)
- fold = editor.tokenizedLineForScreenRow(1).fold
- expect(fold.getStartRow()).toBe 1
- expect(fold.getEndRow()).toBe 3
+ [fold] = languageMode.unfoldAll()
+ expect([fold.start.row, fold.end.row]).toEqual [1, 3]
describe "when the bufferRow is a single-line comment", ->
it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", ->
buffer.insert([1, 0], " //this is a single line comment\n")
languageMode.foldBufferRow(1)
- fold = editor.tokenizedLineForScreenRow(0).fold
- expect(fold.getStartRow()).toBe 0
- expect(fold.getEndRow()).toBe 13
+ [fold] = languageMode.unfoldAll()
+ expect([fold.start.row, fold.end.row]).toEqual [0, 13]
describe ".foldAllAtIndentLevel(indentLevel)", ->
it "folds blocks of text at the given indentation level", ->
languageMode.foldAllAtIndentLevel(0)
- expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
+ expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" + editor.displayLayer.foldCharacter
expect(editor.getLastScreenRow()).toBe 0
languageMode.foldAllAtIndentLevel(1)
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
- expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {"
+ expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {" + editor.displayLayer.foldCharacter
expect(editor.getLastScreenRow()).toBe 4
languageMode.foldAllAtIndentLevel(2)
@@ -429,45 +419,47 @@ describe "LanguageMode", ->
it "folds every foldable line", ->
languageMode.foldAll()
- fold1 = editor.tokenizedLineForScreenRow(0).fold
- expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 19]
- fold1.destroy()
-
- fold2 = editor.tokenizedLineForScreenRow(1).fold
- expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [1, 4]
-
- fold3 = editor.tokenizedLineForScreenRow(2).fold.destroy()
-
- fold4 = editor.tokenizedLineForScreenRow(3).fold
- expect([fold4.getStartRow(), fold4.getEndRow()]).toEqual [6, 8]
+ folds = languageMode.unfoldAll()
+ expect(folds.length).toBe 8
+ expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30]
+ expect([folds[1].start.row, folds[1].end.row]).toEqual [1, 4]
+ expect([folds[2].start.row, folds[2].end.row]).toEqual [5, 27]
+ expect([folds[3].start.row, folds[3].end.row]).toEqual [6, 8]
+ expect([folds[4].start.row, folds[4].end.row]).toEqual [11, 16]
+ expect([folds[5].start.row, folds[5].end.row]).toEqual [17, 20]
+ expect([folds[6].start.row, folds[6].end.row]).toEqual [21, 22]
+ expect([folds[7].start.row, folds[7].end.row]).toEqual [24, 25]
describe ".foldAllAtIndentLevel()", ->
it "folds every foldable range at a given indentLevel", ->
languageMode.foldAllAtIndentLevel(2)
- fold1 = editor.tokenizedLineForScreenRow(6).fold
- expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [6, 8]
- fold1.destroy()
-
- fold2 = editor.tokenizedLineForScreenRow(11).fold
- expect([fold2.getStartRow(), fold2.getEndRow()]).toEqual [11, 14]
- fold2.destroy()
+ folds = languageMode.unfoldAll()
+ expect(folds.length).toBe 5
+ expect([folds[0].start.row, folds[0].end.row]).toEqual [6, 8]
+ expect([folds[1].start.row, folds[1].end.row]).toEqual [11, 16]
+ expect([folds[2].start.row, folds[2].end.row]).toEqual [17, 20]
+ expect([folds[3].start.row, folds[3].end.row]).toEqual [21, 22]
+ expect([folds[4].start.row, folds[4].end.row]).toEqual [24, 25]
it "does not fold anything but the indentLevel", ->
languageMode.foldAllAtIndentLevel(0)
- fold1 = editor.tokenizedLineForScreenRow(0).fold
- expect([fold1.getStartRow(), fold1.getEndRow()]).toEqual [0, 19]
- fold1.destroy()
-
- fold2 = editor.tokenizedLineForScreenRow(5).fold
- expect(fold2).toBeFalsy()
+ folds = languageMode.unfoldAll()
+ expect(folds.length).toBe 1
+ expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30]
describe ".isFoldableAtBufferRow(bufferRow)", ->
it "returns true if the line starts a multi-line comment", ->
expect(languageMode.isFoldableAtBufferRow(1)).toBe true
expect(languageMode.isFoldableAtBufferRow(6)).toBe true
- expect(languageMode.isFoldableAtBufferRow(17)).toBe false
+ expect(languageMode.isFoldableAtBufferRow(8)).toBe false
+ expect(languageMode.isFoldableAtBufferRow(11)).toBe true
+ expect(languageMode.isFoldableAtBufferRow(15)).toBe false
+ expect(languageMode.isFoldableAtBufferRow(17)).toBe true
+ expect(languageMode.isFoldableAtBufferRow(21)).toBe true
+ expect(languageMode.isFoldableAtBufferRow(24)).toBe true
+ expect(languageMode.isFoldableAtBufferRow(28)).toBe false
it "does not return true for a line in the middle of a comment that's followed by an indented line", ->
expect(languageMode.isFoldableAtBufferRow(7)).toBe false
diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee
index 46935510f..bb0294b54 100644
--- a/spec/lines-yardstick-spec.coffee
+++ b/spec/lines-yardstick-spec.coffee
@@ -19,36 +19,45 @@ describe "LinesYardstick", ->
screenRowsToMeasure = []
buildLineNode = (screenRow) ->
- tokenizedLine = editor.tokenizedLineForScreenRow(screenRow)
- iterator = tokenizedLine.getTokenIterator()
+ startIndex = 0
+ scopes = []
+ screenLine = editor.screenLineForScreenRow(screenRow)
lineNode = document.createElement("div")
lineNode.style.whiteSpace = "pre"
- while iterator.next()
- span = document.createElement("span")
- span.className = iterator.getScopes().join(' ').replace(/\.+/g, ' ')
- span.textContent = iterator.getText()
- lineNode.appendChild(span)
+ for tagCode in screenLine.tagCodes when tagCode isnt 0
+ if editor.displayLayer.isCloseTagCode(tagCode)
+ scopes.pop()
+ else if editor.displayLayer.isOpenTagCode(tagCode)
+ scopes.push(editor.displayLayer.tagForCode(tagCode))
+ else
+ text = screenLine.lineText.substr(startIndex, tagCode)
+ startIndex += tagCode
+ span = document.createElement("span")
+ span.className = scopes.join(' ').replace(/\.+/g, ' ')
+ span.textContent = text
+ lineNode.appendChild(span)
jasmine.attachToDOM(lineNode)
createdLineNodes.push(lineNode)
lineNode
mockLineNodesProvider =
- lineNodeForLineIdAndScreenRow: (lineId, screenRow) ->
- buildLineNode(screenRow)
+ lineNodesById: {}
+ lineIdForScreenRow: (screenRow) ->
+ editor.screenLineForScreenRow(screenRow).id
- textNodesForLineIdAndScreenRow: (lineId, screenRow) ->
- lineNode = @lineNodeForLineIdAndScreenRow(lineId, screenRow)
+ lineNodeForScreenRow: (screenRow) ->
+ @lineNodesById[@lineIdForScreenRow(screenRow)] ?= buildLineNode(screenRow)
+
+ textNodesForScreenRow: (screenRow) ->
+ lineNode = @lineNodeForScreenRow(screenRow)
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT)
textNodes = []
- while textNode = iterator.nextNode()
- textNodes.push(textNode)
+ textNodes.push(textNode) while textNode = iterator.nextNode()
textNodes
editor.setLineHeightInPixels(14)
- lineTopIndex = new LineTopIndex({
- defaultLineHeight: editor.getLineHeightInPixels()
- })
+ lineTopIndex = new LineTopIndex({defaultLineHeight: editor.getLineHeightInPixels()})
linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars)
afterEach ->
@@ -69,9 +78,9 @@ describe "LinesYardstick", ->
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 0))).toEqual({left: 0, top: 0})
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 1))).toEqual({left: 7, top: 0})
- expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5))).toEqual({left: 37.78125, top: 0})
- expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 43.171875, top: 14})
- expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 72.171875, top: 14})
+ expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5))).toEqual({left: 38, top: 0})
+ expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 43, top: 14})
+ expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 72, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, Infinity))).toEqual({left: 287.859375, top: 28})
it "reuses already computed pixel positions unless it is invalidated", ->
@@ -82,9 +91,9 @@ describe "LinesYardstick", ->
}
"""
- expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19.203125, top: 14})
+ expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 57.609375, top: 28})
- expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 95.609375, top: 70})
+ expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 96, top: 70})
atom.styles.addStyleSheet """
* {
@@ -92,9 +101,9 @@ describe "LinesYardstick", ->
}
"""
- expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19.203125, top: 14})
+ expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19, top: 14})
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 57.609375, top: 28})
- expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 95.609375, top: 70})
+ expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 96, top: 70})
linesYardstick.invalidateCache()
@@ -102,23 +111,6 @@ describe "LinesYardstick", ->
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 72, top: 28})
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 120, top: 70})
- it "correctly handles RTL characters", ->
- atom.styles.addStyleSheet """
- * {
- font-size: 14px;
- font-family: monospace;
- }
- """
-
- editor.setText("السلام عليكم")
- expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 0)).left).toBe 0
- expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 1)).left).toBe 8
- expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 2)).left).toBe 16
- expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5)).left).toBe 33
- expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 7)).left).toBe 50
- expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 9)).left).toBe 67
- expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 11)).left).toBe 84
-
it "doesn't report a width greater than 0 when the character to measure is at the beginning of a text node", ->
# This spec documents what seems to be a bug in Chromium, because we'd
# expect that Range(0, 0).getBoundingClientRect().width to always be zero.
@@ -163,9 +155,38 @@ describe "LinesYardstick", ->
expect(linesYardstick.screenPositionForPixelPosition({top: 28, left: 100})).toEqual([2, 14])
expect(linesYardstick.screenPositionForPixelPosition({top: 32, left: 24.3})).toEqual([2, 3])
expect(linesYardstick.screenPositionForPixelPosition({top: 46, left: 66.5})).toEqual([3, 9])
- expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 99.9})).toEqual([5, 14])
- expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 224.2365234375})).toEqual([5, 29])
- expect(linesYardstick.screenPositionForPixelPosition({top: 80, left: 225})).toEqual([5, 30])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 99.9})).toEqual([5, 14])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 29])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 225})).toEqual([5, 30])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 33])
+
+ it "overshoots to the nearest character when text nodes are not spatially contiguous", ->
+ atom.styles.addStyleSheet """
+ * {
+ font-size: 12px;
+ font-family: monospace;
+ }
+ """
+
+ buildLineNode = (screenRow) ->
+ lineNode = document.createElement("div")
+ lineNode.style.whiteSpace = "pre"
+ lineNode.innerHTML = 'foobar'
+ jasmine.attachToDOM(lineNode)
+ createdLineNodes.push(lineNode)
+ lineNode
+ editor.setText("foobar")
+
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 7})).toEqual([0, 1])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 14})).toEqual([0, 2])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 21})).toEqual([0, 3])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 30})).toEqual([0, 3])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 50})).toEqual([0, 3])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 62})).toEqual([0, 3])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 69})).toEqual([0, 4])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 76})).toEqual([0, 5])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 100})).toEqual([0, 6])
+ expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 200})).toEqual([0, 6])
it "clips pixel positions above buffer start", ->
expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0]
@@ -178,3 +199,7 @@ describe "LinesYardstick", ->
expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: Infinity)).toEqual [12, 2]
expect(linesYardstick.screenPositionForPixelPosition(top: (editor.getLastScreenRow() + 1) * 14, left: 0)).toEqual [12, 2]
expect(linesYardstick.screenPositionForPixelPosition(top: editor.getLastScreenRow() * 14, left: 0)).toEqual [12, 0]
+
+ it "clips negative horizontal pixel positions", ->
+ expect(linesYardstick.screenPositionForPixelPosition(top: 0, left: -10)).toEqual [0, 0]
+ expect(linesYardstick.screenPositionForPixelPosition(top: 1 * 14, left: -10)).toEqual [1, 0]
diff --git a/spec/module-cache-spec.coffee b/spec/module-cache-spec.coffee
index 3a995aec7..4c0a549aa 100644
--- a/spec/module-cache-spec.coffee
+++ b/spec/module-cache-spec.coffee
@@ -8,13 +8,13 @@ describe 'ModuleCache', ->
beforeEach ->
spyOn(Module, '_findPath').andCallThrough()
- it 'resolves atom shell module paths without hitting the filesystem', ->
+ it 'resolves Electron module paths without hitting the filesystem', ->
builtins = ModuleCache.cache.builtins
expect(Object.keys(builtins).length).toBeGreaterThan 0
for builtinName, builtinPath of builtins
expect(require.resolve(builtinName)).toBe builtinPath
- expect(fs.isFileSync(require.resolve(builtinName)))
+ expect(fs.isFileSync(require.resolve(builtinName))).toBeTruthy()
expect(Module._findPath.callCount).toBe 0
diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee
index 46d1d11ee..f68005cf5 100644
--- a/spec/package-manager-spec.coffee
+++ b/spec/package-manager-spec.coffee
@@ -17,6 +17,20 @@ describe "PackageManager", ->
beforeEach ->
workspaceElement = atom.views.getView(atom.workspace)
+ describe "::getApmPath()", ->
+ it "returns the path to the apm command", ->
+ apmPath = path.join(process.resourcesPath, "app", "apm", "bin", "apm")
+ if process.platform is 'win32'
+ apmPath += ".cmd"
+ expect(atom.packages.getApmPath()).toBe apmPath
+
+ describe "when the core.apmPath setting is set", ->
+ beforeEach ->
+ atom.config.set("core.apmPath", "/path/to/apm")
+
+ it "returns the value of the core.apmPath config setting", ->
+ expect(atom.packages.getApmPath()).toBe "/path/to/apm"
+
describe "::loadPackage(name)", ->
beforeEach ->
atom.config.set("core.disabledPackages", [])
@@ -52,15 +66,23 @@ describe "PackageManager", ->
expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-with-broken-package-json package")
expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-broken-package-json"
+ it "returns null if the package name or path starts with a dot", ->
+ expect(atom.packages.loadPackage("/Users/user/.atom/packages/.git")).toBeNull()
+
it "normalizes short repository urls in package.json", ->
{metadata} = atom.packages.loadPackage("package-with-short-url-package-json")
expect(metadata.repository.type).toBe "git"
- expect(metadata.repository.url).toBe "https://github.com/example/repo.git"
+ expect(metadata.repository.url).toBe "https://github.com/example/repo"
{metadata} = atom.packages.loadPackage("package-with-invalid-url-package-json")
expect(metadata.repository.type).toBe "git"
expect(metadata.repository.url).toBe "foo"
+ it "trims git+ from the beginning and .git from the end of repository URLs, even if npm already normalized them ", ->
+ {metadata} = atom.packages.loadPackage("package-with-prefixed-and-suffixed-repo-url")
+ expect(metadata.repository.type).toBe "git"
+ expect(metadata.repository.url).toBe "https://github.com/example/repo"
+
it "returns null if the package is not found in any package directory", ->
spyOn(console, 'warn')
expect(atom.packages.loadPackage("this-package-cannot-be-found")).toBeNull()
@@ -88,18 +110,16 @@ describe "PackageManager", ->
state1 = {deserializer: 'Deserializer1', a: 'b'}
expect(atom.deserializers.deserialize(state1)).toEqual {
- wasDeserializedBy: 'Deserializer1'
+ wasDeserializedBy: 'deserializeMethod1'
state: state1
}
state2 = {deserializer: 'Deserializer2', c: 'd'}
expect(atom.deserializers.deserialize(state2)).toEqual {
- wasDeserializedBy: 'Deserializer2'
+ wasDeserializedBy: 'deserializeMethod2'
state: state2
}
- expect(pack.mainModule).toBeNull()
-
describe "when there are view providers specified in the package's package.json", ->
model1 = {worksWithViewProvider1: true}
model2 = {worksWithViewProvider2: true}
@@ -448,16 +468,15 @@ describe "PackageManager", ->
pack = null
waitsForPromise ->
atom.packages.activatePackage("package-with-serialization").then (p) -> pack = p
-
runs ->
expect(pack.mainModule.someNumber).not.toBe 77
pack.mainModule.someNumber = 77
atom.packages.deactivatePackage("package-with-serialization")
spyOn(pack.mainModule, 'activate').andCallThrough()
- waitsForPromise ->
- atom.packages.activatePackage("package-with-serialization")
- runs ->
- expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77})
+ waitsForPromise ->
+ atom.packages.activatePackage("package-with-serialization")
+ runs ->
+ expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77})
it "invokes ::onDidActivatePackage listeners with the activated package", ->
activatedPackage = null
@@ -821,6 +840,34 @@ describe "PackageManager", ->
expect(atom.packages.isPackageActive("package-with-missing-provided-services")).toBe true
expect(addErrorHandler.callCount).toBe 0
+ describe "::serialize", ->
+ it "does not serialize packages that threw an error during activation", ->
+ spyOn(console, 'warn')
+ badPack = null
+ waitsForPromise ->
+ atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p
+
+ runs ->
+ spyOn(badPack.mainModule, 'serialize').andCallThrough()
+
+ atom.packages.serialize()
+ expect(badPack.mainModule.serialize).not.toHaveBeenCalled()
+
+ it "absorbs exceptions that are thrown by the package module's serialize method", ->
+ spyOn(console, 'error')
+
+ waitsForPromise ->
+ atom.packages.activatePackage('package-with-serialize-error')
+
+ waitsForPromise ->
+ atom.packages.activatePackage('package-with-serialization')
+
+ runs ->
+ atom.packages.serialize()
+ expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined()
+ expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1
+ expect(console.error).toHaveBeenCalled()
+
describe "::deactivatePackage(id)", ->
afterEach ->
atom.packages.unloadPackages()
@@ -852,33 +899,6 @@ describe "PackageManager", ->
expect(badPack.mainModule.deactivate).not.toHaveBeenCalled()
expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeFalsy()
- it "does not serialize packages that have not been activated called on their main module", ->
- spyOn(console, 'warn')
- badPack = null
- waitsForPromise ->
- atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p
-
- runs ->
- spyOn(badPack.mainModule, 'serialize').andCallThrough()
-
- atom.packages.deactivatePackage("package-that-throws-on-activate")
- expect(badPack.mainModule.serialize).not.toHaveBeenCalled()
-
- it "absorbs exceptions that are thrown by the package module's serialize method", ->
- spyOn(console, 'error')
-
- waitsForPromise ->
- atom.packages.activatePackage('package-with-serialize-error')
-
- waitsForPromise ->
- atom.packages.activatePackage('package-with-serialization')
-
- runs ->
- atom.packages.deactivatePackages()
- expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined()
- expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1
- expect(console.error).toHaveBeenCalled()
-
it "absorbs exceptions that are thrown by the package module's deactivate method", ->
spyOn(console, 'error')
diff --git a/spec/pane-axis-element-spec.coffee b/spec/pane-axis-element-spec.coffee
new file mode 100644
index 000000000..702e9c5fc
--- /dev/null
+++ b/spec/pane-axis-element-spec.coffee
@@ -0,0 +1,34 @@
+PaneAxis = require '../src/pane-axis'
+PaneContainer = require '../src/pane-container'
+Pane = require '../src/pane'
+
+buildPane = ->
+ new Pane({
+ applicationDelegate: atom.applicationDelegate,
+ config: atom.config,
+ deserializerManager: atom.deserializers,
+ notificationManager: atom.notifications
+ })
+
+describe "PaneAxisElement", ->
+ it "correctly subscribes and unsubscribes to the underlying model events on attach/detach", ->
+ container = new PaneContainer(config: atom.config, applicationDelegate: atom.applicationDelegate)
+ axis = new PaneAxis
+ axis.setContainer(container)
+ axisElement = atom.views.getView(axis)
+
+ panes = [buildPane(), buildPane(), buildPane()]
+
+ jasmine.attachToDOM(axisElement)
+ axis.addChild(panes[0])
+ expect(axisElement.children[0]).toBe(atom.views.getView(panes[0]))
+
+ axisElement.remove()
+ axis.addChild(panes[1])
+ expect(axisElement.children[2]).toBeUndefined()
+
+ jasmine.attachToDOM(axisElement)
+ expect(axisElement.children[2]).toBe(atom.views.getView(panes[1]))
+
+ axis.addChild(panes[2])
+ expect(axisElement.children[4]).toBe(atom.views.getView(panes[2]))
diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee
index 8c228e2a8..8abbb0ece 100644
--- a/spec/pane-spec.coffee
+++ b/spec/pane-spec.coffee
@@ -1,5 +1,6 @@
{extend} = require 'underscore-plus'
{Emitter} = require 'event-kit'
+Grim = require 'grim'
Pane = require '../src/pane'
PaneAxis = require '../src/pane-axis'
PaneContainer = require '../src/pane-container'
@@ -18,8 +19,8 @@ describe "Pane", ->
onDidDestroy: (fn) -> @emitter.on('did-destroy', fn)
destroy: -> @destroyed = true; @emitter.emit('did-destroy')
isDestroyed: -> @destroyed
- isPending: -> @pending
- pending: false
+ onDidTerminatePendingState: (callback) -> @emitter.on 'terminate-pending-state', callback
+ terminatePendingState: -> @emitter.emit 'terminate-pending-state'
beforeEach ->
confirm = spyOn(atom.applicationDelegate, 'confirm')
@@ -92,7 +93,7 @@ describe "Pane", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B")]))
[item1, item2] = pane.getItems()
item3 = new Item("C")
- pane.addItem(item3, 1)
+ pane.addItem(item3, index: 1)
expect(pane.getItems()).toEqual [item1, item3, item2]
it "adds the item after the active item if no index is provided", ->
@@ -115,7 +116,7 @@ describe "Pane", ->
pane.onDidAddItem (event) -> events.push(event)
item = new Item("C")
- pane.addItem(item, 1)
+ pane.addItem(item, index: 1)
expect(events).toEqual [{item, index: 1, moved: false}]
it "throws an exception if the item is already present on a pane", ->
@@ -132,15 +133,56 @@ describe "Pane", ->
expect(-> pane.addItem('foo')).toThrow()
expect(-> pane.addItem(1)).toThrow()
- it "destroys any existing pending item if the new item is pending", ->
+ it "destroys any existing pending item", ->
pane = new Pane(paneParams(items: []))
itemA = new Item("A")
itemB = new Item("B")
- itemA.pending = true
- itemB.pending = true
- pane.addItem(itemA)
+ itemC = new Item("C")
+ pane.addItem(itemA, pending: false)
+ pane.addItem(itemB, pending: true)
+ pane.addItem(itemC, pending: false)
+ expect(itemB.isDestroyed()).toBe true
+
+ it "adds the new item before destroying any existing pending item", ->
+ eventOrder = []
+
+ pane = new Pane(paneParams(items: []))
+ itemA = new Item("A")
+ itemB = new Item("B")
+ pane.addItem(itemA, pending: true)
+
+ pane.onDidAddItem ({item}) ->
+ eventOrder.push("add") if item is itemB
+
+ pane.onDidRemoveItem ({item}) ->
+ eventOrder.push("remove") if item is itemA
+
pane.addItem(itemB)
- expect(itemA.isDestroyed()).toBe true
+
+ waitsFor ->
+ eventOrder.length is 2
+
+ runs ->
+ expect(eventOrder).toEqual ["add", "remove"]
+
+ describe "when using the old API of ::addItem(item, index)", ->
+ beforeEach ->
+ spyOn Grim, "deprecate"
+
+ it "supports the older public API", ->
+ pane = new Pane(paneParams(items: []))
+ itemA = new Item("A")
+ itemB = new Item("B")
+ itemC = new Item("C")
+ pane.addItem(itemA, 0)
+ pane.addItem(itemB, 0)
+ pane.addItem(itemC, 0)
+ expect(pane.getItems()).toEqual [itemC, itemB, itemA]
+
+ it "shows a deprecation warning", ->
+ pane = new Pane(paneParams(items: []))
+ pane.addItem(new Item(), 2)
+ expect(Grim.deprecate).toHaveBeenCalledWith "Pane::addItem(item, 2) is deprecated in favor of Pane::addItem(item, {index: 2})"
describe "::activateItem(item)", ->
pane = null
@@ -172,21 +214,103 @@ describe "Pane", ->
beforeEach ->
itemC = new Item("C")
itemD = new Item("D")
- itemC.pending = true
- itemD.pending = true
it "replaces the active item if it is pending", ->
- pane.activateItem(itemC)
+ pane.activateItem(itemC, pending: true)
expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'C', 'B']
- pane.activateItem(itemD)
+ pane.activateItem(itemD, pending: true)
expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'D', 'B']
it "adds the item after the active item if it is not pending", ->
- pane.activateItem(itemC)
+ pane.activateItem(itemC, pending: true)
pane.activateItemAtIndex(2)
- pane.activateItem(itemD)
+ pane.activateItem(itemD, pending: true)
expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'B', 'D']
+ describe "::setPendingItem", ->
+ pane = null
+
+ beforeEach ->
+ pane = atom.workspace.getActivePane()
+
+ it "changes the pending item", ->
+ expect(pane.getPendingItem()).toBeNull()
+ pane.setPendingItem("fake item")
+ expect(pane.getPendingItem()).toEqual "fake item"
+
+ describe "::onItemDidTerminatePendingState callback", ->
+ pane = null
+ callbackCalled = false
+
+ beforeEach ->
+ pane = atom.workspace.getActivePane()
+ callbackCalled = false
+
+ it "is called when the pending item changes", ->
+ pane.setPendingItem("fake item one")
+ pane.onItemDidTerminatePendingState (item) ->
+ callbackCalled = true
+ expect(item).toEqual "fake item one"
+ pane.setPendingItem("fake item two")
+ expect(callbackCalled).toBeTruthy()
+
+ it "has access to the new pending item via ::getPendingItem", ->
+ pane.setPendingItem("fake item one")
+ pane.onItemDidTerminatePendingState (item) ->
+ callbackCalled = true
+ expect(pane.getPendingItem()).toEqual "fake item two"
+ pane.setPendingItem("fake item two")
+ expect(callbackCalled).toBeTruthy()
+
+ it "isn't called when a pending item is replaced with a new one", ->
+ pane = null
+ pendingSpy = jasmine.createSpy("onItemDidTerminatePendingState")
+ destroySpy = jasmine.createSpy("onWillDestroyItem")
+
+ waitsForPromise ->
+ atom.workspace.open('sample.txt', pending: true).then ->
+ pane = atom.workspace.getActivePane()
+
+ runs ->
+ pane.onItemDidTerminatePendingState pendingSpy
+ pane.onWillDestroyItem destroySpy
+
+ waitsForPromise ->
+ atom.workspace.open('sample.js', pending: true)
+
+ runs ->
+ expect(destroySpy).toHaveBeenCalled()
+ expect(pendingSpy).not.toHaveBeenCalled()
+
+ describe "::activateNextRecentlyUsedItem() and ::activatePreviousRecentlyUsedItem()", ->
+ it "sets the active item to the next/previous item in the itemStack, looping around at either end", ->
+ pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D"), new Item("E")]))
+ [item1, item2, item3, item4, item5] = pane.getItems()
+ pane.itemStack = [item3, item1, item2, item5, item4]
+
+ pane.activateItem(item4)
+ expect(pane.getActiveItem()).toBe item4
+ pane.activateNextRecentlyUsedItem()
+ expect(pane.getActiveItem()).toBe item5
+ pane.activateNextRecentlyUsedItem()
+ expect(pane.getActiveItem()).toBe item2
+ pane.activatePreviousRecentlyUsedItem()
+ expect(pane.getActiveItem()).toBe item5
+ pane.activatePreviousRecentlyUsedItem()
+ expect(pane.getActiveItem()).toBe item4
+ pane.activatePreviousRecentlyUsedItem()
+ expect(pane.getActiveItem()).toBe item3
+ pane.activatePreviousRecentlyUsedItem()
+ expect(pane.getActiveItem()).toBe item1
+ pane.activateNextRecentlyUsedItem()
+ expect(pane.getActiveItem()).toBe item3
+ pane.activateNextRecentlyUsedItem()
+ expect(pane.getActiveItem()).toBe item4
+ pane.activateNextRecentlyUsedItem()
+ pane.moveActiveItemToTopOfStack()
+ expect(pane.getActiveItem()).toBe item5
+ expect(pane.itemStack[4]).toBe item5
+
describe "::activateNextItem() and ::activatePreviousItem()", ->
it "sets the active item to the next/previous item, looping around at either end", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")]))
@@ -253,7 +377,7 @@ describe "Pane", ->
pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")]))
[item1, item2, item3] = pane.getItems()
- it "removes the item from the items list and destroyes it", ->
+ it "removes the item from the items list and destroys it", ->
expect(pane.getActiveItem()).toBe item1
pane.destroyItem(item2)
expect(item2 in pane.getItems()).toBe false
@@ -264,6 +388,23 @@ describe "Pane", ->
expect(item1 in pane.getItems()).toBe false
expect(item1.isDestroyed()).toBe true
+ it "removes the item from the itemStack", ->
+ pane.itemStack = [item2, item3, item1]
+
+ pane.activateItem(item1)
+ expect(pane.getActiveItem()).toBe item1
+ pane.destroyItem(item3)
+ expect(pane.itemStack).toEqual [item2, item1]
+ expect(pane.getActiveItem()).toBe item1
+
+ pane.destroyItem(item1)
+ expect(pane.itemStack).toEqual [item2]
+ expect(pane.getActiveItem()).toBe item2
+
+ pane.destroyItem(item2)
+ expect(pane.itemStack).toEqual []
+ expect(pane.getActiveItem()).toBeUndefined()
+
it "invokes ::onWillDestroyItem() observers before destroying the item", ->
events = []
pane.onWillDestroyItem (event) ->
@@ -605,6 +746,23 @@ describe "Pane", ->
expect(pane2.isDestroyed()).toBe true
expect(item4.isDestroyed()).toBe false
+ describe "when the item being moved is pending", ->
+ it "is made permanent in the new pane", ->
+ item6 = new Item("F")
+ pane1.addItem(item6, pending: true)
+ expect(pane1.getPendingItem()).toEqual item6
+ pane1.moveItemToPane(item6, pane2, 0)
+ expect(pane2.getPendingItem()).not.toEqual item6
+
+ describe "when the target pane has a pending item", ->
+ it "does not destroy the pending item", ->
+ item6 = new Item("F")
+ pane1.addItem(item6, pending: true)
+ expect(pane1.getPendingItem()).toEqual item6
+ pane2.moveItemToPane(item5, pane1, 0)
+ expect(pane1.getPendingItem()).toEqual item6
+
+
describe "split methods", ->
[pane1, item1, container] = []
@@ -759,6 +917,82 @@ describe "Pane", ->
expect(item1.save).not.toHaveBeenCalled()
expect(pane.isDestroyed()).toBe false
+ describe "when item fails to save", ->
+ [pane, item1, item2] = []
+
+ beforeEach ->
+ pane = new Pane({items: [new Item("A"), new Item("B")], applicationDelegate: atom.applicationDelegate, config: atom.config})
+ [item1, item2] = pane.getItems()
+
+ item1.shouldPromptToSave = -> true
+ item1.getURI = -> "/test/path"
+
+ item1.save = jasmine.createSpy("save").andCallFake ->
+ error = new Error("EACCES, permission denied '/test/path'")
+ error.path = '/test/path'
+ error.code = 'EACCES'
+ throw error
+
+ it "does not destroy the pane if save fails and user clicks cancel", ->
+ confirmations = 0
+ confirm.andCallFake ->
+ confirmations++
+ if confirmations is 1
+ return 0 # click save
+ else
+ return 1 # click cancel
+
+ pane.close()
+
+ expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
+ expect(confirmations).toBe(2)
+ expect(item1.save).toHaveBeenCalled()
+ expect(pane.isDestroyed()).toBe false
+
+ it "does destroy the pane if the user saves the file under a new name", ->
+ item1.saveAs = jasmine.createSpy("saveAs").andReturn(true)
+
+ confirmations = 0
+ confirm.andCallFake ->
+ confirmations++
+ return 0 # save and then save as
+
+ showSaveDialog.andReturn("new/path")
+
+ pane.close()
+
+ expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
+ expect(confirmations).toBe(2)
+ expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled()
+ expect(item1.save).toHaveBeenCalled()
+ expect(item1.saveAs).toHaveBeenCalled()
+ expect(pane.isDestroyed()).toBe true
+
+ it "asks again if the saveAs also fails", ->
+ item1.saveAs = jasmine.createSpy("saveAs").andCallFake ->
+ error = new Error("EACCES, permission denied '/test/path'")
+ error.path = '/test/path'
+ error.code = 'EACCES'
+ throw error
+
+ confirmations = 0
+ confirm.andCallFake ->
+ confirmations++
+ if confirmations < 3
+ return 0 # save, save as, save as
+ return 2 # don't save
+
+ showSaveDialog.andReturn("new/path")
+
+ pane.close()
+
+ expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
+ expect(confirmations).toBe(3)
+ expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled()
+ expect(item1.save).toHaveBeenCalled()
+ expect(item1.saveAs).toHaveBeenCalled()
+ expect(pane.isDestroyed()).toBe true
+
describe "::destroy()", ->
[container, pane1, pane2] = []
@@ -806,6 +1040,67 @@ describe "Pane", ->
pane2.destroy()
expect(container.root).toBe pane1
+ describe "pending state", ->
+ editor1 = null
+ pane = null
+ eventCount = null
+
+ beforeEach ->
+ waitsForPromise ->
+ atom.workspace.open('sample.txt', pending: true).then (o) ->
+ editor1 = o
+ pane = atom.workspace.getActivePane()
+
+ runs ->
+ eventCount = 0
+ editor1.onDidTerminatePendingState -> eventCount++
+
+ it "does not open file in pending state by default", ->
+ waitsForPromise ->
+ atom.workspace.open('sample.js').then (o) ->
+ editor1 = o
+ pane = atom.workspace.getActivePane()
+
+ runs ->
+ expect(pane.getPendingItem()).toBeNull()
+
+ it "opens file in pending state if 'pending' option is true", ->
+ expect(pane.getPendingItem()).toEqual editor1
+
+ it "terminates pending state if ::terminatePendingState is invoked", ->
+ editor1.terminatePendingState()
+
+ expect(pane.getPendingItem()).toBeNull()
+ expect(eventCount).toBe 1
+
+ it "terminates pending state when buffer is changed", ->
+ editor1.insertText('I\'ll be back!')
+ advanceClock(editor1.getBuffer().stoppedChangingDelay)
+
+ expect(pane.getPendingItem()).toBeNull()
+ expect(eventCount).toBe 1
+
+ it "only calls terminate handler once when text is modified twice", ->
+ editor1.insertText('Some text')
+ advanceClock(editor1.getBuffer().stoppedChangingDelay)
+
+ editor1.save()
+
+ editor1.insertText('More text')
+ advanceClock(editor1.getBuffer().stoppedChangingDelay)
+
+ expect(pane.getPendingItem()).toBeNull()
+ expect(eventCount).toBe 1
+
+ it "only calls clearPendingItem if there is a pending item to clear", ->
+ spyOn(pane, "clearPendingItem").andCallThrough()
+
+ editor1.terminatePendingState()
+ editor1.terminatePendingState()
+
+ expect(pane.getPendingItem()).toBeNull()
+ expect(pane.clearPendingItem.callCount).toBe 1
+
describe "serialization", ->
pane = null
@@ -837,3 +1132,30 @@ describe "Pane", ->
pane.focus()
newPane = Pane.deserialize(pane.serialize(), atom)
expect(newPane.focused).toBe true
+
+ it "can serialize and deserialize the order of the items in the itemStack", ->
+ [item1, item2, item3] = pane.getItems()
+ pane.itemStack = [item3, item1, item2]
+ newPane = Pane.deserialize(pane.serialize(), atom)
+ expect(newPane.itemStack).toEqual pane.itemStack
+ expect(newPane.itemStack[2]).toEqual item2
+
+ it "builds the itemStack if the itemStack is not serialized", ->
+ [item1, item2, item3] = pane.getItems()
+ newPane = Pane.deserialize(pane.serialize(), atom)
+ expect(newPane.getItems()).toEqual newPane.itemStack
+
+ it "rebuilds the itemStack if items.length does not match itemStack.length", ->
+ [item1, item2, item3] = pane.getItems()
+ pane.itemStack = [item2, item3]
+ newPane = Pane.deserialize(pane.serialize(), atom)
+ expect(newPane.getItems()).toEqual newPane.itemStack
+
+ it "does not serialize the reference to the items in the itemStack for pane items that will not be serialized", ->
+ [item1, item2, item3] = pane.getItems()
+ pane.itemStack = [item2, item1, item3]
+ unserializable = {}
+ pane.activateItem(unserializable)
+
+ newPane = Pane.deserialize(pane.serialize(), atom)
+ expect(newPane.itemStack).toEqual [item2, item1, item3]
diff --git a/spec/project-spec.coffee b/spec/project-spec.coffee
index 9d42f9a7e..17fea7360 100644
--- a/spec/project-spec.coffee
+++ b/spec/project-spec.coffee
@@ -21,6 +21,14 @@ describe "Project", ->
afterEach ->
deserializedProject?.destroy()
+ it "does not deserialize paths to non directories", ->
+ deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
+ state = atom.project.serialize()
+ state.paths.push('/directory/that/does/not/exist')
+ state.paths.push(path.join(__dirname, 'fixtures', 'sample.js'))
+ deserializedProject.deserialize(state, atom.deserializers)
+ expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths())
+
it "does not include unretained buffers in the serialized state", ->
waitsForPromise ->
atom.project.bufferForPath('a')
@@ -29,7 +37,7 @@ describe "Project", ->
expect(atom.project.getBuffers().length).toBe 1
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
- deserializedProject.deserialize(atom.project.serialize(), atom.deserializers)
+ deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(deserializedProject.getBuffers().length).toBe 0
it "listens for destroyed events on deserialized buffers and removes them when they are destroyed", ->
@@ -39,7 +47,7 @@ describe "Project", ->
runs ->
expect(atom.project.getBuffers().length).toBe 1
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
- deserializedProject.deserialize(atom.project.serialize(), atom.deserializers)
+ deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(deserializedProject.getBuffers().length).toBe 1
deserializedProject.getBuffers()[0].destroy()
@@ -56,7 +64,7 @@ describe "Project", ->
expect(atom.project.getBuffers().length).toBe 1
fs.mkdirSync(pathToOpen)
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
- deserializedProject.deserialize(atom.project.serialize(), atom.deserializers)
+ deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(deserializedProject.getBuffers().length).toBe 0
it "does not deserialize buffers when their path is inaccessible", ->
@@ -70,9 +78,26 @@ describe "Project", ->
expect(atom.project.getBuffers().length).toBe 1
fs.chmodSync(pathToOpen, '000')
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
- deserializedProject.deserialize(atom.project.serialize(), atom.deserializers)
+ deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
expect(deserializedProject.getBuffers().length).toBe 0
+ it "serializes marker layers only if Atom is quitting", ->
+ waitsForPromise ->
+ atom.workspace.open('a')
+
+ runs ->
+ bufferA = atom.project.getBuffers()[0]
+ layerA = bufferA.addMarkerLayer(persistent: true)
+ markerA = layerA.markPosition([0, 3])
+
+ notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
+ notQuittingProject.deserialize(atom.project.serialize({isUnloading: false}))
+ expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined()
+
+ quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
+ quittingProject.deserialize(atom.project.serialize({isUnloading: true}))
+ expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).not.toBeUndefined()
+
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", ->
tempFile = temp.openSync().path
@@ -501,7 +526,7 @@ describe "Project", ->
expect(atom.project.getDirectories()[1].contains(inputPath)).toBe true
expect(atom.project.relativizePath(inputPath)).toEqual [
atom.project.getPaths()[1],
- 'somewhere/something.txt'
+ path.join('somewhere', 'something.txt')
]
describe ".contains(path)", ->
diff --git a/spec/random-editor-spec.coffee b/spec/random-editor-spec.coffee
index 3924a8412..cada2fe22 100644
--- a/spec/random-editor-spec.coffee
+++ b/spec/random-editor-spec.coffee
@@ -17,7 +17,7 @@ describe "TextEditor", ->
buffer = new TextBuffer
editor = atom.workspace.buildTextEditor({buffer})
editor.setEditorWidthInChars(80)
- tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
+ tokenizedBuffer = editor.tokenizedBuffer
steps = []
times 30, ->
@@ -33,8 +33,8 @@ describe "TextEditor", ->
logLines()
throw new Error("Invalid buffer row #{actualBufferRow} for screen row #{screenRow}", )
- actualScreenLine = editor.tokenizedLineForScreenRow(screenRow)
- unless actualScreenLine.text is referenceScreenLine.text
+ actualScreenLine = editor.lineTextForScreenRow(screenRow)
+ unless actualScreenLine is referenceScreenLine
logLines()
throw new Error("Invalid line text at screen row #{screenRow}")
@@ -84,7 +84,8 @@ describe "TextEditor", ->
referenceEditor.setEditorWidthInChars(80)
referenceEditor.setText(editor.getText())
referenceEditor.setSoftWrapped(editor.isSoftWrapped())
- screenLines = referenceEditor.tokenizedLinesForScreenRows(0, referenceEditor.getLastScreenRow())
+
+ screenLines = [0..referenceEditor.getLastScreenRow()].map (row) => referenceEditor.lineTextForScreenRow(row)
bufferRows = referenceEditor.bufferRowsForScreenRows(0, referenceEditor.getLastScreenRow())
{screenLines, bufferRows}
diff --git a/spec/selection-spec.coffee b/spec/selection-spec.coffee
index ec40e32cc..7511c4b39 100644
--- a/spec/selection-spec.coffee
+++ b/spec/selection-spec.coffee
@@ -83,3 +83,40 @@ describe "Selection", ->
selection.setBufferRange([[2, 0], [2, 10]])
selection.destroy()
expect(selection.marker.isDestroyed()).toBeTruthy()
+
+ describe ".insertText(text, options)", ->
+ it "allows pasting white space only lines when autoIndent is enabled", ->
+ selection.setBufferRange [[0, 0], [0, 0]]
+ selection.insertText(" \n \n\n", autoIndent: true)
+ expect(buffer.lineForRow(0)).toBe " "
+ expect(buffer.lineForRow(1)).toBe " "
+ expect(buffer.lineForRow(2)).toBe ""
+
+ it "auto-indents if only a newline is inserted", ->
+ selection.setBufferRange [[2, 0], [3, 0]]
+ selection.insertText("\n", autoIndent: true)
+ expect(buffer.lineForRow(2)).toBe " "
+
+ it "auto-indents if only a carriage return + newline is inserted", ->
+ selection.setBufferRange [[2, 0], [3, 0]]
+ selection.insertText("\r\n", autoIndent: true)
+ expect(buffer.lineForRow(2)).toBe " "
+
+ describe ".fold()", ->
+ it "folds the buffer range spanned by the selection", ->
+ selection.setBufferRange([[0, 3], [1, 6]])
+ selection.fold()
+
+ expect(selection.getScreenRange()).toEqual([[0, 4], [0, 4]])
+ expect(selection.getBufferRange()).toEqual([[1, 6], [1, 6]])
+ expect(editor.lineTextForScreenRow(0)).toBe "var#{editor.displayLayer.foldCharacter}sort = function(items) {"
+ expect(editor.isFoldedAtBufferRow(0)).toBe(true)
+
+ it "doesn't create a fold when the selection is empty", ->
+ selection.setBufferRange([[0, 3], [0, 3]])
+ selection.fold()
+
+ expect(selection.getScreenRange()).toEqual([[0, 3], [0, 3]])
+ expect(selection.getBufferRange()).toEqual([[0, 3], [0, 3]])
+ expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
+ expect(editor.isFoldedAtBufferRow(0)).toBe(false)
diff --git a/spec/spawner-spec.coffee b/spec/spawner-spec.coffee
new file mode 100644
index 000000000..daa804bbb
--- /dev/null
+++ b/spec/spawner-spec.coffee
@@ -0,0 +1,57 @@
+ChildProcess = require 'child_process'
+Spawner = require '../src/browser/spawner'
+
+describe "Spawner", ->
+ beforeEach ->
+ # Prevent any commands from actually running and affecting the host
+ originalSpawn = ChildProcess.spawn
+
+ harmlessSpawn =
+ # Just spawn something that won't actually modify the host
+ if process.platform is 'win32'
+ originalSpawn('dir')
+ else
+ originalSpawn('ls')
+
+ spyOn(ChildProcess, 'spawn').andCallFake (command, args, callback) ->
+ harmlessSpawn
+
+ it "invokes passed callback", ->
+ someCallback = jasmine.createSpy('someCallback')
+
+ Spawner.spawn('some-command', 'some-args', someCallback)
+
+ waitsFor ->
+ someCallback.callCount is 1
+
+ it "spawns passed command with arguments", ->
+ actualCommand = null
+ actualArgs = null
+
+ # Redefine fake invocation, so to remember passed arguments
+ jasmine.unspy(ChildProcess, 'spawn')
+ spyOn(ChildProcess, 'spawn').andCallFake (command, args) ->
+ actualCommand = command
+ actualArgs = args
+ harmlessSpawn
+
+ expectedCommand = 'some-command'
+ expectedArgs = 'some-args'
+ someCallback = jasmine.createSpy('someCallback')
+
+ Spawner.spawn(expectedCommand, expectedArgs, someCallback)
+
+ expect(actualCommand).toBe expectedCommand
+ expect(actualArgs).toBe expectedArgs
+
+ it "ignores errors by spawned process", ->
+ # Redefine fake invocation, so to cause an error
+ jasmine.unspy(ChildProcess, 'spawn')
+ spyOn(ChildProcess, 'spawn').andCallFake -> throw new Error("EBUSY")
+
+ someCallback = jasmine.createSpy('someCallback')
+
+ expect(Spawner.spawn('some-command', 'some-args', someCallback)).toBe undefined
+
+ waitsFor ->
+ someCallback.callCount is 1
diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee
index 758a232aa..9c4e09da0 100644
--- a/spec/spec-helper.coffee
+++ b/spec/spec-helper.coffee
@@ -112,14 +112,14 @@ afterEach ->
document.getElementById('jasmine-content').innerHTML = '' unless window.debugContent
- ensureNoPathSubscriptions()
+ warnIfLeakingPathSubscriptions()
waits(0) # yield to ui thread to make screen update more frequently
-ensureNoPathSubscriptions = ->
+warnIfLeakingPathSubscriptions = ->
watchedPaths = pathwatcher.getWatchedPaths()
- pathwatcher.closeAllWatchers()
if watchedPaths.length > 0
- throw new Error("Leaking subscriptions for paths: " + watchedPaths.join(", "))
+ console.error("WARNING: Leaking subscriptions for paths: " + watchedPaths.join(", "))
+ pathwatcher.closeAllWatchers()
ensureNoDeprecatedFunctionsCalled = ->
deprecations = Grim.getDeprecations()
@@ -172,8 +172,8 @@ jasmine.useRealClock = ->
addCustomMatchers = (spec) ->
spec.addMatchers
toBeInstanceOf: (expected) ->
- notText = if @isNot then " not" else ""
- this.message = => "Expected #{jasmine.pp(@actual)} to#{notText} be instance of #{expected.name} class"
+ beOrNotBe = if @isNot then "not be" else "be"
+ this.message = => "Expected #{jasmine.pp(@actual)} to #{beOrNotBe} instance of #{expected.name} class"
@actual instanceof expected
toHaveLength: (expected) ->
@@ -181,32 +181,38 @@ addCustomMatchers = (spec) ->
this.message = => "Expected object #{@actual} has no length method"
false
else
- notText = if @isNot then " not" else ""
- this.message = => "Expected object with length #{@actual.length} to#{notText} have length #{expected}"
+ haveOrNotHave = if @isNot then "not have" else "have"
+ this.message = => "Expected object with length #{@actual.length} to #{haveOrNotHave} length #{expected}"
@actual.length is expected
toExistOnDisk: (expected) ->
- notText = this.isNot and " not" or ""
- @message = -> return "Expected path '" + @actual + "'" + notText + " to exist."
+ toOrNotTo = this.isNot and "not to" or "to"
+ @message = -> return "Expected path '#{@actual}' #{toOrNotTo} exist."
fs.existsSync(@actual)
toHaveFocus: ->
- notText = this.isNot and " not" or ""
+ toOrNotTo = this.isNot and "not to" or "to"
if not document.hasFocus()
console.error "Specs will fail because the Dev Tools have focus. To fix this close the Dev Tools or click the spec runner."
- @message = -> return "Expected element '" + @actual + "' or its descendants" + notText + " to have focus."
+ @message = -> return "Expected element '#{@actual}' or its descendants #{toOrNotTo} have focus."
element = @actual
element = element.get(0) if element.jquery
element is document.activeElement or element.contains(document.activeElement)
toShow: ->
- notText = if @isNot then " not" else ""
+ toOrNotTo = this.isNot and "not to" or "to"
element = @actual
element = element.get(0) if element.jquery
- @message = -> return "Expected element '#{element}' or its descendants#{notText} to show."
+ @message = -> return "Expected element '#{element}' or its descendants #{toOrNotTo} show."
element.style.display in ['block', 'inline-block', 'static', 'fixed']
+ toEqualPath: (expected) ->
+ actualPath = path.normalize(@actual)
+ expectedPath = path.normalize(expected)
+ @message = -> return "Expected path '#{actualPath}' to be equal to '#{expectedPath}'."
+ actualPath is expectedPath
+
window.waitsForPromise = (args...) ->
label = null
if args.length > 1
diff --git a/spec/squirrel-update-spec.coffee b/spec/squirrel-update-spec.coffee
index b71a9b0c4..4a6936a50 100644
--- a/spec/squirrel-update-spec.coffee
+++ b/spec/squirrel-update-spec.coffee
@@ -1,39 +1,37 @@
-ChildProcess = require 'child_process'
{EventEmitter} = require 'events'
fs = require 'fs-plus'
path = require 'path'
temp = require 'temp'
SquirrelUpdate = require '../src/browser/squirrel-update'
+Spawner = require '../src/browser/spawner'
+WinPowerShell = require '../src/browser/win-powershell'
+WinRegistry = require '../src/browser/win-registry'
-describe "Windows squirrel updates", ->
+# Run passed callback as Spawner.spawn() would do
+invokeCallback = (callback) ->
+ error = null
+ stdout = ''
+ callback?(error, stdout)
+
+describe "Windows Squirrel Update", ->
tempHomeDirectory = null
beforeEach ->
- # Prevent the actually home directory from being manipulated
+ # Prevent the actual home directory from being manipulated
tempHomeDirectory = temp.mkdirSync('atom-temp-home-')
spyOn(fs, 'getHomeDirectory').andReturn(tempHomeDirectory)
- # Prevent any commands from actually running and affecting the host
- originalSpawn = ChildProcess.spawn
- spyOn(ChildProcess, 'spawn').andCallFake (command, args) ->
- if path.basename(command) is 'Update.exe' and args?[0] is '--createShortcut'
- fs.writeFileSync(path.join(tempHomeDirectory, 'Desktop', 'Atom.lnk'), '')
+ # Prevent any spawned command from actually running and affecting the host
+ spyOn(Spawner, 'spawn').andCallFake (command, args, callback) ->
+ # do nothing on command, just run passed callback
+ invokeCallback callback
- # Just spawn something that won't actually modify the host
- if process.platform is 'win32'
- originalSpawn('dir')
- else
- originalSpawn('ls')
-
- it "ignores errors spawning Squirrel", ->
- jasmine.unspy(ChildProcess, 'spawn')
- spyOn(ChildProcess, 'spawn').andCallFake -> throw new Error("EBUSY")
-
- app = quit: jasmine.createSpy('quit')
- expect(SquirrelUpdate.handleStartupEvent(app, '--squirrel-install')).toBe true
-
- waitsFor ->
- app.quit.callCount is 1
+ # Prevent any actual change to Windows registry
+ for own method of WinRegistry
+ # all WinRegistry APIs share the same signature
+ spyOn(WinRegistry, method).andCallFake (callback) ->
+ # do nothing on registry, just run passed callback
+ invokeCallback callback
it "quits the app on all squirrel events", ->
app = quit: jasmine.createSpy('quit')
@@ -67,28 +65,56 @@ describe "Windows squirrel updates", ->
runs ->
expect(SquirrelUpdate.handleStartupEvent(app, '--not-squirrel')).toBe false
- it "keeps the desktop shortcut deleted on updates if it was previously deleted after install", ->
- desktopShortcutPath = path.join(tempHomeDirectory, 'Desktop', 'Atom.lnk')
- expect(fs.existsSync(desktopShortcutPath)).toBe false
+ describe "Desktop shortcut", ->
+ desktopShortcutPath = '/non/existing/path'
- app = quit: jasmine.createSpy('quit')
- expect(SquirrelUpdate.handleStartupEvent(app, '--squirrel-install')).toBe true
+ beforeEach ->
+ desktopShortcutPath = path.join(tempHomeDirectory, 'Desktop', 'Atom.lnk')
+
+ jasmine.unspy(Spawner, 'spawn')
+ spyOn(Spawner, 'spawn').andCallFake (command, args, callback) ->
+ if path.basename(command) is 'Update.exe' and args?[0] is '--createShortcut'
+ fs.writeFileSync(desktopShortcutPath, '')
+ else
+ # simply ignore other commands
+
+ invokeCallback callback
- waitsFor ->
- app.quit.callCount is 1
-
- runs ->
- app.quit.reset()
- expect(fs.existsSync(desktopShortcutPath)).toBe true
- fs.removeSync(desktopShortcutPath)
+ it "does not exist before install", ->
expect(fs.existsSync(desktopShortcutPath)).toBe false
- expect(SquirrelUpdate.handleStartupEvent(app, '--squirrel-updated')).toBe true
- waitsFor ->
- app.quit.callCount is 1
+ describe "on install", ->
+ beforeEach ->
+ app = quit: jasmine.createSpy('quit')
+ SquirrelUpdate.handleStartupEvent(app, '--squirrel-install')
+ waitsFor ->
+ app.quit.callCount is 1
- runs ->
- expect(fs.existsSync(desktopShortcutPath)).toBe false
+ it "creates desktop shortcut", ->
+ expect(fs.existsSync(desktopShortcutPath)).toBe true
+
+ describe "when shortcut is deleted and then app is updated", ->
+ beforeEach ->
+ fs.removeSync(desktopShortcutPath)
+ expect(fs.existsSync(desktopShortcutPath)).toBe false
+
+ app = quit: jasmine.createSpy('quit')
+ SquirrelUpdate.handleStartupEvent(app, '--squirrel-updated')
+ waitsFor ->
+ app.quit.callCount is 1
+
+ it "does not recreate shortcut", ->
+ expect(fs.existsSync(desktopShortcutPath)).toBe false
+
+ describe "when shortcut is kept and app is updated", ->
+ beforeEach ->
+ app = quit: jasmine.createSpy('quit')
+ SquirrelUpdate.handleStartupEvent(app, '--squirrel-updated')
+ waitsFor ->
+ app.quit.callCount is 1
+
+ it "still has desktop shortcut", ->
+ expect(fs.existsSync(desktopShortcutPath)).toBe true
describe ".restartAtom", ->
it "quits the app and spawns a new one", ->
@@ -98,7 +124,7 @@ describe "Windows squirrel updates", ->
SquirrelUpdate.restartAtom(app)
expect(app.quit.callCount).toBe 1
- expect(ChildProcess.spawn.callCount).toBe 0
+ expect(Spawner.spawn.callCount).toBe 0
app.emit('will-quit')
- expect(ChildProcess.spawn.callCount).toBe 1
- expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'atom.cmd'
+ expect(Spawner.spawn.callCount).toBe 1
+ expect(path.basename(Spawner.spawn.argsForCall[0][0])).toBe 'atom.cmd'
diff --git a/spec/state-store-spec.js b/spec/state-store-spec.js
new file mode 100644
index 000000000..95fdcb71b
--- /dev/null
+++ b/spec/state-store-spec.js
@@ -0,0 +1,61 @@
+/** @babel */
+import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers'
+
+const StateStore = require('../src/state-store.js')
+
+describe("StateStore", () => {
+ let databaseName = `test-database-${Date.now()}`
+ let version = 1
+
+ it("can save and load states", () => {
+ const store = new StateStore(databaseName, version)
+ return store.save('key', {foo:'bar'})
+ .then(() => store.load('key'))
+ .then((state) => {
+ expect(state).toEqual({foo:'bar'})
+ })
+ })
+
+ it("resolves with null when a non-existent key is loaded", () => {
+ const store = new StateStore(databaseName, version)
+ return store.load('no-such-key').then((value) => {
+ expect(value).toBeNull()
+ })
+ })
+
+ it("can clear the state object store", () => {
+ const store = new StateStore(databaseName, version)
+ return store.save('key', {foo:'bar'})
+ .then(() => store.count())
+ .then((count) =>
+ expect(count).toBe(1)
+ )
+ .then(() => store.clear())
+ .then(() => store.count())
+ .then((count) => {
+ expect(count).toBe(0)
+ })
+ })
+
+ describe("when there is an error reading from the database", () => {
+ it("rejects the promise returned by load", () => {
+ const store = new StateStore(databaseName, version)
+
+ const fakeErrorEvent = {target: {errorCode: "Something bad happened"}}
+
+ spyOn(IDBObjectStore.prototype, 'get').andCallFake((key) => {
+ let request = {}
+ process.nextTick(() => request.onerror(fakeErrorEvent))
+ return request
+ })
+
+ return store.load('nonexistentKey')
+ .then(() => {
+ throw new Error("Promise should have been rejected")
+ })
+ .catch((event) => {
+ expect(event).toBe(fakeErrorEvent)
+ })
+ })
+ })
+})
diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js
index 9663c6222..83f5d1b80 100644
--- a/spec/text-editor-component-spec.js
+++ b/spec/text-editor-component-spec.js
@@ -1,6 +1,6 @@
/** @babel */
-import {it, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers'
+import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers'
import TextEditorElement from '../src/text-editor-element'
import _, {extend, flatten, last, toArray} from 'underscore-plus'
@@ -69,13 +69,12 @@ describe('TextEditorComponent', function () {
describe('line rendering', async function () {
function expectTileContainsRow (tileNode, screenRow, {top}) {
let lineNode = tileNode.querySelector('[data-screen-row="' + screenRow + '"]')
- let tokenizedLine = editor.tokenizedLineForScreenRow(screenRow)
-
+ let text = editor.lineTextForScreenRow(screenRow)
expect(lineNode.offsetTop).toBe(top)
- if (tokenizedLine.text === '') {
- expect(lineNode.innerHTML).toBe(' ')
+ if (text === '') {
+ expect(lineNode.textContent).toBe(' ')
} else {
- expect(lineNode.textContent).toBe(tokenizedLine.text)
+ expect(lineNode.textContent).toBe(text)
}
}
@@ -294,12 +293,12 @@ describe('TextEditorComponent', function () {
await nextViewUpdatePromise()
- expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text)
+ expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.lineTextForScreenRow(3))
buffer.delete([[0, 0], [3, 0]])
await nextViewUpdatePromise()
- expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.tokenizedLineForScreenRow(3).text)
+ expect(component.lineNodeForScreenRow(3).textContent).toBe(editor.lineTextForScreenRow(3))
})
it('updates the top position of lines when the line height changes', async function () {
@@ -361,9 +360,9 @@ describe('TextEditorComponent', function () {
}
})
- it('renders an nbsp on empty lines when no line-ending character is defined', function () {
+ it('renders an placeholder space on empty lines when no line-ending character is defined', function () {
atom.config.set('editor.showInvisibles', false)
- expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP)
+ expect(component.lineNodeForScreenRow(10).textContent).toBe(' ')
})
it('gives the lines and tiles divs the same background color as the editor to improve GPU performance', async function () {
@@ -429,13 +428,14 @@ describe('TextEditorComponent', function () {
expect(leafNodes[0].classList.contains('leading-whitespace')).toBe(false)
})
- it('keeps rebuilding lines when continuous reflow is on', function () {
+ it('keeps rebuilding lines when continuous reflow is on', async function () {
wrapperNode.setContinuousReflow(true)
- let oldLineNode = componentNode.querySelector('.line')
+ let oldLineNode = componentNode.querySelectorAll('.line')[1]
- waitsFor(function () {
- return componentNode.querySelector('.line') !== oldLineNode
- })
+ while (true) {
+ await nextViewUpdatePromise()
+ if (componentNode.querySelectorAll('.line')[1] !== oldLineNode) break
+ }
})
describe('when showInvisibles is enabled', function () {
@@ -484,7 +484,7 @@ describe('TextEditorComponent', function () {
it('displays newlines as their own token outside of the other tokens\' scopeDescriptor', async function () {
editor.setText('let\n')
await nextViewUpdatePromise()
- expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '')
+ expect(component.lineNodeForScreenRow(0).innerHTML).toBe('let' + invisibles.eol + '')
})
it('displays trailing carriage returns using a visible, non-empty value', async function () {
@@ -497,20 +497,20 @@ describe('TextEditorComponent', function () {
expect(component.lineNodeForScreenRow(10).textContent).toBe(invisibles.eol)
})
- it('renders an nbsp on empty lines when the line-ending character is an empty string', async function () {
+ it('renders a placeholder space on empty lines when the line-ending character is an empty string', async function () {
atom.config.set('editor.invisibles', {
eol: ''
})
await nextViewUpdatePromise()
- expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP)
+ expect(component.lineNodeForScreenRow(10).textContent).toBe(' ')
})
- it('renders an nbsp on empty lines when the line-ending character is false', async function () {
+ it('renders an placeholder space on empty lines when the line-ending character is false', async function () {
atom.config.set('editor.invisibles', {
eol: false
})
await nextViewUpdatePromise()
- expect(component.lineNodeForScreenRow(10).textContent).toBe(NBSP)
+ expect(component.lineNodeForScreenRow(10).textContent).toBe(' ')
})
it('interleaves invisible line-ending characters with indent guides on empty lines', async function () {
@@ -518,24 +518,25 @@ describe('TextEditorComponent', function () {
await nextViewUpdatePromise()
+ editor.setTabLength(2)
editor.setTextInBufferRange([[10, 0], [11, 0]], '\r\n', {
normalizeLineEndings: false
})
await nextViewUpdatePromise()
+ expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
- expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
editor.setTabLength(3)
await nextViewUpdatePromise()
+ expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
- expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE ')
editor.setTabLength(1)
await nextViewUpdatePromise()
+ expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
- expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
editor.setTextInBufferRange([[9, 0], [9, Infinity]], ' ')
editor.setTextInBufferRange([[11, 0], [11, Infinity]], ' ')
await nextViewUpdatePromise()
- expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
+ expect(component.lineNodeForScreenRow(10).innerHTML).toBe('CE')
})
describe('when soft wrapping is enabled', function () {
@@ -550,8 +551,8 @@ describe('TextEditorComponent', function () {
})
it('does not show end of line invisibles at the end of wrapped lines', function () {
- expect(component.lineNodeForScreenRow(0).textContent).toBe('a line that ')
- expect(component.lineNodeForScreenRow(1).textContent).toBe('wraps' + invisibles.space + invisibles.eol)
+ expect(component.lineNodeForScreenRow(0).textContent).toBe('a line ')
+ expect(component.lineNodeForScreenRow(1).textContent).toBe('that wraps' + invisibles.space + invisibles.eol)
})
})
})
@@ -986,13 +987,14 @@ describe('TextEditorComponent', function () {
expect(component.lineNumberNodeForScreenRow(3) != null).toBe(true)
})
- it('keeps rebuilding line numbers when continuous reflow is on', function () {
+ it('keeps rebuilding line numbers when continuous reflow is on', async function () {
wrapperNode.setContinuousReflow(true)
let oldLineNode = componentNode.querySelectorAll('.line-number')[1]
- waitsFor(function () {
- return componentNode.querySelectorAll('.line-number')[1] !== oldLineNode
- })
+ while (true) {
+ await nextViewUpdatePromise()
+ if (componentNode.querySelectorAll('.line-number')[1] !== oldLineNode) break
+ }
})
describe('fold decorations', function () {
@@ -1051,7 +1053,7 @@ describe('TextEditorComponent', function () {
beforeEach(async function () {
editor.setSoftWrapped(true)
await nextViewUpdatePromise()
- componentNode.style.width = 16 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
+ componentNode.style.width = 20 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
component.measureDimensions()
await nextViewUpdatePromise()
})
@@ -1060,6 +1062,14 @@ describe('TextEditorComponent', function () {
expect(lineNumberHasClass(0, 'foldable')).toBe(true)
expect(lineNumberHasClass(1, 'foldable')).toBe(false)
})
+
+ it('does not add the folded class for soft-wrapped lines that contain a fold', async function () {
+ editor.foldBufferRange([[3, 19], [3, 21]])
+ await nextViewUpdatePromise()
+
+ expect(lineNumberHasClass(11, 'folded')).toBe(true)
+ expect(lineNumberHasClass(12, 'folded')).toBe(false)
+ })
})
})
@@ -1082,7 +1092,7 @@ describe('TextEditorComponent', function () {
component.destroy()
lineNumber = component.lineNumberNodeForScreenRow(1)
target = lineNumber.querySelector('.icon-right')
- return target.dispatchEvent(buildClickEvent(target))
+ target.dispatchEvent(buildClickEvent(target))
})
})
@@ -1106,6 +1116,37 @@ describe('TextEditorComponent', function () {
expect(lineNumberHasClass(1, 'folded')).toBe(false)
})
+ it('unfolds all the free-form folds intersecting the buffer row when clicked', async function () {
+ expect(lineNumberHasClass(3, 'foldable')).toBe(false)
+
+ editor.foldBufferRange([[3, 4], [5, 4]])
+ editor.foldBufferRange([[5, 5], [8, 10]])
+ await nextViewUpdatePromise()
+ expect(lineNumberHasClass(3, 'folded')).toBe(true)
+ expect(lineNumberHasClass(5, 'folded')).toBe(false)
+
+ let lineNumber = component.lineNumberNodeForScreenRow(3)
+ let target = lineNumber.querySelector('.icon-right')
+ target.dispatchEvent(buildClickEvent(target))
+ await nextViewUpdatePromise()
+ expect(lineNumberHasClass(3, 'folded')).toBe(false)
+ expect(lineNumberHasClass(5, 'folded')).toBe(true)
+
+ editor.setSoftWrapped(true)
+ componentNode.style.width = 20 * charWidth + wrapperNode.getVerticalScrollbarWidth() + 'px'
+ component.measureDimensions()
+ await nextViewUpdatePromise()
+ editor.foldBufferRange([[3, 19], [3, 21]]) // fold starting on a soft-wrapped portion of the line
+ await nextViewUpdatePromise()
+ expect(lineNumberHasClass(11, 'folded')).toBe(true)
+
+ lineNumber = component.lineNumberNodeForScreenRow(11)
+ target = lineNumber.querySelector('.icon-right')
+ target.dispatchEvent(buildClickEvent(target))
+ await nextViewUpdatePromise()
+ expect(lineNumberHasClass(11, 'folded')).toBe(false)
+ })
+
it('does not fold when the line number componentNode is clicked', function () {
let lineNumber = component.lineNumberNodeForScreenRow(1)
lineNumber.dispatchEvent(buildClickEvent(lineNumber))
@@ -1200,7 +1241,7 @@ describe('TextEditorComponent', function () {
let cursor = componentNode.querySelector('.cursor')
let cursorRect = cursor.getBoundingClientRect()
let cursorLocationTextNode = component.lineNodeForScreenRow(0).querySelector('.source.js').childNodes[2]
- let range = document.createRange()
+ let range = document.createRange(cursorLocationTextNode)
range.setStart(cursorLocationTextNode, 0)
range.setEnd(cursorLocationTextNode, 1)
let rangeRect = range.getBoundingClientRect()
@@ -1208,6 +1249,17 @@ describe('TextEditorComponent', function () {
expect(cursorRect.width).toBeCloseTo(rangeRect.width, 0)
})
+ it('positions cursors after the fold-marker when a fold ends the line', async function () {
+ editor.foldBufferRow(0)
+ await nextViewUpdatePromise()
+ editor.setCursorScreenPosition([0, 30])
+ await nextViewUpdatePromise()
+
+ let cursorRect = componentNode.querySelector('.cursor').getBoundingClientRect()
+ let foldMarkerRect = componentNode.querySelector('.fold-marker').getBoundingClientRect()
+ expect(cursorRect.left).toBeCloseTo(foldMarkerRect.right, 0)
+ })
+
it('positions cursors correctly after character widths are changed via a stylesheet change', async function () {
atom.config.set('editor.fontFamily', 'sans-serif')
editor.setCursorScreenPosition([0, 16])
@@ -1475,7 +1527,7 @@ describe('TextEditorComponent', function () {
component.measureDimensions()
await nextViewUpdatePromise()
- let marker2 = editor.displayBuffer.markBufferRange([[9, 0], [9, 0]])
+ let marker2 = editor.markBufferRange([[9, 0], [9, 0]])
editor.decorateMarker(marker2, {
type: ['line-number', 'line'],
'class': 'b'
@@ -1840,17 +1892,22 @@ describe('TextEditorComponent', function () {
expect(component.lineNodeForScreenRow(2).dataset.screenRow).toBe("2")
})
- it('measures block decorations taking into account both top and bottom margins', async function () {
+ it('measures block decorations taking into account both top and bottom margins of the element and its children', async function () {
let [item, blockDecoration] = createBlockDecorationBeforeScreenRow(0, {className: "decoration-1"})
+ let child = document.createElement("div")
+ child.style.height = "7px"
+ child.style.width = "30px"
+ child.style.marginBottom = "20px"
+ item.appendChild(child)
atom.styles.addStyleSheet(
- 'atom-text-editor .decoration-1 { width: 30px; height: 30px; margin-top: 10px; margin-bottom: 5px; }',
+ 'atom-text-editor .decoration-1 { width: 30px; margin-top: 10px; }',
{context: 'atom-text-editor'}
)
await nextAnimationFramePromise() // causes the DOM to update and to retrieve new styles
await nextAnimationFramePromise() // applies the changes
- expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 30 + 10 + 5 + "px")
+ expect(component.tileNodesForLines()[0].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + 10 + 7 + 20 + "px")
expect(component.tileNodesForLines()[0].style.webkitTransform).toBe("translate3d(0px, 0px, 0px)")
expect(component.tileNodesForLines()[1].style.height).toBe(TILE_SIZE * editor.getLineHeightInPixels() + "px")
expect(component.tileNodesForLines()[1].style.webkitTransform).toBe(`translate3d(0px, ${component.tileNodesForLines()[0].offsetHeight}px, 0px)`)
@@ -1882,7 +1939,7 @@ describe('TextEditorComponent', function () {
component.measureDimensions()
await nextViewUpdatePromise()
- marker = editor.displayBuffer.markBufferRange([[9, 2], [9, 4]], {
+ marker = editor.markBufferRange([[9, 2], [9, 4]], {
invalidate: 'inside'
})
editor.decorateMarker(marker, {
@@ -2077,7 +2134,7 @@ describe('TextEditorComponent', function () {
describe('when the marker is empty', function () {
it('renders an overlay decoration when added and removes the overlay when the decoration is destroyed', async function () {
- let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], {
+ let marker = editor.markBufferRange([[2, 13], [2, 13]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@@ -2099,7 +2156,7 @@ describe('TextEditorComponent', function () {
})
it('renders the overlay element with the CSS class specified by the decoration', async function () {
- let marker = editor.displayBuffer.markBufferRange([[2, 13], [2, 13]], {
+ let marker = editor.markBufferRange([[2, 13], [2, 13]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@@ -2120,7 +2177,7 @@ describe('TextEditorComponent', function () {
describe('when the marker is not empty', function () {
it('renders at the head of the marker by default', async function () {
- let marker = editor.displayBuffer.markBufferRange([[2, 5], [2, 10]], {
+ let marker = editor.markBufferRange([[2, 5], [2, 10]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@@ -2151,10 +2208,11 @@ describe('TextEditorComponent', function () {
item.style.height = itemHeight + 'px'
wrapperNode.style.width = windowWidth + 'px'
wrapperNode.style.height = windowHeight + 'px'
- atom.setWindowDimensions({
+ await atom.setWindowDimensions({
width: windowWidth,
height: windowHeight
})
+
component.measureDimensions()
component.measureWindowSize()
await nextViewUpdatePromise()
@@ -2165,7 +2223,7 @@ describe('TextEditorComponent', function () {
})
it('slides horizontally left when near the right edge on #win32 and #darwin', async function () {
- let marker = editor.displayBuffer.markBufferRange([[0, 26], [0, 26]], {
+ let marker = editor.markBufferRange([[0, 26], [0, 26]], {
invalidate: 'never'
})
let decoration = editor.decorateMarker(marker, {
@@ -2747,20 +2805,60 @@ describe('TextEditorComponent', function () {
})
})
- describe('when a line is folded', function () {
- beforeEach(async function () {
- editor.foldBufferRow(4)
+ describe('when a fold marker is clicked', function () {
+ function clickElementAtPosition (marker, position) {
+ linesNode.dispatchEvent(
+ buildMouseEvent('mousedown', clientCoordinatesForScreenPosition(position), {target: marker})
+ )
+ }
+
+ it('unfolds only the selected fold when other folds are on the same line', async function () {
+ editor.foldBufferRange([[4, 6], [4, 10]])
+ editor.foldBufferRange([[4, 15], [4, 20]])
await nextViewUpdatePromise()
+ let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
+ expect(foldMarkers.length).toBe(2)
+ expect(editor.isFoldedAtBufferRow(4)).toBe(true)
+
+ clickElementAtPosition(foldMarkers[0], [4, 6])
+ await nextViewUpdatePromise()
+ foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
+ expect(foldMarkers.length).toBe(1)
+ expect(editor.isFoldedAtBufferRow(4)).toBe(true)
+
+ clickElementAtPosition(foldMarkers[0], [4, 15])
+ await nextViewUpdatePromise()
+ foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
+ expect(foldMarkers.length).toBe(0)
+ expect(editor.isFoldedAtBufferRow(4)).toBe(false)
})
- describe('when the folded line\'s fold-marker is clicked', function () {
- it('unfolds the buffer row', function () {
- let target = component.lineNodeForScreenRow(4).querySelector('.fold-marker')
- linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8]), {
- target: target
- }))
- expect(editor.isFoldedAtBufferRow(4)).toBe(false)
- })
+ it('unfolds only the selected fold when other folds are inside it', async function () {
+ editor.foldBufferRange([[4, 10], [4, 15]])
+ editor.foldBufferRange([[4, 4], [4, 5]])
+ editor.foldBufferRange([[4, 4], [4, 20]])
+ await nextViewUpdatePromise()
+ let foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
+ expect(foldMarkers.length).toBe(1)
+ expect(editor.isFoldedAtBufferRow(4)).toBe(true)
+
+ clickElementAtPosition(foldMarkers[0], [4, 4])
+ await nextViewUpdatePromise()
+ foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
+ expect(foldMarkers.length).toBe(1)
+ expect(editor.isFoldedAtBufferRow(4)).toBe(true)
+
+ clickElementAtPosition(foldMarkers[0], [4, 4])
+ await nextViewUpdatePromise()
+ foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
+ expect(foldMarkers.length).toBe(1)
+ expect(editor.isFoldedAtBufferRow(4)).toBe(true)
+
+ clickElementAtPosition(foldMarkers[0], [4, 10])
+ await nextViewUpdatePromise()
+ foldMarkers = component.lineNodeForScreenRow(4).querySelectorAll('.fold-marker')
+ expect(foldMarkers.length).toBe(0)
+ expect(editor.isFoldedAtBufferRow(4)).toBe(false)
})
})
@@ -3095,7 +3193,7 @@ describe('TextEditorComponent', function () {
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(11), {
shiftKey: true
}))
- expect(editor.getSelectedScreenRange()).toEqual([[7, 4], [16, 0]])
+ expect(editor.getSelectedScreenRange()).toEqual([[7, 4], [17, 0]])
})
})
})
@@ -3169,7 +3267,7 @@ describe('TextEditorComponent', function () {
gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(11), {
metaKey: true
}))
- expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[11, 4], [19, 0]]])
+ expect(editor.getSelectedScreenRanges()).toEqual([[[7, 4], [7, 6]], [[11, 4], [20, 0]]])
})
it('merges overlapping selections on mouseup', async function () {
@@ -3183,7 +3281,7 @@ describe('TextEditorComponent', function () {
gutterNode.dispatchEvent(buildMouseEvent('mouseup', clientCoordinatesForScreenRowInGutter(5), {
metaKey: true
}))
- expect(editor.getSelectedScreenRanges()).toEqual([[[5, 0], [19, 0]]])
+ expect(editor.getSelectedScreenRanges()).toEqual([[[5, 0], [20, 0]]])
})
})
})
@@ -3198,7 +3296,7 @@ describe('TextEditorComponent', function () {
}))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(11)))
await nextAnimationFramePromise()
- expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [11, 14]])
+ expect(editor.getSelectedScreenRange()).toEqual([[1, 4], [11, 5]])
})
})
@@ -3739,6 +3837,21 @@ describe('TextEditorComponent', function () {
return event
}
+ function buildKeydownEvent ({keyCode, target}) {
+ let event = new KeyboardEvent('keydown')
+ Object.defineProperty(event, 'keyCode', {
+ get: function () {
+ return keyCode
+ }
+ })
+ Object.defineProperty(event, 'target', {
+ get: function () {
+ return target
+ }
+ })
+ return event
+ }
+
let inputNode
beforeEach(function () {
@@ -3761,11 +3874,12 @@ describe('TextEditorComponent', function () {
expect(editor.lineTextForBufferRow(0)).toBe('xyvar quicksort = function () {')
})
- it('replaces the last character if the length of the input\'s value does not increase, as occurs with the accented character menu', async function () {
- componentNode.dispatchEvent(buildTextInputEvent({
- data: 'u',
- target: inputNode
- }))
+ it('replaces the last character if a keypress event is bracketed by keydown events with matching keyCodes, which occurs when the accented character menu is shown', async function () {
+ componentNode.dispatchEvent(buildKeydownEvent({keyCode: 85, target: inputNode}))
+ componentNode.dispatchEvent(buildTextInputEvent({data: 'u', target: inputNode}))
+ componentNode.dispatchEvent(new KeyboardEvent('keypress'))
+ componentNode.dispatchEvent(buildKeydownEvent({keyCode: 85, target: inputNode}))
+ componentNode.dispatchEvent(new KeyboardEvent('keyup'))
await nextViewUpdatePromise()
expect(editor.lineTextForBufferRow(0)).toBe('uvar quicksort = function () {')
@@ -4000,15 +4114,33 @@ describe('TextEditorComponent', function () {
})
})
- describe('when changing the font', async function () {
- it('measures the default char, the korean char, the double width char and the half width char widths', async function () {
- expect(editor.getDefaultCharWidth()).toBeCloseTo(12, 0)
+ describe('when decreasing the fontSize', async function () {
+ it('decreases the widths of the korean char, the double width char and the half width char', async function () {
+ originalDefaultCharWidth = editor.getDefaultCharWidth()
+ koreanDefaultCharWidth = editor.getKoreanCharWidth()
+ doubleWidthDefaultCharWidth = editor.getDoubleWidthCharWidth()
+ halfWidthDefaultCharWidth = editor.getHalfWidthCharWidth()
component.setFontSize(10)
await nextViewUpdatePromise()
- expect(editor.getDefaultCharWidth()).toBeCloseTo(6, 0)
- expect(editor.getKoreanCharWidth()).toBeCloseTo(9, 0)
- expect(editor.getDoubleWidthCharWidth()).toBe(10)
- expect(editor.getHalfWidthCharWidth()).toBe(5)
+ expect(editor.getDefaultCharWidth()).toBeLessThan(originalDefaultCharWidth)
+ expect(editor.getKoreanCharWidth()).toBeLessThan(koreanDefaultCharWidth)
+ expect(editor.getDoubleWidthCharWidth()).toBeLessThan(doubleWidthDefaultCharWidth)
+ expect(editor.getHalfWidthCharWidth()).toBeLessThan(halfWidthDefaultCharWidth)
+ })
+ })
+
+ describe('when increasing the fontSize', function() {
+ it('increases the widths of the korean char, the double width char and the half width char', async function () {
+ originalDefaultCharWidth = editor.getDefaultCharWidth()
+ koreanDefaultCharWidth = editor.getKoreanCharWidth()
+ doubleWidthDefaultCharWidth = editor.getDoubleWidthCharWidth()
+ halfWidthDefaultCharWidth = editor.getHalfWidthCharWidth()
+ component.setFontSize(25)
+ await nextViewUpdatePromise()
+ expect(editor.getDefaultCharWidth()).toBeGreaterThan(originalDefaultCharWidth)
+ expect(editor.getKoreanCharWidth()).toBeGreaterThan(koreanDefaultCharWidth)
+ expect(editor.getDoubleWidthCharWidth()).toBeGreaterThan(doubleWidthDefaultCharWidth)
+ expect(editor.getHalfWidthCharWidth()).toBeGreaterThan(halfWidthDefaultCharWidth)
})
})
@@ -4834,7 +4966,7 @@ describe('TextEditorComponent', function () {
it('pastes the previously selected text at the clicked location', async function () {
let clipboardWrittenTo = false
- spyOn(require('ipc'), 'send').andCallFake(function (eventName, selectedText) {
+ spyOn(require('electron').ipcRenderer, 'send').andCallFake(function (eventName, selectedText) {
if (eventName === 'write-text-to-selection-clipboard') {
require('../src/safe-clipboard').writeText(selectedText, 'selection')
clipboardWrittenTo = true
@@ -4926,7 +5058,7 @@ describe('TextEditorComponent', function () {
function lineNumberForBufferRowHasClass (bufferRow, klass) {
let screenRow
- screenRow = editor.displayBuffer.screenRowForBufferRow(bufferRow)
+ screenRow = editor.screenRowForBufferRow(bufferRow)
return component.lineNumberNodeForScreenRow(screenRow).classList.contains(klass)
}
diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee
index 9253ff103..c770c74c1 100644
--- a/spec/text-editor-presenter-spec.coffee
+++ b/spec/text-editor-presenter-spec.coffee
@@ -91,11 +91,13 @@ describe "TextEditorPresenter", ->
expectNoStateUpdate = (presenter, fn) -> expectStateUpdatedToBe(false, presenter, fn)
waitsForStateToUpdate = (presenter, fn) ->
- waitsFor "presenter state to update", 1000, (done) ->
- fn?()
+ line = new Error().stack.split('\n')[2].split(':')[1]
+
+ waitsFor "presenter state to update at line #{line}", 1000, (done) ->
disposable = presenter.onDidUpdateState ->
disposable.dispose()
process.nextTick(done)
+ fn?()
tiledContentContract = (stateFn) ->
it "contains states for tiles that are visible on screen", ->
@@ -633,16 +635,28 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> presenter.setExplicitHeight(500)
expect(getState(presenter).verticalScrollbar.scrollHeight).toBe 500
- it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", ->
- presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10)
- expectStateUpdate presenter, -> presenter.setScrollTop(300)
- expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
+ describe "scrollPastEnd", ->
+ it "adds the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true", ->
+ presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10)
+ expectStateUpdate presenter, -> presenter.setScrollTop(300)
+ expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
- expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true)
- expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3)
+ expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true)
+ expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight + presenter.clientHeight - (presenter.lineHeight * 3)
- expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false)
- expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
+ expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false)
+ expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
+
+ it "doesn't add the computed clientHeight to the computed scrollHeight if editor.scrollPastEnd is true but the presenter is created with scrollPastEnd as false", ->
+ presenter = buildPresenter(scrollTop: 10, explicitHeight: 50, horizontalScrollbarHeight: 10, scrollPastEnd: false)
+ expectStateUpdate presenter, -> presenter.setScrollTop(300)
+ expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
+
+ expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", true)
+ expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
+
+ expectStateUpdate presenter, -> atom.config.set("editor.scrollPastEnd", false)
+ expect(getState(presenter).verticalScrollbar.scrollHeight).toBe presenter.contentHeight
describe ".scrollTop", ->
it "tracks the value of ::scrollTop", ->
@@ -1129,53 +1143,6 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> presenter.setScrollLeft(-300)
expect(getState(presenter).content.scrollLeft).toBe 0
- describe ".indentGuidesVisible", ->
- it "is initialized based on the editor.showIndentGuide config setting", ->
- presenter = buildPresenter()
- expect(getState(presenter).content.indentGuidesVisible).toBe false
-
- atom.config.set('editor.showIndentGuide', true)
- presenter = buildPresenter()
- expect(getState(presenter).content.indentGuidesVisible).toBe true
-
- it "updates when the editor.showIndentGuide config setting changes", ->
- presenter = buildPresenter()
- expect(getState(presenter).content.indentGuidesVisible).toBe false
-
- expectStateUpdate presenter, -> atom.config.set('editor.showIndentGuide', true)
- expect(getState(presenter).content.indentGuidesVisible).toBe true
-
- expectStateUpdate presenter, -> atom.config.set('editor.showIndentGuide', false)
- expect(getState(presenter).content.indentGuidesVisible).toBe false
-
- it "updates when the editor's grammar changes", ->
- atom.config.set('editor.showIndentGuide', true, scopeSelector: ".source.js")
-
- presenter = buildPresenter()
- expect(getState(presenter).content.indentGuidesVisible).toBe false
-
- stateUpdated = false
- presenter.onDidUpdateState -> stateUpdated = true
-
- waitsForPromise -> atom.packages.activatePackage('language-javascript')
-
- runs ->
- expect(stateUpdated).toBe true
- expect(getState(presenter).content.indentGuidesVisible).toBe true
-
- expectStateUpdate presenter, -> editor.setGrammar(atom.grammars.selectGrammar('.txt'))
- expect(getState(presenter).content.indentGuidesVisible).toBe false
-
- it "is always false when the editor is mini", ->
- atom.config.set('editor.showIndentGuide', true)
- editor.setMini(true)
- presenter = buildPresenter()
- expect(getState(presenter).content.indentGuidesVisible).toBe false
- editor.setMini(false)
- expect(getState(presenter).content.indentGuidesVisible).toBe true
- editor.setMini(true)
- expect(getState(presenter).content.indentGuidesVisible).toBe false
-
describe ".backgroundColor", ->
it "is assigned to ::backgroundColor unless the editor is mini", ->
presenter = buildPresenter()
@@ -1215,9 +1182,19 @@ describe "TextEditorPresenter", ->
describe ".tiles", ->
lineStateForScreenRow = (presenter, row) ->
- lineId = presenter.model.tokenizedLineForScreenRow(row).id
- tileRow = presenter.tileForRow(row)
- getState(presenter).content.tiles[tileRow]?.lines[lineId]
+ tilesState = getState(presenter).content.tiles
+ lineId = presenter.linesByScreenRow.get(row)?.id
+ tilesState[presenter.tileForRow(row)]?.lines[lineId]
+
+ tagsForCodes = (presenter, tagCodes) ->
+ openTags = []
+ closeTags = []
+ for tagCode in tagCodes when tagCode < 0 # skip text codes
+ if presenter.isOpenTagCode(tagCode)
+ openTags.push(presenter.tagForCode(tagCode))
+ else
+ closeTags.push(presenter.tagForCode(tagCode))
+ {openTags, closeTags}
tiledContentContract (presenter) -> getState(presenter).content
@@ -1227,73 +1204,12 @@ describe "TextEditorPresenter", ->
presenter.setExplicitHeight(3)
expect(lineStateForScreenRow(presenter, 2)).toBeUndefined()
-
- line3 = editor.tokenizedLineForScreenRow(3)
- expectValues lineStateForScreenRow(presenter, 3), {
- screenRow: 3
- text: line3.text
- tags: line3.tags
- specialTokens: line3.specialTokens
- firstNonWhitespaceIndex: line3.firstNonWhitespaceIndex
- firstTrailingWhitespaceIndex: line3.firstTrailingWhitespaceIndex
- invisibles: line3.invisibles
- }
-
- line4 = editor.tokenizedLineForScreenRow(4)
- expectValues lineStateForScreenRow(presenter, 4), {
- screenRow: 4
- text: line4.text
- tags: line4.tags
- specialTokens: line4.specialTokens
- firstNonWhitespaceIndex: line4.firstNonWhitespaceIndex
- firstTrailingWhitespaceIndex: line4.firstTrailingWhitespaceIndex
- invisibles: line4.invisibles
- }
-
- line5 = editor.tokenizedLineForScreenRow(5)
- expectValues lineStateForScreenRow(presenter, 5), {
- screenRow: 5
- text: line5.text
- tags: line5.tags
- specialTokens: line5.specialTokens
- firstNonWhitespaceIndex: line5.firstNonWhitespaceIndex
- firstTrailingWhitespaceIndex: line5.firstTrailingWhitespaceIndex
- invisibles: line5.invisibles
- }
-
- line6 = editor.tokenizedLineForScreenRow(6)
- expectValues lineStateForScreenRow(presenter, 6), {
- screenRow: 6
- text: line6.text
- tags: line6.tags
- specialTokens: line6.specialTokens
- firstNonWhitespaceIndex: line6.firstNonWhitespaceIndex
- firstTrailingWhitespaceIndex: line6.firstTrailingWhitespaceIndex
- invisibles: line6.invisibles
- }
-
- line7 = editor.tokenizedLineForScreenRow(7)
- expectValues lineStateForScreenRow(presenter, 7), {
- screenRow: 7
- text: line7.text
- tags: line7.tags
- specialTokens: line7.specialTokens
- firstNonWhitespaceIndex: line7.firstNonWhitespaceIndex
- firstTrailingWhitespaceIndex: line7.firstTrailingWhitespaceIndex
- invisibles: line7.invisibles
- }
-
- line8 = editor.tokenizedLineForScreenRow(8)
- expectValues lineStateForScreenRow(presenter, 8), {
- screenRow: 8
- text: line8.text
- tags: line8.tags
- specialTokens: line8.specialTokens
- firstNonWhitespaceIndex: line8.firstNonWhitespaceIndex
- firstTrailingWhitespaceIndex: line8.firstTrailingWhitespaceIndex
- invisibles: line8.invisibles
- }
-
+ expectValues lineStateForScreenRow(presenter, 3), {screenRow: 3, tagCodes: editor.screenLineForScreenRow(3).tagCodes}
+ expectValues lineStateForScreenRow(presenter, 4), {screenRow: 4, tagCodes: editor.screenLineForScreenRow(4).tagCodes}
+ expectValues lineStateForScreenRow(presenter, 5), {screenRow: 5, tagCodes: editor.screenLineForScreenRow(5).tagCodes}
+ expectValues lineStateForScreenRow(presenter, 6), {screenRow: 6, tagCodes: editor.screenLineForScreenRow(6).tagCodes}
+ expectValues lineStateForScreenRow(presenter, 7), {screenRow: 7, tagCodes: editor.screenLineForScreenRow(7).tagCodes}
+ expectValues lineStateForScreenRow(presenter, 8), {screenRow: 8, tagCodes: editor.screenLineForScreenRow(8).tagCodes}
expect(lineStateForScreenRow(presenter, 9)).toBeUndefined()
it "updates when the editor's content changes", ->
@@ -1301,34 +1217,20 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n")
- line1 = editor.tokenizedLineForScreenRow(1)
- expectValues lineStateForScreenRow(presenter, 1), {
- text: line1.text
- tags: line1.tags
- }
-
- line2 = editor.tokenizedLineForScreenRow(2)
- expectValues lineStateForScreenRow(presenter, 2), {
- text: line2.text
- tags: line2.tags
- }
-
- line3 = editor.tokenizedLineForScreenRow(3)
- expectValues lineStateForScreenRow(presenter, 3), {
- text: line3.text
- tags: line3.tags
- }
+ expectValues lineStateForScreenRow(presenter, 1), {screenRow: 1, tagCodes: editor.screenLineForScreenRow(1).tagCodes}
+ expectValues lineStateForScreenRow(presenter, 2), {screenRow: 2, tagCodes: editor.screenLineForScreenRow(2).tagCodes}
+ expectValues lineStateForScreenRow(presenter, 3), {screenRow: 3, tagCodes: editor.screenLineForScreenRow(3).tagCodes}
it "includes the .endOfLineInvisibles if the editor.showInvisibles config option is true", ->
editor.setText("hello\nworld\r\n")
presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10)
- expect(lineStateForScreenRow(presenter, 0).endOfLineInvisibles).toBeNull()
- expect(lineStateForScreenRow(presenter, 1).endOfLineInvisibles).toBeNull()
+ expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 0).tagCodes).openTags).not.toContain('invisible-character eol')
+ expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 1).tagCodes).openTags).not.toContain('invisible-character eol')
atom.config.set('editor.showInvisibles', true)
presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10)
- expect(lineStateForScreenRow(presenter, 0).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.eol')]
- expect(lineStateForScreenRow(presenter, 1).endOfLineInvisibles).toEqual [atom.config.get('editor.invisibles.cr'), atom.config.get('editor.invisibles.eol')]
+ expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 0).tagCodes).openTags).toContain('invisible-character eol')
+ expect(tagsForCodes(presenter, lineStateForScreenRow(presenter, 1).tagCodes).openTags).toContain('invisible-character eol')
describe ".blockDecorations", ->
it "contains all block decorations that are present before/after a line, both initially and when decorations change", ->
@@ -1336,9 +1238,11 @@ describe "TextEditorPresenter", ->
presenter = buildPresenter()
blockDecoration2 = addBlockDecorationBeforeScreenRow(3)
blockDecoration3 = addBlockDecorationBeforeScreenRow(7)
- blockDecoration4 = addBlockDecorationAfterScreenRow(7)
+ blockDecoration4 = null
+
+ waitsForStateToUpdate presenter, ->
+ blockDecoration4 = addBlockDecorationAfterScreenRow(7)
- waitsForStateToUpdate presenter
runs ->
expect(lineStateForScreenRow(presenter, 0).precedingBlockDecorations).toEqual([blockDecoration1])
expect(lineStateForScreenRow(presenter, 0).followingBlockDecorations).toEqual([])
@@ -1472,9 +1376,9 @@ describe "TextEditorPresenter", ->
decoration1 = editor.decorateMarker(marker1, type: 'line', class: 'a')
presenter = buildPresenter()
marker2 = editor.addMarkerLayer(maintainHistory: true).markBufferRange([[4, 0], [6, 2]], invalidate: 'touch')
- decoration2 = editor.decorateMarker(marker2, type: 'line', class: 'b')
+ decoration2 = null
- waitsForStateToUpdate presenter
+ waitsForStateToUpdate presenter, -> decoration2 = editor.decorateMarker(marker2, type: 'line', class: 'b')
runs ->
expect(lineStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
expect(lineStateForScreenRow(presenter, 4).decorationClasses).toEqual ['a', 'b']
@@ -2150,31 +2054,40 @@ describe "TextEditorPresenter", ->
}
# becoming empty
- waitsForStateToUpdate presenter, -> editor.getSelections()[1].clear(autoscroll: false)
+ runs ->
+ editor.getSelections()[1].clear(autoscroll: false)
+ waitsForStateToUpdate presenter
runs ->
expectUndefinedStateForSelection(presenter, 1)
# becoming non-empty
- waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false)
+ runs ->
+ editor.getSelections()[1].setBufferRange([[2, 4], [2, 6]], autoscroll: false)
+ waitsForStateToUpdate presenter
runs ->
expectValues stateForSelectionInTile(presenter, 1, 2), {
regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}]
}
# moving out of view
- waitsForStateToUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false)
+ runs ->
+ editor.getSelections()[1].setBufferRange([[3, 4], [3, 6]], autoscroll: false)
+ waitsForStateToUpdate presenter
runs ->
expectUndefinedStateForSelection(presenter, 1)
# adding
- waitsForStateToUpdate presenter, -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false)
+ runs -> editor.addSelectionForBufferRange([[1, 4], [1, 6]], autoscroll: false)
+ waitsForStateToUpdate presenter
runs ->
expectValues stateForSelectionInTile(presenter, 2, 0), {
regions: [{top: 10, left: 4 * 10, width: 2 * 10, height: 10}]
}
# moving added selection
- waitsForStateToUpdate presenter, -> editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false)
+ runs ->
+ editor.getSelections()[2].setBufferRange([[1, 4], [1, 8]], autoscroll: false)
+ waitsForStateToUpdate presenter
destroyedSelection = null
runs ->
@@ -2208,8 +2121,9 @@ describe "TextEditorPresenter", ->
presenter = buildPresenter(explicitHeight: 30, scrollTop: 20, tileSize: 2)
marker = editor.markBufferPosition([2, 2])
- highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a')
+ highlight = null
waitsForStateToUpdate presenter, ->
+ highlight = editor.decorateMarker(marker, type: 'highlight', class: 'a')
marker.setBufferRange([[2, 2], [5, 2]])
highlight.flash('b', 500)
runs ->
@@ -2879,12 +2793,9 @@ describe "TextEditorPresenter", ->
describe ".content.tiles", ->
lineNumberStateForScreenRow = (presenter, screenRow) ->
- editor = presenter.model
- tileRow = presenter.tileForRow(screenRow)
- line = editor.tokenizedLineForScreenRow(screenRow)
-
- gutterState = getLineNumberGutterState(presenter)
- gutterState.content.tiles[tileRow]?.lineNumbers[line?.id]
+ tilesState = getLineNumberGutterState(presenter).content.tiles
+ line = presenter.linesByScreenRow.get(screenRow)
+ tilesState[presenter.tileForRow(screenRow)]?.lineNumbers[line?.id]
tiledContentContract (presenter) -> getLineNumberGutterState(presenter).content
@@ -2893,17 +2804,21 @@ describe "TextEditorPresenter", ->
editor.foldBufferRow(4)
editor.setSoftWrapped(true)
editor.setDefaultCharWidth(1)
- editor.setEditorWidthInChars(50)
- presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10, tileSize: 2)
+ editor.setEditorWidthInChars(51)
+ presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10, tileSize: 3)
+ presenter.setScreenRowsToMeasure([9, 11])
- expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined()
- expectValues lineNumberStateForScreenRow(presenter, 2), {screenRow: 2, bufferRow: 2, softWrapped: false}
+ expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined()
expectValues lineNumberStateForScreenRow(presenter, 3), {screenRow: 3, bufferRow: 3, softWrapped: false}
expectValues lineNumberStateForScreenRow(presenter, 4), {screenRow: 4, bufferRow: 3, softWrapped: true}
expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 4, softWrapped: false}
expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 7, softWrapped: false}
expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 8, softWrapped: false}
- expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined()
+ expectValues lineNumberStateForScreenRow(presenter, 8), {screenRow: 8, bufferRow: 8, softWrapped: true}
+ expect(lineNumberStateForScreenRow(presenter, 9)).toBeUndefined()
+ expect(lineNumberStateForScreenRow(presenter, 10)).toBeUndefined()
+ expect(lineNumberStateForScreenRow(presenter, 11)).toBeUndefined()
+ expect(lineNumberStateForScreenRow(presenter, 12)).toBeUndefined()
it "updates when the editor's content changes", ->
editor.foldBufferRow(4)
@@ -2969,9 +2884,8 @@ describe "TextEditorPresenter", ->
presenter.setBlockDecorationDimensions(blockDecoration4, 0, 35)
presenter.setBlockDecorationDimensions(blockDecoration4, 0, 40)
presenter.setBlockDecorationDimensions(blockDecoration5, 0, 50)
- presenter.setBlockDecorationDimensions(blockDecoration6, 0, 60)
- waitsForStateToUpdate presenter
+ waitsForStateToUpdate presenter, -> presenter.setBlockDecorationDimensions(blockDecoration6, 0, 60)
runs ->
expect(lineNumberStateForScreenRow(presenter, 0).blockDecorationsHeight).toBe(10)
expect(lineNumberStateForScreenRow(presenter, 1).blockDecorationsHeight).toBe(0)
@@ -3159,6 +3073,29 @@ describe "TextEditorPresenter", ->
expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain 'a'
expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toContain 'a'
+ describe "when a fold spans a single soft-wrapped buffer row", ->
+ it "applies the 'folded' decoration only to its initial screen row", ->
+ editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
+ editor.setEditorWidthInChars(20)
+ editor.foldBufferRange([[0, 20], [0, 22]])
+ editor.foldBufferRange([[0, 10], [0, 14]])
+ presenter = buildPresenter(explicitHeight: 35, scrollTop: 0, tileSize: 2)
+
+ expect(lineNumberStateForScreenRow(presenter, 0).decorationClasses).toContain('folded')
+ expect(lineNumberStateForScreenRow(presenter, 1).decorationClasses).toBeNull()
+
+ describe "when a fold is at the end of a soft-wrapped buffer row", ->
+ it "applies the 'folded' decoration only to its initial screen row", ->
+ editor.setSoftWrapped(true)
+ editor.setDefaultCharWidth(1)
+ editor.setEditorWidthInChars(25)
+ editor.foldBufferRow(1)
+ presenter = buildPresenter(explicitHeight: 35, scrollTop: 0, tileSize: 2)
+
+ expect(lineNumberStateForScreenRow(presenter, 2).decorationClasses).toContain('folded')
+ expect(lineNumberStateForScreenRow(presenter, 3).decorationClasses).toBeNull()
+
describe ".foldable", ->
it "marks line numbers at the start of a foldable region as foldable", ->
presenter = buildPresenter()
@@ -3460,9 +3397,9 @@ describe "TextEditorPresenter", ->
gutterName: 'test-gutter-2'
class: 'test-class'
marker4 = editor.markBufferRange([[0, 0], [1, 0]])
- decoration4 = editor.decorateMarker(marker4, decorationParams)
+ decoration4 = null
- waitsForStateToUpdate presenter
+ waitsForStateToUpdate presenter, -> decoration4 = editor.decorateMarker(marker4, decorationParams)
runs ->
expectStateUpdate presenter, -> editor.addGutter({name: 'test-gutter-2'})
diff --git a/spec/text-editor-registry-spec.coffee b/spec/text-editor-registry-spec.coffee
new file mode 100644
index 000000000..80f29f897
--- /dev/null
+++ b/spec/text-editor-registry-spec.coffee
@@ -0,0 +1,49 @@
+TextEditorRegistry = require '../src/text-editor-registry'
+
+describe "TextEditorRegistry", ->
+ [registry, editor] = []
+
+ beforeEach ->
+ registry = new TextEditorRegistry
+
+ describe "when a TextEditor is added", ->
+ it "gets added to the list of registered editors", ->
+ editor = {}
+ registry.add(editor)
+ expect(editor.registered).toBe true
+ expect(registry.editors.size).toBe 1
+ expect(registry.editors.has(editor)).toBe(true)
+
+ it "returns a Disposable that can unregister the editor", ->
+ editor = {}
+ disposable = registry.add(editor)
+ expect(registry.editors.size).toBe 1
+ disposable.dispose()
+ expect(registry.editors.size).toBe 0
+ expect(editor.registered).toBe false
+
+ it "can be removed", ->
+ editor = {}
+ registry.add(editor)
+ expect(registry.editors.size).toBe 1
+ success = registry.remove(editor)
+ expect(success).toBe true
+ expect(registry.editors.size).toBe 0
+ expect(editor.registered).toBe false
+
+ describe "when the registry is observed", ->
+ it "calls the callback for current and future editors until unsubscribed", ->
+ [editor1, editor2, editor3] = [{}, {}, {}]
+
+ registry.add(editor1)
+ subscription = registry.observe spy = jasmine.createSpy()
+ expect(spy.calls.length).toBe 1
+
+ registry.add(editor2)
+ expect(spy.calls.length).toBe 2
+ expect(spy.argsForCall[0][0]).toBe editor1
+ expect(spy.argsForCall[1][0]).toBe editor2
+
+ subscription.dispose()
+ registry.add(editor3)
+ expect(spy.calls.length).toBe 2
diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee
index 6959d4da5..be24baa01 100644
--- a/spec/text-editor-spec.coffee
+++ b/spec/text-editor-spec.coffee
@@ -39,31 +39,19 @@ describe "TextEditor", ->
it "preserves the invisibles setting", ->
atom.config.set('editor.showInvisibles', true)
- previousInvisibles = editor.tokenizedLineForScreenRow(0).invisibles
-
+ previousLineText = editor.lineTextForScreenRow(0)
editor2 = TextEditor.deserialize(editor.serialize(), atom)
-
- expect(previousInvisibles).toBeDefined()
- expect(editor2.displayBuffer.tokenizedLineForScreenRow(0).invisibles).toEqual previousInvisibles
+ expect(editor2.lineTextForScreenRow(0)).toBe(previousLineText)
it "updates invisibles if the settings have changed between serialization and deserialization", ->
atom.config.set('editor.showInvisibles', true)
-
+ previousLineText = editor.lineTextForScreenRow(0)
state = editor.serialize()
atom.config.set('editor.invisibles', eol: '?')
editor2 = TextEditor.deserialize(state, atom)
- expect(editor.tokenizedLineForScreenRow(0).invisibles.eol).toBe '?'
-
- it "restores pending tabs in pending state", ->
- expect(editor.isPending()).toBe false
- editor2 = TextEditor.deserialize(editor.serialize(), atom)
- expect(editor2.isPending()).toBe false
-
- pendingEditor = atom.workspace.buildTextEditor(pending: true)
- expect(pendingEditor.isPending()).toBe true
- editor3 = TextEditor.deserialize(pendingEditor.serialize(), atom)
- expect(editor3.isPending()).toBe true
+ expect(editor2.lineTextForScreenRow(0)).not.toBe(previousLineText)
+ expect(editor2.lineTextForScreenRow(0).endsWith('?')).toBe(true)
describe "when the editor is constructed with the largeFileMode option set to true", ->
it "loads the editor but doesn't tokenize", ->
@@ -74,15 +62,14 @@ describe "TextEditor", ->
runs ->
buffer = editor.getBuffer()
- expect(editor.tokenizedLineForScreenRow(0).text).toBe buffer.lineForRow(0)
- expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
- expect(editor.tokenizedLineForScreenRow(1).tokens.length).toBe 2 # soft tab
- expect(editor.tokenizedLineForScreenRow(12).text).toBe buffer.lineForRow(12)
- expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
+ 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.tokenizedLineForScreenRow(0).tokens.length).toBe 1
- expect(editor.tokenizedLineForScreenRow(1).tokens.length).toBe 2 # sof tab
+ expect(editor.tokensForScreenRow(0).length).toBe 1
+ expect(editor.tokensForScreenRow(1).length).toBe 2 # soft tab
describe ".copy()", ->
it "returns a different edit session with the same initial state", ->
@@ -192,17 +179,19 @@ describe "TextEditor", ->
expect(editor1.getLongTitle()).toBe "readme \u2014 sample-theme-1"
expect(editor2.getLongTitle()).toBe "readme \u2014 sample-theme-2"
- it "returns ' — ' when opened files have identical file and dir names", ->
+ it "returns ' — ' when opened files have identical file names in subdirectories", ->
editor1 = null
editor2 = null
+ path1 = path.join('sample-theme-1', 'src', 'js')
+ path2 = path.join('sample-theme-2', 'src', 'js')
waitsForPromise ->
- atom.workspace.open(path.join('sample-theme-1', 'src', 'js', 'main.js')).then (o) ->
+ atom.workspace.open(path.join(path1, 'main.js')).then (o) ->
editor1 = o
- atom.workspace.open(path.join('sample-theme-2', 'src', 'js', 'main.js')).then (o) ->
+ atom.workspace.open(path.join(path2, 'main.js')).then (o) ->
editor2 = o
runs ->
- expect(editor1.getLongTitle()).toBe "main.js \u2014 sample-theme-1/src/js"
- expect(editor2.getLongTitle()).toBe "main.js \u2014 sample-theme-2/src/js"
+ expect(editor1.getLongTitle()).toBe "main.js \u2014 #{path1}"
+ expect(editor2.getLongTitle()).toBe "main.js \u2014 #{path2}"
it "returns ' — ' when opened files have identical file and same parent dir name", ->
editor1 = null
@@ -214,7 +203,7 @@ describe "TextEditor", ->
editor2 = o
runs ->
expect(editor1.getLongTitle()).toBe "main.js \u2014 js"
- expect(editor2.getLongTitle()).toBe "main.js \u2014 js/plugin"
+ expect(editor2.getLongTitle()).toBe "main.js \u2014 " + path.join('js', 'plugin')
it "notifies ::onDidChangeTitle observers when the underlying buffer path changes", ->
observed = []
@@ -322,7 +311,7 @@ describe "TextEditor", ->
editor.setSoftWrapped(true)
editor.setDefaultCharWidth(1)
editor.setEditorWidthInChars(50)
- editor.createFold(2, 3)
+ editor.foldBufferRowRange(2, 3)
it "positions the cursor at the buffer position that corresponds to the given screen position", ->
editor.setCursorScreenPosition([9, 0])
@@ -503,7 +492,7 @@ describe "TextEditor", ->
it "wraps to the end of the previous line", ->
editor.setCursorScreenPosition([4, 4])
editor.moveLeft()
- expect(editor.getCursorScreenPosition()).toEqual [3, 50]
+ expect(editor.getCursorScreenPosition()).toEqual [3, 46]
describe "when the cursor is on the first line", ->
it "remains in the same position (0,0)", ->
@@ -691,7 +680,7 @@ describe "TextEditor", ->
editor.setCursorScreenPosition([0, 2])
editor.moveToEndOfLine()
cursor = editor.getLastCursor()
- expect(cursor.getScreenPosition()).toEqual [3, 4]
+ expect(cursor.getScreenPosition()).toEqual [4, 4]
describe ".moveToFirstCharacterOfLine()", ->
describe "when soft wrap is on", ->
@@ -1197,14 +1186,10 @@ describe "TextEditor", ->
cursor2 = editor.addCursorAtBufferPosition([1, 4])
expect(cursor2.marker).toBe cursor1.marker
- describe '.logCursorScope()', ->
- beforeEach ->
- spyOn(atom.notifications, 'addInfo')
-
- it 'opens a notification', ->
- editor.logCursorScope()
-
- expect(atom.notifications.addInfo).toHaveBeenCalled()
+ describe '.getCursorScope()', ->
+ it 'returns the current scope', ->
+ descriptor = editor.getCursorScope()
+ expect(descriptor.scopes).toContain('source.js')
describe "selection", ->
selection = null
@@ -1810,22 +1795,22 @@ describe "TextEditor", ->
describe "when the 'preserveFolds' option is false (the default)", ->
it "removes folds that contain the selections", ->
editor.setSelectedBufferRange([[0, 0], [0, 0]])
- editor.createFold(1, 4)
- editor.createFold(2, 3)
- editor.createFold(6, 8)
- editor.createFold(10, 11)
+ editor.foldBufferRowRange(1, 4)
+ editor.foldBufferRowRange(2, 3)
+ editor.foldBufferRowRange(6, 8)
+ editor.foldBufferRowRange(10, 11)
editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 6], [7, 7]]])
- expect(editor.tokenizedLineForScreenRow(1).fold).toBeUndefined()
- expect(editor.tokenizedLineForScreenRow(2).fold).toBeUndefined()
- expect(editor.tokenizedLineForScreenRow(6).fold).toBeUndefined()
- expect(editor.tokenizedLineForScreenRow(10).fold).toBeDefined()
+ expect(editor.isFoldedAtScreenRow(1)).toBeFalsy()
+ expect(editor.isFoldedAtScreenRow(2)).toBeFalsy()
+ expect(editor.isFoldedAtScreenRow(6)).toBeFalsy()
+ expect(editor.isFoldedAtScreenRow(10)).toBeTruthy()
describe "when the 'preserveFolds' option is true", ->
it "does not remove folds that contain the selections", ->
editor.setSelectedBufferRange([[0, 0], [0, 0]])
- editor.createFold(1, 4)
- editor.createFold(6, 8)
+ editor.foldBufferRowRange(1, 4)
+ editor.foldBufferRowRange(6, 8)
editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[6, 0], [6, 1]]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(1)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
@@ -2142,20 +2127,31 @@ describe "TextEditor", ->
editor.splitSelectionsIntoLines()
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]]]
- describe ".consolidateSelections()", ->
- it "destroys all selections but the least recent, returning true if any selections were destroyed", ->
- editor.setSelectedBufferRange([[3, 16], [3, 21]])
- selection1 = editor.getLastSelection()
+ describe "::consolidateSelections()", ->
+ makeMultipleSelections = ->
+ selection.setBufferRange [[3, 16], [3, 21]]
selection2 = editor.addSelectionForBufferRange([[3, 25], [3, 34]])
selection3 = editor.addSelectionForBufferRange([[8, 4], [8, 10]])
+ selection4 = editor.addSelectionForBufferRange([[1, 6], [1, 10]])
+ expect(editor.getSelections()).toEqual [selection, selection2, selection3, selection4]
+ [selection, selection2, selection3, selection4]
+
+ it "destroys all selections but the oldest selection and autoscrolls to it, returning true if any selections were destroyed", ->
+ [selection1] = makeMultipleSelections()
+
+ autoscrollEvents = []
+ editor.onDidRequestAutoscroll (event) -> autoscrollEvents.push(event)
- expect(editor.getSelections()).toEqual [selection1, selection2, selection3]
expect(editor.consolidateSelections()).toBeTruthy()
expect(editor.getSelections()).toEqual [selection1]
expect(selection1.isEmpty()).toBeFalsy()
expect(editor.consolidateSelections()).toBeFalsy()
expect(editor.getSelections()).toEqual [selection1]
+ expect(autoscrollEvents).toEqual([
+ {screenRange: selection1.getScreenRange(), options: {center: true, reversed: false}}
+ ])
+
describe "when the cursor is moved while there is a selection", ->
makeSelection = -> selection.setBufferRange [[1, 2], [1, 5]]
@@ -2226,7 +2222,7 @@ describe "TextEditor", ->
it "moves the line to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true)
expect(editor.getSelectedBufferRange()).toEqual [[4, 2], [4, 9]]
@@ -2254,7 +2250,7 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));"
expect(editor.lineTextForBufferRow(9)).toBe " };"
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2292,7 +2288,7 @@ describe "TextEditor", ->
it "moves the lines to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(3)).toBeFalsy()
@@ -2320,7 +2316,7 @@ describe "TextEditor", ->
it "moves the lines to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
@@ -2364,7 +2360,7 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));"
expect(editor.lineTextForBufferRow(9)).toBe " };"
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
@@ -2404,7 +2400,7 @@ describe "TextEditor", ->
it "moves the lines to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRanges([
[[2, 2], [2, 9]],
[[4, 2], [4, 9]]
@@ -2442,7 +2438,7 @@ describe "TextEditor", ->
describe "when there is a fold", ->
it "moves all lines that spanned by a selection to preceding row, preserving all folds", ->
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2469,8 +2465,8 @@ describe "TextEditor", ->
describe 'and many selections intersects folded rows', ->
it 'moves and preserves all the folds', ->
- editor.createFold(2, 4)
- editor.createFold(7, 9)
+ editor.foldBufferRowRange(2, 4)
+ editor.foldBufferRowRange(7, 9)
editor.setSelectedBufferRanges([
[[1, 0], [5, 4]],
@@ -2554,7 +2550,7 @@ describe "TextEditor", ->
it "moves the line to the following row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[4, 2], [4, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
@@ -2580,7 +2576,7 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2634,7 +2630,7 @@ describe "TextEditor", ->
it "moves the lines to the following row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[3, 2], [4, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(3)).toBeFalsy()
@@ -2662,7 +2658,7 @@ describe "TextEditor", ->
it "moves the lines to the following row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRange([[4, 2], [8, 9]], preserveFolds: true)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
@@ -2692,7 +2688,7 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(2)).toBe " if (items.length <= 1) return items;"
expect(editor.lineTextForBufferRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
@@ -2734,8 +2730,8 @@ describe "TextEditor", ->
describe 'and many selections intersects folded rows', ->
it 'moves and preserves all the folds', ->
- editor.createFold(2, 4)
- editor.createFold(7, 9)
+ editor.foldBufferRowRange(2, 4)
+ editor.foldBufferRowRange(7, 9)
editor.setSelectedBufferRanges([
[[2, 0], [2, 4]],
@@ -2764,7 +2760,7 @@ describe "TextEditor", ->
describe "when there is a fold below one of the selected row", ->
it "moves all lines spanned by a selection to the following row, preserving the fold", ->
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2787,7 +2783,7 @@ describe "TextEditor", ->
describe "when there is a fold below a group of multiple selections without any lines with no selection in-between", ->
it "moves all the lines below the fold, preserving the fold", ->
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
expect(editor.isFoldedAtBufferRow(5)).toBeTruthy()
@@ -2812,7 +2808,7 @@ describe "TextEditor", ->
it "moves the lines to the previous row without breaking the fold", ->
expect(editor.lineTextForBufferRow(4)).toBe " while(items.length > 0) {"
- editor.createFold(4, 7)
+ editor.foldBufferRowRange(4, 7)
editor.setSelectedBufferRanges([
[[2, 2], [2, 9]],
[[4, 2], [4, 9]]
@@ -2879,6 +2875,13 @@ describe "TextEditor", ->
expect(editor.lineTextForBufferRow(1)).toBe "1"
expect(editor.lineTextForBufferRow(2)).toBe "2"
+ describe "when the line is the last buffer row", ->
+ it "doesn't move it", ->
+ editor.setText("abc\ndef")
+ editor.setCursorBufferPosition([1, 0])
+ editor.moveLineDown()
+ expect(editor.getText()).toBe("abc\ndef")
+
describe ".insertText(text)", ->
describe "when there is a single selection", ->
beforeEach ->
@@ -2950,10 +2953,10 @@ describe "TextEditor", ->
describe "when there is a selection that ends on a folded line", ->
it "destroys the selection", ->
- editor.createFold(2, 4)
+ editor.foldBufferRowRange(2, 4)
editor.setSelectedBufferRange([[1, 0], [2, 0]])
editor.insertText('holy cow')
- expect(editor.tokenizedLineForScreenRow(2).fold).toBeUndefined()
+ expect(editor.isFoldedAtScreenRow(2)).toBeFalsy()
describe "when there are ::onWillInsertText and ::onDidInsertText observers", ->
beforeEach ->
@@ -3175,7 +3178,7 @@ describe "TextEditor", ->
expect(editor.indentationForBufferRow(0)).toBe 1
expect(editor.indentationForBufferRow(1)).toBe 1
- it "indents the new line to the correct level when editor.autoIndent is true and using a off-side rule language", ->
+ it "indents the new line to the correct level when editor.autoIndent is true and using an off-side rule language", ->
waitsForPromise ->
atom.packages.activatePackage('language-coffee-script')
@@ -3247,15 +3250,14 @@ describe "TextEditor", ->
editor.setCursorScreenPosition(row: 0, column: 0)
editor.backspace()
- describe "when the cursor is on the first column of a line below a fold", ->
- it "deletes the folded lines", ->
- editor.setCursorScreenPosition([4, 0])
- editor.foldCurrentRow()
- editor.setCursorScreenPosition([5, 0])
+ describe "when the cursor is after a fold", ->
+ it "deletes the folded range", ->
+ editor.foldBufferRange([[4, 7], [5, 8]])
+ editor.setCursorBufferPosition([5, 8])
editor.backspace()
- expect(buffer.lineForRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));"
- expect(buffer.lineForRow(4).fold).toBeUndefined()
+ expect(buffer.lineForRow(4)).toBe " whirrent = items.shift();"
+ expect(editor.isFoldedAtBufferRow(4)).toBe(false)
describe "when the cursor is in the middle of a line below a fold", ->
it "backspaces as normal", ->
@@ -3268,14 +3270,13 @@ describe "TextEditor", ->
expect(buffer.lineForRow(8)).toBe " eturn sort(left).concat(pivot).concat(sort(right));"
describe "when the cursor is on a folded screen line", ->
- it "deletes all of the folded lines along with the fold", ->
+ it "deletes the contents of the fold before the cursor", ->
editor.setCursorBufferPosition([3, 0])
editor.foldCurrentRow()
editor.backspace()
- expect(buffer.lineForRow(1)).toBe ""
- expect(buffer.lineForRow(2)).toBe " return sort(Array.apply(this, arguments));"
- expect(editor.getCursorScreenPosition()).toEqual [1, 0]
+ expect(buffer.lineForRow(1)).toBe " var sort = function(items) var pivot = items.shift(), current, left = [], right = [];"
+ expect(editor.getCursorScreenPosition()).toEqual [1, 29]
describe "when there are multiple cursors", ->
describe "when cursors are on the same line", ->
@@ -3342,7 +3343,7 @@ describe "TextEditor", ->
editor.backspace()
expect(buffer.lineForRow(3)).toBe " while(items.length > 0) {"
- expect(editor.tokenizedLineForScreenRow(3).fold).toBeDefined()
+ expect(editor.isFoldedAtScreenRow(3)).toBe(true)
describe "when there are multiple selections", ->
it "removes all selected text", ->
@@ -3515,16 +3516,16 @@ describe "TextEditor", ->
editor.delete()
expect(buffer.lineForRow(12)).toBe '};'
- describe "when the cursor is on the end of a line above a fold", ->
+ describe "when the cursor is before a fold", ->
it "only deletes the lines inside the fold", ->
- editor.foldBufferRow(4)
- editor.setCursorScreenPosition([3, Infinity])
+ editor.foldBufferRange([[3, 6], [4, 8]])
+ editor.setCursorScreenPosition([3, 6])
cursorPositionBefore = editor.getCursorScreenPosition()
editor.delete()
- expect(buffer.lineForRow(3)).toBe " var pivot = items.shift(), current, left = [], right = [];"
- expect(buffer.lineForRow(4)).toBe " return sort(left).concat(pivot).concat(sort(right));"
+ expect(buffer.lineForRow(3)).toBe " vae(items.length > 0) {"
+ expect(buffer.lineForRow(4)).toBe " current = items.shift();"
expect(editor.getCursorScreenPosition()).toEqual cursorPositionBefore
describe "when the cursor is in the middle a line above a fold", ->
@@ -3536,20 +3537,21 @@ describe "TextEditor", ->
editor.delete()
expect(buffer.lineForRow(3)).toBe " ar pivot = items.shift(), current, left = [], right = [];"
- expect(editor.tokenizedLineForScreenRow(4).fold).toBeDefined()
+ expect(editor.isFoldedAtScreenRow(4)).toBe(true)
expect(editor.getCursorScreenPosition()).toEqual [3, 4]
- describe "when the cursor is on a folded line", ->
- it "removes the lines contained by the fold", ->
- editor.setSelectedBufferRange([[2, 0], [2, 0]])
- editor.createFold(2, 4)
- editor.createFold(2, 6)
- oldLine7 = buffer.lineForRow(7)
- oldLine8 = buffer.lineForRow(8)
+ describe "when the cursor is inside a fold", ->
+ it "removes the folded content after the cursor", ->
+ editor.foldBufferRange([[2, 6], [6, 21]])
+ editor.setCursorBufferPosition([4, 9])
editor.delete()
- expect(editor.tokenizedLineForScreenRow(2).text).toBe oldLine7
- expect(editor.tokenizedLineForScreenRow(3).text).toBe oldLine8
+
+ expect(buffer.lineForRow(2)).toBe ' if (items.length <= 1) return items;'
+ expect(buffer.lineForRow(3)).toBe ' var pivot = items.shift(), current, left = [], right = [];'
+ expect(buffer.lineForRow(4)).toBe ' while ? left.push(current) : right.push(current);'
+ expect(buffer.lineForRow(5)).toBe ' }'
+ expect(editor.getCursorBufferPosition()).toEqual [4, 9]
describe "when there are multiple cursors", ->
describe "when cursors are on the same line", ->
@@ -3806,10 +3808,10 @@ describe "TextEditor", ->
it "cuts up to the end of the line", ->
editor.setSoftWrapped(true)
editor.setDefaultCharWidth(1)
- editor.setEditorWidthInChars(10)
- editor.setCursorScreenPosition([2, 2])
+ editor.setEditorWidthInChars(25)
+ editor.setCursorScreenPosition([2, 6])
editor.cutToEndOfLine()
- expect(editor.tokenizedLineForScreenRow(2).text).toBe '= () {'
+ expect(editor.lineTextForScreenRow(2)).toBe ' var function(items) {'
describe "when soft wrap is off", ->
describe "when nothing is selected", ->
@@ -4560,11 +4562,142 @@ describe "TextEditor", ->
expect(cursor1.getBufferPosition()).toEqual [0, 0]
expect(cursor3.getBufferPosition()).toEqual [1, 2]
+ describe ".moveSelectionLeft()", ->
+ it "moves one active selection on one line one column to the left", ->
+ editor.setSelectedBufferRange [[0, 4], [0, 13]]
+ expect(editor.getSelectedText()).toBe 'quicksort'
+
+ editor.moveSelectionLeft()
+
+ expect(editor.getSelectedText()).toBe 'quicksort'
+ expect(editor.getSelectedBufferRange()).toEqual [[0, 3], [0, 12]]
+
+ it "moves multiple active selections on one line one column to the left", ->
+ editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]])
+ selections = editor.getSelections()
+
+ expect(selections[0].getText()).toBe 'quicksort'
+ expect(selections[1].getText()).toBe 'function'
+
+ editor.moveSelectionLeft()
+
+ expect(selections[0].getText()).toBe 'quicksort'
+ expect(selections[1].getText()).toBe 'function'
+ expect(editor.getSelectedBufferRanges()).toEqual [[[0, 3], [0, 12]], [[0, 15], [0, 23]]]
+
+ it "moves multiple active selections on multiple lines one column to the left", ->
+ editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]])
+ selections = editor.getSelections()
+
+ expect(selections[0].getText()).toBe 'quicksort'
+ expect(selections[1].getText()).toBe 'sort'
+
+ editor.moveSelectionLeft()
+
+ expect(selections[0].getText()).toBe 'quicksort'
+ expect(selections[1].getText()).toBe 'sort'
+ expect(editor.getSelectedBufferRanges()).toEqual [[[0, 3], [0, 12]], [[1, 5], [1, 9]]]
+
+ describe "when a selection is at the first column of a line", ->
+ it "does not change the selection", ->
+ editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[1, 0], [1, 3]]])
+ selections = editor.getSelections()
+
+ expect(selections[0].getText()).toBe 'var'
+ expect(selections[1].getText()).toBe ' v'
+
+ editor.moveSelectionLeft()
+ editor.moveSelectionLeft()
+
+ expect(selections[0].getText()).toBe 'var'
+ expect(selections[1].getText()).toBe ' v'
+ expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]], [[1, 0], [1, 3]]]
+
+ describe "when multiple selections are active on one line", ->
+ it "does not change the selection", ->
+ editor.setSelectedBufferRanges([[[0, 0], [0, 3]], [[0, 4], [0, 13]]])
+ selections = editor.getSelections()
+
+ expect(selections[0].getText()).toBe 'var'
+ expect(selections[1].getText()).toBe 'quicksort'
+
+ editor.moveSelectionLeft()
+
+ expect(selections[0].getText()).toBe 'var'
+ expect(selections[1].getText()).toBe 'quicksort'
+ expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [0, 3]], [[0, 4], [0, 13]]]
+
+ describe ".moveSelectionRight()", ->
+ it "moves one active selection on one line one column to the right", ->
+ editor.setSelectedBufferRange [[0, 4], [0, 13]]
+ expect(editor.getSelectedText()).toBe 'quicksort'
+
+ editor.moveSelectionRight()
+
+ expect(editor.getSelectedText()).toBe 'quicksort'
+ expect(editor.getSelectedBufferRange()).toEqual [[0, 5], [0, 14]]
+
+ it "moves multiple active selections on one line one column to the right", ->
+ editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[0, 16], [0, 24]]])
+ selections = editor.getSelections()
+
+ expect(selections[0].getText()).toBe 'quicksort'
+ expect(selections[1].getText()).toBe 'function'
+
+ editor.moveSelectionRight()
+
+ expect(selections[0].getText()).toBe 'quicksort'
+ expect(selections[1].getText()).toBe 'function'
+ expect(editor.getSelectedBufferRanges()).toEqual [[[0, 5], [0, 14]], [[0, 17], [0, 25]]]
+
+ it "moves multiple active selections on multiple lines one column to the right", ->
+ editor.setSelectedBufferRanges([[[0, 4], [0, 13]], [[1, 6], [1, 10]]])
+ selections = editor.getSelections()
+
+ expect(selections[0].getText()).toBe 'quicksort'
+ expect(selections[1].getText()).toBe 'sort'
+
+ editor.moveSelectionRight()
+
+ expect(selections[0].getText()).toBe 'quicksort'
+ expect(selections[1].getText()).toBe 'sort'
+ expect(editor.getSelectedBufferRanges()).toEqual [[[0, 5], [0, 14]], [[1, 7], [1, 11]]]
+
+ describe "when a selection is at the last column of a line", ->
+ it "does not change the selection", ->
+ editor.setSelectedBufferRanges([[[2, 34], [2, 40]], [[5, 22], [5, 30]]])
+ selections = editor.getSelections()
+
+ expect(selections[0].getText()).toBe 'items;'
+ expect(selections[1].getText()).toBe 'shift();'
+
+ editor.moveSelectionRight()
+ editor.moveSelectionRight()
+
+ expect(selections[0].getText()).toBe 'items;'
+ expect(selections[1].getText()).toBe 'shift();'
+ expect(editor.getSelectedBufferRanges()).toEqual [[[2, 34], [2, 40]], [[5, 22], [5, 30]]]
+
+ describe "when multiple selections are active on one line", ->
+ it "does not change the selection", ->
+ editor.setSelectedBufferRanges([[[2, 27], [2, 33]], [[2, 34], [2, 40]]])
+ selections = editor.getSelections()
+
+ expect(selections[0].getText()).toBe 'return'
+ expect(selections[1].getText()).toBe 'items;'
+
+ editor.moveSelectionRight()
+
+ expect(selections[0].getText()).toBe 'return'
+ expect(selections[1].getText()).toBe 'items;'
+ expect(editor.getSelectedBufferRanges()).toEqual [[[2, 27], [2, 33]], [[2, 34], [2, 40]]]
+
describe 'reading text', ->
it '.lineTextForScreenRow(row)', ->
editor.foldBufferRow(4)
expect(editor.lineTextForScreenRow(5)).toEqual ' return sort(left).concat(pivot).concat(sort(right));'
- expect(editor.lineTextForScreenRow(100)).not.toBeDefined()
+ expect(editor.lineTextForScreenRow(9)).toEqual '};'
+ expect(editor.lineTextForScreenRow(10)).toBeUndefined()
describe ".deleteLine()", ->
it "deletes the first line when the cursor is there", ->
@@ -4921,11 +5054,13 @@ describe "TextEditor", ->
it 'retokenizes when the tab length is updated via .setTabLength()', ->
expect(editor.getTabLength()).toBe 2
- expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2
+ leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
+ expect(leadingWhitespaceTokens.length).toBe(3)
editor.setTabLength(6)
expect(editor.getTabLength()).toBe 6
- expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6
+ leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
+ expect(leadingWhitespaceTokens.length).toBe(1)
changeHandler = jasmine.createSpy('changeHandler')
editor.onDidChange(changeHandler)
@@ -4934,21 +5069,25 @@ describe "TextEditor", ->
it 'retokenizes when the editor.tabLength setting is updated', ->
expect(editor.getTabLength()).toBe 2
- expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2
+ leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
+ expect(leadingWhitespaceTokens.length).toBe(3)
atom.config.set 'editor.tabLength', 6, scopeSelector: '.source.js'
expect(editor.getTabLength()).toBe 6
- expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6
+ leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
+ expect(leadingWhitespaceTokens.length).toBe(1)
it 'updates the tab length when the grammar changes', ->
atom.config.set 'editor.tabLength', 6, scopeSelector: '.source.coffee'
expect(editor.getTabLength()).toBe 2
- expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 2
+ leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
+ expect(leadingWhitespaceTokens.length).toBe(3)
editor.setGrammar(coffeeEditor.getGrammar())
expect(editor.getTabLength()).toBe 6
- expect(editor.tokenizedLineForScreenRow(5).tokens[0].firstNonWhitespaceIndex).toBe 6
+ leadingWhitespaceTokens = editor.tokensForScreenRow(5).filter (token) -> token is 'leading-whitespace'
+ expect(leadingWhitespaceTokens.length).toBe(1)
describe ".indentLevelForLine(line)", ->
it "returns the indent level when the line has only leading whitespace", ->
@@ -4984,11 +5123,11 @@ describe "TextEditor", ->
runs ->
expect(editor.getGrammar()).toBe atom.grammars.nullGrammar
- expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBe 1
+ expect(editor.tokensForScreenRow(0).length).toBe(1)
atom.grammars.addGrammar(jsGrammar)
expect(editor.getGrammar()).toBe jsGrammar
- expect(editor.tokenizedLineForScreenRow(0).tokens.length).toBeGreaterThan 1
+ expect(editor.tokensForScreenRow(0).length).toBeGreaterThan 1
describe "editor.autoIndent", ->
describe "when editor.autoIndent is false (default)", ->
@@ -5024,7 +5163,7 @@ describe "TextEditor", ->
expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1
describe "when the line preceding the newline does't add a level of indentation", ->
- it "indents the new line to the same level a as the preceding line", ->
+ it "indents the new line to the same level as the preceding line", ->
editor.setCursorBufferPosition([5, 14])
editor.insertText('\n')
expect(editor.indentationForBufferRow(6)).toBe editor.indentationForBufferRow(5)
@@ -5129,10 +5268,34 @@ describe "TextEditor", ->
coffeeEditor.insertText("\n")
expect(coffeeEditor.lineTextForBufferRow(2)).toBe ""
+ describe "editor.atomicSoftTabs", ->
+ it "skips tab-length runs of leading whitespace when moving the cursor", ->
+ atom.config.set('editor.tabLength', 4)
+
+ atom.config.set('editor.atomicSoftTabs', true)
+ editor.setCursorScreenPosition([2, 3])
+ expect(editor.getCursorScreenPosition()).toEqual [2, 4]
+
+ atom.config.set('editor.atomicSoftTabs', false)
+ editor.setCursorScreenPosition([2, 3])
+ expect(editor.getCursorScreenPosition()).toEqual [2, 3]
+
+ atom.config.set('editor.atomicSoftTabs', true)
+ editor.setCursorScreenPosition([2, 3])
+ expect(editor.getCursorScreenPosition()).toEqual [2, 4]
+
+ atom.config.set('editor.atomicSoftTabs', false, scopeSelector: '.source.foo')
+ editor.setCursorScreenPosition([2, 3])
+ expect(editor.getCursorScreenPosition()).toEqual [2, 4]
+
+ atom.config.set('editor.atomicSoftTabs', false, scopeSelector: '.source.js')
+ editor.setCursorScreenPosition([2, 3])
+ expect(editor.getCursorScreenPosition()).toEqual [2, 3]
+
describe ".destroy()", ->
it "destroys marker layers associated with the text editor", ->
selectionsMarkerLayerId = editor.selectionsMarkerLayer.id
- foldsMarkerLayerId = editor.displayBuffer.foldsMarkerLayer.id
+ foldsMarkerLayerId = editor.displayLayer.foldsMarkerLayer.id
editor.destroy()
expect(buffer.getMarkerLayer(selectionsMarkerLayerId)).toBeUndefined()
expect(buffer.getMarkerLayer(foldsMarkerLayerId)).toBeUndefined()
@@ -5216,10 +5379,10 @@ describe "TextEditor", ->
expect(editor.getSelectedBufferRanges()).toEqual [[[3, 5], [3, 5]], [[9, 0], [14, 0]]]
# folds are also duplicated
- expect(editor.tokenizedLineForScreenRow(5).fold).toBeDefined()
- expect(editor.tokenizedLineForScreenRow(7).fold).toBeDefined()
- expect(editor.tokenizedLineForScreenRow(7).text).toBe " while(items.length > 0) {"
- expect(editor.tokenizedLineForScreenRow(8).text).toBe " return sort(left).concat(pivot).concat(sort(right));"
+ expect(editor.isFoldedAtScreenRow(5)).toBe(true)
+ expect(editor.isFoldedAtScreenRow(7)).toBe(true)
+ expect(editor.lineTextForScreenRow(7)).toBe " while(items.length > 0) {" + editor.displayLayer.foldCharacter
+ expect(editor.lineTextForScreenRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));"
it "duplicates all folded lines for empty selections on folded lines", ->
editor.foldBufferRow(4)
@@ -5415,17 +5578,15 @@ describe "TextEditor", ->
runs ->
editor.setText("// http://github.com")
- {tokens} = editor.tokenizedLineForScreenRow(0)
- expect(tokens[1].value).toBe " http://github.com"
- expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"]
+ tokens = editor.tokensForScreenRow(0)
+ expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js']
waitsForPromise ->
atom.packages.activatePackage('language-hyperlink')
runs ->
- {tokens} = editor.tokenizedLineForScreenRow(0)
- expect(tokens[2].value).toBe "http://github.com"
- expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "markup.underline.link.http.hyperlink"]
+ tokens = editor.tokensForScreenRow(0)
+ expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js', 'markup.underline.link.http.hyperlink']
describe "when the grammar is updated", ->
it "retokenizes existing buffers that contain tokens that match the injection selector", ->
@@ -5435,25 +5596,22 @@ describe "TextEditor", ->
runs ->
editor.setText("// SELECT * FROM OCTOCATS")
- {tokens} = editor.tokenizedLineForScreenRow(0)
- expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS"
- expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"]
+ tokens = editor.tokensForScreenRow(0)
+ expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js']
waitsForPromise ->
atom.packages.activatePackage('package-with-injection-selector')
runs ->
- {tokens} = editor.tokenizedLineForScreenRow(0)
- expect(tokens[1].value).toBe " SELECT * FROM OCTOCATS"
- expect(tokens[1].scopes).toEqual ["source.js", "comment.line.double-slash.js"]
+ tokens = editor.tokensForScreenRow(0)
+ expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js']
waitsForPromise ->
atom.packages.activatePackage('language-sql')
runs ->
- {tokens} = editor.tokenizedLineForScreenRow(0)
- expect(tokens[2].value).toBe "SELECT"
- expect(tokens[2].scopes).toEqual ["source.js", "comment.line.double-slash.js", "keyword.other.DML.sql"]
+ tokens = editor.tokensForScreenRow(0)
+ expect(tokens).toEqual ['source.js', 'comment.line.double-slash.js', 'punctuation.definition.comment.js', 'keyword.other.DML.sql', 'keyword.operator.star.sql', 'keyword.other.DML.sql']
describe ".normalizeTabsInBufferRange()", ->
it "normalizes tabs depending on the editor's soft tab/tab length settings", ->
@@ -5581,6 +5739,19 @@ describe "TextEditor", ->
expect(editor.getFirstVisibleScreenRow()).toEqual 89
expect(editor.getVisibleRowRange()).toEqual [89, 99]
+ describe "::scrollToScreenPosition(position, [options])", ->
+ it "triggers ::onDidRequestAutoscroll with the logical coordinates along with the options", ->
+ scrollSpy = jasmine.createSpy("::onDidRequestAutoscroll")
+ editor.onDidRequestAutoscroll(scrollSpy)
+
+ editor.scrollToScreenPosition([8, 20])
+ editor.scrollToScreenPosition([8, 20], center: true)
+ editor.scrollToScreenPosition([8, 20], center: false, reversed: true)
+
+ expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {})
+ expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: true})
+ expect(scrollSpy).toHaveBeenCalledWith(screenRange: [[8, 20], [8, 20]], options: {center: false, reversed: true})
+
describe '.get/setPlaceholderText()', ->
it 'can be created with placeholderText', ->
newEditor = atom.workspace.buildTextEditor(
@@ -5598,28 +5769,6 @@ describe "TextEditor", ->
expect(handler).toHaveBeenCalledWith 'OK'
expect(editor.getPlaceholderText()).toBe 'OK'
- describe ".checkoutHeadRevision()", ->
- it "reverts to the version of its file checked into the project repository", ->
- atom.config.set("editor.confirmCheckoutHeadRevision", false)
-
- editor.setCursorBufferPosition([0, 0])
- editor.insertText("---\n")
- expect(editor.lineTextForBufferRow(0)).toBe "---"
-
- waitsForPromise ->
- editor.checkoutHeadRevision()
-
- runs ->
- expect(editor.lineTextForBufferRow(0)).toBe "var quicksort = function () {"
-
- describe "when there's no repository for the editor's file", ->
- it "doesn't do anything", ->
- editor = atom.workspace.buildTextEditor()
- editor.setText("stuff")
- editor.checkoutHeadRevision()
-
- waitsForPromise -> editor.checkoutHeadRevision()
-
describe 'gutters', ->
describe 'the TextEditor constructor', ->
it 'creates a line-number gutter', ->
@@ -5748,6 +5897,7 @@ describe "TextEditor", ->
expect(editor.decorationsStateForScreenRowRange(0, 5)[decoration.id]).toEqual {
properties: {type: 'highlight', class: 'foo'}
screenRange: marker.getScreenRange(),
+ bufferRange: marker.getBufferRange(),
rangeIsReversed: false
}
@@ -5768,26 +5918,31 @@ describe "TextEditor", ->
expect(decorationState["#{layer1Decoration1.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'foo'},
screenRange: marker1.getRange(),
+ bufferRange: marker1.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration1.id}-#{marker2.id}"]).toEqual {
properties: {type: 'highlight', class: 'foo'},
screenRange: marker2.getRange(),
+ bufferRange: marker2.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker1.getRange(),
+ bufferRange: marker1.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker2.getRange(),
+ bufferRange: marker2.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual {
properties: {type: 'highlight', class: 'baz'},
screenRange: marker3.getRange(),
+ bufferRange: marker3.getRange(),
rangeIsReversed: false
}
@@ -5799,16 +5954,19 @@ describe "TextEditor", ->
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker1.getRange(),
+ bufferRange: marker1.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer1Decoration2.id}-#{marker2.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker2.getRange(),
+ bufferRange: marker2.getRange(),
rangeIsReversed: false
}
expect(decorationState["#{layer2Decoration.id}-#{marker3.id}"]).toEqual {
properties: {type: 'highlight', class: 'baz'},
screenRange: marker3.getRange(),
+ bufferRange: marker3.getRange(),
rangeIsReversed: false
}
@@ -5817,6 +5975,7 @@ describe "TextEditor", ->
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'quux'},
screenRange: marker1.getRange(),
+ bufferRange: marker1.getRange(),
rangeIsReversed: false
}
@@ -5825,55 +5984,46 @@ describe "TextEditor", ->
expect(decorationState["#{layer1Decoration2.id}-#{marker1.id}"]).toEqual {
properties: {type: 'highlight', class: 'bar'},
screenRange: marker1.getRange(),
+ bufferRange: marker1.getRange(),
rangeIsReversed: false
}
- describe "pending state", ->
- editor1 = null
- eventCount = null
-
+ describe "when the editor is constructed with the showInvisibles option set to false", ->
beforeEach ->
+ atom.workspace.destroyActivePane()
waitsForPromise ->
- atom.workspace.open('sample.txt', pending: true).then (o) -> editor1 = o
+ atom.workspace.open('sample.js', showInvisibles: false).then (o) -> editor = o
- runs ->
- eventCount = 0
- editor1.onDidTerminatePendingState -> eventCount++
+ it "ignores invisibles even if editor.showInvisibles is true", ->
+ atom.config.set('editor.showInvisibles', true)
+ expect(editor.lineTextForScreenRow(0).indexOf(atom.config.get('editor.invisibles.eol'))).toBe(-1)
- it "does not open file in pending state by default", ->
- expect(editor.isPending()).toBe false
+ describe "indent guides", ->
+ it "shows indent guides when `editor.showIndentGuide` is set to true and the editor is not mini", ->
+ editor.setText(" foo")
+ atom.config.set('editor.tabLength', 2)
- it "opens file in pending state if 'pending' option is true", ->
- expect(editor1.isPending()).toBe true
+ atom.config.set('editor.showIndentGuide', false)
+ expect(editor.tokensForScreenRow(0)).toEqual ['source.js', 'leading-whitespace']
- it "terminates pending state if ::terminatePendingState is invoked", ->
- editor1.terminatePendingState()
+ atom.config.set('editor.showIndentGuide', true)
+ expect(editor.tokensForScreenRow(0)).toEqual ['source.js', 'leading-whitespace indent-guide']
- expect(editor1.isPending()).toBe false
- expect(eventCount).toBe 1
+ editor.setMini(true)
+ expect(editor.tokensForScreenRow(0)).toEqual ['source.js', 'leading-whitespace']
- it "terminates pending state when buffer is changed", ->
- editor1.insertText('I\'ll be back!')
- advanceClock(editor1.getBuffer().stoppedChangingDelay)
+ describe "when the editor is constructed with the grammar option set", ->
+ beforeEach ->
+ atom.workspace.destroyActivePane()
+ waitsForPromise ->
+ atom.packages.activatePackage('language-coffee-script')
- expect(editor1.isPending()).toBe false
- expect(eventCount).toBe 1
+ waitsForPromise ->
+ atom.workspace.open('sample.js', grammar: atom.grammars.grammarForScopeName('source.coffee')).then (o) -> editor = o
- it "only calls terminate handler once when text is modified twice", ->
- editor1.insertText('Some text')
- advanceClock(editor1.getBuffer().stoppedChangingDelay)
+ it "sets the grammar", ->
+ expect(editor.getGrammar().name).toBe 'CoffeeScript'
- editor1.save()
-
- editor1.insertText('More text')
- advanceClock(editor1.getBuffer().stoppedChangingDelay)
-
- expect(editor1.isPending()).toBe false
- expect(eventCount).toBe 1
-
- it "only calls terminate handler once when terminatePendingState is called twice", ->
- editor1.terminatePendingState()
- editor1.terminatePendingState()
-
- expect(editor1.isPending()).toBe false
- expect(eventCount).toBe 1
+ describe "::getElement", ->
+ it "returns an element", ->
+ expect(editor.getElement() instanceof HTMLElement).toBe(true)
diff --git a/spec/text-utils-spec.coffee b/spec/text-utils-spec.coffee
index aa36c5003..bae7f5997 100644
--- a/spec/text-utils-spec.coffee
+++ b/spec/text-utils-spec.coffee
@@ -75,22 +75,23 @@ describe 'text utilities', ->
expect(textUtils.isKoreanCharacter("O")).toBe(false)
- describe ".isCJKCharacter(character)", ->
- it "returns true when the character is either a korean, half-width or double-width character", ->
- expect(textUtils.isCJKCharacter("我")).toBe(true)
- expect(textUtils.isCJKCharacter("私")).toBe(true)
- expect(textUtils.isCJKCharacter("B")).toBe(true)
- expect(textUtils.isCJKCharacter(",")).toBe(true)
- expect(textUtils.isCJKCharacter("¢")).toBe(true)
- expect(textUtils.isCJKCharacter("ハ")).toBe(true)
- expect(textUtils.isCJKCharacter("ヒ")).toBe(true)
- expect(textUtils.isCJKCharacter("ᆲ")).toBe(true)
- expect(textUtils.isCJKCharacter("■")).toBe(true)
- expect(textUtils.isCJKCharacter("우")).toBe(true)
- expect(textUtils.isCJKCharacter("가")).toBe(true)
- expect(textUtils.isCJKCharacter("ㅢ")).toBe(true)
- expect(textUtils.isCJKCharacter("ㄼ")).toBe(true)
+ describe ".isWrapBoundary(previousCharacter, character)", ->
+ it "returns true when the character is CJK or when the previous character is a space/tab", ->
+ anyCharacter = 'x'
+ expect(textUtils.isWrapBoundary(anyCharacter, "我")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "私")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "B")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, ",")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "¢")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "ハ")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "ヒ")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "ᆲ")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "■")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "우")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "가")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "ㅢ")).toBe(true)
+ expect(textUtils.isWrapBoundary(anyCharacter, "ㄼ")).toBe(true)
- expect(textUtils.isDoubleWidthCharacter("a")).toBe(false)
- expect(textUtils.isDoubleWidthCharacter("O")).toBe(false)
- expect(textUtils.isDoubleWidthCharacter("z")).toBe(false)
+ expect(textUtils.isWrapBoundary(' ', 'h')).toBe(true)
+ expect(textUtils.isWrapBoundary('\t', 'h')).toBe(true)
+ expect(textUtils.isWrapBoundary('a', 'h')).toBe(false)
diff --git a/spec/theme-manager-spec.coffee b/spec/theme-manager-spec.coffee
index ea0ca19e6..47b848809 100644
--- a/spec/theme-manager-spec.coffee
+++ b/spec/theme-manager-spec.coffee
@@ -175,7 +175,7 @@ describe "atom.themes", ->
expect(styleElementAddedHandler).toHaveBeenCalled()
element = document.querySelector('head style[source-path*="css.css"]')
- expect(element.getAttribute('source-path')).toBe atom.themes.stringToId(cssPath)
+ expect(element.getAttribute('source-path')).toEqualPath atom.themes.stringToId(cssPath)
expect(element.textContent).toBe fs.readFileSync(cssPath, 'utf8')
# doesn't append twice
@@ -194,7 +194,7 @@ describe "atom.themes", ->
expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1
element = document.querySelector('head style[source-path*="sample.less"]')
- expect(element.getAttribute('source-path')).toBe atom.themes.stringToId(lessPath)
+ expect(element.getAttribute('source-path')).toEqualPath atom.themes.stringToId(lessPath)
expect(element.textContent).toBe """
#header {
color: #4d926f;
@@ -213,9 +213,9 @@ describe "atom.themes", ->
it "supports requiring css and less stylesheets without an explicit extension", ->
atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'css')
- expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toBe atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('css.css'))
+ expect(document.querySelector('head style[source-path*="css.css"]').getAttribute('source-path')).toEqualPath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('css.css'))
atom.themes.requireStylesheet path.join(__dirname, 'fixtures', 'sample')
- expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toBe atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('sample.less'))
+ expect(document.querySelector('head style[source-path*="sample.less"]').getAttribute('source-path')).toEqualPath atom.themes.stringToId(atom.project.getDirectories()[0]?.resolve('sample.less'))
document.querySelector('head style[source-path*="css.css"]').remove()
document.querySelector('head style[source-path*="sample.less"]').remove()
diff --git a/spec/tokenized-buffer-iterator-spec.js b/spec/tokenized-buffer-iterator-spec.js
new file mode 100644
index 000000000..8d0e458f4
--- /dev/null
+++ b/spec/tokenized-buffer-iterator-spec.js
@@ -0,0 +1,103 @@
+/** @babel */
+
+import TokenizedBufferIterator from '../src/tokenized-buffer-iterator'
+import {Point} from 'text-buffer'
+
+describe('TokenizedBufferIterator', () => {
+ 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 grammarRegistry = {
+ scopeForId () {
+ return 'foo'
+ }
+ }
+
+ const iterator = new TokenizedBufferIterator(tokenizedBuffer, grammarRegistry)
+
+ iterator.seek(Point(0, 0))
+ expect(iterator.getPosition()).toEqual(Point(0, 0))
+ expect(iterator.getCloseTags()).toEqual([])
+ expect(iterator.getOpenTags()).toEqual(['foo'])
+
+ iterator.moveToSuccessor()
+ expect(iterator.getPosition()).toEqual(Point(0, 0))
+ expect(iterator.getCloseTags()).toEqual(['foo'])
+ expect(iterator.getOpenTags()).toEqual(['foo'])
+
+ iterator.moveToSuccessor()
+ expect(iterator.getCloseTags()).toEqual(['foo'])
+ expect(iterator.getOpenTags()).toEqual([])
+ })
+
+ it("reports a boundary at line end if the next line's open scopes don't match the containing tags for the current line", () => {
+ const tokenizedBuffer = {
+ tokenizedLineForRow (row) {
+ if (row === 0) {
+ return {
+ tags: [-1, 3, -2, -3],
+ text: 'bar',
+ openScopes: []
+ }
+ } else if (row === 1) {
+ return {
+ tags: [3],
+ text: 'baz',
+ openScopes: [-1]
+ }
+ } else if (row === 2) {
+ return {
+ tags: [-2],
+ text: '',
+ openScopes: [-1]
+ }
+ }
+ }
+ }
+
+ const grammarRegistry = {
+ scopeForId (id) {
+ if (id === -2 || id === -1) {
+ return 'foo'
+ } else if (id === -3) {
+ return 'qux'
+ }
+ }
+ }
+
+ const iterator = new TokenizedBufferIterator(tokenizedBuffer, grammarRegistry)
+
+ iterator.seek(Point(0, 0))
+ expect(iterator.getPosition()).toEqual(Point(0, 0))
+ expect(iterator.getCloseTags()).toEqual([])
+ expect(iterator.getOpenTags()).toEqual(['foo'])
+
+ iterator.moveToSuccessor()
+ expect(iterator.getPosition()).toEqual(Point(0, 3))
+ expect(iterator.getCloseTags()).toEqual(['foo'])
+ expect(iterator.getOpenTags()).toEqual(['qux'])
+
+ iterator.moveToSuccessor()
+ expect(iterator.getPosition()).toEqual(Point(0, 3))
+ expect(iterator.getCloseTags()).toEqual(['qux'])
+ expect(iterator.getOpenTags()).toEqual([])
+
+ iterator.moveToSuccessor()
+ expect(iterator.getPosition()).toEqual(Point(1, 0))
+ expect(iterator.getCloseTags()).toEqual([])
+ expect(iterator.getOpenTags()).toEqual(['foo'])
+
+ iterator.moveToSuccessor()
+ expect(iterator.getPosition()).toEqual(Point(2, 0))
+ expect(iterator.getCloseTags()).toEqual(['foo'])
+ expect(iterator.getOpenTags()).toEqual([])
+ })
+})
diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee
index 72b292cc2..ee418a386 100644
--- a/spec/tokenized-buffer-spec.coffee
+++ b/spec/tokenized-buffer-spec.coffee
@@ -1,5 +1,5 @@
TokenizedBuffer = require '../src/tokenized-buffer'
-TextBuffer = require 'text-buffer'
+{Point} = TextBuffer = require 'text-buffer'
_ = require 'underscore-plus'
describe "TokenizedBuffer", ->
@@ -134,13 +134,10 @@ describe "TokenizedBuffer", ->
describe "on construction", ->
it "initially creates un-tokenized screen lines, then tokenizes lines chunk at a time in the background", ->
line0 = tokenizedBuffer.tokenizedLineForRow(0)
- expect(line0.tokens.length).toBe 1
- expect(line0.tokens[0]).toEqual(value: line0.text, scopes: ['source.js'])
+ expect(line0.tokens).toEqual([value: line0.text, scopes: ['source.js']])
line11 = tokenizedBuffer.tokenizedLineForRow(11)
- expect(line11.tokens.length).toBe 2
- expect(line11.tokens[0]).toEqual(value: " ", scopes: ['source.js'], isAtomic: true)
- expect(line11.tokens[1]).toEqual(value: "return sort(Array.apply(this, arguments));", scopes: ['source.js'])
+ expect(line11.tokens).toEqual([value: " return sort(Array.apply(this, arguments));", scopes: ['source.js']])
# background tokenization has not begun
expect(tokenizedBuffer.tokenizedLineForRow(0).ruleStack).toBeUndefined()
@@ -202,8 +199,7 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.firstInvalidRow()).toBe 3
advanceClock()
- # we discover that row 2 starts a foldable region when line 3 gets tokenized
- expect(changeHandler).toHaveBeenCalledWith(start: 2, end: 7, delta: 0)
+ expect(changeHandler).toHaveBeenCalledWith(start: 3, end: 7, delta: 0)
expect(tokenizedBuffer.firstInvalidRow()).toBe 8
describe "when there is a buffer change surrounding an invalid row", ->
@@ -237,7 +233,7 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1]).toEqual(value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js'])
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[0]).toEqual(value: '7', scopes: ['source.js', 'constant.numeric.decimal.js'])
# line 2 is unchanged
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[2]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
+ expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
@@ -253,7 +249,7 @@ describe "TokenizedBuffer", ->
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
- expect(event).toEqual(start: 1, end: 2, delta: 0)
+ expect(event).toEqual(start: 2, end: 2, delta: 0)
changeHandler.reset()
advanceClock()
@@ -263,8 +259,7 @@ describe "TokenizedBuffer", ->
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
- # we discover that row 2 starts a foldable region when line 3 gets tokenized
- expect(event).toEqual(start: 2, end: 5, delta: 0)
+ expect(event).toEqual(start: 3, end: 5, delta: 0)
it "resumes highlighting with the state of the previous line", ->
buffer.insert([0, 0], '/*')
@@ -285,14 +280,14 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
# lines below deleted regions should be shifted upward
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[2]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
- expect(tokenizedBuffer.tokenizedLineForRow(3).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
- expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[4]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.comparison.js'])
+ expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
+ expect(tokenizedBuffer.tokenizedLineForRow(3).tokens[1]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
+ expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[1]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.comparison.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
- expect(event).toEqual(start: 0, end: 3, delta: -2) # starts at 0 because foldable on row 0 becomes false
+ expect(event).toEqual(start: 1, end: 3, delta: -2)
describe "when the change invalidates the tokenization of subsequent lines", ->
it "schedules the invalidated lines to be tokenized in the background", ->
@@ -305,7 +300,7 @@ describe "TokenizedBuffer", ->
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
- expect(event).toEqual(start: 1, end: 3, delta: -1)
+ expect(event).toEqual(start: 2, end: 3, delta: -1)
changeHandler.reset()
advanceClock()
@@ -314,8 +309,7 @@ describe "TokenizedBuffer", ->
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
- # we discover that row 2 starts a foldable region when line 3 gets tokenized
- expect(event).toEqual(start: 2, end: 4, delta: 0)
+ expect(event).toEqual(start: 3, end: 4, delta: 0)
describe "when lines are both updated and inserted", ->
it "updates tokens to reflect the change", ->
@@ -334,12 +328,12 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[4]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
# previous line 3 is pushed down to become line 5
- expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[4]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
+ expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[3]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
- expect(event).toEqual(start: 0, end: 2, delta: 2) # starts at 0 because .foldable becomes false on row 0
+ expect(event).toEqual(start: 1, end: 2, delta: 2)
describe "when the change invalidates the tokenization of subsequent lines", ->
it "schedules the invalidated lines to be tokenized in the background", ->
@@ -350,7 +344,7 @@ describe "TokenizedBuffer", ->
expect(changeHandler).toHaveBeenCalled()
[event] = changeHandler.argsForCall[0]
delete event.bufferChange
- expect(event).toEqual(start: 1, end: 2, delta: 2)
+ expect(event).toEqual(start: 2, end: 2, delta: 2)
expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.js']
expect(tokenizedBuffer.tokenizedLineForRow(3).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
expect(tokenizedBuffer.tokenizedLineForRow(4).tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
@@ -380,32 +374,6 @@ describe "TokenizedBuffer", ->
expect(tokenizedBuffer.tokenizedLineForRow(5).ruleStack?).toBeTruthy()
expect(tokenizedBuffer.tokenizedLineForRow(6).ruleStack?).toBeTruthy()
- it "tokenizes leading whitespace based on the new tab length", ->
- expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].isAtomic).toBeTruthy()
- expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].value).toBe " "
- expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].isAtomic).toBeTruthy()
- expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].value).toBe " "
-
- tokenizedBuffer.setTabLength(4)
- fullyTokenize(tokenizedBuffer)
-
- expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].isAtomic).toBeTruthy()
- expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[0].value).toBe " "
- expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].isAtomic).toBeFalsy()
- expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[1].value).toBe " current "
-
- it "does not tokenize whitespaces followed by combining characters as leading whitespace", ->
- buffer.setText(" \u030b")
- fullyTokenize(tokenizedBuffer)
-
- {tokens} = tokenizedBuffer.tokenizedLineForRow(0)
- expect(tokens[0].value).toBe " "
- expect(tokens[0].hasLeadingWhitespace()).toBe true
- expect(tokens[1].value).toBe " "
- expect(tokens[1].hasLeadingWhitespace()).toBe true
- expect(tokens[2].value).toBe " \u030b"
- expect(tokens[2].hasLeadingWhitespace()).toBe false
-
it "does not break out soft tabs across a scope boundary", ->
waitsForPromise ->
atom.packages.activatePackage('language-gfm')
@@ -442,133 +410,6 @@ describe "TokenizedBuffer", ->
beforeEach ->
fullyTokenize(tokenizedBuffer)
- it "renders each tab as its own atomic token with a value of size tabLength", ->
- tabAsSpaces = _.multiplyString(' ', tokenizedBuffer.getTabLength())
- screenLine0 = tokenizedBuffer.tokenizedLineForRow(0)
- expect(screenLine0.text).toBe "# Econ 101#{tabAsSpaces}"
- {tokens} = screenLine0
-
- expect(tokens.length).toBe 4
- expect(tokens[0].value).toBe "#"
- expect(tokens[1].value).toBe " Econ 101"
- expect(tokens[2].value).toBe tabAsSpaces
- expect(tokens[2].scopes).toEqual tokens[1].scopes
- expect(tokens[2].isAtomic).toBeTruthy()
- expect(tokens[3].value).toBe ""
-
- expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "#{tabAsSpaces} buy()#{tabAsSpaces}while supply > demand"
-
- it "aligns the hard tabs to the correct tab stop column", ->
- buffer.setText """
- 1\t2 \t3\t4
- 12\t3 \t4\t5
- 123\t4 \t5\t6
- """
-
- tokenizedBuffer.setTabLength(4)
- fullyTokenize(tokenizedBuffer)
-
- expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4"
- expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 3
-
- expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5"
- expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 2
-
- expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6"
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 1
-
- tokenizedBuffer.setTabLength(3)
- fullyTokenize(tokenizedBuffer)
-
- expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4"
- expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 2
-
- expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5"
- expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 1
-
- expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6"
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 3
-
- tokenizedBuffer.setTabLength(2)
- fullyTokenize(tokenizedBuffer)
-
- expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4"
- expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 1
-
- expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5"
- expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 2
-
- expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6"
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 1
-
- tokenizedBuffer.setTabLength(1)
- fullyTokenize(tokenizedBuffer)
-
- expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "1 2 3 4"
- expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].bufferDelta).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[1].screenDelta).toBe 1
-
- expect(tokenizedBuffer.tokenizedLineForRow(1).text).toBe "12 3 4 5"
- expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].bufferDelta).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].screenDelta).toBe 1
-
- expect(tokenizedBuffer.tokenizedLineForRow(2).text).toBe "123 4 5 6"
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].bufferDelta).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].screenDelta).toBe 1
-
- describe "when the buffer contains UTF-8 surrogate pairs", ->
- beforeEach ->
- waitsForPromise ->
- atom.packages.activatePackage('language-javascript')
-
- runs ->
- buffer = atom.project.bufferForPathSync 'sample-with-pairs.js'
- buffer.setText """
- 'abc\uD835\uDF97def'
- //\uD835\uDF97xyz
- """
- tokenizedBuffer = new TokenizedBuffer({
- buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
- })
- fullyTokenize(tokenizedBuffer)
-
- afterEach ->
- tokenizedBuffer.destroy()
- buffer.release()
-
- it "renders each UTF-8 surrogate pair as its own atomic token", ->
- screenLine0 = tokenizedBuffer.tokenizedLineForRow(0)
- expect(screenLine0.text).toBe "'abc\uD835\uDF97def'"
- {tokens} = screenLine0
-
- expect(tokens.length).toBe 5
- expect(tokens[0].value).toBe "'"
- expect(tokens[1].value).toBe "abc"
- expect(tokens[2].value).toBe "\uD835\uDF97"
- expect(tokens[2].isAtomic).toBeTruthy()
- expect(tokens[3].value).toBe "def"
- expect(tokens[4].value).toBe "'"
-
- screenLine1 = tokenizedBuffer.tokenizedLineForRow(1)
- expect(screenLine1.text).toBe "//\uD835\uDF97xyz"
- {tokens} = screenLine1
-
- expect(tokens.length).toBe 4
- expect(tokens[0].value).toBe '//'
- expect(tokens[1].value).toBe '\uD835\uDF97'
- expect(tokens[1].value).toBeTruthy()
- expect(tokens[2].value).toBe 'xyz'
- expect(tokens[3].value).toBe ''
-
describe "when the grammar is tokenized", ->
it "emits the `tokenized` event", ->
editor = null
@@ -578,7 +419,7 @@ describe "TokenizedBuffer", ->
atom.workspace.open('sample.js').then (o) -> editor = o
runs ->
- tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
+ tokenizedBuffer = editor.tokenizedBuffer
tokenizedBuffer.onDidTokenize tokenizedHandler
fullyTokenize(tokenizedBuffer)
expect(tokenizedHandler.callCount).toBe(1)
@@ -591,7 +432,7 @@ describe "TokenizedBuffer", ->
atom.workspace.open('sample.js').then (o) -> editor = o
runs ->
- tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
+ tokenizedBuffer = editor.tokenizedBuffer
fullyTokenize(tokenizedBuffer)
tokenizedBuffer.onDidTokenize tokenizedHandler
@@ -609,7 +450,7 @@ describe "TokenizedBuffer", ->
atom.workspace.open('coffee.coffee').then (o) -> editor = o
runs ->
- tokenizedBuffer = editor.displayBuffer.tokenizedBuffer
+ tokenizedBuffer = editor.tokenizedBuffer
tokenizedBuffer.onDidTokenize tokenizedHandler
fullyTokenize(tokenizedBuffer)
tokenizedHandler.reset()
@@ -686,132 +527,7 @@ describe "TokenizedBuffer", ->
it "returns the range covered by all contigous tokens (within a single line)", ->
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual [[1, 6], [1, 28]]
- describe "when the editor.tabLength config value changes", ->
- it "updates the tab length of the tokenized lines", ->
- buffer = atom.project.bufferForPathSync('sample.js')
- buffer.setText('\ttest')
- tokenizedBuffer = new TokenizedBuffer({
- buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
- })
- fullyTokenize(tokenizedBuffer)
- expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
- atom.config.set('editor.tabLength', 6)
- expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
-
- it "does not allow the tab length to be less than 1", ->
- buffer = atom.project.bufferForPathSync('sample.js')
- buffer.setText('\ttest')
- tokenizedBuffer = new TokenizedBuffer({
- buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
- })
- fullyTokenize(tokenizedBuffer)
- expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
- atom.config.set('editor.tabLength', 1)
- expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
- atom.config.set('editor.tabLength', 0)
- expect(tokenizedBuffer.tokenForPosition([0, 0]).value).toBe ' '
-
- describe "when the invisibles value changes", ->
- beforeEach ->
-
- it "updates the tokens with the appropriate invisible characters", ->
- buffer = new TextBuffer(text: " \t a line with tabs\tand \tspaces \t ")
- tokenizedBuffer = new TokenizedBuffer({
- buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
- })
- fullyTokenize(tokenizedBuffer)
-
- atom.config.set("editor.showInvisibles", true)
- atom.config.set("editor.invisibles", space: 'S', tab: 'T')
- fullyTokenize(tokenizedBuffer)
-
- expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe "SST Sa line with tabsTand T spacesSTS"
- # Also needs to work for copies
- expect(tokenizedBuffer.tokenizedLineForRow(0).copy().text).toBe "SST Sa line with tabsTand T spacesSTS"
-
- it "assigns endOfLineInvisibles to tokenized lines", ->
- buffer = new TextBuffer(text: "a line that ends in a carriage-return-line-feed \r\na line that ends in just a line-feed\na line with no ending")
- tokenizedBuffer = new TokenizedBuffer({
- buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
- })
-
- atom.config.set('editor.showInvisibles', true)
- atom.config.set("editor.invisibles", cr: 'R', eol: 'N')
- fullyTokenize(tokenizedBuffer)
-
- expect(tokenizedBuffer.tokenizedLineForRow(0).endOfLineInvisibles).toEqual ['R', 'N']
- expect(tokenizedBuffer.tokenizedLineForRow(1).endOfLineInvisibles).toEqual ['N']
-
- # Lines ending in soft wraps get no invisibles
- [left, right] = tokenizedBuffer.tokenizedLineForRow(0).softWrapAt(20)
- expect(left.endOfLineInvisibles).toBe null
- expect(right.endOfLineInvisibles).toEqual ['R', 'N']
-
- atom.config.set("editor.invisibles", cr: 'R', eol: false)
- expect(tokenizedBuffer.tokenizedLineForRow(0).endOfLineInvisibles).toEqual ['R']
- expect(tokenizedBuffer.tokenizedLineForRow(1).endOfLineInvisibles).toEqual []
-
- describe "leading and trailing whitespace", ->
- beforeEach ->
- buffer = atom.project.bufferForPathSync('sample.js')
- tokenizedBuffer = new TokenizedBuffer({
- buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
- })
- fullyTokenize(tokenizedBuffer)
-
- it "assigns ::firstNonWhitespaceIndex on tokens that have leading whitespace", ->
- expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[0].firstNonWhitespaceIndex).toBe null
- expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[0].firstNonWhitespaceIndex).toBe 2
- expect(tokenizedBuffer.tokenizedLineForRow(1).tokens[1].firstNonWhitespaceIndex).toBe null
-
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[0].firstNonWhitespaceIndex).toBe 2
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[1].firstNonWhitespaceIndex).toBe 2
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[2].firstNonWhitespaceIndex).toBe null
-
- # The 4th token *has* leading whitespace, but isn't entirely whitespace
- buffer.insert([5, 0], ' ')
- expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[3].firstNonWhitespaceIndex).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(5).tokens[4].firstNonWhitespaceIndex).toBe null
-
- # Lines that are *only* whitespace are not considered to have leading whitespace
- buffer.insert([10, 0], ' ')
- expect(tokenizedBuffer.tokenizedLineForRow(10).tokens[0].firstNonWhitespaceIndex).toBe null
-
- it "assigns ::firstTrailingWhitespaceIndex on tokens that have trailing whitespace", ->
- buffer.insert([0, Infinity], ' ')
- expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[11].firstTrailingWhitespaceIndex).toBe null
- expect(tokenizedBuffer.tokenizedLineForRow(0).tokens[12].firstTrailingWhitespaceIndex).toBe 0
-
- # The last token *has* trailing whitespace, but isn't entirely whitespace
- buffer.setTextInRange([[2, 39], [2, 40]], ' ')
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[14].firstTrailingWhitespaceIndex).toBe null
- expect(tokenizedBuffer.tokenizedLineForRow(2).tokens[15].firstTrailingWhitespaceIndex).toBe 6
-
- # Lines that are *only* whitespace are considered to have trailing whitespace
- buffer.insert([10, 0], ' ')
- expect(tokenizedBuffer.tokenizedLineForRow(10).tokens[0].firstTrailingWhitespaceIndex).toBe 0
-
- it "only marks trailing whitespace on the last segment of a soft-wrapped line", ->
- buffer.insert([0, Infinity], ' ')
- tokenizedLine = tokenizedBuffer.tokenizedLineForRow(0)
- [segment1, segment2] = tokenizedLine.softWrapAt(16)
- expect(segment1.tokens[5].value).toBe ' '
- expect(segment1.tokens[5].firstTrailingWhitespaceIndex).toBe null
- expect(segment2.tokens[6].value).toBe ' '
- expect(segment2.tokens[6].firstTrailingWhitespaceIndex).toBe 0
-
- it "sets leading and trailing whitespace correctly on a line with invisible characters that is copied", ->
- buffer.setText(" \t a line with tabs\tand \tspaces \t ")
-
- atom.config.set("editor.showInvisibles", true)
- atom.config.set("editor.invisibles", space: 'S', tab: 'T')
- fullyTokenize(tokenizedBuffer)
-
- line = tokenizedBuffer.tokenizedLineForRow(0).copy()
- expect(line.tokens[0].firstNonWhitespaceIndex).toBe 2
- expect(line.tokens[line.tokens.length - 1].firstTrailingWhitespaceIndex).toBe 0
-
- describe ".indentLevel on tokenized lines", ->
+ describe ".indentLevelForRow(row)", ->
beforeEach ->
buffer = atom.project.bufferForPathSync('sample.js')
tokenizedBuffer = new TokenizedBuffer({
@@ -821,43 +537,43 @@ describe "TokenizedBuffer", ->
describe "when the line is non-empty", ->
it "has an indent level based on the leading whitespace on the line", ->
- expect(tokenizedBuffer.tokenizedLineForRow(0).indentLevel).toBe 0
- expect(tokenizedBuffer.tokenizedLineForRow(1).indentLevel).toBe 1
- expect(tokenizedBuffer.tokenizedLineForRow(2).indentLevel).toBe 2
+ expect(tokenizedBuffer.indentLevelForRow(0)).toBe 0
+ expect(tokenizedBuffer.indentLevelForRow(1)).toBe 1
+ expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2
buffer.insert([2, 0], ' ')
- expect(tokenizedBuffer.tokenizedLineForRow(2).indentLevel).toBe 2.5
+ expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2.5
describe "when the line is empty", ->
it "assumes the indentation level of the first non-empty line below or above if one exists", ->
buffer.insert([12, 0], ' ')
buffer.insert([12, Infinity], '\n\n')
- expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 2
- expect(tokenizedBuffer.tokenizedLineForRow(14).indentLevel).toBe 2
+ expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
+ expect(tokenizedBuffer.indentLevelForRow(14)).toBe 2
buffer.insert([1, Infinity], '\n\n')
- expect(tokenizedBuffer.tokenizedLineForRow(2).indentLevel).toBe 2
- expect(tokenizedBuffer.tokenizedLineForRow(3).indentLevel).toBe 2
+ expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2
+ expect(tokenizedBuffer.indentLevelForRow(3)).toBe 2
buffer.setText('\n\n\n')
- expect(tokenizedBuffer.tokenizedLineForRow(1).indentLevel).toBe 0
+ expect(tokenizedBuffer.indentLevelForRow(1)).toBe 0
describe "when the changed lines are surrounded by whitespace-only lines", ->
it "updates the indentLevel of empty lines that precede the change", ->
- expect(tokenizedBuffer.tokenizedLineForRow(12).indentLevel).toBe 0
+ expect(tokenizedBuffer.indentLevelForRow(12)).toBe 0
buffer.insert([12, 0], '\n')
buffer.insert([13, 0], ' ')
- expect(tokenizedBuffer.tokenizedLineForRow(12).indentLevel).toBe 1
+ expect(tokenizedBuffer.indentLevelForRow(12)).toBe 1
it "updates empty line indent guides when the empty line is the last line", ->
buffer.insert([12, 2], '\n')
# The newline and the tab need to be in two different operations to surface the bug
buffer.insert([12, 0], ' ')
- expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 1
+ expect(tokenizedBuffer.indentLevelForRow(13)).toBe 1
buffer.insert([12, 0], ' ')
- expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 2
+ expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
expect(tokenizedBuffer.tokenizedLineForRow(14)).not.toBeDefined()
it "updates the indentLevel of empty lines surrounding a change that inserts lines", ->
@@ -865,24 +581,24 @@ describe "TokenizedBuffer", ->
buffer.insert([7, 0], '\n\n')
buffer.insert([5, 0], '\n\n')
- expect(tokenizedBuffer.tokenizedLineForRow(5).indentLevel).toBe 3
- expect(tokenizedBuffer.tokenizedLineForRow(6).indentLevel).toBe 3
- expect(tokenizedBuffer.tokenizedLineForRow(9).indentLevel).toBe 3
- expect(tokenizedBuffer.tokenizedLineForRow(10).indentLevel).toBe 3
- expect(tokenizedBuffer.tokenizedLineForRow(11).indentLevel).toBe 2
+ expect(tokenizedBuffer.indentLevelForRow(5)).toBe 3
+ expect(tokenizedBuffer.indentLevelForRow(6)).toBe 3
+ expect(tokenizedBuffer.indentLevelForRow(9)).toBe 3
+ expect(tokenizedBuffer.indentLevelForRow(10)).toBe 3
+ expect(tokenizedBuffer.indentLevelForRow(11)).toBe 2
tokenizedBuffer.onDidChange changeHandler = jasmine.createSpy('changeHandler')
buffer.setTextInRange([[7, 0], [8, 65]], ' one\n two\n three\n four')
delete changeHandler.argsForCall[0][0].bufferChange
- expect(changeHandler).toHaveBeenCalledWith(start: 5, end: 10, delta: 2)
+ expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, delta: 2)
- expect(tokenizedBuffer.tokenizedLineForRow(5).indentLevel).toBe 4
- expect(tokenizedBuffer.tokenizedLineForRow(6).indentLevel).toBe 4
- expect(tokenizedBuffer.tokenizedLineForRow(11).indentLevel).toBe 4
- expect(tokenizedBuffer.tokenizedLineForRow(12).indentLevel).toBe 4
- expect(tokenizedBuffer.tokenizedLineForRow(13).indentLevel).toBe 2
+ expect(tokenizedBuffer.indentLevelForRow(5)).toBe 4
+ expect(tokenizedBuffer.indentLevelForRow(6)).toBe 4
+ expect(tokenizedBuffer.indentLevelForRow(11)).toBe 4
+ expect(tokenizedBuffer.indentLevelForRow(12)).toBe 4
+ expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
it "updates the indentLevel of empty lines surrounding a change that removes lines", ->
# create some new lines
@@ -894,16 +610,16 @@ describe "TokenizedBuffer", ->
buffer.setTextInRange([[7, 0], [8, 65]], ' ok')
delete changeHandler.argsForCall[0][0].bufferChange
- expect(changeHandler).toHaveBeenCalledWith(start: 4, end: 10, delta: -1) # starts at row 4 because it became foldable
+ expect(changeHandler).toHaveBeenCalledWith(start: 7, end: 8, delta: -1)
- expect(tokenizedBuffer.tokenizedLineForRow(5).indentLevel).toBe 2
- expect(tokenizedBuffer.tokenizedLineForRow(6).indentLevel).toBe 2
- expect(tokenizedBuffer.tokenizedLineForRow(7).indentLevel).toBe 2 # new text
- expect(tokenizedBuffer.tokenizedLineForRow(8).indentLevel).toBe 2
- expect(tokenizedBuffer.tokenizedLineForRow(9).indentLevel).toBe 2
- expect(tokenizedBuffer.tokenizedLineForRow(10).indentLevel).toBe 2 # }
+ expect(tokenizedBuffer.indentLevelForRow(5)).toBe 2
+ expect(tokenizedBuffer.indentLevelForRow(6)).toBe 2
+ expect(tokenizedBuffer.indentLevelForRow(7)).toBe 2 # new text
+ expect(tokenizedBuffer.indentLevelForRow(8)).toBe 2
+ expect(tokenizedBuffer.indentLevelForRow(9)).toBe 2
+ expect(tokenizedBuffer.indentLevelForRow(10)).toBe 2 # }
- describe ".foldable on tokenized lines", ->
+ describe "::isFoldableAtRow(row)", ->
changes = null
beforeEach ->
@@ -915,74 +631,66 @@ describe "TokenizedBuffer", ->
buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
})
fullyTokenize(tokenizedBuffer)
- tokenizedBuffer.onDidChange (change) ->
- delete change.bufferChange
- changes.push(change)
- it "sets .foldable to true on the first line of multi-line comments", ->
- expect(tokenizedBuffer.tokenizedLineForRow(0).foldable).toBe true
- expect(tokenizedBuffer.tokenizedLineForRow(1).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(2).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(3).foldable).toBe true # because of indent
- expect(tokenizedBuffer.tokenizedLineForRow(13).foldable).toBe true
- expect(tokenizedBuffer.tokenizedLineForRow(14).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(15).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(16).foldable).toBe false
+ 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(changes).toEqual [{start: 0, end: 1, delta: 1}]
- expect(tokenizedBuffer.tokenizedLineForRow(0).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(1).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(2).foldable).toBe true
- expect(tokenizedBuffer.tokenizedLineForRow(3).foldable).toBe false
+ expect(tokenizedBuffer.isFoldableAtRow(0)).toBe false
+ expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false
+ expect(tokenizedBuffer.isFoldableAtRow(2)).toBe true
+ expect(tokenizedBuffer.isFoldableAtRow(3)).toBe false
- changes = []
buffer.undo()
- expect(changes).toEqual [{start: 0, end: 2, delta: -1}]
- expect(tokenizedBuffer.tokenizedLineForRow(0).foldable).toBe true
- expect(tokenizedBuffer.tokenizedLineForRow(1).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(2).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(3).foldable).toBe true # because of indent
- it "sets .foldable to true on non-comment lines that precede an increase in indentation", ->
+ 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.tokenizedLineForRow(1).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(2).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(3).foldable).toBe true
- expect(tokenizedBuffer.tokenizedLineForRow(4).foldable).toBe true
- expect(tokenizedBuffer.tokenizedLineForRow(5).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(6).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(7).foldable).toBe true
- expect(tokenizedBuffer.tokenizedLineForRow(8).foldable).toBe false
- changes = []
+ 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(changes).toEqual [{start: 6, end: 7, delta: 0}]
- expect(tokenizedBuffer.tokenizedLineForRow(6).foldable).toBe true
- expect(tokenizedBuffer.tokenizedLineForRow(7).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(8).foldable).toBe false
- changes = []
+ expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true
+ expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false
+ expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
+
buffer.undo()
- expect(changes).toEqual [{start: 6, end: 7, delta: 0}]
- expect(tokenizedBuffer.tokenizedLineForRow(6).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(7).foldable).toBe true
- expect(tokenizedBuffer.tokenizedLineForRow(8).foldable).toBe false
- changes = []
+ 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(changes).toEqual [{start: 6, end: 7, delta: 2}]
- expect(tokenizedBuffer.tokenizedLineForRow(6).foldable).toBe true
- expect(tokenizedBuffer.tokenizedLineForRow(7).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(8).foldable).toBe false
- changes = []
+ expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true
+ expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false
+ expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
+
buffer.insert([9, 0], " ")
- expect(changes).toEqual [{start: 9, end: 9, delta: 0}]
- expect(tokenizedBuffer.tokenizedLineForRow(6).foldable).toBe true
- expect(tokenizedBuffer.tokenizedLineForRow(7).foldable).toBe false
- expect(tokenizedBuffer.tokenizedLineForRow(8).foldable).toBe false
+
+ expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true
+ expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false
+ expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
describe "when the buffer is configured with the null grammar", ->
it "uses the placeholder tokens and does not actually tokenize using the grammar", ->
@@ -1061,3 +769,107 @@ describe "TokenizedBuffer", ->
runs ->
expect(coffeeCalled).toBe true
+
+ 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, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
+ tokenizedBuffer.setGrammar(atom.grammars.selectGrammar(".js"))
+ fullyTokenize(tokenizedBuffer)
+
+ iterator = tokenizedBuffer.buildIterator()
+ iterator.seek(Point(0, 0))
+
+ expectedBoundaries = [
+ {position: Point(0, 0), closeTags: [], openTags: ["source.js", "storage.type.var.js"]}
+ {position: Point(0, 3), closeTags: ["storage.type.var.js"], openTags: []}
+ {position: Point(0, 8), closeTags: [], openTags: ["keyword.operator.assignment.js"]}
+ {position: Point(0, 9), closeTags: ["keyword.operator.assignment.js"], openTags: []}
+ {position: Point(0, 10), closeTags: [], openTags: ["constant.numeric.decimal.js"]}
+ {position: Point(0, 11), closeTags: ["constant.numeric.decimal.js"], openTags: []}
+ {position: Point(0, 12), closeTags: [], openTags: ["comment.block.js", "punctuation.definition.comment.js"]}
+ {position: Point(0, 14), closeTags: ["punctuation.definition.comment.js"], openTags: []}
+ {position: Point(1, 5), closeTags: [], openTags: ["punctuation.definition.comment.js"]}
+ {position: Point(1, 7), closeTags: ["punctuation.definition.comment.js", "comment.block.js"], openTags: ["storage.type.var.js"]}
+ {position: Point(1, 10), closeTags: ["storage.type.var.js"], openTags: []}
+ {position: Point(1, 15), closeTags: [], openTags: ["keyword.operator.assignment.js"]}
+ {position: Point(1, 16), closeTags: ["keyword.operator.assignment.js"], openTags: []}
+ {position: Point(1, 17), closeTags: [], openTags: ["constant.numeric.decimal.js"]}
+ {position: Point(1, 18), closeTags: ["constant.numeric.decimal.js"], openTags: []}
+ ]
+
+ loop
+ boundary = {
+ position: iterator.getPosition(),
+ closeTags: iterator.getCloseTags(),
+ openTags: iterator.getOpenTags()
+ }
+
+ expect(boundary).toEqual(expectedBoundaries.shift())
+ break unless iterator.moveToSuccessor()
+
+ expect(iterator.seek(Point(0, 1))).toEqual(["source.js", "storage.type.var.js"])
+ expect(iterator.getPosition()).toEqual(Point(0, 3))
+ expect(iterator.seek(Point(0, 8))).toEqual(["source.js"])
+ expect(iterator.getPosition()).toEqual(Point(0, 8))
+ expect(iterator.seek(Point(1, 0))).toEqual(["source.js", "comment.block.js"])
+ expect(iterator.getPosition()).toEqual(Point(1, 5))
+ expect(iterator.seek(Point(1, 18))).toEqual(["source.js", "constant.numeric.decimal.js"])
+ expect(iterator.getPosition()).toEqual(Point(1, 18))
+
+ expect(iterator.seek(Point(2, 0))).toEqual(["source.js"])
+ iterator.moveToSuccessor() # ensure we don't infinitely loop (regression test)
+
+ it "does not report columns beyond the length of the line", ->
+ waitsForPromise ->
+ atom.packages.activatePackage('language-coffee-script')
+
+ runs ->
+ buffer = new TextBuffer(text: "# hello\n# world")
+ tokenizedBuffer = new TokenizedBuffer({
+ buffer, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
+ tokenizedBuffer.setGrammar(atom.grammars.selectGrammar(".coffee"))
+ fullyTokenize(tokenizedBuffer)
+
+ 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)", ->
+ 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, config: atom.config, grammarRegistry: atom.grammars, packageManager: atom.packages, assert: atom.assert
+ })
+ tokenizedBuffer.setGrammar(grammar)
+ fullyTokenize(tokenizedBuffer)
+
+ iterator = tokenizedBuffer.buildIterator()
+ iterator.seek(Point(1, 0))
+
+ expect(iterator.getPosition()).toEqual([1, 0])
+ expect(iterator.getCloseTags()).toEqual ['blue.broken']
+ expect(iterator.getOpenTags()).toEqual ['yellow.broken']
diff --git a/spec/tokenized-line-spec.coffee b/spec/tokenized-line-spec.coffee
deleted file mode 100644
index f1dce7b9e..000000000
--- a/spec/tokenized-line-spec.coffee
+++ /dev/null
@@ -1,19 +0,0 @@
-describe "TokenizedLine", ->
- editor = null
-
- beforeEach ->
- waitsForPromise -> atom.packages.activatePackage('language-coffee-script')
-
- describe "::isOnlyWhitespace()", ->
- beforeEach ->
- waitsForPromise ->
- atom.workspace.open('coffee.coffee').then (o) -> editor = o
-
- it "returns true when the line is only whitespace", ->
- expect(editor.tokenizedLineForScreenRow(3).isOnlyWhitespace()).toBe true
- expect(editor.tokenizedLineForScreenRow(7).isOnlyWhitespace()).toBe true
- expect(editor.tokenizedLineForScreenRow(23).isOnlyWhitespace()).toBe true
-
- it "returns false when the line is not only whitespace", ->
- expect(editor.tokenizedLineForScreenRow(0).isOnlyWhitespace()).toBe false
- expect(editor.tokenizedLineForScreenRow(2).isOnlyWhitespace()).toBe false
diff --git a/spec/tooltip-manager-spec.coffee b/spec/tooltip-manager-spec.coffee
index 87082504a..d4bfc1bd6 100644
--- a/spec/tooltip-manager-spec.coffee
+++ b/spec/tooltip-manager-spec.coffee
@@ -28,6 +28,12 @@ describe "TooltipManager", ->
hover element, ->
expect(document.body.querySelector(".tooltip")).toHaveText("Title")
+ it "creates a tooltip immediately if the trigger type is manual", ->
+ disposable = manager.add element, title: "Title", trigger: "manual"
+ expect(document.body.querySelector(".tooltip")).toHaveText("Title")
+ disposable.dispose()
+ expect(document.body.querySelector(".tooltip")).toBeNull()
+
it "allows jQuery elements to be passed as the target", ->
element2 = document.createElement('div')
jasmine.attachToDOM(element2)
diff --git a/spec/view-registry-spec.coffee b/spec/view-registry-spec.coffee
index 16672b25d..68a482b48 100644
--- a/spec/view-registry-spec.coffee
+++ b/spec/view-registry-spec.coffee
@@ -23,6 +23,15 @@ describe "ViewRegistry", ->
component = new TestComponent
expect(registry.getView(component)).toBe component.element
+ describe "when passed an object with a getElement function", ->
+ it "returns the return value of getElement if it's an instance of HTMLElement", ->
+ class TestComponent
+ getElement: ->
+ @myElement ?= document.createElement('div')
+
+ component = new TestComponent
+ expect(registry.getView(component)).toBe component.myElement
+
describe "when passed a model object", ->
describe "when a view provider is registered matching the object's constructor", ->
it "constructs a view element and assigns the model on it", ->
diff --git a/spec/window-event-handler-spec.coffee b/spec/window-event-handler-spec.coffee
index a988ae7de..22f43c90f 100644
--- a/spec/window-event-handler-spec.coffee
+++ b/spec/window-event-handler-spec.coffee
@@ -4,7 +4,7 @@ fs = require 'fs-plus'
temp = require 'temp'
TextEditor = require '../src/text-editor'
WindowEventHandler = require '../src/window-event-handler'
-ipc = require 'ipc'
+{ipcRenderer} = require 'electron'
describe "WindowEventHandler", ->
[projectPath, windowEventHandler] = []
@@ -53,7 +53,7 @@ describe "WindowEventHandler", ->
describe "beforeunload event", ->
beforeEach ->
jasmine.unspy(TextEditor.prototype, "shouldPromptToSave")
- spyOn(ipc, 'send')
+ spyOn(ipcRenderer, 'send')
describe "when pane items are modified", ->
editor = null
@@ -65,17 +65,17 @@ describe "WindowEventHandler", ->
spyOn(atom.workspace, 'confirmClose').andReturn(true)
window.dispatchEvent(new CustomEvent('beforeunload'))
expect(atom.workspace.confirmClose).toHaveBeenCalled()
- expect(ipc.send).not.toHaveBeenCalledWith('did-cancel-window-unload')
+ expect(ipcRenderer.send).not.toHaveBeenCalledWith('did-cancel-window-unload')
it "cancels the unload if the user selects cancel", ->
spyOn(atom.workspace, 'confirmClose').andReturn(false)
window.dispatchEvent(new CustomEvent('beforeunload'))
expect(atom.workspace.confirmClose).toHaveBeenCalled()
- expect(ipc.send).toHaveBeenCalledWith('did-cancel-window-unload')
+ expect(ipcRenderer.send).toHaveBeenCalledWith('did-cancel-window-unload')
describe "when a link is clicked", ->
it "opens the http/https links in an external application", ->
- shell = require 'shell'
+ {shell} = require 'electron'
spyOn(shell, 'openExternal')
link = document.createElement('a')
diff --git a/spec/workspace-element-spec.coffee b/spec/workspace-element-spec.coffee
index 883bad2fc..efb1d1b26 100644
--- a/spec/workspace-element-spec.coffee
+++ b/spec/workspace-element-spec.coffee
@@ -1,4 +1,4 @@
-ipc = require 'ipc'
+{ipcRenderer} = require 'electron'
path = require 'path'
temp = require('temp').track()
@@ -47,9 +47,14 @@ describe "WorkspaceElement", ->
it "updates the font-family based on the 'editor.fontFamily' config value", ->
initialCharWidth = editor.getDefaultCharWidth()
- expect(getComputedStyle(editorElement).fontFamily).toBe atom.config.get('editor.fontFamily')
+ fontFamily = atom.config.get('editor.fontFamily')
+ fontFamily += ", 'Apple Color Emoji'" if process.platform is 'darwin'
+ expect(getComputedStyle(editorElement).fontFamily).toBe fontFamily
+
atom.config.set('editor.fontFamily', 'sans-serif')
- expect(getComputedStyle(editorElement).fontFamily).toBe atom.config.get('editor.fontFamily')
+ fontFamily = atom.config.get('editor.fontFamily')
+ fontFamily += ", 'Apple Color Emoji'" if process.platform is 'darwin'
+ expect(getComputedStyle(editorElement).fontFamily).toBe fontFamily
expect(editor.getDefaultCharWidth()).not.toBe initialCharWidth
it "updates the line-height based on the 'editor.lineHeight' config value", ->
@@ -127,35 +132,35 @@ describe "WorkspaceElement", ->
describe "the 'window:run-package-specs' command", ->
it "runs the package specs for the active item's project path, or the first project path", ->
workspaceElement = atom.views.getView(atom.workspace)
- spyOn(ipc, 'send')
+ spyOn(ipcRenderer, 'send')
# No project paths. Don't try to run specs.
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
- expect(ipc.send).not.toHaveBeenCalledWith("run-package-specs")
+ expect(ipcRenderer.send).not.toHaveBeenCalledWith("run-package-specs")
projectPaths = [temp.mkdirSync("dir1-"), temp.mkdirSync("dir2-")]
atom.project.setPaths(projectPaths)
# No active item. Use first project directory.
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
- expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
- ipc.send.reset()
+ expect(ipcRenderer.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
+ ipcRenderer.send.reset()
# Active item doesn't implement ::getPath(). Use first project directory.
item = document.createElement("div")
atom.workspace.getActivePane().activateItem(item)
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
- expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
- ipc.send.reset()
+ expect(ipcRenderer.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
+ ipcRenderer.send.reset()
# Active item has no path. Use first project directory.
item.getPath = -> null
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
- expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
- ipc.send.reset()
+ expect(ipcRenderer.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[0], "spec"))
+ ipcRenderer.send.reset()
# Active item has path. Use project path for item path.
item.getPath = -> path.join(projectPaths[1], "a-file.txt")
atom.commands.dispatch(workspaceElement, "window:run-package-specs")
- expect(ipc.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[1], "spec"))
- ipc.send.reset()
+ expect(ipcRenderer.send).toHaveBeenCalledWith("run-package-specs", path.join(projectPaths[1], "spec"))
+ ipcRenderer.send.reset()
diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee
index e89e4c6bd..f08e85558 100644
--- a/spec/workspace-spec.coffee
+++ b/spec/workspace-spec.coffee
@@ -22,11 +22,11 @@ describe "Workspace", ->
describe "serialization", ->
simulateReload = ->
workspaceState = atom.workspace.serialize()
- projectState = atom.project.serialize()
+ projectState = atom.project.serialize({isUnloading: true})
atom.workspace.destroy()
atom.project.destroy()
atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom)})
- atom.project.deserialize(projectState, atom.deserializers)
+ atom.project.deserialize(projectState)
atom.workspace = new Workspace({
config: atom.config, project: atom.project, packageManager: atom.packages,
grammarRegistry: atom.grammars, deserializerManager: atom.deserializers,
@@ -80,7 +80,8 @@ describe "Workspace", ->
expect(untitledEditor.getText()).toBe("An untitled editor.")
expect(atom.workspace.getActiveTextEditor().getPath()).toBe editor3.getPath()
- expect(document.title).toMatch ///^#{path.basename(editor3.getLongTitle())}\ \u2014\ #{atom.project.getPaths()[0]}///
+ pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
+ expect(document.title).toMatch ///^#{path.basename(editor3.getLongTitle())}\ \u2014\ #{pathEscaped}///
describe "where there are no open panes or editors", ->
it "constructs the view with no open editors", ->
@@ -428,7 +429,7 @@ describe "Workspace", ->
workspace.open('sample.js').then (e) -> editor = e
runs ->
- expect(editor.displayBuffer.largeFileMode).toBe true
+ expect(editor.largeFileMode).toBe true
describe "when the file is over 20MB", ->
it "prompts the user to make sure they want to open a file this big", ->
@@ -453,7 +454,7 @@ describe "Workspace", ->
runs ->
expect(atom.applicationDelegate.confirm).toHaveBeenCalled()
- expect(editor.displayBuffer.largeFileMode).toBe true
+ expect(editor.largeFileMode).toBe true
describe "when passed a path that matches a custom opener", ->
it "returns the resource returned by the custom opener", ->
@@ -585,6 +586,72 @@ describe "Workspace", ->
open = -> workspace.open('file1', workspace.getActivePane())
expect(open).toThrow()
+ describe "when the file is already open in pending state", ->
+ it "should terminate the pending state", ->
+ editor = null
+ pane = null
+
+ waitsForPromise ->
+ atom.workspace.open('sample.js', pending: true).then (o) ->
+ editor = o
+ pane = atom.workspace.getActivePane()
+
+ runs ->
+ expect(pane.getPendingItem()).toEqual editor
+
+ waitsForPromise ->
+ atom.workspace.open('sample.js')
+
+ runs ->
+ expect(pane.getPendingItem()).toBeNull()
+
+ describe "when opening will switch from a pending tab to a permanent tab", ->
+ it "keeps the pending tab open", ->
+ editor1 = null
+ editor2 = null
+
+ waitsForPromise ->
+ atom.workspace.open('sample.txt').then (o) ->
+ editor1 = o
+
+ waitsForPromise ->
+ atom.workspace.open('sample2.txt', pending: true).then (o) ->
+ editor2 = o
+
+ runs ->
+ pane = atom.workspace.getActivePane()
+ pane.activateItem(editor1)
+ expect(pane.getItems().length).toBe 2
+ expect(pane.getItems()).toEqual [editor1, editor2]
+
+ describe "when replacing a pending item which is the last item in a second pane", ->
+ it "does not destroy the pane even if core.destroyEmptyPanes is on", ->
+ atom.config.set('core.destroyEmptyPanes', true)
+ editor1 = null
+ editor2 = null
+ leftPane = atom.workspace.getActivePane()
+ rightPane = null
+
+ waitsForPromise ->
+ atom.workspace.open('sample.js', pending: true, split: 'right').then (o) ->
+ editor1 = o
+ rightPane = atom.workspace.getActivePane()
+ spyOn rightPane, "destroyed"
+
+ runs ->
+ expect(leftPane).not.toBe rightPane
+ expect(atom.workspace.getActivePane()).toBe rightPane
+ expect(atom.workspace.getActivePane().getItems().length).toBe 1
+ expect(rightPane.getPendingItem()).toBe editor1
+
+ waitsForPromise ->
+ atom.workspace.open('sample.txt', pending: true).then (o) ->
+ editor2 = o
+
+ runs ->
+ expect(rightPane.getPendingItem()).toBe editor2
+ expect(rightPane.destroyed.callCount).toBe 0
+
describe "::reopenItem()", ->
it "opens the uri associated with the last closed pane that isn't currently open", ->
pane = workspace.getActivePane()
@@ -767,25 +834,29 @@ describe "Workspace", ->
describe "when there is an active pane item", ->
it "sets the title to the pane item's title plus the project path", ->
item = atom.workspace.getActivePaneItem()
- expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}///
+ pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
+ expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}///
describe "when the title of the active pane item changes", ->
it "updates the window title based on the item's new title", ->
editor = atom.workspace.getActivePaneItem()
editor.buffer.setPath(path.join(temp.dir, 'hi'))
- expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}///
+ pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
+ expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{pathEscaped}///
describe "when the active pane's item changes", ->
it "updates the title to the new item's title plus the project path", ->
atom.workspace.getActivePane().activateNextItem()
item = atom.workspace.getActivePaneItem()
- expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{atom.project.getPaths()[0]}///
+ pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
+ expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}///
describe "when the last pane item is removed", ->
it "updates the title to contain the project's path", ->
atom.workspace.getActivePane().destroy()
expect(atom.workspace.getActivePaneItem()).toBeUndefined()
- expect(document.title).toMatch ///^#{atom.project.getPaths()[0]}///
+ pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
+ expect(document.title).toMatch ///^#{pathEscaped}///
describe "when an inactive pane's item changes", ->
it "does not update the title", ->
@@ -809,7 +880,8 @@ describe "Workspace", ->
})
workspace2.deserialize(atom.workspace.serialize(), atom.deserializers)
item = workspace2.getActivePaneItem()
- expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{atom.project.getPaths()[0]}///
+ pathEscaped = escapeStringRegex(atom.project.getPaths()[0])
+ expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{pathEscaped}///
workspace2.destroy()
describe "document edited status", ->
@@ -1532,3 +1604,60 @@ describe "Workspace", ->
atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow()
expect(atom.close).toHaveBeenCalled()
+
+ describe "when the core.allowPendingPaneItems option is falsey", ->
+ it "does not open item with `pending: true` option as pending", ->
+ pane = null
+ atom.config.set('core.allowPendingPaneItems', false)
+
+ waitsForPromise ->
+ atom.workspace.open('sample.js', pending: true).then ->
+ pane = atom.workspace.getActivePane()
+
+ runs ->
+ expect(pane.getPendingItem()).toBeFalsy()
+
+ describe "grammar activation", ->
+ beforeEach ->
+ waitsForPromise ->
+ atom.packages.activatePackage('language-javascript')
+
+ it "notifies the workspace of which grammar is used", ->
+ editor = null
+
+ grammarUsed = jasmine.createSpy()
+ atom.workspace.handleGrammarUsed = grammarUsed
+
+ waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o
+ waitsFor -> grammarUsed.callCount is 1
+ runs ->
+ expect(grammarUsed.argsForCall[0][0].name).toBe 'JavaScript'
+
+ describe ".checkoutHeadRevision()", ->
+ editor = null
+ beforeEach ->
+ atom.config.set("editor.confirmCheckoutHeadRevision", false)
+
+ waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o
+
+ it "reverts to the version of its file checked into the project repository", ->
+ editor.setCursorBufferPosition([0, 0])
+ editor.insertText("---\n")
+ expect(editor.lineTextForBufferRow(0)).toBe "---"
+
+ waitsForPromise ->
+ atom.workspace.checkoutHeadRevision(editor)
+
+ runs ->
+ expect(editor.lineTextForBufferRow(0)).toBe ""
+
+ describe "when there's no repository for the editor's file", ->
+ it "doesn't do anything", ->
+ editor = atom.workspace.buildTextEditor()
+ editor.setText("stuff")
+ atom.workspace.checkoutHeadRevision(editor)
+
+ waitsForPromise -> atom.workspace.checkoutHeadRevision(editor)
+
+ escapeStringRegex = (str) ->
+ str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee
index 59259d223..aee02ee8e 100644
--- a/src/application-delegate.coffee
+++ b/src/application-delegate.coffee
@@ -1,69 +1,73 @@
_ = require 'underscore-plus'
-ipc = require 'ipc'
-remote = require 'remote'
-shell = require 'shell'
-webFrame = require 'web-frame'
+{screen, ipcRenderer, remote, shell, webFrame} = require 'electron'
+ipcHelpers = require './ipc-helpers'
{Disposable} = require 'event-kit'
{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
module.exports =
class ApplicationDelegate
open: (params) ->
- ipc.send('open', params)
+ ipcRenderer.send('open', params)
pickFolder: (callback) ->
responseChannel = "atom-pick-folder-response"
- ipc.on responseChannel, (path) ->
- ipc.removeAllListeners(responseChannel)
+ ipcRenderer.on responseChannel, (event, path) ->
+ ipcRenderer.removeAllListeners(responseChannel)
callback(path)
- ipc.send("pick-folder", responseChannel)
+ ipcRenderer.send("pick-folder", responseChannel)
getCurrentWindow: ->
remote.getCurrentWindow()
closeWindow: ->
- ipc.send("call-window-method", "close")
+ ipcRenderer.send("call-window-method", "close")
+
+ getTemporaryWindowState: ->
+ ipcHelpers.call('get-temporary-window-state')
+
+ setTemporaryWindowState: (state) ->
+ ipcHelpers.call('set-temporary-window-state', state)
getWindowSize: ->
[width, height] = remote.getCurrentWindow().getSize()
{width, height}
setWindowSize: (width, height) ->
- remote.getCurrentWindow().setSize(width, height)
+ ipcHelpers.call('set-window-size', width, height)
getWindowPosition: ->
[x, y] = remote.getCurrentWindow().getPosition()
{x, y}
setWindowPosition: (x, y) ->
- ipc.send("call-window-method", "setPosition", x, y)
+ ipcHelpers.call('set-window-position', x, y)
centerWindow: ->
- ipc.send("call-window-method", "center")
+ ipcHelpers.call('center-window')
focusWindow: ->
- ipc.send("call-window-method", "focus")
+ ipcHelpers.call('focus-window')
showWindow: ->
- ipc.send("call-window-method", "show")
+ ipcHelpers.call('show-window')
hideWindow: ->
- ipc.send("call-window-method", "hide")
+ ipcHelpers.call('hide-window')
- restartWindow: ->
- ipc.send("call-window-method", "restart")
+ reloadWindow: ->
+ ipcRenderer.send("call-window-method", "reload")
isWindowMaximized: ->
remote.getCurrentWindow().isMaximized()
maximizeWindow: ->
- ipc.send("call-window-method", "maximize")
+ ipcRenderer.send("call-window-method", "maximize")
isWindowFullScreen: ->
remote.getCurrentWindow().isFullScreen()
setWindowFullScreen: (fullScreen=false) ->
- ipc.send("call-window-method", "setFullScreen", fullScreen)
+ ipcRenderer.send("call-window-method", "setFullScreen", fullScreen)
openWindowDevTools: ->
new Promise (resolve) ->
@@ -75,7 +79,7 @@ class ApplicationDelegate
resolve()
else
remote.getCurrentWindow().once("devtools-opened", -> resolve())
- ipc.send("call-window-method", "openDevTools")
+ ipcRenderer.send("call-window-method", "openDevTools")
closeWindowDevTools: ->
new Promise (resolve) ->
@@ -87,7 +91,7 @@ class ApplicationDelegate
resolve()
else
remote.getCurrentWindow().once("devtools-closed", -> resolve())
- ipc.send("call-window-method", "closeDevTools")
+ ipcRenderer.send("call-window-method", "closeDevTools")
toggleWindowDevTools: ->
new Promise (resolve) =>
@@ -101,16 +105,16 @@ class ApplicationDelegate
@openWindowDevTools().then(resolve)
executeJavaScriptInWindowDevTools: (code) ->
- ipc.send("call-window-method", "executeJavaScriptInDevTools", code)
+ ipcRenderer.send("execute-javascript-in-dev-tools", code)
setWindowDocumentEdited: (edited) ->
- ipc.send("call-window-method", "setDocumentEdited", edited)
+ ipcRenderer.send("call-window-method", "setDocumentEdited", edited)
setRepresentedFilename: (filename) ->
- ipc.send("call-window-method", "setRepresentedFilename", filename)
+ ipcRenderer.send("call-window-method", "setRepresentedFilename", filename)
addRecentDocument: (filename) ->
- ipc.send("add-recent-document", filename)
+ ipcRenderer.send("add-recent-document", filename)
setRepresentedDirectoryPaths: (paths) ->
loadSettings = getWindowLoadSettings()
@@ -118,14 +122,13 @@ class ApplicationDelegate
setWindowLoadSettings(loadSettings)
setAutoHideWindowMenuBar: (autoHide) ->
- ipc.send("call-window-method", "setAutoHideMenuBar", autoHide)
+ ipcRenderer.send("call-window-method", "setAutoHideMenuBar", autoHide)
setWindowMenuBarVisibility: (visible) ->
remote.getCurrentWindow().setMenuBarVisibility(visible)
getPrimaryDisplayWorkAreaSize: ->
- screen = remote.require 'screen'
- screen.getPrimaryDisplay().workAreaSize
+ remote.screen.getPrimaryDisplay().workAreaSize
confirm: ({message, detailedMessage, buttons}) ->
buttons ?= {}
@@ -134,8 +137,7 @@ class ApplicationDelegate
else
buttonLabels = Object.keys(buttons)
- dialog = remote.require('dialog')
- chosen = dialog.showMessageBox(remote.getCurrentWindow(), {
+ chosen = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info'
message: message
detail: detailedMessage
@@ -157,45 +159,110 @@ class ApplicationDelegate
params = _.clone(params)
params.title ?= 'Save File'
params.defaultPath ?= getWindowLoadSettings().initialPaths[0]
- dialog = remote.require('dialog')
- dialog.showSaveDialog remote.getCurrentWindow(), params
+ remote.dialog.showSaveDialog remote.getCurrentWindow(), params
playBeepSound: ->
shell.beep()
onDidOpenLocations: (callback) ->
- outerCallback = (message, detail) ->
- if message is 'open-locations'
- callback(detail)
+ outerCallback = (event, message, detail) ->
+ callback(detail) if message is 'open-locations'
- ipc.on('message', outerCallback)
+ ipcRenderer.on('message', outerCallback)
new Disposable ->
- ipc.removeListener('message', outerCallback)
+ ipcRenderer.removeListener('message', outerCallback)
onUpdateAvailable: (callback) ->
- outerCallback = (message, detail) ->
- if message is 'update-available'
- callback(detail)
+ 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'
- ipc.on('message', outerCallback)
+ ipcRenderer.on('message', outerCallback)
new Disposable ->
- ipc.removeListener('message', outerCallback)
+ 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) ->
- ipc.on('command', callback)
+ outerCallback = (event, args...) ->
+ callback(args...)
+
+ ipcRenderer.on('command', outerCallback)
new Disposable ->
- ipc.removeListener('command', callback)
+ ipcRenderer.removeListener('command', outerCallback)
onContextMenuCommand: (callback) ->
- ipc.on('context-command', callback)
+ outerCallback = (event, args...) ->
+ callback(args...)
+
+ ipcRenderer.on('context-command', outerCallback)
new Disposable ->
- ipc.removeListener('context-command', callback)
+ ipcRenderer.removeListener('context-command', outerCallback)
didCancelWindowUnload: ->
- ipc.send('did-cancel-window-unload')
+ ipcRenderer.send('did-cancel-window-unload')
openExternal: (url) ->
shell.openExternal(url)
- disablePinchToZoom: ->
- webFrame.setZoomLevelLimits(1, 1)
+ disableZoom: ->
+ outerCallback = ->
+ webFrame.setZoomLevelLimits(1, 1)
+
+ outerCallback()
+ # 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)
+
+ 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')
diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee
index 49fe81f0d..1d60872df 100644
--- a/src/atom-environment.coffee
+++ b/src/atom-environment.coffee
@@ -1,15 +1,16 @@
crypto = require 'crypto'
path = require 'path'
-ipc = require 'ipc'
+{ipcRenderer} = require 'electron'
_ = require 'underscore-plus'
{deprecate} = require 'grim'
-{CompositeDisposable, Emitter} = require 'event-kit'
+{CompositeDisposable, Disposable, Emitter} = require 'event-kit'
fs = require 'fs-plus'
{mapSourcePosition} = require 'source-map-support'
Model = require './model'
WindowEventHandler = require './window-event-handler'
StylesElement = require './styles-element'
+StateStore = require './state-store'
StorageFolder = require './storage-folder'
{getWindowLoadSettings, setWindowLoadSettings} = require './window-load-settings-helpers'
registerDefaultCommands = require './register-default-commands'
@@ -40,6 +41,8 @@ Project = require './project'
TextEditor = require './text-editor'
TextBuffer = require 'text-buffer'
Gutter = require './gutter'
+TextEditorRegistry = require './text-editor-registry'
+AutoUpdateManager = require './auto-update-manager'
WorkspaceElement = require './workspace-element'
PanelContainerElement = require './panel-container-element'
@@ -111,22 +114,35 @@ class AtomEnvironment extends Model
# Public: A {Workspace} instance
workspace: null
+ # Public: A {TextEditorRegistry} instance
+ textEditors: null
+
+ # Private: An {AutoUpdateManager} instance
+ autoUpdater: null
+
+ saveStateDebounceInterval: 1000
+
###
Section: Construction and Destruction
###
# Call .loadOrCreate instead
constructor: (params={}) ->
- {@blobStore, @applicationDelegate, @window, @document, configDirPath, @enablePersistence, onlyLoadBaseStyleSheets} = params
-
- @state = {version: @constructor.version}
+ {@blobStore, @applicationDelegate, @window, @document, @configDirPath, @enablePersistence, onlyLoadBaseStyleSheets} = params
+ @unloaded = false
@loadTime = null
- {devMode, safeMode, resourcePath} = @getLoadSettings()
+ {devMode, safeMode, resourcePath, clearWindowState} = @getLoadSettings()
@emitter = new Emitter
@disposables = new CompositeDisposable
+ @stateStore = new StateStore('AtomEnvironments', 1)
+
+ if clearWindowState
+ @getStorageFolder().clear()
+ @stateStore.clear()
+
@deserializers = new DeserializerManager(this)
@deserializeTimings = {}
@@ -134,10 +150,10 @@ class AtomEnvironment extends Model
@notifications = new NotificationManager
- @config = new Config({configDirPath, resourcePath, notificationManager: @notifications, @enablePersistence})
+ @config = new Config({@configDirPath, resourcePath, notificationManager: @notifications, @enablePersistence})
@setConfigSchema()
- @keymaps = new KeymapManager({configDirPath, resourcePath, notificationManager: @notifications})
+ @keymaps = new KeymapManager({@configDirPath, resourcePath, notificationManager: @notifications})
@tooltips = new TooltipManager(keymapManager: @keymaps)
@@ -146,16 +162,16 @@ class AtomEnvironment extends Model
@grammars = new GrammarRegistry({@config})
- @styles = new StyleManager({configDirPath})
+ @styles = new StyleManager({@configDirPath})
@packages = new PackageManager({
- devMode, configDirPath, resourcePath, safeMode, @config, styleManager: @styles,
+ devMode, @configDirPath, resourcePath, safeMode, @config, styleManager: @styles,
commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications,
grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views
})
@themes = new ThemeManager({
- packageManager: @packages, configDirPath, resourcePath, safeMode, @config,
+ packageManager: @packages, @configDirPath, resourcePath, safeMode, @config,
styleManager: @styles, notificationManager: @notifications, viewRegistry: @views
})
@@ -179,6 +195,9 @@ class AtomEnvironment extends Model
})
@themes.workspace = @workspace
+ @textEditors = new TextEditorRegistry
+ @autoUpdater = new AutoUpdateManager({@applicationDelegate})
+
@config.load()
@themes.loadBaseStylesheets()
@@ -189,7 +208,7 @@ class AtomEnvironment extends Model
@stylesElement = @styles.buildStylesElement()
@document.head.appendChild(@stylesElement)
- @applicationDelegate.disablePinchToZoom()
+ @disposables.add(@applicationDelegate.disableZoom())
@keymaps.subscribeToFileReadFailure()
@keymaps.loadBundledKeymaps()
@@ -200,19 +219,30 @@ class AtomEnvironment extends Model
@registerDefaultViewProviders()
@installUncaughtErrorHandler()
+ @attachSaveStateListeners()
@installWindowEventHandler()
@observeAutoHideMenuBar()
checkPortableHomeWritable = ->
responseChannel = "check-portable-home-writable-response"
- ipc.on responseChannel, (response) ->
- ipc.removeAllListeners(responseChannel)
+ ipcRenderer.on responseChannel, (event, response) ->
+ ipcRenderer.removeAllListeners(responseChannel)
atom.notifications.addWarning("#{response.message.replace(/([\\\.+\\-_#!])/g, '\\$1')}") if not response.writable
- ipc.send('check-portable-home-writable', responseChannel)
+ ipcRenderer.send('check-portable-home-writable', responseChannel)
checkPortableHomeWritable()
+ attachSaveStateListeners: ->
+ saveState = _.debounce((=>
+ window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded
+ ), @saveStateDebounceInterval)
+ @document.addEventListener('mousedown', saveState, true)
+ @document.addEventListener('keydown', saveState, true)
+ @disposables.add new Disposable =>
+ @document.removeEventListener('mousedown', saveState, true)
+ @document.removeEventListener('keydown', saveState, true)
+
setConfigSchema: ->
@config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))}
@@ -226,7 +256,7 @@ class AtomEnvironment extends Model
@deserializers.add(TextBuffer)
registerDefaultCommands: ->
- registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller})
+ registerDefaultCommands({commandRegistry: @commands, @config, @commandInstaller, notificationManager: @notifications, @project, @clipboard})
registerDefaultViewProviders: ->
@views.addViewProvider Workspace, (model, env) ->
@@ -241,8 +271,6 @@ class AtomEnvironment extends Model
new PaneAxisElement().initialize(model, env)
@views.addViewProvider Pane, (model, env) ->
new PaneElement().initialize(model, env)
- @views.addViewProvider TextEditor, (model, env) ->
- new TextEditorElement().initialize(model, env)
@views.addViewProvider(Gutter, createGutterView)
registerDefaultOpeners: ->
@@ -302,9 +330,6 @@ class AtomEnvironment extends Model
@views.clear()
@registerDefaultViewProviders()
- @state.packageStates = {}
- delete @state.workspace
-
destroy: ->
return if not @project
@@ -317,6 +342,7 @@ class AtomEnvironment extends Model
@commands.clear()
@stylesElement.remove()
@config.unobserveUserConfig()
+ @autoUpdater.destroy()
@uninstallWindowEventHandler()
@@ -395,6 +421,16 @@ class AtomEnvironment extends Model
getVersion: ->
@appVersion ?= @getLoadSettings().appVersion
+ # Returns the release channel as a {String}. Will return one of `'dev', 'beta', 'stable'`
+ getReleaseChannel: ->
+ version = @getVersion()
+ if version.indexOf('beta') > -1
+ 'beta'
+ else if version.indexOf('dev') > -1
+ 'dev'
+ else
+ 'stable'
+
# Public: Returns a {Boolean} that is `true` if the current version is an official release.
isReleasedVersion: ->
not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix
@@ -497,7 +533,7 @@ class AtomEnvironment extends Model
# Extended: Reload the current window.
reload: ->
- @applicationDelegate.restartWindow()
+ @applicationDelegate.reloadWindow()
# Extended: Returns a {Boolean} that is `true` if the current window is maximized.
isMaximized: ->
@@ -524,21 +560,18 @@ class AtomEnvironment extends Model
# Restore the window to its previous dimensions and show it.
#
- # Also restores the full screen and maximized state on the next tick to
+ # Restores the full screen and maximized state after the window has resized to
# prevent resize glitches.
displayWindow: ->
- dimensions = @restoreWindowDimensions()
- @show()
- @focus()
-
- setImmediate =>
- @setFullScreen(true) if @workspace?.fullScreen
- @maximize() if dimensions?.maximized and process.platform isnt 'darwin'
-
- if @isFirstLoad()
- loadSettings = getWindowLoadSettings()
- loadSettings.firstLoad = false
- setWindowLoadSettings(loadSettings)
+ @restoreWindowDimensions().then =>
+ steps = [
+ @restoreWindowBackground(),
+ @show(),
+ @focus()
+ ]
+ steps.push(@setFullScreen(true)) if @windowDimensions?.fullScreen
+ steps.push(@maximize()) if @windowDimensions?.maximized and process.platform isnt 'darwin'
+ Promise.all(steps)
# Get the dimensions of this window.
#
@@ -566,22 +599,24 @@ class AtomEnvironment extends Model
# * `width` The new width.
# * `height` The new height.
setWindowDimensions: ({x, y, width, height}) ->
+ steps = []
if width? and height?
- @setSize(width, height)
+ steps.push(@setSize(width, height))
if x? and y?
- @setPosition(x, y)
+ steps.push(@setPosition(x, y))
else
- @center()
+ steps.push(@center())
+ Promise.all(steps)
# Returns true if the dimensions are useable, false if they should be ignored.
# Work around for https://github.com/atom/atom-shell/issues/473
isValidDimensions: ({x, y, width, height}={}) ->
width > 0 and height > 0 and x + width > 0 and y + height > 0
- storeDefaultWindowDimensions: ->
- dimensions = @getWindowDimensions()
- if @isValidDimensions(dimensions)
- localStorage.setItem("defaultWindowDimensions", JSON.stringify(dimensions))
+ storeWindowDimensions: ->
+ @windowDimensions = @getWindowDimensions()
+ if @isValidDimensions(@windowDimensions)
+ localStorage.setItem("defaultWindowDimensions", JSON.stringify(@windowDimensions))
getDefaultWindowDimensions: ->
{windowDimensions} = @getLoadSettings()
@@ -601,22 +636,16 @@ class AtomEnvironment extends Model
{x: 0, y: 0, width: Math.min(1024, width), height}
restoreWindowDimensions: ->
- dimensions = null
+ unless @windowDimensions? and @isValidDimensions(@windowDimensions)
+ @windowDimensions = @getDefaultWindowDimensions()
+ @setWindowDimensions(@windowDimensions).then -> @windowDimensions
- # The first time the window's loaded we want to use the default dimensions.
- # But after that, e.g., when the window's been reloaded, we want to use the
- # dimensions we've saved for it.
- if not @isFirstLoad()
- dimensions = @state.windowDimensions
-
- unless @isValidDimensions(dimensions)
- dimensions = @getDefaultWindowDimensions()
- @setWindowDimensions(dimensions)
- dimensions
-
- storeWindowDimensions: ->
- dimensions = @getWindowDimensions()
- @state.windowDimensions = dimensions if @isValidDimensions(dimensions)
+ restoreWindowBackground: ->
+ if backgroundColor = window.localStorage.getItem('atom:window-background-color')
+ @backgroundStylesheet = document.createElement('style')
+ @backgroundStylesheet.type = 'text/css'
+ @backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + ' !important; }'
+ document.head.appendChild(@backgroundStylesheet)
storeWindowBackground: ->
return if @inSpecMode()
@@ -627,44 +656,58 @@ class AtomEnvironment extends Model
# Call this method when establishing a real application window.
startEditorWindow: ->
- @commandInstaller.installAtomCommand false, (error) ->
- console.warn error.message if error?
- @commandInstaller.installApmCommand false, (error) ->
- console.warn error.message if error?
+ @unloaded = false
+ @loadState().then (state) =>
+ @windowDimensions = state?.windowDimensions
+ @displayWindow().then =>
+ @commandInstaller.installAtomCommand false, (error) ->
+ console.warn error.message if error?
+ @commandInstaller.installApmCommand false, (error) ->
+ console.warn error.message if error?
- @disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this)))
- @disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this)))
- @disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this)))
- @listenForUpdates()
+ @disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this)))
+ @disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this)))
+ @disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this)))
+ @listenForUpdates()
- @registerDefaultTargetForKeymaps()
+ @registerDefaultTargetForKeymaps()
- @packages.loadPackages()
+ @packages.loadPackages()
- @document.body.appendChild(@views.getView(@workspace))
+ startTime = Date.now()
+ @deserialize(state) if state?
+ @deserializeTimings.atom = Date.now() - startTime
- @watchProjectPath()
+ @document.body.appendChild(@views.getView(@workspace))
+ @backgroundStylesheet?.remove()
- @packages.activate()
- @keymaps.loadUserKeymap()
- @requireUserInitScript() unless @getLoadSettings().safeMode
+ @watchProjectPaths()
- @menu.update()
+ @packages.activate()
+ @keymaps.loadUserKeymap()
+ @requireUserInitScript() unless @getLoadSettings().safeMode
- @openInitialEmptyEditorIfNecessary()
+ @menu.update()
+
+ @openInitialEmptyEditorIfNecessary()
+
+ serialize: (options) ->
+ version: @constructor.version
+ project: @project.serialize(options)
+ workspace: @workspace.serialize()
+ packageStates: @packages.serialize()
+ grammars: {grammarOverridesByPath: @grammars.grammarOverridesByPath}
+ fullScreen: @isFullScreen()
+ windowDimensions: @windowDimensions
unloadEditorWindow: ->
return if not @project
+ @saveState({isUnloading: true})
@storeWindowBackground()
- @state.grammars = {grammarOverridesByPath: @grammars.grammarOverridesByPath}
- @state.project = @project.serialize()
- @state.workspace = @workspace.serialize()
@packages.deactivatePackages()
- @state.packageStates = @packages.packageStates
- @state.fullScreen = @isFullScreen()
- @saveStateSync()
@saveBlobStoreSync()
+ @unloaded = true
openInitialEmptyEditorIfNecessary: ->
return unless @config.get('core.openEmptyEditorOnStart')
@@ -747,6 +790,7 @@ class AtomEnvironment extends Model
# Returns a {Promise} that resolves when the DevTools have been opened or
# closed.
toggleDevTools: ->
+ require("devtron").install()
@applicationDelegate.toggleWindowDevTools()
# Extended: Execute code in dev tools.
@@ -772,7 +816,7 @@ class AtomEnvironment extends Model
@themes.load()
# Notify the browser project of the window's current project path
- watchProjectPath: ->
+ watchProjectPaths: ->
@disposables.add @project.onDidChangePaths =>
@applicationDelegate.setRepresentedDirectoryPaths(@project.getPaths())
@@ -797,45 +841,48 @@ class AtomEnvironment extends Model
@blobStore.save()
- saveStateSync: ->
- return unless @enablePersistence
+ saveState: (options) ->
+ return Promise.resolve() unless @enablePersistence
- if storageKey = @getStateKey(@project?.getPaths())
- @getStorageFolder().store(storageKey, @state)
+ new Promise (resolve, reject) =>
+ return if not @project
+
+ state = @serialize(options)
+ savePromise =
+ if storageKey = @getStateKey(@project?.getPaths())
+ @stateStore.save(storageKey, state)
+ else
+ @applicationDelegate.setTemporaryWindowState(state)
+ savePromise.catch(reject).then(resolve)
+
+ loadState: ->
+ if @enablePersistence
+ if stateKey = @getStateKey(@getLoadSettings().initialPaths)
+ @stateStore.load(stateKey).then (state) =>
+ if state
+ state
+ else
+ # TODO: remove this when every user has migrated to the IndexedDb state store.
+ @getStorageFolder().load(stateKey)
+ else
+ @applicationDelegate.getTemporaryWindowState()
else
- @getCurrentWindow().loadSettings.windowState = JSON.stringify(@state)
+ Promise.resolve(null)
- loadStateSync: ->
- return unless @enablePersistence
-
- startTime = Date.now()
-
- if stateKey = @getStateKey(@getLoadSettings().initialPaths)
- if state = @getStorageFolder().load(stateKey)
- @state = state
-
- if not @state? and windowState = @getLoadSettings().windowState
- try
- if state = JSON.parse(@getLoadSettings().windowState)
- @state = state
- catch error
- console.warn "Error parsing window state: #{statePath} #{error.stack}", error
-
- @deserializeTimings.atom = Date.now() - startTime
-
- if grammarOverridesByPath = @state.grammars?.grammarOverridesByPath
+ deserialize: (state) ->
+ if grammarOverridesByPath = state.grammars?.grammarOverridesByPath
@grammars.grammarOverridesByPath = grammarOverridesByPath
- @setFullScreen(@state.fullScreen)
+ @setFullScreen(state.fullScreen)
- @packages.packageStates = @state.packageStates ? {}
+ @packages.packageStates = state.packageStates ? {}
startTime = Date.now()
- @project.deserialize(@state.project, @deserializers) if @state.project?
+ @project.deserialize(state.project, @deserializers) if state.project?
@deserializeTimings.project = Date.now() - startTime
startTime = Date.now()
- @workspace.deserialize(@state.workspace, @deserializers) if @state.workspace?
+ @workspace.deserialize(state.workspace, @deserializers) if state.workspace?
@deserializeTimings.workspace = Date.now() - startTime
getStateKey: (paths) ->
@@ -845,12 +892,12 @@ class AtomEnvironment extends Model
else
null
- getConfigDirPath: ->
- @configDirPath ?= process.env.ATOM_HOME
-
getStorageFolder: ->
@storageFolder ?= new StorageFolder(@getConfigDirPath())
+ getConfigDirPath: ->
+ @configDirPath ?= process.env.ATOM_HOME
+
getUserInitScriptPath: ->
initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee'])
initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee')
@@ -864,6 +911,7 @@ class AtomEnvironment extends Model
detail: error.message
dismissable: true
+ # TODO: We should deprecate the update events here, and use `atom.autoUpdater` instead
onUpdateAvailable: (callback) ->
@emitter.on 'update-available', callback
@@ -871,7 +919,8 @@ class AtomEnvironment extends Model
@emitter.emit 'update-available', details
listenForUpdates: ->
- @disposables.add(@applicationDelegate.onUpdateAvailable(@updateAvailable.bind(this)))
+ # listen for updates available locally (that have been successfully downloaded)
+ @disposables.add(@autoUpdater.onDidCompleteDownloadingUpdate(@updateAvailable.bind(this)))
setBodyPlatformClass: ->
@document.body.classList.add("platform-#{process.platform}")
@@ -893,8 +942,8 @@ class AtomEnvironment extends Model
openLocations: (locations) ->
needsProjectPaths = @project?.getPaths().length is 0
- for {pathToOpen, initialLine, initialColumn} in locations
- if pathToOpen? and needsProjectPaths
+ for {pathToOpen, initialLine, initialColumn, forceAddToWindow} in locations
+ if pathToOpen? and (needsProjectPaths or forceAddToWindow)
if fs.existsSync(pathToOpen)
@project.addPath(pathToOpen)
else if fs.existsSync(path.dirname(pathToOpen))
diff --git a/src/auto-update-manager.js b/src/auto-update-manager.js
new file mode 100644
index 000000000..fb6325a26
--- /dev/null
+++ b/src/auto-update-manager.js
@@ -0,0 +1,84 @@
+'use babel'
+
+import {Emitter, CompositeDisposable} from 'event-kit'
+
+export default class AutoUpdateManager {
+ constructor ({applicationDelegate}) {
+ this.applicationDelegate = applicationDelegate
+ this.subscriptions = new CompositeDisposable()
+ this.emitter = new Emitter()
+
+ this.subscriptions.add(
+ applicationDelegate.onDidBeginCheckingForUpdate(() => {
+ this.emitter.emit('did-begin-checking-for-update')
+ }),
+ applicationDelegate.onDidBeginDownloadingUpdate(() => {
+ this.emitter.emit('did-begin-downloading-update')
+ }),
+ applicationDelegate.onDidCompleteDownloadingUpdate((details) => {
+ this.emitter.emit('did-complete-downloading-update', details)
+ }),
+ applicationDelegate.onUpdateNotAvailable(() => {
+ this.emitter.emit('update-not-available')
+ }),
+ applicationDelegate.onUpdateError(() => {
+ this.emitter.emit('update-error')
+ })
+ )
+ }
+
+ destroy () {
+ this.subscriptions.dispose()
+ this.emitter.dispose()
+ }
+
+ checkForUpdate () {
+ this.applicationDelegate.checkForUpdate()
+ }
+
+ restartAndInstallUpdate () {
+ this.applicationDelegate.restartAndInstallUpdate()
+ }
+
+ getState () {
+ return this.applicationDelegate.getAutoUpdateManagerState()
+ }
+
+ getErrorMessage () {
+ return this.applicationDelegate.getAutoUpdateManagerErrorMessage()
+ }
+
+ platformSupportsUpdates () {
+ return atom.getReleaseChannel() !== 'dev' && this.getState() !== 'unsupported'
+ }
+
+ onDidBeginCheckingForUpdate (callback) {
+ return this.emitter.on('did-begin-checking-for-update', callback)
+ }
+
+ onDidBeginDownloadingUpdate (callback) {
+ return this.emitter.on('did-begin-downloading-update', callback)
+ }
+
+ onDidCompleteDownloadingUpdate (callback) {
+ return this.emitter.on('did-complete-downloading-update', callback)
+ }
+
+ // TODO: When https://github.com/atom/electron/issues/4587 is closed, we can
+ // add an update-available event.
+ // onUpdateAvailable (callback) {
+ // return this.emitter.on('update-available', callback)
+ // }
+
+ onUpdateNotAvailable (callback) {
+ return this.emitter.on('update-not-available', callback)
+ }
+
+ onUpdateError (callback) {
+ return this.emitter.on('update-error', callback)
+ }
+
+ getPlatform () {
+ return process.platform
+ }
+}
diff --git a/src/block-decorations-component.coffee b/src/block-decorations-component.coffee
index 0cfa7974f..35aec3921 100644
--- a/src/block-decorations-component.coffee
+++ b/src/block-decorations-component.coffee
@@ -26,7 +26,10 @@ class BlockDecorationsComponent
for id, blockDecorationState of @oldState.blockDecorations
unless @newState.blockDecorations.hasOwnProperty(id)
- @blockDecorationNodesById[id].remove()
+ blockDecorationNode = @blockDecorationNodesById[id]
+ blockDecorationNode.previousSibling.remove()
+ blockDecorationNode.nextSibling.remove()
+ blockDecorationNode.remove()
delete @blockDecorationNodesById[id]
delete @oldState.blockDecorations[id]
@@ -41,19 +44,27 @@ class BlockDecorationsComponent
for decorationId, blockDecorationNode of @blockDecorationNodesById
style = getComputedStyle(blockDecorationNode)
decoration = @newState.blockDecorations[decorationId].decoration
- marginBottom = parseInt(style.marginBottom) ? 0
- marginTop = parseInt(style.marginTop) ? 0
- @presenter.setBlockDecorationDimensions(
- decoration,
- blockDecorationNode.offsetWidth,
- blockDecorationNode.offsetHeight + marginTop + marginBottom
- )
+ topRuler = blockDecorationNode.previousSibling
+ bottomRuler = blockDecorationNode.nextSibling
+
+ width = blockDecorationNode.offsetWidth
+ height = bottomRuler.offsetTop - topRuler.offsetTop
+ @presenter.setBlockDecorationDimensions(decoration, width, height)
createAndAppendBlockDecorationNode: (id) ->
blockDecorationState = @newState.blockDecorations[id]
+ blockDecorationClass = "atom--block-decoration-#{id}"
+ topRuler = document.createElement("div")
blockDecorationNode = @views.getView(blockDecorationState.decoration.getProperties().item)
- blockDecorationNode.id = "atom--block-decoration-#{id}"
+ bottomRuler = document.createElement("div")
+ topRuler.classList.add(blockDecorationClass)
+ blockDecorationNode.classList.add(blockDecorationClass)
+ bottomRuler.classList.add(blockDecorationClass)
+
+ @container.appendChild(topRuler)
@container.appendChild(blockDecorationNode)
+ @container.appendChild(bottomRuler)
+
@blockDecorationNodesById[id] = blockDecorationNode
@updateBlockDecorationNode(id)
@@ -63,9 +74,13 @@ class BlockDecorationsComponent
blockDecorationNode = @blockDecorationNodesById[id]
if newBlockDecorationState.isVisible
+ blockDecorationNode.previousSibling.classList.remove("atom--invisible-block-decoration")
blockDecorationNode.classList.remove("atom--invisible-block-decoration")
+ blockDecorationNode.nextSibling.classList.remove("atom--invisible-block-decoration")
else
+ blockDecorationNode.previousSibling.classList.add("atom--invisible-block-decoration")
blockDecorationNode.classList.add("atom--invisible-block-decoration")
+ blockDecorationNode.nextSibling.classList.add("atom--invisible-block-decoration")
if oldBlockDecorationState.screenRow isnt newBlockDecorationState.screenRow
blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow
diff --git a/src/browser/application-menu.coffee b/src/browser/application-menu.coffee
index 74da80e43..b0a6e3267 100644
--- a/src/browser/application-menu.coffee
+++ b/src/browser/application-menu.coffee
@@ -1,6 +1,4 @@
-app = require 'app'
-ipc = require 'ipc'
-Menu = require 'menu'
+{app, Menu} = require 'electron'
_ = require 'underscore-plus'
# Used to manage the global application menu.
diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee
index 44848eb72..d08990264 100644
--- a/src/browser/atom-application.coffee
+++ b/src/browser/atom-application.coffee
@@ -2,14 +2,11 @@ AtomWindow = require './atom-window'
ApplicationMenu = require './application-menu'
AtomProtocolHandler = require './atom-protocol-handler'
AutoUpdateManager = require './auto-update-manager'
-BrowserWindow = require 'browser-window'
StorageFolder = require '../storage-folder'
-Menu = require 'menu'
-app = require 'app'
-dialog = require 'dialog'
-shell = require 'shell'
+Config = require '../config'
+ipcHelpers = require '../ipc-helpers'
+{BrowserWindow, Menu, app, dialog, ipcMain, shell} = require 'electron'
fs = require 'fs-plus'
-ipc = require 'ipc'
path = require 'path'
os = require 'os'
net = require 'net'
@@ -51,7 +48,7 @@ class AtomApplication
client = net.connect {path: options.socketPath}, ->
client.write JSON.stringify(options), ->
client.end()
- app.terminate()
+ app.quit()
client.on 'error', createAtomApplication
@@ -65,7 +62,7 @@ class AtomApplication
exit: (status) -> app.exit(status)
constructor: (options) ->
- {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, timeout} = options
+ {@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, timeout, clearWindowState} = options
@socketPath = null if options.test
@@ -74,7 +71,11 @@ class AtomApplication
@pidsToOpenWindows = {}
@windows = []
- @autoUpdateManager = new AutoUpdateManager(@version, options.test, @resourcePath)
+ @config = new Config({configDirPath: process.env.ATOM_HOME, @resourcePath, enablePersistence: true})
+ @config.setSchema null, {type: 'object', properties: _.clone(require('../config-schema'))}
+ @config.load()
+
+ @autoUpdateManager = new AutoUpdateManager(@version, options.test, @resourcePath, @config)
@applicationMenu = new ApplicationMenu(@version, @autoUpdateManager)
@atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode)
@@ -89,16 +90,16 @@ class AtomApplication
else
@loadState(options) or @openPath(options)
- openWithOptions: ({pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout}) ->
+ openWithOptions: ({initialPaths, pathsToOpen, executedFrom, urlsToOpen, test, pidToKillWhenClosed, devMode, safeMode, newWindow, logFile, profileStartup, timeout, clearWindowState, addToLastWindow, env}) ->
if test
- @runTests({headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, logFile, timeout})
+ @runTests({headless: true, devMode, @resourcePath, executedFrom, pathsToOpen, logFile, timeout, env})
else if pathsToOpen.length > 0
- @openPaths({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
+ @openPaths({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env})
else if urlsToOpen.length > 0
- @openUrl({urlToOpen, devMode, safeMode}) for urlToOpen in urlsToOpen
+ @openUrl({urlToOpen, devMode, safeMode, env}) for urlToOpen in urlsToOpen
else
# Always open a editor window if this is the first instance of Atom.
- @openPath({pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup})
+ @openPath({initialPaths, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env})
# Public: Removes the {AtomWindow} from the global window list.
removeWindow: (window) ->
@@ -137,8 +138,13 @@ class AtomApplication
return unless @socketPath?
@deleteSocketFile()
server = net.createServer (connection) =>
- connection.on 'data', (data) =>
- @openWithOptions(JSON.parse(data))
+ 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
@@ -168,9 +174,6 @@ class AtomApplication
@on 'application:quit', -> app.quit()
@on 'application:new-window', -> @openPath(getLoadSettings())
@on 'application:new-file', -> (@focusedWindow() ? this).openPath()
- @on 'application:open', -> @promptForPathToOpen('all', getLoadSettings())
- @on 'application:open-file', -> @promptForPathToOpen('file', getLoadSettings())
- @on 'application:open-folder', -> @promptForPathToOpen('folder', getLoadSettings())
@on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true)
@on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true)
@on 'application:inspect', ({x, y, atomWindow}) ->
@@ -179,7 +182,6 @@ class AtomApplication
@on 'application:open-documentation', -> shell.openExternal('https://atom.io/docs/latest/?app')
@on 'application:open-discussions', -> shell.openExternal('https://discuss.atom.io')
- @on 'application:open-roadmap', -> shell.openExternal('https://atom.io/roadmap?app')
@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#submitting-issues')
@@ -212,7 +214,6 @@ class AtomApplication
@openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
app.on 'before-quit', =>
- @saveState(false)
@quitting = true
app.on 'will-quit', =>
@@ -232,7 +233,7 @@ class AtomApplication
@emit('application:new-window')
# A request from the associated render process to open a new render process.
- ipc.on 'open', (event, options) =>
+ ipcMain.on 'open', (event, options) =>
window = @windowForEvent(event)
if options?
if typeof options.pathsToOpen is 'string'
@@ -245,44 +246,88 @@ class AtomApplication
else
@promptForPathToOpen('all', {window})
- ipc.on 'update-application-menu', (event, template, keystrokesByCommand) =>
+ ipcMain.on 'update-application-menu', (event, template, keystrokesByCommand) =>
win = BrowserWindow.fromWebContents(event.sender)
@applicationMenu.update(win, template, keystrokesByCommand)
- ipc.on 'run-package-specs', (event, packageSpecPath) =>
+ ipcMain.on 'run-package-specs', (event, packageSpecPath) =>
@runTests({resourcePath: @devResourcePath, pathsToOpen: [packageSpecPath], headless: false})
- ipc.on 'command', (event, command) =>
+ ipcMain.on 'command', (event, command) =>
@emit(command)
- ipc.on 'window-command', (event, command, args...) ->
+ ipcMain.on '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
+
+ ipcMain.on 'window-command', (event, command, args...) ->
win = BrowserWindow.fromWebContents(event.sender)
win.emit(command, args...)
- ipc.on 'call-window-method', (event, method, args...) ->
+ ipcMain.on 'call-window-method', (event, method, args...) ->
win = BrowserWindow.fromWebContents(event.sender)
win[method](args...)
- ipc.on 'pick-folder', (event, responseChannel) =>
+ ipcMain.on 'pick-folder', (event, responseChannel) =>
@promptForPath "folder", (selectedPaths) ->
event.sender.send(responseChannel, selectedPaths)
- ipc.on 'did-cancel-window-unload', =>
+ ipcHelpers.respondTo 'set-window-size', (win, width, height) ->
+ win.setSize(width, height)
+
+ ipcHelpers.respondTo 'set-window-position', (win, x, y) ->
+ win.setPosition(x, y)
+
+ ipcHelpers.respondTo 'center-window', (win) ->
+ win.center()
+
+ ipcHelpers.respondTo 'focus-window', (win) ->
+ win.focus()
+
+ ipcHelpers.respondTo 'show-window', (win) ->
+ win.show()
+
+ ipcHelpers.respondTo 'hide-window', (win) ->
+ win.hide()
+
+ ipcHelpers.respondTo 'get-temporary-window-state', (win) ->
+ win.temporaryState
+
+ ipcHelpers.respondTo 'set-temporary-window-state', (win, state) ->
+ win.temporaryState = state
+
+ ipcMain.on 'did-cancel-window-unload', =>
@quitting = false
clipboard = require '../safe-clipboard'
- ipc.on 'write-text-to-selection-clipboard', (event, selectedText) ->
+ ipcMain.on 'write-text-to-selection-clipboard', (event, selectedText) ->
clipboard.writeText(selectedText, 'selection')
- ipc.on 'write-to-stdout', (event, output) ->
+ ipcMain.on 'write-to-stdout', (event, output) ->
process.stdout.write(output)
- ipc.on 'write-to-stderr', (event, output) ->
+ ipcMain.on 'write-to-stderr', (event, output) ->
process.stderr.write(output)
- ipc.on 'add-recent-document', (event, filename) ->
+ ipcMain.on 'add-recent-document', (event, filename) ->
app.addRecentDocument(filename)
+ ipcMain.on 'execute-javascript-in-dev-tools', (event, code) ->
+ event.sender.devToolsWebContents?.executeJavaScript(code)
+
+ ipcMain.on 'get-auto-update-manager-state', (event) =>
+ event.returnValue = @autoUpdateManager.getState()
+
+ ipcMain.on 'get-auto-update-manager-error', (event) =>
+ event.returnValue = @autoUpdateManager.getErrorMessage()
+
+ ipcMain.on 'execute-javascript-in-dev-tools', (event, code) ->
+ event.sender.devToolsWebContents?.executeJavaScript(code)
+
setupDockMenu: ->
if process.platform is 'darwin'
dockMenu = Menu.buildFromTemplate [
@@ -350,7 +395,7 @@ class AtomApplication
_.find @windows, (atomWindow) ->
atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen)
- # Returns the {AtomWindow} for the given ipc event.
+ # Returns the {AtomWindow} for the given ipcMain event.
windowForEvent: ({sender}) ->
window = BrowserWindow.fromWebContents(sender)
_.find @windows, ({browserWindow}) -> window is browserWindow
@@ -387,8 +432,9 @@ class AtomApplication
# :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.
- openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window} = {}) ->
- @openPaths({pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window})
+ # :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.
#
@@ -400,10 +446,12 @@ class AtomApplication
# :safeMode - Boolean to control the opened window's safe mode.
# :windowDimensions - Object with height and width keys.
# :window - {AtomWindow} to open file paths in.
- openPaths: ({pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window}={}) ->
+ # :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}={}) ->
devMode = Boolean(devMode)
safeMode = Boolean(safeMode)
- locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom) for pathToOpen in pathsToOpen)
+ clearWindowState = Boolean(clearWindowState)
+ locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom, addToLastWindow) for pathToOpen in pathsToOpen)
pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen)
unless pidToKillWhenClosed or newWindow
@@ -412,6 +460,7 @@ class AtomApplication
unless existingWindow?
if currentWindow = window ? @lastFocusedWindow
existingWindow = currentWindow if (
+ addToLastWindow or
currentWindow.devMode is devMode and
(
stats.every((stat) -> stat.isFile?()) or
@@ -426,6 +475,7 @@ class AtomApplication
openedWindow.restore()
else
openedWindow.focus()
+ openedWindow.replaceEnvironment(env)
else
if devMode
try
@@ -435,7 +485,7 @@ class AtomApplication
windowInitializationScript ?= require.resolve('../initialize-application-window')
resourcePath ?= @resourcePath
windowDimensions ?= @getDimensionsForNewWindow()
- openedWindow = new AtomWindow({locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup})
+ openedWindow = new AtomWindow({initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env})
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
@@ -472,13 +522,15 @@ class AtomApplication
if loadSettings = window.getLoadSettings()
states.push(initialPaths: loadSettings.initialPaths)
if states.length > 0 or allowEmpty
- @storageFolder.store('application.json', states)
+ @storageFolder.storeSync('application.json', states)
loadState: (options) ->
- if (states = @storageFolder.load('application.json'))?.length > 0
+ restorePreviousState = @config.get('core.restorePreviousWindowsOnStart') ? true
+ if restorePreviousState and (states = @storageFolder.load('application.json'))?.length > 0
for state in states
@openWithOptions(_.extend(options, {
- pathsToOpen: state.initialPaths
+ initialPaths: state.initialPaths
+ pathsToOpen: state.initialPaths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
urlsToOpen: []
devMode: @devMode
safeMode: @safeMode
@@ -497,7 +549,7 @@ class AtomApplication
# :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}) ->
+ openUrl: ({urlToOpen, devMode, safeMode, env}) ->
unless @packages?
PackageManager = require '../package-manager'
@packages = new PackageManager
@@ -511,8 +563,8 @@ class AtomApplication
if pack.urlMain
packagePath = @packages.resolvePackagePath(packageName)
windowInitializationScript = path.resolve(packagePath, pack.urlMain)
- windowDimensions = @focusedWindow()?.getDimensions()
- new AtomWindow({windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions})
+ windowDimensions = @getDimensionsForNewWindow()
+ new AtomWindow({windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env})
else
console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}"
else
@@ -527,7 +579,7 @@ class AtomApplication
# :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}) ->
+ runTests: ({headless, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout, env}) ->
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
resourcePath = @resourcePath
@@ -557,7 +609,7 @@ class AtomApplication
devMode = true
isSpec = true
safeMode ?= false
- new AtomWindow({windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode})
+ new AtomWindow({windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env})
resolveTestRunnerPath: (testPath) ->
FindParentDir ?= require 'find-parent-dir'
@@ -580,7 +632,7 @@ class AtomApplication
catch error
require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner'))
- locationForPathToOpen: (pathToOpen, executedFrom='') ->
+ locationForPathToOpen: (pathToOpen, executedFrom='', forceAddToWindow) ->
return {pathToOpen} unless pathToOpen
pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
@@ -596,7 +648,7 @@ class AtomApplication
unless url.parse(pathToOpen).protocol?
pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen))
- {pathToOpen, initialLine, initialColumn}
+ {pathToOpen, initialLine, initialColumn, forceAddToWindow}
# Opens a native dialog to prompt the user for a path.
#
@@ -610,11 +662,13 @@ class AtomApplication
# :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.
- promptForPathToOpen: (type, {devMode, safeMode, window}) ->
- @promptForPath type, (pathsToOpen) =>
- @openPaths({pathsToOpen, devMode, safeMode, window})
+ # :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) ->
+ promptForPath: (type, callback, path) ->
properties =
switch type
when 'file' then ['openFile']
@@ -637,8 +691,8 @@ class AtomApplication
when 'folder' then 'Open Folder'
else 'Open'
- if process.platform is 'linux'
- if projectPath = @lastFocusedWindow?.projectPath
- openOptions.defaultPath = projectPath
+ # File dialog defaults to project directory of currently active editor
+ if path?
+ openOptions.defaultPath = path
dialog.showOpenDialog(parentWindow, openOptions, callback)
diff --git a/src/browser/atom-portable.coffee b/src/browser/atom-portable.coffee
index 5f8f10cf6..ae4bb67ec 100644
--- a/src/browser/atom-portable.coffee
+++ b/src/browser/atom-portable.coffee
@@ -1,6 +1,6 @@
fs = require 'fs-plus'
path = require 'path'
-ipc = require 'ipc'
+{ipcMain} = require 'electron'
module.exports =
class AtomPortable
@@ -30,6 +30,6 @@ class AtomPortable
catch error
message = "Failed to use portable Atom home directory (#{@getPortableAtomHomePath()}). Using the default instead (#{defaultHome}). #{error.message}"
- ipc.on 'check-portable-home-writable', (event) ->
+ ipcMain.on 'check-portable-home-writable', (event) ->
event.sender.send 'check-portable-home-writable-response', {writable, message}
writable
diff --git a/src/browser/atom-protocol-handler.coffee b/src/browser/atom-protocol-handler.coffee
index 0fda8095b..3967c0525 100644
--- a/src/browser/atom-protocol-handler.coffee
+++ b/src/browser/atom-protocol-handler.coffee
@@ -1,7 +1,6 @@
-app = require 'app'
+{app, protocol} = require 'electron'
fs = require 'fs'
path = require 'path'
-protocol = require 'protocol'
# Handles requests with 'atom' protocol.
#
diff --git a/src/browser/atom-window.coffee b/src/browser/atom-window.coffee
index 20ba2ad5c..60e6d0553 100644
--- a/src/browser/atom-window.coffee
+++ b/src/browser/atom-window.coffee
@@ -1,6 +1,4 @@
-BrowserWindow = require 'browser-window'
-app = require 'app'
-dialog = require 'dialog'
+{BrowserWindow, app, dialog} = require 'electron'
path = require 'path'
fs = require 'fs'
url = require 'url'
@@ -19,7 +17,7 @@ class AtomWindow
isSpec: null
constructor: (settings={}) ->
- {@resourcePath, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
+ {@resourcePath, initialPaths, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
locationsToOpen ?= [{pathToOpen}] if pathToOpen
locationsToOpen ?= []
@@ -43,20 +41,13 @@ class AtomWindow
@handleEvents()
loadSettings = _.extend({}, settings)
- loadSettings.windowState ?= '{}'
loadSettings.appVersion = app.getVersion()
loadSettings.resourcePath = @resourcePath
loadSettings.devMode ?= false
loadSettings.safeMode ?= false
loadSettings.atomHome = process.env.ATOM_HOME
- loadSettings.firstLoad = true
-
- # Only send to the first non-spec window created
- if @constructor.includeShellLoadTime and not @isSpec
- @constructor.includeShellLoadTime = false
- loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
-
- loadSettings.initialPaths =
+ loadSettings.clearWindowState ?= false
+ loadSettings.initialPaths ?=
for {pathToOpen} in locationsToOpen when pathToOpen
if fs.statSyncNoException(pathToOpen).isFile?()
path.dirname(pathToOpen)
@@ -65,24 +56,27 @@ class AtomWindow
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
+
@browserWindow.loadSettings = loadSettings
+
@browserWindow.once 'window:loaded', =>
@emit 'window:loaded'
@loaded = true
@setLoadSettings(loadSettings)
+ @env = loadSettings.env if loadSettings.env?
@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()
- setLoadSettings: (loadSettingsObj) ->
- # Ignore the windowState when passing loadSettings via URL, since it could
- # be quite large.
- loadSettings = _.clone(loadSettingsObj)
- delete loadSettings['windowState']
-
- @browserWindow.loadUrl url.format
+ setLoadSettings: (loadSettings) ->
+ @browserWindow.loadURL url.format
protocol: 'file'
pathname: "#{@resourcePath}/static/index.html"
slashes: true
@@ -90,7 +84,7 @@ class AtomWindow
getLoadSettings: ->
if @browserWindow.webContents? and not @browserWindow.webContents.isLoading()
- hash = url.parse(@browserWindow.webContents.getUrl()).hash.substr(1)
+ hash = url.parse(@browserWindow.webContents.getURL()).hash.substr(1)
JSON.parse(decodeURIComponent(hash))
hasProjectPath: -> @getLoadSettings().initialPaths?.length > 0
@@ -122,6 +116,9 @@ class AtomWindow
false
handleEvents: ->
+ @browserWindow.on 'close', ->
+ global.atomApplication.saveState(false)
+
@browserWindow.on 'closed', =>
global.atomApplication.removeWindow(this)
@@ -145,10 +142,10 @@ class AtomWindow
detail: 'Please report this issue to https://github.com/atom/atom'
switch chosen
when 0 then @browserWindow.destroy()
- when 1 then @browserWindow.restart()
+ when 1 then @browserWindow.reload()
@browserWindow.webContents.on 'will-navigate', (event, url) =>
- unless url is @browserWindow.webContents.getUrl()
+ unless url is @browserWindow.webContents.getURL()
event.preventDefault()
@setupContextMenu()
@@ -173,6 +170,9 @@ class AtomWindow
else
@browserWindow.once 'window:loaded', => @openLocations(locationsToOpen)
+ replaceEnvironment: (env) ->
+ @browserWindow.webContents.send 'environment', env
+
sendMessage: (message, detail) ->
@browserWindow.webContents.send 'message', message, detail
@@ -221,6 +221,6 @@ class AtomWindow
isSpecWindow: -> @isSpec
- reload: -> @browserWindow.restart()
+ reload: -> @browserWindow.reload()
toggleDevTools: -> @browserWindow.toggleDevTools()
diff --git a/src/browser/auto-update-manager.coffee b/src/browser/auto-update-manager.coffee
index 6a008d44d..602284b07 100644
--- a/src/browser/auto-update-manager.coffee
+++ b/src/browser/auto-update-manager.coffee
@@ -1,6 +1,5 @@
autoUpdater = null
_ = require 'underscore-plus'
-Config = require '../config'
{EventEmitter} = require 'events'
path = require 'path'
@@ -16,39 +15,45 @@ module.exports =
class AutoUpdateManager
_.extend @prototype, EventEmitter.prototype
- constructor: (@version, @testMode, resourcePath) ->
+ constructor: (@version, @testMode, resourcePath, @config) ->
@state = IdleState
@iconPath = path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
@feedUrl = "https://atom.io/api/updates?version=#{@version}"
- @config = new Config({configDirPath: process.env.ATOM_HOME, resourcePath, enablePersistence: true})
- @config.setSchema null, {type: 'object', properties: _.clone(require('../config-schema'))}
- @config.load()
process.nextTick => @setupAutoUpdater()
setupAutoUpdater: ->
if process.platform is 'win32'
autoUpdater = require './auto-updater-win32'
else
- autoUpdater = require 'auto-updater'
+ {autoUpdater} = require 'electron'
autoUpdater.on 'error', (event, message) =>
- @setState(ErrorState)
+ @setState(ErrorState, message)
+ @emitWindowEvent('update-error')
console.error "Error Downloading Update: #{message}"
- autoUpdater.setFeedUrl @feedUrl
+ autoUpdater.setFeedURL @feedUrl
autoUpdater.on 'checking-for-update', =>
@setState(CheckingState)
+ @emitWindowEvent('checking-for-update')
autoUpdater.on 'update-not-available', =>
@setState(NoUpdateAvailableState)
+ @emitWindowEvent('update-not-available')
autoUpdater.on 'update-available', =>
@setState(DownladingState)
+ # We use sendMessage to send an event called 'update-available' in 'update-downloaded'
+ # once the update download is complete. This mismatch between the electron
+ # autoUpdater events is unfortunate but in the interest of not changing the
+ # one existing event handled by applicationDelegate
+ @emitWindowEvent('did-begin-downloading-update')
+ @emit('did-begin-download')
autoUpdater.on 'update-downloaded', (event, releaseNotes, @releaseVersion) =>
@setState(UpdateAvailableState)
- @emitUpdateAvailableEvent(@getWindows()...)
+ @emitUpdateAvailableEvent()
@config.onDidChange 'core.automaticallyUpdate', ({newValue}) =>
if newValue
@@ -64,20 +69,28 @@ class AutoUpdateManager
when 'linux'
@setState(UnsupportedState)
- emitUpdateAvailableEvent: (windows...) ->
+ emitUpdateAvailableEvent: ->
return unless @releaseVersion?
- for atomWindow in windows
- atomWindow.sendMessage('update-available', {@releaseVersion})
+ @emitWindowEvent('update-available', {@releaseVersion})
return
- setState: (state) ->
+ emitWindowEvent: (eventName, payload) ->
+ for atomWindow in @getWindows()
+ atomWindow.sendMessage(eventName, payload)
+ return
+
+ setState: (state, errorMessage) ->
return if @state is state
@state = state
+ @errorMessage = errorMessage
@emit 'state-changed', @state
getState: ->
@state
+ getErrorMessage: ->
+ @errorMessage
+
scheduleUpdateCheck: ->
# Only schedule update check periodically if running in release version and
# and there is no existing scheduled update check.
@@ -104,7 +117,7 @@ class AutoUpdateManager
onUpdateNotAvailable: =>
autoUpdater.removeListener 'error', @onUpdateError
- dialog = require 'dialog'
+ {dialog} = require 'electron'
dialog.showMessageBox
type: 'info'
buttons: ['OK']
@@ -115,7 +128,7 @@ class AutoUpdateManager
onUpdateError: (event, message) =>
autoUpdater.removeListener 'update-not-available', @onUpdateNotAvailable
- dialog = require 'dialog'
+ {dialog} = require 'electron'
dialog.showMessageBox
type: 'warning'
buttons: ['OK']
diff --git a/src/browser/auto-updater-win32.coffee b/src/browser/auto-updater-win32.coffee
index 4d043ac4e..e31578d49 100644
--- a/src/browser/auto-updater-win32.coffee
+++ b/src/browser/auto-updater-win32.coffee
@@ -5,13 +5,13 @@ SquirrelUpdate = require './squirrel-update'
class AutoUpdater
_.extend @prototype, EventEmitter.prototype
- setFeedUrl: (@updateUrl) ->
+ setFeedURL: (@updateUrl) ->
quitAndInstall: ->
if SquirrelUpdate.existsSync()
- SquirrelUpdate.restartAtom(require('app'))
+ SquirrelUpdate.restartAtom(require('electron').app)
else
- require('auto-updater').quitAndInstall()
+ require('electron').autoUpdater.quitAndInstall()
downloadUpdate: (callback) ->
SquirrelUpdate.spawn ['--download', @updateUrl], (error, stdout) ->
diff --git a/src/browser/context-menu.coffee b/src/browser/context-menu.coffee
index 44b57cdc9..1bc9c29ba 100644
--- a/src/browser/context-menu.coffee
+++ b/src/browser/context-menu.coffee
@@ -1,4 +1,4 @@
-Menu = require 'menu'
+{Menu} = require 'electron'
module.exports =
class ContextMenu
diff --git a/src/browser/main.coffee b/src/browser/main.coffee
index ca9d7e3ae..6bf8817f9 100644
--- a/src/browser/main.coffee
+++ b/src/browser/main.coffee
@@ -4,15 +4,16 @@ process.on 'uncaughtException', (error={}) ->
console.log(error.message) if error.message?
console.log(error.stack) if error.stack?
-crashReporter = require 'crash-reporter'
-app = require 'app'
+{crashReporter, app} = require 'electron'
fs = require 'fs-plus'
path = require 'path'
+temp = require 'temp'
yargs = require 'yargs'
console.log = require 'nslog'
start = ->
args = parseCommandLine()
+ args.env = process.env
setupAtomHome(args)
setupCompileCache()
return if handleStartupEventWithSquirrel()
@@ -32,6 +33,11 @@ start = ->
app.on 'open-url', addUrlToOpen
app.on 'will-finish-launching', setupCrashReporter
+ if args.userDataDir?
+ app.setPath('userData', args.userDataDir)
+ else if args.test
+ app.setPath('userData', temp.mkdirSync('atom-test-data'))
+
app.on 'ready', ->
app.removeListener 'open-file', addPathToOpen
app.removeListener 'open-url', addUrlToOpen
@@ -54,12 +60,12 @@ handleStartupEventWithSquirrel = ->
SquirrelUpdate.handleStartupEvent(app, squirrelCommand)
setupCrashReporter = ->
- crashReporter.start(productName: 'Atom', companyName: 'GitHub')
+ crashReporter.start(productName: 'Atom', companyName: 'GitHub', submitURL: 'http://54.249.141.255:1127/post')
setupAtomHome = ({setPortable}) ->
return if process.env.ATOM_HOME
- atomHome = path.join(app.getHomeDir(), '.atom')
+ atomHome = path.join(app.getPath('home'), '.atom')
AtomPortable = require './atom-portable'
if setPortable and not AtomPortable.isPortableInstall(process.platform, process.env.ATOM_HOME, atomHome)
@@ -81,6 +87,15 @@ setupCompileCache = ->
compileCache = require('../compile-cache')
compileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
+writeFullVersion = ->
+ process.stdout.write """
+ Atom : #{app.getVersion()}
+ Electron: #{process.versions.electron}
+ Chrome : #{process.versions.chrome}
+ Node : #{process.versions.node}
+
+ """
+
parseCommandLine = ->
version = app.getVersion()
options = yargs(process.argv[1..]).wrap(100)
@@ -116,9 +131,12 @@ parseCommandLine = ->
options.boolean('portable').describe('portable', 'Set portable mode. Copies the ~/.atom folder to be a sibling of the installed Atom location if a .atom folder is not already there.')
options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.')
options.string('timeout').describe('timeout', 'When in test mode, waits until the specified time (in minutes) and kills the process (exit code: 130).')
- options.alias('v', 'version').boolean('v').describe('v', 'Print the version.')
+ options.alias('v', 'version').boolean('v').describe('v', 'Print the version information.')
options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.')
+ options.alias('a', 'add').boolean('a').describe('add', 'Open path as a new project in last used window.')
options.string('socket-path')
+ options.string('user-data-dir')
+ options.boolean('clear-window-state').describe('clear-window-state', 'Delete all Atom environment state.')
args = options.argv
@@ -127,9 +145,10 @@ parseCommandLine = ->
process.exit(0)
if args.version
- process.stdout.write("#{version}\n")
+ writeFullVersion()
process.exit(0)
+ addToLastWindow = args['add']
executedFrom = args['executed-from']?.toString() ? process.cwd()
devMode = args['dev']
safeMode = args['safe']
@@ -140,9 +159,11 @@ parseCommandLine = ->
pidToKillWhenClosed = args['pid'] if args['wait']
logFile = args['log-file']
socketPath = args['socket-path']
+ userDataDir = args['user-data-dir']
profileStartup = args['profile-startup']
+ clearWindowState = args['clear-window-state']
urlsToOpen = []
- devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getHomeDir(), 'github', 'atom')
+ devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH ? path.join(app.getPath('home'), 'github', 'atom')
setPortable = args.portable
if args['resource-path']
@@ -164,6 +185,7 @@ parseCommandLine = ->
{resourcePath, devResourcePath, pathsToOpen, urlsToOpen, executedFrom, test,
version, pidToKillWhenClosed, devMode, safeMode, newWindow,
- logFile, socketPath, profileStartup, timeout, setPortable}
+ logFile, socketPath, userDataDir, profileStartup, timeout, setPortable,
+ clearWindowState, addToLastWindow}
start()
diff --git a/src/browser/spawner.coffee b/src/browser/spawner.coffee
new file mode 100644
index 000000000..edf93182e
--- /dev/null
+++ b/src/browser/spawner.coffee
@@ -0,0 +1,36 @@
+ChildProcess = require 'child_process'
+
+# Spawn a command and invoke the callback when it completes with an error
+# and the output from standard out.
+#
+# * `command` The underlying OS command {String} to execute.
+# * `args` (optional) The {Array} with arguments to be passed to command.
+# * `callback` (optional) The {Function} to call after the command has run. It will be invoked with arguments:
+# * `error` (optional) An {Error} object returned by the command, `null` if no error was thrown.
+# * `code` Error code returned by the command.
+# * `stdout` The {String} output text generated by the command.
+# * `stdout` The {String} output text generated by the command.
+#
+# Returns `undefined`.
+exports.spawn = (command, args, callback) ->
+ stdout = ''
+
+ try
+ spawnedProcess = ChildProcess.spawn(command, args)
+ catch error
+ # Spawn can throw an error
+ process.nextTick -> callback?(error, stdout)
+ return
+
+ spawnedProcess.stdout.on 'data', (data) -> stdout += data
+
+ error = null
+ spawnedProcess.on 'error', (processError) -> error ?= processError
+ spawnedProcess.on 'close', (code, signal) ->
+ error ?= new Error("Command failed: #{signal ? code}") if code isnt 0
+ error?.code ?= code
+ error?.stdout ?= stdout
+ callback?(error, stdout)
+ # This is necessary if using Powershell 2 on Windows 7 to get the events to raise
+ # http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs
+ spawnedProcess.stdin.end()
diff --git a/src/browser/squirrel-update.coffee b/src/browser/squirrel-update.coffee
index 3660158fc..a1bfc5359 100644
--- a/src/browser/squirrel-update.coffee
+++ b/src/browser/squirrel-update.coffee
@@ -1,6 +1,8 @@
-ChildProcess = require 'child_process'
fs = require 'fs-plus'
path = require 'path'
+Spawner = require './spawner'
+WinRegistry = require './win-registry'
+WinPowerShell = require './win-powershell'
appFolder = path.resolve(process.execPath, '..')
rootAtomFolder = path.resolve(appFolder, '..')
@@ -10,121 +12,18 @@ exeName = path.basename(process.execPath)
if process.env.SystemRoot
system32Path = path.join(process.env.SystemRoot, 'System32')
- regPath = path.join(system32Path, 'reg.exe')
setxPath = path.join(system32Path, 'setx.exe')
else
- regPath = 'reg.exe'
setxPath = 'setx.exe'
-# Registry keys used for context menu
-fileKeyPath = 'HKCU\\Software\\Classes\\*\\shell\\Atom'
-directoryKeyPath = 'HKCU\\Software\\Classes\\directory\\shell\\Atom'
-backgroundKeyPath = 'HKCU\\Software\\Classes\\directory\\background\\shell\\Atom'
-environmentKeyPath = 'HKCU\\Environment'
-
-# Spawn a command and invoke the callback when it completes with an error
-# and the output from standard out.
-spawn = (command, args, callback) ->
- stdout = ''
-
- try
- spawnedProcess = ChildProcess.spawn(command, args)
- catch error
- # Spawn can throw an error
- process.nextTick -> callback?(error, stdout)
- return
-
- spawnedProcess.stdout.on 'data', (data) -> stdout += data
-
- error = null
- spawnedProcess.on 'error', (processError) -> error ?= processError
- spawnedProcess.on 'close', (code, signal) ->
- error ?= new Error("Command failed: #{signal ? code}") if code isnt 0
- error?.code ?= code
- error?.stdout ?= stdout
- callback?(error, stdout)
-
-# Spawn reg.exe and callback when it completes
-spawnReg = (args, callback) ->
- spawn(regPath, args, callback)
-
# Spawn setx.exe and callback when it completes
spawnSetx = (args, callback) ->
- spawn(setxPath, args, callback)
+ Spawner.spawn(setxPath, args, callback)
# Spawn the Update.exe with the given arguments and invoke the callback when
# the command completes.
spawnUpdate = (args, callback) ->
- spawn(updateDotExe, args, callback)
-
-# Install the Open with Atom explorer context menu items via the registry.
-installContextMenu = (callback) ->
- addToRegistry = (args, callback) ->
- args.unshift('add')
- args.push('/f')
- spawnReg(args, callback)
-
- installMenu = (keyPath, arg, callback) ->
- args = [keyPath, '/ve', '/d', 'Open with Atom']
- addToRegistry args, ->
- args = [keyPath, '/v', 'Icon', '/d', "\"#{process.execPath}\""]
- addToRegistry args, ->
- args = ["#{keyPath}\\command", '/ve', '/d', "\"#{process.execPath}\" \"#{arg}\""]
- addToRegistry(args, callback)
-
- installMenu fileKeyPath, '%1', ->
- installMenu directoryKeyPath, '%1', ->
- installMenu(backgroundKeyPath, '%V', callback)
-
-isAscii = (text) ->
- index = 0
- while index < text.length
- return false if text.charCodeAt(index) > 127
- index++
- true
-
-# Get the user's PATH environment variable registry value.
-getPath = (callback) ->
- spawnReg ['query', environmentKeyPath, '/v', 'Path'], (error, stdout) ->
- if error?
- if error.code is 1
- # FIXME Don't overwrite path when reading value is disabled
- # https://github.com/atom/atom/issues/5092
- if stdout.indexOf('ERROR: Registry editing has been disabled by your administrator.') isnt -1
- return callback(error)
-
- # The query failed so the Path does not exist yet in the registry
- return callback(null, '')
- else
- return callback(error)
-
- # Registry query output is in the form:
- #
- # HKEY_CURRENT_USER\Environment
- # Path REG_SZ C:\a\folder\on\the\path;C\another\folder
- #
-
- lines = stdout.split(/[\r\n]+/).filter (line) -> line
- segments = lines[lines.length - 1]?.split(' ')
- if segments[1] is 'Path' and segments.length >= 3
- pathEnv = segments?[3..].join(' ')
- if isAscii(pathEnv)
- callback(null, pathEnv)
- else
- # FIXME Don't corrupt non-ASCII PATH values
- # https://github.com/atom/atom/issues/5063
- callback(new Error('PATH contains non-ASCII values'))
- else
- callback(new Error('Registry query for PATH failed'))
-
-# Uninstall the Open with Atom explorer context menu items via the registry.
-uninstallContextMenu = (callback) ->
- deleteFromRegistry = (keyPath, callback) ->
- spawnReg(['delete', keyPath, '/f'], callback)
-
- deleteFromRegistry fileKeyPath, ->
- deleteFromRegistry directoryKeyPath, ->
- deleteFromRegistry(backgroundKeyPath, callback)
+ Spawner.spawn(updateDotExe, args, callback)
# Add atom and apm to the PATH
#
@@ -163,7 +62,7 @@ addCommandsToPath = (callback) ->
installCommands (error) ->
return callback(error) if error?
- getPath (error, pathEnv) ->
+ WinPowerShell.getPath (error, pathEnv) ->
return callback(error) if error?
pathSegments = pathEnv.split(/;+/).filter (pathSegment) -> pathSegment
@@ -174,7 +73,7 @@ addCommandsToPath = (callback) ->
# Remove atom and apm from the PATH
removeCommandsFromPath = (callback) ->
- getPath (error, pathEnv) ->
+ WinPowerShell.getPath (error, pathEnv) ->
return callback(error) if error?
pathSegments = pathEnv.split(/;+/).filter (pathSegment) ->
@@ -223,7 +122,7 @@ exports.existsSync = ->
exports.restartAtom = (app) ->
if projectPath = global.atomApplication?.lastFocusedWindow?.projectPath
args = [projectPath]
- app.once 'will-quit', -> spawn(path.join(binFolder, 'atom.cmd'), args)
+ app.once 'will-quit', -> Spawner.spawn(path.join(binFolder, 'atom.cmd'), args)
app.quit()
# Handle squirrel events denoted by --squirrel-* command line arguments.
@@ -231,19 +130,19 @@ exports.handleStartupEvent = (app, squirrelCommand) ->
switch squirrelCommand
when '--squirrel-install'
createShortcuts ->
- installContextMenu ->
+ WinRegistry.installContextMenu ->
addCommandsToPath ->
app.quit()
true
when '--squirrel-updated'
updateShortcuts ->
- installContextMenu ->
+ WinRegistry.installContextMenu ->
addCommandsToPath ->
app.quit()
true
when '--squirrel-uninstall'
removeShortcuts ->
- uninstallContextMenu ->
+ WinRegistry.uninstallContextMenu ->
removeCommandsFromPath ->
app.quit()
true
diff --git a/src/browser/win-powershell.coffee b/src/browser/win-powershell.coffee
new file mode 100644
index 000000000..29925a7c1
--- /dev/null
+++ b/src/browser/win-powershell.coffee
@@ -0,0 +1,39 @@
+path = require 'path'
+Spawner = require './spawner'
+
+if process.env.SystemRoot
+ system32Path = path.join(process.env.SystemRoot, 'System32')
+ powershellPath = path.join(system32Path, 'WindowsPowerShell', 'v1.0', 'powershell.exe')
+else
+ powershellPath = 'powershell.exe'
+
+# Spawn powershell.exe and callback when it completes
+spawnPowershell = (args, callback) ->
+ # Set encoding and execute the command, capture the output, and return it
+ # via .NET's console in order to have consistent UTF-8 encoding.
+ # See http://stackoverflow.com/questions/22349139/utf-8-output-from-powershell
+ # to address https://github.com/atom/atom/issues/5063
+ args[0] = """
+ [Console]::OutputEncoding=[System.Text.Encoding]::UTF8
+ $output=#{args[0]}
+ [Console]::WriteLine($output)
+ """
+ args.unshift('-command')
+ args.unshift('RemoteSigned')
+ args.unshift('-ExecutionPolicy')
+ args.unshift('-noprofile')
+ Spawner.spawn(powershellPath, args, callback)
+
+# Get the user's PATH environment variable registry value.
+#
+# * `callback` The {Function} to call after registry operation is done.
+# It will be invoked with the same arguments provided by {Spawner.spawn}.
+#
+# Returns the user's path {String}.
+exports.getPath = (callback) ->
+ spawnPowershell ['[environment]::GetEnvironmentVariable(\'Path\',\'User\')'], (error, stdout) ->
+ if error?
+ return callback(error)
+
+ pathOutput = stdout.replace(/^\s+|\s+$/g, '')
+ callback(null, pathOutput)
diff --git a/src/browser/win-registry.coffee b/src/browser/win-registry.coffee
new file mode 100644
index 000000000..f4b81b377
--- /dev/null
+++ b/src/browser/win-registry.coffee
@@ -0,0 +1,62 @@
+path = require 'path'
+Spawner = require './spawner'
+
+if process.env.SystemRoot
+ system32Path = path.join(process.env.SystemRoot, 'System32')
+ regPath = path.join(system32Path, 'reg.exe')
+else
+ regPath = 'reg.exe'
+
+# Registry keys used for context menu
+fileKeyPath = 'HKCU\\Software\\Classes\\*\\shell\\Atom'
+directoryKeyPath = 'HKCU\\Software\\Classes\\directory\\shell\\Atom'
+backgroundKeyPath = 'HKCU\\Software\\Classes\\directory\\background\\shell\\Atom'
+applicationsKeyPath = 'HKCU\\Software\\Classes\\Applications\\atom.exe'
+
+# Spawn reg.exe and callback when it completes
+spawnReg = (args, callback) ->
+ Spawner.spawn(regPath, args, callback)
+
+# Install the Open with Atom explorer context menu items via the registry.
+#
+# * `callback` The {Function} to call after registry operation is done.
+# It will be invoked with the same arguments provided by {Spawner.spawn}.
+#
+# Returns `undefined`.
+exports.installContextMenu = (callback) ->
+ addToRegistry = (args, callback) ->
+ args.unshift('add')
+ args.push('/f')
+ spawnReg(args, callback)
+
+ installFileHandler = (callback) ->
+ args = ["#{applicationsKeyPath}\\shell\\open\\command", '/ve', '/d', "\"#{process.execPath}\" \"%1\""]
+ addToRegistry(args, callback)
+
+ installMenu = (keyPath, arg, callback) ->
+ args = [keyPath, '/ve', '/d', 'Open with Atom']
+ addToRegistry args, ->
+ args = [keyPath, '/v', 'Icon', '/d', "\"#{process.execPath}\""]
+ addToRegistry args, ->
+ args = ["#{keyPath}\\command", '/ve', '/d', "\"#{process.execPath}\" \"#{arg}\""]
+ addToRegistry(args, callback)
+
+ installMenu fileKeyPath, '%1', ->
+ installMenu directoryKeyPath, '%1', ->
+ installMenu backgroundKeyPath, '%V', ->
+ installFileHandler(callback)
+
+# Uninstall the Open with Atom explorer context menu items via the registry.
+#
+# * `callback` The {Function} to call after registry operation is done.
+# It will be invoked with the same arguments provided by {Spawner.spawn}.
+#
+# Returns `undefined`.
+exports.uninstallContextMenu = (callback) ->
+ deleteFromRegistry = (keyPath, callback) ->
+ spawnReg(['delete', keyPath, '/f'], callback)
+
+ deleteFromRegistry fileKeyPath, ->
+ deleteFromRegistry directoryKeyPath, ->
+ deleteFromRegistry backgroundKeyPath, ->
+ deleteFromRegistry(applicationsKeyPath, callback)
diff --git a/src/buffered-node-process.coffee b/src/buffered-node-process.coffee
index bb1a1c655..406775277 100644
--- a/src/buffered-node-process.coffee
+++ b/src/buffered-node-process.coffee
@@ -46,7 +46,8 @@ class BufferedNodeProcess extends BufferedProcess
options ?= {}
options.env ?= Object.create(process.env)
- options.env['ATOM_SHELL_INTERNAL_RUN_AS_NODE'] = 1
+ options.env['ELECTRON_RUN_AS_NODE'] = 1
+ options.env['ELECTRON_NO_ATTACH_CONSOLE'] = 1
args = args?.slice() ? []
args.unshift(command)
diff --git a/src/buffered-process.coffee b/src/buffered-process.coffee
index c7097d711..07fcfb664 100644
--- a/src/buffered-process.coffee
+++ b/src/buffered-process.coffee
@@ -50,7 +50,7 @@ class BufferedProcess
options ?= {}
@command = command
# Related to joyent/node#2318
- if process.platform is 'win32'
+ if process.platform is 'win32' and not options.shell?
# Quote all arguments and escapes inner quotes
if args?
cmdArgs = args.filter (arg) -> arg?
@@ -67,7 +67,7 @@ class BufferedProcess
cmdArgs.unshift("\"#{command}\"")
else
cmdArgs.unshift(command)
- cmdArgs = ['/s', '/c', "\"#{cmdArgs.join(' ')}\""]
+ cmdArgs = ['/s', '/d', '/c', "\"#{cmdArgs.join(' ')}\""]
cmdOptions = _.clone(options)
cmdOptions.windowsVerbatimArguments = true
@spawn(@getCmdPath(), cmdArgs, cmdOptions)
@@ -111,11 +111,13 @@ class BufferedProcess
stream.on 'data', (data) =>
return if @killed
+ bufferedLength = buffered.length
buffered += data
- lastNewlineIndex = buffered.lastIndexOf('\n')
+ lastNewlineIndex = data.lastIndexOf('\n')
if lastNewlineIndex isnt -1
- onLines(buffered.substring(0, lastNewlineIndex + 1))
- buffered = buffered.substring(lastNewlineIndex + 1)
+ lineLength = lastNewlineIndex + bufferedLength + 1
+ onLines(buffered.substring(0, lineLength))
+ buffered = buffered.substring(lineLength)
stream.on 'close', =>
return if @killed
diff --git a/src/color.coffee b/src/color.coffee
index fc751ce42..b413b9e2c 100644
--- a/src/color.coffee
+++ b/src/color.coffee
@@ -85,5 +85,5 @@ parseAlpha = (alpha) ->
numberToHexString = (number) ->
hex = number.toString(16)
- hex = "0#{hex}" if number < 10
+ hex = "0#{hex}" if number < 16
hex
diff --git a/src/command-registry.coffee b/src/command-registry.coffee
index db2cf498d..955a1b540 100644
--- a/src/command-registry.coffee
+++ b/src/command-registry.coffee
@@ -244,11 +244,14 @@ class CommandRegistry
(@selectorBasedListenersByCommandName[event.type] ? [])
.filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector)
.sort (a, b) -> a.compare(b)
- listeners = listeners.concat(selectorBasedListeners)
+ listeners = selectorBasedListeners.concat(listeners)
matched = true if listeners.length > 0
- for listener in listeners
+ # Call inline listeners first in reverse registration order,
+ # and selector-based listeners by specificity and reverse
+ # registration order.
+ for listener in listeners by -1
break if immediatePropagationStopped
listener.callback.call(currentTarget, dispatchedEvent)
@@ -271,8 +274,8 @@ class SelectorBasedListener
@sequenceNumber = SequenceCount++
compare: (other) ->
- other.specificity - @specificity or
- other.sequenceNumber - @sequenceNumber
+ @specificity - other.specificity or
+ @sequenceNumber - other.sequenceNumber
class InlineListener
constructor: (@callback) ->
diff --git a/src/config-schema.coffee b/src/config-schema.coffee
index 15e5223b8..ed6691380 100644
--- a/src/config-schema.coffee
+++ b/src/config-schema.coffee
@@ -108,6 +108,10 @@ module.exports =
description: 'Automatically update Atom when a new release is available.'
type: 'boolean'
default: true
+ allowPendingPaneItems:
+ description: 'Allow items to be previewed without adding them to a pane permanently, such as when single clicking files in the tree view.'
+ type: 'boolean'
+ default: true
editor:
type: 'object'
@@ -151,6 +155,10 @@ module.exports =
type: 'boolean'
default: true
description: 'Show line numbers in the editor\'s gutter.'
+ atomicSoftTabs:
+ type: 'boolean'
+ default: true
+ description: 'Skip over tab-length runs of leading whitespace when moving the cursor.'
autoIndent:
type: 'boolean'
default: true
diff --git a/src/config.coffee b/src/config.coffee
index 348b7b94f..66f07516e 100644
--- a/src/config.coffee
+++ b/src/config.coffee
@@ -319,6 +319,23 @@ ScopeDescriptor = require './scope-descriptor'
# * line breaks - `line breaks
`
# * ~~strikethrough~~ - `~~strikethrough~~`
#
+# #### order
+#
+# The settings view orders your settings alphabetically. You can override this
+# ordering with the order key.
+#
+# ```coffee
+# config:
+# zSetting:
+# type: 'integer'
+# default: 4
+# order: 1
+# aSetting:
+# type: 'integer'
+# default: 4
+# order: 2
+# ```
+#
# ## Best practices
#
# * Don't depend on (or write to) configuration keys outside of your keypath.
diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee
index 1d80dec09..936a9c6b6 100644
--- a/src/context-menu-manager.coffee
+++ b/src/context-menu-manager.coffee
@@ -4,7 +4,7 @@ CSON = require 'season'
fs = require 'fs-plus'
{calculateSpecificity, validateSelector} = require 'clear-cut'
{Disposable} = require 'event-kit'
-remote = require 'remote'
+{remote} = require 'electron'
MenuHelpers = require './menu-helpers'
platformContextMenu = require('../package.json')?._atomMenu?['context-menu']
diff --git a/src/cursor.coffee b/src/cursor.coffee
index 9a8e9f6d0..b8f7c72b4 100644
--- a/src/cursor.coffee
+++ b/src/cursor.coffee
@@ -9,7 +9,7 @@ EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g
# where text can be inserted.
#
# Cursors belong to {TextEditor}s and have some metadata attached in the form
-# of a {TextEditorMarker}.
+# of a {DisplayMarker}.
module.exports =
class Cursor extends Model
screenPosition: null
@@ -129,7 +129,7 @@ class Cursor extends Model
Section: Cursor Position Details
###
- # Public: Returns the underlying {TextEditorMarker} for the cursor.
+ # Public: Returns the underlying {DisplayMarker} for the cursor.
# Useful with overlay {Decoration}s.
getMarker: -> @marker
@@ -261,11 +261,11 @@ class Cursor extends Model
while columnCount > column and row > 0
columnCount -= column
- column = @editor.lineTextForScreenRow(--row).length
+ column = @editor.lineLengthForScreenRow(--row)
columnCount-- # subtract 1 for the row move
column = column - columnCount
- @setScreenPosition({row, column}, clip: 'backward')
+ @setScreenPosition({row, column}, clipDirection: 'backward')
# Public: Moves the cursor right one screen column.
#
@@ -280,7 +280,7 @@ class Cursor extends Model
else
{row, column} = @getScreenPosition()
maxLines = @editor.getScreenLineCount()
- rowLength = @editor.lineTextForScreenRow(row).length
+ rowLength = @editor.lineLengthForScreenRow(row)
columnsRemainingInLine = rowLength - column
while columnCount > columnsRemainingInLine and row < maxLines - 1
@@ -288,11 +288,11 @@ class Cursor extends Model
columnCount-- # subtract 1 for the row move
column = 0
- rowLength = @editor.lineTextForScreenRow(++row).length
+ rowLength = @editor.lineLengthForScreenRow(++row)
columnsRemainingInLine = rowLength
column = column + columnCount
- @setScreenPosition({row, column}, clip: 'forward', wrapBeyondNewlines: true, wrapAtSoftNewlines: true)
+ @setScreenPosition({row, column}, clipDirection: 'forward')
# Public: Moves the cursor to the top of the buffer.
moveToTop: ->
diff --git a/src/decoration-manager.coffee b/src/decoration-manager.coffee
new file mode 100644
index 000000000..edb9dfb33
--- /dev/null
+++ b/src/decoration-manager.coffee
@@ -0,0 +1,181 @@
+{Emitter} = require 'event-kit'
+Model = require './model'
+Decoration = require './decoration'
+LayerDecoration = require './layer-decoration'
+
+module.exports =
+class DecorationManager extends Model
+ didUpdateDecorationsEventScheduled: false
+ updatedSynchronously: false
+
+ constructor: (@displayLayer, @defaultMarkerLayer) ->
+ super
+
+ @emitter = new Emitter
+ @decorationsById = {}
+ @decorationsByMarkerId = {}
+ @overlayDecorationsById = {}
+ @layerDecorationsByMarkerLayerId = {}
+ @decorationCountsByLayerId = {}
+ @layerUpdateDisposablesByLayerId = {}
+
+ observeDecorations: (callback) ->
+ callback(decoration) for decoration in @getDecorations()
+ @onDidAddDecoration(callback)
+
+ onDidAddDecoration: (callback) ->
+ @emitter.on 'did-add-decoration', callback
+
+ onDidRemoveDecoration: (callback) ->
+ @emitter.on 'did-remove-decoration', callback
+
+ onDidUpdateDecorations: (callback) ->
+ @emitter.on 'did-update-decorations', callback
+
+ setUpdatedSynchronously: (@updatedSynchronously) ->
+
+ decorationForId: (id) ->
+ @decorationsById[id]
+
+ getDecorations: (propertyFilter) ->
+ allDecorations = []
+ for markerId, decorations of @decorationsByMarkerId
+ allDecorations.push(decorations...) if decorations?
+ if propertyFilter?
+ allDecorations = allDecorations.filter (decoration) ->
+ for key, value of propertyFilter
+ return false unless decoration.properties[key] is value
+ true
+ allDecorations
+
+ getLineDecorations: (propertyFilter) ->
+ @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line')
+
+ getLineNumberDecorations: (propertyFilter) ->
+ @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line-number')
+
+ getHighlightDecorations: (propertyFilter) ->
+ @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('highlight')
+
+ getOverlayDecorations: (propertyFilter) ->
+ result = []
+ for id, decoration of @overlayDecorationsById
+ result.push(decoration)
+ if propertyFilter?
+ result.filter (decoration) ->
+ for key, value of propertyFilter
+ return false unless decoration.properties[key] is value
+ true
+ else
+ result
+
+ decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
+ decorationsByMarkerId = {}
+ for marker in @defaultMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow])
+ if decorations = @decorationsByMarkerId[marker.id]
+ decorationsByMarkerId[marker.id] = decorations
+ decorationsByMarkerId
+
+ decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
+ decorationsState = {}
+
+ for layerId of @decorationCountsByLayerId
+ layer = @displayLayer.getMarkerLayer(layerId)
+
+ for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid()
+ screenRange = marker.getScreenRange()
+ bufferRange = marker.getBufferRange()
+ rangeIsReversed = marker.isReversed()
+
+ if decorations = @decorationsByMarkerId[marker.id]
+ for decoration in decorations
+ decorationsState[decoration.id] = {
+ properties: decoration.properties
+ screenRange, bufferRange, rangeIsReversed
+ }
+
+ if layerDecorations = @layerDecorationsByMarkerLayerId[layerId]
+ for layerDecoration in layerDecorations
+ decorationsState["#{layerDecoration.id}-#{marker.id}"] = {
+ properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties
+ screenRange, bufferRange, rangeIsReversed
+ }
+
+ decorationsState
+
+ decorateMarker: (marker, decorationParams) ->
+ throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed()
+ marker = @displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id)
+ decoration = new Decoration(marker, this, decorationParams)
+ @decorationsByMarkerId[marker.id] ?= []
+ @decorationsByMarkerId[marker.id].push(decoration)
+ @overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay')
+ @decorationsById[decoration.id] = decoration
+ @observeDecoratedLayer(marker.layer)
+ @scheduleUpdateDecorationsEvent()
+ @emitter.emit 'did-add-decoration', decoration
+ decoration
+
+ decorateMarkerLayer: (markerLayer, decorationParams) ->
+ decoration = new LayerDecoration(markerLayer, this, decorationParams)
+ @layerDecorationsByMarkerLayerId[markerLayer.id] ?= []
+ @layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration)
+ @observeDecoratedLayer(markerLayer)
+ @scheduleUpdateDecorationsEvent()
+ decoration
+
+ decorationsForMarkerId: (markerId) ->
+ @decorationsByMarkerId[markerId]
+
+ scheduleUpdateDecorationsEvent: ->
+ if @updatedSynchronously
+ @emitter.emit 'did-update-decorations'
+ return
+
+ unless @didUpdateDecorationsEventScheduled
+ @didUpdateDecorationsEventScheduled = true
+ process.nextTick =>
+ @didUpdateDecorationsEventScheduled = false
+ @emitter.emit 'did-update-decorations'
+
+ decorationDidChangeType: (decoration) ->
+ if decoration.isType('overlay')
+ @overlayDecorationsById[decoration.id] = decoration
+ else
+ delete @overlayDecorationsById[decoration.id]
+
+ didDestroyDecoration: (decoration) ->
+ {marker} = decoration
+ return unless decorations = @decorationsByMarkerId[marker.id]
+ index = decorations.indexOf(decoration)
+
+ if index > -1
+ decorations.splice(index, 1)
+ delete @decorationsById[decoration.id]
+ @emitter.emit 'did-remove-decoration', decoration
+ delete @decorationsByMarkerId[marker.id] if decorations.length is 0
+ delete @overlayDecorationsById[decoration.id]
+ @unobserveDecoratedLayer(marker.layer)
+ @scheduleUpdateDecorationsEvent()
+
+ didDestroyLayerDecoration: (decoration) ->
+ {markerLayer} = decoration
+ return unless decorations = @layerDecorationsByMarkerLayerId[markerLayer.id]
+ index = decorations.indexOf(decoration)
+
+ if index > -1
+ decorations.splice(index, 1)
+ delete @layerDecorationsByMarkerLayerId[markerLayer.id] if decorations.length is 0
+ @unobserveDecoratedLayer(markerLayer)
+ @scheduleUpdateDecorationsEvent()
+
+ observeDecoratedLayer: (layer) ->
+ @decorationCountsByLayerId[layer.id] ?= 0
+ if ++@decorationCountsByLayerId[layer.id] is 1
+ @layerUpdateDisposablesByLayerId[layer.id] = layer.onDidUpdate(@scheduleUpdateDecorationsEvent.bind(this))
+
+ unobserveDecoratedLayer: (layer) ->
+ if --@decorationCountsByLayerId[layer.id] is 0
+ @layerUpdateDisposablesByLayerId[layer.id].dispose()
+ delete @decorationCountsByLayerId[layer.id]
+ delete @layerUpdateDisposablesByLayerId[layer.id]
diff --git a/src/decoration.coffee b/src/decoration.coffee
index 11e32236d..63be29d86 100644
--- a/src/decoration.coffee
+++ b/src/decoration.coffee
@@ -11,7 +11,7 @@ translateDecorationParamsOldToNew = (decorationParams) ->
decorationParams.gutterName = 'line-number'
decorationParams
-# Essential: Represents a decoration that follows a {TextEditorMarker}. A decoration is
+# Essential: Represents a decoration that follows a {DisplayMarker}. A decoration is
# basically a visual representation of a marker. It allows you to add CSS
# classes to line numbers in the gutter, lines, and add selection-line regions
# around marked ranges of text.
@@ -25,7 +25,7 @@ translateDecorationParamsOldToNew = (decorationParams) ->
# decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'})
# ```
#
-# Best practice for destroying the decoration is by destroying the {TextEditorMarker}.
+# Best practice for destroying the decoration is by destroying the {DisplayMarker}.
#
# ```coffee
# marker.destroy()
@@ -62,7 +62,7 @@ class Decoration
Section: Construction and Destruction
###
- constructor: (@marker, @displayBuffer, properties) ->
+ constructor: (@marker, @decorationManager, properties) ->
@emitter = new Emitter
@id = nextId()
@setProperties properties
@@ -71,14 +71,14 @@ class Decoration
# Essential: Destroy this marker.
#
- # If you own the marker, you should use {TextEditorMarker::destroy} which will destroy
+ # If you own the marker, you should use {DisplayMarker::destroy} which will destroy
# this decoration.
destroy: ->
return if @destroyed
@markerDestroyDisposable.dispose()
@markerDestroyDisposable = null
@destroyed = true
- @displayBuffer.didDestroyDecoration(this)
+ @decorationManager.didDestroyDecoration(this)
@emitter.emit 'did-destroy'
@emitter.dispose()
@@ -149,8 +149,8 @@ class Decoration
oldProperties = @properties
@properties = translateDecorationParamsOldToNew(newProperties)
if newProperties.type?
- @displayBuffer.decorationDidChangeType(this)
- @displayBuffer.scheduleUpdateDecorationsEvent()
+ @decorationManager.decorationDidChangeType(this)
+ @decorationManager.scheduleUpdateDecorationsEvent()
@emitter.emit 'did-change-properties', {oldProperties, newProperties}
###
@@ -175,5 +175,5 @@ class Decoration
@properties.flashCount++
@properties.flashClass = klass
@properties.flashDuration = duration
- @displayBuffer.scheduleUpdateDecorationsEvent()
+ @decorationManager.scheduleUpdateDecorationsEvent()
@emitter.emit 'did-flash'
diff --git a/src/default-directory-provider.coffee b/src/default-directory-provider.coffee
index 6b05a582f..ed4e9ba36 100644
--- a/src/default-directory-provider.coffee
+++ b/src/default-directory-provider.coffee
@@ -16,8 +16,8 @@ class DefaultDirectoryProvider
# * `null` if the given URI is not compatibile with this provider.
directoryForURISync: (uri) ->
normalizedPath = path.normalize(uri)
- {protocol} = url.parse(uri)
- directoryPath = if protocol?
+ {host} = url.parse(uri)
+ directoryPath = if host
uri
else if not fs.isDirectorySync(normalizedPath) and fs.isDirectorySync(path.dirname(normalizedPath))
path.dirname(normalizedPath)
@@ -26,7 +26,7 @@ class DefaultDirectoryProvider
# TODO: Stop normalizing the path in pathwatcher's Directory.
directory = new Directory(directoryPath)
- if protocol?
+ if host
directory.path = directoryPath
if fs.isCaseInsensitive()
directory.lowerCasePath = directoryPath.toLowerCase()
diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee
deleted file mode 100644
index 8b95656f9..000000000
--- a/src/display-buffer.coffee
+++ /dev/null
@@ -1,1144 +0,0 @@
-_ = require 'underscore-plus'
-{CompositeDisposable, Emitter} = require 'event-kit'
-{Point, Range} = require 'text-buffer'
-TokenizedBuffer = require './tokenized-buffer'
-RowMap = require './row-map'
-Fold = require './fold'
-Model = require './model'
-Token = require './token'
-Decoration = require './decoration'
-LayerDecoration = require './layer-decoration'
-TextEditorMarkerLayer = require './text-editor-marker-layer'
-
-class BufferToScreenConversionError extends Error
- constructor: (@message, @metadata) ->
- super
- Error.captureStackTrace(this, BufferToScreenConversionError)
-
-module.exports =
-class DisplayBuffer extends Model
- verticalScrollMargin: 2
- horizontalScrollMargin: 6
- changeCount: 0
- softWrapped: null
- editorWidthInChars: null
- lineHeightInPixels: null
- defaultCharWidth: null
- height: null
- width: null
- didUpdateDecorationsEventScheduled: false
- updatedSynchronously: false
-
- @deserialize: (state, atomEnvironment) ->
- state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
- state.foldsMarkerLayer = state.tokenizedBuffer.buffer.getMarkerLayer(state.foldsMarkerLayerId)
- state.config = atomEnvironment.config
- state.assert = atomEnvironment.assert
- state.grammarRegistry = atomEnvironment.grammars
- state.packageManager = atomEnvironment.packages
- new this(state)
-
- constructor: (params={}) ->
- super
-
- {
- tabLength, @editorWidthInChars, @tokenizedBuffer, @foldsMarkerLayer, buffer,
- ignoreInvisibles, @largeFileMode, @config, @assert, @grammarRegistry, @packageManager
- } = params
-
- @emitter = new Emitter
- @disposables = new CompositeDisposable
-
- @tokenizedBuffer ?= new TokenizedBuffer({
- tabLength, buffer, ignoreInvisibles, @largeFileMode, @config,
- @grammarRegistry, @packageManager, @assert
- })
- @buffer = @tokenizedBuffer.buffer
- @charWidthsByScope = {}
- @defaultMarkerLayer = new TextEditorMarkerLayer(this, @buffer.getDefaultMarkerLayer(), true)
- @customMarkerLayersById = {}
- @foldsByMarkerId = {}
- @decorationsById = {}
- @decorationsByMarkerId = {}
- @overlayDecorationsById = {}
- @layerDecorationsByMarkerLayerId = {}
- @decorationCountsByLayerId = {}
- @layerUpdateDisposablesByLayerId = {}
-
- @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings
- @disposables.add @tokenizedBuffer.onDidChange @handleTokenizedBufferChange
- @disposables.add @buffer.onDidCreateMarker @didCreateDefaultLayerMarker
-
- @foldsMarkerLayer ?= @buffer.addMarkerLayer()
- folds = (new Fold(this, marker) for marker in @foldsMarkerLayer.getMarkers())
- @updateAllScreenLines()
- @decorateFold(fold) for fold in folds
-
- subscribeToScopedConfigSettings: =>
- @scopedConfigSubscriptions?.dispose()
- @scopedConfigSubscriptions = subscriptions = new CompositeDisposable
-
- scopeDescriptor = @getRootScopeDescriptor()
-
- oldConfigSettings = @configSettings
- @configSettings =
- scrollPastEnd: @config.get('editor.scrollPastEnd', scope: scopeDescriptor)
- softWrap: @config.get('editor.softWrap', scope: scopeDescriptor)
- softWrapAtPreferredLineLength: @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor)
- softWrapHangingIndent: @config.get('editor.softWrapHangingIndent', scope: scopeDescriptor)
- preferredLineLength: @config.get('editor.preferredLineLength', scope: scopeDescriptor)
-
- subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, ({newValue}) =>
- @configSettings.softWrap = newValue
- @updateWrappedScreenLines()
-
- subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, ({newValue}) =>
- @configSettings.softWrapHangingIndent = newValue
- @updateWrappedScreenLines()
-
- subscriptions.add @config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, ({newValue}) =>
- @configSettings.softWrapAtPreferredLineLength = newValue
- @updateWrappedScreenLines() if @isSoftWrapped()
-
- subscriptions.add @config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, ({newValue}) =>
- @configSettings.preferredLineLength = newValue
- @updateWrappedScreenLines() if @isSoftWrapped() and @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor)
-
- subscriptions.add @config.observe 'editor.scrollPastEnd', scope: scopeDescriptor, (value) =>
- @configSettings.scrollPastEnd = value
-
- @updateWrappedScreenLines() if oldConfigSettings? and not _.isEqual(oldConfigSettings, @configSettings)
-
- serialize: ->
- deserializer: 'DisplayBuffer'
- id: @id
- softWrapped: @isSoftWrapped()
- editorWidthInChars: @editorWidthInChars
- tokenizedBuffer: @tokenizedBuffer.serialize()
- largeFileMode: @largeFileMode
- foldsMarkerLayerId: @foldsMarkerLayer.id
-
- copy: ->
- foldsMarkerLayer = @foldsMarkerLayer.copy()
- new DisplayBuffer({
- @buffer, tabLength: @getTabLength(), @largeFileMode, @config, @assert,
- @grammarRegistry, @packageManager, foldsMarkerLayer
- })
-
- updateAllScreenLines: ->
- @maxLineLength = 0
- @screenLines = []
- @rowMap = new RowMap
- @updateScreenLines(0, @buffer.getLineCount(), null, suppressChangeEvent: true)
-
- onDidChangeSoftWrapped: (callback) ->
- @emitter.on 'did-change-soft-wrapped', callback
-
- onDidChangeGrammar: (callback) ->
- @tokenizedBuffer.onDidChangeGrammar(callback)
-
- onDidTokenize: (callback) ->
- @tokenizedBuffer.onDidTokenize(callback)
-
- onDidChange: (callback) ->
- @emitter.on 'did-change', callback
-
- onDidChangeCharacterWidths: (callback) ->
- @emitter.on 'did-change-character-widths', callback
-
- onDidRequestAutoscroll: (callback) ->
- @emitter.on 'did-request-autoscroll', callback
-
- observeDecorations: (callback) ->
- callback(decoration) for decoration in @getDecorations()
- @onDidAddDecoration(callback)
-
- onDidAddDecoration: (callback) ->
- @emitter.on 'did-add-decoration', callback
-
- onDidRemoveDecoration: (callback) ->
- @emitter.on 'did-remove-decoration', callback
-
- onDidCreateMarker: (callback) ->
- @emitter.on 'did-create-marker', callback
-
- onDidUpdateMarkers: (callback) ->
- @emitter.on 'did-update-markers', callback
-
- onDidUpdateDecorations: (callback) ->
- @emitter.on 'did-update-decorations', callback
-
- emitDidChange: (eventProperties, refreshMarkers=true) ->
- @emitter.emit 'did-change', eventProperties
- if refreshMarkers
- @refreshMarkerScreenPositions()
- @emitter.emit 'did-update-markers'
-
- updateWrappedScreenLines: ->
- start = 0
- end = @getLastRow()
- @updateAllScreenLines()
- screenDelta = @getLastRow() - end
- bufferDelta = 0
- @emitDidChange({start, end, screenDelta, bufferDelta})
-
- # Sets the visibility of the tokenized buffer.
- #
- # visible - A {Boolean} indicating of the tokenized buffer is shown
- setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
-
- setUpdatedSynchronously: (@updatedSynchronously) ->
-
- getVerticalScrollMargin: ->
- maxScrollMargin = Math.floor(((@getHeight() / @getLineHeightInPixels()) - 1) / 2)
- Math.min(@verticalScrollMargin, maxScrollMargin)
-
- setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin
-
- getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@getWidth() / @getDefaultCharWidth()) - 1) / 2))
- setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin
-
- getHeight: ->
- @height
-
- setHeight: (@height) ->
- @height
-
- getWidth: ->
- @width
-
- setWidth: (newWidth) ->
- oldWidth = @width
- @width = newWidth
- @updateWrappedScreenLines() if newWidth isnt oldWidth and @isSoftWrapped()
- @width
-
- getLineHeightInPixels: -> @lineHeightInPixels
- setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels
-
- getKoreanCharWidth: -> @koreanCharWidth
-
- getHalfWidthCharWidth: -> @halfWidthCharWidth
-
- getDoubleWidthCharWidth: -> @doubleWidthCharWidth
-
- getDefaultCharWidth: -> @defaultCharWidth
-
- setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) ->
- doubleWidthCharWidth ?= defaultCharWidth
- halfWidthCharWidth ?= defaultCharWidth
- koreanCharWidth ?= defaultCharWidth
- if defaultCharWidth isnt @defaultCharWidth or doubleWidthCharWidth isnt @doubleWidthCharWidth and halfWidthCharWidth isnt @halfWidthCharWidth and koreanCharWidth isnt @koreanCharWidth
- @defaultCharWidth = defaultCharWidth
- @doubleWidthCharWidth = doubleWidthCharWidth
- @halfWidthCharWidth = halfWidthCharWidth
- @koreanCharWidth = koreanCharWidth
- @updateWrappedScreenLines() if @isSoftWrapped() and @getEditorWidthInChars()?
- defaultCharWidth
-
- getCursorWidth: -> 1
-
- scrollToScreenRange: (screenRange, options = {}) ->
- scrollEvent = {screenRange, options}
- @emitter.emit "did-request-autoscroll", scrollEvent
-
- scrollToScreenPosition: (screenPosition, options) ->
- @scrollToScreenRange(new Range(screenPosition, screenPosition), options)
-
- scrollToBufferPosition: (bufferPosition, options) ->
- @scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition), options)
-
- # Retrieves the current tab length.
- #
- # Returns a {Number}.
- getTabLength: ->
- @tokenizedBuffer.getTabLength()
-
- # Specifies the tab length.
- #
- # tabLength - A {Number} that defines the new tab length.
- setTabLength: (tabLength) ->
- @tokenizedBuffer.setTabLength(tabLength)
-
- setIgnoreInvisibles: (ignoreInvisibles) ->
- @tokenizedBuffer.setIgnoreInvisibles(ignoreInvisibles)
-
- setSoftWrapped: (softWrapped) ->
- if softWrapped isnt @softWrapped
- @softWrapped = softWrapped
- @updateWrappedScreenLines()
- softWrapped = @isSoftWrapped()
- @emitter.emit 'did-change-soft-wrapped', softWrapped
- softWrapped
- else
- @isSoftWrapped()
-
- isSoftWrapped: ->
- if @largeFileMode
- false
- else
- @softWrapped ? @configSettings.softWrap ? false
-
- # Set the number of characters that fit horizontally in the editor.
- #
- # editorWidthInChars - A {Number} of characters.
- setEditorWidthInChars: (editorWidthInChars) ->
- if editorWidthInChars > 0
- previousWidthInChars = @editorWidthInChars
- @editorWidthInChars = editorWidthInChars
- if editorWidthInChars isnt previousWidthInChars and @isSoftWrapped()
- @updateWrappedScreenLines()
-
- # Returns the editor width in characters for soft wrap.
- getEditorWidthInChars: ->
- width = @getWidth()
- if width? and @defaultCharWidth > 0
- Math.max(0, Math.floor(width / @defaultCharWidth))
- else
- @editorWidthInChars
-
- getSoftWrapColumn: ->
- if @configSettings.softWrapAtPreferredLineLength
- Math.min(@getEditorWidthInChars(), @configSettings.preferredLineLength)
- else
- @getEditorWidthInChars()
-
- getSoftWrapColumnForTokenizedLine: (tokenizedLine) ->
- lineMaxWidth = @getSoftWrapColumn() * @getDefaultCharWidth()
-
- return if Number.isNaN(lineMaxWidth)
- return 0 if lineMaxWidth is 0
-
- iterator = tokenizedLine.getTokenIterator(false)
- column = 0
- currentWidth = 0
- while iterator.next()
- textIndex = 0
- text = iterator.getText()
- while textIndex < text.length
- if iterator.isPairedCharacter()
- charLength = 2
- else
- charLength = 1
-
- if iterator.hasDoubleWidthCharacterAt(textIndex)
- charWidth = @getDoubleWidthCharWidth()
- else if iterator.hasHalfWidthCharacterAt(textIndex)
- charWidth = @getHalfWidthCharWidth()
- else if iterator.hasKoreanCharacterAt(textIndex)
- charWidth = @getKoreanCharWidth()
- else
- charWidth = @getDefaultCharWidth()
-
- return column if currentWidth + charWidth > lineMaxWidth
-
- currentWidth += charWidth
- column += charLength
- textIndex += charLength
- column
-
- # Gets the screen line for the given screen row.
- #
- # * `screenRow` - A {Number} indicating the screen row.
- #
- # Returns {TokenizedLine}
- tokenizedLineForScreenRow: (screenRow) ->
- if @largeFileMode
- if line = @tokenizedBuffer.tokenizedLineForRow(screenRow)
- if line.text.length > @maxLineLength
- @maxLineLength = line.text.length
- @longestScreenRow = screenRow
- line
- else
- @screenLines[screenRow]
-
- # Gets the screen lines for the given screen row range.
- #
- # startRow - A {Number} indicating the beginning screen row.
- # endRow - A {Number} indicating the ending screen row.
- #
- # Returns an {Array} of {TokenizedLine}s.
- tokenizedLinesForScreenRows: (startRow, endRow) ->
- if @largeFileMode
- @tokenizedBuffer.tokenizedLinesForRows(startRow, endRow)
- else
- @screenLines[startRow..endRow]
-
- # Gets all the screen lines.
- #
- # Returns an {Array} of {TokenizedLine}s.
- getTokenizedLines: ->
- if @largeFileMode
- @tokenizedBuffer.tokenizedLinesForRows(0, @getLastRow())
- else
- new Array(@screenLines...)
-
- indentLevelForLine: (line) ->
- @tokenizedBuffer.indentLevelForLine(line)
-
- # Given starting and ending screen rows, this returns an array of the
- # buffer rows corresponding to every screen row in the range
- #
- # startScreenRow - The screen row {Number} to start at
- # endScreenRow - The screen row {Number} to end at (default: the last screen row)
- #
- # Returns an {Array} of buffer rows as {Numbers}s.
- bufferRowsForScreenRows: (startScreenRow, endScreenRow) ->
- if @largeFileMode
- [startScreenRow..endScreenRow]
- else
- for screenRow in [startScreenRow..endScreenRow]
- @rowMap.bufferRowRangeForScreenRow(screenRow)[0]
-
- # Creates a new fold between two row numbers.
- #
- # startRow - The row {Number} to start folding at
- # endRow - The row {Number} to end the fold
- #
- # Returns the new {Fold}.
- createFold: (startRow, endRow) ->
- unless @largeFileMode
- if foldMarker = @findFoldMarker({startRow, endRow})
- @foldForMarker(foldMarker)
- else
- foldMarker = @foldsMarkerLayer.markRange([[startRow, 0], [endRow, Infinity]])
- fold = new Fold(this, foldMarker)
- fold.updateDisplayBuffer()
- @decorateFold(fold)
- fold
-
- isFoldedAtBufferRow: (bufferRow) ->
- @largestFoldContainingBufferRow(bufferRow)?
-
- isFoldedAtScreenRow: (screenRow) ->
- @largestFoldContainingBufferRow(@bufferRowForScreenRow(screenRow))?
-
- # Destroys the fold with the given id
- destroyFoldWithId: (id) ->
- @foldsByMarkerId[id]?.destroy()
-
- # Removes any folds found that contain the given buffer row.
- #
- # bufferRow - The buffer row {Number} to check against
- unfoldBufferRow: (bufferRow) ->
- fold.destroy() for fold in @foldsContainingBufferRow(bufferRow)
- return
-
- # Given a buffer row, this returns the largest fold that starts there.
- #
- # Largest is defined as the fold whose difference between its start and end points
- # are the greatest.
- #
- # bufferRow - A {Number} indicating the buffer row
- #
- # Returns a {Fold} or null if none exists.
- largestFoldStartingAtBufferRow: (bufferRow) ->
- @foldsStartingAtBufferRow(bufferRow)[0]
-
- # Public: Given a buffer row, this returns all folds that start there.
- #
- # bufferRow - A {Number} indicating the buffer row
- #
- # Returns an {Array} of {Fold}s.
- foldsStartingAtBufferRow: (bufferRow) ->
- for marker in @findFoldMarkers(startRow: bufferRow)
- @foldForMarker(marker)
-
- # Given a screen row, this returns the largest fold that starts there.
- #
- # Largest is defined as the fold whose difference between its start and end points
- # are the greatest.
- #
- # screenRow - A {Number} indicating the screen row
- #
- # Returns a {Fold}.
- largestFoldStartingAtScreenRow: (screenRow) ->
- @largestFoldStartingAtBufferRow(@bufferRowForScreenRow(screenRow))
-
- # Given a buffer row, this returns the largest fold that includes it.
- #
- # Largest is defined as the fold whose difference between its start and end rows
- # is the greatest.
- #
- # bufferRow - A {Number} indicating the buffer row
- #
- # Returns a {Fold}.
- largestFoldContainingBufferRow: (bufferRow) ->
- @foldsContainingBufferRow(bufferRow)[0]
-
- # Returns the folds in the given row range (exclusive of end row) that are
- # not contained by any other folds.
- outermostFoldsInBufferRowRange: (startRow, endRow) ->
- folds = []
- lastFoldEndRow = -1
-
- for marker in @findFoldMarkers(intersectsRowRange: [startRow, endRow])
- range = marker.getRange()
- if range.start.row > lastFoldEndRow
- lastFoldEndRow = range.end.row
- if startRow <= range.start.row <= range.end.row < endRow
- folds.push(@foldForMarker(marker))
-
- folds
-
- # Returns all the folds that intersect the given row range.
- foldsIntersectingBufferRowRange: (startRow, endRow) ->
- @foldForMarker(marker) for marker in @findFoldMarkers(intersectsRowRange: [startRow, endRow])
-
- # Public: Given a buffer row, this returns folds that include it.
- #
- #
- # bufferRow - A {Number} indicating the buffer row
- #
- # Returns an {Array} of {Fold}s.
- foldsContainingBufferRow: (bufferRow) ->
- for marker in @findFoldMarkers(intersectsRow: bufferRow)
- @foldForMarker(marker)
-
- # Given a buffer row, this converts it into a screen row.
- #
- # bufferRow - A {Number} representing a buffer row
- #
- # Returns a {Number}.
- screenRowForBufferRow: (bufferRow) ->
- if @largeFileMode
- bufferRow
- else
- @rowMap.screenRowRangeForBufferRow(bufferRow)[0]
-
- lastScreenRowForBufferRow: (bufferRow) ->
- if @largeFileMode
- bufferRow
- else
- @rowMap.screenRowRangeForBufferRow(bufferRow)[1] - 1
-
- # Given a screen row, this converts it into a buffer row.
- #
- # screenRow - A {Number} representing a screen row
- #
- # Returns a {Number}.
- bufferRowForScreenRow: (screenRow) ->
- if @largeFileMode
- screenRow
- else
- @rowMap.bufferRowRangeForScreenRow(screenRow)[0]
-
- # Given a buffer range, this converts it into a screen position.
- #
- # bufferRange - The {Range} to convert
- #
- # Returns a {Range}.
- screenRangeForBufferRange: (bufferRange, options) ->
- bufferRange = Range.fromObject(bufferRange)
- start = @screenPositionForBufferPosition(bufferRange.start, options)
- end = @screenPositionForBufferPosition(bufferRange.end, options)
- new Range(start, end)
-
- # Given a screen range, this converts it into a buffer position.
- #
- # screenRange - The {Range} to convert
- #
- # Returns a {Range}.
- bufferRangeForScreenRange: (screenRange) ->
- screenRange = Range.fromObject(screenRange)
- start = @bufferPositionForScreenPosition(screenRange.start)
- end = @bufferPositionForScreenPosition(screenRange.end)
- new Range(start, end)
-
- # Gets the number of screen lines.
- #
- # Returns a {Number}.
- getLineCount: ->
- if @largeFileMode
- @tokenizedBuffer.getLineCount()
- else
- @screenLines.length
-
- # Gets the number of the last screen line.
- #
- # Returns a {Number}.
- getLastRow: ->
- @getLineCount() - 1
-
- # Gets the length of the longest screen line.
- #
- # Returns a {Number}.
- getMaxLineLength: ->
- @maxLineLength
-
- # Gets the row number of the longest screen line.
- #
- # Return a {}
- getLongestScreenRow: ->
- @longestScreenRow
-
- # Given a buffer position, this converts it into a screen position.
- #
- # bufferPosition - An object that represents a buffer position. It can be either
- # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
- # options - A hash of options with the following keys:
- # wrapBeyondNewlines:
- # wrapAtSoftNewlines:
- #
- # Returns a {Point}.
- screenPositionForBufferPosition: (bufferPosition, options) ->
- throw new Error("This TextEditor has been destroyed") if @isDestroyed()
-
- {row, column} = @buffer.clipPosition(bufferPosition)
- [startScreenRow, endScreenRow] = @rowMap.screenRowRangeForBufferRow(row)
- for screenRow in [startScreenRow...endScreenRow]
- screenLine = @tokenizedLineForScreenRow(screenRow)
-
- unless screenLine?
- throw new BufferToScreenConversionError "No screen line exists when converting buffer row to screen row",
- softWrapEnabled: @isSoftWrapped()
- foldCount: @findFoldMarkers().length
- lastBufferRow: @buffer.getLastRow()
- lastScreenRow: @getLastRow()
- bufferRow: row
- screenRow: screenRow
- displayBufferChangeCount: @changeCount
- tokenizedBufferChangeCount: @tokenizedBuffer.changeCount
- bufferChangeCount: @buffer.changeCount
-
- maxBufferColumn = screenLine.getMaxBufferColumn()
- if screenLine.isSoftWrapped() and column > maxBufferColumn
- continue
- else
- if column <= maxBufferColumn
- screenColumn = screenLine.screenColumnForBufferColumn(column)
- else
- screenColumn = Infinity
- break
-
- @clipScreenPosition([screenRow, screenColumn], options)
-
- # Given a buffer position, this converts it into a screen position.
- #
- # screenPosition - An object that represents a buffer position. It can be either
- # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
- # options - A hash of options with the following keys:
- # wrapBeyondNewlines:
- # wrapAtSoftNewlines:
- #
- # Returns a {Point}.
- bufferPositionForScreenPosition: (screenPosition, options) ->
- {row, column} = @clipScreenPosition(Point.fromObject(screenPosition), options)
- [bufferRow] = @rowMap.bufferRowRangeForScreenRow(row)
- new Point(bufferRow, @tokenizedLineForScreenRow(row).bufferColumnForScreenColumn(column))
-
- # Retrieves the grammar's token scopeDescriptor for a buffer position.
- #
- # bufferPosition - A {Point} in the {TextBuffer}
- #
- # Returns a {ScopeDescriptor}.
- scopeDescriptorForBufferPosition: (bufferPosition) ->
- @tokenizedBuffer.scopeDescriptorForPosition(bufferPosition)
-
- bufferRangeForScopeAtPosition: (selector, position) ->
- @tokenizedBuffer.bufferRangeForScopeAtPosition(selector, position)
-
- # Retrieves the grammar's token for a buffer position.
- #
- # bufferPosition - A {Point} in the {TextBuffer}.
- #
- # Returns a {Token}.
- tokenForBufferPosition: (bufferPosition) ->
- @tokenizedBuffer.tokenForPosition(bufferPosition)
-
- # Get the grammar for this buffer.
- #
- # Returns the current {Grammar} or the {NullGrammar}.
- getGrammar: ->
- @tokenizedBuffer.grammar
-
- # Sets the grammar for the buffer.
- #
- # grammar - Sets the new grammar rules
- setGrammar: (grammar) ->
- @tokenizedBuffer.setGrammar(grammar)
-
- # Reloads the current grammar.
- reloadGrammar: ->
- @tokenizedBuffer.reloadGrammar()
-
- # Given a position, this clips it to a real position.
- #
- # For example, if `position`'s row exceeds the row count of the buffer,
- # or if its column goes beyond a line's length, this "sanitizes" the value
- # to a real position.
- #
- # position - The {Point} to clip
- # options - A hash with the following values:
- # wrapBeyondNewlines: if `true`, continues wrapping past newlines
- # wrapAtSoftNewlines: if `true`, continues wrapping past soft newlines
- # skipSoftWrapIndentation: if `true`, skips soft wrap indentation without wrapping to the previous line
- # screenLine: if `true`, indicates that you're using a line number, not a row number
- #
- # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed.
- clipScreenPosition: (screenPosition, options={}) ->
- {wrapBeyondNewlines, wrapAtSoftNewlines, skipSoftWrapIndentation} = options
- {row, column} = Point.fromObject(screenPosition)
-
- if row < 0
- row = 0
- column = 0
- else if row > @getLastRow()
- row = @getLastRow()
- column = Infinity
- else if column < 0
- column = 0
-
- screenLine = @tokenizedLineForScreenRow(row)
- unless screenLine?
- error = new Error("Undefined screen line when clipping screen position")
- Error.captureStackTrace(error)
- error.metadata = {
- screenRow: row
- screenColumn: column
- maxScreenRow: @getLastRow()
- screenLinesDefined: @screenLines.map (sl) -> sl?
- displayBufferChangeCount: @changeCount
- tokenizedBufferChangeCount: @tokenizedBuffer.changeCount
- bufferChangeCount: @buffer.changeCount
- }
- throw error
-
- maxScreenColumn = screenLine.getMaxScreenColumn()
-
- if screenLine.isSoftWrapped() and column >= maxScreenColumn
- if wrapAtSoftNewlines
- row++
- column = @tokenizedLineForScreenRow(row).clipScreenColumn(0)
- else
- column = screenLine.clipScreenColumn(maxScreenColumn - 1)
- else if screenLine.isColumnInsideSoftWrapIndentation(column)
- if skipSoftWrapIndentation
- column = screenLine.clipScreenColumn(0)
- else
- row--
- column = @tokenizedLineForScreenRow(row).getMaxScreenColumn() - 1
- else if wrapBeyondNewlines and column > maxScreenColumn and row < @getLastRow()
- row++
- column = 0
- else
- column = screenLine.clipScreenColumn(column, options)
- new Point(row, column)
-
- # Clip the start and end of the given range to valid positions on screen.
- # See {::clipScreenPosition} for more information.
- #
- # * `range` The {Range} to clip.
- # * `options` (optional) See {::clipScreenPosition} `options`.
- # Returns a {Range}.
- clipScreenRange: (range, options) ->
- start = @clipScreenPosition(range.start, options)
- end = @clipScreenPosition(range.end, options)
-
- new Range(start, end)
-
- # Calculates a {Range} representing the start of the {TextBuffer} until the end.
- #
- # Returns a {Range}.
- rangeForAllLines: ->
- new Range([0, 0], @clipScreenPosition([Infinity, Infinity]))
-
- decorationForId: (id) ->
- @decorationsById[id]
-
- getDecorations: (propertyFilter) ->
- allDecorations = []
- for markerId, decorations of @decorationsByMarkerId
- allDecorations.push(decorations...) if decorations?
- if propertyFilter?
- allDecorations = allDecorations.filter (decoration) ->
- for key, value of propertyFilter
- return false unless decoration.properties[key] is value
- true
- allDecorations
-
- getLineDecorations: (propertyFilter) ->
- @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line')
-
- getLineNumberDecorations: (propertyFilter) ->
- @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line-number')
-
- getHighlightDecorations: (propertyFilter) ->
- @getDecorations(propertyFilter).filter (decoration) -> decoration.isType('highlight')
-
- getOverlayDecorations: (propertyFilter) ->
- result = []
- for id, decoration of @overlayDecorationsById
- result.push(decoration)
- if propertyFilter?
- result.filter (decoration) ->
- for key, value of propertyFilter
- return false unless decoration.properties[key] is value
- true
- else
- result
-
- decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
- decorationsByMarkerId = {}
- for marker in @findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow])
- if decorations = @decorationsByMarkerId[marker.id]
- decorationsByMarkerId[marker.id] = decorations
- decorationsByMarkerId
-
- decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
- decorationsState = {}
-
- for layerId of @decorationCountsByLayerId
- layer = @getMarkerLayer(layerId)
-
- for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid()
- screenRange = marker.getScreenRange()
- rangeIsReversed = marker.isReversed()
-
- if decorations = @decorationsByMarkerId[marker.id]
- for decoration in decorations
- decorationsState[decoration.id] = {
- properties: decoration.properties
- screenRange, rangeIsReversed
- }
-
- if layerDecorations = @layerDecorationsByMarkerLayerId[layerId]
- for layerDecoration in layerDecorations
- decorationsState["#{layerDecoration.id}-#{marker.id}"] = {
- properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties
- screenRange, rangeIsReversed
- }
-
- decorationsState
-
- decorateMarker: (marker, decorationParams) ->
- throw new Error("Cannot decorate a destroyed marker") if marker.isDestroyed()
- marker = @getMarkerLayer(marker.layer.id).getMarker(marker.id)
- decoration = new Decoration(marker, this, decorationParams)
- @decorationsByMarkerId[marker.id] ?= []
- @decorationsByMarkerId[marker.id].push(decoration)
- @overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay')
- @decorationsById[decoration.id] = decoration
- @observeDecoratedLayer(marker.layer)
- @scheduleUpdateDecorationsEvent()
- @emitter.emit 'did-add-decoration', decoration
- decoration
-
- decorateMarkerLayer: (markerLayer, decorationParams) ->
- decoration = new LayerDecoration(markerLayer, this, decorationParams)
- @layerDecorationsByMarkerLayerId[markerLayer.id] ?= []
- @layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration)
- @observeDecoratedLayer(markerLayer)
- @scheduleUpdateDecorationsEvent()
- decoration
-
- decorationsForMarkerId: (markerId) ->
- @decorationsByMarkerId[markerId]
-
- # Retrieves a {TextEditorMarker} based on its id.
- #
- # id - A {Number} representing a marker id
- #
- # Returns the {TextEditorMarker} (if it exists).
- getMarker: (id) ->
- @defaultMarkerLayer.getMarker(id)
-
- # Retrieves the active markers in the buffer.
- #
- # Returns an {Array} of existing {TextEditorMarker}s.
- getMarkers: ->
- @defaultMarkerLayer.getMarkers()
-
- getMarkerCount: ->
- @buffer.getMarkerCount()
-
- # Public: Constructs a new marker at the given screen range.
- #
- # range - The marker {Range} (representing the distance between the head and tail)
- # options - Options to pass to the {TextEditorMarker} constructor
- #
- # Returns a {Number} representing the new marker's ID.
- markScreenRange: (screenRange, options) ->
- @defaultMarkerLayer.markScreenRange(screenRange, options)
-
- # Public: Constructs a new marker at the given buffer range.
- #
- # range - The marker {Range} (representing the distance between the head and tail)
- # options - Options to pass to the {TextEditorMarker} constructor
- #
- # Returns a {Number} representing the new marker's ID.
- markBufferRange: (bufferRange, options) ->
- @defaultMarkerLayer.markBufferRange(bufferRange, options)
-
- # Public: Constructs a new marker at the given screen position.
- #
- # range - The marker {Range} (representing the distance between the head and tail)
- # options - Options to pass to the {TextEditorMarker} constructor
- #
- # Returns a {Number} representing the new marker's ID.
- markScreenPosition: (screenPosition, options) ->
- @defaultMarkerLayer.markScreenPosition(screenPosition, options)
-
- # Public: Constructs a new marker at the given buffer position.
- #
- # range - The marker {Range} (representing the distance between the head and tail)
- # options - Options to pass to the {TextEditorMarker} constructor
- #
- # Returns a {Number} representing the new marker's ID.
- markBufferPosition: (bufferPosition, options) ->
- @defaultMarkerLayer.markBufferPosition(bufferPosition, options)
-
- # Finds the first marker satisfying the given attributes
- #
- # Refer to {DisplayBuffer::findMarkers} for details.
- #
- # Returns a {TextEditorMarker} or null
- findMarker: (params) ->
- @defaultMarkerLayer.findMarkers(params)[0]
-
- # Public: Find all markers satisfying a set of parameters.
- #
- # params - An {Object} containing parameters that all returned markers must
- # satisfy. Unreserved keys will be compared against the markers' custom
- # properties. There are also the following reserved keys with special
- # meaning for the query:
- # :startBufferRow - A {Number}. Only returns markers starting at this row in
- # buffer coordinates.
- # :endBufferRow - A {Number}. Only returns markers ending at this row in
- # buffer coordinates.
- # :containsBufferRange - A {Range} or range-compatible {Array}. Only returns
- # markers containing this range in buffer coordinates.
- # :containsBufferPosition - A {Point} or point-compatible {Array}. Only
- # returns markers containing this position in buffer coordinates.
- # :containedInBufferRange - A {Range} or range-compatible {Array}. Only
- # returns markers contained within this range.
- #
- # Returns an {Array} of {TextEditorMarker}s
- findMarkers: (params) ->
- @defaultMarkerLayer.findMarkers(params)
-
- addMarkerLayer: (options) ->
- bufferLayer = @buffer.addMarkerLayer(options)
- @getMarkerLayer(bufferLayer.id)
-
- getMarkerLayer: (id) ->
- if layer = @customMarkerLayersById[id]
- layer
- else if bufferLayer = @buffer.getMarkerLayer(id)
- @customMarkerLayersById[id] = new TextEditorMarkerLayer(this, bufferLayer)
-
- getDefaultMarkerLayer: -> @defaultMarkerLayer
-
- findFoldMarker: (params) ->
- @findFoldMarkers(params)[0]
-
- findFoldMarkers: (params) ->
- @foldsMarkerLayer.findMarkers(params)
-
- refreshMarkerScreenPositions: ->
- @defaultMarkerLayer.refreshMarkerScreenPositions()
- layer.refreshMarkerScreenPositions() for id, layer of @customMarkerLayersById
- return
-
- destroyed: ->
- @defaultMarkerLayer.destroy()
- @foldsMarkerLayer.destroy()
- @scopedConfigSubscriptions.dispose()
- @disposables.dispose()
- @tokenizedBuffer.destroy()
-
- logLines: (start=0, end=@getLastRow()) ->
- for row in [start..end]
- line = @tokenizedLineForScreenRow(row).text
- console.log row, @bufferRowForScreenRow(row), line, line.length
- return
-
- getRootScopeDescriptor: ->
- @tokenizedBuffer.rootScopeDescriptor
-
- handleTokenizedBufferChange: (tokenizedBufferChange) =>
- @changeCount = @tokenizedBuffer.changeCount
- {start, end, delta, bufferChange} = tokenizedBufferChange
- @updateScreenLines(start, end + 1, delta, refreshMarkers: false)
-
- updateScreenLines: (startBufferRow, endBufferRow, bufferDelta=0, options={}) ->
- return if @largeFileMode
- return if @isDestroyed()
-
- startBufferRow = @rowMap.bufferRowRangeForBufferRow(startBufferRow)[0]
- endBufferRow = @rowMap.bufferRowRangeForBufferRow(endBufferRow - 1)[1]
- startScreenRow = @rowMap.screenRowRangeForBufferRow(startBufferRow)[0]
- endScreenRow = @rowMap.screenRowRangeForBufferRow(endBufferRow - 1)[1]
- {screenLines, regions} = @buildScreenLines(startBufferRow, endBufferRow + bufferDelta)
- screenDelta = screenLines.length - (endScreenRow - startScreenRow)
-
- _.spliceWithArray(@screenLines, startScreenRow, endScreenRow - startScreenRow, screenLines, 10000)
-
- @checkScreenLinesInvariant()
-
- @rowMap.spliceRegions(startBufferRow, endBufferRow - startBufferRow, regions)
- @findMaxLineLength(startScreenRow, endScreenRow, screenLines, screenDelta)
-
- return if options.suppressChangeEvent
-
- changeEvent =
- start: startScreenRow
- end: endScreenRow - 1
- screenDelta: screenDelta
- bufferDelta: bufferDelta
-
- @emitDidChange(changeEvent, options.refreshMarkers)
-
- buildScreenLines: (startBufferRow, endBufferRow) ->
- screenLines = []
- regions = []
- rectangularRegion = null
-
- foldsByStartRow = {}
- for fold in @outermostFoldsInBufferRowRange(startBufferRow, endBufferRow)
- foldsByStartRow[fold.getStartRow()] = fold
-
- bufferRow = startBufferRow
- while bufferRow < endBufferRow
- tokenizedLine = @tokenizedBuffer.tokenizedLineForRow(bufferRow)
-
- if fold = foldsByStartRow[bufferRow]
- foldLine = tokenizedLine.copy()
- foldLine.fold = fold
- screenLines.push(foldLine)
-
- if rectangularRegion?
- regions.push(rectangularRegion)
- rectangularRegion = null
-
- foldedRowCount = fold.getBufferRowCount()
- regions.push(bufferRows: foldedRowCount, screenRows: 1)
- bufferRow += foldedRowCount
- else
- softWraps = 0
- if @isSoftWrapped()
- while wrapScreenColumn = tokenizedLine.findWrapColumn(@getSoftWrapColumnForTokenizedLine(tokenizedLine))
- [wrappedLine, tokenizedLine] = tokenizedLine.softWrapAt(
- wrapScreenColumn,
- @configSettings.softWrapHangingIndent
- )
- break if wrappedLine.hasOnlySoftWrapIndentation()
- screenLines.push(wrappedLine)
- softWraps++
- screenLines.push(tokenizedLine)
-
- if softWraps > 0
- if rectangularRegion?
- regions.push(rectangularRegion)
- rectangularRegion = null
- regions.push(bufferRows: 1, screenRows: softWraps + 1)
- else
- rectangularRegion ?= {bufferRows: 0, screenRows: 0}
- rectangularRegion.bufferRows++
- rectangularRegion.screenRows++
-
- bufferRow++
-
- if rectangularRegion?
- regions.push(rectangularRegion)
-
- {screenLines, regions}
-
- findMaxLineLength: (startScreenRow, endScreenRow, newScreenLines, screenDelta) ->
- oldMaxLineLength = @maxLineLength
-
- if startScreenRow <= @longestScreenRow < endScreenRow
- @longestScreenRow = 0
- @maxLineLength = 0
- maxLengthCandidatesStartRow = 0
- maxLengthCandidates = @screenLines
- else
- @longestScreenRow += screenDelta if endScreenRow <= @longestScreenRow
- maxLengthCandidatesStartRow = startScreenRow
- maxLengthCandidates = newScreenLines
-
- for screenLine, i in maxLengthCandidates
- screenRow = maxLengthCandidatesStartRow + i
- length = screenLine.text.length
- if length > @maxLineLength
- @longestScreenRow = screenRow
- @maxLineLength = length
-
- didCreateDefaultLayerMarker: (textBufferMarker) =>
- if marker = @getMarker(textBufferMarker.id)
- # The marker might have been removed in some other handler called before
- # this one. Only emit when the marker still exists.
- @emitter.emit 'did-create-marker', marker
-
- scheduleUpdateDecorationsEvent: ->
- if @updatedSynchronously
- @emitter.emit 'did-update-decorations'
- return
-
- unless @didUpdateDecorationsEventScheduled
- @didUpdateDecorationsEventScheduled = true
- process.nextTick =>
- @didUpdateDecorationsEventScheduled = false
- @emitter.emit 'did-update-decorations'
-
- decorateFold: (fold) ->
- @decorateMarker(fold.marker, type: 'line-number', class: 'folded')
-
- foldForMarker: (marker) ->
- @foldsByMarkerId[marker.id]
-
- decorationDidChangeType: (decoration) ->
- if decoration.isType('overlay')
- @overlayDecorationsById[decoration.id] = decoration
- else
- delete @overlayDecorationsById[decoration.id]
-
- didDestroyDecoration: (decoration) ->
- {marker} = decoration
- return unless decorations = @decorationsByMarkerId[marker.id]
- index = decorations.indexOf(decoration)
-
- if index > -1
- decorations.splice(index, 1)
- delete @decorationsById[decoration.id]
- @emitter.emit 'did-remove-decoration', decoration
- delete @decorationsByMarkerId[marker.id] if decorations.length is 0
- delete @overlayDecorationsById[decoration.id]
- @unobserveDecoratedLayer(marker.layer)
- @scheduleUpdateDecorationsEvent()
-
- didDestroyLayerDecoration: (decoration) ->
- {markerLayer} = decoration
- return unless decorations = @layerDecorationsByMarkerLayerId[markerLayer.id]
- index = decorations.indexOf(decoration)
-
- if index > -1
- decorations.splice(index, 1)
- delete @layerDecorationsByMarkerLayerId[markerLayer.id] if decorations.length is 0
- @unobserveDecoratedLayer(markerLayer)
- @scheduleUpdateDecorationsEvent()
-
- observeDecoratedLayer: (layer) ->
- @decorationCountsByLayerId[layer.id] ?= 0
- if ++@decorationCountsByLayerId[layer.id] is 1
- @layerUpdateDisposablesByLayerId[layer.id] = layer.onDidUpdate(@scheduleUpdateDecorationsEvent.bind(this))
-
- unobserveDecoratedLayer: (layer) ->
- if --@decorationCountsByLayerId[layer.id] is 0
- @layerUpdateDisposablesByLayerId[layer.id].dispose()
- delete @decorationCountsByLayerId[layer.id]
- delete @layerUpdateDisposablesByLayerId[layer.id]
-
- checkScreenLinesInvariant: ->
- return if @isSoftWrapped()
- return if _.size(@foldsByMarkerId) > 0
-
- screenLinesCount = @screenLines.length
- tokenizedLinesCount = @tokenizedBuffer.getLineCount()
- bufferLinesCount = @buffer.getLineCount()
-
- @assert screenLinesCount is tokenizedLinesCount, "Display buffer line count out of sync with tokenized buffer", (error) ->
- error.metadata = {screenLinesCount, tokenizedLinesCount, bufferLinesCount}
-
- @assert screenLinesCount is bufferLinesCount, "Display buffer line count out of sync with buffer", (error) ->
- error.metadata = {screenLinesCount, tokenizedLinesCount, bufferLinesCount}
diff --git a/src/environment-helpers.js b/src/environment-helpers.js
new file mode 100644
index 000000000..2d1bd5b60
--- /dev/null
+++ b/src/environment-helpers.js
@@ -0,0 +1,113 @@
+'use babel'
+
+import {spawnSync} from 'child_process'
+import os from 'os'
+
+// Gets a dump of the user's configured shell environment.
+//
+// Returns the output of the `env` command or `undefined` if there was an error.
+function getRawShellEnv () {
+ let shell = getUserShell()
+
+ // The `-ilc` set of options was tested to work with the OS X v10.11
+ // default-installed versions of bash, zsh, sh, and ksh. It *does not*
+ // work with csh or tcsh.
+ let results = spawnSync(shell, ['-ilc', 'env'], {encoding: 'utf8'})
+ if (results.error || !results.stdout || results.stdout.length <= 0) {
+ return
+ }
+
+ return results.stdout
+}
+
+function getUserShell () {
+ if (process.env.SHELL) {
+ return process.env.SHELL
+ }
+
+ return '/bin/bash'
+}
+
+// Gets the user's configured shell environment.
+//
+// Returns a copy of the user's shell enviroment.
+function getFromShell () {
+ let shellEnvText = getRawShellEnv()
+ if (!shellEnvText) {
+ return
+ }
+
+ let env = {}
+
+ for (let line of shellEnvText.split(os.EOL)) {
+ if (line.includes('=')) {
+ let components = line.split('=')
+ if (components.length === 2) {
+ env[components[0]] = components[1]
+ } else {
+ let k = components.shift()
+ let v = components.join('=')
+ env[k] = v
+ }
+ }
+ }
+
+ return env
+}
+
+function needsPatching (options = { platform: process.platform, env: process.env }) {
+ if (options.platform === 'darwin' && !options.env.PWD) {
+ let shell = getUserShell()
+ if (shell.endsWith('csh') || shell.endsWith('tcsh') || shell.endsWith('fish')) {
+ return false
+ }
+ return true
+ }
+
+ return false
+}
+
+// Fix for #11302 because `process.env` on Windows is a magic object that offers case-insensitive
+// environment variable matching. By always cloning to `process.env` we prevent breaking the
+// underlying functionality.
+function clone (to, from) {
+ for (var key in to) {
+ delete to[key]
+ }
+
+ Object.assign(to, from)
+}
+
+function normalize (options = {}) {
+ if (options && options.env) {
+ clone(process.env, options.env)
+ }
+
+ if (!options.env) {
+ options.env = process.env
+ }
+
+ if (!options.platform) {
+ options.platform = process.platform
+ }
+
+ if (needsPatching(options)) {
+ // Patch the `process.env` on startup to fix the problem first documented
+ // in #4126. Retain the original in case someone needs it.
+ let shellEnv = getFromShell()
+ if (shellEnv && shellEnv.PATH) {
+ process._originalEnv = Object.assign({}, process.env)
+ clone(process.env, shellEnv)
+ }
+ }
+}
+
+function replace (env) {
+ if (!env || !env.PATH) {
+ return
+ }
+
+ clone(process.env, env)
+}
+
+export default { getFromShell, needsPatching, normalize, replace }
diff --git a/src/fold.coffee b/src/fold.coffee
deleted file mode 100644
index 051be9f4c..000000000
--- a/src/fold.coffee
+++ /dev/null
@@ -1,83 +0,0 @@
-{Point, Range} = require 'text-buffer'
-
-# Represents a fold that collapses multiple buffer lines into a single
-# line on the screen.
-#
-# Their creation is managed by the {DisplayBuffer}.
-module.exports =
-class Fold
- id: null
- displayBuffer: null
- marker: null
-
- constructor: (@displayBuffer, @marker) ->
- @id = @marker.id
- @displayBuffer.foldsByMarkerId[@marker.id] = this
- @marker.onDidDestroy => @destroyed()
- @marker.onDidChange ({isValid}) => @destroy() unless isValid
-
- # Returns whether this fold is contained within another fold
- isInsideLargerFold: ->
- largestContainingFoldMarker = @displayBuffer.findFoldMarker(containsRange: @getBufferRange())
- largestContainingFoldMarker and
- not largestContainingFoldMarker.getRange().isEqual(@getBufferRange())
-
- # Destroys this fold
- destroy: ->
- @marker.destroy()
-
- # Returns the fold's {Range} in buffer coordinates
- #
- # includeNewline - A {Boolean} which, if `true`, includes the trailing newline
- #
- # Returns a {Range}.
- getBufferRange: ({includeNewline}={}) ->
- range = @marker.getRange()
-
- if range.end.row > range.start.row and nextFold = @displayBuffer.largestFoldStartingAtBufferRow(range.end.row)
- nextRange = nextFold.getBufferRange()
- range = new Range(range.start, nextRange.end)
-
- if includeNewline
- range = range.copy()
- range.end.row++
- range.end.column = 0
- range
-
- getBufferRowRange: ->
- {start, end} = @getBufferRange()
- [start.row, end.row]
-
- # Returns the fold's start row as a {Number}.
- getStartRow: ->
- @getBufferRange().start.row
-
- # Returns the fold's end row as a {Number}.
- getEndRow: ->
- @getBufferRange().end.row
-
- # Returns a {String} representation of the fold.
- inspect: ->
- "Fold(#{@getStartRow()}, #{@getEndRow()})"
-
- # Retrieves the number of buffer rows spanned by the fold.
- #
- # Returns a {Number}.
- getBufferRowCount: ->
- @getEndRow() - @getStartRow() + 1
-
- # Identifies if a fold is nested within a fold.
- #
- # fold - A {Fold} to check
- #
- # Returns a {Boolean}.
- isContainedByFold: (fold) ->
- @isContainedByRange(fold.getBufferRange())
-
- updateDisplayBuffer: ->
- unless @isInsideLargerFold()
- @displayBuffer.updateScreenLines(@getStartRow(), @getEndRow() + 1, 0, updateMarkers: true)
-
- destroyed: ->
- delete @displayBuffer.foldsByMarkerId[@marker.id]
- @updateDisplayBuffer()
diff --git a/src/git-repository-async.js b/src/git-repository-async.js
index 38792e02e..66b73ba77 100644
--- a/src/git-repository-async.js
+++ b/src/git-repository-async.js
@@ -1,20 +1,14 @@
'use babel'
-import fs from 'fs-plus'
-import path from 'path'
-import Git from 'nodegit'
-import {Emitter, CompositeDisposable, Disposable} from 'event-kit'
-
-const modifiedStatusFlags = Git.Status.STATUS.WT_MODIFIED | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.WT_TYPECHANGE | Git.Status.STATUS.INDEX_TYPECHANGE
-const newStatusFlags = Git.Status.STATUS.WT_NEW | Git.Status.STATUS.INDEX_NEW
-const deletedStatusFlags = Git.Status.STATUS.WT_DELETED | Git.Status.STATUS.INDEX_DELETED
-const indexStatusFlags = Git.Status.STATUS.INDEX_NEW | Git.Status.STATUS.INDEX_MODIFIED | Git.Status.STATUS.INDEX_DELETED | Git.Status.STATUS.INDEX_RENAMED | Git.Status.STATUS.INDEX_TYPECHANGE
-const ignoredStatusFlags = 1 << 14 // TODO: compose this from libgit2 constants
-const submoduleMode = 57344 // TODO: compose this from libgit2 constants
-
-// Just using this for _.isEqual and _.object, we should impl our own here
-import _ from 'underscore-plus'
+import {Repository} from 'ohnogit'
+import {CompositeDisposable, Disposable} from 'event-kit'
+// For the most part, this class behaves the same as `GitRepository`, with a few
+// notable differences:
+// * Errors are generally propagated out to the caller instead of being
+// swallowed within `GitRepositoryAsync`.
+// * Methods accepting a path shouldn't be given a null path, unless it is
+// specifically allowed as noted in the method's documentation.
export default class GitRepositoryAsync {
static open (path, options = {}) {
// QUESTION: Should this wrap Git.Repository and reject with a nicer message?
@@ -22,32 +16,19 @@ export default class GitRepositoryAsync {
}
static get Git () {
- return Git
+ return Repository.Git
}
// The name of the error thrown when an action is attempted on a destroyed
// repository.
static get DestroyedErrorName () {
- return 'GitRepositoryAsync.destroyed'
+ return Repository.DestroyedErrorName
}
constructor (_path, options = {}) {
- Git.enableThreadSafety()
+ this.repo = Repository.open(_path, options)
- this.emitter = new Emitter()
this.subscriptions = new CompositeDisposable()
- this.pathStatusCache = {}
-
- // NB: These needs to happen before the following .openRepository call.
- this.openedPath = _path
- this._openExactPath = options.openExactPath || false
-
- this.repoPromise = this.openRepository()
- this.isCaseInsensitive = fs.isCaseInsensitive()
- this.upstream = {}
- this.submodules = {}
-
- this._refreshingPromise = Promise.resolve()
let {refreshOnWindowFocus = true} = options
if (refreshOnWindowFocus) {
@@ -64,22 +45,26 @@ export default class GitRepositoryAsync {
}
}
+ // This exists to provide backwards compatibility.
+ get _refreshingPromise () {
+ return this.repo._refreshingPromise
+ }
+
+ get openedPath () {
+ return this.repo.openedPath
+ }
+
// Public: Destroy this {GitRepositoryAsync} object.
//
// This destroys any tasks and subscriptions and releases the underlying
// libgit2 repository handle. This method is idempotent.
destroy () {
- if (this.emitter) {
- this.emitter.emit('did-destroy')
- this.emitter.dispose()
- this.emitter = null
- }
+ this.repo.destroy()
+
if (this.subscriptions) {
this.subscriptions.dispose()
this.subscriptions = null
}
-
- this.repoPromise = null
}
// Event subscription
@@ -92,7 +77,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidDestroy (callback) {
- return this.emitter.on('did-destroy', callback)
+ return this.repo.onDidDestroy(callback)
}
// Public: Invoke the given callback when a specific file's status has
@@ -107,7 +92,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeStatus (callback) {
- return this.emitter.on('did-change-status', callback)
+ return this.repo.onDidChangeStatus(callback)
}
// Public: Invoke the given callback when a multiple files' statuses have
@@ -119,7 +104,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeStatuses (callback) {
- return this.emitter.on('did-change-statuses', callback)
+ return this.repo.onDidChangeStatuses(callback)
}
// Repository details
@@ -136,13 +121,13 @@ export default class GitRepositoryAsync {
// Public: Returns a {Promise} which resolves to the {String} path of the
// repository.
getPath () {
- return this.getRepo().then(repo => repo.path().replace(/\/$/, ''))
+ return this.repo.getPath()
}
// Public: Returns a {Promise} which resolves to the {String} working
// directory path of the repository.
- getWorkingDirectory () {
- return this.getRepo().then(repo => repo.workdir())
+ getWorkingDirectory (_path) {
+ return this.repo.getWorkingDirectory()
}
// Public: Returns a {Promise} that resolves to true if at the root, false if
@@ -151,9 +136,8 @@ export default class GitRepositoryAsync {
if (!this.project) return Promise.resolve(false)
if (!this.projectAtRoot) {
- this.projectAtRoot = this.getRepo()
- .then(repo => this.project.relativize(repo.workdir()))
- .then(relativePath => relativePath === '')
+ this.projectAtRoot = this.getWorkingDirectory()
+ .then(wd => this.project.relativize(wd) === '')
}
return this.projectAtRoot
@@ -165,8 +149,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Promise} which resolves to the relative {String} path.
relativizeToWorkingDirectory (_path) {
- return this.getRepo()
- .then(repo => this.relativize(_path, repo.workdir()))
+ return this.repo.relativizeToWorkingDirectory(_path)
}
// Public: Makes a path relative to the repository's working directory.
@@ -176,73 +159,13 @@ export default class GitRepositoryAsync {
//
// Returns the relative {String} path.
relativize (_path, workingDirectory) {
- // The original implementation also handled null workingDirectory as it
- // pulled it from a sync function that could return null. We require it
- // to be passed here.
- let openedWorkingDirectory
- if (!_path || !workingDirectory) {
- return _path
- }
-
- // If the opened directory and the workdir differ, this is a symlinked repo
- // root, so we have to do all the checks below twice--once against the realpath
- // and one against the opened path
- const opened = this.openedPath.replace(/\/\.git$/, '')
- if (path.relative(opened, workingDirectory) !== '') {
- openedWorkingDirectory = opened
- }
-
- if (process.platform === 'win32') {
- _path = _path.replace(/\\/g, '/')
- } else {
- if (_path[0] !== '/') {
- return _path
- }
- }
-
- workingDirectory = workingDirectory.replace(/\/$/, '')
-
- if (this.isCaseInsensitive) {
- _path = _path.toLowerCase()
- workingDirectory = workingDirectory.toLowerCase()
- }
-
- // Depending on where the paths come from, they may have a '/private/'
- // prefix. Standardize by stripping that out.
- _path = _path.replace(/^\/private\//, '/')
- workingDirectory = workingDirectory.replace(/^\/private\//, '/')
-
- const originalPath = _path
- if (_path.indexOf(workingDirectory) === 0) {
- return originalPath.substring(workingDirectory.length + 1)
- } else if (_path === workingDirectory) {
- return ''
- }
-
- if (openedWorkingDirectory) {
- if (this.isCaseInsensitive) {
- openedWorkingDirectory = openedWorkingDirectory.toLowerCase()
- }
- openedWorkingDirectory = openedWorkingDirectory.replace(/\/$/, '')
- openedWorkingDirectory = openedWorkingDirectory.replace(/^\/private\//, '/')
-
- if (_path.indexOf(openedWorkingDirectory) === 0) {
- return originalPath.substring(openedWorkingDirectory.length + 1)
- } else if (_path === openedWorkingDirectory) {
- return ''
- }
- }
-
- return _path
+ return this.repo.relativize(_path, workingDirectory)
}
// Public: Returns a {Promise} which resolves to whether the given branch
// exists.
hasBranch (branch) {
- return this.getRepo()
- .then(repo => repo.getBranch(branch))
- .then(branch => branch != null)
- .catch(_ => false)
+ return this.repo.hasBranch(branch)
}
// Public: Retrieves a shortened version of the HEAD reference value.
@@ -256,9 +179,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Promise} which resolves to a {String}.
getShortHead (_path) {
- return this.getRepo(_path)
- .then(repo => repo.getCurrentBranch())
- .then(branch => branch.shorthand())
+ return this.repo.getShortHead(_path)
}
// Public: Is the given path a submodule in the repository?
@@ -268,15 +189,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} that resolves true if the given path is a submodule in
// the repository.
isSubmodule (_path) {
- return this.getRepo()
- .then(repo => repo.openIndex())
- .then(index => Promise.all([index, this.relativizeToWorkingDirectory(_path)]))
- .then(([index, relativePath]) => {
- const entry = index.getByPath(relativePath)
- if (!entry) return false
-
- return entry.mode === submoduleMode
- })
+ return this.repo.isSubmodule(_path)
}
// Public: Returns the number of commits behind the current branch is from the
@@ -290,16 +203,7 @@ export default class GitRepositoryAsync {
// * `ahead` The {Number} of commits ahead.
// * `behind` The {Number} of commits behind.
getAheadBehindCount (reference, _path) {
- return this.getRepo(_path)
- .then(repo => Promise.all([repo, repo.getBranch(reference)]))
- .then(([repo, local]) => {
- const upstream = Git.Branch.upstream(local)
- return Promise.all([repo, local, upstream])
- })
- .then(([repo, local, upstream]) => {
- return Git.Graph.aheadBehind(repo, local.target(), upstream.target())
- })
- .catch(_ => ({ahead: 0, behind: 0}))
+ return this.repo.getAheadBehindCount(reference, _path)
}
// Public: Get the cached ahead/behind commit counts for the current branch's
@@ -312,15 +216,7 @@ export default class GitRepositoryAsync {
// * `ahead` The {Number} of commits ahead.
// * `behind` The {Number} of commits behind.
getCachedUpstreamAheadBehindCount (_path) {
- return this.relativizeToWorkingDirectory(_path)
- .then(relativePath => this._submoduleForPath(_path))
- .then(submodule => {
- if (submodule) {
- return submodule.getCachedUpstreamAheadBehindCount(_path)
- } else {
- return this.upstream
- }
- })
+ return this.repo.getCachedUpstreamAheadBehindCount(_path)
}
// Public: Returns the git configuration value specified by the key.
@@ -331,10 +227,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to the {String} git configuration value
// specified by the key.
getConfigValue (key, _path) {
- return this.getRepo(_path)
- .then(repo => repo.configSnapshot())
- .then(config => config.getStringBuf(key))
- .catch(_ => null)
+ return this.repo.getConfigValue(key, _path)
}
// Public: Get the URL for the 'origin' remote.
@@ -345,7 +238,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to the {String} origin url of the
// repository.
getOriginURL (_path) {
- return this.getConfigValue('remote.origin.url', _path)
+ return this.repo.getOriginURL(_path)
}
// Public: Returns the upstream branch for the current HEAD, or null if there
@@ -357,9 +250,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {String} branch name such as
// `refs/remotes/origin/master`.
getUpstreamBranch (_path) {
- return this.getRepo(_path)
- .then(repo => repo.getCurrentBranch())
- .then(branch => Git.Branch.upstream(branch))
+ return this.repo.getUpstreamBranch(_path)
}
// Public: Gets all the local and remote references.
@@ -372,23 +263,7 @@ export default class GitRepositoryAsync {
// * `remotes` An {Array} of remote reference names.
// * `tags` An {Array} of tag reference names.
getReferences (_path) {
- return this.getRepo(_path)
- .then(repo => repo.getReferences(Git.Reference.TYPE.LISTALL))
- .then(refs => {
- const heads = []
- const remotes = []
- const tags = []
- for (const ref of refs) {
- if (ref.isTag()) {
- tags.push(ref.name())
- } else if (ref.isRemote()) {
- remotes.push(ref.name())
- } else if (ref.isBranch()) {
- heads.push(ref.name())
- }
- }
- return {heads, remotes, tags}
- })
+ return this.repo.getReferences(_path)
}
// Public: Get the SHA for the given reference.
@@ -400,9 +275,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to the current {String} SHA for the
// given reference.
getReferenceTarget (reference, _path) {
- return this.getRepo(_path)
- .then(repo => Git.Reference.nameToId(repo, reference))
- .then(oid => oid.tostrS())
+ return this.repo.getReferenceTarget(reference, _path)
}
// Reading Status
@@ -415,9 +288,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {Boolean} that's true if the `path`
// is modified.
isPathModified (_path) {
- return this.relativizeToWorkingDirectory(_path)
- .then(relativePath => this._getStatus([relativePath]))
- .then(statuses => statuses.some(status => status.isModified()))
+ return this.repo.isPathModified(_path)
}
// Public: Resolves true if the given path is new.
@@ -427,9 +298,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {Boolean} that's true if the `path`
// is new.
isPathNew (_path) {
- return this.relativizeToWorkingDirectory(_path)
- .then(relativePath => this._getStatus([relativePath]))
- .then(statuses => statuses.some(status => status.isNew()))
+ return this.repo.isPathNew(_path)
}
// Public: Is the given path ignored?
@@ -439,12 +308,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {Boolean} that's true if the `path`
// is ignored.
isPathIgnored (_path) {
- return this.getRepo()
- .then(repo => {
- const relativePath = this.relativize(_path, repo.workdir())
- return Git.Ignore.pathIsIgnored(repo, relativePath)
- })
- .then(ignored => Boolean(ignored))
+ return this.repo.isPathIgnored(_path)
}
// Get the status of a directory in the repository's working directory.
@@ -455,15 +319,7 @@ export default class GitRepositoryAsync {
// value can be passed to {::isStatusModified} or {::isStatusNew} to get more
// information.
getDirectoryStatus (directoryPath) {
- return this.relativizeToWorkingDirectory(directoryPath)
- .then(relativePath => this._getStatus([relativePath]))
- .then(statuses => {
- return Promise.all(statuses.map(s => s.statusBit())).then(bits => {
- return bits
- .filter(b => b > 0)
- .reduce((status, bit) => status | bit, 0)
- })
- })
+ return this.repo.getDirectoryStatus(directoryPath)
}
// Refresh the status bit for the given path.
@@ -476,27 +332,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a {Number} which is the refreshed
// status bit for the path.
refreshStatusForPath (_path) {
- let relativePath
- return this.getRepo()
- .then(repo => {
- relativePath = this.relativize(_path, repo.workdir())
- return this._getStatus([relativePath])
- })
- .then(statuses => {
- const cachedStatus = this.pathStatusCache[relativePath] || 0
- const status = statuses[0] ? statuses[0].statusBit() : Git.Status.STATUS.CURRENT
- if (status !== cachedStatus) {
- if (status === Git.Status.STATUS.CURRENT) {
- delete this.pathStatusCache[relativePath]
- } else {
- this.pathStatusCache[relativePath] = status
- }
-
- this.emitter.emit('did-change-status', {path: _path, pathStatus: status})
- }
-
- return status
- })
+ return this.repo.refreshStatusForPath(_path)
}
// Returns a Promise that resolves to the status bit of a given path if it has
@@ -512,8 +348,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} which resolves to a status {Number} or null if the
// path is not in the cache.
getCachedPathStatus (_path) {
- return this.relativizeToWorkingDirectory(_path)
- .then(relativePath => this.pathStatusCache[relativePath])
+ return this.repo.getCachedPathStatus(_path)
}
// Public: Get the cached statuses for the repository.
@@ -521,7 +356,7 @@ export default class GitRepositoryAsync {
// Returns an {Object} of {Number} statuses, keyed by {String} working
// directory-relative file names.
getCachedPathStatuses () {
- return this.pathStatusCache
+ return this.repo.pathStatusCache
}
// Public: Returns true if the given status indicates modification.
@@ -530,7 +365,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Boolean} that's true if the `statusBit` indicates modification.
isStatusModified (statusBit) {
- return (statusBit & modifiedStatusFlags) > 0
+ return this.repo.isStatusModified(statusBit)
}
// Public: Returns true if the given status indicates a new path.
@@ -539,7 +374,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Boolean} that's true if the `statusBit` indicates a new path.
isStatusNew (statusBit) {
- return (statusBit & newStatusFlags) > 0
+ return this.repo.isStatusNew(statusBit)
}
// Public: Returns true if the given status indicates the path is staged.
@@ -549,7 +384,7 @@ export default class GitRepositoryAsync {
// Returns a {Boolean} that's true if the `statusBit` indicates the path is
// staged.
isStatusStaged (statusBit) {
- return (statusBit & indexStatusFlags) > 0
+ return this.repo.isStatusStaged(statusBit)
}
// Public: Returns true if the given status indicates the path is ignored.
@@ -559,7 +394,7 @@ export default class GitRepositoryAsync {
// Returns a {Boolean} that's true if the `statusBit` indicates the path is
// ignored.
isStatusIgnored (statusBit) {
- return (statusBit & ignoredStatusFlags) > 0
+ return this.repo.isStatusIgnored(statusBit)
}
// Public: Returns true if the given status indicates the path is deleted.
@@ -569,7 +404,7 @@ export default class GitRepositoryAsync {
// Returns a {Boolean} that's true if the `statusBit` indicates the path is
// deleted.
isStatusDeleted (statusBit) {
- return (statusBit & deletedStatusFlags) > 0
+ return this.repo.isStatusDeleted(statusBit)
}
// Retrieving Diffs
@@ -585,27 +420,7 @@ export default class GitRepositoryAsync {
// * `added` The {Number} of added lines.
// * `deleted` The {Number} of deleted lines.
getDiffStats (_path) {
- return this.getRepo()
- .then(repo => Promise.all([repo, repo.getHeadCommit()]))
- .then(([repo, headCommit]) => Promise.all([repo, headCommit.getTree()]))
- .then(([repo, tree]) => {
- const options = new Git.DiffOptions()
- options.pathspec = this.relativize(_path, repo.workdir())
- return Git.Diff.treeToWorkdir(repo, tree, options)
- })
- .then(diff => this._getDiffLines(diff))
- .then(lines => {
- const stats = {added: 0, deleted: 0}
- for (const line of lines) {
- const origin = line.origin()
- if (origin === Git.Diff.LINE.ADDITION) {
- stats.added++
- } else if (origin === Git.Diff.LINE.DELETION) {
- stats.deleted++
- }
- }
- return stats
- })
+ return this.repo.getDiffStats(_path)
}
// Public: Retrieves the line diffs comparing the `HEAD` version of the given
@@ -620,25 +435,7 @@ export default class GitRepositoryAsync {
// * `oldLines` The {Number} of lines in the old hunk.
// * `newLines` The {Number} of lines in the new hunk
getLineDiffs (_path, text) {
- let relativePath = null
- return this.getRepo()
- .then(repo => {
- relativePath = this.relativize(_path, repo.workdir())
- return repo.getHeadCommit()
- })
- .then(commit => commit.getEntry(relativePath))
- .then(entry => entry.getBlob())
- .then(blob => {
- const options = new Git.DiffOptions()
- options.contextLines = 0
- if (process.platform === 'win32') {
- // Ignore eol of line differences on windows so that files checked in
- // as LF don't report every line modified when the text contains CRLF
- // endings.
- options.flags = Git.Diff.OPTION.IGNORE_WHITESPACE_EOL
- }
- return this._diffBlobToBuffer(blob, text, options)
- })
+ return this.repo.getLineDiffs(_path, text)
}
// Checking Out
@@ -659,14 +456,7 @@ export default class GitRepositoryAsync {
// Returns a {Promise} that resolves or rejects depending on whether the
// method was successful.
checkoutHead (_path) {
- return this.getRepo()
- .then(repo => {
- const checkoutOptions = new Git.CheckoutOptions()
- checkoutOptions.paths = [this.relativize(_path, repo.workdir())]
- checkoutOptions.checkoutStrategy = Git.Checkout.STRATEGY.FORCE | Git.Checkout.STRATEGY.DISABLE_PATHSPEC_MATCH
- return Git.Checkout.head(repo, checkoutOptions)
- })
- .then(() => this.refreshStatusForPath(_path))
+ return this.repo.checkoutHead(_path)
}
// Public: Checks out a branch in your repository.
@@ -677,17 +467,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Promise} that resolves if the method was successful.
checkoutReference (reference, create) {
- return this.getRepo()
- .then(repo => repo.checkoutBranch(reference))
- .catch(error => {
- if (create) {
- return this._createBranch(reference)
- .then(_ => this.checkoutReference(reference, false))
- } else {
- throw error
- }
- })
- .then(_ => null)
+ return this.repo.checkoutReference(reference, create)
}
// Private
@@ -706,93 +486,10 @@ export default class GitRepositoryAsync {
return this.checkoutHead(filePath)
}
- // Create a new branch with the given name.
+ // Refreshes the git status.
//
- // * `name` The {String} name of the new branch.
- //
- // Returns a {Promise} which resolves to a {NodeGit.Ref} reference to the
- // created branch.
- _createBranch (name) {
- return this.getRepo()
- .then(repo => Promise.all([repo, repo.getHeadCommit()]))
- .then(([repo, commit]) => repo.createBranch(name, commit))
- }
-
- // Get all the hunks in the diff.
- //
- // * `diff` The {NodeGit.Diff} whose hunks should be retrieved.
- //
- // Returns a {Promise} which resolves to an {Array} of {NodeGit.Hunk}.
- _getDiffHunks (diff) {
- return diff.patches()
- .then(patches => Promise.all(patches.map(p => p.hunks()))) // patches :: Array
- .then(hunks => _.flatten(hunks)) // hunks :: Array>
- }
-
- // Get all the lines contained in the diff.
- //
- // * `diff` The {NodeGit.Diff} use lines should be retrieved.
- //
- // Returns a {Promise} which resolves to an {Array} of {NodeGit.Line}.
- _getDiffLines (diff) {
- return this._getDiffHunks(diff)
- .then(hunks => Promise.all(hunks.map(h => h.lines())))
- .then(lines => _.flatten(lines)) // lines :: Array>
- }
-
- // Diff the given blob and buffer with the provided options.
- //
- // * `blob` The {NodeGit.Blob}
- // * `buffer` The {String} buffer.
- // * `options` The {NodeGit.DiffOptions}
- //
- // Returns a {Promise} which resolves to an {Array} of {Object}s which have
- // the following keys:
- // * `oldStart` The {Number} of the old starting line.
- // * `newStart` The {Number} of the new starting line.
- // * `oldLines` The {Number} of old lines.
- // * `newLines` The {Number} of new lines.
- _diffBlobToBuffer (blob, buffer, options) {
- const hunks = []
- const hunkCallback = (delta, hunk, payload) => {
- hunks.push({
- oldStart: hunk.oldStart(),
- newStart: hunk.newStart(),
- oldLines: hunk.oldLines(),
- newLines: hunk.newLines()
- })
- }
-
- return Git.Diff.blobToBuffer(blob, null, buffer, null, options, null, null, hunkCallback, null)
- .then(_ => hunks)
- }
-
- // Get the current branch and update this.branch.
- //
- // Returns a {Promise} which resolves to the {String} branch name.
- _refreshBranch () {
- return this.getRepo()
- .then(repo => repo.getCurrentBranch())
- .then(ref => ref.name())
- .then(branchName => this.branch = branchName)
- }
-
- // Refresh the cached ahead/behind count with the given branch.
- //
- // * `branchName` The {String} name of the branch whose ahead/behind should be
- // used for the refresh.
- //
- // Returns a {Promise} which will resolve to {null}.
- _refreshAheadBehindCount (branchName) {
- return this.getAheadBehindCount(branchName)
- .then(counts => this.upstream = counts)
- }
-
- // Get the status for this repository.
- //
- // Returns a {Promise} that will resolve to an object of {String} paths to the
- // {Number} status.
- _getRepositoryStatus () {
+ // Returns a {Promise} which will resolve to {null} when refresh is complete.
+ refreshStatus () {
let projectPathsPromises = [Promise.resolve('')]
if (this.project) {
projectPathsPromises = this.project.getPaths()
@@ -800,157 +497,8 @@ export default class GitRepositoryAsync {
}
return Promise.all(projectPathsPromises)
- .then(paths => paths.filter(p => p.length > 0))
- .then(projectPaths => {
- return this._getStatus(projectPaths.length > 0 ? projectPaths : null)
- })
- .then(statuses => {
- const statusPairs = statuses.map(status => [status.path(), status.statusBit()])
- return _.object(statusPairs)
- })
- }
-
- // Get the status for the given submodule.
- //
- // * `submodule` The {GitRepositoryAsync} for the submodule.
- //
- // Returns a {Promise} which resolves to an {Object}, keyed by {String}
- // repo-relative {Number} statuses.
- async _getSubmoduleStatus (submodule) {
- // At this point, we've called submodule._refreshSubmodules(), which would
- // have refreshed the status on *its* submodules, etc. So we know that its
- // cached path statuses are up-to-date.
- //
- // Now we just need to hoist those statuses into our repository by changing
- // their paths to be relative to us.
-
- const statuses = submodule.getCachedPathStatuses()
- const repoRelativeStatuses = {}
- const submoduleRepo = await submodule.getRepo()
- const submoduleWorkDir = submoduleRepo.workdir()
- for (const relativePath in statuses) {
- const statusBit = statuses[relativePath]
- const absolutePath = path.join(submoduleWorkDir, relativePath)
- const repoRelativePath = await this.relativizeToWorkingDirectory(absolutePath)
- repoRelativeStatuses[repoRelativePath] = statusBit
- }
-
- return repoRelativeStatuses
- }
-
- // Refresh the list of submodules in the repository.
- //
- // Returns a {Promise} which resolves to an {Object} keyed by {String}
- // submodule names with {GitRepositoryAsync} values.
- async _refreshSubmodules () {
- const repo = await this.getRepo()
- const submoduleNames = await repo.getSubmoduleNames()
- for (const name of submoduleNames) {
- const alreadyExists = Boolean(this.submodules[name])
- if (alreadyExists) continue
-
- const submodule = await Git.Submodule.lookup(repo, name)
- const absolutePath = path.join(repo.workdir(), submodule.path())
- const submoduleRepo = GitRepositoryAsync.open(absolutePath, {openExactPath: true, refreshOnWindowFocus: false})
- this.submodules[name] = submoduleRepo
- }
-
- for (const name in this.submodules) {
- const repo = this.submodules[name]
- const gone = submoduleNames.indexOf(name) < 0
- if (gone) {
- repo.destroy()
- delete this.submodules[name]
- } else {
- try {
- await repo.refreshStatus()
- } catch (e) {
- // libgit2 will sometimes report submodules that aren't actually valid
- // (https://github.com/libgit2/libgit2/issues/3580). So check the
- // validity of the submodules by removing any that fail.
- repo.destroy()
- delete this.submodules[name]
- }
- }
- }
-
- return _.values(this.submodules)
- }
-
- // Get the status for the submodules in the repository.
- //
- // Returns a {Promise} that will resolve to an object of {String} paths to the
- // {Number} status.
- _getSubmoduleStatuses () {
- return this._refreshSubmodules()
- .then(repos => {
- return Promise.all(repos.map(repo => this._getSubmoduleStatus(repo)))
- })
- .then(statuses => _.extend({}, ...statuses))
- }
-
- // Refresh the cached status.
- //
- // Returns a {Promise} which will resolve to {null}.
- _refreshStatus () {
- return Promise.all([this._getRepositoryStatus(), this._getSubmoduleStatuses()])
- .then(([repositoryStatus, submoduleStatus]) => {
- const statusesByPath = _.extend({}, repositoryStatus, submoduleStatus)
- if (!_.isEqual(this.pathStatusCache, statusesByPath) && this.emitter != null) {
- this.emitter.emit('did-change-statuses')
- }
- this.pathStatusCache = statusesByPath
- })
- }
-
- // Refreshes the git status.
- //
- // Returns a {Promise} which will resolve to {null} when refresh is complete.
- refreshStatus () {
- const status = this._refreshStatus()
- const branch = this._refreshBranch()
- const aheadBehind = branch.then(branchName => this._refreshAheadBehindCount(branchName))
-
- this._refreshingPromise = this._refreshingPromise.then(_ => {
- return Promise.all([status, branch, aheadBehind])
- .then(_ => null)
- // Because all these refresh steps happen asynchronously, it's entirely
- // possible the repository was destroyed while we were working. In which
- // case we should just swallow the error.
- .catch(e => {
- if (this._isDestroyed()) {
- return null
- } else {
- return Promise.reject(e)
- }
- })
- .catch(e => {
- console.error('Error refreshing repository status:')
- console.error(e)
- return Promise.reject(e)
- })
- })
- return this._refreshingPromise
- }
-
- // Get the submodule for the given path.
- //
- // Returns a {Promise} which resolves to the {GitRepositoryAsync} submodule or
- // null if it isn't a submodule path.
- async _submoduleForPath (_path) {
- let relativePath = await this.relativizeToWorkingDirectory(_path)
- for (const submodulePath in this.submodules) {
- const submoduleRepo = this.submodules[submodulePath]
- if (relativePath === submodulePath) {
- return submoduleRepo
- } else if (relativePath.indexOf(`${submodulePath}/`) === 0) {
- relativePath = relativePath.substring(submodulePath.length + 1)
- const innerSubmodule = await submoduleRepo._submoduleForPath(relativePath)
- return innerSubmodule || submoduleRepo
- }
- }
-
- return null
+ .then(paths => paths.map(p => p.length > 0 ? p + '/**' : '*'))
+ .then(pathspecs => this.repo.refreshStatus(pathspecs))
}
// Get the NodeGit repository for the given path.
@@ -961,16 +509,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Promise} which resolves to the {NodeGit.Repository}.
getRepo (_path) {
- if (this._isDestroyed()) {
- const error = new Error('Repository has been destroyed')
- error.name = GitRepositoryAsync.DestroyedErrorName
- return Promise.reject(error)
- }
-
- if (!_path) return this.repoPromise
-
- return this._submoduleForPath(_path)
- .then(submodule => submodule ? submodule.getRepo() : this.repoPromise)
+ return this.repo.getRepo(_path)
}
// Open a new instance of the underlying {NodeGit.Repository}.
@@ -980,11 +519,7 @@ export default class GitRepositoryAsync {
//
// Returns the new {NodeGit.Repository}.
openRepository () {
- if (this._openExactPath) {
- return Git.Repository.open(this.openedPath)
- } else {
- return Git.Repository.openExt(this.openedPath, 0, '')
- }
+ return this.repo.openRepository()
}
// Section: Private
@@ -994,7 +529,7 @@ export default class GitRepositoryAsync {
//
// Returns a {Boolean}.
_isDestroyed () {
- return this.repoPromise == null
+ return this.repo._isDestroyed()
}
// Subscribe to events on the given buffer.
@@ -1020,26 +555,4 @@ export default class GitRepositoryAsync {
this.subscriptions.add(bufferSubscriptions)
}
-
- // Get the status for the given paths.
- //
- // * `paths` The {String} paths whose status is wanted. If undefined, get the
- // status for the whole repository.
- //
- // Returns a {Promise} which resolves to an {Array} of {NodeGit.StatusFile}
- // statuses for the paths.
- _getStatus (paths, repo) {
- return this.getRepo()
- .then(repo => {
- const opts = {
- flags: Git.Status.OPT.INCLUDE_UNTRACKED | Git.Status.OPT.RECURSE_UNTRACKED_DIRS | Git.Status.OPT.DISABLE_PATHSPEC_MATCH
- }
-
- if (paths) {
- opts.pathspec = paths
- }
-
- return repo.getStatusExt(opts)
- })
- }
}
diff --git a/src/git-repository.coffee b/src/git-repository.coffee
index 44b86a433..fcbc40830 100644
--- a/src/git-repository.coffee
+++ b/src/git-repository.coffee
@@ -83,11 +83,12 @@ class GitRepository
asyncOptions.subscribeToBuffers = false
@async = GitRepositoryAsync.open(path, asyncOptions)
- @statuses = {}
@upstream = {ahead: 0, behind: 0}
for submodulePath, submoduleRepo of @repo.submodules
submoduleRepo.upstream = {ahead: 0, behind: 0}
+ @statusesByPath = {}
+
{@project, @config, refreshOnWindowFocus} = options
refreshOnWindowFocus ?= true
@@ -317,7 +318,7 @@ class GitRepository
getDirectoryStatus: (directoryPath) ->
directoryPath = "#{@relativize(directoryPath)}/"
directoryStatus = 0
- for path, status of @statuses
+ for path, status of _.extend({}, @async.getCachedPathStatuses(), @statusesByPath)
directoryStatus |= status if path.indexOf(directoryPath) is 0
directoryStatus
@@ -328,18 +329,26 @@ class GitRepository
# Returns a {Number} representing the status. This value can be passed to
# {::isStatusModified} or {::isStatusNew} to get more information.
getPathStatus: (path) ->
+ repo = @getRepo(path)
+ relativePath = @relativize(path)
+
+ # This is a bit particular. If a package calls `getPathStatus` like this:
+ # - change the file
+ # - getPathStatus
+ # - change the file
+ # - getPathStatus
+ # We need to preserve the guarantee that each call to `getPathStatus` will
+ # synchronously emit 'did-change-status'. So we need to keep a cache of the
+ # statuses found from this call.
+ currentPathStatus = @getCachedRelativePathStatus(relativePath) ? 0
+
# Trigger events emitted on the async repo as well
@async.refreshStatusForPath(path)
- repo = @getRepo(path)
- relativePath = @relativize(path)
- currentPathStatus = @statuses[relativePath] ? 0
pathStatus = repo.getStatus(repo.relativize(path)) ? 0
pathStatus = 0 if repo.isStatusIgnored(pathStatus)
- if pathStatus > 0
- @statuses[relativePath] = pathStatus
- else
- delete @statuses[relativePath]
+ @statusesByPath[relativePath] = pathStatus
+
if currentPathStatus isnt pathStatus
@emitter.emit 'did-change-status', {path, pathStatus}
@@ -351,7 +360,11 @@ class GitRepository
#
# Returns a status {Number} or null if the path is not in the cache.
getCachedPathStatus: (path) ->
- @statuses[@relativize(path)]
+ relativePath = @relativize(path)
+ @getCachedRelativePathStatus(relativePath)
+
+ getCachedRelativePathStatus: (relativePath) ->
+ @statusesByPath[relativePath] ? @async.getCachedPathStatuses()[relativePath]
# Public: Returns true if the given status indicates modification.
#
@@ -478,25 +491,34 @@ class GitRepository
#
# Returns a promise that resolves when the repository has been refreshed.
refreshStatus: ->
- asyncRefresh = @async.refreshStatus()
+ statusesChanged = false
+
+ # Listen for `did-change-statuses` so we know if something changed. But we
+ # need to wait to propagate it until after we've set the branch and cleared
+ # the `statusesByPath` cache. So just set a flag, and we'll emit the event
+ # after refresh is done.
+ subscription = @async.onDidChangeStatuses ->
+ subscription?.dispose()
+ subscription = null
+
+ statusesChanged = true
+
+ asyncRefresh = @async.refreshStatus().then =>
+ subscription?.dispose()
+ subscription = null
+
+ @branch = @async?.branch
+ @statusesByPath = {}
+
+ if statusesChanged
+ @emitter.emit 'did-change-statuses'
+
syncRefresh = new Promise (resolve, reject) =>
@handlerPath ?= require.resolve('./repository-status-handler')
- relativeProjectPaths = @project?.getPaths()
- .map (path) => @relativize(path)
- .filter (path) -> path.length > 0
- .map (path) -> path + '/**'
-
@statusTask?.terminate()
- @statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) =>
- statusesUnchanged = _.isEqual(statuses, @statuses) and
- _.isEqual(upstream, @upstream) and
- _.isEqual(branch, @branch) and
- _.isEqual(submodules, @submodules)
-
- @statuses = statuses
+ @statusTask = Task.once @handlerPath, @getPath(), ({upstream, submodules}) =>
@upstream = upstream
- @branch = branch
@submodules = submodules
for submodulePath, submoduleRepo of @getRepo().submodules
@@ -504,7 +526,4 @@ class GitRepository
resolve()
- unless statusesUnchanged
- @emitter.emit 'did-change-statuses'
-
return Promise.all([asyncRefresh, syncRefresh])
diff --git a/src/gutter.coffee b/src/gutter.coffee
index f59fa7b6e..2fc362cbd 100644
--- a/src/gutter.coffee
+++ b/src/gutter.coffee
@@ -71,13 +71,13 @@ class Gutter
isVisible: ->
@visible
- # Essential: Add a decoration that tracks a {TextEditorMarker}. When the marker moves,
+ # Essential: Add a decoration that tracks a {DisplayMarker}. When the marker moves,
# is invalidated, or is destroyed, the decoration will be updated to reflect
# the marker's state.
#
# ## Arguments
#
- # * `marker` A {TextEditorMarker} you want this decoration to follow.
+ # * `marker` A {DisplayMarker} you want this decoration to follow.
# * `decorationParams` An {Object} representing the decoration. It is passed
# to {TextEditor::decorateMarker} as its `decorationParams` and so supports
# all options documented there.
diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee
index 5c5e936c5..463bdd48e 100644
--- a/src/initialize-application-window.coffee
+++ b/src/initialize-application-window.coffee
@@ -1,10 +1,15 @@
# Like sands through the hourglass, so are the days of our lives.
module.exports = ({blobStore}) ->
+ environmentHelpers = require('./environment-helpers')
path = require 'path'
require './window'
{getWindowLoadSettings} = require './window-load-settings-helpers'
+ {ipcRenderer} = require 'electron'
+ {resourcePath, isSpec, devMode, env} = getWindowLoadSettings()
- {resourcePath, isSpec, devMode} = getWindowLoadSettings()
+ # Set baseline environment
+ environmentHelpers.normalize({env: env})
+ env = process.env
# Add application-specific exports to module search path.
exportsPath = path.join(resourcePath, 'exports')
@@ -21,14 +26,16 @@ module.exports = ({blobStore}) ->
applicationDelegate: new ApplicationDelegate,
configDirPath: process.env.ATOM_HOME
enablePersistence: true
+ env: process.env
})
- atom.loadStateSync()
- atom.displayWindow()
- atom.startEditorWindow()
+ atom.startEditorWindow().then ->
- # Workaround for focus getting cleared upon window creation
- windowFocused = ->
- window.removeEventListener('focus', windowFocused)
- setTimeout (-> document.querySelector('atom-workspace').focus()), 0
- window.addEventListener('focus', windowFocused)
+ # Workaround for focus getting cleared upon window creation
+ windowFocused = ->
+ window.removeEventListener('focus', windowFocused)
+ setTimeout (-> document.querySelector('atom-workspace').focus()), 0
+ window.addEventListener('focus', windowFocused)
+ ipcRenderer.on('environment', (event, env) ->
+ environmentHelpers.replace(env)
+ )
diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee
index f3507b479..690180fc8 100644
--- a/src/initialize-test-window.coffee
+++ b/src/initialize-test-window.coffee
@@ -4,17 +4,17 @@ cloneObject = (object) ->
clone
module.exports = ({blobStore}) ->
+ {crashReporter, remote} = require 'electron'
# Start the crash reporter before anything else.
- require('crash-reporter').start(productName: 'Atom', companyName: 'GitHub')
- remote = require 'remote'
+ crashReporter.start(productName: 'Atom', companyName: 'GitHub', submitURL: 'http://54.249.141.255:1127/post')
exitWithStatusCode = (status) ->
- remote.require('app').emit('will-quit')
+ remote.app.emit('will-quit')
remote.process.exit(status)
try
path = require 'path'
- ipc = require 'ipc'
+ {ipcRenderer} = require 'electron'
{getWindowLoadSettings} = require './window-load-settings-helpers'
AtomEnvironment = require '../src/atom-environment'
ApplicationDelegate = require '../src/application-delegate'
@@ -29,15 +29,15 @@ module.exports = ({blobStore}) ->
handleKeydown = (event) ->
# Reload: cmd-r / ctrl-r
if (event.metaKey or event.ctrlKey) and event.keyCode is 82
- ipc.send('call-window-method', 'restart')
+ ipcRenderer.send('call-window-method', 'reload')
# Toggle Dev Tools: cmd-alt-i / ctrl-alt-i
if (event.metaKey or event.ctrlKey) and event.altKey and event.keyCode is 73
- ipc.send('call-window-method', 'toggleDevTools')
+ ipcRenderer.send('call-window-method', 'toggleDevTools')
# Reload: cmd-w / ctrl-w
if (event.metaKey or event.ctrlKey) and event.keyCode is 87
- ipc.send('call-window-method', 'close')
+ ipcRenderer.send('call-window-method', 'close')
window.addEventListener('keydown', handleKeydown, true)
@@ -68,7 +68,8 @@ module.exports = ({blobStore}) ->
logFile, headless, testPaths, buildAtomEnvironment, buildDefaultApplicationDelegate, legacyTestRunner
})
- promise.then(exitWithStatusCode) if getWindowLoadSettings().headless
+ promise.then (statusCode) ->
+ exitWithStatusCode(statusCode) if getWindowLoadSettings().headless
catch error
if getWindowLoadSettings().headless
console.error(error.stack ? error)
diff --git a/src/ipc-helpers.js b/src/ipc-helpers.js
new file mode 100644
index 000000000..c0b38c50e
--- /dev/null
+++ b/src/ipc-helpers.js
@@ -0,0 +1,40 @@
+var ipcRenderer = null
+var ipcMain = null
+var BrowserWindow = null
+
+exports.call = function (methodName, ...args) {
+ if (!ipcRenderer) {
+ ipcRenderer = require('electron').ipcRenderer
+ }
+
+ var responseChannel = getResponseChannel(methodName)
+
+ return new Promise(function (resolve) {
+ ipcRenderer.on(responseChannel, function (event, result) {
+ ipcRenderer.removeAllListeners(responseChannel)
+ resolve(result)
+ })
+
+ ipcRenderer.send(methodName, ...args)
+ })
+}
+
+exports.respondTo = function (methodName, callback) {
+ if (!ipcMain) {
+ var electron = require('electron')
+ ipcMain = electron.ipcMain
+ BrowserWindow = electron.BrowserWindow
+ }
+
+ var responseChannel = getResponseChannel(methodName)
+
+ ipcMain.on(methodName, function (event, ...args) {
+ var browserWindow = BrowserWindow.fromWebContents(event.sender)
+ var result = callback(browserWindow, ...args)
+ event.sender.send(responseChannel, result)
+ })
+}
+
+function getResponseChannel (methodName) {
+ return 'ipc-helpers-' + methodName + '-response'
+}
diff --git a/src/language-mode.coffee b/src/language-mode.coffee
index ac8a5af76..a0392acf6 100644
--- a/src/language-mode.coffee
+++ b/src/language-mode.coffee
@@ -10,6 +10,7 @@ class LanguageMode
# editor - The {TextEditor} to associate with
constructor: (@editor, @config) ->
{@buffer} = @editor
+ @regexesByPattern = {}
destroy: ->
@@ -89,30 +90,36 @@ class LanguageMode
# Folds all the foldable lines in the buffer.
foldAll: ->
+ @unfoldAll()
+ foldedRowRanges = {}
for currentRow in [0..@buffer.getLastRow()] by 1
- [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
+ rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
- @editor.createFold(startRow, endRow)
+ continue if foldedRowRanges[rowRange]
+
+ @editor.foldBufferRowRange(startRow, endRow)
+ foldedRowRanges[rowRange] = true
return
# Unfolds all the foldable lines in the buffer.
unfoldAll: ->
- for fold in @editor.displayBuffer.foldsIntersectingBufferRowRange(0, @buffer.getLastRow()) by -1
- fold.destroy()
- return
+ @editor.displayLayer.destroyAllFolds()
# Fold all comment and code blocks at a given indentLevel
#
# indentLevel - A {Number} indicating indentLevel; 0 based.
foldAllAtIndentLevel: (indentLevel) ->
@unfoldAll()
+ foldedRowRanges = {}
for currentRow in [0..@buffer.getLastRow()] by 1
- [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
+ rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow?
+ continue if foldedRowRanges[rowRange]
# assumption: startRow will always be the min indent level for the entire range
if @editor.indentationForBufferRow(startRow) is indentLevel
- @editor.createFold(startRow, endRow)
+ @editor.foldBufferRowRange(startRow, endRow)
+ foldedRowRanges[rowRange] = true
return
# Given a buffer row, creates a fold at it.
@@ -124,8 +131,8 @@ class LanguageMode
for currentRow in [bufferRow..0] by -1
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
continue unless startRow? and startRow <= bufferRow <= endRow
- fold = @editor.displayBuffer.largestFoldStartingAtBufferRow(startRow)
- return @editor.createFold(startRow, endRow) unless fold
+ unless @editor.isFoldedAtBufferRow(startRow)
+ return @editor.foldBufferRowRange(startRow, endRow)
# Find the row range for a fold at a given bufferRow. Will handle comments
# and code.
@@ -139,21 +146,19 @@ class LanguageMode
rowRange
rowRangeForCommentAtBufferRow: (bufferRow) ->
- return unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
+ return unless @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
startRow = bufferRow
endRow = bufferRow
if bufferRow > 0
for currentRow in [bufferRow-1..0] by -1
- break if @buffer.isRowBlank(currentRow)
- break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
+ break unless @editor.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
startRow = currentRow
if bufferRow < @buffer.getLastRow()
for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1
- break if @buffer.isRowBlank(currentRow)
- break unless @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
+ break unless @editor.tokenizedBuffer.tokenizedLineForRow(currentRow).isComment()
endRow = currentRow
return [startRow, endRow] if startRow isnt endRow
@@ -176,13 +181,13 @@ class LanguageMode
[bufferRow, foldEndRow]
isFoldableAtBufferRow: (bufferRow) ->
- @editor.displayBuffer.tokenizedBuffer.isFoldableAtRow(bufferRow)
+ @editor.tokenizedBuffer.isFoldableAtRow(bufferRow)
# Returns a {Boolean} indicating whether the line at the given buffer
# row is a comment.
isLineCommentedAtBufferRow: (bufferRow) ->
return false unless 0 <= bufferRow <= @editor.getLastBufferRow()
- @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
+ @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
# Find a row range for a 'paragraph' around specified bufferRow. A paragraph
# is a block of text bounded by and empty line or a block of text that is not
@@ -235,11 +240,11 @@ class LanguageMode
# Returns a {Number}.
suggestedIndentForBufferRow: (bufferRow, options) ->
line = @buffer.lineForRow(bufferRow)
- tokenizedLine = @editor.displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow)
+ tokenizedLine = @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow)
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
suggestedIndentForLineAtBufferRow: (bufferRow, line, options) ->
- tokenizedLine = @editor.displayBuffer.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line)
+ tokenizedLine = @editor.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line)
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
suggestedIndentForTokenizedLineAtBufferRow: (bufferRow, line, tokenizedLine, options) ->
@@ -328,7 +333,8 @@ class LanguageMode
getRegexForProperty: (scopeDescriptor, property) ->
if pattern = @config.get(property, scope: scopeDescriptor)
- new OnigRegExp(pattern)
+ @regexesByPattern[pattern] ?= new OnigRegExp(pattern)
+ @regexesByPattern[pattern]
increaseIndentRegexForScopeDescriptor: (scopeDescriptor) ->
@getRegexForProperty(scopeDescriptor, 'editor.increaseIndentPattern')
diff --git a/src/layer-decoration.coffee b/src/layer-decoration.coffee
index 1f76140a3..e00e024cb 100644
--- a/src/layer-decoration.coffee
+++ b/src/layer-decoration.coffee
@@ -7,7 +7,7 @@ nextId = -> idCounter++
# layer. Created via {TextEditor::decorateMarkerLayer}.
module.exports =
class LayerDecoration
- constructor: (@markerLayer, @displayBuffer, @properties) ->
+ constructor: (@markerLayer, @decorationManager, @properties) ->
@id = nextId()
@destroyed = false
@markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy()
@@ -19,7 +19,7 @@ class LayerDecoration
@markerLayerDestroyedDisposable.dispose()
@markerLayerDestroyedDisposable = null
@destroyed = true
- @displayBuffer.didDestroyLayerDecoration(this)
+ @decorationManager.didDestroyLayerDecoration(this)
# Essential: Determine whether this decoration is destroyed.
#
@@ -44,11 +44,11 @@ class LayerDecoration
setProperties: (newProperties) ->
return if @destroyed
@properties = newProperties
- @displayBuffer.scheduleUpdateDecorationsEvent()
+ @decorationManager.scheduleUpdateDecorationsEvent()
# Essential: Override the decoration properties for a specific marker.
#
- # * `marker` The {TextEditorMarker} or {Marker} for which to override
+ # * `marker` The {DisplayMarker} or {Marker} for which to override
# properties.
# * `properties` An {Object} containing properties to apply to this marker.
# Pass `null` to clear the override.
@@ -58,4 +58,4 @@ class LayerDecoration
@overridePropertiesByMarkerId[marker.id] = properties
else
delete @overridePropertiesByMarkerId[marker.id]
- @displayBuffer.scheduleUpdateDecorationsEvent()
+ @decorationManager.scheduleUpdateDecorationsEvent()
diff --git a/src/line-number-gutter-component.coffee b/src/line-number-gutter-component.coffee
index bb66ff144..3a3c199c2 100644
--- a/src/line-number-gutter-component.coffee
+++ b/src/line-number-gutter-component.coffee
@@ -93,9 +93,9 @@ class LineNumberGutterComponent extends TiledComponent
{target} = event
lineNumber = target.parentNode
- if target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
+ if target.classList.contains('icon-right')
bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row'))
if lineNumber.classList.contains('folded')
@editor.unfoldBufferRow(bufferRow)
- else
+ else if lineNumber.classList.contains('foldable')
@editor.foldBufferRow(bufferRow)
diff --git a/src/lines-component.coffee b/src/lines-component.coffee
index b5af56885..88645589a 100644
--- a/src/lines-component.coffee
+++ b/src/lines-component.coffee
@@ -43,7 +43,7 @@ class LinesComponent extends TiledComponent
@domNode
shouldRecreateAllTilesOnUpdate: ->
- @oldState.indentGuidesVisible isnt @newState.indentGuidesVisible or @newState.continuousReflow
+ @newState.continuousReflow
beforeUpdateSync: (state) ->
if @newState.maxHeight isnt @oldState.maxHeight
@@ -70,8 +70,6 @@ class LinesComponent extends TiledComponent
@cursorsComponent.updateSync(state)
- @oldState.indentGuidesVisible = @newState.indentGuidesVisible
-
buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert, @grammars})
buildEmptyState: ->
@@ -97,10 +95,14 @@ class LinesComponent extends TiledComponent
@presenter.setLineHeight(lineHeightInPixels)
@presenter.setBaseCharacterWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
- lineNodeForLineIdAndScreenRow: (lineId, screenRow) ->
+ lineIdForScreenRow: (screenRow) ->
tile = @presenter.tileForRow(screenRow)
- @getComponentForTile(tile)?.lineNodeForLineId(lineId)
+ @getComponentForTile(tile)?.lineIdForScreenRow(screenRow)
- textNodesForLineIdAndScreenRow: (lineId, screenRow) ->
+ lineNodeForScreenRow: (screenRow) ->
tile = @presenter.tileForRow(screenRow)
- @getComponentForTile(tile)?.textNodesForLineId(lineId)
+ @getComponentForTile(tile)?.lineNodeForScreenRow(screenRow)
+
+ textNodesForScreenRow: (screenRow) ->
+ tile = @presenter.tileForRow(screenRow)
+ @getComponentForTile(tile)?.textNodesForScreenRow(screenRow)
diff --git a/src/lines-tile-component.coffee b/src/lines-tile-component.coffee
index defcc0d8a..6844f21de 100644
--- a/src/lines-tile-component.coffee
+++ b/src/lines-tile-component.coffee
@@ -1,10 +1,10 @@
_ = require 'underscore-plus'
HighlightsComponent = require './highlights-component'
-TokenIterator = require './token-iterator'
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
TokenTextEscapeRegex = /[&"'<>]/g
MaxTokenLength = 20000
+ZERO_WIDTH_NBSP = '\ufeff'
cloneObject = (object) ->
clone = {}
@@ -14,7 +14,6 @@ cloneObject = (object) ->
module.exports =
class LinesTileComponent
constructor: ({@presenter, @id, @domElementPool, @assert, grammars}) ->
- @tokenIterator = new TokenIterator(grammarRegistry: grammars)
@measuredLines = new Set
@lineNodesByLineId = {}
@screenRowsByLineId = {}
@@ -69,13 +68,10 @@ class LinesTileComponent
@oldTileState.top = @newTileState.top
@oldTileState.left = @newTileState.left
- @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible
@updateLineNodes()
@highlightsComponent.updateSync(@newTileState)
- @oldState.indentGuidesVisible = @newState.indentGuidesVisible
-
removeLineNodes: ->
@removeLineNode(id) for id of @oldTileState.lines
return
@@ -149,7 +145,7 @@ class LinesTileComponent
if newLineState.screenRow isnt oldLineState.screenRow
insertionPoint.dataset.screenRow = newLineState.screenRow
- precedingBlockDecorationsSelector = newLineState.precedingBlockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',')
+ precedingBlockDecorationsSelector = newLineState.precedingBlockDecorations.map((d) -> ".atom--block-decoration-#{d.id}").join(',')
if precedingBlockDecorationsSelector isnt oldLineState.precedingBlockDecorationsSelector
insertionPoint.setAttribute("select", precedingBlockDecorationsSelector)
@@ -180,7 +176,7 @@ class LinesTileComponent
if newLineState.screenRow isnt oldLineState.screenRow
insertionPoint.dataset.screenRow = newLineState.screenRow
- followingBlockDecorationsSelector = newLineState.followingBlockDecorations.map((d) -> "#atom--block-decoration-#{d.id}").join(',')
+ followingBlockDecorationsSelector = newLineState.followingBlockDecorations.map((d) -> ".atom--block-decoration-#{d.id}").join(',')
if followingBlockDecorationsSelector isnt oldLineState.followingBlockDecorationsSelector
insertionPoint.setAttribute("select", followingBlockDecorationsSelector)
@@ -195,8 +191,7 @@ class LinesTileComponent
screenRowForNode: (node) -> parseInt(node.dataset.screenRow)
buildLineNode: (id) ->
- {width} = @newState
- {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id]
+ {lineText, tagCodes, screenRow, decorationClasses} = @newTileState.lines[id]
lineNode = @domElementPool.buildElement("div", "line")
lineNode.dataset.screenRow = screenRow
@@ -205,185 +200,40 @@ class LinesTileComponent
for decorationClass in decorationClasses
lineNode.classList.add(decorationClass)
- @currentLineTextNodes = []
- if text is ""
- @setEmptyLineInnerNodes(id, lineNode)
- else
- @setLineInnerNodes(id, lineNode)
- @textNodesByLineId[id] = @currentLineTextNodes
-
- lineNode.appendChild(@domElementPool.buildElement("span", "fold-marker")) if fold
- lineNode
-
- setEmptyLineInnerNodes: (id, lineNode) ->
- {indentGuidesVisible} = @newState
- {indentLevel, tabLength, endOfLineInvisibles} = @newTileState.lines[id]
-
- if indentGuidesVisible and indentLevel > 0
- invisibleIndex = 0
- for i in [0...indentLevel]
- indentGuide = @domElementPool.buildElement("span", "indent-guide")
- for j in [0...tabLength]
- if invisible = endOfLineInvisibles?[invisibleIndex++]
- invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
- textNode = @domElementPool.buildText(invisible)
- invisibleSpan.appendChild(textNode)
- indentGuide.appendChild(invisibleSpan)
-
- @currentLineTextNodes.push(textNode)
- else
- textNode = @domElementPool.buildText(" ")
- indentGuide.appendChild(textNode)
-
- @currentLineTextNodes.push(textNode)
- lineNode.appendChild(indentGuide)
-
- while invisibleIndex < endOfLineInvisibles?.length
- invisible = endOfLineInvisibles[invisibleIndex++]
- invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
- textNode = @domElementPool.buildText(invisible)
- invisibleSpan.appendChild(textNode)
- lineNode.appendChild(invisibleSpan)
-
- @currentLineTextNodes.push(textNode)
- else
- unless @appendEndOfLineNodes(id, lineNode)
- textNode = @domElementPool.buildText("\u00a0")
- lineNode.appendChild(textNode)
-
- @currentLineTextNodes.push(textNode)
-
- setLineInnerNodes: (id, lineNode) ->
- lineState = @newTileState.lines[id]
- {firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState
- lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0
-
- @tokenIterator.reset(lineState)
+ textNodes = []
+ lineLength = 0
+ startIndex = 0
openScopeNode = lineNode
-
- while @tokenIterator.next()
- for scope in @tokenIterator.getScopeEnds()
+ for tagCode in tagCodes when tagCode isnt 0
+ if @presenter.isCloseTagCode(tagCode)
openScopeNode = openScopeNode.parentElement
-
- for scope in @tokenIterator.getScopeStarts()
+ else if @presenter.isOpenTagCode(tagCode)
+ scope = @presenter.tagForCode(tagCode)
newScopeNode = @domElementPool.buildElement("span", scope.replace(/\.+/g, ' '))
openScopeNode.appendChild(newScopeNode)
openScopeNode = newScopeNode
-
- tokenStart = @tokenIterator.getScreenStart()
- tokenEnd = @tokenIterator.getScreenEnd()
- tokenText = @tokenIterator.getText()
- isHardTab = @tokenIterator.isHardTab()
-
- if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex
- tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart
else
- tokenFirstNonWhitespaceIndex = null
+ textNode = @domElementPool.buildText(lineText.substr(startIndex, tagCode))
+ startIndex += tagCode
+ openScopeNode.appendChild(textNode)
+ textNodes.push(textNode)
- if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex
- tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart)
- else
- tokenFirstTrailingWhitespaceIndex = null
+ if startIndex is 0
+ textNode = @domElementPool.buildText(' ')
+ lineNode.appendChild(textNode)
+ textNodes.push(textNode)
- hasIndentGuide =
- @newState.indentGuidesVisible and
- (hasLeadingWhitespace or lineIsWhitespaceOnly)
+ if lineText.endsWith(@presenter.displayLayer.foldCharacter)
+ # Insert a zero-width non-breaking whitespace, so that
+ # LinesYardstick can take the fold-marker::after pseudo-element
+ # into account during measurements when such marker is the last
+ # character on the line.
+ textNode = @domElementPool.buildText(ZERO_WIDTH_NBSP)
+ lineNode.appendChild(textNode)
+ textNodes.push(textNode)
- hasInvisibleCharacters =
- (invisibles?.tab and isHardTab) or
- (invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace))
-
- @appendTokenNodes(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, openScopeNode)
-
- @appendEndOfLineNodes(id, lineNode)
-
- appendTokenNodes: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters, scopeNode) ->
- if isHardTab
- textNode = @domElementPool.buildText(tokenText)
- hardTabNode = @domElementPool.buildElement("span", "hard-tab")
- hardTabNode.classList.add("leading-whitespace") if firstNonWhitespaceIndex?
- hardTabNode.classList.add("trailing-whitespace") if firstTrailingWhitespaceIndex?
- hardTabNode.classList.add("indent-guide") if hasIndentGuide
- hardTabNode.classList.add("invisible-character") if hasInvisibleCharacters
- hardTabNode.appendChild(textNode)
-
- scopeNode.appendChild(hardTabNode)
- @currentLineTextNodes.push(textNode)
- else
- startIndex = 0
- endIndex = tokenText.length
-
- leadingWhitespaceNode = null
- leadingWhitespaceTextNode = null
- trailingWhitespaceNode = null
- trailingWhitespaceTextNode = null
-
- if firstNonWhitespaceIndex?
- leadingWhitespaceTextNode =
- @domElementPool.buildText(tokenText.substring(0, firstNonWhitespaceIndex))
- leadingWhitespaceNode = @domElementPool.buildElement("span", "leading-whitespace")
- leadingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide
- leadingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
- leadingWhitespaceNode.appendChild(leadingWhitespaceTextNode)
-
- startIndex = firstNonWhitespaceIndex
-
- if firstTrailingWhitespaceIndex?
- tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0
-
- trailingWhitespaceTextNode =
- @domElementPool.buildText(tokenText.substring(firstTrailingWhitespaceIndex))
- trailingWhitespaceNode = @domElementPool.buildElement("span", "trailing-whitespace")
- trailingWhitespaceNode.classList.add("indent-guide") if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace
- trailingWhitespaceNode.classList.add("invisible-character") if hasInvisibleCharacters
- trailingWhitespaceNode.appendChild(trailingWhitespaceTextNode)
-
- endIndex = firstTrailingWhitespaceIndex
-
- if leadingWhitespaceNode?
- scopeNode.appendChild(leadingWhitespaceNode)
- @currentLineTextNodes.push(leadingWhitespaceTextNode)
-
- if tokenText.length > MaxTokenLength
- while startIndex < endIndex
- textNode = @domElementPool.buildText(
- @sliceText(tokenText, startIndex, startIndex + MaxTokenLength)
- )
- textSpan = @domElementPool.buildElement("span")
-
- textSpan.appendChild(textNode)
- scopeNode.appendChild(textSpan)
- startIndex += MaxTokenLength
- @currentLineTextNodes.push(textNode)
- else
- textNode = @domElementPool.buildText(@sliceText(tokenText, startIndex, endIndex))
- scopeNode.appendChild(textNode)
- @currentLineTextNodes.push(textNode)
-
- if trailingWhitespaceNode?
- scopeNode.appendChild(trailingWhitespaceNode)
- @currentLineTextNodes.push(trailingWhitespaceTextNode)
-
- sliceText: (tokenText, startIndex, endIndex) ->
- if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length
- tokenText = tokenText.slice(startIndex, endIndex)
- tokenText
-
- appendEndOfLineNodes: (id, lineNode) ->
- {endOfLineInvisibles} = @newTileState.lines[id]
-
- hasInvisibles = false
- if endOfLineInvisibles?
- for invisible in endOfLineInvisibles
- hasInvisibles = true
- invisibleSpan = @domElementPool.buildElement("span", "invisible-character")
- textNode = @domElementPool.buildText(invisible)
- invisibleSpan.appendChild(textNode)
- lineNode.appendChild(invisibleSpan)
-
- @currentLineTextNodes.push(textNode)
-
- hasInvisibles
+ @textNodesByLineId[id] = textNodes
+ lineNode
updateLineNode: (id) ->
oldLineState = @oldTileState.lines[id]
@@ -436,3 +286,9 @@ class LinesTileComponent
textNodesForLineId: (lineId) ->
@textNodesByLineId[lineId].slice()
+
+ lineIdForScreenRow: (screenRow) ->
+ @lineIdsByScreenRow[screenRow]
+
+ textNodesForScreenRow: (screenRow) ->
+ @textNodesByLineId[@lineIdsByScreenRow[screenRow]]?.slice()
diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee
index 2373463af..cfc954cf2 100644
--- a/src/lines-yardstick.coffee
+++ b/src/lines-yardstick.coffee
@@ -1,15 +1,14 @@
-TokenIterator = require './token-iterator'
{Point} = require 'text-buffer'
+{isPairedCharacter} = require './text-utils'
module.exports =
class LinesYardstick
constructor: (@model, @lineNodesProvider, @lineTopIndex, grammarRegistry) ->
- @tokenIterator = new TokenIterator({grammarRegistry})
@rangeForMeasurement = document.createRange()
@invalidateCache()
invalidateCache: ->
- @pixelPositionsByLineIdAndColumn = {}
+ @leftPixelPositionCache = {}
measuredRowForPixelPosition: (pixelPosition) ->
targetTop = pixelPosition.top
@@ -21,61 +20,63 @@ class LinesYardstick
targetLeft = pixelPosition.left
defaultCharWidth = @model.getDefaultCharWidth()
row = @lineTopIndex.rowForPixelPosition(targetTop)
- targetLeft = 0 if targetTop < 0
+ targetLeft = 0 if targetTop < 0 or targetLeft < 0
targetLeft = Infinity if row > @model.getLastScreenRow()
row = Math.min(row, @model.getLastScreenRow())
row = Math.max(0, row)
- line = @model.tokenizedLineForScreenRow(row)
- lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row)
+ lineNode = @lineNodesProvider.lineNodeForScreenRow(row)
+ return Point(row, 0) unless lineNode
- return Point(row, 0) unless lineNode? and line?
+ textNodes = @lineNodesProvider.textNodesForScreenRow(row)
+ lineOffset = lineNode.getBoundingClientRect().left
+ targetLeft += lineOffset
- textNodes = @lineNodesProvider.textNodesForLineIdAndScreenRow(line.id, row)
- column = 0
- previousColumn = 0
- previousLeft = 0
+ textNodeIndex = 0
+ low = 0
+ high = textNodes.length - 1
+ while low <= high
+ mid = low + (high - low >> 1)
+ textNode = textNodes[mid]
+ rangeRect = @clientRectForRange(textNode, 0, textNode.length)
+ if targetLeft < rangeRect.left
+ high = mid - 1
+ textNodeIndex = Math.max(0, mid - 1)
+ else if targetLeft > rangeRect.right
+ low = mid + 1
+ textNodeIndex = Math.min(textNodes.length - 1, mid + 1)
+ else
+ textNodeIndex = mid
+ break
- @tokenIterator.reset(line, false)
- while @tokenIterator.next()
- text = @tokenIterator.getText()
- textIndex = 0
- while textIndex < text.length
- if @tokenIterator.isPairedCharacter()
- char = text
- charLength = 2
- textIndex += 2
+ textNode = textNodes[textNodeIndex]
+ characterIndex = 0
+ low = 0
+ high = textNode.textContent.length - 1
+ while low <= high
+ charIndex = low + (high - low >> 1)
+ if isPairedCharacter(textNode.textContent, charIndex)
+ nextCharIndex = charIndex + 2
+ else
+ nextCharIndex = charIndex + 1
+
+ rangeRect = @clientRectForRange(textNode, charIndex, nextCharIndex)
+ if targetLeft < rangeRect.left
+ high = charIndex - 1
+ characterIndex = Math.max(0, charIndex - 1)
+ else if targetLeft > rangeRect.right
+ low = nextCharIndex
+ characterIndex = Math.min(textNode.textContent.length, nextCharIndex)
+ else
+ if targetLeft <= ((rangeRect.left + rangeRect.right) / 2)
+ characterIndex = charIndex
else
- char = text[textIndex]
- charLength = 1
- textIndex++
+ characterIndex = nextCharIndex
+ break
- unless textNode?
- textNode = textNodes.shift()
- textNodeLength = textNode.textContent.length
- textNodeIndex = 0
- nextTextNodeIndex = textNodeLength
-
- while nextTextNodeIndex <= column
- textNode = textNodes.shift()
- textNodeLength = textNode.textContent.length
- textNodeIndex = nextTextNodeIndex
- nextTextNodeIndex = textNodeIndex + textNodeLength
-
- indexWithinTextNode = column - textNodeIndex
- left = @leftPixelPositionForCharInTextNode(lineNode, textNode, indexWithinTextNode)
- charWidth = left - previousLeft
-
- return Point(row, previousColumn) if targetLeft <= previousLeft + (charWidth / 2)
-
- previousLeft = left
- previousColumn = column
- column += charLength
-
- if targetLeft <= previousLeft + (charWidth / 2)
- Point(row, previousColumn)
- else
- Point(row, column)
+ textNodeStartColumn = 0
+ textNodeStartColumn += textNodes[i].length for i in [0...textNodeIndex] by 1
+ Point(row, textNodeStartColumn + characterIndex)
pixelPositionForScreenPosition: (screenPosition) ->
targetRow = screenPosition.row
@@ -87,76 +88,41 @@ class LinesYardstick
{top, left}
leftPixelPositionForScreenPosition: (row, column) ->
- line = @model.tokenizedLineForScreenRow(row)
- lineNode = @lineNodesProvider.lineNodeForLineIdAndScreenRow(line?.id, row)
+ lineNode = @lineNodesProvider.lineNodeForScreenRow(row)
+ lineId = @lineNodesProvider.lineIdForScreenRow(row)
- return 0 unless line? and lineNode?
+ return 0 unless lineNode?
- if cachedPosition = @pixelPositionsByLineIdAndColumn[line.id]?[column]
+ if cachedPosition = @leftPixelPositionCache[lineId]?[column]
return cachedPosition
- textNodes = @lineNodesProvider.textNodesForLineIdAndScreenRow(line.id, row)
- indexWithinTextNode = null
- charIndex = 0
+ textNodes = @lineNodesProvider.textNodesForScreenRow(row)
+ textNodeStartColumn = 0
- @tokenIterator.reset(line, false)
- while @tokenIterator.next()
- break if foundIndexWithinTextNode?
-
- text = @tokenIterator.getText()
-
- textIndex = 0
- while textIndex < text.length
- if @tokenIterator.isPairedCharacter()
- char = text
- charLength = 2
- textIndex += 2
- else
- char = text[textIndex]
- charLength = 1
- textIndex++
-
- unless textNode?
- textNode = textNodes.shift()
- textNodeLength = textNode.textContent.length
- textNodeIndex = 0
- nextTextNodeIndex = textNodeLength
-
- while nextTextNodeIndex <= charIndex
- textNode = textNodes.shift()
- textNodeLength = textNode.textContent.length
- textNodeIndex = nextTextNodeIndex
- nextTextNodeIndex = textNodeIndex + textNodeLength
-
- if charIndex is column
- foundIndexWithinTextNode = charIndex - textNodeIndex
- break
-
- charIndex += charLength
+ for textNode in textNodes
+ textNodeEndColumn = textNodeStartColumn + textNode.textContent.length
+ if textNodeEndColumn > column
+ indexInTextNode = column - textNodeStartColumn
+ break
+ else
+ textNodeStartColumn = textNodeEndColumn
if textNode?
- foundIndexWithinTextNode ?= textNode.textContent.length
- position = @leftPixelPositionForCharInTextNode(
- lineNode, textNode, foundIndexWithinTextNode
- )
- @pixelPositionsByLineIdAndColumn[line.id] ?= {}
- @pixelPositionsByLineIdAndColumn[line.id][column] = position
- position
+ indexInTextNode ?= textNode.textContent.length
+ lineOffset = lineNode.getBoundingClientRect().left
+ if indexInTextNode is 0
+ leftPixelPosition = @clientRectForRange(textNode, 0, 1).left
+ else
+ leftPixelPosition = @clientRectForRange(textNode, 0, indexInTextNode).right
+ leftPixelPosition -= lineOffset
+
+ @leftPixelPositionCache[lineId] ?= {}
+ @leftPixelPositionCache[lineId][column] = leftPixelPosition
+ leftPixelPosition
else
0
- leftPixelPositionForCharInTextNode: (lineNode, textNode, charIndex) ->
- if charIndex is 0
- width = 0
- else
- @rangeForMeasurement.setStart(textNode, 0)
- @rangeForMeasurement.setEnd(textNode, charIndex)
- width = @rangeForMeasurement.getBoundingClientRect().width
-
- @rangeForMeasurement.setStart(textNode, 0)
- @rangeForMeasurement.setEnd(textNode, textNode.textContent.length)
- left = @rangeForMeasurement.getBoundingClientRect().left
-
- offset = lineNode.getBoundingClientRect().left
-
- left + width - offset
+ clientRectForRange: (textNode, startIndex, endIndex) ->
+ @rangeForMeasurement.setStart(textNode, startIndex)
+ @rangeForMeasurement.setEnd(textNode, endIndex)
+ @rangeForMeasurement.getClientRects()[0] ? @rangeForMeasurement.getBoundingClientRect()
diff --git a/src/marker-observation-window.coffee b/src/marker-observation-window.coffee
index aa7b71f69..ffb92c0ab 100644
--- a/src/marker-observation-window.coffee
+++ b/src/marker-observation-window.coffee
@@ -1,9 +1,9 @@
module.exports =
class MarkerObservationWindow
- constructor: (@displayBuffer, @bufferWindow) ->
+ constructor: (@decorationManager, @bufferWindow) ->
setScreenRange: (range) ->
- @bufferWindow.setRange(@displayBuffer.bufferRangeForScreenRange(range))
+ @bufferWindow.setRange(@decorationManager.bufferRangeForScreenRange(range))
setBufferRange: (range) ->
@bufferWindow.setRange(range)
diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee
index fa78d3cd6..67076dbfa 100644
--- a/src/menu-manager.coffee
+++ b/src/menu-manager.coffee
@@ -1,7 +1,7 @@
path = require 'path'
_ = require 'underscore-plus'
-ipc = require 'ipc'
+{ipcRenderer} = require 'electron'
CSON = require 'season'
fs = require 'fs-plus'
{Disposable} = require 'event-kit'
@@ -191,7 +191,7 @@ class MenuManager
sendToBrowserProcess: (template, keystrokesByCommand) ->
keystrokesByCommand = @filterMultipleKeystroke(keystrokesByCommand)
- ipc.send 'update-application-menu', template, keystrokesByCommand
+ ipcRenderer.send 'update-application-menu', template, keystrokesByCommand
# Get an {Array} of {String} classes for the given element.
classesForElement: (element) ->
diff --git a/src/module-cache.coffee b/src/module-cache.coffee
index e9245cf40..9a2961bf6 100644
--- a/src/module-cache.coffee
+++ b/src/module-cache.coffee
@@ -202,13 +202,13 @@ registerBuiltins = (devMode) ->
atomShellRoot = path.join(process.resourcesPath, 'atom.asar')
- commonRoot = path.join(atomShellRoot, 'common', 'api', 'lib')
- commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'screen', 'shell']
+ commonRoot = path.join(atomShellRoot, 'common', 'api')
+ commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'shell']
for builtin in commonBuiltins
cache.builtins[builtin] = path.join(commonRoot, "#{builtin}.js")
- rendererRoot = path.join(atomShellRoot, 'renderer', 'api', 'lib')
- rendererBuiltins = ['ipc', 'remote']
+ rendererRoot = path.join(atomShellRoot, 'renderer', 'api')
+ rendererBuiltins = ['ipc-renderer', 'remote', 'screen']
for builtin in rendererBuiltins
cache.builtins[builtin] = path.join(rendererRoot, "#{builtin}.js")
diff --git a/src/notification-manager.coffee b/src/notification-manager.coffee
index 46c781c20..3d8b1895c 100644
--- a/src/notification-manager.coffee
+++ b/src/notification-manager.coffee
@@ -3,6 +3,9 @@ Notification = require '../src/notification'
# Public: A notification manager used to create {Notification}s to be shown
# to the user.
+#
+# An instance of this class is always available as the `atom.notifications`
+# global.
module.exports =
class NotificationManager
constructor: ->
diff --git a/src/package-manager.coffee b/src/package-manager.coffee
index 1ecdc5448..8f2924358 100644
--- a/src/package-manager.coffee
+++ b/src/package-manager.coffee
@@ -128,8 +128,12 @@ class PackageManager
# Public: Get the path to the apm command.
#
+ # Uses the value of the `core.apmPath` config setting if it exists.
+ #
# Return a {String} file path to apm.
getApmPath: ->
+ configPath = atom.config.get('core.apmPath')
+ return configPath if configPath
return @apmPath if @apmPath?
commandName = 'apm'
@@ -357,10 +361,14 @@ class PackageManager
packagePaths = @getAvailablePackagePaths()
packagePaths = packagePaths.filter (packagePath) => not @isPackageDisabled(path.basename(packagePath))
packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath)
- @loadPackage(packagePath) for packagePath in packagePaths
+ @config.transact =>
+ @loadPackage(packagePath) for packagePath in packagePaths
+ return
@emitter.emit 'did-load-initial-packages'
loadPackage: (nameOrPath) ->
+ return null if path.basename(nameOrPath)[0].match /^\./ # primarily to skip .git folder
+
return pack if pack = @getLoadedPackage(nameOrPath)
if packagePath = @resolvePackagePath(nameOrPath)
@@ -467,6 +475,14 @@ class PackageManager
return unless hook? and _.isString(hook) and hook.length > 0
@activationHookEmitter.on(hook, callback)
+ serialize: ->
+ for pack in @getActivePackages()
+ @serializePackage(pack)
+ @packageStates
+
+ serializePackage: (pack) ->
+ @setPackageState(pack.name, state) if state = pack.serialize?()
+
# Deactivate all packages
deactivatePackages: ->
@config.transact =>
@@ -478,8 +494,7 @@ class PackageManager
# Deactivate the package with the given name
deactivatePackage: (name) ->
pack = @getLoadedPackage(name)
- if @isPackageActive(name)
- @setPackageState(pack.name, state) if state = pack.serialize?()
+ @serializePackage(pack) if @isPackageActive(pack.name)
pack.deactivate()
delete @activePackages[pack.name]
delete @activatingPackages[pack.name]
@@ -532,11 +547,12 @@ class PackageManager
unless typeof metadata.name is 'string' and metadata.name.length > 0
metadata.name = packageName
+ if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
+ metadata.repository.url = metadata.repository.url.replace(/(^git\+)|(\.git$)/g, '')
+
metadata
normalizePackageMetadata: (metadata) ->
unless metadata?._id
normalizePackageData ?= require 'normalize-package-data'
normalizePackageData(metadata)
- if metadata.repository?.type is 'git' and typeof metadata.repository.url is 'string'
- metadata.repository.url = metadata.repository.url.replace(/^git\+/, '')
diff --git a/src/package.coffee b/src/package.coffee
index 8230ce4e4..94e759947 100644
--- a/src/package.coffee
+++ b/src/package.coffee
@@ -84,7 +84,7 @@ class Package
@loadKeymaps()
@loadMenus()
@loadStylesheets()
- @loadDeserializers()
+ @registerDeserializerMethods()
@configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata()
@settingsPromise = @loadSettings()
if @shouldRequireMainModuleOnLoad() and not @mainModule?
@@ -277,24 +277,24 @@ class Package
@stylesheets = @getStylesheetPaths().map (stylesheetPath) =>
[stylesheetPath, @themeManager.loadStylesheet(stylesheetPath, true)]
- loadDeserializers: ->
+ registerDeserializerMethods: ->
if @metadata.deserializers?
- for name, implementationPath of @metadata.deserializers
- do =>
- deserializePath = path.join(@path, implementationPath)
- deserializeFunction = null
- atom.deserializers.add
- name: name,
- deserialize: =>
- @registerViewProviders()
- deserializeFunction ?= require(deserializePath)
- deserializeFunction.apply(this, arguments)
+ Object.keys(@metadata.deserializers).forEach (deserializerName) =>
+ methodName = @metadata.deserializers[deserializerName]
+ atom.deserializers.add
+ name: deserializerName,
+ deserialize: (state, atomEnvironment) =>
+ @registerViewProviders()
+ @requireMainModule()
+ @mainModule[methodName](state, atomEnvironment)
return
registerViewProviders: ->
if @metadata.viewProviders? and not @registeredViewProviders
- for implementationPath in @metadata.viewProviders
- @viewRegistry.addViewProvider(require(path.join(@path, implementationPath)))
+ @requireMainModule()
+ @metadata.viewProviders.forEach (methodName) =>
+ @viewRegistry.addViewProvider (model) =>
+ @mainModule[methodName](model)
@registeredViewProviders = true
getStylesheetsPath: ->
diff --git a/src/pane-axis-element.coffee b/src/pane-axis-element.coffee
index eaa26a9fe..07439b914 100644
--- a/src/pane-axis-element.coffee
+++ b/src/pane-axis-element.coffee
@@ -2,20 +2,18 @@
PaneResizeHandleElement = require './pane-resize-handle-element'
class PaneAxisElement extends HTMLElement
- createdCallback: ->
- @subscriptions = new CompositeDisposable
+ attachedCallback: ->
+ @subscriptions ?= @subscribeToModel()
+ @childAdded({child, index}) for child, index in @model.getChildren()
detachedCallback: ->
@subscriptions.dispose()
+ @subscriptions = null
+ @childRemoved({child}) for child in @model.getChildren()
initialize: (@model, {@views}) ->
throw new Error("Must pass a views parameter when initializing TextEditorElements") unless @views?
-
- @subscriptions.add @model.onDidAddChild(@childAdded.bind(this))
- @subscriptions.add @model.onDidRemoveChild(@childRemoved.bind(this))
- @subscriptions.add @model.onDidReplaceChild(@childReplaced.bind(this))
- @subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this))
-
+ @subscriptions ?= @subscribeToModel()
@childAdded({child, index}) for child, index in @model.getChildren()
switch @model.getOrientation()
@@ -25,6 +23,14 @@ class PaneAxisElement extends HTMLElement
@classList.add('vertical', 'pane-column')
this
+ subscribeToModel: ->
+ subscriptions = new CompositeDisposable
+ subscriptions.add @model.onDidAddChild(@childAdded.bind(this))
+ subscriptions.add @model.onDidRemoveChild(@childRemoved.bind(this))
+ subscriptions.add @model.onDidReplaceChild(@childReplaced.bind(this))
+ subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this))
+ subscriptions
+
isPaneResizeHandleElement: (element) ->
element?.nodeName.toLowerCase() is 'atom-pane-resize-handle'
diff --git a/src/pane.coffee b/src/pane.coffee
index 0a5cca4c3..add6a365b 100644
--- a/src/pane.coffee
+++ b/src/pane.coffee
@@ -1,5 +1,6 @@
+Grim = require 'grim'
{find, compact, extend, last} = require 'underscore-plus'
-{Emitter} = require 'event-kit'
+{CompositeDisposable, Emitter} = require 'event-kit'
Model = require './model'
PaneAxis = require './pane-axis'
TextEditor = require './text-editor'
@@ -8,6 +9,11 @@ TextEditor = require './text-editor'
# Panes can contain multiple items, one of which is *active* at a given time.
# The view corresponding to the active item is displayed in the interface. In
# the default configuration, tabs are also displayed for each item.
+#
+# Each pane may also contain one *pending* item. When a pending item is added
+# to a pane, it will replace the currently pending item, if any, instead of
+# simply being added. In the default configuration, the text in the tab for
+# pending items is shown in italics.
module.exports =
class Pane extends Model
container: undefined
@@ -15,7 +21,7 @@ class Pane extends Model
focused: false
@deserialize: (state, {deserializers, applicationDelegate, config, notifications}) ->
- {items, activeItemURI, activeItemUri} = state
+ {items, itemStackIndices, activeItemURI, activeItemUri} = state
activeItemURI ?= activeItemUri
state.items = compact(items.map (itemState) -> deserializers.deserialize(itemState))
state.activeItem = find state.items, (item) ->
@@ -37,20 +43,25 @@ class Pane extends Model
} = params
@emitter = new Emitter
- @itemSubscriptions = new WeakMap
+ @subscriptionsPerItem = new WeakMap
@items = []
+ @itemStack = []
@addItems(compact(params?.items ? []))
@setActiveItem(@items[0]) unless @getActiveItem()?
+ @addItemsToStack(params?.itemStackIndices ? [])
@setFlexScale(params?.flexScale ? 1)
serialize: ->
if typeof @activeItem?.getURI is 'function'
activeItemURI = @activeItem.getURI()
+ itemsToBeSerialized = compact(@items.map((item) -> item if typeof item.serialize is 'function'))
+ itemStackIndices = (itemsToBeSerialized.indexOf(item) for item in @itemStack when typeof item.serialize is 'function')
deserializer: 'Pane'
id: @id
- items: compact(@items.map((item) -> item.serialize?()))
+ items: itemsToBeSerialized.map((item) -> item.serialize())
+ itemStackIndices: itemStackIndices
activeItemURI: activeItemURI
focused: @focused
flexScale: @flexScale
@@ -260,8 +271,8 @@ class Pane extends Model
getPanes: -> [this]
unsubscribeFromItem: (item) ->
- @itemSubscriptions.get(item)?.dispose()
- @itemSubscriptions.delete(item)
+ @subscriptionsPerItem.get(item)?.dispose()
+ @subscriptionsPerItem.delete(item)
###
Section: Items
@@ -278,12 +289,30 @@ class Pane extends Model
# Returns a pane item.
getActiveItem: -> @activeItem
- setActiveItem: (activeItem) ->
+ setActiveItem: (activeItem, options) ->
+ {modifyStack} = options if options?
unless activeItem is @activeItem
+ @addItemToStack(activeItem) unless modifyStack is false
@activeItem = activeItem
@emitter.emit 'did-change-active-item', @activeItem
@activeItem
+ # Build the itemStack after deserializing
+ addItemsToStack: (itemStackIndices) ->
+ if @items.length > 0
+ if itemStackIndices.length is 0 or itemStackIndices.length isnt @items.length or itemStackIndices.indexOf(-1) >= 0
+ itemStackIndices = (i for i in [0..@items.length-1])
+ for itemIndex in itemStackIndices
+ @addItemToStack(@items[itemIndex])
+ return
+
+ # Add item (or move item) to the end of the itemStack
+ addItemToStack: (newItem) ->
+ return unless newItem?
+ index = @itemStack.indexOf(newItem)
+ @itemStack.splice(index, 1) unless index is -1
+ @itemStack.push(newItem)
+
# Return an {TextEditor} if the pane item is an {TextEditor}, or null otherwise.
getActiveEditor: ->
@activeItem if @activeItem instanceof TextEditor
@@ -296,6 +325,29 @@ class Pane extends Model
itemAtIndex: (index) ->
@items[index]
+ # Makes the next item in the itemStack active.
+ activateNextRecentlyUsedItem: ->
+ if @items.length > 1
+ @itemStackIndex = @itemStack.length - 1 unless @itemStackIndex?
+ @itemStackIndex = @itemStack.length if @itemStackIndex is 0
+ @itemStackIndex = @itemStackIndex - 1
+ nextRecentlyUsedItem = @itemStack[@itemStackIndex]
+ @setActiveItem(nextRecentlyUsedItem, modifyStack: false)
+
+ # Makes the previous item in the itemStack active.
+ activatePreviousRecentlyUsedItem: ->
+ if @items.length > 1
+ if @itemStackIndex + 1 is @itemStack.length or not @itemStackIndex?
+ @itemStackIndex = -1
+ @itemStackIndex = @itemStackIndex + 1
+ previousRecentlyUsedItem = @itemStack[@itemStackIndex]
+ @setActiveItem(previousRecentlyUsedItem, modifyStack: false)
+
+ # Moves the active item to the end of the itemStack once the ctrl key is lifted
+ moveActiveItemToTopOfStack: ->
+ delete @itemStackIndex
+ @addItemToStack(@activeItem)
+
# Public: Makes the next item active.
activateNextItem: ->
index = @getActiveItemIndex()
@@ -342,43 +394,83 @@ class Pane extends Model
# Public: Make the given item *active*, causing it to be displayed by
# the pane's view.
- activateItem: (item) ->
+ #
+ # * `options` (optional) {Object}
+ # * `pending` (optional) {Boolean} indicating that the item should be added
+ # in a pending state if it does not yet exist in the pane. Existing pending
+ # items in a pane are replaced with new pending items when they are opened.
+ activateItem: (item, options={}) ->
if item?
- if @activeItem?.isPending?()
+ if @getPendingItem() is @activeItem
index = @getActiveItemIndex()
else
index = @getActiveItemIndex() + 1
- @addItem(item, index, false)
+ @addItem(item, extend({}, options, {index: index}))
@setActiveItem(item)
# Public: Add the given item to the pane.
#
# * `item` The item to add. It can be a model with an associated view or a
# view.
- # * `index` (optional) {Number} indicating the index at which to add the item.
- # If omitted, the item is added after the current active item.
+ # * `options` (optional) {Object}
+ # * `index` (optional) {Number} indicating the index at which to add the item.
+ # If omitted, the item is added after the current active item.
+ # * `pending` (optional) {Boolean} indicating that the item should be
+ # added in a pending state. Existing pending items in a pane are replaced with
+ # new pending items when they are opened.
#
# Returns the added item.
- addItem: (item, index=@getActiveItemIndex() + 1, moved=false) ->
+ addItem: (item, options={}) ->
+ # Backward compat with old API:
+ # addItem(item, index=@getActiveItemIndex() + 1)
+ if typeof options is "number"
+ Grim.deprecate("Pane::addItem(item, #{options}) is deprecated in favor of Pane::addItem(item, {index: #{options}})")
+ options = index: options
+
+ index = options.index ? @getActiveItemIndex() + 1
+ moved = options.moved ? false
+ pending = options.pending ? false
+
throw new Error("Pane items must be objects. Attempted to add item #{item}.") unless item? and typeof item is 'object'
throw new Error("Adding a pane item with URI '#{item.getURI?()}' that has already been destroyed") if item.isDestroyed?()
return if item in @items
- if item.isPending?()
- for existingItem, i in @items
- if existingItem.isPending?()
- @destroyItem(existingItem)
- break
-
if typeof item.onDidDestroy is 'function'
- @itemSubscriptions.set item, item.onDidDestroy => @removeItem(item, false)
+ itemSubscriptions = new CompositeDisposable
+ itemSubscriptions.add item.onDidDestroy => @removeItem(item, false)
+ if typeof item.onDidTerminatePendingState is "function"
+ itemSubscriptions.add item.onDidTerminatePendingState =>
+ @clearPendingItem() if @getPendingItem() is item
+ @subscriptionsPerItem.set item, itemSubscriptions
@items.splice(index, 0, item)
+ lastPendingItem = @getPendingItem()
+ replacingPendingItem = lastPendingItem? and not moved
+ @pendingItem = null if replacingPendingItem
+ @setPendingItem(item) if pending
+
@emitter.emit 'did-add-item', {item, index, moved}
+ @destroyItem(lastPendingItem) if replacingPendingItem
@setActiveItem(item) unless @getActiveItem()?
item
+ setPendingItem: (item) =>
+ if @pendingItem isnt item
+ mostRecentPendingItem = @pendingItem
+ @pendingItem = item
+ if mostRecentPendingItem?
+ @emitter.emit 'item-did-terminate-pending-state', mostRecentPendingItem
+
+ getPendingItem: =>
+ @pendingItem or null
+
+ clearPendingItem: =>
+ @setPendingItem(null)
+
+ onItemDidTerminatePendingState: (callback) =>
+ @emitter.on 'item-did-terminate-pending-state', callback
+
# Public: Add the given items to the pane.
#
# * `items` An {Array} of items to add. Items can be views or models with
@@ -390,13 +482,14 @@ class Pane extends Model
# Returns an {Array} of added items.
addItems: (items, index=@getActiveItemIndex() + 1) ->
items = items.filter (item) => not (item in @items)
- @addItem(item, index + i, false) for item, i in items
+ @addItem(item, {index: index + i}) for item, i in items
items
removeItem: (item, moved) ->
index = @items.indexOf(item)
return if index is -1
-
+ @pendingItem = null if @getPendingItem() is item
+ @removeItemFromStack(item)
@emitter.emit 'will-remove-item', {item, index, destroyed: not moved, moved}
@unsubscribeFromItem(item)
@@ -412,6 +505,14 @@ class Pane extends Model
@container?.didDestroyPaneItem({item, index, pane: this}) unless moved
@destroy() if @items.length is 0 and @config.get('core.destroyEmptyPanes')
+ # Remove the given item from the itemStack.
+ #
+ # * `item` The item to remove.
+ # * `index` {Number} indicating the index to which to remove the item from the itemStack.
+ removeItemFromStack: (item) ->
+ index = @itemStack.indexOf(item)
+ @itemStack.splice(index, 1) unless index is -1
+
# Public: Move the given item to the given index.
#
# * `item` The item to move.
@@ -430,7 +531,7 @@ class Pane extends Model
# given pane.
moveItemToPane: (item, pane, index) ->
@removeItem(item, true)
- pane.addItem(item, index, true)
+ pane.addItem(item, {index: index, moved: true})
# Public: Destroy the active item and activate the next item.
destroyActiveItem: ->
@@ -476,15 +577,23 @@ class Pane extends Model
else
return true
- chosen = @applicationDelegate.confirm
- message: "'#{item.getTitle?() ? uri}' has changes, do you want to save them?"
- detailedMessage: "Your changes will be lost if you close this item without saving."
- buttons: ["Save", "Cancel", "Don't Save"]
+ saveDialog = (saveButtonText, saveFn, message) =>
+ chosen = @applicationDelegate.confirm
+ message: message
+ detailedMessage: "Your changes will be lost if you close this item without saving."
+ buttons: [saveButtonText, "Cancel", "Don't save"]
+ switch chosen
+ when 0 then saveFn(item, saveError)
+ when 1 then false
+ when 2 then true
- switch chosen
- when 0 then @saveItem(item, -> true)
- when 1 then false
- when 2 then true
+ saveError = (error) =>
+ if error
+ saveDialog("Save as", @saveItemAs, "'#{item.getTitle?() ? uri}' could not be saved.\nError: #{@getMessageForErrorCode(error.code)}")
+ else
+ true
+
+ saveDialog("Save", @saveItem, "'#{item.getTitle?() ? uri}' has changes, do you want to save them?")
# Public: Save the active item.
saveActiveItem: (nextAction) ->
@@ -501,9 +610,11 @@ class Pane extends Model
# Public: Save the given item.
#
# * `item` The item to save.
- # * `nextAction` (optional) {Function} which will be called after the item is
- # successfully saved.
- saveItem: (item, nextAction) ->
+ # * `nextAction` (optional) {Function} which will be called with no argument
+ # after the item is successfully saved, or with the error if it failed.
+ # The return value will be that of `nextAction` or `undefined` if it was not
+ # provided
+ saveItem: (item, nextAction) =>
if typeof item?.getURI is 'function'
itemURI = item.getURI()
else if typeof item?.getUri is 'function'
@@ -512,9 +623,12 @@ class Pane extends Model
if itemURI?
try
item.save?()
+ nextAction?()
catch error
- @handleSaveError(error, item)
- nextAction?()
+ if nextAction
+ nextAction(error)
+ else
+ @handleSaveError(error, item)
else
@saveItemAs(item, nextAction)
@@ -522,9 +636,11 @@ class Pane extends Model
# path they select.
#
# * `item` The item to save.
- # * `nextAction` (optional) {Function} which will be called after the item is
- # successfully saved.
- saveItemAs: (item, nextAction) ->
+ # * `nextAction` (optional) {Function} which will be called with no argument
+ # after the item is successfully saved, or with the error if it failed.
+ # The return value will be that of `nextAction` or `undefined` if it was not
+ # provided
+ saveItemAs: (item, nextAction) =>
return unless item?.saveAs?
saveOptions = item.getSaveDialogOptions?() ? {}
@@ -533,9 +649,12 @@ class Pane extends Model
if newItemPath
try
item.saveAs(newItemPath)
+ nextAction?()
catch error
- @handleSaveError(error, item)
- nextAction?()
+ if nextAction
+ nextAction(error)
+ else
+ @handleSaveError(error, item)
# Public: Save all items.
saveItems: ->
@@ -661,7 +780,7 @@ class Pane extends Model
@parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this], @flexScale}))
@setFlexScale(1)
- newPane = new Pane(extend({@applicationDelegate, @deserializerManager, @config}, params))
+ newPane = new Pane(extend({@applicationDelegate, @notificationManager, @deserializerManager, @config}, params))
switch side
when 'before' then @parent.insertChildBefore(this, newPane)
when 'after' then @parent.insertChildAfter(this, newPane)
@@ -713,7 +832,7 @@ class Pane extends Model
if @parent.orientation is 'vertical'
bottommostSibling = last(@parent.children)
if bottommostSibling instanceof PaneAxis
- @splitRight()
+ @splitDown()
else
bottommostSibling
else
diff --git a/src/project.coffee b/src/project.coffee
index 008d81e3e..bf64753cf 100644
--- a/src/project.coffee
+++ b/src/project.coffee
@@ -54,8 +54,9 @@ class Project extends Model
Section: Serialization
###
- deserialize: (state, deserializerManager) ->
+ deserialize: (state) ->
state.paths = [state.path] if state.path? # backward compatibility
+ state.paths = state.paths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
@buffers = _.compact state.buffers.map (bufferState) ->
# Check that buffer's file path is accessible
@@ -65,15 +66,15 @@ class Project extends Model
fs.closeSync(fs.openSync(bufferState.filePath, 'r'))
catch error
return unless error.code is 'ENOENT'
- deserializerManager.deserialize(bufferState)
+ TextBuffer.deserialize(bufferState)
@subscribeToBuffer(buffer) for buffer in @buffers
@setPaths(state.paths)
- serialize: ->
+ serialize: (options={}) ->
deserializer: 'Project'
paths: @getPaths()
- buffers: _.compact(@buffers.map (buffer) -> buffer.serialize() if buffer.isRetained())
+ buffers: _.compact(@buffers.map (buffer) -> buffer.serialize({markerLayers: options.isUnloading is true}) if buffer.isRetained())
###
Section: Event Subscription
@@ -390,6 +391,9 @@ class Project extends Model
subscribeToBuffer: (buffer) ->
buffer.onDidDestroy => @removeBuffer(buffer)
+ buffer.onDidChangePath =>
+ unless @getPaths().length > 0
+ @setPaths([path.dirname(buffer.getPath())])
buffer.onWillThrowWatchError ({error, handle}) =>
handle()
@notificationManager.addWarning """
diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee
index 32c74569b..1fff6f633 100644
--- a/src/register-default-commands.coffee
+++ b/src/register-default-commands.coffee
@@ -1,7 +1,10 @@
-ipc = require 'ipc'
+{ipcRenderer} = require 'electron'
-module.exports = ({commandRegistry, commandInstaller, config}) ->
+module.exports = ({commandRegistry, commandInstaller, config, notificationManager, project, clipboard}) ->
commandRegistry.add 'atom-workspace',
+ 'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem()
+ 'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem()
+ 'pane:move-active-item-to-top-of-stack': -> @getModel().getActivePane().moveActiveItemToTopOfStack()
'pane:show-next-item': -> @getModel().getActivePane().activateNextItem()
'pane:show-previous-item': -> @getModel().getActivePane().activatePreviousItem()
'pane:show-item-1': -> @getModel().getActivePane().activateItemAtIndex(0)
@@ -18,30 +21,36 @@ module.exports = ({commandRegistry, commandInstaller, config}) ->
'window:increase-font-size': -> @getModel().increaseFontSize()
'window:decrease-font-size': -> @getModel().decreaseFontSize()
'window:reset-font-size': -> @getModel().resetFontSize()
- 'application:about': -> ipc.send('command', 'application:about')
- 'application:show-preferences': -> ipc.send('command', 'application:show-settings')
- 'application:show-settings': -> ipc.send('command', 'application:show-settings')
- 'application:quit': -> ipc.send('command', 'application:quit')
- 'application:hide': -> ipc.send('command', 'application:hide')
- 'application:hide-other-applications': -> ipc.send('command', 'application:hide-other-applications')
- 'application:install-update': -> ipc.send('command', 'application:install-update')
- 'application:unhide-all-applications': -> ipc.send('command', 'application:unhide-all-applications')
- 'application:new-window': -> ipc.send('command', 'application:new-window')
- 'application:new-file': -> ipc.send('command', 'application:new-file')
- 'application:open': -> ipc.send('command', 'application:open')
- 'application:open-file': -> ipc.send('command', 'application:open-file')
- 'application:open-folder': -> ipc.send('command', 'application:open-folder')
- 'application:open-dev': -> ipc.send('command', 'application:open-dev')
- 'application:open-safe': -> ipc.send('command', 'application:open-safe')
+ 'application:about': -> ipcRenderer.send('command', 'application:about')
+ 'application:show-preferences': -> ipcRenderer.send('command', 'application:show-settings')
+ 'application:show-settings': -> ipcRenderer.send('command', 'application:show-settings')
+ 'application:quit': -> ipcRenderer.send('command', 'application:quit')
+ 'application:hide': -> ipcRenderer.send('command', 'application:hide')
+ 'application:hide-other-applications': -> ipcRenderer.send('command', 'application:hide-other-applications')
+ 'application:install-update': -> ipcRenderer.send('command', 'application:install-update')
+ 'application:unhide-all-applications': -> ipcRenderer.send('command', 'application:unhide-all-applications')
+ 'application:new-window': -> ipcRenderer.send('command', 'application:new-window')
+ 'application:new-file': -> ipcRenderer.send('command', 'application:new-file')
+ 'application:open': ->
+ defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0]
+ ipcRenderer.send('open-command', 'application:open', defaultPath)
+ 'application:open-file': ->
+ defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0]
+ ipcRenderer.send('open-command', 'application:open-file', defaultPath)
+ 'application:open-folder': ->
+ defaultPath = atom.workspace.getActiveTextEditor()?.getPath() ? atom.project.getPaths()?[0]
+ ipcRenderer.send('open-command', 'application:open-folder', defaultPath)
+ 'application:open-dev': -> ipcRenderer.send('command', 'application:open-dev')
+ 'application:open-safe': -> ipcRenderer.send('command', 'application:open-safe')
'application:add-project-folder': -> atom.addProjectFolder()
- 'application:minimize': -> ipc.send('command', 'application:minimize')
- 'application:zoom': -> ipc.send('command', 'application:zoom')
- 'application:bring-all-windows-to-front': -> ipc.send('command', 'application:bring-all-windows-to-front')
- 'application:open-your-config': -> ipc.send('command', 'application:open-your-config')
- 'application:open-your-init-script': -> ipc.send('command', 'application:open-your-init-script')
- 'application:open-your-keymap': -> ipc.send('command', 'application:open-your-keymap')
- 'application:open-your-snippets': -> ipc.send('command', 'application:open-your-snippets')
- 'application:open-your-stylesheet': -> ipc.send('command', 'application:open-your-stylesheet')
+ 'application:minimize': -> ipcRenderer.send('command', 'application:minimize')
+ 'application:zoom': -> ipcRenderer.send('command', 'application:zoom')
+ 'application:bring-all-windows-to-front': -> ipcRenderer.send('command', 'application:bring-all-windows-to-front')
+ 'application:open-your-config': -> ipcRenderer.send('command', 'application:open-your-config')
+ 'application:open-your-init-script': -> ipcRenderer.send('command', 'application:open-your-init-script')
+ 'application:open-your-keymap': -> ipcRenderer.send('command', 'application:open-your-keymap')
+ 'application:open-your-snippets': -> ipcRenderer.send('command', 'application:open-your-snippets')
+ 'application:open-your-stylesheet': -> ipcRenderer.send('command', 'application:open-your-stylesheet')
'application:open-license': -> @getModel().openLicense()
'window:run-package-specs': -> @runPackageSpecs()
'window:focus-next-pane': -> @getModel().activateNextPane()
@@ -184,9 +193,9 @@ module.exports = ({commandRegistry, commandInstaller, config}) ->
'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6)
'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7)
'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8)
- 'editor:log-cursor-scope': -> @logCursorScope()
- 'editor:copy-path': -> @copyPathToClipboard(false)
- 'editor:copy-project-path': -> @copyPathToClipboard(true)
+ 'editor:log-cursor-scope': -> showCursorScope(@getCursorScope(), notificationManager)
+ 'editor:copy-path': -> copyPathToClipboard(this, project, clipboard, false)
+ 'editor:copy-project-path': -> copyPathToClipboard(this, project, clipboard, true)
'editor:toggle-indent-guide': -> config.set('editor.showIndentGuide', not config.get('editor.showIndentGuide'))
'editor:toggle-line-numbers': -> config.set('editor.showLineNumbers', not config.get('editor.showLineNumbers'))
'editor:scroll-to-cursor': -> @scrollToCursorPosition()
@@ -201,9 +210,11 @@ module.exports = ({commandRegistry, commandInstaller, config}) ->
'editor:newline-below': -> @insertNewlineBelow()
'editor:newline-above': -> @insertNewlineAbove()
'editor:toggle-line-comments': -> @toggleLineCommentsInSelection()
- 'editor:checkout-head-revision': -> @checkoutHeadRevision()
+ 'editor:checkout-head-revision': -> atom.workspace.checkoutHeadRevision(this)
'editor:move-line-up': -> @moveLineUp()
'editor:move-line-down': -> @moveLineDown()
+ 'editor:move-selection-left': -> @moveSelectionLeft()
+ 'editor:move-selection-right': -> @moveSelectionRight()
'editor:duplicate-lines': -> @duplicateLines()
'editor:join-lines': -> @joinLines()
)
@@ -227,3 +238,15 @@ stopEventPropagationAndGroupUndo = (config, commandListeners) ->
model.transact config.get('editor.undoGroupingInterval'), ->
commandListener.call(model, event)
newCommandListeners
+
+showCursorScope = (descriptor, notificationManager) ->
+ list = descriptor.scopes.toString().split(',')
+ list = list.map (item) -> "* #{item}"
+ content = "Scopes at Cursor\n#{list.join('\n')}"
+
+ notificationManager.addInfo(content, dismissable: true)
+
+copyPathToClipboard = (editor, project, clipboard, relative) ->
+ if filePath = editor.getPath()
+ filePath = project.relativize(filePath) if relative
+ clipboard.write(filePath)
diff --git a/src/repository-status-handler.coffee b/src/repository-status-handler.coffee
index 2fda9a335..adae7bc4f 100644
--- a/src/repository-status-handler.coffee
+++ b/src/repository-status-handler.coffee
@@ -5,32 +5,15 @@ module.exports = (repoPath, paths = []) ->
repo = Git.open(repoPath)
upstream = {}
- statuses = {}
submodules = {}
- branch = null
if repo?
- # Statuses in main repo
- workingDirectoryPath = repo.getWorkingDirectory()
- repoStatus = (if paths.length > 0 then repo.getStatusForPaths(paths) else repo.getStatus())
- for filePath, status of repoStatus
- statuses[filePath] = status
-
- # Statuses in submodules
for submodulePath, submoduleRepo of repo.submodules
submodules[submodulePath] =
branch: submoduleRepo.getHead()
upstream: submoduleRepo.getAheadBehindCount()
- workingDirectoryPath = submoduleRepo.getWorkingDirectory()
- for filePath, status of submoduleRepo.getStatus()
- absolutePath = path.join(workingDirectoryPath, filePath)
- # Make path relative to parent repository
- relativePath = repo.relativize(absolutePath)
- statuses[relativePath] = status
-
upstream = repo.getAheadBehindCount()
- branch = repo.getHead()
repo.release()
- {statuses, upstream, branch, submodules}
+ {upstream, submodules}
diff --git a/src/safe-clipboard.coffee b/src/safe-clipboard.coffee
index 8301f9d54..1f91803e2 100644
--- a/src/safe-clipboard.coffee
+++ b/src/safe-clipboard.coffee
@@ -1,6 +1,6 @@
# Using clipboard in renderer process is not safe on Linux.
module.exports =
if process.platform is 'linux' and process.type is 'renderer'
- require('remote').require('clipboard')
+ require('electron').remote.clipboard
else
- require('clipboard')
+ require('electron').clipboard
diff --git a/src/selection.coffee b/src/selection.coffee
index 2ba66ebb0..7ecbb3fbc 100644
--- a/src/selection.coffee
+++ b/src/selection.coffee
@@ -87,7 +87,7 @@ class Selection extends Model
setBufferRange: (bufferRange, options={}) ->
bufferRange = Range.fromObject(bufferRange)
options.reversed ?= @isReversed()
- @editor.destroyFoldsContainingBufferRange(bufferRange) unless options.preserveFolds
+ @editor.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
@modifySelection =>
needsFlash = options.flash
delete options.flash if options.flash?
@@ -174,7 +174,7 @@ class Selection extends Model
# range. Defaults to `true` if this is the most recently added selection,
# `false` otherwise.
clear: (options) ->
- @marker.setProperties(goalScreenRange: null)
+ @goalScreenRange = null
@marker.clearTail() unless @retainSelection
@autoscroll() if options?.autoscroll ? @isLastSelection()
@finalize()
@@ -365,7 +365,6 @@ class Selection extends Model
# * `undo` if `skip`, skips the undo stack for this operation.
insertText: (text, options={}) ->
oldBufferRange = @getBufferRange()
- @editor.unfoldBufferRow(oldBufferRange.end.row)
wasReversed = @isReversed()
@clear()
@@ -378,7 +377,8 @@ class Selection extends Model
indentAdjustment = @editor.indentLevelForLine(precedingText) - options.indentBasis
@adjustIndent(remainingLines, indentAdjustment)
- if options.autoIndent and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0
+ textIsAutoIndentable = text is '\n' or text is '\r\n' or NonWhitespaceRegExp.test(text)
+ if options.autoIndent and textIsAutoIndentable and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0
autoIndentFirstLine = true
firstLine = precedingText + firstInsertedLine
desiredIndentLevel = @editor.languageMode.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine)
@@ -393,7 +393,7 @@ class Selection extends Model
if options.select
@setBufferRange(newBufferRange, reversed: wasReversed)
else
- @cursor.setBufferPosition(newBufferRange.end, clip: 'forward') if wasReversed
+ @cursor.setBufferPosition(newBufferRange.end) if wasReversed
if autoIndentFirstLine
@editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel)
@@ -410,7 +410,7 @@ class Selection extends Model
# Public: Removes the first character before the selection if the selection
# is empty otherwise it deletes the selection.
backspace: ->
- @selectLeft() if @isEmpty() and not @editor.isFoldedAtScreenRow(@cursor.getScreenRow())
+ @selectLeft() if @isEmpty()
@deleteSelectedText()
# Public: Removes the selection or, if nothing is selected, then all
@@ -445,11 +445,7 @@ class Selection extends Model
# Public: Removes the selection or the next character after the start of the
# selection if the selection is empty.
delete: ->
- if @isEmpty()
- if @cursor.isAtEndOfLine() and fold = @editor.largestFoldStartingAtScreenRow(@cursor.getScreenRow() + 1)
- @selectToBufferPosition(fold.getBufferRange().end)
- else
- @selectRight()
+ @selectRight() if @isEmpty()
@deleteSelectedText()
# Public: If the selection is empty, removes all text from the cursor to the
@@ -482,8 +478,6 @@ class Selection extends Model
# Public: Removes only the selected text.
deleteSelectedText: ->
bufferRange = @getBufferRange()
- if bufferRange.isEmpty() and fold = @editor.largestFoldContainingBufferRow(bufferRange.start.row)
- bufferRange = bufferRange.union(fold.getBufferRange(includeNewline: true))
@editor.buffer.delete(bufferRange) unless bufferRange.isEmpty()
@cursor?.setBufferPosition(bufferRange.start)
@@ -515,7 +509,7 @@ class Selection extends Model
if selectedRange.isEmpty()
return if selectedRange.start.row is @editor.buffer.getLastRow()
else
- joinMarker = @editor.markBufferRange(selectedRange, invalidationStrategy: 'never')
+ joinMarker = @editor.markBufferRange(selectedRange, invalidate: 'never')
rowCount = Math.max(1, selectedRange.getRowCount() - 1)
for row in [0...rowCount]
@@ -634,8 +628,9 @@ class Selection extends Model
# Public: Creates a fold containing the current selection.
fold: ->
range = @getBufferRange()
- @editor.createFold(range.start.row, range.end.row)
- @cursor.setBufferPosition([range.end.row + 1, 0])
+ unless range.isEmpty()
+ @editor.foldBufferRange(range)
+ @cursor.setBufferPosition(range.end)
# Private: Increase the indentation level of the given text by given number
# of levels. Leaves the first line unchanged.
@@ -689,7 +684,7 @@ class Selection extends Model
# Public: Moves the selection down one row.
addSelectionBelow: ->
- range = (@getGoalScreenRange() ? @getScreenRange()).copy()
+ range = @getGoalScreenRange().copy()
nextRow = range.end.row + 1
for row in [nextRow..@editor.getLastScreenRow()]
@@ -702,14 +697,15 @@ class Selection extends Model
else
continue if clippedRange.isEmpty()
- @editor.addSelectionForScreenRange(clippedRange, goalScreenRange: range)
+ selection = @editor.addSelectionForScreenRange(clippedRange)
+ selection.setGoalScreenRange(range)
break
return
# Public: Moves the selection up one row.
addSelectionAbove: ->
- range = (@getGoalScreenRange() ? @getScreenRange()).copy()
+ range = @getGoalScreenRange().copy()
previousRow = range.end.row - 1
for row in [previousRow..0]
@@ -722,7 +718,8 @@ class Selection extends Model
else
continue if clippedRange.isEmpty()
- @editor.addSelectionForScreenRange(clippedRange, goalScreenRange: range)
+ selection = @editor.addSelectionForScreenRange(clippedRange)
+ selection.setGoalScreenRange(range)
break
return
@@ -755,12 +752,18 @@ class Selection extends Model
#
# * `otherSelection` A {Selection} to compare against
compare: (otherSelection) ->
- @getBufferRange().compare(otherSelection.getBufferRange())
+ @marker.compare(otherSelection.marker)
###
Section: Private Utilities
###
+ setGoalScreenRange: (range) ->
+ @goalScreenRange = Range.fromObject(range)
+
+ getGoalScreenRange: ->
+ @goalScreenRange ? @getScreenRange()
+
markerDidChange: (e) ->
{oldHeadBufferPosition, oldTailBufferPosition, newHeadBufferPosition} = e
{oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e
@@ -810,11 +813,11 @@ class Selection extends Model
@wordwise = false
@linewise = false
- autoscroll: ->
+ autoscroll: (options) ->
if @marker.hasTail()
- @editor.scrollToScreenRange(@getScreenRange(), reversed: @isReversed())
+ @editor.scrollToScreenRange(@getScreenRange(), Object.assign({reversed: @isReversed()}, options))
else
- @cursor.autoscroll()
+ @cursor.autoscroll(options)
clearAutoscroll: ->
@@ -831,7 +834,3 @@ class Selection extends Model
# Returns a {Point} representing the new tail position.
plantTail: ->
@marker.plantTail()
-
- getGoalScreenRange: ->
- if goalScreenRange = @marker.getProperties().goalScreenRange
- Range.fromObject(goalScreenRange)
diff --git a/src/state-store.js b/src/state-store.js
new file mode 100644
index 000000000..a2d3b476b
--- /dev/null
+++ b/src/state-store.js
@@ -0,0 +1,95 @@
+'use strict'
+
+module.exports =
+class StateStore {
+ constructor (databaseName, version) {
+ this.dbPromise = new Promise((resolve) => {
+ let dbOpenRequest = indexedDB.open(databaseName, version)
+ dbOpenRequest.onupgradeneeded = (event) => {
+ let db = event.target.result
+ db.createObjectStore('states')
+ }
+ dbOpenRequest.onsuccess = () => {
+ resolve(dbOpenRequest.result)
+ }
+ dbOpenRequest.onerror = (error) => {
+ console.error('Could not connect to indexedDB', error)
+ resolve(null)
+ }
+ })
+ }
+
+ connect () {
+ return this.dbPromise.then(db => !!db)
+ }
+
+ save (key, value) {
+ return new Promise((resolve, reject) => {
+ this.dbPromise.then(db => {
+ if (db == null) resolve()
+
+ var request = db.transaction(['states'], 'readwrite')
+ .objectStore('states')
+ .put({value: value, storedAt: new Date().toString()}, key)
+
+ request.onsuccess = resolve
+ request.onerror = reject
+ })
+ })
+ }
+
+ load (key) {
+ return this.dbPromise.then(db => {
+ if (!db) return
+
+ return new Promise((resolve, reject) => {
+ var request = db.transaction(['states'])
+ .objectStore('states')
+ .get(key)
+
+ request.onsuccess = (event) => {
+ let result = event.target.result
+ if (result && !result.isJSON) {
+ resolve(result.value)
+ } else {
+ resolve(null)
+ }
+ }
+
+ request.onerror = (event) => reject(event)
+ })
+ })
+ }
+
+ clear () {
+ return this.dbPromise.then(db => {
+ if (!db) return
+
+ return new Promise((resolve, reject) => {
+ var request = db.transaction(['states'], 'readwrite')
+ .objectStore('states')
+ .clear()
+
+ request.onsuccess = resolve
+ request.onerror = reject
+ })
+ })
+ }
+
+ count () {
+ return this.dbPromise.then(db => {
+ if (!db) return
+
+ return new Promise((resolve, reject) => {
+ var request = db.transaction(['states'])
+ .objectStore('states')
+ .count()
+
+ request.onsuccess = () => {
+ resolve(request.result)
+ }
+ request.onerror = reject
+ })
+ })
+ }
+}
diff --git a/src/storage-folder.coffee b/src/storage-folder.coffee
index da8af3f2e..280eb8b5c 100644
--- a/src/storage-folder.coffee
+++ b/src/storage-folder.coffee
@@ -6,7 +6,15 @@ class StorageFolder
constructor: (containingPath) ->
@path = path.join(containingPath, "storage") if containingPath?
- store: (name, object) ->
+ clear: ->
+ return unless @path?
+
+ try
+ fs.removeSync(@path)
+ catch error
+ console.warn "Error deleting #{@path}", error.stack, error
+
+ storeSync: (name, object) ->
return unless @path?
fs.writeFileSync(@pathForKey(name), JSON.stringify(object), 'utf8')
diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee
index 4a51badbd..048033a20 100644
--- a/src/text-editor-component.coffee
+++ b/src/text-editor-component.coffee
@@ -2,7 +2,7 @@ _ = require 'underscore-plus'
scrollbarStyle = require 'scrollbar-style'
{Range, Point} = require 'text-buffer'
{CompositeDisposable} = require 'event-kit'
-ipc = require 'ipc'
+{ipcRenderer} = require 'electron'
TextEditorPresenter = require './text-editor-presenter'
GutterContainerComponent = require './gutter-container-component'
@@ -43,7 +43,7 @@ class TextEditorComponent
@assert domNode?, "TextEditorComponent::domNode was set to null."
@domNodeValue = domNode
- constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars}) ->
+ constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars, scrollPastEnd}) ->
@tileSize = tileSize if tileSize?
@disposables = new CompositeDisposable
@@ -61,6 +61,7 @@ class TextEditorComponent
stoppedScrollingDelay: 200
config: @config
lineTopIndex: lineTopIndex
+ scrollPastEnd: scrollPastEnd
@presenter.onDidUpdateState(@requestUpdate)
@@ -246,9 +247,50 @@ class TextEditorComponent
@scrollViewNode.addEventListener 'mousedown', @onMouseDown
@scrollViewNode.addEventListener 'scroll', @onScrollViewScroll
+ @detectAccentedCharacterMenu()
@listenForIMEEvents()
@trackSelectionClipboard() if process.platform is 'linux'
+ detectAccentedCharacterMenu: ->
+ # We need to get clever to detect when the accented character menu is
+ # opened on OS X. Usually, every keydown event that could cause input is
+ # followed by a corresponding keypress. However, pressing and holding
+ # long enough to open the accented character menu causes additional keydown
+ # events to fire that aren't followed by their own keypress and textInput
+ # events.
+ #
+ # Therefore, we assume the accented character menu has been deployed if,
+ # before observing any keyup event, we observe events in the following
+ # sequence:
+ #
+ # keydown(keyCode: X), keypress, keydown(keyCode: X)
+ #
+ # The keyCode X must be the same in the keydown events that bracket the
+ # keypress, meaning we're *holding* the _same_ key we intially pressed.
+ # Got that?
+ lastKeydown = null
+ lastKeydownBeforeKeypress = null
+
+ @domNode.addEventListener 'keydown', (event) =>
+ if lastKeydownBeforeKeypress
+ if lastKeydownBeforeKeypress.keyCode is event.keyCode
+ @openedAccentedCharacterMenu = true
+ lastKeydownBeforeKeypress = null
+ else
+ lastKeydown = event
+
+ @domNode.addEventListener 'keypress', =>
+ lastKeydownBeforeKeypress = lastKeydown
+ lastKeydown = null
+
+ # This cancels the accented character behavior if we type a key normally
+ # with the menu open.
+ @openedAccentedCharacterMenu = false
+
+ @domNode.addEventListener 'keyup', ->
+ lastKeydownBeforeKeypress = null
+ lastKeydown = null
+
listenForIMEEvents: ->
# The IME composition events work like this:
#
@@ -265,6 +307,9 @@ class TextEditorComponent
checkpoint = null
@domNode.addEventListener 'compositionstart', =>
+ if @openedAccentedCharacterMenu
+ @editor.selectLeft()
+ @openedAccentedCharacterMenu = false
checkpoint = @editor.createCheckpoint()
@domNode.addEventListener 'compositionupdate', (event) =>
@editor.insertText(event.data, select: true)
@@ -279,10 +324,10 @@ class TextEditorComponent
writeSelectedTextToSelectionClipboard = =>
return if @editor.isDestroyed()
if selectedText = @editor.getSelectedText()
- # This uses ipc.send instead of clipboard.writeText because
- # clipboard.writeText is a sync ipc call on Linux and that
+ # This uses ipcRenderer.send instead of clipboard.writeText because
+ # clipboard.writeText is a sync ipcRenderer call on Linux and that
# will slow down selections.
- ipc.send('write-text-to-selection-clipboard', selectedText)
+ ipcRenderer.send('write-text-to-selection-clipboard', selectedText)
@disposables.add @editor.onDidChangeSelectionRange ->
clearTimeout(timeoutId)
timeoutId = setTimeout(writeSelectedTextToSelectionClipboard)
@@ -320,24 +365,21 @@ class TextEditorComponent
onTextInput: (event) =>
event.stopPropagation()
-
- # If we prevent the insertion of a space character, then the browser
- # interprets the spacebar keypress as a page-down command.
- event.preventDefault() unless event.data is ' '
+ event.preventDefault()
return unless @isInputEnabled()
- inputNode = event.target
+ # Workaround of the accented character suggestion feature in OS X.
+ # This will only occur when the user is not composing in IME mode.
+ # When the user selects a modified character from the OSX menu, `textInput`
+ # will occur twice, once for the initial character, and once for the
+ # modified character. However, only a single keypress will have fired. If
+ # this is the case, select backward to replace the original character.
+ if @openedAccentedCharacterMenu
+ @editor.selectLeft()
+ @openedAccentedCharacterMenu = false
- # Work around of the accented character suggestion feature in OS X.
- # Text input fires before a character is inserted, and if the browser is
- # replacing the previous un-accented character with an accented variant, it
- # will select backward over it.
- selectedLength = inputNode.selectionEnd - inputNode.selectionStart
- @editor.selectLeft() if selectedLength is 1
-
- insertedRange = @editor.insertText(event.data, groupUndo: true)
- inputNode.value = event.data if insertedRange
+ @editor.insertText(event.data, groupUndo: true)
onVerticalScroll: (scrollTop) =>
return if @updateRequested or scrollTop is @presenter.getScrollTop()
@@ -449,10 +491,10 @@ class TextEditorComponent
screenPosition = Point.fromObject(screenPosition)
screenPosition = @editor.clipScreenPosition(screenPosition) if clip
- unless @presenter.isRowVisible(screenPosition.row)
+ unless @presenter.isRowRendered(screenPosition.row)
@presenter.setScreenRowsToMeasure([screenPosition.row])
- unless @linesComponent.lineNodeForLineIdAndScreenRow(@presenter.lineIdForScreenRow(screenPosition.row), screenPosition.row)?
+ unless @linesComponent.lineNodeForScreenRow(screenPosition.row)?
@updateSyncPreMeasurement()
pixelPosition = @linesYardstick.pixelPositionForScreenPosition(screenPosition)
@@ -461,7 +503,7 @@ class TextEditorComponent
screenPositionForPixelPosition: (pixelPosition) ->
row = @linesYardstick.measuredRowForPixelPosition(pixelPosition)
- if row? and not @presenter.isRowVisible(row)
+ if row? and not @presenter.isRowRendered(row)
@presenter.setScreenRowsToMeasure([row])
@updateSyncPreMeasurement()
@@ -471,9 +513,9 @@ class TextEditorComponent
pixelRectForScreenRange: (screenRange) ->
rowsToMeasure = []
- unless @presenter.isRowVisible(screenRange.start.row)
+ unless @presenter.isRowRendered(screenRange.start.row)
rowsToMeasure.push(screenRange.start.row)
- unless @presenter.isRowVisible(screenRange.end.row)
+ unless @presenter.isRowRendered(screenRange.end.row)
rowsToMeasure.push(screenRange.end.row)
if rowsToMeasure.length > 0
@@ -518,8 +560,8 @@ class TextEditorComponent
screenPosition = @screenPositionForMouseEvent(event)
if event.target?.classList.contains('fold-marker')
- bufferRow = @editor.bufferRowForScreenRow(screenPosition.row)
- @editor.unfoldBufferRow(bufferRow)
+ bufferPosition = @editor.bufferPositionForScreenPosition(screenPosition)
+ @editor.destroyFoldsIntersectingBufferRange([bufferPosition, bufferPosition])
return
switch detail
@@ -565,7 +607,7 @@ class TextEditorComponent
clickedScreenRow = @screenPositionForMouseEvent(event).row
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
- @editor.addSelectionForScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false)
+ @editor.addSelectionForScreenRange(initialScreenRange, autoscroll: false)
@handleGutterDrag(initialScreenRange)
onGutterShiftClick: (event) =>
@@ -848,10 +890,7 @@ class TextEditorComponent
e.abortKeyBinding() unless @editor.consolidateSelections()
lineNodeForScreenRow: (screenRow) ->
- tileRow = @presenter.tileForRow(screenRow)
- tileComponent = @linesComponent.getComponentForTile(tileRow)
-
- tileComponent?.lineNodeForScreenRow(screenRow)
+ @linesComponent.lineNodeForScreenRow(screenRow)
lineNumberNodeForScreenRow: (screenRow) ->
tileRow = @presenter.tileForRow(screenRow)
@@ -908,7 +947,7 @@ class TextEditorComponent
screenPositionForMouseEvent: (event, linesClientRect) ->
pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect)
- @screenPositionForPixelPosition(pixelPosition, true)
+ @screenPositionForPixelPosition(pixelPosition)
pixelPositionForMouseEvent: (event, linesClientRect) ->
{clientX, clientY} = event
diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee
index 380417163..0c9fa6123 100644
--- a/src/text-editor-element.coffee
+++ b/src/text-editor-element.coffee
@@ -17,6 +17,8 @@ class TextEditorElement extends HTMLElement
focusOnAttach: false
hasTiledRendering: true
logicalDisplayBuffer: true
+ scrollPastEnd: true
+ autoHeight: true
createdCallback: ->
# Use globals when the following instance variables aren't set.
@@ -38,6 +40,9 @@ class TextEditorElement extends HTMLElement
@setAttribute('tabindex', -1)
initializeContent: (attributes) ->
+ unless @autoHeight
+ @style.height = "100%"
+
if @config.get('editor.useShadowDOM')
@useShadowDOM = true
@@ -86,12 +91,12 @@ class TextEditorElement extends HTMLElement
@subscriptions.add @component.onDidChangeScrollLeft =>
@emitter.emit("did-change-scroll-left", arguments...)
- initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}) ->
- throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @views?
+ initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}, @autoHeight = true, @scrollPastEnd = true) ->
+ throw new Error("Must pass a views parameter when initializing TextEditorElements") unless @views?
throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @config?
throw new Error("Must pass a themes parameter when initializing TextEditorElements") unless @themes?
throw new Error("Must pass a workspace parameter when initializing TextEditorElements") unless @workspace?
- throw new Error("Must pass a assert parameter when initializing TextEditorElements") unless @assert?
+ throw new Error("Must pass an assert parameter when initializing TextEditorElements") unless @assert?
throw new Error("Must pass a styles parameter when initializing TextEditorElements") unless @styles?
throw new Error("Must pass a grammars parameter when initializing TextEditorElements") unless @grammars?
@@ -143,6 +148,7 @@ class TextEditorElement extends HTMLElement
workspace: @workspace
assert: @assert
grammars: @grammars
+ scrollPastEnd: @scrollPastEnd
)
@rootElement.appendChild(@component.getDomNode())
diff --git a/src/text-editor-marker-layer.coffee b/src/text-editor-marker-layer.coffee
deleted file mode 100644
index e99ad7323..000000000
--- a/src/text-editor-marker-layer.coffee
+++ /dev/null
@@ -1,192 +0,0 @@
-TextEditorMarker = require './text-editor-marker'
-
-# Public: *Experimental:* A container for a related set of markers at the
-# {TextEditor} level. Wraps an underlying {MarkerLayer} on the editor's
-# {TextBuffer}.
-#
-# This API is experimental and subject to change on any release.
-module.exports =
-class TextEditorMarkerLayer
- constructor: (@displayBuffer, @bufferMarkerLayer, @isDefaultLayer) ->
- @id = @bufferMarkerLayer.id
- @markersById = {}
-
- ###
- Section: Lifecycle
- ###
-
- # Essential: Destroy this layer.
- destroy: ->
- if @isDefaultLayer
- marker.destroy() for id, marker of @markersById
- else
- @bufferMarkerLayer.destroy()
-
- ###
- Section: Querying
- ###
-
- # Essential: Get an existing marker by its id.
- #
- # Returns a {TextEditorMarker}.
- getMarker: (id) ->
- if editorMarker = @markersById[id]
- editorMarker
- else if bufferMarker = @bufferMarkerLayer.getMarker(id)
- @markersById[id] = new TextEditorMarker(this, bufferMarker)
-
- # Essential: Get all markers in the layer.
- #
- # Returns an {Array} of {TextEditorMarker}s.
- getMarkers: ->
- @bufferMarkerLayer.getMarkers().map ({id}) => @getMarker(id)
-
- # Public: Get the number of markers in the marker layer.
- #
- # Returns a {Number}.
- getMarkerCount: ->
- @bufferMarkerLayer.getMarkerCount()
-
- # Public: Find markers in the layer conforming to the given parameters.
- #
- # See the documentation for {TextEditor::findMarkers}.
- findMarkers: (params) ->
- params = @translateToBufferMarkerParams(params)
- @bufferMarkerLayer.findMarkers(params).map (stringMarker) => @getMarker(stringMarker.id)
-
- ###
- Section: Marker creation
- ###
-
- # Essential: Create a marker on this layer with the given range in buffer
- # coordinates.
- #
- # See the documentation for {TextEditor::markBufferRange}
- markBufferRange: (bufferRange, options) ->
- @getMarker(@bufferMarkerLayer.markRange(bufferRange, options).id)
-
- # Essential: Create a marker on this layer with the given range in screen
- # coordinates.
- #
- # See the documentation for {TextEditor::markScreenRange}
- markScreenRange: (screenRange, options) ->
- bufferRange = @displayBuffer.bufferRangeForScreenRange(screenRange)
- @markBufferRange(bufferRange, options)
-
- # Public: Create a marker on this layer with the given buffer position and no
- # tail.
- #
- # See the documentation for {TextEditor::markBufferPosition}
- markBufferPosition: (bufferPosition, options) ->
- @getMarker(@bufferMarkerLayer.markPosition(bufferPosition, options).id)
-
- # Public: Create a marker on this layer with the given screen position and no
- # tail.
- #
- # See the documentation for {TextEditor::markScreenPosition}
- markScreenPosition: (screenPosition, options) ->
- bufferPosition = @displayBuffer.bufferPositionForScreenPosition(screenPosition)
- @markBufferPosition(bufferPosition, options)
-
- ###
- Section: Event Subscription
- ###
-
- # Public: Subscribe to be notified asynchronously whenever markers are
- # created, updated, or destroyed on this layer. *Prefer this method for
- # optimal performance when interacting with layers that could contain large
- # numbers of markers.*
- #
- # * `callback` A {Function} that will be called with no arguments when changes
- # occur on this layer.
- #
- # Subscribers are notified once, asynchronously when any number of changes
- # occur in a given tick of the event loop. You should re-query the layer
- # to determine the state of markers in which you're interested in. It may
- # be counter-intuitive, but this is much more efficient than subscribing to
- # events on individual markers, which are expensive to deliver.
- #
- # Returns a {Disposable}.
- onDidUpdate: (callback) ->
- @bufferMarkerLayer.onDidUpdate(callback)
-
- # Public: Subscribe to be notified synchronously whenever markers are created
- # on this layer. *Avoid this method for optimal performance when interacting
- # with layers that could contain large numbers of markers.*
- #
- # * `callback` A {Function} that will be called with a {TextEditorMarker}
- # whenever a new marker is created.
- #
- # You should prefer {onDidUpdate} when synchronous notifications aren't
- # absolutely necessary.
- #
- # Returns a {Disposable}.
- onDidCreateMarker: (callback) ->
- @bufferMarkerLayer.onDidCreateMarker (bufferMarker) =>
- callback(@getMarker(bufferMarker.id))
-
- # Public: Subscribe to be notified synchronously when this layer is destroyed.
- #
- # Returns a {Disposable}.
- onDidDestroy: (callback) ->
- @bufferMarkerLayer.onDidDestroy(callback)
-
- ###
- Section: Private
- ###
-
- refreshMarkerScreenPositions: ->
- for marker in @getMarkers()
- marker.notifyObservers(textChanged: false)
- return
-
- didDestroyMarker: (marker) ->
- delete @markersById[marker.id]
-
- translateToBufferMarkerParams: (params) ->
- bufferMarkerParams = {}
- for key, value of params
- switch key
- when 'startBufferPosition'
- key = 'startPosition'
- when 'endBufferPosition'
- key = 'endPosition'
- when 'startScreenPosition'
- key = 'startPosition'
- value = @displayBuffer.bufferPositionForScreenPosition(value)
- when 'endScreenPosition'
- key = 'endPosition'
- value = @displayBuffer.bufferPositionForScreenPosition(value)
- when 'startBufferRow'
- key = 'startRow'
- when 'endBufferRow'
- key = 'endRow'
- when 'startScreenRow'
- key = 'startRow'
- value = @displayBuffer.bufferRowForScreenRow(value)
- when 'endScreenRow'
- key = 'endRow'
- value = @displayBuffer.bufferRowForScreenRow(value)
- when 'intersectsBufferRowRange'
- key = 'intersectsRowRange'
- when 'intersectsScreenRowRange'
- key = 'intersectsRowRange'
- [startRow, endRow] = value
- value = [@displayBuffer.bufferRowForScreenRow(startRow), @displayBuffer.bufferRowForScreenRow(endRow)]
- when 'containsBufferRange'
- key = 'containsRange'
- when 'containsBufferPosition'
- key = 'containsPosition'
- when 'containedInBufferRange'
- key = 'containedInRange'
- when 'containedInScreenRange'
- key = 'containedInRange'
- value = @displayBuffer.bufferRangeForScreenRange(value)
- when 'intersectsBufferRange'
- key = 'intersectsRange'
- when 'intersectsScreenRange'
- key = 'intersectsRange'
- value = @displayBuffer.bufferRangeForScreenRange(value)
- bufferMarkerParams[key] = value
-
- bufferMarkerParams
diff --git a/src/text-editor-marker.coffee b/src/text-editor-marker.coffee
deleted file mode 100644
index df84700ee..000000000
--- a/src/text-editor-marker.coffee
+++ /dev/null
@@ -1,371 +0,0 @@
-_ = require 'underscore-plus'
-{CompositeDisposable, Emitter} = require 'event-kit'
-
-# Essential: Represents a buffer annotation that remains logically stationary
-# even as the buffer changes. This is used to represent cursors, folds, snippet
-# targets, misspelled words, and anything else that needs to track a logical
-# location in the buffer over time.
-#
-# ### TextEditorMarker Creation
-#
-# Use {TextEditor::markBufferRange} rather than creating Markers directly.
-#
-# ### Head and Tail
-#
-# Markers always have a *head* and sometimes have a *tail*. If you think of a
-# marker as an editor selection, the tail is the part that's stationary and the
-# head is the part that moves when the mouse is moved. A marker without a tail
-# always reports an empty range at the head position. A marker with a head position
-# greater than the tail is in a "normal" orientation. If the head precedes the
-# tail the marker is in a "reversed" orientation.
-#
-# ### Validity
-#
-# Markers are considered *valid* when they are first created. Depending on the
-# invalidation strategy you choose, certain changes to the buffer can cause a
-# marker to become invalid, for example if the text surrounding the marker is
-# deleted. The strategies, in order of descending fragility:
-#
-# * __never__: The marker is never marked as invalid. This is a good choice for
-# markers representing selections in an editor.
-# * __surround__: The marker is invalidated by changes that completely surround it.
-# * __overlap__: The marker is invalidated by changes that surround the
-# start or end of the marker. This is the default.
-# * __inside__: The marker is invalidated by changes that extend into the
-# inside of the marker. Changes that end at the marker's start or
-# start at the marker's end do not invalidate the marker.
-# * __touch__: The marker is invalidated by a change that touches the marked
-# region in any way, including changes that end at the marker's
-# start or start at the marker's end. This is the most fragile strategy.
-#
-# See {TextEditor::markBufferRange} for usage.
-module.exports =
-class TextEditorMarker
- bufferMarkerSubscription: null
- oldHeadBufferPosition: null
- oldHeadScreenPosition: null
- oldTailBufferPosition: null
- oldTailScreenPosition: null
- wasValid: true
- hasChangeObservers: false
-
- ###
- Section: Construction and Destruction
- ###
-
- constructor: (@layer, @bufferMarker) ->
- {@displayBuffer} = @layer
- @emitter = new Emitter
- @disposables = new CompositeDisposable
- @id = @bufferMarker.id
-
- @disposables.add @bufferMarker.onDidDestroy => @destroyed()
-
- # Essential: Destroys the marker, causing it to emit the 'destroyed' event. Once
- # destroyed, a marker cannot be restored by undo/redo operations.
- destroy: ->
- @bufferMarker.destroy()
- @disposables.dispose()
-
- # Essential: Creates and returns a new {TextEditorMarker} with the same properties as
- # this marker.
- #
- # {Selection} markers (markers with a custom property `type: "selection"`)
- # should be copied with a different `type` value, for example with
- # `marker.copy({type: null})`. Otherwise, the new marker's selection will
- # be merged with this marker's selection, and a `null` value will be
- # returned.
- #
- # * `properties` (optional) {Object} properties to associate with the new
- # marker. The new marker's properties are computed by extending this marker's
- # properties with `properties`.
- #
- # Returns a {TextEditorMarker}.
- copy: (properties) ->
- @layer.getMarker(@bufferMarker.copy(properties).id)
-
- ###
- Section: Event Subscription
- ###
-
- # Essential: Invoke the given callback when the state of the marker changes.
- #
- # * `callback` {Function} to be called when the marker changes.
- # * `event` {Object} with the following keys:
- # * `oldHeadBufferPosition` {Point} representing the former head buffer position
- # * `newHeadBufferPosition` {Point} representing the new head buffer position
- # * `oldTailBufferPosition` {Point} representing the former tail buffer position
- # * `newTailBufferPosition` {Point} representing the new tail buffer position
- # * `oldHeadScreenPosition` {Point} representing the former head screen position
- # * `newHeadScreenPosition` {Point} representing the new head screen position
- # * `oldTailScreenPosition` {Point} representing the former tail screen position
- # * `newTailScreenPosition` {Point} representing the new tail screen position
- # * `wasValid` {Boolean} indicating whether the marker was valid before the change
- # * `isValid` {Boolean} indicating whether the marker is now valid
- # * `hadTail` {Boolean} indicating whether the marker had a tail before the change
- # * `hasTail` {Boolean} indicating whether the marker now has a tail
- # * `oldProperties` {Object} containing the marker's custom properties before the change.
- # * `newProperties` {Object} containing the marker's custom properties after the change.
- # * `textChanged` {Boolean} indicating whether this change was caused by a textual change
- # to the buffer or whether the marker was manipulated directly via its public API.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidChange: (callback) ->
- unless @hasChangeObservers
- @oldHeadBufferPosition = @getHeadBufferPosition()
- @oldHeadScreenPosition = @getHeadScreenPosition()
- @oldTailBufferPosition = @getTailBufferPosition()
- @oldTailScreenPosition = @getTailScreenPosition()
- @wasValid = @isValid()
- @disposables.add @bufferMarker.onDidChange (event) => @notifyObservers(event)
- @hasChangeObservers = true
- @emitter.on 'did-change', callback
-
- # Essential: Invoke the given callback when the marker is destroyed.
- #
- # * `callback` {Function} to be called when the marker is destroyed.
- #
- # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
- onDidDestroy: (callback) ->
- @emitter.on 'did-destroy', callback
-
- ###
- Section: TextEditorMarker Details
- ###
-
- # Essential: Returns a {Boolean} indicating whether the marker is valid. Markers can be
- # invalidated when a region surrounding them in the buffer is changed.
- isValid: ->
- @bufferMarker.isValid()
-
- # Essential: Returns a {Boolean} indicating whether the marker has been destroyed. A marker
- # can be invalid without being destroyed, in which case undoing the invalidating
- # operation would restore the marker. Once a marker is destroyed by calling
- # {TextEditorMarker::destroy}, no undo/redo operation can ever bring it back.
- isDestroyed: ->
- @bufferMarker.isDestroyed()
-
- # Essential: Returns a {Boolean} indicating whether the head precedes the tail.
- isReversed: ->
- @bufferMarker.isReversed()
-
- # Essential: Get the invalidation strategy for this marker.
- #
- # Valid values include: `never`, `surround`, `overlap`, `inside`, and `touch`.
- #
- # Returns a {String}.
- getInvalidationStrategy: ->
- @bufferMarker.getInvalidationStrategy()
-
- # Essential: Returns an {Object} containing any custom properties associated with
- # the marker.
- getProperties: ->
- @bufferMarker.getProperties()
-
- # Essential: Merges an {Object} containing new properties into the marker's
- # existing properties.
- #
- # * `properties` {Object}
- setProperties: (properties) ->
- @bufferMarker.setProperties(properties)
-
- matchesProperties: (attributes) ->
- attributes = @layer.translateToBufferMarkerParams(attributes)
- @bufferMarker.matchesParams(attributes)
-
- ###
- Section: Comparing to other markers
- ###
-
- # Essential: Returns a {Boolean} indicating whether this marker is equivalent to
- # another marker, meaning they have the same range and options.
- #
- # * `other` {TextEditorMarker} other marker
- isEqual: (other) ->
- return false unless other instanceof @constructor
- @bufferMarker.isEqual(other.bufferMarker)
-
- # Essential: Compares this marker to another based on their ranges.
- #
- # * `other` {TextEditorMarker}
- #
- # Returns a {Number}
- compare: (other) ->
- @bufferMarker.compare(other.bufferMarker)
-
- ###
- Section: Managing the marker's range
- ###
-
- # Essential: Gets the buffer range of the display marker.
- #
- # Returns a {Range}.
- getBufferRange: ->
- @bufferMarker.getRange()
-
- # Essential: Modifies the buffer range of the display marker.
- #
- # * `bufferRange` The new {Range} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation.
- setBufferRange: (bufferRange, properties) ->
- @bufferMarker.setRange(bufferRange, properties)
-
- # Essential: Gets the screen range of the display marker.
- #
- # Returns a {Range}.
- getScreenRange: ->
- @displayBuffer.screenRangeForBufferRange(@getBufferRange(), wrapAtSoftNewlines: true)
-
- # Essential: Modifies the screen range of the display marker.
- #
- # * `screenRange` The new {Range} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- # * `reversed` {Boolean} If true, the marker will to be in a reversed orientation.
- setScreenRange: (screenRange, options) ->
- @setBufferRange(@displayBuffer.bufferRangeForScreenRange(screenRange), options)
-
- # Essential: Retrieves the buffer position of the marker's start. This will always be
- # less than or equal to the result of {TextEditorMarker::getEndBufferPosition}.
- #
- # Returns a {Point}.
- getStartBufferPosition: ->
- @bufferMarker.getStartPosition()
-
- # Essential: Retrieves the screen position of the marker's start. This will always be
- # less than or equal to the result of {TextEditorMarker::getEndScreenPosition}.
- #
- # Returns a {Point}.
- getStartScreenPosition: ->
- @displayBuffer.screenPositionForBufferPosition(@getStartBufferPosition(), wrapAtSoftNewlines: true)
-
- # Essential: Retrieves the buffer position of the marker's end. This will always be
- # greater than or equal to the result of {TextEditorMarker::getStartBufferPosition}.
- #
- # Returns a {Point}.
- getEndBufferPosition: ->
- @bufferMarker.getEndPosition()
-
- # Essential: Retrieves the screen position of the marker's end. This will always be
- # greater than or equal to the result of {TextEditorMarker::getStartScreenPosition}.
- #
- # Returns a {Point}.
- getEndScreenPosition: ->
- @displayBuffer.screenPositionForBufferPosition(@getEndBufferPosition(), wrapAtSoftNewlines: true)
-
- # Extended: Retrieves the buffer position of the marker's head.
- #
- # Returns a {Point}.
- getHeadBufferPosition: ->
- @bufferMarker.getHeadPosition()
-
- # Extended: Sets the buffer position of the marker's head.
- #
- # * `bufferPosition` The new {Point} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- setHeadBufferPosition: (bufferPosition, properties) ->
- @bufferMarker.setHeadPosition(bufferPosition, properties)
-
- # Extended: Retrieves the screen position of the marker's head.
- #
- # Returns a {Point}.
- getHeadScreenPosition: ->
- @displayBuffer.screenPositionForBufferPosition(@getHeadBufferPosition(), wrapAtSoftNewlines: true)
-
- # Extended: Sets the screen position of the marker's head.
- #
- # * `screenPosition` The new {Point} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- setHeadScreenPosition: (screenPosition, properties) ->
- @setHeadBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, properties))
-
- # Extended: Retrieves the buffer position of the marker's tail.
- #
- # Returns a {Point}.
- getTailBufferPosition: ->
- @bufferMarker.getTailPosition()
-
- # Extended: Sets the buffer position of the marker's tail.
- #
- # * `bufferPosition` The new {Point} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- setTailBufferPosition: (bufferPosition) ->
- @bufferMarker.setTailPosition(bufferPosition)
-
- # Extended: Retrieves the screen position of the marker's tail.
- #
- # Returns a {Point}.
- getTailScreenPosition: ->
- @displayBuffer.screenPositionForBufferPosition(@getTailBufferPosition(), wrapAtSoftNewlines: true)
-
- # Extended: Sets the screen position of the marker's tail.
- #
- # * `screenPosition` The new {Point} to use
- # * `properties` (optional) {Object} properties to associate with the marker.
- setTailScreenPosition: (screenPosition, options) ->
- @setTailBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options))
-
- # Extended: Returns a {Boolean} indicating whether the marker has a tail.
- hasTail: ->
- @bufferMarker.hasTail()
-
- # Extended: Plants the marker's tail at the current head position. After calling
- # the marker's tail position will be its head position at the time of the
- # call, regardless of where the marker's head is moved.
- #
- # * `properties` (optional) {Object} properties to associate with the marker.
- plantTail: ->
- @bufferMarker.plantTail()
-
- # Extended: Removes the marker's tail. After calling the marker's head position
- # will be reported as its current tail position until the tail is planted
- # again.
- #
- # * `properties` (optional) {Object} properties to associate with the marker.
- clearTail: (properties) ->
- @bufferMarker.clearTail(properties)
-
- ###
- Section: Private utility methods
- ###
-
- # Returns a {String} representation of the marker
- inspect: ->
- "TextEditorMarker(id: #{@id}, bufferRange: #{@getBufferRange()})"
-
- destroyed: ->
- @layer.didDestroyMarker(this)
- @emitter.emit 'did-destroy'
- @emitter.dispose()
-
- notifyObservers: ({textChanged}) ->
- textChanged ?= false
-
- newHeadBufferPosition = @getHeadBufferPosition()
- newHeadScreenPosition = @getHeadScreenPosition()
- newTailBufferPosition = @getTailBufferPosition()
- newTailScreenPosition = @getTailScreenPosition()
- isValid = @isValid()
-
- return if isValid is @wasValid and
- newHeadBufferPosition.isEqual(@oldHeadBufferPosition) and
- newHeadScreenPosition.isEqual(@oldHeadScreenPosition) and
- newTailBufferPosition.isEqual(@oldTailBufferPosition) and
- newTailScreenPosition.isEqual(@oldTailScreenPosition)
-
- changeEvent = {
- @oldHeadScreenPosition, newHeadScreenPosition,
- @oldTailScreenPosition, newTailScreenPosition,
- @oldHeadBufferPosition, newHeadBufferPosition,
- @oldTailBufferPosition, newTailBufferPosition,
- textChanged,
- isValid
- }
-
- @oldHeadBufferPosition = newHeadBufferPosition
- @oldHeadScreenPosition = newHeadScreenPosition
- @oldTailBufferPosition = newTailBufferPosition
- @oldTailScreenPosition = newTailScreenPosition
- @wasValid = isValid
-
- @emitter.emit 'did-change', changeEvent
diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee
index b13bf9036..14ed2bdc9 100644
--- a/src/text-editor-presenter.coffee
+++ b/src/text-editor-presenter.coffee
@@ -13,9 +13,10 @@ class TextEditorPresenter
minimumReflowInterval: 200
constructor: (params) ->
- {@model, @config, @lineTopIndex} = params
+ {@model, @config, @lineTopIndex, scrollPastEnd} = params
{@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @tileSize} = params
{@contentFrameWidth} = params
+ {@displayLayer} = @model
@gutterWidth = 0
@tileSize ?= 6
@@ -23,6 +24,7 @@ class TextEditorPresenter
@realScrollLeft = @scrollLeft
@disposables = new CompositeDisposable
@emitter = new Emitter
+ @linesByScreenRow = new Map
@visibleHighlights = {}
@characterWidthsByScope = {}
@lineDecorationsByScreenRow = {}
@@ -42,6 +44,8 @@ class TextEditorPresenter
@startReflowing() if @continuousReflow
@updating = false
+ @scrollPastEndOverride = scrollPastEnd ? true
+
setLinesYardstick: (@linesYardstick) ->
getLinesYardstick: -> @linesYardstick
@@ -85,6 +89,8 @@ class TextEditorPresenter
@updateCommonGutterState()
@updateReflowState()
+ @updateLines()
+
if @shouldUpdateDecorations
@fetchDecorations()
@updateLineDecorations()
@@ -104,6 +110,8 @@ class TextEditorPresenter
@clearPendingScrollPosition()
@updateRowsPerPage()
+ @updateLines()
+
@updateFocusedState()
@updateHeightState()
@updateVerticalScrollState()
@@ -130,8 +138,11 @@ class TextEditorPresenter
@shouldUpdateDecorations = true
observeModel: ->
- @disposables.add @model.onDidChange ({start, end, screenDelta}) =>
- @spliceBlockDecorationsInRange(start, end, screenDelta)
+ @disposables.add @model.displayLayer.onDidChangeSync (changes) =>
+ for change in changes
+ startRow = change.start.row
+ endRow = startRow + change.oldExtent.row
+ @spliceBlockDecorationsInRange(startRow, endRow, change.newExtent.row - change.oldExtent.row)
@shouldUpdateDecorations = true
@emitDidUpdateState()
@@ -164,7 +175,6 @@ class TextEditorPresenter
@scrollPastEnd = @config.get('editor.scrollPastEnd', configParams)
@showLineNumbers = @config.get('editor.showLineNumbers', configParams)
- @showIndentGuide = @config.get('editor.showIndentGuide', configParams)
if @configDisposables?
@configDisposables?.dispose()
@@ -173,10 +183,6 @@ class TextEditorPresenter
@configDisposables = new CompositeDisposable
@disposables.add(@configDisposables)
- @configDisposables.add @config.onDidChange 'editor.showIndentGuide', configParams, ({newValue}) =>
- @showIndentGuide = newValue
-
- @emitDidUpdateState()
@configDisposables.add @config.onDidChange 'editor.scrollPastEnd', configParams, ({newValue}) =>
@scrollPastEnd = newValue
@updateScrollHeight()
@@ -284,7 +290,6 @@ class TextEditorPresenter
@state.content.width = Math.max(@contentWidth + @verticalScrollbarWidth, @contentFrameWidth)
@state.content.scrollWidth = @scrollWidth
@state.content.scrollLeft = @scrollLeft
- @state.content.indentGuidesVisible = not @model.isMini() and @showIndentGuide
@state.content.backgroundColor = if @model.isMini() then null else @backgroundColor
@state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null
@@ -295,15 +300,15 @@ class TextEditorPresenter
Math.max(0, Math.min(row, @model.getScreenLineCount()))
getStartTileRow: ->
- @constrainRow(@tileForRow(@startRow))
+ @constrainRow(@tileForRow(@startRow ? 0))
getEndTileRow: ->
- @constrainRow(@tileForRow(@endRow))
+ @constrainRow(@tileForRow(@endRow ? 0))
isValidScreenRow: (screenRow) ->
screenRow >= 0 and screenRow < @model.getScreenLineCount()
- getScreenRows: ->
+ getScreenRowsToRender: ->
startRow = @getStartTileRow()
endRow = @constrainRow(@getEndTileRow() + @tileSize)
@@ -318,6 +323,22 @@ class TextEditorPresenter
screenRows.sort (a, b) -> a - b
_.uniq(screenRows, true)
+ getScreenRangesToRender: ->
+ screenRows = @getScreenRowsToRender()
+ screenRows.push(Infinity) # makes the loop below inclusive
+
+ startRow = screenRows[0]
+ endRow = startRow - 1
+ screenRanges = []
+ for row in screenRows
+ if row is endRow + 1
+ endRow++
+ else
+ screenRanges.push([startRow, endRow])
+ startRow = endRow = row
+
+ screenRanges
+
setScreenRowsToMeasure: (screenRows) ->
return if not screenRows? or screenRows.length is 0
@@ -330,7 +351,7 @@ class TextEditorPresenter
updateTilesState: ->
return unless @startRow? and @endRow? and @lineHeight?
- screenRows = @getScreenRows()
+ screenRows = @getScreenRowsToRender()
visibleTiles = {}
startRow = screenRows[0]
endRow = screenRows[screenRows.length - 1]
@@ -373,7 +394,7 @@ class TextEditorPresenter
visibleTiles[tileStartRow] = true
zIndex++
- if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)?
+ if @mouseWheelScreenRow? and 0 <= @mouseWheelScreenRow < @model.getScreenLineCount()
mouseWheelTile = @tileForRow(@mouseWheelScreenRow)
unless visibleTiles[mouseWheelTile]?
@@ -391,7 +412,7 @@ class TextEditorPresenter
tileState.lines ?= {}
visibleLineIds = {}
for screenRow in screenRows
- line = @model.tokenizedLineForScreenRow(screenRow)
+ line = @linesByScreenRow.get(screenRow)
unless line?
throw new Error("No line exists for row #{screenRow}. Last screen row: #{@model.getLastScreenRow()}")
@@ -409,18 +430,8 @@ class TextEditorPresenter
else
tileState.lines[line.id] =
screenRow: screenRow
- text: line.text
- openScopes: line.openScopes
- tags: line.tags
- specialTokens: line.specialTokens
- firstNonWhitespaceIndex: line.firstNonWhitespaceIndex
- firstTrailingWhitespaceIndex: line.firstTrailingWhitespaceIndex
- invisibles: line.invisibles
- endOfLineInvisibles: line.endOfLineInvisibles
- isOnlyWhitespace: line.isOnlyWhitespace()
- indentLevel: line.indentLevel
- tabLength: line.tabLength
- fold: line.fold
+ lineText: line.lineText
+ tagCodes: line.tagCodes
decorationClasses: @lineDecorationClassesForRow(screenRow)
precedingBlockDecorations: precedingBlockDecorations
followingBlockDecorations: followingBlockDecorations
@@ -432,18 +443,14 @@ class TextEditorPresenter
return
updateCursorsState: ->
- @state.content.cursors = {}
- @updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation
- return
-
- updateCursorState: (cursor) ->
return unless @startRow? and @endRow? and @hasPixelRectRequirements() and @baseCharacterWidth?
- screenRange = cursor.getScreenRange()
- return unless cursor.isVisible() and @startRow <= screenRange.start.row < @endRow
- pixelRect = @pixelRectForScreenRange(screenRange)
- pixelRect.width = Math.round(@baseCharacterWidth) if pixelRect.width is 0
- @state.content.cursors[cursor.id] = pixelRect
+ @state.content.cursors = {}
+ for cursor in @model.cursorsForScreenRowRange(@startRow, @endRow - 1) when cursor.isVisible()
+ pixelRect = @pixelRectForScreenRange(cursor.getScreenRange())
+ pixelRect.width = Math.round(@baseCharacterWidth) if pixelRect.width is 0
+ @state.content.cursors[cursor.id] = pixelRect
+ return
updateOverlaysState: ->
return unless @hasOverlayPositionRequirements()
@@ -461,19 +468,21 @@ class TextEditorPresenter
pixelPosition = @pixelPositionForScreenPosition(screenPosition)
- top = pixelPosition.top + @lineHeight
- left = pixelPosition.left + @gutterWidth
+ # Fixed positioning.
+ top = @boundingClientRect.top + pixelPosition.top + @lineHeight
+ left = @boundingClientRect.left + pixelPosition.left + @gutterWidth
if overlayDimensions = @overlayDimensions[decoration.id]
{itemWidth, itemHeight, contentMargin} = overlayDimensions
- rightDiff = left + @boundingClientRect.left + itemWidth + contentMargin - @windowWidth
+ rightDiff = left + itemWidth + contentMargin - @windowWidth
left -= rightDiff if rightDiff > 0
- leftDiff = left + @boundingClientRect.left + contentMargin
+ leftDiff = left + contentMargin
left -= leftDiff if leftDiff < 0
- if top + @boundingClientRect.top + itemHeight > @windowHeight and top - (itemHeight + @lineHeight) >= 0
+ if top + itemHeight > @windowHeight and
+ top - (itemHeight + @lineHeight) >= 0
top -= itemHeight + @lineHeight
pixelPosition.top = top
@@ -592,36 +601,19 @@ class TextEditorPresenter
tileState.lineNumbers ?= {}
visibleLineNumberIds = {}
- startRow = screenRows[screenRows.length - 1]
- endRow = Math.min(screenRows[0] + 1, @model.getScreenLineCount())
+ for screenRow in screenRows when @isRowRendered(screenRow)
+ lineId = @linesByScreenRow.get(screenRow).id
+ {bufferRow, softWrappedAtStart: softWrapped} = @displayLayer.softWrapDescriptorForScreenRow(screenRow)
+ foldable = not softWrapped and @model.isFoldableAtBufferRow(bufferRow)
+ decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
+ blockDecorationsBeforeCurrentScreenRowHeight = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow) - @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow)
+ blockDecorationsHeight = blockDecorationsBeforeCurrentScreenRowHeight
+ if screenRow % @tileSize isnt 0
+ blockDecorationsAfterPreviousScreenRowHeight = @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow) - @lineHeight - @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow - 1)
+ blockDecorationsHeight += blockDecorationsAfterPreviousScreenRowHeight
- if startRow > 0
- rowBeforeStartRow = startRow - 1
- lastBufferRow = @model.bufferRowForScreenRow(rowBeforeStartRow)
- else
- lastBufferRow = null
-
- if endRow > startRow
- bufferRows = @model.bufferRowsForScreenRows(startRow, endRow - 1)
- for bufferRow, i in bufferRows
- if bufferRow is lastBufferRow
- softWrapped = true
- else
- lastBufferRow = bufferRow
- softWrapped = false
-
- screenRow = startRow + i
- line = @model.tokenizedLineForScreenRow(screenRow)
- decorationClasses = @lineNumberDecorationClassesForRow(screenRow)
- foldable = @model.isFoldableAtScreenRow(screenRow)
- blockDecorationsBeforeCurrentScreenRowHeight = @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow) - @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow)
- blockDecorationsHeight = blockDecorationsBeforeCurrentScreenRowHeight
- if screenRow % @tileSize isnt 0
- blockDecorationsAfterPreviousScreenRowHeight = @lineTopIndex.pixelPositionBeforeBlocksForRow(screenRow) - @lineHeight - @lineTopIndex.pixelPositionAfterBlocksForRow(screenRow - 1)
- blockDecorationsHeight += blockDecorationsAfterPreviousScreenRowHeight
-
- tileState.lineNumbers[line.id] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight}
- visibleLineNumberIds[line.id] = true
+ tileState.lineNumbers[lineId] = {screenRow, bufferRow, softWrapped, decorationClasses, foldable, blockDecorationsHeight}
+ visibleLineNumberIds[lineId] = true
for id of tileState.lineNumbers
delete tileState.lineNumbers[id] unless visibleLineNumberIds[id]
@@ -659,7 +651,7 @@ class TextEditorPresenter
return unless @contentHeight? and @clientHeight?
contentHeight = @contentHeight
- if @scrollPastEnd
+ if @scrollPastEnd and @scrollPastEndOverride
extraScrollHeight = @clientHeight - (@lineHeight * 3)
contentHeight += extraScrollHeight if extraScrollHeight > 0
scrollHeight = Math.max(contentHeight, @height)
@@ -681,9 +673,7 @@ class TextEditorPresenter
updateHorizontalDimensions: ->
if @baseCharacterWidth?
oldContentWidth = @contentWidth
- rightmostPosition = Point(@model.getLongestScreenRow(), @model.getMaxScreenLineLength())
- if @model.tokenizedLineForScreenRow(rightmostPosition.row)?.isSoftWrapped()
- rightmostPosition = @model.clipScreenPosition(rightmostPosition)
+ rightmostPosition = @model.getRightmostScreenPosition()
@contentWidth = @pixelPositionForScreenPosition(rightmostPosition).left
@contentWidth += @scrollLeft
@contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width
@@ -1051,6 +1041,16 @@ class TextEditorPresenter
rect.height = Math.round(rect.height)
rect
+ updateLines: ->
+ @linesByScreenRow.clear()
+
+ for [startRow, endRow] in @getScreenRangesToRender()
+ for line, index in @displayLayer.getScreenLines(startRow, endRow + 1)
+ @linesByScreenRow.set(startRow + index, line)
+
+ lineIdForScreenRow: (screenRow) ->
+ @linesByScreenRow.get(screenRow)?.id
+
fetchDecorations: ->
return unless 0 <= @startRow <= @endRow <= Infinity
@decorations = @model.decorationsStateForScreenRowRange(@startRow, @endRow - 1)
@@ -1098,9 +1098,9 @@ class TextEditorPresenter
@customGutterDecorationsByGutterName = {}
for decorationId, decorationState of @decorations
- {properties, screenRange, rangeIsReversed} = decorationState
+ {properties, bufferRange, screenRange, rangeIsReversed} = decorationState
if Decoration.isType(properties, 'line') or Decoration.isType(properties, 'line-number')
- @addToLineDecorationCaches(decorationId, properties, screenRange, rangeIsReversed)
+ @addToLineDecorationCaches(decorationId, properties, bufferRange, screenRange, rangeIsReversed)
else if Decoration.isType(properties, 'gutter') and properties.gutterName?
@customGutterDecorationsByGutterName[properties.gutterName] ?= {}
@@ -1121,7 +1121,7 @@ class TextEditorPresenter
return
- addToLineDecorationCaches: (decorationId, properties, screenRange, rangeIsReversed) ->
+ addToLineDecorationCaches: (decorationId, properties, bufferRange, screenRange, rangeIsReversed) ->
if screenRange.isEmpty()
return if properties.onlyNonEmpty
else
@@ -1129,21 +1129,26 @@ class TextEditorPresenter
omitLastRow = screenRange.end.column is 0
if rangeIsReversed
- headPosition = screenRange.start
+ headScreenPosition = screenRange.start
else
- headPosition = screenRange.end
+ headScreenPosition = screenRange.end
- for row in [screenRange.start.row..screenRange.end.row] by 1
- continue if properties.onlyHead and row isnt headPosition.row
- continue if omitLastRow and row is screenRange.end.row
+ if properties.class is 'folded' and Decoration.isType(properties, 'line-number')
+ screenRow = @model.screenRowForBufferRow(bufferRange.start.row)
+ @lineNumberDecorationsByScreenRow[screenRow] ?= {}
+ @lineNumberDecorationsByScreenRow[screenRow][decorationId] = properties
+ else
+ for row in [screenRange.start.row..screenRange.end.row] by 1
+ continue if properties.onlyHead and row isnt headScreenPosition.row
+ continue if omitLastRow and row is screenRange.end.row
- if Decoration.isType(properties, 'line')
- @lineDecorationsByScreenRow[row] ?= {}
- @lineDecorationsByScreenRow[row][decorationId] = properties
+ if Decoration.isType(properties, 'line')
+ @lineDecorationsByScreenRow[row] ?= {}
+ @lineDecorationsByScreenRow[row][decorationId] = properties
- if Decoration.isType(properties, 'line-number')
- @lineNumberDecorationsByScreenRow[row] ?= {}
- @lineNumberDecorationsByScreenRow[row][decorationId] = properties
+ if Decoration.isType(properties, 'line-number')
+ @lineNumberDecorationsByScreenRow[row] ?= {}
+ @lineNumberDecorationsByScreenRow[row][decorationId] = properties
return
@@ -1520,8 +1525,14 @@ class TextEditorPresenter
getVisibleRowRange: ->
[@startRow, @endRow]
- isRowVisible: (row) ->
- @startRow <= row < @endRow
+ isRowRendered: (row) ->
+ @getStartTileRow() <= row < @constrainRow(@getEndTileRow() + @tileSize)
- lineIdForScreenRow: (screenRow) ->
- @model.tokenizedLineForScreenRow(screenRow)?.id
+ isOpenTagCode: (tagCode) ->
+ @displayLayer.isOpenTagCode(tagCode)
+
+ isCloseTagCode: (tagCode) ->
+ @displayLayer.isCloseTagCode(tagCode)
+
+ tagForCode: (tagCode) ->
+ @displayLayer.tagForCode(tagCode)
diff --git a/src/text-editor-registry.coffee b/src/text-editor-registry.coffee
new file mode 100644
index 000000000..e31630fee
--- /dev/null
+++ b/src/text-editor-registry.coffee
@@ -0,0 +1,52 @@
+{Emitter, Disposable} = require 'event-kit'
+
+# Experimental: This global registry tracks registered `TextEditors`.
+#
+# If you want to add functionality to a wider set of text editors than just
+# those appearing within workspace panes, use `atom.textEditors.observe` to
+# invoke a callback for all current and future registered text editors.
+#
+# If you want packages to be able to add functionality to your non-pane text
+# editors (such as a search field in a custom user interface element), register
+# them for observation via `atom.textEditors.add`. **Important:** When you're
+# done using your editor, be sure to call `dispose` on the returned disposable
+# to avoid leaking editors.
+module.exports =
+class TextEditorRegistry
+ constructor: ->
+ @editors = new Set
+ @emitter = new Emitter
+
+ # Register a `TextEditor`.
+ #
+ # * `editor` The editor to register.
+ #
+ # Returns a {Disposable} on which `.dispose()` can be called to remove the
+ # added editor. To avoid any memory leaks this should be called when the
+ # editor is destroyed.
+ add: (editor) ->
+ @editors.add(editor)
+ editor.registered = true
+
+ @emitter.emit 'did-add-editor', editor
+ new Disposable => @remove(editor)
+
+ # Remove a `TextEditor`.
+ #
+ # * `editor` The editor to remove.
+ #
+ # Returns a {Boolean} indicating whether the editor was successfully removed.
+ remove: (editor) ->
+ removed = @editors.delete(editor)
+ editor.registered = false
+ removed
+
+ # Invoke the given callback with all the current and future registered
+ # `TextEditors`.
+ #
+ # * `callback` {Function} to be called with current and future text editors.
+ #
+ # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
+ observe: (callback) ->
+ @editors.forEach(callback)
+ @emitter.on 'did-add-editor', callback
diff --git a/src/text-editor.coffee b/src/text-editor.coffee
index 2ba45a3ba..740f9e5f3 100644
--- a/src/text-editor.coffee
+++ b/src/text-editor.coffee
@@ -4,13 +4,17 @@ Grim = require 'grim'
{CompositeDisposable, Emitter} = require 'event-kit'
{Point, Range} = TextBuffer = require 'text-buffer'
LanguageMode = require './language-mode'
-DisplayBuffer = require './display-buffer'
+DecorationManager = require './decoration-manager'
+TokenizedBuffer = require './tokenized-buffer'
Cursor = require './cursor'
Model = require './model'
Selection = require './selection'
TextMateScopeSelector = require('first-mate').ScopeSelector
-{Directory} = require "pathwatcher"
GutterContainer = require './gutter-container'
+TextEditorElement = require './text-editor-element'
+{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils'
+
+ZERO_WIDTH_NBSP = '\ufeff'
# Essential: This class represents all essential editing state for a single
# {TextBuffer}, including cursor and selection positions, folds, and soft wraps.
@@ -54,6 +58,8 @@ GutterContainer = require './gutter-container'
# soft wraps and folds to ensure your code interacts with them correctly.
module.exports =
class TextEditor extends Model
+ serializationVersion: 1
+
buffer: null
languageMode: null
cursors: null
@@ -61,71 +67,106 @@ class TextEditor extends Model
suppressSelectionMerging: false
selectionFlashDuration: 500
gutterContainer: null
+ editorElement: null
+ verticalScrollMargin: 2
+ horizontalScrollMargin: 6
+ softWrapped: null
+ editorWidthInChars: null
+ lineHeightInPixels: null
+ defaultCharWidth: null
+ height: null
+ width: null
+ registered: false
+
+ Object.defineProperty @prototype, "element",
+ get: -> @getElement()
+
+ Object.defineProperty(@prototype, 'displayBuffer', get: ->
+ Grim.deprecate("""
+ `TextEditor.prototype.displayBuffer` has always been private, but now
+ it is gone. Reading the `displayBuffer` property now returns a reference
+ to the containing `TextEditor`, which now provides *some* of the API of
+ the defunct `DisplayBuffer` class.
+ """)
+ this
+ )
@deserialize: (state, atomEnvironment) ->
+ # TODO: Return null on version mismatch when 1.8.0 has been out for a while
+ if state.version isnt @prototype.serializationVersion and state.displayBuffer?
+ state.tokenizedBuffer = state.displayBuffer.tokenizedBuffer
+
try
- displayBuffer = DisplayBuffer.deserialize(state.displayBuffer, atomEnvironment)
+ state.tokenizedBuffer = TokenizedBuffer.deserialize(state.tokenizedBuffer, atomEnvironment)
catch error
if error.syscall is 'read'
return # Error reading the file, don't deserialize an editor for it
else
throw error
- state.displayBuffer = displayBuffer
- state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId)
+ state.buffer = state.tokenizedBuffer.buffer
+ state.displayLayer = state.buffer.getDisplayLayer(state.displayLayerId) ? state.buffer.addDisplayLayer()
+ state.selectionsMarkerLayer = state.displayLayer.getMarkerLayer(state.selectionsMarkerLayerId)
state.config = atomEnvironment.config
- state.notificationManager = atomEnvironment.notifications
- state.packageManager = atomEnvironment.packages
state.clipboard = atomEnvironment.clipboard
- state.viewRegistry = atomEnvironment.views
state.grammarRegistry = atomEnvironment.grammars
- state.project = atomEnvironment.project
state.assert = atomEnvironment.assert.bind(atomEnvironment)
- state.applicationDelegate = atomEnvironment.applicationDelegate
- new this(state)
+ editor = new this(state)
+ if state.registered
+ disposable = atomEnvironment.textEditors.add(editor)
+ editor.onDidDestroy -> disposable.dispose()
+ editor
constructor: (params={}) ->
super
{
- @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength,
- softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation,
- @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config,
- @notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry,
- @project, @assert, @applicationDelegate, @pending
+ @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, @tabLength,
+ @softWrapped, @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation,
+ @mini, @placeholderText, lineNumberGutterVisible, @largeFileMode, @config, @clipboard, @grammarRegistry,
+ @assert, @applicationDelegate, grammar, @showInvisibles, @autoHeight, @scrollPastEnd, @editorWidthInChars,
+ @tokenizedBuffer, @ignoreInvisibles, @displayLayer
} = params
throw new Error("Must pass a config parameter when constructing TextEditors") unless @config?
- throw new Error("Must pass a notificationManager parameter when constructing TextEditors") unless @notificationManager?
- throw new Error("Must pass a packageManager parameter when constructing TextEditors") unless @packageManager?
throw new Error("Must pass a clipboard parameter when constructing TextEditors") unless @clipboard?
- throw new Error("Must pass a viewRegistry parameter when constructing TextEditors") unless @viewRegistry?
throw new Error("Must pass a grammarRegistry parameter when constructing TextEditors") unless @grammarRegistry?
- throw new Error("Must pass a project parameter when constructing TextEditors") unless @project?
- throw new Error("Must pass an assert parameter when constructing TextEditors") unless @assert?
+ @assert ?= (condition) -> condition
@firstVisibleScreenRow ?= 0
@firstVisibleScreenColumn ?= 0
@emitter = new Emitter
@disposables = new CompositeDisposable
@cursors = []
+ @cursorsByMarkerId = new Map
@selections = []
+ @autoHeight ?= true
+ @scrollPastEnd ?= true
+ @hasTerminatedPendingState = false
- buffer ?= new TextBuffer
- @displayBuffer ?= new DisplayBuffer({
- buffer, tabLength, softWrapped, ignoreInvisibles: @mini, largeFileMode,
- @config, @assert, @grammarRegistry, @packageManager
+ @showInvisibles ?= true
+
+ @buffer ?= new TextBuffer
+ @tokenizedBuffer ?= new TokenizedBuffer({
+ @tabLength, @buffer, @largeFileMode, @config, @grammarRegistry, @assert
})
- @buffer = @displayBuffer.buffer
- @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true)
+ @displayLayer ?= @buffer.addDisplayLayer()
+ @displayLayer.setTextDecorationLayer(@tokenizedBuffer)
+ @defaultMarkerLayer = @displayLayer.addMarkerLayer()
+ @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true)
+
+ @decorationManager = new DecorationManager(@displayLayer, @defaultMarkerLayer)
+
+ @decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'})
+
+ @disposables.add @tokenizedBuffer.observeGrammar @subscribeToScopedConfigSettings
for marker in @selectionsMarkerLayer.getMarkers()
- marker.setProperties(preserveFolds: true)
@addSelection(marker)
@subscribeToTabTypeConfig()
@subscribeToBuffer()
- @subscribeToDisplayBuffer()
+ @subscribeToDisplayLayer()
if @cursors.length is 0 and not suppressCursorCreation
initialLine = Math.max(parseInt(initialLine) or 0, 0)
@@ -142,37 +183,70 @@ class TextEditor extends Model
priority: 0
visible: lineNumberGutterVisible
+ if grammar?
+ @setGrammar(grammar)
+
serialize: ->
+ tokenizedBufferState = @tokenizedBuffer.serialize()
+
deserializer: 'TextEditor'
+ version: @serializationVersion
id: @id
softTabs: @softTabs
firstVisibleScreenRow: @getFirstVisibleScreenRow()
firstVisibleScreenColumn: @getFirstVisibleScreenColumn()
- displayBuffer: @displayBuffer.serialize()
selectionsMarkerLayerId: @selectionsMarkerLayer.id
- pending: @isPending()
+ softWrapped: @isSoftWrapped()
+ editorWidthInChars: @editorWidthInChars
+ # TODO: Remove this forward-compatible fallback once 1.8 reaches stable.
+ displayBuffer: {tokenizedBuffer: tokenizedBufferState}
+ tokenizedBuffer: tokenizedBufferState
+ largeFileMode: @largeFileMode
+ displayLayerId: @displayLayer.id
+ registered: @registered
subscribeToBuffer: ->
@buffer.retain()
@disposables.add @buffer.onDidChangePath =>
- unless @project.getPaths().length > 0
- @project.setPaths([path.dirname(@getPath())])
@emitter.emit 'did-change-title', @getTitle()
@emitter.emit 'did-change-path', @getPath()
@disposables.add @buffer.onDidChangeEncoding =>
@emitter.emit 'did-change-encoding', @getEncoding()
@disposables.add @buffer.onDidDestroy => @destroy()
- if @pending
- @disposables.add @buffer.onDidChangeModified =>
- @terminatePendingState() if @buffer.isModified()
+ @disposables.add @buffer.onDidChangeModified =>
+ @terminatePendingState() if not @hasTerminatedPendingState and @buffer.isModified()
@preserveCursorPositionOnBufferReload()
- subscribeToDisplayBuffer: ->
+ terminatePendingState: ->
+ @emitter.emit 'did-terminate-pending-state' if not @hasTerminatedPendingState
+ @hasTerminatedPendingState = true
+
+ onDidTerminatePendingState: (callback) ->
+ @emitter.on 'did-terminate-pending-state', callback
+
+ subscribeToScopedConfigSettings: =>
+ @scopedConfigSubscriptions?.dispose()
+ @scopedConfigSubscriptions = subscriptions = new CompositeDisposable
+
+ scopeDescriptor = @getRootScopeDescriptor()
+ subscriptions.add @config.onDidChange 'editor.atomicSoftTabs', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.tabLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.invisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.showInvisibles', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.showIndentGuide', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.softWrap', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.softWrapHangingIndent', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.softWrapAtPreferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+ subscriptions.add @config.onDidChange 'editor.preferredLineLength', scope: scopeDescriptor, @resetDisplayLayer.bind(this)
+
+ @resetDisplayLayer()
+
+ subscribeToDisplayLayer: ->
@disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this)
- @disposables.add @displayBuffer.onDidChangeGrammar @handleGrammarChange.bind(this)
- @disposables.add @displayBuffer.onDidTokenize @handleTokenization.bind(this)
- @disposables.add @displayBuffer.onDidChange (e) =>
+ @disposables.add @tokenizedBuffer.onDidChangeGrammar @handleGrammarChange.bind(this)
+ @disposables.add @tokenizedBuffer.onDidTokenize @handleTokenization.bind(this)
+ @disposables.add @displayLayer.onDidChangeSync (e) =>
@mergeIntersectingSelections()
@emitter.emit 'did-change', e
@@ -181,13 +255,28 @@ class TextEditor extends Model
@tabTypeSubscription = @config.observe 'editor.tabType', scope: @getRootScopeDescriptor(), =>
@softTabs = @shouldUseSoftTabs(defaultValue: @softTabs)
+ resetDisplayLayer: ->
+ @displayLayer.reset({
+ invisibles: @getInvisibles(),
+ softWrapColumn: @getSoftWrapColumn(),
+ showIndentGuides: not @isMini() and @config.get('editor.showIndentGuide', scope: @getRootScopeDescriptor()),
+ atomicSoftTabs: @config.get('editor.atomicSoftTabs', scope: @getRootScopeDescriptor()),
+ tabLength: @getTabLength(),
+ ratioForCharacter: @ratioForCharacter.bind(this),
+ isWrapBoundary: isWrapBoundary,
+ foldCharacter: ZERO_WIDTH_NBSP
+ })
+
destroyed: ->
@disposables.dispose()
+ @displayLayer.destroy()
+ @scopedConfigSubscriptions.dispose()
+ @disposables.dispose()
+ @tokenizedBuffer.destroy()
@tabTypeSubscription.dispose()
selection.destroy() for selection in @selections.slice()
@selectionsMarkerLayer.destroy()
@buffer.release()
- @displayBuffer.destroy()
@languageMode.destroy()
@gutterContainer.destroy()
@emitter.emit 'did-destroy'
@@ -271,7 +360,7 @@ class TextEditor extends Model
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeSoftWrapped: (callback) ->
- @displayBuffer.onDidChangeSoftWrapped(callback)
+ @emitter.on 'did-change-soft-wrapped', callback
# Extended: Calls your `callback` when the buffer's encoding has changed.
#
@@ -425,7 +514,7 @@ class TextEditor extends Model
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observeDecorations: (callback) ->
- @displayBuffer.observeDecorations(callback)
+ @decorationManager.observeDecorations(callback)
# Extended: Calls your `callback` when a {Decoration} is added to the editor.
#
@@ -434,7 +523,7 @@ class TextEditor extends Model
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidAddDecoration: (callback) ->
- @displayBuffer.onDidAddDecoration(callback)
+ @decorationManager.onDidAddDecoration(callback)
# Extended: Calls your `callback` when a {Decoration} is removed from the editor.
#
@@ -443,7 +532,7 @@ class TextEditor extends Model
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidRemoveDecoration: (callback) ->
- @displayBuffer.onDidRemoveDecoration(callback)
+ @decorationManager.onDidRemoveDecoration(callback)
# Extended: Calls your `callback` when the placeholder text is changed.
#
@@ -454,34 +543,28 @@ class TextEditor extends Model
onDidChangePlaceholderText: (callback) ->
@emitter.on 'did-change-placeholder-text', callback
- onDidChangeCharacterWidths: (callback) ->
- @displayBuffer.onDidChangeCharacterWidths(callback)
-
onDidChangeFirstVisibleScreenRow: (callback, fromView) ->
@emitter.on 'did-change-first-visible-screen-row', callback
onDidChangeScrollTop: (callback) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.")
- @viewRegistry.getView(this).onDidChangeScrollTop(callback)
+ @getElement().onDidChangeScrollTop(callback)
onDidChangeScrollLeft: (callback) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.")
- @viewRegistry.getView(this).onDidChangeScrollLeft(callback)
+ @getElement().onDidChangeScrollLeft(callback)
onDidRequestAutoscroll: (callback) ->
- @displayBuffer.onDidRequestAutoscroll(callback)
+ @emitter.on 'did-request-autoscroll', callback
# TODO Remove once the tabs package no longer uses .on subscriptions
onDidChangeIcon: (callback) ->
@emitter.on 'did-change-icon', callback
- onDidUpdateMarkers: (callback) ->
- @displayBuffer.onDidUpdateMarkers(callback)
-
onDidUpdateDecorations: (callback) ->
- @displayBuffer.onDidUpdateDecorations(callback)
+ @decorationManager.onDidUpdateDecorations(callback)
# Essential: Retrieves the current {TextBuffer}.
getBuffer: -> @buffer
@@ -491,31 +574,32 @@ class TextEditor extends Model
# Create an {TextEditor} with its initial state based on this object
copy: ->
- displayBuffer = @displayBuffer.copy()
- selectionsMarkerLayer = displayBuffer.getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id)
+ displayLayer = @displayLayer.copy()
+ selectionsMarkerLayer = displayLayer.getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id)
softTabs = @getSoftTabs()
newEditor = new TextEditor({
- @buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs,
- suppressCursorCreation: true, @config, @notificationManager, @packageManager,
+ @buffer, selectionsMarkerLayer, @tabLength, softTabs,
+ suppressCursorCreation: true, @config,
@firstVisibleScreenRow, @firstVisibleScreenColumn,
- @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate
+ @clipboard, @grammarRegistry, @assert, displayLayer
})
newEditor
# Controls visibility based on the given {Boolean}.
- setVisible: (visible) -> @displayBuffer.setVisible(visible)
+ setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
setMini: (mini) ->
if mini isnt @mini
@mini = mini
- @displayBuffer.setIgnoreInvisibles(@mini)
+ @ignoreInvisibles = @mini
+ @resetDisplayLayer()
@emitter.emit 'did-change-mini', @mini
@mini
isMini: -> @mini
setUpdatedSynchronously: (updatedSynchronously) ->
- @displayBuffer.setUpdatedSynchronously(updatedSynchronously)
+ @decorationManager.setUpdatedSynchronously(updatedSynchronously)
onDidChangeMini: (callback) ->
@emitter.on 'did-change-mini', callback
@@ -568,19 +652,18 @@ class TextEditor extends Model
# * `editorWidthInChars` A {Number} representing the width of the
# {TextEditorElement} in characters.
setEditorWidthInChars: (editorWidthInChars) ->
- @displayBuffer.setEditorWidthInChars(editorWidthInChars)
+ if editorWidthInChars > 0
+ previousWidthInChars = @editorWidthInChars
+ @editorWidthInChars = editorWidthInChars
+ if editorWidthInChars isnt previousWidthInChars and @isSoftWrapped()
+ @resetDisplayLayer()
# Returns the editor width in characters.
getEditorWidthInChars: ->
- @displayBuffer.getEditorWidthInChars()
-
- onDidTerminatePendingState: (callback) ->
- @emitter.on 'did-terminate-pending-state', callback
-
- terminatePendingState: ->
- return if not @pending
- @pending = false
- @emitter.emit 'did-terminate-pending-state'
+ if @width? and @defaultCharWidth > 0
+ Math.max(0, Math.floor(@width / @defaultCharWidth))
+ else
+ @editorWidthInChars
###
Section: File Details
@@ -665,15 +748,6 @@ class TextEditor extends Model
# Essential: Returns {Boolean} `true` if this editor has no content.
isEmpty: -> @buffer.isEmpty()
- # Returns {Boolean} `true` if this editor is pending and `false` if it is permanent.
- isPending: -> Boolean(@pending)
-
- # Copies the current file path to the native clipboard.
- copyPathToClipboard: (relative = false) ->
- if filePath = @getPath()
- filePath = atom.project.relativize(filePath) if relative
- @clipboard.write(filePath)
-
###
Section: File Operations
###
@@ -702,25 +776,6 @@ class TextEditor extends Model
# via {Pane::saveItemAs}.
getSaveDialogOptions: -> {}
- checkoutHeadRevision: ->
- if @getPath()
- checkoutHead = =>
- @project.repositoryForDirectory(new Directory(@getDirectoryPath()))
- .then (repository) =>
- repository?.async.checkoutHeadForEditor(this)
-
- if @config.get('editor.confirmCheckoutHeadRevision')
- @applicationDelegate.confirm
- message: 'Confirm Checkout HEAD Revision'
- detailedMessage: "Are you sure you want to discard all changes to \"#{@getFileName()}\" since the last Git commit?"
- buttons:
- OK: checkoutHead
- Cancel: null
- else
- checkoutHead()
- else
- Promise.resolve(false)
-
###
Section: Reading Text
###
@@ -741,7 +796,7 @@ class TextEditor extends Model
# Essential: Returns a {Number} representing the number of screen lines in the
# editor. This accounts for folds.
- getScreenLineCount: -> @displayBuffer.getLineCount()
+ getScreenLineCount: -> @displayLayer.getScreenLineCount()
# Essential: Returns a {Number} representing the last zero-indexed buffer row
# number of the editor.
@@ -749,7 +804,7 @@ class TextEditor extends Model
# Essential: Returns a {Number} representing the last zero-indexed screen row
# number of the editor.
- getLastScreenRow: -> @displayBuffer.getLastRow()
+ getLastScreenRow: -> @getScreenLineCount() - 1
# Essential: Returns a {String} representing the contents of the line at the
# given buffer row.
@@ -761,29 +816,43 @@ class TextEditor extends Model
# given screen row.
#
# * `screenRow` A {Number} representing a zero-indexed screen row.
- lineTextForScreenRow: (screenRow) -> @displayBuffer.tokenizedLineForScreenRow(screenRow)?.text
+ lineTextForScreenRow: (screenRow) ->
+ @screenLineForScreenRow(screenRow)?.lineText
- # Gets the screen line for the given screen row.
- #
- # * `screenRow` - A {Number} indicating the screen row.
- #
- # Returns {TokenizedLine}
- tokenizedLineForScreenRow: (screenRow) -> @displayBuffer.tokenizedLineForScreenRow(screenRow)
+ logScreenLines: (start=0, end=@getLastScreenRow()) ->
+ for row in [start..end]
+ line = @lineTextForScreenRow(row)
+ console.log row, @bufferRowForScreenRow(row), line, line.length
+ return
- # {Delegates to: DisplayBuffer.tokenizedLinesForScreenRows}
- tokenizedLinesForScreenRows: (start, end) -> @displayBuffer.tokenizedLinesForScreenRows(start, end)
+ tokensForScreenRow: (screenRow) ->
+ for tagCode in @screenLineForScreenRow(screenRow).tagCodes when @displayLayer.isOpenTagCode(tagCode)
+ @displayLayer.tagForCode(tagCode)
- bufferRowForScreenRow: (row) -> @displayBuffer.bufferRowForScreenRow(row)
+ screenLineForScreenRow: (screenRow) ->
+ return if screenRow < 0 or screenRow > @getLastScreenRow()
+ @displayLayer.getScreenLines(screenRow, screenRow + 1)[0]
- # {Delegates to: DisplayBuffer.bufferRowsForScreenRows}
- bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow)
+ bufferRowForScreenRow: (screenRow) ->
+ @displayLayer.translateScreenPosition(Point(screenRow, 0)).row
- screenRowForBufferRow: (row) -> @displayBuffer.screenRowForBufferRow(row)
+ bufferRowsForScreenRows: (startScreenRow, endScreenRow) ->
+ for screenRow in [startScreenRow..endScreenRow]
+ @bufferRowForScreenRow(screenRow)
- # {Delegates to: DisplayBuffer.getMaxLineLength}
- getMaxScreenLineLength: -> @displayBuffer.getMaxLineLength()
+ screenRowForBufferRow: (row) ->
+ if @largeFileMode
+ row
+ else
+ @displayLayer.translateBufferPosition(Point(row, 0)).row
- getLongestScreenRow: -> @displayBuffer.getLongestScreenRow()
+ getRightmostScreenPosition: -> @displayLayer.getRightmostScreenPosition()
+
+ getMaxScreenLineLength: -> @getRightmostScreenPosition().column
+
+ getLongestScreenRow: -> @getRightmostScreenPosition().row
+
+ lineLengthForScreenRow: (screenRow) -> @displayLayer.lineLengthForScreenRow(screenRow)
# Returns the range for the given buffer row.
#
@@ -891,8 +960,7 @@ class TextEditor extends Model
# Move lines intersecting the most recent selection or multiple selections
# up by one row in screen coordinates.
moveLineUp: ->
- selections = @getSelectedBufferRanges()
- selections.sort (a, b) -> a.compare(b)
+ selections = @getSelectedBufferRanges().sort((a, b) -> a.compare(b))
if selections[0].start.row is 0
return
@@ -913,58 +981,38 @@ class TextEditor extends Model
selection.end.row = selections[0].end.row
selections.shift()
- # Compute the range spanned by all these selections...
- linesRangeStart = [selection.start.row, 0]
+ # Compute the buffer range spanned by all these selections, expanding it
+ # so that it includes any folded region that intersects them.
+ startRow = selection.start.row
+ endRow = selection.end.row
if selection.end.row > selection.start.row and selection.end.column is 0
# Don't move the last line of a multi-line selection if the selection ends at column 0
- linesRange = new Range(linesRangeStart, selection.end)
- else
- linesRange = new Range(linesRangeStart, [selection.end.row + 1, 0])
+ endRow--
- # If there's a fold containing either the starting row or the end row
- # of the selection then the whole fold needs to be moved and restored.
- # The initial fold range is stored and will be translated once the
- # insert delta is know.
- selectionFoldRanges = []
- foldAtSelectionStart =
- @displayBuffer.largestFoldContainingBufferRow(selection.start.row)
- foldAtSelectionEnd =
- @displayBuffer.largestFoldContainingBufferRow(selection.end.row)
- if fold = foldAtSelectionStart ? foldAtSelectionEnd
- selectionFoldRanges.push range = fold.getBufferRange()
- newEndRow = range.end.row + 1
- linesRange.end.row = newEndRow if newEndRow > linesRange.end.row
- fold.destroy()
+ {bufferRow: startRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow)
+ {bufferRow: endRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow)
+ linesRange = new Range(Point(startRow, 0), Point(endRow, 0))
# If selected line range is preceded by a fold, one line above on screen
# could be multiple lines in the buffer.
- precedingScreenRow = @screenRowForBufferRow(linesRange.start.row) - 1
- precedingBufferRow = @bufferRowForScreenRow(precedingScreenRow)
- insertDelta = linesRange.start.row - precedingBufferRow
+ {bufferRow: precedingRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow - 1)
+ insertDelta = linesRange.start.row - precedingRow
# Any folds in the text that is moved will need to be re-created.
# It includes the folds that were intersecting with the selection.
- rangesToRefold = selectionFoldRanges.concat(
- @outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) ->
- range = fold.getBufferRange()
- fold.destroy()
- range
- ).map (range) -> range.translate([-insertDelta, 0])
-
- # Make sure the inserted text doesn't go into an existing fold
- if fold = @displayBuffer.largestFoldStartingAtBufferRow(precedingBufferRow)
- rangesToRefold.push(fold.getBufferRange().translate([linesRange.getRowCount() - 1, 0]))
- fold.destroy()
+ rangesToRefold = @displayLayer
+ .destroyFoldsIntersectingBufferRange(linesRange)
+ .map((range) -> range.translate([-insertDelta, 0]))
# Delete lines spanned by selection and insert them on the preceding buffer row
lines = @buffer.getTextInRange(linesRange)
lines += @buffer.lineEndingForRow(linesRange.end.row - 1) unless lines[lines.length - 1] is '\n'
@buffer.delete(linesRange)
- @buffer.insert([precedingBufferRow, 0], lines)
+ @buffer.insert([precedingRow, 0], lines)
# Restore folds that existed before the lines were moved
for rangeToRefold in rangesToRefold
- @displayBuffer.createFold(rangeToRefold.start.row, rangeToRefold.end.row)
+ @displayLayer.foldBufferRange(rangeToRefold)
for selection in selectionsToMove
newSelectionRanges.push(selection.translate([-insertDelta, 0]))
@@ -995,63 +1043,42 @@ class TextEditor extends Model
selection.start.row = selections[0].start.row
selections.shift()
- # Compute the range spanned by all these selections...
- linesRangeStart = [selection.start.row, 0]
+ # Compute the buffer range spanned by all these selections, expanding it
+ # so that it includes any folded region that intersects them.
+ startRow = selection.start.row
+ endRow = selection.end.row
if selection.end.row > selection.start.row and selection.end.column is 0
# Don't move the last line of a multi-line selection if the selection ends at column 0
- linesRange = new Range(linesRangeStart, selection.end)
- else
- linesRange = new Range(linesRangeStart, [selection.end.row + 1, 0])
+ endRow--
- # If there's a fold containing either the starting row or the end row
- # of the selection then the whole fold needs to be moved and restored.
- # The initial fold range is stored and will be translated once the
- # insert delta is know.
- selectionFoldRanges = []
- foldAtSelectionStart =
- @displayBuffer.largestFoldContainingBufferRow(selection.start.row)
- foldAtSelectionEnd =
- @displayBuffer.largestFoldContainingBufferRow(selection.end.row)
- if fold = foldAtSelectionStart ? foldAtSelectionEnd
- selectionFoldRanges.push range = fold.getBufferRange()
- newEndRow = range.end.row + 1
- linesRange.end.row = newEndRow if newEndRow > linesRange.end.row
- fold.destroy()
+ {bufferRow: startRow} = @displayLayer.lineStartBoundaryForBufferRow(startRow)
+ {bufferRow: endRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow)
+ linesRange = new Range(Point(startRow, 0), Point(endRow, 0))
# If selected line range is followed by a fold, one line below on screen
# could be multiple lines in the buffer. But at the same time, if the
# next buffer row is wrapped, one line in the buffer can represent many
# screen rows.
- followingScreenRow = @displayBuffer.lastScreenRowForBufferRow(linesRange.end.row) + 1
- followingBufferRow = @bufferRowForScreenRow(followingScreenRow)
- insertDelta = followingBufferRow - linesRange.end.row
+ {bufferRow: followingRow} = @displayLayer.lineEndBoundaryForBufferRow(endRow)
+ insertDelta = followingRow - linesRange.end.row
# Any folds in the text that is moved will need to be re-created.
# It includes the folds that were intersecting with the selection.
- rangesToRefold = selectionFoldRanges.concat(
- @outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) ->
- range = fold.getBufferRange()
- fold.destroy()
- range
- ).map (range) -> range.translate([insertDelta, 0])
-
- # Make sure the inserted text doesn't go into an existing fold
- if fold = @displayBuffer.largestFoldStartingAtBufferRow(followingBufferRow)
- rangesToRefold.push(fold.getBufferRange().translate([insertDelta - 1, 0]))
- fold.destroy()
+ rangesToRefold = @displayLayer
+ .destroyFoldsIntersectingBufferRange(linesRange)
+ .map((range) -> range.translate([insertDelta, 0]))
# Delete lines spanned by selection and insert them on the following correct buffer row
- insertPosition = new Point(selection.translate([insertDelta, 0]).start.row, 0)
lines = @buffer.getTextInRange(linesRange)
- if linesRange.end.row is @buffer.getLastRow()
+ if followingRow - 1 is @buffer.getLastRow()
lines = "\n#{lines}"
+ @buffer.insert([followingRow, 0], lines)
@buffer.delete(linesRange)
- @buffer.insert(insertPosition, lines)
# Restore folds that existed before the lines were moved
for rangeToRefold in rangesToRefold
- @displayBuffer.createFold(rangeToRefold.start.row, rangeToRefold.end.row)
+ @displayLayer.foldBufferRange(rangeToRefold)
for selection in selectionsToMove
newSelectionRanges.push(selection.translate([insertDelta, 0]))
@@ -1060,6 +1087,50 @@ class TextEditor extends Model
@autoIndentSelectedRows() if @shouldAutoIndent()
@scrollToBufferPosition([newSelectionRanges[0].start.row - 1, 0])
+ # Move any active selections one column to the left.
+ moveSelectionLeft: ->
+ selections = @getSelectedBufferRanges()
+ noSelectionAtStartOfLine = selections.every((selection) ->
+ selection.start.column isnt 0
+ )
+
+ translationDelta = [0, -1]
+ translatedRanges = []
+
+ if noSelectionAtStartOfLine
+ @transact =>
+ for selection in selections
+ charToLeftOfSelection = new Range(selection.start.translate(translationDelta), selection.start)
+ charTextToLeftOfSelection = @buffer.getTextInRange(charToLeftOfSelection)
+
+ @buffer.insert(selection.end, charTextToLeftOfSelection)
+ @buffer.delete(charToLeftOfSelection)
+ translatedRanges.push(selection.translate(translationDelta))
+
+ @setSelectedBufferRanges(translatedRanges)
+
+ # Move any active selections one column to the right.
+ moveSelectionRight: ->
+ selections = @getSelectedBufferRanges()
+ noSelectionAtEndOfLine = selections.every((selection) =>
+ selection.end.column isnt @buffer.lineLengthForRow(selection.end.row)
+ )
+
+ translationDelta = [0, 1]
+ translatedRanges = []
+
+ if noSelectionAtEndOfLine
+ @transact =>
+ for selection in selections
+ charToRightOfSelection = new Range(selection.end, selection.end.translate(translationDelta))
+ charTextToRightOfSelection = @buffer.getTextInRange(charToRightOfSelection)
+
+ @buffer.delete(charToRightOfSelection)
+ @buffer.insert(selection.start, charTextToRightOfSelection)
+ translatedRanges.push(selection.translate(translationDelta))
+
+ @setSelectedBufferRanges(translatedRanges)
+
# Duplicate the most recent cursor's current line.
duplicateLines: ->
@transact =>
@@ -1067,15 +1138,12 @@ class TextEditor extends Model
selectedBufferRange = selection.getBufferRange()
if selection.isEmpty()
{start} = selection.getScreenRange()
- selection.selectToScreenPosition([start.row + 1, 0])
+ selection.setScreenRange([[start.row, 0], [start.row + 1, 0]], preserveFolds: true)
[startRow, endRow] = selection.getBufferRowRange()
endRow++
- foldedRowRanges =
- @outermostFoldsInBufferRowRange(startRow, endRow)
- .map (fold) -> fold.getBufferRowRange()
-
+ intersectingFolds = @displayLayer.foldsIntersectingBufferRange([[startRow, 0], [endRow, 0]])
rangeToDuplicate = [[startRow, 0], [endRow, 0]]
textToDuplicate = @getTextInBufferRange(rangeToDuplicate)
textToDuplicate = '\n' + textToDuplicate if endRow > @getLastBufferRow()
@@ -1083,8 +1151,9 @@ class TextEditor extends Model
delta = endRow - startRow
selection.setBufferRange(selectedBufferRange.translate([delta, 0]))
- for [foldStartRow, foldEndRow] in foldedRowRanges
- @createFold(foldStartRow + delta, foldEndRow + delta)
+ for fold in intersectingFolds
+ foldRange = @displayLayer.bufferRangeForFold(fold)
+ @displayLayer.foldBufferRange(foldRange.translate([delta, 0]))
return
replaceSelectedText: (options={}, fn) ->
@@ -1323,7 +1392,18 @@ class TextEditor extends Model
# * `options` (optional) An options hash for {::clipScreenPosition}.
#
# Returns a {Point}.
- screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options)
+ screenPositionForBufferPosition: (bufferPosition, options) ->
+ if options?.clip?
+ Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.")
+ options.clipDirection ?= options.clip
+ if options?.wrapAtSoftNewlines?
+ Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward'
+ if options?.wrapBeyondNewlines?
+ Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward'
+
+ @displayLayer.translateBufferPosition(bufferPosition, options)
# Essential: Convert a position in screen-coordinates to buffer-coordinates.
#
@@ -1333,21 +1413,40 @@ class TextEditor extends Model
# * `options` (optional) An options hash for {::clipScreenPosition}.
#
# Returns a {Point}.
- bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options)
+ bufferPositionForScreenPosition: (screenPosition, options) ->
+ if options?.clip?
+ Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.")
+ options.clipDirection ?= options.clip
+ if options?.wrapAtSoftNewlines?
+ Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward'
+ if options?.wrapBeyondNewlines?
+ Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward'
+
+ @displayLayer.translateScreenPosition(screenPosition, options)
# Essential: Convert a range in buffer-coordinates to screen-coordinates.
#
# * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates.
#
# Returns a {Range}.
- screenRangeForBufferRange: (bufferRange) -> @displayBuffer.screenRangeForBufferRange(bufferRange)
+ screenRangeForBufferRange: (bufferRange, options) ->
+ bufferRange = Range.fromObject(bufferRange)
+ start = @screenPositionForBufferPosition(bufferRange.start, options)
+ end = @screenPositionForBufferPosition(bufferRange.end, options)
+ new Range(start, end)
# Essential: Convert a range in screen-coordinates to buffer-coordinates.
#
# * `screenRange` {Range} in screen coordinates to translate into buffer coordinates.
#
# Returns a {Range}.
- bufferRangeForScreenRange: (screenRange) -> @displayBuffer.bufferRangeForScreenRange(screenRange)
+ bufferRangeForScreenRange: (screenRange) ->
+ screenRange = Range.fromObject(screenRange)
+ start = @bufferPositionForScreenPosition(screenRange.start)
+ end = @bufferPositionForScreenPosition(screenRange.end)
+ new Range(start, end)
# Extended: Clip the given {Point} to a valid position in the buffer.
#
@@ -1396,26 +1495,44 @@ class TextEditor extends Model
#
# * `screenPosition` The {Point} representing the position to clip.
# * `options` (optional) {Object}
- # * `wrapBeyondNewlines` {Boolean} if `true`, continues wrapping past newlines
- # * `wrapAtSoftNewlines` {Boolean} if `true`, continues wrapping past soft newlines
- # * `screenLine` {Boolean} if `true`, indicates that you're using a line number, not a row number
+ # * `clipDirection` {String} If `'backward'`, returns the first valid
+ # position preceding an invalid position. If `'forward'`, returns the
+ # first valid position following an invalid position. If `'closest'`,
+ # returns the first valid position closest to an invalid position.
+ # Defaults to `'closest'`.
#
# Returns a {Point}.
- clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options)
+ clipScreenPosition: (screenPosition, options) ->
+ if options?.clip?
+ Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.")
+ options.clipDirection ?= options.clip
+ if options?.wrapAtSoftNewlines?
+ Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward'
+ if options?.wrapBeyondNewlines?
+ Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward'
+
+ @displayLayer.clipScreenPosition(screenPosition, options)
# Extended: Clip the start and end of the given range to valid positions on screen.
# See {::clipScreenPosition} for more information.
#
# * `range` The {Range} to clip.
# * `options` (optional) See {::clipScreenPosition} `options`.
+ #
# Returns a {Range}.
- clipScreenRange: (range, options) -> @displayBuffer.clipScreenRange(range, options)
+ clipScreenRange: (screenRange, options) ->
+ screenRange = Range.fromObject(screenRange)
+ start = @displayLayer.clipScreenPosition(screenRange.start, options)
+ end = @displayLayer.clipScreenPosition(screenRange.end, options)
+ Range(start, end)
###
Section: Decorations
###
- # Essential: Add a decoration that tracks a {TextEditorMarker}. When the
+ # Essential: Add a decoration that tracks a {DisplayMarker}. When the
# marker moves, is invalidated, or is destroyed, the decoration will be
# updated to reflect the marker's state.
#
@@ -1436,8 +1553,8 @@ class TextEditor extends Model
#
# ```
# * __overlay__: Positions the view associated with the given item at the head
- # or tail of the given `TextEditorMarker`.
- # * __gutter__: A decoration that tracks a {TextEditorMarker} in a {Gutter}. Gutter
+ # or tail of the given `DisplayMarker`.
+ # * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter
# decorations are created by calling {Gutter::decorateMarker} on the
# desired `Gutter` instance.
# * __block__: Positions the view associated with the given item before or
@@ -1445,21 +1562,21 @@ class TextEditor extends Model
#
# ## Arguments
#
- # * `marker` A {TextEditorMarker} you want this decoration to follow.
+ # * `marker` A {DisplayMarker} you want this decoration to follow.
# * `decorationParams` An {Object} representing the decoration e.g.
# `{type: 'line-number', class: 'linter-error'}`
# * `type` There are several supported decoration types. The behavior of the
# types are as follows:
# * `line` Adds the given `class` to the lines overlapping the rows
- # spanned by the `TextEditorMarker`.
+ # spanned by the `DisplayMarker`.
# * `line-number` Adds the given `class` to the line numbers overlapping
- # the rows spanned by the `TextEditorMarker`.
+ # the rows spanned by the `DisplayMarker`.
# * `highlight` Creates a `.highlight` div with the nested class with up
- # to 3 nested regions that fill the area spanned by the `TextEditorMarker`.
+ # to 3 nested regions that fill the area spanned by the `DisplayMarker`.
# * `overlay` Positions the view associated with the given item at the
- # head or tail of the given `TextEditorMarker`, depending on the `position`
+ # head or tail of the given `DisplayMarker`, depending on the `position`
# property.
- # * `gutter` Tracks a {TextEditorMarker} in a {Gutter}. Created by calling
+ # * `gutter` Tracks a {DisplayMarker} in a {Gutter}. Created by calling
# {Gutter::decorateMarker} on the desired `Gutter` instance.
# * `block` Positions the view associated with the given item before or
# after the row of the given `TextEditorMarker`, depending on the `position`
@@ -1470,13 +1587,13 @@ class TextEditor extends Model
# corresponding view registered. Only applicable to the `gutter`,
# `overlay` and `block` types.
# * `onlyHead` (optional) If `true`, the decoration will only be applied to
- # the head of the `TextEditorMarker`. Only applicable to the `line` and
+ # the head of the `DisplayMarker`. Only applicable to the `line` and
# `line-number` types.
# * `onlyEmpty` (optional) If `true`, the decoration will only be applied if
- # the associated `TextEditorMarker` is empty. Only applicable to the `gutter`,
+ # the associated `DisplayMarker` is empty. Only applicable to the `gutter`,
# `line`, and `line-number` types.
# * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
- # if the associated `TextEditorMarker` is non-empty. Only applicable to the
+ # if the associated `DisplayMarker` is non-empty. Only applicable to the
# `gutter`, `line`, and `line-number` types.
# * `position` (optional) Only applicable to decorations of type `overlay` and `block`,
# controls where the view is positioned relative to the `TextEditorMarker`.
@@ -1485,21 +1602,19 @@ class TextEditor extends Model
#
# Returns a {Decoration} object
decorateMarker: (marker, decorationParams) ->
- @displayBuffer.decorateMarker(marker, decorationParams)
+ @decorationManager.decorateMarker(marker, decorationParams)
- # Essential: *Experimental:* Add a decoration to every marker in the given
- # marker layer. Can be used to decorate a large number of markers without
- # having to create and manage many individual decorations.
+ # Essential: Add a decoration to every marker in the given marker layer. Can
+ # be used to decorate a large number of markers without having to create and
+ # manage many individual decorations.
#
- # * `markerLayer` A {TextEditorMarkerLayer} or {MarkerLayer} to decorate.
+ # * `markerLayer` A {DisplayMarkerLayer} or {MarkerLayer} to decorate.
# * `decorationParams` The same parameters that are passed to
- # {decorateMarker}, except the `type` cannot be `overlay` or `gutter`.
- #
- # This API is experimental and subject to change on any release.
+ # {TextEditor::decorateMarker}, except the `type` cannot be `overlay` or `gutter`.
#
# Returns a {LayerDecoration}.
decorateMarkerLayer: (markerLayer, decorationParams) ->
- @displayBuffer.decorateMarkerLayer(markerLayer, decorationParams)
+ @decorationManager.decorateMarkerLayer(markerLayer, decorationParams)
# Deprecated: Get all the decorations within a screen row range on the default
# layer.
@@ -1509,14 +1624,14 @@ class TextEditor extends Model
#
# Returns an {Object} of decorations in the form
# `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}`
- # where the keys are {TextEditorMarker} IDs, and the values are an array of decoration
+ # where the keys are {DisplayMarker} IDs, and the values are an array of decoration
# params objects attached to the marker.
# Returns an empty object when no decorations are found
decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
- @displayBuffer.decorationsForScreenRowRange(startScreenRow, endScreenRow)
+ @decorationManager.decorationsForScreenRowRange(startScreenRow, endScreenRow)
decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
- @displayBuffer.decorationsStateForScreenRowRange(startScreenRow, endScreenRow)
+ @decorationManager.decorationsStateForScreenRowRange(startScreenRow, endScreenRow)
# Extended: Get all decorations.
#
@@ -1525,7 +1640,7 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getDecorations: (propertyFilter) ->
- @displayBuffer.getDecorations(propertyFilter)
+ @decorationManager.getDecorations(propertyFilter)
# Extended: Get all decorations of type 'line'.
#
@@ -1534,7 +1649,7 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getLineDecorations: (propertyFilter) ->
- @displayBuffer.getLineDecorations(propertyFilter)
+ @decorationManager.getLineDecorations(propertyFilter)
# Extended: Get all decorations of type 'line-number'.
#
@@ -1543,7 +1658,7 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getLineNumberDecorations: (propertyFilter) ->
- @displayBuffer.getLineNumberDecorations(propertyFilter)
+ @decorationManager.getLineNumberDecorations(propertyFilter)
# Extended: Get all decorations of type 'highlight'.
#
@@ -1552,7 +1667,7 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getHighlightDecorations: (propertyFilter) ->
- @displayBuffer.getHighlightDecorations(propertyFilter)
+ @decorationManager.getHighlightDecorations(propertyFilter)
# Extended: Get all decorations of type 'overlay'.
#
@@ -1561,13 +1676,13 @@ class TextEditor extends Model
#
# Returns an {Array} of {Decoration}s.
getOverlayDecorations: (propertyFilter) ->
- @displayBuffer.getOverlayDecorations(propertyFilter)
+ @decorationManager.getOverlayDecorations(propertyFilter)
decorationForId: (id) ->
- @displayBuffer.decorationForId(id)
+ @decorationManager.decorationForId(id)
decorationsForMarkerId: (id) ->
- @displayBuffer.decorationsForMarkerId(id)
+ @decorationManager.decorationsForMarkerId(id)
###
Section: Markers
@@ -1587,8 +1702,6 @@ class TextEditor extends Model
# operations, but uses more time and memory. (default: false)
# * `reversed` (optional) {Boolean} Creates the marker in a reversed
# orientation. (default: false)
- # * `persistent` (optional) {Boolean} Whether to include this marker when
- # serializing the buffer. (default: true)
# * `invalidate` (optional) {String} Determines the rules by which changes
# to the buffer *invalidate* the marker. (default: 'overlap') It can be
# any of the following strategies, in order of fragility:
@@ -1604,9 +1717,9 @@ class TextEditor extends Model
# region in any way, including changes that end at the marker's
# start or start at the marker's end. This is the most fragile strategy.
#
- # Returns a {TextEditorMarker}.
- markBufferRange: (args...) ->
- @displayBuffer.markBufferRange(args...)
+ # Returns a {DisplayMarker}.
+ markBufferRange: (bufferRange, options) ->
+ @defaultMarkerLayer.markBufferRange(bufferRange, options)
# Essential: Create a marker on the default marker layer with the given range
# in screen coordinates. This marker will maintain its logical location as the
@@ -1622,8 +1735,6 @@ class TextEditor extends Model
# operations, but uses more time and memory. (default: false)
# * `reversed` (optional) {Boolean} Creates the marker in a reversed
# orientation. (default: false)
- # * `persistent` (optional) {Boolean} Whether to include this marker when
- # serializing the buffer. (default: true)
# * `invalidate` (optional) {String} Determines the rules by which changes
# to the buffer *invalidate* the marker. (default: 'overlap') It can be
# any of the following strategies, in order of fragility:
@@ -1639,31 +1750,66 @@ class TextEditor extends Model
# region in any way, including changes that end at the marker's
# start or start at the marker's end. This is the most fragile strategy.
#
- # Returns a {TextEditorMarker}.
- markScreenRange: (args...) ->
- @displayBuffer.markScreenRange(args...)
+ # Returns a {DisplayMarker}.
+ markScreenRange: (screenRange, options) ->
+ @defaultMarkerLayer.markScreenRange(screenRange, options)
- # Essential: Mark the given position in buffer coordinates on the default
- # marker layer.
+ # Essential: Create a marker on the default marker layer with the given buffer
+ # position and no tail. To group multiple markers together in their own
+ # private layer, see {::addMarkerLayer}.
#
- # * `position` A {Point} or {Array} of `[row, column]`.
- # * `options` (optional) See {TextBuffer::markRange}.
+ # * `bufferPosition` A {Point} or point-compatible {Array}
+ # * `options` (optional) An {Object} with the following keys:
+ # * `invalidate` (optional) {String} Determines the rules by which changes
+ # to the buffer *invalidate* the marker. (default: 'overlap') It can be
+ # any of the following strategies, in order of fragility:
+ # * __never__: The marker is never marked as invalid. This is a good choice for
+ # markers representing selections in an editor.
+ # * __surround__: The marker is invalidated by changes that completely surround it.
+ # * __overlap__: The marker is invalidated by changes that surround the
+ # start or end of the marker. This is the default.
+ # * __inside__: The marker is invalidated by changes that extend into the
+ # inside of the marker. Changes that end at the marker's start or
+ # start at the marker's end do not invalidate the marker.
+ # * __touch__: The marker is invalidated by a change that touches the marked
+ # region in any way, including changes that end at the marker's
+ # start or start at the marker's end. This is the most fragile strategy.
#
- # Returns a {TextEditorMarker}.
- markBufferPosition: (args...) ->
- @displayBuffer.markBufferPosition(args...)
+ # Returns a {DisplayMarker}.
+ markBufferPosition: (bufferPosition, options) ->
+ @defaultMarkerLayer.markBufferPosition(bufferPosition, options)
- # Essential: Mark the given position in screen coordinates on the default
- # marker layer.
+ # Essential: Create a marker on the default marker layer with the given screen
+ # position and no tail. To group multiple markers together in their own
+ # private layer, see {::addMarkerLayer}.
#
- # * `position` A {Point} or {Array} of `[row, column]`.
- # * `options` (optional) See {TextBuffer::markRange}.
+ # * `screenPosition` A {Point} or point-compatible {Array}
+ # * `options` (optional) An {Object} with the following keys:
+ # * `invalidate` (optional) {String} Determines the rules by which changes
+ # to the buffer *invalidate* the marker. (default: 'overlap') It can be
+ # any of the following strategies, in order of fragility:
+ # * __never__: The marker is never marked as invalid. This is a good choice for
+ # markers representing selections in an editor.
+ # * __surround__: The marker is invalidated by changes that completely surround it.
+ # * __overlap__: The marker is invalidated by changes that surround the
+ # start or end of the marker. This is the default.
+ # * __inside__: The marker is invalidated by changes that extend into the
+ # inside of the marker. Changes that end at the marker's start or
+ # start at the marker's end do not invalidate the marker.
+ # * __touch__: The marker is invalidated by a change that touches the marked
+ # region in any way, including changes that end at the marker's
+ # start or start at the marker's end. This is the most fragile strategy.
+ # * `clipDirection` {String} If `'backward'`, returns the first valid
+ # position preceding an invalid position. If `'forward'`, returns the
+ # first valid position following an invalid position. If `'closest'`,
+ # returns the first valid position closest to an invalid position.
+ # Defaults to `'closest'`.
#
- # Returns a {TextEditorMarker}.
- markScreenPosition: (args...) ->
- @displayBuffer.markScreenPosition(args...)
+ # Returns a {DisplayMarker}.
+ markScreenPosition: (screenPosition, options) ->
+ @defaultMarkerLayer.markScreenPosition(screenPosition, options)
- # Essential: Find all {TextEditorMarker}s on the default marker layer that
+ # Essential: Find all {DisplayMarker}s on the default marker layer that
# match the given properties.
#
# This method finds markers based on the given properties. Markers can be
@@ -1683,20 +1829,22 @@ class TextEditor extends Model
# in range-compatible {Array} in buffer coordinates.
# * `containsBufferPosition` Only include markers containing this {Point}
# or {Array} of `[row, column]` in buffer coordinates.
- findMarkers: (properties) ->
- @displayBuffer.findMarkers(properties)
+ #
+ # Returns an {Array} of {DisplayMarker}s
+ findMarkers: (params) ->
+ @defaultMarkerLayer.findMarkers(params)
- # Extended: Get the {TextEditorMarker} on the default layer for the given
+ # Extended: Get the {DisplayMarker} on the default layer for the given
# marker id.
#
# * `id` {Number} id of the marker
getMarker: (id) ->
- @displayBuffer.getMarker(id)
+ @defaultMarkerLayer.getMarker(id)
- # Extended: Get all {TextEditorMarker}s on the default marker layer. Consider
+ # Extended: Get all {DisplayMarker}s on the default marker layer. Consider
# using {::findMarkers}
getMarkers: ->
- @displayBuffer.getMarkers()
+ @defaultMarkerLayer.getMarkers()
# Extended: Get the number of markers in the default marker layer.
#
@@ -1707,39 +1855,38 @@ class TextEditor extends Model
destroyMarker: (id) ->
@getMarker(id)?.destroy()
- # Extended: *Experimental:* Create a marker layer to group related markers.
+ # Essential: Create a marker layer to group related markers.
#
# * `options` An {Object} containing the following keys:
# * `maintainHistory` A {Boolean} indicating whether marker state should be
# restored on undo/redo. Defaults to `false`.
+ # * `persistent` A {Boolean} indicating whether or not this marker layer
+ # should be serialized and deserialized along with the rest of the
+ # buffer. Defaults to `false`. If `true`, the marker layer's id will be
+ # maintained across the serialization boundary, allowing you to retrieve
+ # it via {::getMarkerLayer}.
#
- # This API is experimental and subject to change on any release.
- #
- # Returns a {TextEditorMarkerLayer}.
+ # Returns a {DisplayMarkerLayer}.
addMarkerLayer: (options) ->
- @displayBuffer.addMarkerLayer(options)
+ @displayLayer.addMarkerLayer(options)
- # Public: *Experimental:* Get a {TextEditorMarkerLayer} by id.
+ # Essential: Get a {DisplayMarkerLayer} by id.
#
# * `id` The id of the marker layer to retrieve.
#
- # This API is experimental and subject to change on any release.
- #
- # Returns a {MarkerLayer} or `undefined` if no layer exists with the given
- # id.
+ # Returns a {DisplayMarkerLayer} or `undefined` if no layer exists with the
+ # given id.
getMarkerLayer: (id) ->
- @displayBuffer.getMarkerLayer(id)
+ @displayLayer.getMarkerLayer(id)
- # Public: *Experimental:* Get the default {TextEditorMarkerLayer}.
+ # Essential: Get the default {DisplayMarkerLayer}.
#
# All marker APIs not tied to an explicit layer interact with this default
# layer.
#
- # This API is experimental and subject to change on any release.
- #
- # Returns a {TextEditorMarkerLayer}.
+ # Returns a {DisplayMarkerLayer}.
getDefaultMarkerLayer: ->
- @displayBuffer.getDefaultMarkerLayer()
+ @defaultMarkerLayer
###
Section: Cursors
@@ -1763,7 +1910,7 @@ class TextEditor extends Model
# If there are multiple cursors, they will be consolidated to a single cursor.
#
# * `position` A {Point} or {Array} of `[row, column]`
- # * `options` (optional) An {Object} combining options for {::clipScreenPosition} with:
+ # * `options` (optional) An {Object} containing the following keys:
# * `autoscroll` Determines whether the editor scrolls to the new cursor's
# position. Defaults to true.
setCursorBufferPosition: (position, options) ->
@@ -1801,6 +1948,16 @@ class TextEditor extends Model
# * `autoscroll` Determines whether the editor scrolls to the new cursor's
# position. Defaults to true.
setCursorScreenPosition: (position, options) ->
+ if options?.clip?
+ Grim.deprecate("The `clip` parameter has been deprecated and will be removed soon. Please, use `clipDirection` instead.")
+ options.clipDirection ?= options.clip
+ if options?.wrapAtSoftNewlines?
+ Grim.deprecate("The `wrapAtSoftNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapAtSoftNewlines then 'forward' else 'backward'
+ if options?.wrapBeyondNewlines?
+ Grim.deprecate("The `wrapBeyondNewlines` parameter has been deprecated and will be removed soon. Please, use `clipDirection: 'forward'` instead.")
+ options.clipDirection ?= if options.wrapBeyondNewlines then 'forward' else 'backward'
+
@moveCursors (cursor) -> cursor.setScreenPosition(position, options)
# Essential: Add a cursor at the given position in buffer coordinates.
@@ -1809,7 +1966,7 @@ class TextEditor extends Model
#
# Returns a {Cursor}.
addCursorAtBufferPosition: (bufferPosition, options) ->
- @selectionsMarkerLayer.markBufferPosition(bufferPosition, @getSelectionMarkerAttributes())
+ @selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'})
@getLastSelection().cursor.autoscroll() unless options?.autoscroll is false
@getLastSelection().cursor
@@ -1819,7 +1976,7 @@ class TextEditor extends Model
#
# Returns a {Cursor}.
addCursorAtScreenPosition: (screenPosition, options) ->
- @selectionsMarkerLayer.markScreenPosition(screenPosition, @getSelectionMarkerAttributes())
+ @selectionsMarkerLayer.markScreenPosition(screenPosition, {invalidate: 'never'})
@getLastSelection().cursor.autoscroll() unless options?.autoscroll is false
@getLastSelection().cursor
@@ -1944,10 +2101,18 @@ class TextEditor extends Model
getCursorsOrderedByBufferPosition: ->
@getCursors().sort (a, b) -> a.compare(b)
- # Add a cursor based on the given {TextEditorMarker}.
+ cursorsForScreenRowRange: (startScreenRow, endScreenRow) ->
+ cursors = []
+ for marker in @selectionsMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow])
+ if cursor = @cursorsByMarkerId.get(marker.id)
+ cursors.push(cursor)
+ cursors
+
+ # Add a cursor based on the given {DisplayMarker}.
addCursor: (marker) ->
cursor = new Cursor(editor: this, marker: marker, config: @config)
@cursors.push(cursor)
+ @cursorsByMarkerId.set(marker.id, cursor)
@decorateMarker(marker, type: 'line-number', class: 'cursor-line')
@decorateMarker(marker, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true)
@decorateMarker(marker, type: 'line', class: 'cursor-line', onlyEmpty: true)
@@ -2094,10 +2259,14 @@ class TextEditor extends Model
# * `options` (optional) An options {Object}:
# * `reversed` A {Boolean} indicating whether to create the selection in a
# reversed orientation.
+ # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
+ # selection is set.
#
# Returns the added {Selection}.
addSelectionForBufferRange: (bufferRange, options={}) ->
- @selectionsMarkerLayer.markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options))
+ unless options.preserveFolds
+ @destroyFoldsIntersectingBufferRange(bufferRange)
+ @selectionsMarkerLayer.markBufferRange(bufferRange, {invalidate: 'never', reversed: options.reversed ? false})
@getLastSelection().autoscroll() unless options.autoscroll is false
@getLastSelection()
@@ -2107,12 +2276,11 @@ class TextEditor extends Model
# * `options` (optional) An options {Object}:
# * `reversed` A {Boolean} indicating whether to create the selection in a
# reversed orientation.
- #
+ # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the
+ # selection is set.
# Returns the added {Selection}.
addSelectionForScreenRange: (screenRange, options={}) ->
- @selectionsMarkerLayer.markScreenRange(screenRange, _.defaults(@getSelectionMarkerAttributes(), options))
- @getLastSelection().autoscroll() unless options.autoscroll is false
- @getLastSelection()
+ @addSelectionForBufferRange(@bufferRangeForScreenRange(screenRange), options)
# Essential: Select from the current cursor position to the given position in
# buffer coordinates.
@@ -2293,7 +2461,7 @@ class TextEditor extends Model
# Extended: Select the range of the given marker if it is valid.
#
- # * `marker` A {TextEditorMarker}
+ # * `marker` A {DisplayMarker}
#
# Returns the selected {Range} or `undefined` if the marker is invalid.
selectMarker: (marker) ->
@@ -2419,20 +2587,18 @@ class TextEditor extends Model
_.reduce(tail, reducer, [head])
return result if fn?
- # Add a {Selection} based on the given {TextEditorMarker}.
+ # Add a {Selection} based on the given {DisplayMarker}.
#
- # * `marker` The {TextEditorMarker} to highlight
+ # * `marker` The {DisplayMarker} to highlight
# * `options` (optional) An {Object} that pertains to the {Selection} constructor.
#
# Returns the new {Selection}.
addSelection: (marker, options={}) ->
- unless marker.getProperties().preserveFolds
- @destroyFoldsContainingBufferRange(marker.getBufferRange())
cursor = @addCursor(marker)
selection = new Selection(_.extend({editor: this, marker, cursor, @clipboard}, options))
@selections.push(selection)
selectionBufferRange = selection.getBufferRange()
- @mergeIntersectingSelections(preserveFolds: marker.getProperties().preserveFolds)
+ @mergeIntersectingSelections(preserveFolds: options.preserveFolds)
if selection.destroyed
for selection in @getSelections()
@@ -2446,6 +2612,7 @@ class TextEditor extends Model
removeSelection: (selection) ->
_.remove(@cursors, selection.cursor)
_.remove(@selections, selection)
+ @cursorsByMarkerId.delete(selection.cursor.marker.id)
@emitter.emit 'did-remove-cursor', selection.cursor
@emitter.emit 'did-remove-selection', selection
@@ -2460,6 +2627,7 @@ class TextEditor extends Model
selections = @getSelections()
if selections.length > 1
selection.destroy() for selection in selections[1...(selections.length)]
+ selections[0].autoscroll(center: true)
true
else
false
@@ -2541,14 +2709,36 @@ class TextEditor extends Model
# Essential: Get the on-screen length of tab characters.
#
# Returns a {Number}.
- getTabLength: -> @displayBuffer.getTabLength()
+ getTabLength: ->
+ if @tabLength?
+ @tabLength
+ else
+ @config.get('editor.tabLength', scope: @getRootScopeDescriptor())
# Essential: Set the on-screen length of tab characters. Setting this to a
# {Number} This will override the `editor.tabLength` setting.
#
# * `tabLength` {Number} length of a single tab. Setting to `null` will
# fallback to using the `editor.tabLength` config setting
- setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength)
+ setTabLength: (tabLength) ->
+ return if tabLength is @tabLength
+
+ @tabLength = tabLength
+ @tokenizedBuffer.setTabLength(@tabLength)
+ @resetDisplayLayer()
+
+ setIgnoreInvisibles: (ignoreInvisibles) ->
+ return if ignoreInvisibles is @ignoreInvisibles
+
+ @ignoreInvisibles = ignoreInvisibles
+ @resetDisplayLayer()
+
+ getInvisibles: ->
+ scopeDescriptor = @getRootScopeDescriptor()
+ if @config.get('editor.showInvisibles', scope: scopeDescriptor) and not @ignoreInvisibles and @showInvisibles
+ @config.get('editor.invisibles', scope: scopeDescriptor)
+ else
+ {}
# Extended: Determine if the buffer uses hard or soft tabs.
#
@@ -2559,7 +2749,7 @@ class TextEditor extends Model
# whitespace.
usesSoftTabs: ->
for bufferRow in [0..@buffer.getLastRow()]
- continue if @displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
+ continue if @tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment()
line = @buffer.lineForRow(bufferRow)
return true if line[0] is ' '
@@ -2602,14 +2792,27 @@ class TextEditor extends Model
# Essential: Determine whether lines in this editor are soft-wrapped.
#
# Returns a {Boolean}.
- isSoftWrapped: (softWrapped) -> @displayBuffer.isSoftWrapped()
+ isSoftWrapped: ->
+ if @largeFileMode
+ false
+ else
+ scopeDescriptor = @getRootScopeDescriptor()
+ @softWrapped ? @config.get('editor.softWrap', scope: scopeDescriptor) ? false
# Essential: Enable or disable soft wrapping for this editor.
#
# * `softWrapped` A {Boolean}
#
# Returns a {Boolean}.
- setSoftWrapped: (softWrapped) -> @displayBuffer.setSoftWrapped(softWrapped)
+ setSoftWrapped: (softWrapped) ->
+ if softWrapped isnt @softWrapped
+ @softWrapped = softWrapped
+ @resetDisplayLayer()
+ softWrapped = @isSoftWrapped()
+ @emitter.emit 'did-change-soft-wrapped', softWrapped
+ softWrapped
+ else
+ @isSoftWrapped()
# Essential: Toggle soft wrapping for this editor
#
@@ -2617,7 +2820,15 @@ class TextEditor extends Model
toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped())
# Essential: Gets the column at which column will soft wrap
- getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn()
+ getSoftWrapColumn: ->
+ scopeDescriptor = @getRootScopeDescriptor()
+ if @isSoftWrapped()
+ if @config.get('editor.softWrapAtPreferredLineLength', scope: scopeDescriptor)
+ @config.get('editor.preferredLineLength', scope: scopeDescriptor)
+ else
+ @getEditorWidthInChars()
+ else
+ Infinity
###
Section: Indentation
@@ -2675,7 +2886,7 @@ class TextEditor extends Model
#
# Returns a {Number}.
indentLevelForLine: (line) ->
- @displayBuffer.indentLevelForLine(line)
+ @tokenizedBuffer.indentLevelForLine(line)
# Extended: Indent rows intersecting selections based on the grammar's suggested
# indent level.
@@ -2703,7 +2914,7 @@ class TextEditor extends Model
# Essential: Get the current {Grammar} of this editor.
getGrammar: ->
- @displayBuffer.getGrammar()
+ @tokenizedBuffer.grammar
# Essential: Set the current {Grammar} of this editor.
#
@@ -2712,11 +2923,15 @@ class TextEditor extends Model
#
# * `grammar` {Grammar}
setGrammar: (grammar) ->
- @displayBuffer.setGrammar(grammar)
+ @tokenizedBuffer.setGrammar(grammar)
# Reload the grammar based on the file name.
reloadGrammar: ->
- @displayBuffer.reloadGrammar()
+ @tokenizedBuffer.reloadGrammar()
+
+ # Experimental: Get a notification when async tokenization is completed.
+ onDidTokenize: (callback) ->
+ @tokenizedBuffer.onDidTokenize(callback)
###
Section: Managing Syntax Scopes
@@ -2726,7 +2941,7 @@ class TextEditor extends Model
# e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with
# {Config::get} to get language specific config values.
getRootScopeDescriptor: ->
- @displayBuffer.getRootScopeDescriptor()
+ @tokenizedBuffer.rootScopeDescriptor
# Essential: Get the syntactic scopeDescriptor for the given position in buffer
# coordinates. Useful with {Config::get}.
@@ -2739,7 +2954,7 @@ class TextEditor extends Model
#
# Returns a {ScopeDescriptor}.
scopeDescriptorForBufferPosition: (bufferPosition) ->
- @displayBuffer.scopeDescriptorForBufferPosition(bufferPosition)
+ @tokenizedBuffer.scopeDescriptorForPosition(bufferPosition)
# Extended: Get the range in buffer coordinates of all tokens surrounding the
# cursor that match the given scope selector.
@@ -2751,7 +2966,10 @@ class TextEditor extends Model
#
# Returns a {Range}.
bufferRangeForScopeAtCursor: (scopeSelector) ->
- @displayBuffer.bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition())
+ @bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition())
+
+ bufferRangeForScopeAtPosition: (scopeSelector, position) ->
+ @tokenizedBuffer.bufferRangeForScopeAtPosition(scopeSelector, position)
# Extended: Determine if the given row is entirely a comment
isBufferRowCommented: (bufferRow) ->
@@ -2759,16 +2977,12 @@ class TextEditor extends Model
@commentScopeSelector ?= new TextMateScopeSelector('comment.*')
@commentScopeSelector.matches(@scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes)
- logCursorScope: ->
- scopeDescriptor = @getLastCursor().getScopeDescriptor()
- list = scopeDescriptor.scopes.toString().split(',')
- list = list.map (item) -> "* #{item}"
- content = "Scopes at Cursor\n#{list.join('\n')}"
+ # Get the scope descriptor at the cursor.
+ getCursorScope: ->
+ @getLastCursor().getScopeDescriptor()
- @notificationManager.addInfo(content, dismissable: true)
-
- # {Delegates to: DisplayBuffer.tokenForBufferPosition}
- tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition)
+ tokenForBufferPosition: (bufferPosition) ->
+ @tokenizedBuffer.tokenForPosition(bufferPosition)
###
Section: Clipboard Operations
@@ -2793,7 +3007,7 @@ class TextEditor extends Model
maintainClipboard = false
for selection in @getSelectionsOrderedByBufferPosition()
if not selection.isEmpty()
- selection.copy(maintainClipboard, true)
+ selection.copy(maintainClipboard, false)
maintainClipboard = true
return
@@ -2900,7 +3114,7 @@ class TextEditor extends Model
#
# * `bufferRow` A {Number}
unfoldBufferRow: (bufferRow) ->
- @displayBuffer.unfoldBufferRow(bufferRow)
+ @displayLayer.destroyFoldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity)))
# Extended: For each selection, fold the rows it intersects.
foldSelectedLines: ->
@@ -2914,6 +3128,7 @@ class TextEditor extends Model
# Extended: Unfold all existing folds.
unfoldAll: ->
@languageMode.unfoldAll()
+ @scrollToCursorPosition()
# Extended: Fold all foldable lines at the given indent level.
#
@@ -2929,8 +3144,7 @@ class TextEditor extends Model
#
# Returns a {Boolean}.
isFoldableAtBufferRow: (bufferRow) ->
- # @languageMode.isFoldableAtBufferRow(bufferRow)
- @displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow)?.foldable ? false
+ @tokenizedBuffer.isFoldableAtRow(bufferRow)
# Extended: Determine whether the given row in screen coordinates is foldable.
#
@@ -2940,8 +3154,7 @@ class TextEditor extends Model
#
# Returns a {Boolean}.
isFoldableAtScreenRow: (screenRow) ->
- bufferRow = @displayBuffer.bufferRowForScreenRow(screenRow)
- @isFoldableAtBufferRow(bufferRow)
+ @isFoldableAtBufferRow(@bufferRowForScreenRow(screenRow))
# Extended: Fold the given buffer row if it isn't currently folded, and unfold
# it otherwise.
@@ -2963,7 +3176,7 @@ class TextEditor extends Model
#
# Returns a {Boolean}.
isFoldedAtBufferRow: (bufferRow) ->
- @displayBuffer.isFoldedAtBufferRow(bufferRow)
+ @displayLayer.foldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))).length > 0
# Extended: Determine whether the given row in screen coordinates is folded.
#
@@ -2971,41 +3184,23 @@ class TextEditor extends Model
#
# Returns a {Boolean}.
isFoldedAtScreenRow: (screenRow) ->
- @displayBuffer.isFoldedAtScreenRow(screenRow)
+ @isFoldedAtBufferRow(@bufferRowForScreenRow(screenRow))
- # TODO: Rename to foldRowRange?
- createFold: (startRow, endRow) ->
- @displayBuffer.createFold(startRow, endRow)
+ # Creates a new fold between two row numbers.
+ #
+ # startRow - The row {Number} to start folding at
+ # endRow - The row {Number} to end the fold
+ #
+ # Returns the new {Fold}.
+ foldBufferRowRange: (startRow, endRow) ->
+ @foldBufferRange(Range(Point(startRow, Infinity), Point(endRow, Infinity)))
- # {Delegates to: DisplayBuffer.destroyFoldWithId}
- destroyFoldWithId: (id) ->
- @displayBuffer.destroyFoldWithId(id)
+ foldBufferRange: (range) ->
+ @displayLayer.foldBufferRange(range)
# Remove any {Fold}s found that intersect the given buffer range.
destroyFoldsIntersectingBufferRange: (bufferRange) ->
- @destroyFoldsContainingBufferRange(bufferRange)
-
- for row in [bufferRange.end.row..bufferRange.start.row]
- fold.destroy() for fold in @displayBuffer.foldsStartingAtBufferRow(row)
-
- return
-
- # Remove any {Fold}s found that contain the given buffer range.
- destroyFoldsContainingBufferRange: (bufferRange) ->
- @unfoldBufferRow(bufferRange.start.row)
- @unfoldBufferRow(bufferRange.end.row)
-
- # {Delegates to: DisplayBuffer.largestFoldContainingBufferRow}
- largestFoldContainingBufferRow: (bufferRow) ->
- @displayBuffer.largestFoldContainingBufferRow(bufferRow)
-
- # {Delegates to: DisplayBuffer.largestFoldStartingAtScreenRow}
- largestFoldStartingAtScreenRow: (screenRow) ->
- @displayBuffer.largestFoldStartingAtScreenRow(screenRow)
-
- # {Delegates to: DisplayBuffer.outermostFoldsForBufferRowRange}
- outermostFoldsInBufferRowRange: (startRow, endRow) ->
- @displayBuffer.outermostFoldsInBufferRowRange(startRow, endRow)
+ @displayLayer.destroyFoldsIntersectingBufferRange(bufferRange)
###
Section: Gutters
@@ -3056,7 +3251,7 @@ class TextEditor extends Model
# * `options` (optional) {Object}
# * `center` Center the editor around the position if possible. (default: false)
scrollToBufferPosition: (bufferPosition, options) ->
- @displayBuffer.scrollToBufferPosition(bufferPosition, options)
+ @scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition), options)
# Essential: Scrolls the editor to the given screen position.
#
@@ -3065,29 +3260,31 @@ class TextEditor extends Model
# * `options` (optional) {Object}
# * `center` Center the editor around the position if possible. (default: false)
scrollToScreenPosition: (screenPosition, options) ->
- @displayBuffer.scrollToScreenPosition(screenPosition, options)
+ @scrollToScreenRange(new Range(screenPosition, screenPosition), options)
scrollToTop: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.")
- @viewRegistry.getView(this).scrollToTop()
+ @getElement().scrollToTop()
scrollToBottom: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.")
- @viewRegistry.getView(this).scrollToBottom()
+ @getElement().scrollToBottom()
- scrollToScreenRange: (screenRange, options) -> @displayBuffer.scrollToScreenRange(screenRange, options)
+ scrollToScreenRange: (screenRange, options = {}) ->
+ scrollEvent = {screenRange, options}
+ @emitter.emit "did-request-autoscroll", scrollEvent
getHorizontalScrollbarHeight: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.")
- @viewRegistry.getView(this).getHorizontalScrollbarHeight()
+ @getElement().getHorizontalScrollbarHeight()
getVerticalScrollbarWidth: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.")
- @viewRegistry.getView(this).getVerticalScrollbarWidth()
+ @getElement().getVerticalScrollbarWidth()
pageUp: ->
@moveUp(@getRowsPerPage())
@@ -3133,6 +3330,10 @@ class TextEditor extends Model
Section: TextEditor Rendering
###
+ # Get the Element for the editor.
+ getElement: ->
+ @editorElement ?= new TextEditorElement().initialize(this, atom, @autoHeight, @scrollPastEnd)
+
# Essential: Retrieves the greyed out placeholder of a mini editor.
#
# Returns a {String}.
@@ -3150,68 +3351,84 @@ class TextEditor extends Model
pixelPositionForBufferPosition: (bufferPosition) ->
Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead")
- @viewRegistry.getView(this).pixelPositionForBufferPosition(bufferPosition)
+ @getElement().pixelPositionForBufferPosition(bufferPosition)
pixelPositionForScreenPosition: (screenPosition) ->
Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead")
- @viewRegistry.getView(this).pixelPositionForScreenPosition(screenPosition)
+ @getElement().pixelPositionForScreenPosition(screenPosition)
- getSelectionMarkerAttributes: ->
- {type: 'selection', invalidate: 'never'}
+ getVerticalScrollMargin: ->
+ maxScrollMargin = Math.floor(((@height / @getLineHeightInPixels()) - 1) / 2)
+ Math.min(@verticalScrollMargin, maxScrollMargin)
- getVerticalScrollMargin: -> @displayBuffer.getVerticalScrollMargin()
- setVerticalScrollMargin: (verticalScrollMargin) -> @displayBuffer.setVerticalScrollMargin(verticalScrollMargin)
+ setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin
- getHorizontalScrollMargin: -> @displayBuffer.getHorizontalScrollMargin()
- setHorizontalScrollMargin: (horizontalScrollMargin) -> @displayBuffer.setHorizontalScrollMargin(horizontalScrollMargin)
+ getHorizontalScrollMargin: -> Math.min(@horizontalScrollMargin, Math.floor(((@width / @getDefaultCharWidth()) - 1) / 2))
+ setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin
- getLineHeightInPixels: -> @displayBuffer.getLineHeightInPixels()
- setLineHeightInPixels: (lineHeightInPixels) -> @displayBuffer.setLineHeightInPixels(lineHeightInPixels)
+ getLineHeightInPixels: -> @lineHeightInPixels
+ setLineHeightInPixels: (@lineHeightInPixels) -> @lineHeightInPixels
- getKoreanCharWidth: -> @displayBuffer.getKoreanCharWidth()
+ getKoreanCharWidth: -> @koreanCharWidth
+ getHalfWidthCharWidth: -> @halfWidthCharWidth
+ getDoubleWidthCharWidth: -> @doubleWidthCharWidth
+ getDefaultCharWidth: -> @defaultCharWidth
- getHalfWidthCharWidth: -> @displayBuffer.getHalfWidthCharWidth()
+ ratioForCharacter: (character) ->
+ if isKoreanCharacter(character)
+ @getKoreanCharWidth() / @getDefaultCharWidth()
+ else if isHalfWidthCharacter(character)
+ @getHalfWidthCharWidth() / @getDefaultCharWidth()
+ else if isDoubleWidthCharacter(character)
+ @getDoubleWidthCharWidth() / @getDefaultCharWidth()
+ else
+ 1
- getDoubleWidthCharWidth: -> @displayBuffer.getDoubleWidthCharWidth()
-
- getDefaultCharWidth: -> @displayBuffer.getDefaultCharWidth()
setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) ->
- @displayBuffer.setDefaultCharWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
+ doubleWidthCharWidth ?= defaultCharWidth
+ halfWidthCharWidth ?= defaultCharWidth
+ koreanCharWidth ?= defaultCharWidth
+ if defaultCharWidth isnt @defaultCharWidth or doubleWidthCharWidth isnt @doubleWidthCharWidth and halfWidthCharWidth isnt @halfWidthCharWidth and koreanCharWidth isnt @koreanCharWidth
+ @defaultCharWidth = defaultCharWidth
+ @doubleWidthCharWidth = doubleWidthCharWidth
+ @halfWidthCharWidth = halfWidthCharWidth
+ @koreanCharWidth = koreanCharWidth
+ @resetDisplayLayer() if @isSoftWrapped() and @getEditorWidthInChars()?
+ defaultCharWidth
setHeight: (height, reentrant=false) ->
if reentrant
- @displayBuffer.setHeight(height)
+ @height = height
else
Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.")
- @viewRegistry.getView(this).setHeight(height)
+ @getElement().setHeight(height)
getHeight: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.")
- @displayBuffer.getHeight()
-
- getClientHeight: -> @displayBuffer.getClientHeight()
+ @height
setWidth: (width, reentrant=false) ->
if reentrant
- @displayBuffer.setWidth(width)
+ oldWidth = @width
+ @width = width
+ @resetDisplayLayer() if width isnt oldWidth and @isSoftWrapped()
+ @width
else
Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.")
- @viewRegistry.getView(this).setWidth(width)
+ @getElement().setWidth(width)
getWidth: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.")
- @displayBuffer.getWidth()
+ @width
# Experimental: Scroll the editor such that the given screen row is at the
# top of the visible area.
setFirstVisibleScreenRow: (screenRow, fromView) ->
unless fromView
- maxScreenRow = @getLineCount() - 1
- unless @config.get('editor.scrollPastEnd')
- height = @displayBuffer.getHeight()
- lineHeightInPixels = @displayBuffer.getLineHeightInPixels()
- if height? and lineHeightInPixels?
- maxScreenRow -= Math.floor(height / lineHeightInPixels)
+ maxScreenRow = @getScreenLineCount() - 1
+ unless @config.get('editor.scrollPastEnd') and @scrollPastEnd
+ if @height? and @lineHeightInPixels?
+ maxScreenRow -= Math.floor(@height / @lineHeightInPixels)
screenRow = Math.max(Math.min(screenRow, maxScreenRow), 0)
unless screenRow is @firstVisibleScreenRow
@@ -3221,10 +3438,8 @@ class TextEditor extends Model
getFirstVisibleScreenRow: -> @firstVisibleScreenRow
getLastVisibleScreenRow: ->
- height = @displayBuffer.getHeight()
- lineHeightInPixels = @displayBuffer.getLineHeightInPixels()
- if height? and lineHeightInPixels?
- Math.min(@firstVisibleScreenRow + Math.floor(height / lineHeightInPixels), @getLineCount() - 1)
+ if @height? and @lineHeightInPixels?
+ Math.min(@firstVisibleScreenRow + Math.floor(@height / @lineHeightInPixels), @getScreenLineCount() - 1)
else
null
@@ -3240,77 +3455,77 @@ class TextEditor extends Model
getScrollTop: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.")
- @viewRegistry.getView(this).getScrollTop()
+ @getElement().getScrollTop()
setScrollTop: (scrollTop) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollTop instead.")
- @viewRegistry.getView(this).setScrollTop(scrollTop)
+ @getElement().setScrollTop(scrollTop)
getScrollBottom: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollBottom instead.")
- @viewRegistry.getView(this).getScrollBottom()
+ @getElement().getScrollBottom()
setScrollBottom: (scrollBottom) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollBottom instead.")
- @viewRegistry.getView(this).setScrollBottom(scrollBottom)
+ @getElement().setScrollBottom(scrollBottom)
getScrollLeft: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollLeft instead.")
- @viewRegistry.getView(this).getScrollLeft()
+ @getElement().getScrollLeft()
setScrollLeft: (scrollLeft) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollLeft instead.")
- @viewRegistry.getView(this).setScrollLeft(scrollLeft)
+ @getElement().setScrollLeft(scrollLeft)
getScrollRight: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollRight instead.")
- @viewRegistry.getView(this).getScrollRight()
+ @getElement().getScrollRight()
setScrollRight: (scrollRight) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollRight instead.")
- @viewRegistry.getView(this).setScrollRight(scrollRight)
+ @getElement().setScrollRight(scrollRight)
getScrollHeight: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollHeight instead.")
- @viewRegistry.getView(this).getScrollHeight()
+ @getElement().getScrollHeight()
getScrollWidth: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollWidth instead.")
- @viewRegistry.getView(this).getScrollWidth()
+ @getElement().getScrollWidth()
getMaxScrollTop: ->
Grim.deprecate("This is now a view method. Call TextEditorElement::getMaxScrollTop instead.")
- @viewRegistry.getView(this).getMaxScrollTop()
+ @getElement().getMaxScrollTop()
intersectsVisibleRowRange: (startRow, endRow) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.")
- @viewRegistry.getView(this).intersectsVisibleRowRange(startRow, endRow)
+ @getElement().intersectsVisibleRowRange(startRow, endRow)
selectionIntersectsVisibleRowRange: (selection) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.")
- @viewRegistry.getView(this).selectionIntersectsVisibleRowRange(selection)
+ @getElement().selectionIntersectsVisibleRowRange(selection)
screenPositionForPixelPosition: (pixelPosition) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.")
- @viewRegistry.getView(this).screenPositionForPixelPosition(pixelPosition)
+ @getElement().screenPositionForPixelPosition(pixelPosition)
pixelRectForScreenRange: (screenRange) ->
Grim.deprecate("This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.")
- @viewRegistry.getView(this).pixelRectForScreenRange(screenRange)
+ @getElement().pixelRectForScreenRange(screenRange)
###
Section: Utility
@@ -3319,8 +3534,6 @@ class TextEditor extends Model
inspect: ->
""
- logScreenLines: (start, end) -> @displayBuffer.logLines(start, end)
-
emitWillInsertTextEvent: (text) ->
result = true
cancel = -> result = false
diff --git a/src/text-utils.coffee b/src/text-utils.coffee
index af17335aa..f4d62772e 100644
--- a/src/text-utils.coffee
+++ b/src/text-utils.coffee
@@ -94,6 +94,13 @@ isCJKCharacter = (character) ->
isHalfWidthCharacter(character) or
isKoreanCharacter(character)
+isWordStart = (previousCharacter, character) ->
+ (previousCharacter is ' ' or previousCharacter is '\t') and
+ (character isnt ' ' and character isnt '\t')
+
+isWrapBoundary = (previousCharacter, character) ->
+ isWordStart(previousCharacter, character) or isCJKCharacter(character)
+
# Does the given string contain at least surrogate pair, variation sequence,
# or combined character?
#
@@ -107,4 +114,8 @@ hasPairedCharacter = (string) ->
index++
false
-module.exports = {isPairedCharacter, hasPairedCharacter, isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isCJKCharacter}
+module.exports = {
+ isPairedCharacter, hasPairedCharacter,
+ isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter,
+ isWrapBoundary
+}
diff --git a/src/token-iterator.coffee b/src/token-iterator.coffee
index 8f0fe202f..f9af1e4ca 100644
--- a/src/token-iterator.coffee
+++ b/src/token-iterator.coffee
@@ -1,106 +1,57 @@
-{SoftTab, HardTab, PairedCharacter, SoftWrapIndent} = require './special-token-symbols'
-{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter} = require './text-utils'
-
module.exports =
class TokenIterator
- constructor: ({@grammarRegistry}, line, enableScopes) ->
- @reset(line, enableScopes) if line?
+ constructor: ({@grammarRegistry}, line) ->
+ @reset(line) if line?
- reset: (@line, @enableScopes=true) ->
+ reset: (@line) ->
@index = null
- @bufferStart = @line.startBufferColumn
- @bufferEnd = @bufferStart
- @screenStart = 0
- @screenEnd = 0
- @resetScopes() if @enableScopes
+ @startColumn = 0
+ @endColumn = 0
+ @scopes = @line.openScopes.map (id) => @grammarRegistry.scopeForId(id)
+ @scopeStarts = @scopes.slice()
+ @scopeEnds = []
this
next: ->
{tags} = @line
if @index?
+ @startColumn = @endColumn
+ @scopeEnds.length = 0
+ @scopeStarts.length = 0
@index++
- @bufferStart = @bufferEnd
- @screenStart = @screenEnd
- @clearScopeStartsAndEnds() if @enableScopes
else
@index = 0
while @index < tags.length
tag = tags[@index]
if tag < 0
- @handleScopeForTag(tag) if @enableScopes
+ scope = @grammarRegistry.scopeForId(tag)
+ if tag % 2 is 0
+ if @scopeStarts[@scopeStarts.length - 1] is scope
+ @scopeStarts.pop()
+ else
+ @scopeEnds.push(scope)
+ @scopes.pop()
+ else
+ @scopeStarts.push(scope)
+ @scopes.push(scope)
@index++
else
- if @isHardTab()
- @screenEnd = @screenStart + tag
- @bufferEnd = @bufferStart + 1
- else if @isSoftWrapIndentation()
- @screenEnd = @screenStart + tag
- @bufferEnd = @bufferStart + 0
- else
- @screenEnd = @screenStart + tag
- @bufferEnd = @bufferStart + tag
-
- @text = @line.text.substring(@screenStart, @screenEnd)
+ @endColumn += tag
+ @text = @line.text.substring(@startColumn, @endColumn)
return true
false
- resetScopes: ->
- @scopes = @line.openScopes.map (id) => @grammarRegistry.scopeForId(id)
- @scopeStarts = @scopes.slice()
- @scopeEnds = []
-
- clearScopeStartsAndEnds: ->
- @scopeEnds.length = 0
- @scopeStarts.length = 0
-
- handleScopeForTag: (tag) ->
- scope = @grammarRegistry.scopeForId(tag)
- if tag % 2 is 0
- if @scopeStarts[@scopeStarts.length - 1] is scope
- @scopeStarts.pop()
- else
- @scopeEnds.push(scope)
- @scopes.pop()
- else
- @scopeStarts.push(scope)
- @scopes.push(scope)
-
- getBufferStart: -> @bufferStart
- getBufferEnd: -> @bufferEnd
-
- getScreenStart: -> @screenStart
- getScreenEnd: -> @screenEnd
+ getScopes: -> @scopes
getScopeStarts: -> @scopeStarts
- getScopeEnds: -> @scopeEnds
- getScopes: -> @scopes
+ getScopeEnds: -> @scopeEnds
getText: -> @text
- isSoftTab: ->
- @line.specialTokens[@index] is SoftTab
+ getBufferStart: -> @startColumn
- isHardTab: ->
- @line.specialTokens[@index] is HardTab
-
- isSoftWrapIndentation: ->
- @line.specialTokens[@index] is SoftWrapIndent
-
- isPairedCharacter: ->
- @line.specialTokens[@index] is PairedCharacter
-
- hasDoubleWidthCharacterAt: (charIndex) ->
- isDoubleWidthCharacter(@getText()[charIndex])
-
- hasHalfWidthCharacterAt: (charIndex) ->
- isHalfWidthCharacter(@getText()[charIndex])
-
- hasKoreanCharacterAt: (charIndex) ->
- isKoreanCharacter(@getText()[charIndex])
-
- isAtomic: ->
- @isSoftTab() or @isHardTab() or @isSoftWrapIndentation() or @isPairedCharacter()
+ getBufferEnd: -> @endColumn
diff --git a/src/token.coffee b/src/token.coffee
index 60e8194f8..d531ba04a 100644
--- a/src/token.coffee
+++ b/src/token.coffee
@@ -7,41 +7,20 @@ WhitespaceRegex = /\S/
module.exports =
class Token
value: null
- hasPairedCharacter: false
scopes: null
- isAtomic: null
- isHardTab: null
- firstNonWhitespaceIndex: null
- firstTrailingWhitespaceIndex: null
- hasInvisibleCharacters: false
constructor: (properties) ->
- {@value, @scopes, @isAtomic, @isHardTab, @bufferDelta} = properties
- {@hasInvisibleCharacters, @hasPairedCharacter, @isSoftWrapIndentation} = properties
- @firstNonWhitespaceIndex = properties.firstNonWhitespaceIndex ? null
- @firstTrailingWhitespaceIndex = properties.firstTrailingWhitespaceIndex ? null
-
- @screenDelta = @value.length
- @bufferDelta ?= @screenDelta
+ {@value, @scopes} = properties
isEqual: (other) ->
# TODO: scopes is deprecated. This is here for the sake of lang package tests
- @value is other.value and _.isEqual(@scopes, other.scopes) and !!@isAtomic is !!other.isAtomic
+ @value is other.value and _.isEqual(@scopes, other.scopes)
isBracket: ->
/^meta\.brace\b/.test(_.last(@scopes))
- isOnlyWhitespace: ->
- not WhitespaceRegex.test(@value)
-
matchesScopeSelector: (selector) ->
targetClasses = selector.replace(StartDotRegex, '').split('.')
_.any @scopes, (scope) ->
scopeClasses = scope.split('.')
_.isSubset(targetClasses, scopeClasses)
-
- hasLeadingWhitespace: ->
- @firstNonWhitespaceIndex? and @firstNonWhitespaceIndex > 0
-
- hasTrailingWhitespace: ->
- @firstTrailingWhitespaceIndex? and @firstTrailingWhitespaceIndex < @value.length
diff --git a/src/tokenized-buffer-iterator.coffee b/src/tokenized-buffer-iterator.coffee
new file mode 100644
index 000000000..780156e42
--- /dev/null
+++ b/src/tokenized-buffer-iterator.coffee
@@ -0,0 +1,122 @@
+{Point} = require 'text-buffer'
+
+module.exports =
+class TokenizedBufferIterator
+ constructor: (@tokenizedBuffer, @grammarRegistry) ->
+ @openTags = null
+ @closeTags = null
+ @containingTags = null
+
+ seek: (position) ->
+ @openTags = []
+ @closeTags = []
+ @tagIndex = null
+
+ currentLine = @tokenizedBuffer.tokenizedLineForRow(position.row)
+ @currentTags = currentLine.tags
+ @currentLineOpenTags = currentLine.openScopes
+ @currentLineLength = currentLine.text.length
+ @containingTags = @currentLineOpenTags.map (id) => @grammarRegistry.scopeForId(id)
+ currentColumn = 0
+ for tag, index in @currentTags
+ if tag >= 0
+ if currentColumn >= position.column and @isAtTagBoundary()
+ @tagIndex = index
+ break
+ else
+ currentColumn += tag
+ @containingTags.pop() while @closeTags.shift()
+ @containingTags.push(tag) while tag = @openTags.shift()
+ else
+ scopeName = @grammarRegistry.scopeForId(tag)
+ if tag % 2 is 0
+ if @openTags.length > 0
+ @tagIndex = index
+ break
+ else
+ @closeTags.push(scopeName)
+ else
+ @openTags.push(scopeName)
+
+ @tagIndex ?= @currentTags.length
+ @position = Point(position.row, Math.min(@currentLineLength, currentColumn))
+ @containingTags.slice()
+
+ moveToSuccessor: ->
+ @containingTags.pop() for tag in @closeTags
+ @containingTags.push(tag) for tag in @openTags
+ @openTags = []
+ @closeTags = []
+
+ loop
+ if @tagIndex is @currentTags.length
+ if @isAtTagBoundary()
+ break
+ else
+ if @shouldMoveToNextLine
+ @moveToNextLine()
+ @openTags = @currentLineOpenTags.map (id) => @grammarRegistry.scopeForId(id)
+ @shouldMoveToNextLine = false
+ else if @nextLineHasMismatchedContainingTags()
+ @closeTags = @containingTags.slice().reverse()
+ @containingTags = []
+ @shouldMoveToNextLine = true
+ else
+ return false unless @moveToNextLine()
+ else
+ tag = @currentTags[@tagIndex]
+ if tag >= 0
+ if @isAtTagBoundary()
+ break
+ else
+ @position = Point(@position.row, Math.min(@currentLineLength, @position.column + @currentTags[@tagIndex]))
+ else
+ scopeName = @grammarRegistry.scopeForId(tag)
+ if tag % 2 is 0
+ if @openTags.length > 0
+ break
+ else
+ @closeTags.push(scopeName)
+ else
+ @openTags.push(scopeName)
+ @tagIndex++
+
+ true
+
+ getPosition: ->
+ @position
+
+ getCloseTags: ->
+ @closeTags.slice()
+
+ getOpenTags: ->
+ @openTags.slice()
+
+ ###
+ Section: Private Methods
+ ###
+
+ nextLineHasMismatchedContainingTags: ->
+ if line = @tokenizedBuffer.tokenizedLineForRow(@position.row + 1)
+ return true if line.openScopes.length isnt @containingTags.length
+
+ for i in [0...@containingTags.length] by 1
+ if @containingTags[i] isnt @grammarRegistry.scopeForId(line.openScopes[i])
+ return true
+ false
+ else
+ false
+
+ moveToNextLine: ->
+ @position = Point(@position.row + 1, 0)
+ if tokenizedLine = @tokenizedBuffer.tokenizedLineForRow(@position.row)
+ @currentTags = tokenizedLine.tags
+ @currentLineLength = tokenizedLine.text.length
+ @currentLineOpenTags = tokenizedLine.openScopes
+ @tagIndex = 0
+ true
+ else
+ false
+
+ isAtTagBoundary: ->
+ @closeTags.length > 0 or @openTags.length > 0
diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee
index 0fdf4eea8..ea7082a6d 100644
--- a/src/tokenized-buffer.coffee
+++ b/src/tokenized-buffer.coffee
@@ -7,6 +7,7 @@ TokenizedLine = require './tokenized-line'
TokenIterator = require './token-iterator'
Token = require './token'
ScopeDescriptor = require './scope-descriptor'
+TokenizedBufferIterator = require './tokenized-buffer-iterator'
module.exports =
class TokenizedBuffer extends Model
@@ -29,14 +30,13 @@ class TokenizedBuffer extends Model
state.buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath)
state.config = atomEnvironment.config
state.grammarRegistry = atomEnvironment.grammars
- state.packageManager = atomEnvironment.packages
state.assert = atomEnvironment.assert
new this(state)
constructor: (params) ->
{
- @buffer, @tabLength, @ignoreInvisibles, @largeFileMode, @config,
- @grammarRegistry, @packageManager, @assert, grammarScopeName
+ @buffer, @tabLength, @largeFileMode, @config,
+ @grammarRegistry, @assert, grammarScopeName
} = params
@emitter = new Emitter
@@ -58,13 +58,24 @@ class TokenizedBuffer extends Model
destroyed: ->
@disposables.dispose()
+ buildIterator: ->
+ new TokenizedBufferIterator(this, @grammarRegistry)
+
+ getInvalidatedRanges: ->
+ if @invalidatedRange?
+ [@invalidatedRange]
+ else
+ []
+
+ onDidInvalidateRange: (fn) ->
+ @emitter.on 'did-invalidate-range', fn
+
serialize: ->
state = {
deserializer: 'TokenizedBuffer'
bufferPath: @buffer.getPath()
bufferId: @buffer.getId()
tabLength: @tabLength
- ignoreInvisibles: @ignoreInvisibles
largeFileMode: @largeFileMode
}
state.grammarScopeName = @grammar?.scopeName unless @buffer.getPath()
@@ -105,28 +116,18 @@ class TokenizedBuffer extends Model
@grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines()
@disposables.add(@grammarUpdateDisposable)
- scopeOptions = {scope: @rootScopeDescriptor}
- @configSettings =
- tabLength: @config.get('editor.tabLength', scopeOptions)
- invisibles: @config.get('editor.invisibles', scopeOptions)
- showInvisibles: @config.get('editor.showInvisibles', scopeOptions)
+ @configSettings = {tabLength: @config.get('editor.tabLength', {scope: @rootScopeDescriptor})}
if @configSubscriptions?
@configSubscriptions.dispose()
@disposables.remove(@configSubscriptions)
@configSubscriptions = new CompositeDisposable
- @configSubscriptions.add @config.onDidChange 'editor.tabLength', scopeOptions, ({newValue}) =>
+ @configSubscriptions.add @config.onDidChange 'editor.tabLength', {scope: @rootScopeDescriptor}, ({newValue}) =>
@configSettings.tabLength = newValue
- @retokenizeLines()
- ['invisibles', 'showInvisibles'].forEach (key) =>
- @configSubscriptions.add @config.onDidChange "editor.#{key}", scopeOptions, ({newValue}) =>
- oldInvisibles = @getInvisiblesToShow()
- @configSettings[key] = newValue
- @retokenizeLines() unless _.isEqual(@getInvisiblesToShow(), oldInvisibles)
@disposables.add(@configSubscriptions)
@retokenizeLines()
- @packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used")
+
@emitter.emit 'did-change-grammar', grammar
getGrammarSelectionContent: ->
@@ -163,13 +164,6 @@ class TokenizedBuffer extends Model
return if tabLength is @tabLength
@tabLength = tabLength
- @retokenizeLines()
-
- setIgnoreInvisibles: (ignoreInvisibles) ->
- if ignoreInvisibles isnt @ignoreInvisibles
- @ignoreInvisibles = ignoreInvisibles
- if @configSettings.showInvisibles and @configSettings.invisibles?
- @retokenizeLines()
tokenizeInBackground: ->
return if not @visible or @pendingChunk or not @isAlive()
@@ -210,10 +204,9 @@ class TokenizedBuffer extends Model
@validateRow(endRow)
@invalidateRow(endRow + 1) unless filledRegion
- [startRow, endRow] = @updateFoldableStatus(startRow, endRow)
-
event = {start: startRow, end: endRow, delta: 0}
@emitter.emit 'did-change', event
+ @emitter.emit 'did-invalidate-range', Range(Point(startRow, 0), Point(endRow + 1, 0))
if @firstInvalidRow()?
@tokenizeInBackground()
@@ -264,46 +257,15 @@ class TokenizedBuffer extends Model
newTokenizedLines = @buildTokenizedLinesForRows(start, end + delta, @stackForRow(start - 1), @openScopesForRow(start))
_.spliceWithArray(@tokenizedLines, start, end - start + 1, newTokenizedLines)
- start = @retokenizeWhitespaceRowsIfIndentLevelChanged(start - 1, -1)
- end = @retokenizeWhitespaceRowsIfIndentLevelChanged(newRange.end.row + 1, 1) - delta
-
newEndStack = @stackForRow(end + delta)
if newEndStack and not _.isEqual(newEndStack, previousEndStack)
@invalidateRow(end + delta + 1)
- [start, end] = @updateFoldableStatus(start, end + delta)
- end -= delta
+ @invalidatedRange = Range(start, end)
event = {start, end, delta, bufferChange: e}
@emitter.emit 'did-change', event
- retokenizeWhitespaceRowsIfIndentLevelChanged: (row, increment) ->
- line = @tokenizedLineForRow(row)
- if line?.isOnlyWhitespace() and @indentLevelForRow(row) isnt line.indentLevel
- while line?.isOnlyWhitespace()
- @tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1), @openScopesForRow(row))
- row += increment
- line = @tokenizedLineForRow(row)
-
- row - increment
-
- updateFoldableStatus: (startRow, endRow) ->
- return [startRow, endRow] if @largeFileMode
-
- scanStartRow = @buffer.previousNonBlankRow(startRow) ? startRow
- scanStartRow-- while scanStartRow > 0 and @tokenizedLineForRow(scanStartRow).isComment()
- scanEndRow = @buffer.nextNonBlankRow(endRow) ? endRow
-
- for row in [scanStartRow..scanEndRow] by 1
- foldable = @isFoldableAtRow(row)
- line = @tokenizedLineForRow(row)
- unless line.foldable is foldable
- line.foldable = foldable
- startRow = Math.min(startRow, row)
- endRow = Math.max(endRow, row)
-
- [startRow, endRow]
-
isFoldableAtRow: (row) ->
if @largeFileMode
false
@@ -368,26 +330,16 @@ class TokenizedBuffer extends Model
openScopes = [@grammar.startIdForScope(@grammar.scopeName)]
text = @buffer.lineForRow(row)
tags = [text.length]
- tabLength = @getTabLength()
- indentLevel = @indentLevelForRow(row)
lineEnding = @buffer.lineEndingForRow(row)
- new TokenizedLine({openScopes, text, tags, tabLength, indentLevel, invisibles: @getInvisiblesToShow(), lineEnding, @tokenIterator})
+ new TokenizedLine({openScopes, text, tags, lineEnding, @tokenIterator})
buildTokenizedLineForRow: (row, ruleStack, openScopes) ->
@buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack, openScopes)
buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) ->
lineEnding = @buffer.lineEndingForRow(row)
- tabLength = @getTabLength()
- indentLevel = @indentLevelForRow(row)
{tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false)
- new TokenizedLine({openScopes, text, tags, ruleStack, tabLength, lineEnding, indentLevel, invisibles: @getInvisiblesToShow(), @tokenIterator})
-
- getInvisiblesToShow: ->
- if @configSettings.showInvisibles and not @ignoreInvisibles
- @configSettings.invisibles
- else
- null
+ new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator})
tokenizedLineForRow: (bufferRow) ->
if 0 <= bufferRow < @tokenizedLines.length
@@ -428,6 +380,7 @@ class TokenizedBuffer extends Model
filePath: @buffer.getPath()
fileContents: @buffer.getText()
}
+ break
scopes
indentLevelForRow: (bufferRow) ->
diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee
index c97a621ac..f8faad865 100644
--- a/src/tokenized-line.coffee
+++ b/src/tokenized-line.coffee
@@ -1,188 +1,18 @@
_ = require 'underscore-plus'
{isPairedCharacter, isCJKCharacter} = require './text-utils'
Token = require './token'
-{SoftTab, HardTab, PairedCharacter, SoftWrapIndent} = require './special-token-symbols'
-
-NonWhitespaceRegex = /\S/
-LeadingWhitespaceRegex = /^\s*/
-TrailingWhitespaceRegex = /\s*$/
-RepeatedSpaceRegex = /[ ]/g
CommentScopeRegex = /(\b|\.)comment/
-TabCharCode = 9
-SpaceCharCode = 32
-SpaceString = ' '
-TabStringsByLength = {
- 1: ' '
- 2: ' '
- 3: ' '
- 4: ' '
-}
idCounter = 1
-getTabString = (length) ->
- TabStringsByLength[length] ?= buildTabString(length)
-
-buildTabString = (length) ->
- string = SpaceString
- string += SpaceString for i in [1...length] by 1
- string
-
module.exports =
class TokenizedLine
- endOfLineInvisibles: null
- lineIsWhitespaceOnly: false
- firstNonWhitespaceIndex: 0
- foldable: false
-
constructor: (properties) ->
@id = idCounter++
return unless properties?
- @specialTokens = {}
- {@openScopes, @text, @tags, @lineEnding, @ruleStack, @tokenIterator} = properties
- {@startBufferColumn, @fold, @tabLength, @indentLevel, @invisibles} = properties
-
- @startBufferColumn ?= 0
- @bufferDelta = @text.length
-
- @transformContent()
- @buildEndOfLineInvisibles() if @invisibles? and @lineEnding?
-
- transformContent: ->
- text = ''
- bufferColumn = 0
- screenColumn = 0
- tokenIndex = 0
- tokenOffset = 0
- firstNonWhitespaceColumn = null
- lastNonWhitespaceColumn = null
-
- substringStart = 0
- substringEnd = 0
-
- while bufferColumn < @text.length
- # advance to next token if we've iterated over its length
- if tokenOffset is @tags[tokenIndex]
- tokenIndex++
- tokenOffset = 0
-
- # advance to next token tag
- tokenIndex++ while @tags[tokenIndex] < 0
-
- charCode = @text.charCodeAt(bufferColumn)
-
- # split out unicode surrogate pairs
- if isPairedCharacter(@text, bufferColumn)
- prefix = tokenOffset
- suffix = @tags[tokenIndex] - tokenOffset - 2
-
- i = tokenIndex
- @tags.splice(i, 1)
- @tags.splice(i++, 0, prefix) if prefix > 0
- @tags.splice(i++, 0, 2)
- @tags.splice(i, 0, suffix) if suffix > 0
-
- firstNonWhitespaceColumn ?= screenColumn
- lastNonWhitespaceColumn = screenColumn + 1
-
- substringEnd += 2
- screenColumn += 2
- bufferColumn += 2
-
- tokenIndex++ if prefix > 0
- @specialTokens[tokenIndex] = PairedCharacter
- tokenIndex++
- tokenOffset = 0
-
- # split out leading soft tabs
- else if charCode is SpaceCharCode
- if firstNonWhitespaceColumn?
- substringEnd += 1
- else
- if (screenColumn + 1) % @tabLength is 0
- suffix = @tags[tokenIndex] - @tabLength
- if suffix >= 0
- @specialTokens[tokenIndex] = SoftTab
- @tags.splice(tokenIndex, 1, @tabLength)
- @tags.splice(tokenIndex + 1, 0, suffix) if suffix > 0
-
- if @invisibles?.space
- if substringEnd > substringStart
- text += @text.substring(substringStart, substringEnd)
- substringStart = substringEnd
- text += @invisibles.space
- substringStart += 1
-
- substringEnd += 1
-
- screenColumn++
- bufferColumn++
- tokenOffset++
-
- # expand hard tabs to the next tab stop
- else if charCode is TabCharCode
- if substringEnd > substringStart
- text += @text.substring(substringStart, substringEnd)
- substringStart = substringEnd
-
- tabLength = @tabLength - (screenColumn % @tabLength)
- if @invisibles?.tab
- text += @invisibles.tab
- text += getTabString(tabLength - 1) if tabLength > 1
- else
- text += getTabString(tabLength)
-
- substringStart += 1
- substringEnd += 1
-
- prefix = tokenOffset
- suffix = @tags[tokenIndex] - tokenOffset - 1
-
- i = tokenIndex
- @tags.splice(i, 1)
- @tags.splice(i++, 0, prefix) if prefix > 0
- @tags.splice(i++, 0, tabLength)
- @tags.splice(i, 0, suffix) if suffix > 0
-
- screenColumn += tabLength
- bufferColumn++
-
- tokenIndex++ if prefix > 0
- @specialTokens[tokenIndex] = HardTab
- tokenIndex++
- tokenOffset = 0
-
- # continue past any other character
- else
- firstNonWhitespaceColumn ?= screenColumn
- lastNonWhitespaceColumn = screenColumn
-
- substringEnd += 1
- screenColumn++
- bufferColumn++
- tokenOffset++
-
- if substringEnd > substringStart
- unless substringStart is 0 and substringEnd is @text.length
- text += @text.substring(substringStart, substringEnd)
- @text = text
- else
- @text = text
-
- @firstNonWhitespaceIndex = firstNonWhitespaceColumn
- if lastNonWhitespaceColumn?
- if lastNonWhitespaceColumn + 1 < @text.length
- @firstTrailingWhitespaceIndex = lastNonWhitespaceColumn + 1
- if @invisibles?.space
- @text =
- @text.substring(0, @firstTrailingWhitespaceIndex) +
- @text.substring(@firstTrailingWhitespaceIndex)
- .replace(RepeatedSpaceRegex, @invisibles.space)
- else
- @lineIsWhitespaceOnly = true
- @firstTrailingWhitespaceIndex = 0
+ {@openScopes, @text, @tags, @ruleStack, @tokenIterator} = properties
getTokenIterator: -> @tokenIterator.reset(this, arguments...)
@@ -191,285 +21,21 @@ class TokenizedLine
tokens = []
while iterator.next()
- properties = {
+ tokens.push(new Token({
value: iterator.getText()
scopes: iterator.getScopes().slice()
- isAtomic: iterator.isAtomic()
- isHardTab: iterator.isHardTab()
- hasPairedCharacter: iterator.isPairedCharacter()
- isSoftWrapIndentation: iterator.isSoftWrapIndentation()
- }
-
- if iterator.isHardTab()
- properties.bufferDelta = 1
- properties.hasInvisibleCharacters = true if @invisibles?.tab
-
- if iterator.getScreenStart() < @firstNonWhitespaceIndex
- properties.firstNonWhitespaceIndex =
- Math.min(@firstNonWhitespaceIndex, iterator.getScreenEnd()) - iterator.getScreenStart()
- properties.hasInvisibleCharacters = true if @invisibles?.space
-
- if @lineEnding? and iterator.getScreenEnd() > @firstTrailingWhitespaceIndex
- properties.firstTrailingWhitespaceIndex =
- Math.max(0, @firstTrailingWhitespaceIndex - iterator.getScreenStart())
- properties.hasInvisibleCharacters = true if @invisibles?.space
-
- tokens.push(new Token(properties))
+ }))
tokens
- copy: ->
- copy = new TokenizedLine
- copy.tokenIterator = @tokenIterator
- copy.openScopes = @openScopes
- copy.text = @text
- copy.tags = @tags
- copy.specialTokens = @specialTokens
- copy.startBufferColumn = @startBufferColumn
- copy.bufferDelta = @bufferDelta
- copy.ruleStack = @ruleStack
- copy.lineEnding = @lineEnding
- copy.invisibles = @invisibles
- copy.endOfLineInvisibles = @endOfLineInvisibles
- copy.indentLevel = @indentLevel
- copy.tabLength = @tabLength
- copy.firstNonWhitespaceIndex = @firstNonWhitespaceIndex
- copy.firstTrailingWhitespaceIndex = @firstTrailingWhitespaceIndex
- copy.fold = @fold
- copy
-
- # This clips a given screen column to a valid column that's within the line
- # and not in the middle of any atomic tokens.
- #
- # column - A {Number} representing the column to clip
- # options - A hash with the key clip. Valid values for this key:
- # 'closest' (default): clip to the closest edge of an atomic token.
- # 'forward': clip to the forward edge.
- # 'backward': clip to the backward edge.
- #
- # Returns a {Number} representing the clipped column.
- clipScreenColumn: (column, options={}) ->
- return 0 if @tags.length is 0
-
- {clip} = options
- column = Math.min(column, @getMaxScreenColumn())
-
- tokenStartColumn = 0
-
- iterator = @getTokenIterator()
- while iterator.next()
- break if iterator.getScreenEnd() > column
-
- if iterator.isSoftWrapIndentation()
- iterator.next() while iterator.isSoftWrapIndentation()
- iterator.getScreenStart()
- else if iterator.isAtomic() and iterator.getScreenStart() < column
- if clip is 'forward'
- iterator.getScreenEnd()
- else if clip is 'backward'
- iterator.getScreenStart()
- else #'closest'
- if column > ((iterator.getScreenStart() + iterator.getScreenEnd()) / 2)
- iterator.getScreenEnd()
- else
- iterator.getScreenStart()
- else
- column
-
- screenColumnForBufferColumn: (targetBufferColumn, options) ->
- iterator = @getTokenIterator()
- while iterator.next()
- tokenBufferStart = iterator.getBufferStart()
- tokenBufferEnd = iterator.getBufferEnd()
- if tokenBufferStart <= targetBufferColumn < tokenBufferEnd
- overshoot = targetBufferColumn - tokenBufferStart
- return Math.min(
- iterator.getScreenStart() + overshoot,
- iterator.getScreenEnd()
- )
- iterator.getScreenEnd()
-
- bufferColumnForScreenColumn: (targetScreenColumn) ->
- iterator = @getTokenIterator()
- while iterator.next()
- tokenScreenStart = iterator.getScreenStart()
- tokenScreenEnd = iterator.getScreenEnd()
- if tokenScreenStart <= targetScreenColumn < tokenScreenEnd
- overshoot = targetScreenColumn - tokenScreenStart
- return Math.min(
- iterator.getBufferStart() + overshoot,
- iterator.getBufferEnd()
- )
- iterator.getBufferEnd()
-
- getMaxScreenColumn: ->
- if @fold
- 0
- else
- @text.length
-
- getMaxBufferColumn: ->
- @startBufferColumn + @bufferDelta
-
- # Given a boundary column, finds the point where this line would wrap.
- #
- # maxColumn - The {Number} where you want soft wrapping to occur
- #
- # Returns a {Number} representing the `line` position where the wrap would take place.
- # Returns `null` if a wrap wouldn't occur.
- findWrapColumn: (maxColumn) ->
- return unless maxColumn?
- return unless @text.length > maxColumn
-
- if /\s/.test(@text[maxColumn])
- # search forward for the start of a word past the boundary
- for column in [maxColumn..@text.length]
- return column if /\S/.test(@text[column])
-
- return @text.length
- else if isCJKCharacter(@text[maxColumn])
- maxColumn
- else
- # search backward for the start of the word on the boundary
- for column in [maxColumn..@firstNonWhitespaceIndex]
- if /\s/.test(@text[column]) or isCJKCharacter(@text[column])
- return column + 1
-
- return maxColumn
-
- softWrapAt: (column, hangingIndent) ->
- return [null, this] if column is 0
-
- leftText = @text.substring(0, column)
- rightText = @text.substring(column)
-
- leftTags = []
- rightTags = []
-
- leftSpecialTokens = {}
- rightSpecialTokens = {}
-
- rightOpenScopes = @openScopes.slice()
-
- screenColumn = 0
-
- for tag, index in @tags
- # tag represents a token
- if tag >= 0
- # token ends before the soft wrap column
- if screenColumn + tag <= column
- if specialToken = @specialTokens[index]
- leftSpecialTokens[index] = specialToken
- leftTags.push(tag)
- screenColumn += tag
-
- # token starts before and ends after the split column
- else if screenColumn <= column
- leftSuffix = column - screenColumn
- rightPrefix = screenColumn + tag - column
-
- leftTags.push(leftSuffix) if leftSuffix > 0
-
- softWrapIndent = @indentLevel * @tabLength + (hangingIndent ? 0)
- for i in [0...softWrapIndent] by 1
- rightText = ' ' + rightText
- remainingSoftWrapIndent = softWrapIndent
- while remainingSoftWrapIndent > 0
- indentToken = Math.min(remainingSoftWrapIndent, @tabLength)
- rightSpecialTokens[rightTags.length] = SoftWrapIndent
- rightTags.push(indentToken)
- remainingSoftWrapIndent -= indentToken
-
- rightTags.push(rightPrefix) if rightPrefix > 0
-
- screenColumn += tag
-
- # token is after split column
- else
- if specialToken = @specialTokens[index]
- rightSpecialTokens[rightTags.length] = specialToken
- rightTags.push(tag)
-
- # tag represents the start of a scope
- else if (tag % 2) is -1
- if screenColumn < column
- leftTags.push(tag)
- rightOpenScopes.push(tag)
- else
- rightTags.push(tag)
-
- # tag represents the end of a scope
- else
- if screenColumn <= column
- leftTags.push(tag)
- rightOpenScopes.pop()
- else
- rightTags.push(tag)
-
- splitBufferColumn = @bufferColumnForScreenColumn(column)
-
- leftFragment = new TokenizedLine
- leftFragment.tokenIterator = @tokenIterator
- leftFragment.openScopes = @openScopes
- leftFragment.text = leftText
- leftFragment.tags = leftTags
- leftFragment.specialTokens = leftSpecialTokens
- leftFragment.startBufferColumn = @startBufferColumn
- leftFragment.bufferDelta = splitBufferColumn - @startBufferColumn
- leftFragment.ruleStack = @ruleStack
- leftFragment.invisibles = @invisibles
- leftFragment.lineEnding = null
- leftFragment.indentLevel = @indentLevel
- leftFragment.tabLength = @tabLength
- leftFragment.firstNonWhitespaceIndex = Math.min(column, @firstNonWhitespaceIndex)
- leftFragment.firstTrailingWhitespaceIndex = Math.min(column, @firstTrailingWhitespaceIndex)
-
- rightFragment = new TokenizedLine
- rightFragment.tokenIterator = @tokenIterator
- rightFragment.openScopes = rightOpenScopes
- rightFragment.text = rightText
- rightFragment.tags = rightTags
- rightFragment.specialTokens = rightSpecialTokens
- rightFragment.startBufferColumn = splitBufferColumn
- rightFragment.bufferDelta = @startBufferColumn + @bufferDelta - splitBufferColumn
- rightFragment.ruleStack = @ruleStack
- rightFragment.invisibles = @invisibles
- rightFragment.lineEnding = @lineEnding
- rightFragment.indentLevel = @indentLevel
- rightFragment.tabLength = @tabLength
- rightFragment.endOfLineInvisibles = @endOfLineInvisibles
- rightFragment.firstNonWhitespaceIndex = Math.max(softWrapIndent, @firstNonWhitespaceIndex - column + softWrapIndent)
- rightFragment.firstTrailingWhitespaceIndex = Math.max(softWrapIndent, @firstTrailingWhitespaceIndex - column + softWrapIndent)
-
- [leftFragment, rightFragment]
-
- isSoftWrapped: ->
- @lineEnding is null
-
- isColumnInsideSoftWrapIndentation: (targetColumn) ->
- targetColumn < @getSoftWrapIndentationDelta()
-
- getSoftWrapIndentationDelta: ->
- delta = 0
- for tag, index in @tags
- if tag >= 0
- if @specialTokens[index] is SoftWrapIndent
- delta += tag
- else
- break
- delta
-
- hasOnlySoftWrapIndentation: ->
- @getSoftWrapIndentationDelta() is @text.length
-
tokenAtBufferColumn: (bufferColumn) ->
@tokens[@tokenIndexAtBufferColumn(bufferColumn)]
tokenIndexAtBufferColumn: (bufferColumn) ->
- delta = 0
+ column = 0
for token, index in @tokens
- delta += token.bufferDelta
- return index if delta > bufferColumn
+ column += token.value.length
+ return index if column > bufferColumn
index - 1
tokenStartColumnForBufferColumn: (bufferColumn) ->
@@ -480,30 +46,20 @@ class TokenizedLine
delta = nextDelta
delta
- buildEndOfLineInvisibles: ->
- @endOfLineInvisibles = []
- {cr, eol} = @invisibles
-
- switch @lineEnding
- when '\r\n'
- @endOfLineInvisibles.push(cr) if cr
- @endOfLineInvisibles.push(eol) if eol
- when '\n'
- @endOfLineInvisibles.push(eol) if eol
-
isComment: ->
+ return @isCommentLine if @isCommentLine?
+
+ @isCommentLine = false
iterator = @getTokenIterator()
while iterator.next()
scopes = iterator.getScopes()
continue if scopes.length is 1
- continue unless NonWhitespaceRegex.test(iterator.getText())
for scope in scopes
- return true if CommentScopeRegex.test(scope)
+ if CommentScopeRegex.test(scope)
+ @isCommentLine = true
+ break
break
- false
-
- isOnlyWhitespace: ->
- @lineIsWhitespaceOnly
+ @isCommentLine
tokenAtIndex: (index) ->
@tokens[index]
diff --git a/src/tooltip-manager.coffee b/src/tooltip-manager.coffee
index 247437535..90f0ab8e6 100644
--- a/src/tooltip-manager.coffee
+++ b/src/tooltip-manager.coffee
@@ -63,6 +63,8 @@ class TooltipManager
# full list of options. You can also supply the following additional options:
# * `title` A {String} or {Function} to use for the text in the tip. If
# given a function, `this` will be set to the `target` element.
+ # * `trigger` A {String} that's the same as Bootstrap 'click | hover | focus
+ # | manual', except 'manual' will show the tooltip immediately.
# * `keyBindingCommand` A {String} containing a command name. If you specify
# this option and a key binding exists that matches the command, it will
# be appended to the title or rendered alone if no title is specified.
diff --git a/src/tooltip.js b/src/tooltip.js
index 4ea952a64..ad5ce0cdd 100644
--- a/src/tooltip.js
+++ b/src/tooltip.js
@@ -64,7 +64,9 @@ Tooltip.prototype.init = function (element, options) {
if (trigger === 'click') {
this.disposables.add(listen(this.element, 'click', this.options.selector, this.toggle.bind(this)))
- } else if (trigger !== 'manual') {
+ } else if (trigger === 'manual') {
+ this.show()
+ } else {
var eventIn, eventOut
if (trigger === 'hover') {
diff --git a/src/view-registry.coffee b/src/view-registry.coffee
index ef7151353..5fbfba729 100644
--- a/src/view-registry.coffee
+++ b/src/view-registry.coffee
@@ -171,6 +171,11 @@ class ViewRegistry
if object instanceof HTMLElement
return object
+ if typeof object?.getElement is 'function'
+ element = object.getElement()
+ if element instanceof HTMLElement
+ return element
+
if object?.element instanceof HTMLElement
return object.element
diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee
index d3a231f77..6c338320d 100644
--- a/src/window-event-handler.coffee
+++ b/src/window-event-handler.coffee
@@ -15,7 +15,8 @@ class WindowEventHandler
@addEventListener(@window, 'focus', @handleWindowFocus)
@addEventListener(@window, 'blur', @handleWindowBlur)
- @addEventListener(@document, 'keydown', @handleDocumentKeydown)
+ @addEventListener(@document, 'keyup', @handleDocumentKeyEvent)
+ @addEventListener(@document, 'keydown', @handleDocumentKeyEvent)
@addEventListener(@document, 'drop', @handleDocumentDrop)
@addEventListener(@document, 'dragover', @handleDocumentDragover)
@addEventListener(@document, 'contextmenu', @handleDocumentContextmenu)
@@ -66,7 +67,7 @@ class WindowEventHandler
target.addEventListener(eventName, handler)
@subscriptions.add(new Disposable(-> target.removeEventListener(eventName, handler)))
- handleDocumentKeydown: (event) =>
+ handleDocumentKeyEvent: (event) =>
@atomEnvironment.keymaps.handleKeyboardEvent(event)
event.stopImmediatePropagation()
@@ -133,7 +134,7 @@ class WindowEventHandler
handleWindowBlur: =>
@document.body.classList.add('is-blurred')
- @atomEnvironment.storeDefaultWindowDimensions()
+ @atomEnvironment.storeWindowDimensions()
handleWindowBeforeunload: =>
confirmed = @atomEnvironment.workspace?.confirmClose(windowCloseRequested: true)
@@ -141,7 +142,6 @@ class WindowEventHandler
@atomEnvironment.hide()
@reloadRequested = false
- @atomEnvironment.storeDefaultWindowDimensions()
@atomEnvironment.storeWindowDimensions()
if confirmed
@atomEnvironment.unloadEditorWindow()
diff --git a/src/window-load-settings-helpers.coffee b/src/window-load-settings-helpers.coffee
index 59ee2f382..73fd31a3d 100644
--- a/src/window-load-settings-helpers.coffee
+++ b/src/window-load-settings-helpers.coffee
@@ -1,19 +1,10 @@
-remote = require 'remote'
+{remote} = require 'electron'
_ = require 'underscore-plus'
windowLoadSettings = null
exports.getWindowLoadSettings = ->
windowLoadSettings ?= JSON.parse(window.decodeURIComponent(window.location.hash.substr(1)))
- clone = _.deepClone(windowLoadSettings)
-
- # The windowLoadSettings.windowState could be large, request it only when needed.
- clone.__defineGetter__ 'windowState', ->
- remote.getCurrentWindow().loadSettings.windowState
- clone.__defineSetter__ 'windowState', (value) ->
- remote.getCurrentWindow().loadSettings.windowState = value
-
- clone
exports.setWindowLoadSettings = (settings) ->
windowLoadSettings = settings
diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee
index f7805ed57..02ff2e5b4 100644
--- a/src/workspace-element.coffee
+++ b/src/workspace-element.coffee
@@ -1,4 +1,4 @@
-ipc = require 'ipc'
+{ipcRenderer} = require 'electron'
path = require 'path'
{Disposable, CompositeDisposable} = require 'event-kit'
Grim = require 'grim'
@@ -44,10 +44,15 @@ class WorkspaceElement extends HTMLElement
@subscriptions.add @config.onDidChange 'editor.lineHeight', @updateGlobalTextEditorStyleSheet.bind(this)
updateGlobalTextEditorStyleSheet: ->
+ fontFamily = @config.get('editor.fontFamily')
+ # TODO: There is a bug in how some emojis (e.g. ❤️) are rendered on OSX.
+ # This workaround should be removed once we update to Chromium 51, where the
+ # problem was fixed.
+ fontFamily += ', "Apple Color Emoji"' if process.platform is 'darwin'
styleSheetSource = """
atom-text-editor {
font-size: #{@config.get('editor.fontSize')}px;
- font-family: #{@config.get('editor.fontFamily')};
+ font-family: #{fontFamily};
line-height: #{@config.get('editor.lineHeight')};
}
"""
@@ -117,6 +122,6 @@ class WorkspaceElement extends HTMLElement
[projectPath] = @project.relativizePath(activePath)
else
[projectPath] = @project.getPaths()
- ipc.send('run-package-specs', path.join(projectPath, 'spec')) if projectPath
+ ipcRenderer.send('run-package-specs', path.join(projectPath, 'spec')) if projectPath
module.exports = WorkspaceElement = document.registerElement 'atom-workspace', prototype: WorkspaceElement.prototype
diff --git a/src/workspace.coffee b/src/workspace.coffee
index ebaf1b337..5e9de93dd 100644
--- a/src/workspace.coffee
+++ b/src/workspace.coffee
@@ -4,6 +4,7 @@ path = require 'path'
{join} = path
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
fs = require 'fs-plus'
+{Directory} = require 'pathwatcher'
DefaultDirectorySearcher = require './default-directory-searcher'
Model = require './model'
TextEditor = require './text-editor'
@@ -43,6 +44,12 @@ class Workspace extends Model
@defaultDirectorySearcher = new DefaultDirectorySearcher()
@consumeServices(@packageManager)
+ # One cannot simply .bind here since it could be used as a component with
+ # Etch, in which case it'd be `new`d. And when it's `new`d, `this` is always
+ # the newly created object.
+ realThis = this
+ @buildTextEditor = -> Workspace.prototype.buildTextEditor.apply(realThis, arguments)
+
@panelContainers =
top: new PanelContainer({location: 'top'})
left: new PanelContainer({location: 'left'})
@@ -394,15 +401,18 @@ class Workspace extends Model
# initially. Defaults to `0`.
# * `initialColumn` A {Number} indicating which column to move the cursor to
# initially. Defaults to `0`.
- # * `split` Either 'left', 'right', 'top' or 'bottom'.
+ # * `split` Either 'left', 'right', 'up' or 'down'.
# If 'left', the item will be opened in leftmost pane of the current active pane's row.
- # If 'right', the item will be opened in the rightmost pane of the current active pane's row.
- # If 'up', the item will be opened in topmost pane of the current active pane's row.
- # If 'down', the item will be opened in the bottommost pane of the current active pane's row.
+ # If 'right', the item will be opened in the rightmost pane of the current active pane's row. If only one pane exists in the row, a new pane will be created.
+ # If 'up', the item will be opened in topmost pane of the current active pane's column.
+ # If 'down', the item will be opened in the bottommost pane of the current active pane's column. If only one pane exists in the column, a new pane will be created.
# * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on
# containing pane. Defaults to `true`.
# * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem}
# on containing pane. Defaults to `true`.
+ # * `pending` A {Boolean} indicating whether or not the item should be opened
+ # in a pending state. Existing pending items in a pane are replaced with
+ # new pending items when they are opened.
# * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to
# activate an existing item for the given URI on any pane.
# If `false`, only the active pane will be searched for
@@ -414,6 +424,9 @@ class Workspace extends Model
split = options.split
uri = @project.resolvePath(uri)
+ if not atom.config.get('core.allowPendingPaneItems')
+ options.pending = false
+
# Avoid adding URLs as recent documents to work-around this Spotlight crash:
# https://github.com/atom/atom/issues/10071
if uri? and not url.parse(uri).protocol?
@@ -473,7 +486,8 @@ class Workspace extends Model
activateItem = options.activateItem ? true
if uri?
- item = pane.itemForURI(uri)
+ if item = pane.itemForURI(uri)
+ pane.clearPendingItem() if not options.pending and pane.getPendingItem() is item
item ?= opener(uri, options) for opener in @getOpeners() when not item
try
@@ -496,7 +510,7 @@ class Workspace extends Model
return item if pane.isDestroyed()
@itemOpened(item)
- pane.activateItem(item) if activateItem
+ pane.activateItem(item, {pending: options.pending}) if activateItem
pane.activate() if activatePane
initialLine = initialColumn = 0
@@ -535,7 +549,18 @@ class Workspace extends Model
throw error
@project.bufferForPath(filePath, options).then (buffer) =>
- @buildTextEditor(_.extend({buffer, largeFileMode}, options))
+ editor = @buildTextEditor(_.extend({buffer, largeFileMode}, options))
+ disposable = atom.textEditors.add(editor)
+ grammarSubscription = editor.observeGrammar(@handleGrammarUsed.bind(this))
+ editor.onDidDestroy ->
+ grammarSubscription.dispose()
+ disposable.dispose()
+ editor
+
+ handleGrammarUsed: (grammar) ->
+ return unless grammar?
+
+ @packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used")
# Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`.
#
@@ -548,8 +573,7 @@ class Workspace extends Model
# Returns a {TextEditor}.
buildTextEditor: (params) ->
params = _.extend({
- @config, @notificationManager, @packageManager, @clipboard, @viewRegistry,
- @grammarRegistry, @project, @assert, @applicationDelegate
+ @config, @clipboard, @grammarRegistry, @assert
}, params)
new TextEditor(params)
@@ -1063,3 +1087,22 @@ class Workspace extends Model
inProcessFinished = true
checkFinished()
+
+ checkoutHeadRevision: (editor) ->
+ if editor.getPath()
+ checkoutHead = =>
+ @project.repositoryForDirectory(new Directory(editor.getDirectoryPath()))
+ .then (repository) ->
+ repository?.async.checkoutHeadForEditor(editor)
+
+ if @config.get('editor.confirmCheckoutHeadRevision')
+ @applicationDelegate.confirm
+ message: 'Confirm Checkout HEAD Revision'
+ detailedMessage: "Are you sure you want to discard all changes to \"#{editor.getFileName()}\" since the last Git commit?"
+ buttons:
+ OK: checkoutHead
+ Cancel: null
+ else
+ checkoutHead()
+ else
+ Promise.resolve(false)
diff --git a/static/index.html b/static/index.html
index 5fcb30ad2..0bd4a7954 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1,7 +1,7 @@
-
+
diff --git a/static/index.js b/static/index.js
index 6d65d3c52..c33eda67a 100644
--- a/static/index.js
+++ b/static/index.js
@@ -54,7 +54,7 @@
}
function handleSetupError (error) {
- var currentWindow = require('remote').getCurrentWindow()
+ var currentWindow = require('electron').remote.getCurrentWindow()
currentWindow.setSize(800, 600)
currentWindow.center()
currentWindow.show()
@@ -71,9 +71,10 @@
ModuleCache.add(loadSettings.resourcePath)
// Start the crash reporter before anything else.
- require('crash-reporter').start({
+ require('electron').crashReporter.start({
productName: 'Atom',
companyName: 'GitHub',
+ submitURL: 'http://54.249.141.255:1127/post',
// By explicitly passing the app version here, we could save the call
// of "require('remote').require('app').getVersion()".
extra: {_version: loadSettings.appVersion}
@@ -83,8 +84,9 @@
setupCsonCache(CompileCache.getCacheDirectory())
var initialize = require(loadSettings.windowInitializationScript)
- initialize({blobStore: blobStore})
- require('ipc').sendChannel('window-command', 'window:loaded')
+ return initialize({blobStore: blobStore}).then(function () {
+ require('electron').ipcRenderer.send('window-command', 'window:loaded')
+ })
}
function setupCsonCache (cacheDir) {
@@ -112,19 +114,15 @@
function profileStartup (loadSettings, initialTime) {
function profile () {
console.profile('startup')
- try {
- var startTime = Date.now()
- setupWindow(loadSettings)
+ var startTime = Date.now()
+ setupWindow(loadSettings).then(function () {
setLoadTime(Date.now() - startTime + initialTime)
- } catch (error) {
- handleSetupError(error)
- } finally {
console.profileEnd('startup')
console.log('Switch to the Profiles tab to view the created startup profile')
- }
+ })
}
- var currentWindow = require('remote').getCurrentWindow()
+ var currentWindow = require('electron').remote.getCurrentWindow()
if (currentWindow.devToolsWebContents) {
profile()
} else {
@@ -145,31 +143,6 @@
}
}
- function setupWindowBackground () {
- if (loadSettings && loadSettings.isSpec) {
- return
- }
-
- var backgroundColor = window.localStorage.getItem('atom:window-background-color')
- if (!backgroundColor) {
- return
- }
-
- var backgroundStylesheet = document.createElement('style')
- backgroundStylesheet.type = 'text/css'
- backgroundStylesheet.innerText = 'html, body { background: ' + backgroundColor + ' !important; }'
- document.head.appendChild(backgroundStylesheet)
-
- // Remove once the page loads
- window.addEventListener('load', function loadWindow () {
- window.removeEventListener('load', loadWindow, false)
- setTimeout(function () {
- backgroundStylesheet.remove()
- backgroundStylesheet = null
- }, 1000)
- }, false)
- }
-
var setupAtomHome = function () {
if (process.env.ATOM_HOME) {
return
@@ -185,5 +158,4 @@
parseLoadSettings()
setupAtomHome()
- setupWindowBackground()
})()
diff --git a/static/text-editor-light.less b/static/text-editor-light.less
index 7fafade1e..f5429fd7f 100644
--- a/static/text-editor-light.less
+++ b/static/text-editor-light.less
@@ -15,7 +15,7 @@ atom-text-editor[mini] {
}
atom-overlay {
- position: absolute;
+ position: fixed;
display: block;
z-index: 4;
}