mirror of
https://github.com/atom/atom.git
synced 2026-04-06 03:02:13 -04:00
Merge branch 'master' into wl-build-on-node-7
This commit is contained in:
@@ -2,13 +2,15 @@
|
||||
|
||||
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
|
||||
|
||||
The following is a set of guidelines for contributing to Atom and its packages, which are hosted in the [Atom Organization](https://github.com/atom) on GitHub.
|
||||
These are just guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
|
||||
The following is a set of guidelines for contributing to Atom and its packages, which are hosted in the [Atom Organization](https://github.com/atom) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
|
||||
|
||||
#### Table Of Contents
|
||||
|
||||
[Code of Conduct](#code-of-conduct)
|
||||
|
||||
[I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question)
|
||||
|
||||
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
||||
* [Code of Conduct](#code-of-conduct)
|
||||
* [Atom and Packages](#atom-and-packages)
|
||||
* [Atom Design Decisions](#design-decisions)
|
||||
|
||||
@@ -28,23 +30,38 @@ These are just guidelines, not rules. Use your best judgment, and feel free to p
|
||||
[Additional Notes](#additional-notes)
|
||||
* [Issue and Pull Request Labels](#issue-and-pull-request-labels)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project and everyone participating in it is governed by the [Atom Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [atom@github.com](mailto:atom@github.com).
|
||||
|
||||
## I don't want to read this whole thing I just have a question!!!
|
||||
|
||||
> **Note:** [Please don't file an issue to ask a question.](http://blog.atom.io/2016/04/19/managing-the-deluge-of-atom-issues.html) You'll get faster results by using the resources below.
|
||||
|
||||
We have an official message board with a detailed FAQ and where the community chimes in with helpful advice if you have questions.
|
||||
|
||||
* [Discuss, the official Atom and Electron message board](https://discuss.atom.io)
|
||||
* [Atom FAQ](https://discuss.atom.io/c/faq)
|
||||
|
||||
If chat is more your speed, you can join the Atom and Electron Slack team:
|
||||
|
||||
* [Join the Atom and Electron Slack Team](http://atom-slack.herokuapp.com/)
|
||||
* Even though Slack is a chat service, sometimes it takes several hours for community members to respond — please be patient!
|
||||
* Use the `#atom` channel for general questions or discussion about Atom
|
||||
* Use the `#electron` channel for questions about Electron
|
||||
* Use the `#packages` channel for questions or discussion about writing or contributing to Atom packages (both core and community)
|
||||
* Use the `#ui` channel for questions and discussion about Atom UI and themes
|
||||
* There are many other channels available, check the channel list
|
||||
|
||||
## What should I know before I get started?
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md).
|
||||
By participating, you are expected to uphold this code.
|
||||
Please report unacceptable behavior to [atom@github.com](mailto:atom@github.com).
|
||||
|
||||
### Atom and Packages
|
||||
|
||||
Atom is a large open source project—it's made up of over [200 repositories](https://github.com/atom).
|
||||
When you initially consider contributing to Atom, you might be unsure about which of those 200 repositories implements the functionality you want to change or report a bug for.
|
||||
This section should help you with that.
|
||||
Atom is a large open source project — it's made up of over [200 repositories](https://github.com/atom). When you initially consider contributing to Atom, you might be unsure about which of those 200 repositories implements the functionality you want to change or report a bug for. This section should help you with that.
|
||||
|
||||
Atom is intentionally very modular.
|
||||
Nearly every non-editor UI element you interact with comes from a package, even fundamental things like tabs and the status-bar.
|
||||
These packages are packages in the same way that packages in the [package store](https://atom.io/packages) are packages, with one difference: they are bundled into the [default distribution](https://github.com/atom/atom/blob/10b8de6fc499a7def9b072739486e68530d67ab4/package.json#L58).
|
||||
Atom is intentionally very modular. Nearly every non-editor UI element you interact with comes from a package, even fundamental things like tabs and the status-bar. These packages are packages in the same way that packages in the [Atom package repository](https://atom.io/packages) are packages, with one difference: they are bundled into the [default distribution](https://github.com/atom/atom/blob/10b8de6fc499a7def9b072739486e68530d67ab4/package.json#L58).
|
||||
|
||||
<a id="atom-packages-image"/>
|
||||
|
||||

|
||||
|
||||
@@ -52,7 +69,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) 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 [Atom API](https://atom.io/docs/api/latest) and for large, overarching design proposals.
|
||||
* [tree-view](https://github.com/atom/tree-view) - file and directory listing on the left of the UI.
|
||||
* [fuzzy-finder](https://github.com/atom/fuzzy-finder) - the quick file opener.
|
||||
* [find-and-replace](https://github.com/atom/find-and-replace) - all search and replace functionality.
|
||||
@@ -68,11 +85,23 @@ Here's a list of the big ones:
|
||||
* [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).
|
||||
|
||||
There are many more, but this list should be a good starting point.
|
||||
For more information on how to work with Atom's official packages, see [Contributing to Atom Packages](https://github.com/atom/atom/blob/master/docs/contributing-to-packages.md).
|
||||
There are many more, but this list should be a good starting point. For more information on how to work with Atom's official packages, see [Contributing to Atom Packages](http://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/).
|
||||
|
||||
Also, because Atom is so extensible, it's possible that a feature you've become accustomed to in Atom or an issue you're encountering isn't coming from a bundled package at all, but rather a [community package](https://atom.io/packages) you've installed.
|
||||
Each community package has its own repository too, and you should be able to find it in Settings > Packages for the packages you installed and contribute there.
|
||||
Also, because Atom is so extensible, it's possible that a feature you've become accustomed to in Atom or an issue you're encountering isn't coming from a bundled package at all, but rather a [community package](https://atom.io/packages) you've installed. Each community package has its own repository too, the [Atom FAQ](https://discuss.atom.io/c/faq) has instructions on how to [contact the maintainers of any Atom community package or theme.](https://discuss.atom.io/t/i-have-a-question-about-a-specific-atom-community-package-where-is-the-best-place-to-ask-it/25581)
|
||||
|
||||
#### Package Conventions
|
||||
|
||||
Thera are a few conventions that have developed over time around packages:
|
||||
|
||||
* Packages that add one or more syntax highlighting grammars are named `language-[language-name]`
|
||||
* Language packages can add other things besides just a grammar. Many offer commonly-used snippets. Try not to add too much though.
|
||||
* Theme packages are split into two categories: UI and Syntax themes
|
||||
* UI themes are named `[theme-name]-ui`
|
||||
* Syntax themes are named `[theme-name]-syntax`
|
||||
* Often themes that are designed to work together are given the same root name, for example: `one-dark-ui` and `one-dark-syntax`
|
||||
* UI themes style everything outside of the editor pane — all of the green areas in the [packages image above](#atom-packages-image)
|
||||
* Syntax themes style just the items inside the editor pane, mostly syntax highlighting
|
||||
* Packages that add [autocomplete providers](https://github.com/atom/autocomplete-plus/wiki/Autocomplete-Providers) are named `autocomplete-[what-they-autocomplete]` — ex: [autocomplete-css](https://github.com/atom/autocomplete-css)
|
||||
|
||||
### Design Decisions
|
||||
|
||||
@@ -86,12 +115,14 @@ This section guides you through submitting a bug report for Atom. Following thes
|
||||
|
||||
Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](ISSUE_TEMPLATE.md), the information it asks for helps us resolve issues faster.
|
||||
|
||||
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
|
||||
|
||||
#### Before Submitting A Bug Report
|
||||
|
||||
* **Check the [debugging guide](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.
|
||||
* **Perform a [cursory search](https://github.com/issues?q=+is%3Aissue+user%3Aatom)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
|
||||
|
||||
#### How Do I Submit A (Good) Bug Report?
|
||||
|
||||
@@ -106,8 +137,8 @@ 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 macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
|
||||
* **If you're reporting that Atom crashed**, include a crash report with a stack trace from the operating system. On macOS, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist.
|
||||
* **If the problem is related to performance**, 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 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 block](https://help.github.com/articles/markdown-basics/#multiple-lines) and as a screenshot.
|
||||
* **If the problem is related to performance or memory**, include a [CPU profile capture](http://flight-manual.atom.io/hacking-atom/sections/debugging/#diagnose-runtime-performance) with your report.
|
||||
* **If Chrome's developer tools pane is shown without you triggering it**, that normally means that you have a syntax error in one of your themes or in your `styles.less`. Try running in [Safe Mode](http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode) and using a different theme or comment out the contents of your `styles.less` to see if that fixes the problem.
|
||||
* **If the problem 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:
|
||||
@@ -169,23 +200,20 @@ If you want to read about using Atom or developing packages in Atom, the [Atom F
|
||||
### Pull Requests
|
||||
|
||||
* Fill in [the required template](PULL_REQUEST_TEMPLATE.md)
|
||||
* Do not include issue numbers in the PR title
|
||||
* Include screenshots and animated GIFs in your pull request whenever possible.
|
||||
* Follow the [JavaScript](#javascript-styleguide) and [CoffeeScript](#coffeescript-styleguide) styleguides.
|
||||
* Include thoughtfully-worded, well-structured
|
||||
[Jasmine](http://jasmine.github.io/) specs in the `./spec` folder. Run them using `apm test`. See the [Specs Styleguide](#specs-styleguide) below.
|
||||
* Document new code based on the
|
||||
[Documentation Styleguide](#documentation-styleguide)
|
||||
* End files with a newline.
|
||||
* Include thoughtfully-worded, well-structured [Jasmine](http://jasmine.github.io/) specs in the `./spec` folder. Run them using `atom --test spec`. See the [Specs Styleguide](#specs-styleguide) below.
|
||||
* Document new code based on the [Documentation Styleguide](#documentation-styleguide)
|
||||
* End all files with a newline
|
||||
* [Avoid platform-dependent code](http://flight-manual.atom.io/hacking-atom/sections/cross-platform-compatibility/)
|
||||
* Place requires in the following order:
|
||||
* Built in Node Modules (such as `path`)
|
||||
* Built in Atom and Electron Modules (such as `atom`, `remote`)
|
||||
* Local Modules (using relative paths)
|
||||
* Place class properties in the following order:
|
||||
* Class methods and properties (methods starting with a `@`)
|
||||
* Class methods and properties (methods starting with a `@` in CoffeeScript or `static` in JavaScript)
|
||||
* Instance methods and properties
|
||||
* [Avoid platform-dependent code](http://flight-manual.atom.io/hacking-atom/sections/cross-platform-compatibility/)
|
||||
* Use a plain `return` when returning explicitly at the end of a function.
|
||||
* Not `return null`, `return undefined`, `null`, or `undefined`
|
||||
|
||||
## Styleguides
|
||||
|
||||
@@ -194,7 +222,7 @@ If you want to read about using Atom or developing packages in Atom, the [Atom F
|
||||
* Use the present tense ("Add feature" not "Added feature")
|
||||
* Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
|
||||
* Limit the first line to 72 characters or less
|
||||
* Reference issues and pull requests liberally
|
||||
* Reference issues and pull requests liberally after the first line
|
||||
* When only changing documentation, include `[ci skip]` in the commit description
|
||||
* Consider starting the commit message with an applicable emoji:
|
||||
* :art: `:art:` when improving the format/structure of the code
|
||||
@@ -257,8 +285,7 @@ All JavaScript must adhere to [JavaScript Standard Style](http://standardjs.com/
|
||||
|
||||
### Specs Styleguide
|
||||
|
||||
- Include thoughtfully-worded, well-structured
|
||||
[Jasmine](http://jasmine.github.io/) specs in the `./spec` folder.
|
||||
- Include thoughtfully-worded, well-structured [Jasmine](http://jasmine.github.io/) specs in the `./spec` folder.
|
||||
- Treat `describe` as a noun or situation.
|
||||
- Treat `it` as a statement about state or how an operation changes state.
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"url": "https://github.com/atom/atom.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"atom-package-manager": "1.18.1"
|
||||
"atom-package-manager": "1.18.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ install:
|
||||
- npm install -g npm
|
||||
|
||||
build_script:
|
||||
- IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp
|
||||
- SET SQUIRREL_TEMP=C:\sqtemp
|
||||
- CD %APPVEYOR_BUILD_FOLDER%
|
||||
- IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] (
|
||||
script\build.cmd --code-sign --create-windows-installer --compress-artifacts
|
||||
@@ -46,8 +48,6 @@ deploy: off
|
||||
artifacts:
|
||||
- path: out\AtomSetup.exe
|
||||
name: AtomSetup.exe
|
||||
- path: out\AtomSetup.msi
|
||||
name: AtomSetup.msi
|
||||
- path: out\atom-windows.zip
|
||||
name: atom-windows.zip
|
||||
- path: out\RELEASES
|
||||
|
||||
@@ -26,7 +26,7 @@ export default async function ({test}) {
|
||||
|
||||
let t0 = window.performance.now()
|
||||
const buffer = new TextBuffer({text})
|
||||
const editor = new TextEditor({buffer, largeFileMode: true})
|
||||
const editor = new TextEditor({buffer, autoHeight: false, largeFileMode: true})
|
||||
atom.workspace.getActivePane().activateItem(editor)
|
||||
let t1 = window.performance.now()
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export default async function ({test}) {
|
||||
|
||||
let t0 = window.performance.now()
|
||||
const buffer = new TextBuffer({text})
|
||||
const editor = new TextEditor({buffer, largeFileMode: true})
|
||||
const editor = new TextEditor({buffer, autoHeight: false, largeFileMode: true})
|
||||
editor.setGrammar(atom.grammars.grammarForScopeName('source.js'))
|
||||
atom.workspace.getActivePane().activateItem(editor)
|
||||
let t1 = window.performance.now()
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
| [Exception Reporting](https://github.com/atom/exception-reporting) | [](https://travis-ci.org/atom/exception-reporting) | [](https://ci.appveyor.com/project/Atom/exception-reporting/branch/master) | [](https://david-dm.org/atom/exception-reporting) |
|
||||
| [Find and Replace](https://github.com/atom/find-and-replace) | [](https://travis-ci.org/atom/find-and-replace) | [](https://ci.appveyor.com/project/Atom/find-and-replace/branch/master) | [](https://david-dm.org/atom/find-and-replace) |
|
||||
| [Fuzzy Finder](https://github.com/atom/fuzzy-finder) | [](https://travis-ci.org/atom/fuzzy-finder) | [](https://ci.appveyor.com/project/Atom/fuzzy-finder/branch/master) | [](https://david-dm.org/atom/fuzzy-finder) |
|
||||
| [GitHub](https://github.com/atom/github) | [](https://travis-ci.com/atom/github) | [](https://ci.appveyor.com/project/Atom/github/branch/master) | [](https://david-dm.org/atom/github) |
|
||||
| [Git Diff](https://github.com/atom/git-diff) | [](https://travis-ci.org/atom/git-diff) | [](https://ci.appveyor.com/project/Atom/git-diff/branch/master) | [](https://david-dm.org/atom/git-diff) |
|
||||
| [Go to Line](https://github.com/atom/go-to-line) | [](https://travis-ci.org/atom/go-to-line) | [](https://ci.appveyor.com/project/Atom/go-to-line/branch/master) | [](https://david-dm.org/atom/go-to-line) |
|
||||
| [Grammar Selector](https://github.com/atom/grammar-selector) | [](https://travis-ci.org/atom/grammar-selector) | [](https://ci.appveyor.com/project/Atom/grammar-selector/branch/master) | [](https://david-dm.org/atom/grammar-selector) |
|
||||
@@ -40,7 +41,7 @@
|
||||
| [Markdown Preview](https://github.com/atom/markdown-preview) | [](https://travis-ci.org/atom/markdown-preview) | [](https://ci.appveyor.com/project/Atom/markdown-preview/branch/master) | [](https://david-dm.org/atom/markdown-preview) |
|
||||
| [Metrics](https://github.com/atom/metrics) | [](https://travis-ci.org/atom/metrics) | [](https://ci.appveyor.com/project/Atom/metrics/branch/master) | [](https://david-dm.org/atom/metrics) |
|
||||
| [Notifications](https://github.com/atom/notifications) | [](https://travis-ci.org/atom/notifications) | [](https://ci.appveyor.com/project/Atom/notifications/branch/master) | [](https://david-dm.org/atom/notifications) |
|
||||
| [Open on Github](https://github.com/atom/open-on-github) | [](https://travis-ci.org/atom/open-on-github) | [](https://ci.appveyor.com/project/Atom/open-on-github/branch/master) | [](https://david-dm.org/atom/open-on-github) |
|
||||
| [Open on GitHub](https://github.com/atom/open-on-github) | [](https://travis-ci.org/atom/open-on-github) | [](https://ci.appveyor.com/project/Atom/open-on-github/branch/master) | [](https://david-dm.org/atom/open-on-github) |
|
||||
| [Package Generator](https://github.com/atom/package-generator) | [](https://travis-ci.org/atom/package-generator)| [](https://ci.appveyor.com/project/Atom/package-generator/branch/master) | [](https://david-dm.org/atom/package-generator) |
|
||||
| [Settings View](https://github.com/atom/settings-view) | [](https://travis-ci.org/atom/settings-view) | [](https://ci.appveyor.com/project/Atom/settings-view/branch/master) | [](https://david-dm.org/atom/settings-view) |
|
||||
| [Snippets](https://github.com/atom/snippets) | [](https://travis-ci.org/atom/snippets) | [](https://ci.appveyor.com/project/Atom/snippets/branch/master) | [](https://david-dm.org/atom/snippets) |
|
||||
@@ -109,6 +110,7 @@
|
||||
| [Sass](https://github.com/atom/language-sass) | [](https://travis-ci.org/atom/language-sass) | [](https://ci.appveyor.com/project/Atom/language-sass/branch/master) |
|
||||
| [ShellScript](https://github.com/atom/language-shellscript) | [](https://travis-ci.org/atom/language-shellscript) | [](https://ci.appveyor.com/project/Atom/language-shellscript/branch/master) |
|
||||
| [SQL](https://github.com/atom/language-sql) | [](https://travis-ci.org/atom/language-sql) | [](https://ci.appveyor.com/project/Atom/language-sql/branch/master) |
|
||||
| [Text](https://github.com/atom/language-text) | [](https://travis-ci.org/atom/language-text) | [](https://ci.appveyor.com/project/Atom/language-text/branch/master) |
|
||||
| [TODO](https://github.com/atom/language-todo) | [](https://travis-ci.org/atom/language-todo) | [](https://ci.appveyor.com/project/Atom/language-todo/branch/master) |
|
||||
| [TOML](https://github.com/atom/language-toml) | [](https://travis-ci.org/atom/language-toml) | [](https://ci.appveyor.com/project/Atom/language-toml/branch/master) |
|
||||
| [XML](https://github.com/atom/language-xml) | [](https://travis-ci.org/atom/language-xml) | [](https://ci.appveyor.com/project/Atom/language-xml/branch/master) |
|
||||
|
||||
77
package.json
77
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "atom",
|
||||
"productName": "Atom",
|
||||
"version": "1.18.0-dev",
|
||||
"version": "1.19.0-dev",
|
||||
"description": "A hackable text editor for the 21st Century.",
|
||||
"main": "./src/main-process/main.js",
|
||||
"repository": {
|
||||
@@ -12,7 +12,7 @@
|
||||
"url": "https://github.com/atom/atom/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"electronVersion": "1.3.15",
|
||||
"electronVersion": "1.6.9",
|
||||
"dependencies": {
|
||||
"async": "0.2.6",
|
||||
"atom-keymap": "8.1.2",
|
||||
@@ -27,9 +27,10 @@
|
||||
"color": "^0.7.3",
|
||||
"dedent": "^0.6.0",
|
||||
"devtron": "1.3.0",
|
||||
"etch": "^0.12.4",
|
||||
"event-kit": "^2.3.0",
|
||||
"find-parent-dir": "^0.3.0",
|
||||
"first-mate": "7.0.4",
|
||||
"first-mate": "7.0.7",
|
||||
"fs-plus": "^3.0.0",
|
||||
"fstream": "0.1.24",
|
||||
"fuzzaldrin": "^2.1",
|
||||
@@ -38,10 +39,9 @@
|
||||
"grim": "1.5.0",
|
||||
"jasmine-json": "~0.0",
|
||||
"jasmine-tagged": "^1.1.4",
|
||||
"jquery": "2.1.4",
|
||||
"key-path-helpers": "^0.4.0",
|
||||
"less-cache": "1.1.0",
|
||||
"line-top-index": "0.2.0",
|
||||
"line-top-index": "0.3.1",
|
||||
"marked": "^0.3.6",
|
||||
"minimatch": "^3.0.3",
|
||||
"mocha": "2.5.1",
|
||||
@@ -63,9 +63,9 @@
|
||||
"semver": "^4.3.3",
|
||||
"service-hub": "^0.7.3",
|
||||
"sinon": "1.17.4",
|
||||
"source-map-support": "^0.3.2",
|
||||
"@atom/source-map-support": "^0.3.4",
|
||||
"temp": "^0.8.3",
|
||||
"text-buffer": "11.4.1",
|
||||
"text-buffer": "12.1.4",
|
||||
"typescript-simple": "1.0.0",
|
||||
"underscore-plus": "^1.6.6",
|
||||
"winreg": "^1.2.1",
|
||||
@@ -87,24 +87,24 @@
|
||||
"about": "1.7.6",
|
||||
"archive-view": "0.63.2",
|
||||
"autocomplete-atom-api": "0.10.1",
|
||||
"autocomplete-css": "0.16.1",
|
||||
"autocomplete-html": "0.7.3",
|
||||
"autocomplete-css": "0.16.2",
|
||||
"autocomplete-html": "0.8.0",
|
||||
"autocomplete-plus": "2.35.4",
|
||||
"autocomplete-snippets": "1.11.0",
|
||||
"autoflow": "0.29.0",
|
||||
"autosave": "0.24.3",
|
||||
"background-tips": "0.27.0",
|
||||
"background-tips": "0.27.1",
|
||||
"bookmarks": "0.44.4",
|
||||
"bracket-matcher": "0.85.5",
|
||||
"bracket-matcher": "0.86.0",
|
||||
"command-palette": "0.40.4",
|
||||
"dalek": "0.2.1",
|
||||
"deprecation-cop": "0.56.7",
|
||||
"dev-live-reload": "0.47.1",
|
||||
"encoding-selector": "0.23.3",
|
||||
"exception-reporting": "0.41.4",
|
||||
"find-and-replace": "0.208.1",
|
||||
"fuzzy-finder": "1.5.6",
|
||||
"github": "0.0.6",
|
||||
"find-and-replace": "0.208.3",
|
||||
"fuzzy-finder": "1.5.8",
|
||||
"github": "0.3.0",
|
||||
"git-diff": "1.3.6",
|
||||
"go-to-line": "0.32.1",
|
||||
"grammar-selector": "0.49.4",
|
||||
@@ -114,34 +114,34 @@
|
||||
"line-ending-selector": "0.6.3",
|
||||
"link": "0.31.3",
|
||||
"markdown-preview": "0.159.12",
|
||||
"metrics": "1.2.3",
|
||||
"notifications": "0.67.1",
|
||||
"metrics": "1.2.5",
|
||||
"notifications": "0.67.2",
|
||||
"open-on-github": "1.2.1",
|
||||
"package-generator": "1.1.1",
|
||||
"settings-view": "0.249.4",
|
||||
"settings-view": "0.250.0",
|
||||
"snippets": "1.1.4",
|
||||
"spell-check": "0.71.4",
|
||||
"status-bar": "1.8.8",
|
||||
"styleguide": "0.49.6",
|
||||
"symbols-view": "0.116.0",
|
||||
"tabs": "0.105.5",
|
||||
"tabs": "0.106.0",
|
||||
"timecop": "0.36.0",
|
||||
"tree-view": "0.217.0-7",
|
||||
"update-package-dependencies": "0.11.0",
|
||||
"welcome": "0.36.3",
|
||||
"tree-view": "0.217.1",
|
||||
"update-package-dependencies": "0.12.0",
|
||||
"welcome": "0.36.4",
|
||||
"whitespace": "0.36.2",
|
||||
"wrap-guide": "0.40.2",
|
||||
"language-c": "0.58.0",
|
||||
"language-clojure": "0.22.2",
|
||||
"language-c": "0.58.1",
|
||||
"language-clojure": "0.22.3",
|
||||
"language-coffee-script": "0.48.7",
|
||||
"language-csharp": "0.14.2",
|
||||
"language-css": "0.42.2",
|
||||
"language-gfm": "0.89.0",
|
||||
"language-gfm": "0.89.1",
|
||||
"language-git": "0.19.1",
|
||||
"language-go": "0.44.0",
|
||||
"language-html": "0.47.2",
|
||||
"language-go": "0.44.1",
|
||||
"language-html": "0.47.3",
|
||||
"language-hyperlink": "0.16.1",
|
||||
"language-java": "0.27.1",
|
||||
"language-java": "0.27.2",
|
||||
"language-javascript": "0.126.1",
|
||||
"language-json": "0.19.1",
|
||||
"language-less": "0.32.0",
|
||||
@@ -149,20 +149,20 @@
|
||||
"language-mustache": "0.14.1",
|
||||
"language-objective-c": "0.15.1",
|
||||
"language-perl": "0.37.0",
|
||||
"language-php": "0.38.0",
|
||||
"language-php": "0.39.0",
|
||||
"language-property-list": "0.9.1",
|
||||
"language-python": "0.45.2",
|
||||
"language-ruby": "0.71.0",
|
||||
"language-python": "0.45.3",
|
||||
"language-ruby": "0.71.1",
|
||||
"language-ruby-on-rails": "0.25.2",
|
||||
"language-sass": "0.59.0",
|
||||
"language-shellscript": "0.25.0",
|
||||
"language-shellscript": "0.25.1",
|
||||
"language-source": "0.9.0",
|
||||
"language-sql": "0.25.5",
|
||||
"language-text": "0.7.2",
|
||||
"language-sql": "0.25.6",
|
||||
"language-text": "0.7.3",
|
||||
"language-todo": "0.29.1",
|
||||
"language-toml": "0.18.1",
|
||||
"language-xml": "0.35.0",
|
||||
"language-yaml": "0.29.0"
|
||||
"language-xml": "0.35.1",
|
||||
"language-yaml": "0.30.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -186,7 +186,12 @@
|
||||
"spyOn",
|
||||
"waitsFor",
|
||||
"waitsForPromise",
|
||||
"indexedDB"
|
||||
"indexedDB",
|
||||
"IntersectionObserver",
|
||||
"FocusEvent",
|
||||
"requestAnimationFrame",
|
||||
"HTMLElement",
|
||||
"snapshotResult"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
41
script/lib/backup-node-modules.js
Normal file
41
script/lib/backup-node-modules.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
|
||||
module.exports = function(packagePath) {
|
||||
const nodeModulesPath = path.join(packagePath, 'node_modules')
|
||||
const nodeModulesBackupPath = path.join(packagePath, 'node_modules.bak')
|
||||
|
||||
if (fs.existsSync(nodeModulesBackupPath)) {
|
||||
throw new Error("Cannot back up " + nodeModulesPath + "; " + nodeModulesBackupPath + " already exists")
|
||||
}
|
||||
|
||||
// some packages may have no node_modules after deduping, but we still want
|
||||
// to "back-up" and later restore that fact
|
||||
if (!fs.existsSync(nodeModulesPath)) {
|
||||
const msg = "Skipping backing up " + nodeModulesPath + " as it does not exist"
|
||||
console.log(msg.gray)
|
||||
|
||||
const restore = function stubRestoreNodeModules() {
|
||||
if (fs.existsSync(nodeModulesPath)) {
|
||||
fs.removeSync(nodeModulesPath)
|
||||
}
|
||||
}
|
||||
|
||||
return {restore, nodeModulesPath, nodeModulesBackupPath}
|
||||
}
|
||||
|
||||
fs.copySync(nodeModulesPath, nodeModulesBackupPath)
|
||||
|
||||
const restore = function restoreNodeModules() {
|
||||
if (!fs.existsSync(nodeModulesBackupPath)) {
|
||||
throw new Error("Cannot restore " + nodeModulesPath + "; " + nodeModulesBackupPath + " does not exist")
|
||||
}
|
||||
|
||||
if (fs.existsSync(nodeModulesPath)) {
|
||||
fs.removeSync(nodeModulesPath)
|
||||
}
|
||||
fs.renameSync(nodeModulesBackupPath, nodeModulesPath)
|
||||
}
|
||||
|
||||
return {restore, nodeModulesPath, nodeModulesBackupPath}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ module.exports = (packagedAppPath, codeSign) => {
|
||||
iconUrl: `https://raw.githubusercontent.com/atom/atom/master/resources/app-icons/${CONFIG.channel}/atom.ico`,
|
||||
loadingGif: path.join(CONFIG.repositoryRootPath, 'resources', 'win', 'loading.gif'),
|
||||
outputDirectory: CONFIG.buildOutputPath,
|
||||
noMsi: true,
|
||||
remoteReleases: `https://atom.io/api/updates${archSuffix}?version=${CONFIG.appMetadata.version}`,
|
||||
setupIcon: path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'atom.ico')
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ module.exports = function (packagedAppPath) {
|
||||
relativePath === path.join('..', 'node_modules', 'markdown-preview', 'node_modules', 'htmlparser2', 'lib', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'roaster', 'node_modules', 'htmlparser2', 'lib', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'task-lists', 'node_modules', 'htmlparser2', 'lib', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'iconv-lite', 'encodings', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'iconv-lite', 'lib', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'less', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'less', 'lib', 'less', 'fs.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'less', 'lib', 'less-node', 'index.js') ||
|
||||
|
||||
@@ -1,29 +1,54 @@
|
||||
'use strict'
|
||||
|
||||
const CompileCache = require('../../src/compile-cache')
|
||||
const fs = require('fs')
|
||||
const fs = require('fs-extra')
|
||||
const glob = require('glob')
|
||||
const path = require('path')
|
||||
|
||||
const CONFIG = require('../config')
|
||||
const backupNodeModules = require('./backup-node-modules')
|
||||
const runApmInstall = require('./run-apm-install')
|
||||
|
||||
require('colors')
|
||||
|
||||
|
||||
module.exports = function () {
|
||||
console.log(`Transpiling packages with custom transpiler configurations in ${CONFIG.intermediateAppPath}`)
|
||||
let pathsToCompile = []
|
||||
for (let packageName of Object.keys(CONFIG.appMetadata.packageDependencies)) {
|
||||
const packagePath = path.join(CONFIG.intermediateAppPath, 'node_modules', packageName)
|
||||
const metadataPath = path.join(packagePath, 'package.json')
|
||||
const metadata = require(metadataPath)
|
||||
if (metadata.atomTranspilers) {
|
||||
CompileCache.addTranspilerConfigForPath(packagePath, metadata.name, metadata, metadata.atomTranspilers)
|
||||
for (let config of metadata.atomTranspilers) {
|
||||
pathsToCompile = pathsToCompile.concat(glob.sync(path.join(packagePath, config.glob), {nodir: true}))
|
||||
}
|
||||
}
|
||||
}
|
||||
const rootPackagePath = path.join(CONFIG.repositoryRootPath, 'node_modules', packageName)
|
||||
const intermediatePackagePath = path.join(CONFIG.intermediateAppPath, 'node_modules', packageName)
|
||||
|
||||
for (let path of pathsToCompile) {
|
||||
transpilePath(path)
|
||||
const metadataPath = path.join(intermediatePackagePath, 'package.json')
|
||||
const metadata = require(metadataPath)
|
||||
|
||||
if (metadata.atomTranspilers) {
|
||||
console.log(' transpiling for package '.cyan + packageName.cyan)
|
||||
const rootPackageBackup = backupNodeModules(rootPackagePath)
|
||||
const intermediatePackageBackup = backupNodeModules(intermediatePackagePath)
|
||||
|
||||
// Run `apm install` in the *root* pacakge's path, so we get devDeps w/o apm's weird caching
|
||||
// Then copy this folder into the intermediate package's path so we can run the transpilation in-line.
|
||||
runApmInstall(rootPackagePath)
|
||||
if (fs.existsSync(intermediatePackageBackup.nodeModulesPath)) {
|
||||
fs.removeSync(intermediatePackageBackup.nodeModulesPath)
|
||||
}
|
||||
fs.copySync(rootPackageBackup.nodeModulesPath, intermediatePackageBackup.nodeModulesPath)
|
||||
|
||||
CompileCache.addTranspilerConfigForPath(intermediatePackagePath, metadata.name, metadata, metadata.atomTranspilers)
|
||||
for (let config of metadata.atomTranspilers) {
|
||||
const pathsToCompile = glob.sync(path.join(intermediatePackagePath, config.glob), {nodir: true})
|
||||
pathsToCompile.forEach(transpilePath)
|
||||
}
|
||||
|
||||
// Now that we've transpiled everything in-place, we no longer want Atom to try to transpile
|
||||
// the same files when they're being required.
|
||||
delete metadata.atomTranspilers
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, ' '), 'utf8')
|
||||
|
||||
CompileCache.removeTranspilerConfigForPath(intermediatePackagePath)
|
||||
rootPackageBackup.restore()
|
||||
intermediatePackageBackup.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
"coffeelint": "1.15.7",
|
||||
"colors": "1.1.2",
|
||||
"csslint": "1.0.2",
|
||||
"donna": "1.0.13",
|
||||
"electron-chromedriver": "~1.3",
|
||||
"electron-link": "0.0.23",
|
||||
"electron-mksnapshot": "~1.3",
|
||||
"donna": "1.0.16",
|
||||
"electron-chromedriver": "~1.6",
|
||||
"electron-link": "0.1.0",
|
||||
"electron-mksnapshot": "~1.6",
|
||||
"electron-packager": "7.3.0",
|
||||
"electron-winstaller": "2.5.1",
|
||||
"electron-winstaller": "2.5.2",
|
||||
"fs-extra": "0.30.0",
|
||||
"glob": "7.0.3",
|
||||
"joanna": "0.0.8",
|
||||
|
||||
@@ -11,6 +11,7 @@ const glob = require('glob')
|
||||
const path = require('path')
|
||||
|
||||
const CONFIG = require('./config')
|
||||
const backupNodeModules = require('./lib/backup-node-modules')
|
||||
const runApmInstall = require('./lib/run-apm-install')
|
||||
|
||||
const resourcePath = CONFIG.repositoryRootPath
|
||||
@@ -94,11 +95,8 @@ for (let packageName in CONFIG.appMetadata.packageDependencies) {
|
||||
if (require(pkgJsonPath).atomTestRunner) {
|
||||
console.log(`Installing test runner dependencies for ${packageName}`.bold.green)
|
||||
if (fs.existsSync(nodeModulesPath)) {
|
||||
fs.copySync(nodeModulesPath, nodeModulesBackupPath)
|
||||
finalize = () => {
|
||||
fs.removeSync(nodeModulesPath)
|
||||
fs.renameSync(nodeModulesBackupPath, nodeModulesPath)
|
||||
}
|
||||
const backup = backupNodeModules(repositoryPackagePath)
|
||||
finalize = backup.restore
|
||||
} else {
|
||||
finalize = () => fs.removeSync(nodeModulesPath)
|
||||
}
|
||||
@@ -110,6 +108,7 @@ for (let packageName in CONFIG.appMetadata.packageDependencies) {
|
||||
const cp = childProcess.spawn(executablePath, testArguments)
|
||||
let stderrOutput = ''
|
||||
cp.stderr.on('data', data => { stderrOutput += data })
|
||||
cp.stdout.on('data', data => { stderrOutput += data })
|
||||
cp.on('error', error => {
|
||||
finalize()
|
||||
callback(error)
|
||||
|
||||
@@ -29,10 +29,12 @@ describe "AtomEnvironment", ->
|
||||
atom.setSize(originalSize.width, originalSize.height)
|
||||
|
||||
it 'sets the size of the window, and can retrieve the size just set', ->
|
||||
newWidth = originalSize.width + 12
|
||||
newHeight = originalSize.height + 23
|
||||
atom.setSize(newWidth, newHeight)
|
||||
expect(atom.getSize()).toEqual width: newWidth, height: newHeight
|
||||
newWidth = originalSize.width - 12
|
||||
newHeight = originalSize.height - 23
|
||||
waitsForPromise ->
|
||||
atom.setSize(newWidth, newHeight)
|
||||
runs ->
|
||||
expect(atom.getSize()).toEqual width: newWidth, height: newHeight
|
||||
|
||||
describe ".isReleasedVersion()", ->
|
||||
it "returns false if the version is a SHA and true otherwise", ->
|
||||
@@ -221,44 +223,70 @@ describe "AtomEnvironment", ->
|
||||
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')
|
||||
atomEnv = new AtomEnvironment({
|
||||
applicationDelegate: global.atom.applicationDelegate,
|
||||
})
|
||||
idleCallbacks = []
|
||||
spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback)
|
||||
atomEnv.initialize({
|
||||
window: {
|
||||
requestIdleCallback: (callback) -> idleCallbacks.push(callback),
|
||||
addEventListener: ->
|
||||
removeEventListener: ->
|
||||
},
|
||||
document: document.implementation.createHTMLDocument()
|
||||
})
|
||||
|
||||
spyOn(atomEnv, 'saveState')
|
||||
|
||||
keydown = new KeyboardEvent('keydown')
|
||||
atom.document.dispatchEvent(keydown)
|
||||
advanceClock atom.saveStateDebounceInterval
|
||||
atomEnv.document.dispatchEvent(keydown)
|
||||
advanceClock atomEnv.saveStateDebounceInterval
|
||||
idleCallbacks.shift()()
|
||||
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
|
||||
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
|
||||
expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false})
|
||||
expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true})
|
||||
|
||||
atom.saveState.reset()
|
||||
atomEnv.saveState.reset()
|
||||
mousedown = new MouseEvent('mousedown')
|
||||
atom.document.dispatchEvent(mousedown)
|
||||
advanceClock atom.saveStateDebounceInterval
|
||||
atomEnv.document.dispatchEvent(mousedown)
|
||||
advanceClock atomEnv.saveStateDebounceInterval
|
||||
idleCallbacks.shift()()
|
||||
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
|
||||
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
|
||||
expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false})
|
||||
expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true})
|
||||
|
||||
atomEnv.destroy()
|
||||
|
||||
it "ignores mousedown/keydown events happening after calling unloadEditorWindow", ->
|
||||
spyOn(atom, 'saveState')
|
||||
atomEnv = new AtomEnvironment({
|
||||
applicationDelegate: global.atom.applicationDelegate,
|
||||
})
|
||||
idleCallbacks = []
|
||||
spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback)
|
||||
atomEnv.initialize({
|
||||
window: {
|
||||
requestIdleCallback: (callback) -> idleCallbacks.push(callback),
|
||||
addEventListener: ->
|
||||
removeEventListener: ->
|
||||
},
|
||||
document: document.implementation.createHTMLDocument()
|
||||
})
|
||||
|
||||
spyOn(atomEnv, 'saveState')
|
||||
|
||||
mousedown = new MouseEvent('mousedown')
|
||||
atom.document.dispatchEvent(mousedown)
|
||||
atom.unloadEditorWindow()
|
||||
expect(atom.saveState).not.toHaveBeenCalled()
|
||||
atomEnv.document.dispatchEvent(mousedown)
|
||||
atomEnv.unloadEditorWindow()
|
||||
expect(atomEnv.saveState).not.toHaveBeenCalled()
|
||||
|
||||
advanceClock atom.saveStateDebounceInterval
|
||||
advanceClock atomEnv.saveStateDebounceInterval
|
||||
idleCallbacks.shift()()
|
||||
expect(atom.saveState).not.toHaveBeenCalled()
|
||||
expect(atomEnv.saveState).not.toHaveBeenCalled()
|
||||
|
||||
mousedown = new MouseEvent('mousedown')
|
||||
atom.document.dispatchEvent(mousedown)
|
||||
advanceClock atom.saveStateDebounceInterval
|
||||
atomEnv.document.dispatchEvent(mousedown)
|
||||
advanceClock atomEnv.saveStateDebounceInterval
|
||||
idleCallbacks.shift()()
|
||||
expect(atom.saveState).not.toHaveBeenCalled()
|
||||
expect(atomEnv.saveState).not.toHaveBeenCalled()
|
||||
|
||||
atomEnv.destroy()
|
||||
|
||||
it "serializes the project state with all the options supplied in saveState", ->
|
||||
spyOn(atom.project, 'serialize').andReturn({foo: 42})
|
||||
@@ -288,10 +316,13 @@ describe "AtomEnvironment", ->
|
||||
}
|
||||
)
|
||||
})
|
||||
atom2.initialize({document, window})
|
||||
atom2.deserialize(atom.serialize())
|
||||
|
||||
expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain')
|
||||
|
||||
atom2.destroy()
|
||||
|
||||
describe "openInitialEmptyEditorIfNecessary", ->
|
||||
describe "when there are no paths set", ->
|
||||
beforeEach ->
|
||||
@@ -392,6 +423,17 @@ describe "AtomEnvironment", ->
|
||||
expect(atom.workspace.open.callCount).toBe(1)
|
||||
expect(atom.workspace.open).toHaveBeenCalledWith(__filename)
|
||||
|
||||
describe "when a dock has a non-text editor", ->
|
||||
it "doesn't prompt the user to restore state", ->
|
||||
dock = atom.workspace.getLeftDock()
|
||||
dock.getActivePane().addItem
|
||||
getTitle: -> 'title'
|
||||
element: document.createElement 'div'
|
||||
state = Symbol()
|
||||
spyOn(atom, 'confirm')
|
||||
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
|
||||
expect(atom.confirm).not.toHaveBeenCalled()
|
||||
|
||||
describe "when the window is dirty", ->
|
||||
editor = null
|
||||
|
||||
@@ -400,6 +442,17 @@ describe "AtomEnvironment", ->
|
||||
editor = e
|
||||
editor.setText('new editor')
|
||||
|
||||
describe "when a dock has a modified editor", ->
|
||||
it "prompts the user to restore the state", ->
|
||||
dock = atom.workspace.getLeftDock()
|
||||
dock.getActivePane().addItem editor
|
||||
spyOn(atom, "confirm").andReturn(1)
|
||||
spyOn(atom.project, 'addPath')
|
||||
spyOn(atom.workspace, 'open')
|
||||
state = Symbol()
|
||||
atom.attemptRestoreProjectStateForPaths(state, [__dirname], [__filename])
|
||||
expect(atom.confirm).toHaveBeenCalled()
|
||||
|
||||
it "prompts the user to restore the state in a new window, discarding it and adding folder to current window", ->
|
||||
spyOn(atom, "confirm").andReturn(1)
|
||||
spyOn(atom.project, 'addPath')
|
||||
@@ -452,11 +505,14 @@ describe "AtomEnvironment", ->
|
||||
}
|
||||
atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate})
|
||||
atomEnvironment.initialize({window, document: fakeDocument})
|
||||
spyOn(atomEnvironment.packages, 'getAvailablePackagePaths').andReturn []
|
||||
spyOn(atomEnvironment, 'displayWindow').andReturn Promise.resolve()
|
||||
atomEnvironment.startEditorWindow()
|
||||
atomEnvironment.unloadEditorWindow()
|
||||
atomEnvironment.destroy()
|
||||
spyOn(atomEnvironment.packages, 'loadPackages').andReturn(Promise.resolve())
|
||||
spyOn(atomEnvironment.packages, 'activate').andReturn(Promise.resolve())
|
||||
spyOn(atomEnvironment, 'displayWindow').andReturn(Promise.resolve())
|
||||
waitsForPromise ->
|
||||
atomEnvironment.startEditorWindow()
|
||||
runs ->
|
||||
atomEnvironment.unloadEditorWindow()
|
||||
atomEnvironment.destroy()
|
||||
|
||||
describe "::whenShellEnvironmentLoaded()", ->
|
||||
[atomEnvironment, envLoaded, spy] = []
|
||||
@@ -471,21 +527,19 @@ describe "AtomEnvironment", ->
|
||||
applicationDelegate: atom.applicationDelegate
|
||||
updateProcessEnv: -> promise
|
||||
atomEnvironment.initialize({window, document})
|
||||
spyOn(atomEnvironment.packages, 'getAvailablePackagePaths').andReturn []
|
||||
spyOn(atomEnvironment, 'displayWindow').andReturn Promise.resolve()
|
||||
spy = jasmine.createSpy()
|
||||
atomEnvironment.startEditorWindow()
|
||||
|
||||
afterEach ->
|
||||
atomEnvironment.unloadEditorWindow()
|
||||
atomEnvironment.destroy()
|
||||
|
||||
it "is triggered once the shell environment is loaded", ->
|
||||
atomEnvironment.whenShellEnvironmentLoaded spy
|
||||
atomEnvironment.updateProcessEnvAndTriggerHooks()
|
||||
envLoaded()
|
||||
runs -> expect(spy).toHaveBeenCalled()
|
||||
|
||||
it "triggers the callback immediately if the shell environment is already loaded", ->
|
||||
atomEnvironment.updateProcessEnvAndTriggerHooks()
|
||||
envLoaded()
|
||||
runs ->
|
||||
atomEnvironment.whenShellEnvironmentLoaded spy
|
||||
|
||||
@@ -21,8 +21,8 @@ formatStackTrace = (spec, message='', stackTrace) ->
|
||||
lines.shift() if message.trim() is errorMatch?[1]?.trim()
|
||||
|
||||
for line, index in lines
|
||||
# Remove prefix of lines matching: at .<anonymous> (path:1:2)
|
||||
prefixMatch = line.match(/at \.<anonymous> \(([^)]+)\)/)
|
||||
# Remove prefix of lines matching: at jasmine.Spec.<anonymous> (path:1:2)
|
||||
prefixMatch = line.match(/at jasmine\.Spec\.<anonymous> \(([^)]+)\)/)
|
||||
line = "at #{prefixMatch[1]}" if prefixMatch
|
||||
|
||||
# Relativize locations to spec directory
|
||||
|
||||
@@ -43,6 +43,7 @@ describe "Babel transpiler support", ->
|
||||
|
||||
describe "when a .js file does not start with 'use babel';", ->
|
||||
it "does not transpile it using babel", ->
|
||||
spyOn(console, 'error')
|
||||
expect(-> require('./fixtures/babel/invalid.js')).toThrow()
|
||||
|
||||
it "does not try to log to stdout or stderr while parsing the file", ->
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
CustomGutterComponent = require '../src/custom-gutter-component'
|
||||
Gutter = require '../src/gutter'
|
||||
|
||||
describe "CustomGutterComponent", ->
|
||||
[gutterComponent, gutter] = []
|
||||
|
||||
beforeEach ->
|
||||
mockGutterContainer = {}
|
||||
gutter = new Gutter(mockGutterContainer, {name: 'test-gutter'})
|
||||
gutterComponent = new CustomGutterComponent({gutter, views: atom.views})
|
||||
|
||||
it "creates a gutter DOM node with only an empty 'custom-decorations' child node when it is initialized", ->
|
||||
expect(gutterComponent.getDomNode().classList.contains('gutter')).toBe true
|
||||
expect(gutterComponent.getDomNode().getAttribute('gutter-name')).toBe 'test-gutter'
|
||||
expect(gutterComponent.getDomNode().children.length).toBe 1
|
||||
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
|
||||
expect(decorationsWrapperNode.classList.contains('custom-decorations')).toBe true
|
||||
|
||||
it "makes its view accessible from the view registry", ->
|
||||
expect(gutterComponent.getDomNode()).toBe gutter.getElement()
|
||||
|
||||
it "hides its DOM node when ::hideNode is called, and shows its DOM node when ::showNode is called", ->
|
||||
gutterComponent.hideNode()
|
||||
expect(gutterComponent.getDomNode().style.display).toBe 'none'
|
||||
gutterComponent.showNode()
|
||||
expect(gutterComponent.getDomNode().style.display).toBe ''
|
||||
|
||||
describe "::updateSync", ->
|
||||
decorationItem1 = document.createElement('div')
|
||||
|
||||
buildTestState = (customDecorations) ->
|
||||
mockTestState =
|
||||
content: if customDecorations then customDecorations else {}
|
||||
styles:
|
||||
scrollHeight: 100
|
||||
scrollTop: 10
|
||||
backgroundColor: 'black'
|
||||
|
||||
mockTestState
|
||||
|
||||
it "sets the custom-decoration wrapper's scrollHeight, scrollTop, and background color", ->
|
||||
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
|
||||
expect(decorationsWrapperNode.style.height).toBe ''
|
||||
expect(decorationsWrapperNode.style['-webkit-transform']).toBe ''
|
||||
expect(decorationsWrapperNode.style.backgroundColor).toBe ''
|
||||
|
||||
gutterComponent.updateSync(buildTestState({}))
|
||||
expect(decorationsWrapperNode.style.height).not.toBe ''
|
||||
expect(decorationsWrapperNode.style['-webkit-transform']).not.toBe ''
|
||||
expect(decorationsWrapperNode.style.backgroundColor).not.toBe ''
|
||||
|
||||
it "creates a new DOM node for a new decoration and adds it to the gutter at the right place", ->
|
||||
customDecorations =
|
||||
'decoration-id-1':
|
||||
top: 0
|
||||
height: 10
|
||||
item: decorationItem1
|
||||
class: 'test-class-1'
|
||||
|
||||
gutterComponent.updateSync(buildTestState(customDecorations))
|
||||
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
|
||||
expect(decorationsWrapperNode.children.length).toBe 1
|
||||
|
||||
decorationNode = decorationsWrapperNode.children.item(0)
|
||||
expect(decorationNode.style.top).toBe '0px'
|
||||
expect(decorationNode.style.height).toBe '10px'
|
||||
expect(decorationNode.classList.contains('test-class-1')).toBe true
|
||||
expect(decorationNode.classList.contains('decoration')).toBe true
|
||||
expect(decorationNode.children.length).toBe 1
|
||||
|
||||
decorationItem = decorationNode.children.item(0)
|
||||
expect(decorationItem).toBe decorationItem1
|
||||
|
||||
it "updates the existing DOM node for a decoration that existed but has new properties", ->
|
||||
initialCustomDecorations =
|
||||
'decoration-id-1':
|
||||
top: 0
|
||||
height: 10
|
||||
item: decorationItem1
|
||||
class: 'test-class-1'
|
||||
gutterComponent.updateSync(buildTestState(initialCustomDecorations))
|
||||
initialDecorationNode = gutterComponent.getDomNode().children.item(0).children.item(0)
|
||||
|
||||
# Change the dimensions and item, remove the class.
|
||||
decorationItem2 = document.createElement('div')
|
||||
changedCustomDecorations =
|
||||
'decoration-id-1':
|
||||
top: 10
|
||||
height: 20
|
||||
item: decorationItem2
|
||||
gutterComponent.updateSync(buildTestState(changedCustomDecorations))
|
||||
changedDecorationNode = gutterComponent.getDomNode().children.item(0).children.item(0)
|
||||
expect(changedDecorationNode).toBe initialDecorationNode
|
||||
expect(changedDecorationNode.style.top).toBe '10px'
|
||||
expect(changedDecorationNode.style.height).toBe '20px'
|
||||
expect(changedDecorationNode.classList.contains('test-class-1')).toBe false
|
||||
expect(changedDecorationNode.classList.contains('decoration')).toBe true
|
||||
expect(changedDecorationNode.children.length).toBe 1
|
||||
decorationItem = changedDecorationNode.children.item(0)
|
||||
expect(decorationItem).toBe decorationItem2
|
||||
|
||||
# Remove the item, add a class.
|
||||
changedCustomDecorations =
|
||||
'decoration-id-1':
|
||||
top: 10
|
||||
height: 20
|
||||
class: 'test-class-2'
|
||||
gutterComponent.updateSync(buildTestState(changedCustomDecorations))
|
||||
changedDecorationNode = gutterComponent.getDomNode().children.item(0).children.item(0)
|
||||
expect(changedDecorationNode).toBe initialDecorationNode
|
||||
expect(changedDecorationNode.style.top).toBe '10px'
|
||||
expect(changedDecorationNode.style.height).toBe '20px'
|
||||
expect(changedDecorationNode.classList.contains('test-class-2')).toBe true
|
||||
expect(changedDecorationNode.classList.contains('decoration')).toBe true
|
||||
expect(changedDecorationNode.children.length).toBe 0
|
||||
|
||||
it "removes any decorations that existed previously but aren't in the latest update", ->
|
||||
customDecorations =
|
||||
'decoration-id-1':
|
||||
top: 0
|
||||
height: 10
|
||||
class: 'test-class-1'
|
||||
gutterComponent.updateSync(buildTestState(customDecorations))
|
||||
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
|
||||
expect(decorationsWrapperNode.children.length).toBe 1
|
||||
|
||||
emptyCustomDecorations = {}
|
||||
gutterComponent.updateSync(buildTestState(emptyCustomDecorations))
|
||||
expect(decorationsWrapperNode.children.length).toBe 0
|
||||
@@ -1,21 +1,21 @@
|
||||
DecorationManager = require '../src/decoration-manager'
|
||||
TextEditor = require '../src/text-editor'
|
||||
|
||||
describe "DecorationManager", ->
|
||||
[decorationManager, buffer, displayLayer, markerLayer1, markerLayer2] = []
|
||||
[decorationManager, buffer, editor, markerLayer1, markerLayer2] = []
|
||||
|
||||
beforeEach ->
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
displayLayer = buffer.addDisplayLayer()
|
||||
markerLayer1 = displayLayer.addMarkerLayer()
|
||||
markerLayer2 = displayLayer.addMarkerLayer()
|
||||
decorationManager = new DecorationManager(displayLayer)
|
||||
editor = new TextEditor({buffer})
|
||||
markerLayer1 = editor.addMarkerLayer()
|
||||
markerLayer2 = editor.addMarkerLayer()
|
||||
decorationManager = new DecorationManager(editor)
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-javascript')
|
||||
|
||||
afterEach ->
|
||||
decorationManager.destroy()
|
||||
buffer.release()
|
||||
buffer.destroy()
|
||||
|
||||
describe "decorations", ->
|
||||
[layer1Marker, layer2Marker, layer1MarkerDecoration, layer2MarkerDecoration, decorationProperties] = []
|
||||
@@ -29,7 +29,6 @@ describe "DecorationManager", ->
|
||||
it "can add decorations associated with markers and remove them", ->
|
||||
expect(layer1MarkerDecoration).toBeDefined()
|
||||
expect(layer1MarkerDecoration.getProperties()).toBe decorationProperties
|
||||
expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).toBe layer1MarkerDecoration
|
||||
expect(decorationManager.decorationsForScreenRowRange(2, 3)).toEqual {
|
||||
"#{layer1Marker.id}": [layer1MarkerDecoration],
|
||||
"#{layer2Marker.id}": [layer2MarkerDecoration]
|
||||
@@ -37,15 +36,12 @@ describe "DecorationManager", ->
|
||||
|
||||
layer1MarkerDecoration.destroy()
|
||||
expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer1Marker.id]).not.toBeDefined()
|
||||
expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined()
|
||||
layer2MarkerDecoration.destroy()
|
||||
expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer2Marker.id]).not.toBeDefined()
|
||||
expect(decorationManager.decorationForId(layer2MarkerDecoration.id)).not.toBeDefined()
|
||||
|
||||
it "will not fail if the decoration is removed twice", ->
|
||||
layer1MarkerDecoration.destroy()
|
||||
layer1MarkerDecoration.destroy()
|
||||
expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined()
|
||||
|
||||
it "does not allow destroyed markers to be decorated", ->
|
||||
layer1Marker.destroy()
|
||||
@@ -55,7 +51,7 @@ describe "DecorationManager", ->
|
||||
expect(decorationManager.getOverlayDecorations()).toEqual []
|
||||
|
||||
it "does not allow destroyed marker layers to be decorated", ->
|
||||
layer = displayLayer.addMarkerLayer()
|
||||
layer = editor.addMarkerLayer()
|
||||
layer.destroy()
|
||||
expect(->
|
||||
decorationManager.decorateMarkerLayer(layer, {type: 'highlight'})
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('Dock', () => {
|
||||
})
|
||||
|
||||
describe('when a dock is hidden', () => {
|
||||
it('transfers focus back to the active center pane', () => {
|
||||
it('transfers focus back to the active center pane if the dock had focus', () => {
|
||||
jasmine.attachToDOM(atom.workspace.getElement())
|
||||
const dock = atom.workspace.getLeftDock()
|
||||
dock.activate()
|
||||
@@ -31,6 +31,19 @@ describe('Dock', () => {
|
||||
|
||||
dock.toggle()
|
||||
expect(document.activeElement).toBe(atom.workspace.getCenter().getActivePane().getElement())
|
||||
|
||||
// Don't change focus if the dock was not focused in the first place
|
||||
const modalElement = document.createElement('div')
|
||||
modalElement.setAttribute('tabindex', -1)
|
||||
atom.workspace.addModalPanel({item: modalElement})
|
||||
modalElement.focus()
|
||||
expect(document.activeElement).toBe(modalElement)
|
||||
|
||||
dock.show()
|
||||
expect(document.activeElement).toBe(modalElement)
|
||||
|
||||
dock.hide()
|
||||
expect(document.activeElement).toBe(modalElement)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -49,6 +62,70 @@ describe('Dock', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('activating the next pane', () => {
|
||||
describe('when the dock has more than one pane', () => {
|
||||
it('activates the next pane', () => {
|
||||
const dock = atom.workspace.getLeftDock()
|
||||
const pane1 = dock.getPanes()[0]
|
||||
const pane2 = pane1.splitRight()
|
||||
const pane3 = pane2.splitRight()
|
||||
pane2.activate()
|
||||
expect(pane1.isActive()).toBe(false)
|
||||
expect(pane2.isActive()).toBe(true)
|
||||
expect(pane3.isActive()).toBe(false)
|
||||
|
||||
dock.activateNextPane()
|
||||
expect(pane1.isActive()).toBe(false)
|
||||
expect(pane2.isActive()).toBe(false)
|
||||
expect(pane3.isActive()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the dock has only one pane', () => {
|
||||
it('leaves the current pane active', () => {
|
||||
const dock = atom.workspace.getLeftDock()
|
||||
|
||||
expect(dock.getPanes().length).toBe(1)
|
||||
const pane = dock.getPanes()[0]
|
||||
expect(pane.isActive()).toBe(true)
|
||||
dock.activateNextPane()
|
||||
expect(pane.isActive()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('activating the previous pane', () => {
|
||||
describe('when the dock has more than one pane', () => {
|
||||
it('activates the previous pane', () => {
|
||||
const dock = atom.workspace.getLeftDock()
|
||||
const pane1 = dock.getPanes()[0]
|
||||
const pane2 = pane1.splitRight()
|
||||
const pane3 = pane2.splitRight()
|
||||
pane2.activate()
|
||||
expect(pane1.isActive()).toBe(false)
|
||||
expect(pane2.isActive()).toBe(true)
|
||||
expect(pane3.isActive()).toBe(false)
|
||||
|
||||
dock.activatePreviousPane()
|
||||
expect(pane1.isActive()).toBe(true)
|
||||
expect(pane2.isActive()).toBe(false)
|
||||
expect(pane3.isActive()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the dock has only one pane', () => {
|
||||
it('leaves the current pane active', () => {
|
||||
const dock = atom.workspace.getLeftDock()
|
||||
|
||||
expect(dock.getPanes().length).toBe(1)
|
||||
const pane = dock.getPanes()[0]
|
||||
expect(pane.isActive()).toBe(true)
|
||||
dock.activatePreviousPane()
|
||||
expect(pane.isActive()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the dock resize handle is double-clicked', () => {
|
||||
describe('when the dock is open', () => {
|
||||
it('resizes a vertically-oriented dock to the current item\'s preferred width', async () => {
|
||||
@@ -222,21 +299,34 @@ describe('Dock', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('when dragging an item over an empty dock', () => {
|
||||
it('has the preferred size of the item', () => {
|
||||
describe('drag handling', () => {
|
||||
it('expands docks to match the preferred size of the dragged item', () => {
|
||||
jasmine.attachToDOM(atom.workspace.getElement())
|
||||
|
||||
const item = {
|
||||
element: document.createElement('div'),
|
||||
const element = document.createElement('div')
|
||||
element.setAttribute('is', 'tabs-tab')
|
||||
element.item = {
|
||||
element,
|
||||
getDefaultLocation() { return 'left' },
|
||||
getPreferredWidth() { return 144 },
|
||||
serialize: () => ({deserializer: 'DockTestItem'})
|
||||
getPreferredWidth() { return 144 }
|
||||
}
|
||||
const dock = atom.workspace.getLeftDock()
|
||||
const dockElement = dock.getElement()
|
||||
|
||||
dock.setDraggingItem(item)
|
||||
expect(dock.wrapperElement.offsetWidth).toBe(144)
|
||||
const dragEvent = new DragEvent('dragstart')
|
||||
Object.defineProperty(dragEvent, 'target', {value: element})
|
||||
|
||||
atom.workspace.getElement().handleDragStart(dragEvent)
|
||||
expect(atom.workspace.getLeftDock().wrapperElement.offsetWidth).toBe(144)
|
||||
})
|
||||
|
||||
it('does nothing when text nodes are dragged', () => {
|
||||
jasmine.attachToDOM(atom.workspace.getElement())
|
||||
|
||||
const textNode = document.createTextNode('hello')
|
||||
|
||||
const dragEvent = new DragEvent('dragstart')
|
||||
Object.defineProperty(dragEvent, 'target', {value: textNode})
|
||||
|
||||
expect(() => atom.workspace.getElement().handleDragStart(dragEvent)).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
const DOMElementPool = require ('../src/dom-element-pool')
|
||||
|
||||
describe('DOMElementPool', function () {
|
||||
let domElementPool
|
||||
|
||||
beforeEach(() => {
|
||||
domElementPool = new DOMElementPool()
|
||||
spyOn(atom, 'isReleasedVersion').andReturn(true)
|
||||
})
|
||||
|
||||
it('builds DOM nodes, recycling them when they are freed', function () {
|
||||
let elements
|
||||
const [div, span1, span2, span3, span4, span5, textNode] = Array.from(elements = [
|
||||
domElementPool.buildElement('div', 'foo'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildText('Hello world!')
|
||||
])
|
||||
|
||||
expect(div.className).toBe('foo')
|
||||
div.textContent = 'testing'
|
||||
div.style.backgroundColor = 'red'
|
||||
div.dataset.foo = 'bar'
|
||||
|
||||
expect(textNode.textContent).toBe('Hello world!')
|
||||
|
||||
div.appendChild(span1)
|
||||
span1.appendChild(span2)
|
||||
div.appendChild(span3)
|
||||
span3.appendChild(span4)
|
||||
span4.appendChild(textNode)
|
||||
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
domElementPool.freeElementAndDescendants(span5)
|
||||
|
||||
expect(elements.includes(domElementPool.buildElement('div'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildText('another text'))).toBe(true)
|
||||
|
||||
expect(elements.includes(domElementPool.buildElement('div'))).toBe(false)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(false)
|
||||
expect(elements.includes(domElementPool.buildText('unexisting'))).toBe(false)
|
||||
|
||||
expect(div.className).toBe('')
|
||||
expect(div.textContent).toBe('')
|
||||
expect(div.style.backgroundColor).toBe('')
|
||||
expect(div.dataset.foo).toBeUndefined()
|
||||
|
||||
expect(textNode.textContent).toBe('another text')
|
||||
})
|
||||
|
||||
it('forgets free nodes after being cleared', function () {
|
||||
const span = domElementPool.buildElement('span')
|
||||
const div = domElementPool.buildElement('div')
|
||||
domElementPool.freeElementAndDescendants(span)
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
|
||||
domElementPool.clear()
|
||||
|
||||
expect(domElementPool.buildElement('span')).not.toBe(span)
|
||||
expect(domElementPool.buildElement('div')).not.toBe(div)
|
||||
})
|
||||
|
||||
it('does not attempt to free nodes that were not created by the pool', () => {
|
||||
let assertionFailure
|
||||
atom.onDidFailAssertion((error) => assertionFailure = error)
|
||||
|
||||
const foreignDiv = document.createElement('div')
|
||||
const div = domElementPool.buildElement('div')
|
||||
div.appendChild(foreignDiv)
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
const span = domElementPool.buildElement('span')
|
||||
span.appendChild(foreignDiv)
|
||||
domElementPool.freeElementAndDescendants(span)
|
||||
|
||||
expect(assertionFailure).toBeUndefined()
|
||||
})
|
||||
|
||||
it('fails an assertion when freeing the same element twice', function () {
|
||||
let assertionFailure
|
||||
atom.onDidFailAssertion((error) => assertionFailure = error)
|
||||
|
||||
const div = domElementPool.buildElement('div')
|
||||
div.textContent = 'testing'
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
expect(assertionFailure).toBeUndefined()
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
expect(assertionFailure.message).toBe('Assertion failed: The element has already been freed!')
|
||||
expect(assertionFailure.metadata.content).toBe('<div>testing</div>')
|
||||
})
|
||||
|
||||
it('fails an assertion when freeing the same text node twice', function () {
|
||||
let assertionFailure
|
||||
atom.onDidFailAssertion((error) => assertionFailure = error)
|
||||
|
||||
const node = domElementPool.buildText('testing')
|
||||
domElementPool.freeElementAndDescendants(node)
|
||||
expect(assertionFailure).toBeUndefined()
|
||||
domElementPool.freeElementAndDescendants(node)
|
||||
expect(assertionFailure.message).toBe('Assertion failed: The element has already been freed!')
|
||||
expect(assertionFailure.metadata.content).toBe('testing')
|
||||
})
|
||||
|
||||
it('throws an error when trying to free an invalid element', function () {
|
||||
expect(() => domElementPool.freeElementAndDescendants(null)).toThrow()
|
||||
expect(() => domElementPool.freeElementAndDescendants(undefined)).toThrow()
|
||||
})
|
||||
})
|
||||
@@ -1,63 +0,0 @@
|
||||
{Point} = require 'text-buffer'
|
||||
{isPairedCharacter} = require '../src/text-utils'
|
||||
|
||||
module.exports =
|
||||
class FakeLinesYardstick
|
||||
constructor: (@model, @lineTopIndex) ->
|
||||
{@displayLayer} = @model
|
||||
@characterWidthsByScope = {}
|
||||
|
||||
getScopedCharacterWidth: (scopeNames, char) ->
|
||||
@getScopedCharacterWidths(scopeNames)[char]
|
||||
|
||||
getScopedCharacterWidths: (scopeNames) ->
|
||||
scope = @characterWidthsByScope
|
||||
for scopeName in scopeNames
|
||||
scope[scopeName] ?= {}
|
||||
scope = scope[scopeName]
|
||||
scope.characterWidths ?= {}
|
||||
scope.characterWidths
|
||||
|
||||
setScopedCharacterWidth: (scopeNames, character, width) ->
|
||||
@getScopedCharacterWidths(scopeNames)[character] = width
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition) ->
|
||||
screenPosition = Point.fromObject(screenPosition)
|
||||
|
||||
targetRow = screenPosition.row
|
||||
targetColumn = screenPosition.column
|
||||
|
||||
top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow)
|
||||
left = 0
|
||||
column = 0
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
left += characterWidths[char] ? @model.getDefaultCharWidth() unless char is '\0'
|
||||
column += charLength
|
||||
|
||||
{top, left}
|
||||
2
spec/fixtures/babel/invalid.js
vendored
2
spec/fixtures/babel/invalid.js
vendored
@@ -1,3 +1,3 @@
|
||||
'use 6to6';
|
||||
|
||||
module.exports = async function hello() {}
|
||||
module.exports = async function* hello() {}
|
||||
|
||||
4
spec/fixtures/packages/package-with-url-main/index.js
vendored
Normal file
4
spec/fixtures/packages/package-with-url-main/index.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = function initialize() {
|
||||
global.reachedUrlMain = true;
|
||||
return Promise.resolve();
|
||||
};
|
||||
5
spec/fixtures/packages/package-with-url-main/package.json
vendored
Normal file
5
spec/fixtures/packages/package-with-url-main/package.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "package-with-url-main",
|
||||
"version": "1.0.0",
|
||||
"urlMain": "./index.js"
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
Gutter = require '../src/gutter'
|
||||
GutterContainerComponent = require '../src/gutter-container-component'
|
||||
DOMElementPool = require '../src/dom-element-pool'
|
||||
|
||||
describe "GutterContainerComponent", ->
|
||||
[gutterContainerComponent] = []
|
||||
mockGutterContainer = {}
|
||||
|
||||
buildTestState = (gutters) ->
|
||||
styles =
|
||||
scrollHeight: 100
|
||||
scrollTop: 10
|
||||
backgroundColor: 'black'
|
||||
|
||||
mockTestState = {gutters: []}
|
||||
for gutter in gutters
|
||||
if gutter.name is 'line-number'
|
||||
content = {maxLineNumberDigits: 10, lineNumbers: {}}
|
||||
else
|
||||
content = {}
|
||||
mockTestState.gutters.push({gutter, styles, content, visible: gutter.visible})
|
||||
|
||||
mockTestState
|
||||
|
||||
beforeEach ->
|
||||
domElementPool = new DOMElementPool
|
||||
mockEditor = {}
|
||||
mockMouseDown = ->
|
||||
gutterContainerComponent = new GutterContainerComponent({editor: mockEditor, onMouseDown: mockMouseDown, domElementPool, views: atom.views})
|
||||
|
||||
it "creates a DOM node with no child gutter nodes when it is initialized", ->
|
||||
expect(gutterContainerComponent.getDomNode() instanceof HTMLElement).toBe true
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 0
|
||||
|
||||
describe "when updated with state that contains a new line-number gutter", ->
|
||||
it "adds a LineNumberGutterComponent to its children", ->
|
||||
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
|
||||
testState = buildTestState([lineNumberGutter])
|
||||
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 0
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expectedGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedGutterNode.classList.contains('gutter')).toBe true
|
||||
expectedLineNumbersNode = expectedGutterNode.children.item(0)
|
||||
expect(expectedLineNumbersNode.classList.contains('line-numbers')).toBe true
|
||||
|
||||
expect(gutterContainerComponent.getLineNumberGutterComponent().getDomNode()).toBe expectedGutterNode
|
||||
|
||||
describe "when updated with state that contains a new custom gutter", ->
|
||||
it "adds a CustomGutterComponent to its children", ->
|
||||
customGutter = new Gutter(mockGutterContainer, {name: 'custom'})
|
||||
testState = buildTestState([customGutter])
|
||||
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 0
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expectedGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedGutterNode.classList.contains('gutter')).toBe true
|
||||
expectedCustomDecorationsNode = expectedGutterNode.children.item(0)
|
||||
expect(expectedCustomDecorationsNode.classList.contains('custom-decorations')).toBe true
|
||||
|
||||
describe "when updated with state that contains a new gutter that is not visible", ->
|
||||
it "creates the gutter view but hides it, and unhides it when it is later updated to be visible", ->
|
||||
customGutter = new Gutter(mockGutterContainer, {name: 'custom', visible: false})
|
||||
testState = buildTestState([customGutter])
|
||||
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode.style.display).toBe 'none'
|
||||
|
||||
customGutter.show()
|
||||
testState = buildTestState([customGutter])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode.style.display).toBe ''
|
||||
|
||||
describe "when updated with a gutter that already exists", ->
|
||||
it "reuses the existing gutter view, instead of recreating it", ->
|
||||
customGutter = new Gutter(mockGutterContainer, {name: 'custom'})
|
||||
testState = buildTestState([customGutter])
|
||||
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
|
||||
testState = buildTestState([customGutter])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expect(gutterContainerComponent.getDomNode().children.item(0)).toBe expectedCustomGutterNode
|
||||
|
||||
it "removes a gutter from the DOM if it does not appear in the latest state update", ->
|
||||
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
|
||||
testState = buildTestState([lineNumberGutter])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
testState = buildTestState([])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 0
|
||||
|
||||
describe "when updated with multiple gutters", ->
|
||||
it "positions (and repositions) the gutters to match the order they appear in each state update", ->
|
||||
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
|
||||
customGutter1 = new Gutter(mockGutterContainer, {name: 'custom', priority: -100})
|
||||
testState = buildTestState([customGutter1, lineNumberGutter])
|
||||
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 2
|
||||
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode).toBe customGutter1.getElement()
|
||||
expectedLineNumbersNode = gutterContainerComponent.getDomNode().children.item(1)
|
||||
expect(expectedLineNumbersNode).toBe lineNumberGutter.getElement()
|
||||
|
||||
# Add a gutter.
|
||||
customGutter2 = new Gutter(mockGutterContainer, {name: 'custom2', priority: -10})
|
||||
testState = buildTestState([customGutter1, customGutter2, lineNumberGutter])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 3
|
||||
expectedCustomGutterNode1 = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode1).toBe customGutter1.getElement()
|
||||
expectedCustomGutterNode2 = gutterContainerComponent.getDomNode().children.item(1)
|
||||
expect(expectedCustomGutterNode2).toBe customGutter2.getElement()
|
||||
expectedLineNumbersNode = gutterContainerComponent.getDomNode().children.item(2)
|
||||
expect(expectedLineNumbersNode).toBe lineNumberGutter.getElement()
|
||||
|
||||
# Hide one gutter, reposition one gutter, remove one gutter; and add a new gutter.
|
||||
customGutter2.hide()
|
||||
customGutter3 = new Gutter(mockGutterContainer, {name: 'custom3', priority: 100})
|
||||
testState = buildTestState([customGutter2, customGutter1, customGutter3])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 3
|
||||
expectedCustomGutterNode2 = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode2).toBe customGutter2.getElement()
|
||||
expect(expectedCustomGutterNode2.style.display).toBe 'none'
|
||||
expectedCustomGutterNode1 = gutterContainerComponent.getDomNode().children.item(1)
|
||||
expect(expectedCustomGutterNode1).toBe customGutter1.getElement()
|
||||
expectedCustomGutterNode3 = gutterContainerComponent.getDomNode().children.item(2)
|
||||
expect(expectedCustomGutterNode3).toBe customGutter3.getElement()
|
||||
|
||||
it "reorders correctly when prepending multiple gutters at once", ->
|
||||
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
|
||||
testState = buildTestState([lineNumberGutter])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode).toBe lineNumberGutter.getElement()
|
||||
|
||||
# Prepend two gutters at once
|
||||
customGutter1 = new Gutter(mockGutterContainer, {name: 'first', priority: -200})
|
||||
customGutter2 = new Gutter(mockGutterContainer, {name: 'second', priority: -100})
|
||||
testState = buildTestState([customGutter1, customGutter2, lineNumberGutter])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 3
|
||||
expectedCustomGutterNode1 = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode1).toBe customGutter1.getElement()
|
||||
expectedCustomGutterNode2 = gutterContainerComponent.getDomNode().children.item(1)
|
||||
expect(expectedCustomGutterNode2).toBe customGutter2.getElement()
|
||||
@@ -3,7 +3,9 @@ GutterContainer = require '../src/gutter-container'
|
||||
|
||||
describe 'GutterContainer', ->
|
||||
gutterContainer = null
|
||||
fakeTextEditor = {}
|
||||
fakeTextEditor = {
|
||||
scheduleComponentUpdate: ->
|
||||
}
|
||||
|
||||
beforeEach ->
|
||||
gutterContainer = new GutterContainer fakeTextEditor
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
Gutter = require '../src/gutter'
|
||||
|
||||
describe 'Gutter', ->
|
||||
fakeGutterContainer = {}
|
||||
fakeGutterContainer = {
|
||||
scheduleComponentUpdate: ->
|
||||
}
|
||||
name = 'name'
|
||||
|
||||
describe '::hide', ->
|
||||
|
||||
@@ -31,7 +31,6 @@ describe("HistoryManager", () => {
|
||||
})
|
||||
|
||||
historyManager = new HistoryManager({stateStore, project, commands: commandRegistry})
|
||||
historyManager.initialize(window.localStorage)
|
||||
await historyManager.loadState()
|
||||
})
|
||||
|
||||
@@ -76,7 +75,7 @@ describe("HistoryManager", () => {
|
||||
|
||||
it("saves the state", async () => {
|
||||
await historyManager.clearProjects()
|
||||
const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry})
|
||||
const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry})
|
||||
await historyManager2.loadState()
|
||||
expect(historyManager.getProjects().length).toBe(0)
|
||||
})
|
||||
@@ -187,7 +186,7 @@ describe("HistoryManager", () => {
|
||||
it("saves the state", async () => {
|
||||
await historyManager.addProject(["/save/state"])
|
||||
await historyManager.saveState()
|
||||
const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry})
|
||||
const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry})
|
||||
await historyManager2.loadState()
|
||||
expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state'])
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ runAtom = require './helpers/start-atom'
|
||||
|
||||
describe "Smoke Test", ->
|
||||
return unless process.platform is 'darwin' # Fails on win32
|
||||
|
||||
|
||||
atomHome = temp.mkdirSync('atom-home')
|
||||
|
||||
beforeEach ->
|
||||
@@ -14,7 +14,10 @@ describe "Smoke Test", ->
|
||||
season.writeFileSync(path.join(atomHome, 'config.cson'), {
|
||||
'*': {
|
||||
welcome: {showOnStartup: false},
|
||||
core: {telemetryConsent: 'no'}
|
||||
core: {
|
||||
telemetryConsent: 'no',
|
||||
disabledPackages: ['github']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -28,6 +31,7 @@ describe "Smoke Test", ->
|
||||
.then (exists) -> expect(exists).toBe true
|
||||
.waitForPaneItemCount(1, 1000)
|
||||
.click("atom-text-editor")
|
||||
.waitUntil((-> @execute(-> document.activeElement.closest('atom-text-editor'))), 5000)
|
||||
.keys("Hello!")
|
||||
.execute -> atom.workspace.getActiveTextEditor().getText()
|
||||
.then ({value}) -> expect(value).toBe "Hello!"
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
LinesYardstick = require '../src/lines-yardstick'
|
||||
LineTopIndex = require 'line-top-index'
|
||||
{Point} = require 'text-buffer'
|
||||
|
||||
describe "LinesYardstick", ->
|
||||
[editor, mockLineNodesProvider, createdLineNodes, linesYardstick, buildLineNode] = []
|
||||
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-javascript')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('sample.js').then (o) -> editor = o
|
||||
|
||||
runs ->
|
||||
createdLineNodes = []
|
||||
|
||||
buildLineNode = (screenRow) ->
|
||||
startIndex = 0
|
||||
scopes = []
|
||||
screenLine = editor.screenLineForScreenRow(screenRow)
|
||||
lineNode = document.createElement("div")
|
||||
lineNode.style.whiteSpace = "pre"
|
||||
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 =
|
||||
lineNodesById: {}
|
||||
|
||||
lineIdForScreenRow: (screenRow) ->
|
||||
editor.screenLineForScreenRow(screenRow)?.id
|
||||
|
||||
lineNodeForScreenRow: (screenRow) ->
|
||||
if id = @lineIdForScreenRow(screenRow)
|
||||
@lineNodesById[id] ?= buildLineNode(screenRow)
|
||||
|
||||
textNodesForScreenRow: (screenRow) ->
|
||||
lineNode = @lineNodeForScreenRow(screenRow)
|
||||
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT)
|
||||
textNodes = []
|
||||
textNodes.push(textNode) while textNode = iterator.nextNode()
|
||||
textNodes
|
||||
|
||||
editor.setLineHeightInPixels(14)
|
||||
lineTopIndex = new LineTopIndex({defaultLineHeight: editor.getLineHeightInPixels()})
|
||||
linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars)
|
||||
|
||||
afterEach ->
|
||||
lineNode.remove() for lineNode in createdLineNodes
|
||||
atom.themes.removeStylesheet('test')
|
||||
|
||||
describe "::pixelPositionForScreenPosition(screenPosition)", ->
|
||||
it "converts screen positions to pixel positions", ->
|
||||
atom.styles.addStyleSheet """
|
||||
* {
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.syntax--function {
|
||||
font-size: 16px
|
||||
}
|
||||
"""
|
||||
|
||||
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: 38, top: 0})
|
||||
|
||||
switch process.platform
|
||||
when 'darwin'
|
||||
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.875, top: 28})
|
||||
when 'win32'
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 42, top: 14})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 71, top: 14})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, Infinity))).toEqual({left: 280, top: 28})
|
||||
|
||||
it "reuses already computed pixel positions unless it is invalidated", ->
|
||||
atom.styles.addStyleSheet """
|
||||
* {
|
||||
font-size: 16px;
|
||||
font-family: monospace;
|
||||
}
|
||||
"""
|
||||
|
||||
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: 96, top: 70})
|
||||
|
||||
atom.styles.addStyleSheet """
|
||||
* {
|
||||
font-size: 20px;
|
||||
}
|
||||
"""
|
||||
|
||||
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: 96, top: 70})
|
||||
|
||||
linesYardstick.invalidateCache()
|
||||
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 24, top: 14})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 72, top: 28})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 120, top: 70})
|
||||
|
||||
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.
|
||||
atom.styles.addStyleSheet """
|
||||
* {
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
}
|
||||
"""
|
||||
|
||||
text = " \\vec{w}_j^r(\\text{new}) &= \\vec{w}_j^r(\\text{old}) + \\Delta\\vec{w}_j^r, \\\\"
|
||||
buildLineNode = (screenRow) ->
|
||||
lineNode = document.createElement("div")
|
||||
lineNode.style.whiteSpace = "pre"
|
||||
# We couldn't reproduce the problem with a simple string, so we're
|
||||
# attaching the full one that comes from a bug report.
|
||||
lineNode.innerHTML = '<span><span> </span><span> </span><span><span>\\</span>vec</span><span><span>{</span>w<span>}</span></span>_j^r(<span><span>\\</span>text</span><span><span>{</span>new<span>}</span></span>) &= <span><span>\\</span>vec</span><span><span>{</span>w<span>}</span></span>_j^r(<span><span>\\</span>text</span><span><span>{</span>old<span>}</span></span>) + <span><span>\\</span>Delta</span><span><span>\\</span>vec</span><span><span>{</span>w<span>}</span></span>_j^r, <span>\\\\</span></span>'
|
||||
jasmine.attachToDOM(lineNode)
|
||||
createdLineNodes.push(lineNode)
|
||||
lineNode
|
||||
|
||||
editor.setText(text)
|
||||
|
||||
switch process.platform
|
||||
when 'darwin'
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 35)).left).toBe 230.90625
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 36)).left).toBe 237.5
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 37)).left).toBe 244.09375
|
||||
when 'win32'
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 35)).left).toBe 245
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 36)).left).toBe 252
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 37)).left).toBe 259
|
||||
|
||||
it "handles lines containing a mix of left-to-right and right-to-left characters", ->
|
||||
editor.setText('Persian, locally known as Parsi or Farsi (زبان فارسی), the predominant modern descendant of Old Persian.\n')
|
||||
|
||||
atom.styles.addStyleSheet """
|
||||
* {
|
||||
font-size: 14px;
|
||||
font-family: monospace;
|
||||
}
|
||||
"""
|
||||
|
||||
lineTopIndex = new LineTopIndex({defaultLineHeight: editor.getLineHeightInPixels()})
|
||||
linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars)
|
||||
|
||||
switch process.platform
|
||||
when 'darwin'
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 15))).toEqual({left: 126, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 62))).toEqual({left: 521, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 58))).toEqual({left: 487, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, Infinity))).toEqual({left: 873.625, top: 0})
|
||||
when 'win32'
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 15))).toEqual({left: 120, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 62))).toEqual({left: 496, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 58))).toEqual({left: 464, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, Infinity))).toEqual({left: 832, top: 0})
|
||||
|
||||
describe "::screenPositionForPixelPosition(pixelPosition)", ->
|
||||
it "converts pixel positions to screen positions", ->
|
||||
atom.styles.addStyleSheet """
|
||||
* {
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.syntax--function {
|
||||
font-size: 16px
|
||||
}
|
||||
"""
|
||||
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 12.5})).toEqual([0, 2])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 14, left: 18.8})).toEqual([1, 3])
|
||||
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: 70, left: 99.9})).toEqual([5, 14])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 225})).toEqual([5, 30])
|
||||
|
||||
switch process.platform
|
||||
when 'darwin'
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 29])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 33])
|
||||
when 'win32'
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 30])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 34])
|
||||
|
||||
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 = '<span>foo</span><span style="margin-left: 40px">bar</span>'
|
||||
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]
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: Infinity)).toEqual [0, 0]
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: -1, left: Infinity)).toEqual [0, 0]
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: 0, left: Infinity)).toEqual [0, 29]
|
||||
|
||||
it "clips pixel positions below buffer end", ->
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: -Infinity)).toEqual [12, 2]
|
||||
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]
|
||||
@@ -437,6 +437,27 @@ describe('AtomApplication', function () {
|
||||
assert.deepEqual(await getTreeViewRootDirectories(window2), [dirB])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when opening atom:// URLs', function () {
|
||||
it('loads the urlMain file in a new window', async function () {
|
||||
const packagePath = path.join(__dirname, '..', 'fixtures', 'packages', 'package-with-url-main')
|
||||
const packagesDirPath = path.join(process.env.ATOM_HOME, 'packages')
|
||||
fs.mkdirSync(packagesDirPath)
|
||||
fs.symlinkSync(packagePath, path.join(packagesDirPath, 'package-with-url-main'), 'junction')
|
||||
|
||||
const atomApplication = buildAtomApplication()
|
||||
const launchOptions = parseCommandLine([])
|
||||
launchOptions.urlsToOpen = ['atom://package-with-url-main/test']
|
||||
let windows = atomApplication.launch(launchOptions)
|
||||
await windows[0].loadedPromise
|
||||
|
||||
let reached = await evalInWebContents(windows[0].browserWindow.webContents, function (sendBackToMainProcess) {
|
||||
sendBackToMainProcess(global.reachedUrlMain)
|
||||
})
|
||||
assert.equal(reached, true);
|
||||
windows[0].close();
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('before quitting', function () {
|
||||
|
||||
@@ -328,10 +328,15 @@ describe "PackageManager", ->
|
||||
it "requires the main module, loads the config schema and activates keymaps, menus and settings without reactivating them during package activation", ->
|
||||
availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check'
|
||||
availablePackage.isBundled = true
|
||||
metadata = atom.packages.loadPackageMetadata(availablePackage)
|
||||
expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined()
|
||||
expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
|
||||
|
||||
metadata = atom.packages.loadPackageMetadata(availablePackage)
|
||||
atom.packages.packagesCache = {}
|
||||
atom.packages.packagesCache[availablePackage.name] = {
|
||||
main: path.join(availablePackage.path, metadata.main),
|
||||
grammarPaths: []
|
||||
}
|
||||
preloadedPackage = atom.packages.preloadPackage(
|
||||
availablePackage.name,
|
||||
{
|
||||
@@ -345,14 +350,15 @@ describe "PackageManager", ->
|
||||
expect(preloadedPackage.mainModule).toBeTruthy()
|
||||
expect(preloadedPackage.configSchemaRegisteredOnLoad).toBeTruthy()
|
||||
|
||||
atom.packages.loadAvailablePackage(availablePackage)
|
||||
|
||||
spyOn(atom.keymaps, 'add')
|
||||
spyOn(atom.menu, 'add')
|
||||
spyOn(atom.contextMenu, 'add')
|
||||
spyOn(atom.config, 'setSchema')
|
||||
atom.packages.activatePackage(availablePackage.name)
|
||||
|
||||
atom.packages.loadAvailablePackage(availablePackage)
|
||||
expect(preloadedPackage.getMainModulePath()).toBe(path.join(availablePackage.path, metadata.main))
|
||||
|
||||
atom.packages.activatePackage(availablePackage.name)
|
||||
expect(atom.keymaps.add).not.toHaveBeenCalled()
|
||||
expect(atom.menu.add).not.toHaveBeenCalled()
|
||||
expect(atom.contextMenu.add).not.toHaveBeenCalled()
|
||||
@@ -366,10 +372,15 @@ describe "PackageManager", ->
|
||||
it "deactivates disabled keymaps during package activation", ->
|
||||
availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check'
|
||||
availablePackage.isBundled = true
|
||||
metadata = atom.packages.loadPackageMetadata(availablePackage)
|
||||
expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined()
|
||||
expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false)
|
||||
|
||||
metadata = atom.packages.loadPackageMetadata(availablePackage)
|
||||
atom.packages.packagesCache = {}
|
||||
atom.packages.packagesCache[availablePackage.name] = {
|
||||
main: path.join(availablePackage.path, metadata.main),
|
||||
grammarPaths: []
|
||||
}
|
||||
preloadedPackage = atom.packages.preloadPackage(
|
||||
availablePackage.name,
|
||||
{
|
||||
|
||||
@@ -64,6 +64,7 @@ beforeEach ->
|
||||
atom.project.setPaths([specProjectPath])
|
||||
|
||||
window.resetTimeouts()
|
||||
spyOn(Date, 'now').andCallFake -> window.now
|
||||
spyOn(_._, "now").andCallFake -> window.now
|
||||
spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout
|
||||
spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout
|
||||
@@ -179,6 +180,7 @@ jasmine.useRealClock = ->
|
||||
jasmine.unspy(window, 'setTimeout')
|
||||
jasmine.unspy(window, 'clearTimeout')
|
||||
jasmine.unspy(_._, 'now')
|
||||
jasmine.unspy(Date, 'now')
|
||||
|
||||
# The clock is halfway mocked now in a sad and terrible way... only setTimeout
|
||||
# and clearTimeout are included. This method will also include setInterval. We
|
||||
@@ -186,6 +188,8 @@ jasmine.useRealClock = ->
|
||||
jasmine.useMockClock = ->
|
||||
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
|
||||
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
|
||||
spyOn(Date, 'now').andCallFake(-> window.now)
|
||||
|
||||
|
||||
addCustomMatchers = (spec) ->
|
||||
spec.addMatchers
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,320 +0,0 @@
|
||||
TextEditor = require '../src/text-editor'
|
||||
TextEditorElement = require '../src/text-editor-element'
|
||||
{Disposable} = require 'event-kit'
|
||||
|
||||
describe "TextEditorElement", ->
|
||||
jasmineContent = null
|
||||
|
||||
beforeEach ->
|
||||
jasmineContent = document.body.querySelector('#jasmine-content')
|
||||
|
||||
describe "instantiation", ->
|
||||
it "honors the 'mini' attribute", ->
|
||||
jasmineContent.innerHTML = "<atom-text-editor mini>"
|
||||
element = jasmineContent.firstChild
|
||||
expect(element.getModel().isMini()).toBe true
|
||||
|
||||
it "honors the 'placeholder-text' attribute", ->
|
||||
jasmineContent.innerHTML = "<atom-text-editor placeholder-text='testing'>"
|
||||
element = jasmineContent.firstChild
|
||||
expect(element.getModel().getPlaceholderText()).toBe 'testing'
|
||||
|
||||
it "honors the 'gutter-hidden' attribute", ->
|
||||
jasmineContent.innerHTML = "<atom-text-editor gutter-hidden>"
|
||||
element = jasmineContent.firstChild
|
||||
expect(element.getModel().isLineNumberGutterVisible()).toBe false
|
||||
|
||||
it "honors the text content", ->
|
||||
jasmineContent.innerHTML = "<atom-text-editor>testing</atom-text-editor>"
|
||||
element = jasmineContent.firstChild
|
||||
expect(element.getModel().getText()).toBe 'testing'
|
||||
|
||||
describe "when the model is assigned", ->
|
||||
it "adds the 'mini' attribute if .isMini() returns true on the model", ->
|
||||
element = new TextEditorElement
|
||||
model = new TextEditor({mini: true})
|
||||
element.setModel(model)
|
||||
expect(element.hasAttribute('mini')).toBe true
|
||||
|
||||
describe "when the editor is attached to the DOM", ->
|
||||
it "mounts the component and unmounts when removed from the dom", ->
|
||||
element = new TextEditorElement
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
component = element.component
|
||||
expect(component.mounted).toBe true
|
||||
element.remove()
|
||||
expect(component.mounted).toBe false
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.component.mounted).toBe true
|
||||
|
||||
describe "when the editor is detached from the DOM and then reattached", ->
|
||||
it "does not render duplicate line numbers", ->
|
||||
editor = new TextEditor
|
||||
editor.setText('1\n2\n3')
|
||||
element = editor.getElement()
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
initialCount = element.querySelectorAll('.line-number').length
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.querySelectorAll('.line-number').length).toBe initialCount
|
||||
|
||||
it "does not render duplicate decorations in custom gutters", ->
|
||||
editor = new TextEditor
|
||||
editor.setText('1\n2\n3')
|
||||
editor.addGutter({name: 'test-gutter'})
|
||||
marker = editor.markBufferRange([[0, 0], [2, 0]])
|
||||
editor.decorateMarker(marker, {type: 'gutter', gutterName: 'test-gutter'})
|
||||
element = editor.getElement()
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
initialDecorationCount = element.querySelectorAll('.decoration').length
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.querySelectorAll('.decoration').length).toBe initialDecorationCount
|
||||
|
||||
it "can be re-focused using the previous `document.activeElement`", ->
|
||||
editorElement = document.createElement('atom-text-editor')
|
||||
jasmine.attachToDOM(editorElement)
|
||||
editorElement.focus()
|
||||
|
||||
activeElement = document.activeElement
|
||||
|
||||
editorElement.remove()
|
||||
jasmine.attachToDOM(editorElement)
|
||||
activeElement.focus()
|
||||
|
||||
expect(editorElement.hasFocus()).toBe(true)
|
||||
|
||||
describe "focus and blur handling", ->
|
||||
it "proxies focus/blur events to/from the hidden input", ->
|
||||
element = new TextEditorElement
|
||||
jasmineContent.appendChild(element)
|
||||
|
||||
blurCalled = false
|
||||
element.addEventListener 'blur', -> blurCalled = true
|
||||
|
||||
element.focus()
|
||||
expect(blurCalled).toBe false
|
||||
expect(element.hasFocus()).toBe true
|
||||
expect(document.activeElement).toBe element.querySelector('input')
|
||||
|
||||
document.body.focus()
|
||||
expect(blurCalled).toBe true
|
||||
|
||||
it "doesn't trigger a blur event on the editor element when focusing an already focused editor element", ->
|
||||
blurCalled = false
|
||||
element = new TextEditorElement
|
||||
element.addEventListener 'blur', -> blurCalled = true
|
||||
|
||||
jasmineContent.appendChild(element)
|
||||
expect(document.activeElement).toBe(document.body)
|
||||
expect(blurCalled).toBe(false)
|
||||
|
||||
element.focus()
|
||||
expect(document.activeElement).toBe(element.querySelector('input'))
|
||||
expect(blurCalled).toBe(false)
|
||||
|
||||
element.focus()
|
||||
expect(document.activeElement).toBe(element.querySelector('input'))
|
||||
expect(blurCalled).toBe(false)
|
||||
|
||||
describe "when focused while a parent node is being attached to the DOM", ->
|
||||
class ElementThatFocusesChild extends HTMLDivElement
|
||||
attachedCallback: ->
|
||||
@firstChild.focus()
|
||||
|
||||
document.registerElement("element-that-focuses-child",
|
||||
prototype: ElementThatFocusesChild.prototype
|
||||
)
|
||||
|
||||
it "proxies the focus event to the hidden input", ->
|
||||
element = new TextEditorElement
|
||||
parentElement = document.createElement("element-that-focuses-child")
|
||||
parentElement.appendChild(element)
|
||||
jasmineContent.appendChild(parentElement)
|
||||
expect(document.activeElement).toBe element.querySelector('input')
|
||||
|
||||
describe "when the themes finish loading", ->
|
||||
[themeReloadCallback, initialThemeLoadComplete, element] = []
|
||||
|
||||
beforeEach ->
|
||||
themeReloadCallback = null
|
||||
initialThemeLoadComplete = false
|
||||
|
||||
spyOn(atom.themes, 'isInitialLoadComplete').andCallFake ->
|
||||
initialThemeLoadComplete
|
||||
spyOn(atom.themes, 'onDidChangeActiveThemes').andCallFake (fn) ->
|
||||
themeReloadCallback = fn
|
||||
new Disposable
|
||||
|
||||
element = new TextEditorElement()
|
||||
element.style.height = '200px'
|
||||
element.getModel().update({autoHeight: false})
|
||||
element.getModel().setText [0..20].join("\n")
|
||||
|
||||
it "re-renders the scrollbar", ->
|
||||
jasmineContent.appendChild(element)
|
||||
|
||||
atom.styles.addStyleSheet("""
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
""", context: 'atom-text-editor')
|
||||
|
||||
initialThemeLoadComplete = true
|
||||
themeReloadCallback()
|
||||
|
||||
verticalScrollbarNode = element.querySelector(".vertical-scrollbar")
|
||||
scrollbarWidth = verticalScrollbarNode.offsetWidth - verticalScrollbarNode.clientWidth
|
||||
expect(scrollbarWidth).toEqual(8)
|
||||
|
||||
describe "::onDidAttach and ::onDidDetach", ->
|
||||
it "invokes callbacks when the element is attached and detached", ->
|
||||
element = new TextEditorElement
|
||||
|
||||
attachedCallback = jasmine.createSpy("attachedCallback")
|
||||
detachedCallback = jasmine.createSpy("detachedCallback")
|
||||
|
||||
element.onDidAttach(attachedCallback)
|
||||
element.onDidDetach(detachedCallback)
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
expect(attachedCallback).toHaveBeenCalled()
|
||||
expect(detachedCallback).not.toHaveBeenCalled()
|
||||
|
||||
attachedCallback.reset()
|
||||
element.remove()
|
||||
|
||||
expect(attachedCallback).not.toHaveBeenCalled()
|
||||
expect(detachedCallback).toHaveBeenCalled()
|
||||
|
||||
describe "::setUpdatedSynchronously", ->
|
||||
it "controls whether the text editor is updated synchronously", ->
|
||||
spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> fn()
|
||||
|
||||
element = new TextEditorElement
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
element.setUpdatedSynchronously(false)
|
||||
expect(element.isUpdatedSynchronously()).toBe false
|
||||
|
||||
element.getModel().setText("hello")
|
||||
expect(window.requestAnimationFrame).toHaveBeenCalled()
|
||||
|
||||
expect(element.textContent).toContain "hello"
|
||||
|
||||
window.requestAnimationFrame.reset()
|
||||
element.setUpdatedSynchronously(true)
|
||||
element.getModel().setText("goodbye")
|
||||
expect(window.requestAnimationFrame).not.toHaveBeenCalled()
|
||||
expect(element.textContent).toContain "goodbye"
|
||||
|
||||
describe "::getDefaultCharacterWidth", ->
|
||||
it "returns null before the element is attached", ->
|
||||
element = new TextEditorElement
|
||||
expect(element.getDefaultCharacterWidth()).toBeNull()
|
||||
|
||||
it "returns the width of a character in the root scope", ->
|
||||
element = new TextEditorElement
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.getDefaultCharacterWidth()).toBeGreaterThan(0)
|
||||
|
||||
describe "::getMaxScrollTop", ->
|
||||
it "returns the maximum scroll top that can be applied to the element", ->
|
||||
editor = new TextEditor
|
||||
editor.setText('1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16')
|
||||
element = editor.getElement()
|
||||
element.style.lineHeight = "10px"
|
||||
element.style.width = "200px"
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
expect(element.getMaxScrollTop()).toBe(0)
|
||||
|
||||
element.style.height = '100px'
|
||||
editor.update({autoHeight: false})
|
||||
element.component.measureDimensions()
|
||||
expect(element.getMaxScrollTop()).toBe(60)
|
||||
|
||||
element.style.height = '120px'
|
||||
element.component.measureDimensions()
|
||||
expect(element.getMaxScrollTop()).toBe(40)
|
||||
|
||||
element.style.height = '200px'
|
||||
element.component.measureDimensions()
|
||||
expect(element.getMaxScrollTop()).toBe(0)
|
||||
|
||||
describe "on TextEditor::setMini", ->
|
||||
it "changes the element's 'mini' attribute", ->
|
||||
element = new TextEditorElement
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.hasAttribute('mini')).toBe false
|
||||
element.getModel().setMini(true)
|
||||
expect(element.hasAttribute('mini')).toBe true
|
||||
element.getModel().setMini(false)
|
||||
expect(element.hasAttribute('mini')).toBe false
|
||||
|
||||
describe "events", ->
|
||||
element = null
|
||||
|
||||
beforeEach ->
|
||||
element = new TextEditorElement
|
||||
element.getModel().setText("lorem\nipsum\ndolor\nsit\namet")
|
||||
element.setUpdatedSynchronously(true)
|
||||
element.setHeight(20)
|
||||
element.setWidth(20)
|
||||
element.getModel().update({autoHeight: false})
|
||||
|
||||
describe "::onDidChangeScrollTop(callback)", ->
|
||||
it "triggers even when subscribing before attaching the element", ->
|
||||
positions = []
|
||||
subscription1 = element.onDidChangeScrollTop (p) -> positions.push(p)
|
||||
jasmine.attachToDOM(element)
|
||||
subscription2 = element.onDidChangeScrollTop (p) -> positions.push(p)
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollTop(10)
|
||||
expect(positions).toEqual([10, 10])
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollTop(20)
|
||||
expect(positions).toEqual([20, 20])
|
||||
|
||||
subscription1.dispose()
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollTop(30)
|
||||
expect(positions).toEqual([30])
|
||||
|
||||
describe "::onDidChangeScrollLeft(callback)", ->
|
||||
it "triggers even when subscribing before attaching the element", ->
|
||||
positions = []
|
||||
subscription1 = element.onDidChangeScrollLeft (p) -> positions.push(p)
|
||||
jasmine.attachToDOM(element)
|
||||
subscription2 = element.onDidChangeScrollLeft (p) -> positions.push(p)
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollLeft(10)
|
||||
expect(positions).toEqual([10, 10])
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollLeft(20)
|
||||
expect(positions).toEqual([20, 20])
|
||||
|
||||
subscription1.dispose()
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollLeft(30)
|
||||
expect(positions).toEqual([30])
|
||||
428
spec/text-editor-element-spec.js
Normal file
428
spec/text-editor-element-spec.js
Normal file
@@ -0,0 +1,428 @@
|
||||
/* global HTMLDivElement */
|
||||
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers')
|
||||
const TextEditor = require('../src/text-editor')
|
||||
const TextEditorElement = require('../src/text-editor-element')
|
||||
|
||||
describe('TextEditorElement', () => {
|
||||
let jasmineContent
|
||||
|
||||
beforeEach(() => {
|
||||
jasmineContent = document.body.querySelector('#jasmine-content')
|
||||
})
|
||||
|
||||
function buildTextEditorElement (options = {}) {
|
||||
const element = new TextEditorElement()
|
||||
element.setUpdatedSynchronously(false)
|
||||
if (options.attach !== false) jasmine.attachToDOM(element)
|
||||
return element
|
||||
}
|
||||
|
||||
it("honors the 'mini' attribute", () => {
|
||||
jasmineContent.innerHTML = '<atom-text-editor mini>'
|
||||
const element = jasmineContent.firstChild
|
||||
expect(element.getModel().isMini()).toBe(true)
|
||||
|
||||
element.removeAttribute('mini')
|
||||
expect(element.getModel().isMini()).toBe(false)
|
||||
expect(element.getComponent().getGutterContainerWidth()).toBe(0)
|
||||
|
||||
element.setAttribute('mini', '')
|
||||
expect(element.getModel().isMini()).toBe(true)
|
||||
})
|
||||
|
||||
it('sets the editor to mini if the model is accessed prior to attaching the element', () => {
|
||||
const parent = document.createElement('div')
|
||||
parent.innerHTML = '<atom-text-editor mini>'
|
||||
const element = parent.firstChild
|
||||
expect(element.getModel().isMini()).toBe(true)
|
||||
})
|
||||
|
||||
it("honors the 'placeholder-text' attribute", () => {
|
||||
jasmineContent.innerHTML = "<atom-text-editor placeholder-text='testing'>"
|
||||
const element = jasmineContent.firstChild
|
||||
expect(element.getModel().getPlaceholderText()).toBe('testing')
|
||||
|
||||
element.setAttribute('placeholder-text', 'placeholder')
|
||||
expect(element.getModel().getPlaceholderText()).toBe('placeholder')
|
||||
|
||||
element.removeAttribute('placeholder-text')
|
||||
expect(element.getModel().getPlaceholderText()).toBeNull()
|
||||
})
|
||||
|
||||
it("only assigns 'placeholder-text' on the model if the attribute is present", () => {
|
||||
const editor = new TextEditor({placeholderText: 'placeholder'})
|
||||
editor.getElement()
|
||||
expect(editor.getPlaceholderText()).toBe('placeholder')
|
||||
})
|
||||
|
||||
it("honors the 'gutter-hidden' attribute", () => {
|
||||
jasmineContent.innerHTML = '<atom-text-editor gutter-hidden>'
|
||||
const element = jasmineContent.firstChild
|
||||
expect(element.getModel().isLineNumberGutterVisible()).toBe(false)
|
||||
|
||||
element.removeAttribute('gutter-hidden')
|
||||
expect(element.getModel().isLineNumberGutterVisible()).toBe(true)
|
||||
|
||||
element.setAttribute('gutter-hidden', '')
|
||||
expect(element.getModel().isLineNumberGutterVisible()).toBe(false)
|
||||
})
|
||||
|
||||
it('honors the text content', () => {
|
||||
jasmineContent.innerHTML = '<atom-text-editor>testing</atom-text-editor>'
|
||||
const element = jasmineContent.firstChild
|
||||
expect(element.getModel().getText()).toBe('testing')
|
||||
})
|
||||
|
||||
describe('when the model is assigned', () =>
|
||||
it("adds the 'mini' attribute if .isMini() returns true on the model", function (done) {
|
||||
const element = buildTextEditorElement()
|
||||
element.getModel().update({mini: true})
|
||||
atom.views.getNextUpdatePromise().then(() => {
|
||||
expect(element.hasAttribute('mini')).toBe(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the editor is attached to the DOM', () =>
|
||||
it('mounts the component and unmounts when removed from the dom', () => {
|
||||
const element = buildTextEditorElement()
|
||||
|
||||
const { component } = element
|
||||
expect(component.attached).toBe(true)
|
||||
element.remove()
|
||||
expect(component.attached).toBe(false)
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.component.attached).toBe(true)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the editor is detached from the DOM and then reattached', () => {
|
||||
it('does not render duplicate line numbers', () => {
|
||||
const editor = new TextEditor()
|
||||
editor.setText('1\n2\n3')
|
||||
const element = editor.getElement()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
const initialCount = element.querySelectorAll('.line-number').length
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.querySelectorAll('.line-number').length).toBe(initialCount)
|
||||
})
|
||||
|
||||
it('does not render duplicate decorations in custom gutters', () => {
|
||||
const editor = new TextEditor()
|
||||
editor.setText('1\n2\n3')
|
||||
editor.addGutter({name: 'test-gutter'})
|
||||
const marker = editor.markBufferRange([[0, 0], [2, 0]])
|
||||
editor.decorateMarker(marker, {type: 'gutter', gutterName: 'test-gutter'})
|
||||
const element = editor.getElement()
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
const initialDecorationCount = element.querySelectorAll('.decoration').length
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.querySelectorAll('.decoration').length).toBe(initialDecorationCount)
|
||||
})
|
||||
|
||||
it('can be re-focused using the previous `document.activeElement`', () => {
|
||||
const editorElement = buildTextEditorElement()
|
||||
editorElement.focus()
|
||||
|
||||
const { activeElement } = document
|
||||
|
||||
editorElement.remove()
|
||||
jasmine.attachToDOM(editorElement)
|
||||
activeElement.focus()
|
||||
|
||||
expect(editorElement.hasFocus()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('focus and blur handling', () => {
|
||||
it('proxies focus/blur events to/from the hidden input', () => {
|
||||
const element = buildTextEditorElement()
|
||||
jasmineContent.appendChild(element)
|
||||
|
||||
let blurCalled = false
|
||||
element.addEventListener('blur', () => {
|
||||
blurCalled = true
|
||||
})
|
||||
|
||||
element.focus()
|
||||
expect(blurCalled).toBe(false)
|
||||
expect(element.hasFocus()).toBe(true)
|
||||
expect(document.activeElement).toBe(element.querySelector('input'))
|
||||
|
||||
document.body.focus()
|
||||
expect(blurCalled).toBe(true)
|
||||
})
|
||||
|
||||
it("doesn't trigger a blur event on the editor element when focusing an already focused editor element", () => {
|
||||
let blurCalled = false
|
||||
const element = buildTextEditorElement()
|
||||
element.addEventListener('blur', () => { blurCalled = true })
|
||||
|
||||
jasmineContent.appendChild(element)
|
||||
expect(document.activeElement).toBe(document.body)
|
||||
expect(blurCalled).toBe(false)
|
||||
|
||||
element.focus()
|
||||
expect(document.activeElement).toBe(element.querySelector('input'))
|
||||
expect(blurCalled).toBe(false)
|
||||
|
||||
element.focus()
|
||||
expect(document.activeElement).toBe(element.querySelector('input'))
|
||||
expect(blurCalled).toBe(false)
|
||||
})
|
||||
|
||||
describe('when focused while a parent node is being attached to the DOM', () => {
|
||||
class ElementThatFocusesChild extends HTMLDivElement {
|
||||
attachedCallback () {
|
||||
this.firstChild.focus()
|
||||
}
|
||||
}
|
||||
|
||||
document.registerElement('element-that-focuses-child',
|
||||
{prototype: ElementThatFocusesChild.prototype}
|
||||
)
|
||||
|
||||
it('proxies the focus event to the hidden input', () => {
|
||||
const element = buildTextEditorElement()
|
||||
const parentElement = document.createElement('element-that-focuses-child')
|
||||
parentElement.appendChild(element)
|
||||
jasmineContent.appendChild(parentElement)
|
||||
expect(document.activeElement).toBe(element.querySelector('input'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidAttach and ::onDidDetach', () =>
|
||||
it('invokes callbacks when the element is attached and detached', () => {
|
||||
const element = buildTextEditorElement({attach: false})
|
||||
|
||||
const attachedCallback = jasmine.createSpy('attachedCallback')
|
||||
const detachedCallback = jasmine.createSpy('detachedCallback')
|
||||
|
||||
element.onDidAttach(attachedCallback)
|
||||
element.onDidDetach(detachedCallback)
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
expect(attachedCallback).toHaveBeenCalled()
|
||||
expect(detachedCallback).not.toHaveBeenCalled()
|
||||
|
||||
attachedCallback.reset()
|
||||
element.remove()
|
||||
|
||||
expect(attachedCallback).not.toHaveBeenCalled()
|
||||
expect(detachedCallback).toHaveBeenCalled()
|
||||
})
|
||||
)
|
||||
|
||||
describe('::setUpdatedSynchronously', () =>
|
||||
it('controls whether the text editor is updated synchronously', () => {
|
||||
spyOn(window, 'requestAnimationFrame').andCallFake(fn => fn())
|
||||
|
||||
const element = buildTextEditorElement()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
expect(element.isUpdatedSynchronously()).toBe(false)
|
||||
|
||||
element.getModel().setText('hello')
|
||||
expect(window.requestAnimationFrame).toHaveBeenCalled()
|
||||
|
||||
expect(element.textContent).toContain('hello')
|
||||
|
||||
window.requestAnimationFrame.reset()
|
||||
element.setUpdatedSynchronously(true)
|
||||
element.getModel().setText('goodbye')
|
||||
expect(window.requestAnimationFrame).not.toHaveBeenCalled()
|
||||
expect(element.textContent).toContain('goodbye')
|
||||
})
|
||||
)
|
||||
|
||||
describe('::getDefaultCharacterWidth', () => {
|
||||
it('returns 0 before the element is attached', () => {
|
||||
const element = buildTextEditorElement({attach: false})
|
||||
expect(element.getDefaultCharacterWidth()).toBe(0)
|
||||
})
|
||||
|
||||
it('returns the width of a character in the root scope', () => {
|
||||
const element = buildTextEditorElement()
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.getDefaultCharacterWidth()).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('::getMaxScrollTop', () =>
|
||||
it('returns the maximum scroll top that can be applied to the element', async () => {
|
||||
const editor = new TextEditor()
|
||||
editor.setText('1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16')
|
||||
const element = editor.getElement()
|
||||
element.style.lineHeight = '10px'
|
||||
element.style.width = '200px'
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
expect(element.getMaxScrollTop()).toBe(0)
|
||||
await editor.update({autoHeight: false})
|
||||
|
||||
element.style.height = '100px'
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getMaxScrollTop()).toBe(60)
|
||||
|
||||
element.style.height = '120px'
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getMaxScrollTop()).toBe(40)
|
||||
|
||||
element.style.height = '200px'
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getMaxScrollTop()).toBe(0)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::setScrollTop and ::setScrollLeft', () => {
|
||||
it('changes the scroll position', async () => {
|
||||
element = buildTextEditorElement()
|
||||
element.getModel().update({autoHeight: false})
|
||||
element.getModel().setText('lorem\nipsum\ndolor\nsit\namet')
|
||||
element.setHeight(20)
|
||||
await element.getNextUpdatePromise()
|
||||
element.setWidth(20)
|
||||
await element.getNextUpdatePromise()
|
||||
|
||||
element.setScrollTop(22)
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getScrollTop()).toBe(22)
|
||||
|
||||
element.setScrollLeft(32)
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getScrollLeft()).toBe(32)
|
||||
})
|
||||
})
|
||||
|
||||
describe('on TextEditor::setMini', () =>
|
||||
it("changes the element's 'mini' attribute", async () => {
|
||||
const element = buildTextEditorElement()
|
||||
expect(element.hasAttribute('mini')).toBe(false)
|
||||
element.getModel().setMini(true)
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.hasAttribute('mini')).toBe(true)
|
||||
element.getModel().setMini(false)
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.hasAttribute('mini')).toBe(false)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::intersectsVisibleRowRange(start, end)', () => {
|
||||
it('returns true if the given row range intersects the visible row range', async () => {
|
||||
const element = buildTextEditorElement()
|
||||
const editor = element.getModel()
|
||||
editor.update({autoHeight: false})
|
||||
element.getModel().setText('x\n'.repeat(20))
|
||||
element.style.height = '120px'
|
||||
await element.getNextUpdatePromise()
|
||||
element.setScrollTop(80)
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getVisibleRowRange()).toEqual([4, 11])
|
||||
|
||||
expect(element.intersectsVisibleRowRange(0, 4)).toBe(false)
|
||||
expect(element.intersectsVisibleRowRange(0, 5)).toBe(true)
|
||||
expect(element.intersectsVisibleRowRange(5, 8)).toBe(true)
|
||||
expect(element.intersectsVisibleRowRange(11, 12)).toBe(false)
|
||||
expect(element.intersectsVisibleRowRange(12, 13)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('::pixelRectForScreenRange(range)', () => {
|
||||
it('returns a {top/left/width/height} object describing the rectangle between two screen positions, even if they are not on screen', async () => {
|
||||
const element = buildTextEditorElement()
|
||||
const editor = element.getModel()
|
||||
editor.update({autoHeight: false})
|
||||
element.getModel().setText('xxxxxxxxxxxxxxxxxxxxxx\n'.repeat(20))
|
||||
element.style.height = '120px'
|
||||
await element.getNextUpdatePromise()
|
||||
element.setScrollTop(80)
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getVisibleRowRange()).toEqual([4, 11])
|
||||
|
||||
const top = 2 * editor.getLineHeightInPixels()
|
||||
const bottom = 13 * editor.getLineHeightInPixels()
|
||||
const left = Math.round(3 * editor.getDefaultCharWidth())
|
||||
const right = Math.round(11 * editor.getDefaultCharWidth())
|
||||
expect(element.pixelRectForScreenRange([[2, 3], [13, 11]])).toEqual({
|
||||
top,
|
||||
left,
|
||||
height: bottom + editor.getLineHeightInPixels() - top,
|
||||
width: right - left
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('events', () => {
|
||||
let element = null
|
||||
|
||||
beforeEach(async () => {
|
||||
element = buildTextEditorElement()
|
||||
element.getModel().update({autoHeight: false})
|
||||
element.getModel().setText('lorem\nipsum\ndolor\nsit\namet')
|
||||
element.setHeight(20)
|
||||
await element.getNextUpdatePromise()
|
||||
element.setWidth(20)
|
||||
await element.getNextUpdatePromise()
|
||||
})
|
||||
|
||||
describe('::onDidChangeScrollTop(callback)', () =>
|
||||
it('triggers even when subscribing before attaching the element', () => {
|
||||
const positions = []
|
||||
const subscription1 = element.onDidChangeScrollTop(p => positions.push(p))
|
||||
element.onDidChangeScrollTop(p => positions.push(p))
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollTop(10)
|
||||
expect(positions).toEqual([10, 10])
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollTop(20)
|
||||
expect(positions).toEqual([20, 20])
|
||||
|
||||
subscription1.dispose()
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollTop(30)
|
||||
expect(positions).toEqual([30])
|
||||
})
|
||||
)
|
||||
|
||||
describe('::onDidChangeScrollLeft(callback)', () =>
|
||||
it('triggers even when subscribing before attaching the element', () => {
|
||||
const positions = []
|
||||
const subscription1 = element.onDidChangeScrollLeft(p => positions.push(p))
|
||||
element.onDidChangeScrollLeft(p => positions.push(p))
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollLeft(10)
|
||||
expect(positions).toEqual([10, 10])
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollLeft(20)
|
||||
expect(positions).toEqual([20, 20])
|
||||
|
||||
subscription1.dispose()
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollLeft(30)
|
||||
expect(positions).toEqual([30])
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ describe('TextEditorRegistry', function () {
|
||||
packageManager: {deferredActivationHooks: null}
|
||||
})
|
||||
|
||||
editor = new TextEditor()
|
||||
editor = new TextEditor({autoHeight: false})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
|
||||
@@ -99,23 +99,34 @@ describe "TextEditor", ->
|
||||
expect(editor.getAutoWidth()).toBeFalsy()
|
||||
expect(editor.getShowCursorOnSelection()).toBeTruthy()
|
||||
|
||||
editor.update({autoHeight: true, autoWidth: true, showCursorOnSelection: false})
|
||||
element = editor.getElement()
|
||||
element.setHeight(100)
|
||||
element.setWidth(100)
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
editor.update({showCursorOnSelection: false})
|
||||
editor.setSelectedBufferRange([[1, 2], [3, 4]])
|
||||
editor.addSelectionForBufferRange([[5, 6], [7, 8]], reversed: true)
|
||||
editor.firstVisibleScreenRow = 5
|
||||
editor.firstVisibleScreenColumn = 5
|
||||
editor.setScrollTopRow(3)
|
||||
expect(editor.getScrollTopRow()).toBe(3)
|
||||
editor.setScrollLeftColumn(4)
|
||||
expect(editor.getScrollLeftColumn()).toBe(4)
|
||||
editor.foldBufferRow(4)
|
||||
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
|
||||
|
||||
editor2 = editor.copy()
|
||||
element2 = editor2.getElement()
|
||||
element2.setHeight(100)
|
||||
element2.setWidth(100)
|
||||
jasmine.attachToDOM(element2)
|
||||
expect(editor2.id).not.toBe editor.id
|
||||
expect(editor2.getSelectedBufferRanges()).toEqual editor.getSelectedBufferRanges()
|
||||
expect(editor2.getSelections()[1].isReversed()).toBeTruthy()
|
||||
expect(editor2.getFirstVisibleScreenRow()).toBe 5
|
||||
expect(editor2.getFirstVisibleScreenColumn()).toBe 5
|
||||
expect(editor2.getScrollTopRow()).toBe(3)
|
||||
expect(editor2.getScrollLeftColumn()).toBe(4)
|
||||
expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy()
|
||||
expect(editor2.getAutoWidth()).toBeTruthy()
|
||||
expect(editor2.getAutoHeight()).toBeTruthy()
|
||||
expect(editor2.getAutoWidth()).toBe(false)
|
||||
expect(editor2.getAutoHeight()).toBe(false)
|
||||
expect(editor2.getShowCursorOnSelection()).toBeFalsy()
|
||||
|
||||
# editor2 can now diverge from its origin edit session
|
||||
@@ -137,7 +148,7 @@ describe "TextEditor", ->
|
||||
autoHeight: false
|
||||
})
|
||||
|
||||
expect(returnedPromise).toBe(atom.views.getNextUpdatePromise())
|
||||
expect(returnedPromise).toBe(element.component.getNextUpdatePromise())
|
||||
expect(changeSpy.callCount).toBe(1)
|
||||
expect(editor.getTabLength()).toBe(6)
|
||||
expect(editor.getSoftTabs()).toBe(false)
|
||||
@@ -1877,8 +1888,6 @@ describe "TextEditor", ->
|
||||
[[4, 16], [4, 21]]
|
||||
[[4, 25], [4, 29]]
|
||||
]
|
||||
for cursor in editor.getCursors()
|
||||
expect(cursor.isVisible()).toBeTruthy()
|
||||
|
||||
it "skips lines that are too short to create a non-empty selection", ->
|
||||
editor.setSelectedBufferRange([[3, 31], [3, 38]])
|
||||
@@ -2010,8 +2019,6 @@ describe "TextEditor", ->
|
||||
[[2, 16], [2, 21]]
|
||||
[[2, 37], [2, 40]]
|
||||
]
|
||||
for cursor in editor.getCursors()
|
||||
expect(cursor.isVisible()).toBeTruthy()
|
||||
|
||||
it "skips lines that are too short to create a non-empty selection", ->
|
||||
editor.setSelectedBufferRange([[6, 31], [6, 38]])
|
||||
@@ -2181,54 +2188,6 @@ describe "TextEditor", ->
|
||||
editor.setCursorScreenPosition([3, 3])
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
|
||||
describe "cursor visibility while there is a selection", ->
|
||||
describe "when showCursorOnSelection is true", ->
|
||||
it "is visible while there is no selection", ->
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
expect(editor.getShowCursorOnSelection()).toBeTruthy()
|
||||
expect(editor.getCursors().length).toBe 1
|
||||
expect(editor.getCursors()[0].isVisible()).toBeTruthy()
|
||||
|
||||
it "is visible while there is a selection", ->
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
editor.setSelectedBufferRange([[1, 2], [1, 5]])
|
||||
expect(selection.isEmpty()).toBeFalsy()
|
||||
expect(editor.getCursors().length).toBe 1
|
||||
expect(editor.getCursors()[0].isVisible()).toBeTruthy()
|
||||
|
||||
it "is visible while there are multiple selections", ->
|
||||
expect(editor.getSelections().length).toBe 1
|
||||
editor.setSelectedBufferRanges([[[1, 2], [1, 5]], [[2, 2], [2, 5]]])
|
||||
expect(editor.getSelections().length).toBe 2
|
||||
expect(editor.getCursors().length).toBe 2
|
||||
expect(editor.getCursors()[0].isVisible()).toBeTruthy()
|
||||
expect(editor.getCursors()[1].isVisible()).toBeTruthy()
|
||||
|
||||
describe "when showCursorOnSelection is false", ->
|
||||
it "is visible while there is no selection", ->
|
||||
editor.update({showCursorOnSelection: false})
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
expect(editor.getShowCursorOnSelection()).toBeFalsy()
|
||||
expect(editor.getCursors().length).toBe 1
|
||||
expect(editor.getCursors()[0].isVisible()).toBeTruthy()
|
||||
|
||||
it "is not visible while there is a selection", ->
|
||||
editor.update({showCursorOnSelection: false})
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
editor.setSelectedBufferRange([[1, 2], [1, 5]])
|
||||
expect(selection.isEmpty()).toBeFalsy()
|
||||
expect(editor.getCursors().length).toBe 1
|
||||
expect(editor.getCursors()[0].isVisible()).toBeFalsy()
|
||||
|
||||
it "is not visible while there are multiple selections", ->
|
||||
editor.update({showCursorOnSelection: false})
|
||||
expect(editor.getSelections().length).toBe 1
|
||||
editor.setSelectedBufferRanges([[[1, 2], [1, 5]], [[2, 2], [2, 5]]])
|
||||
expect(editor.getSelections().length).toBe 2
|
||||
expect(editor.getCursors().length).toBe 2
|
||||
expect(editor.getCursors()[0].isVisible()).toBeFalsy()
|
||||
expect(editor.getCursors()[1].isVisible()).toBeFalsy()
|
||||
|
||||
it "does not share selections between different edit sessions for the same buffer", ->
|
||||
editor2 = null
|
||||
waitsForPromise ->
|
||||
@@ -2761,6 +2720,22 @@ describe "TextEditor", ->
|
||||
expect(editor.isFoldedAtBufferRow(6)).toBeFalsy()
|
||||
expect(editor.lineTextForBufferRow(6)).toBe " if (items.length <= 1) return items;"
|
||||
|
||||
describe "when the last line of selection does not end with a valid line ending", ->
|
||||
it "appends line ending to last line and moves the lines spanned by the selection to the preceeding row", ->
|
||||
expect(editor.lineTextForBufferRow(9)).toBe " };"
|
||||
expect(editor.lineTextForBufferRow(10)).toBe ""
|
||||
expect(editor.lineTextForBufferRow(11)).toBe " return sort(Array.apply(this, arguments));"
|
||||
expect(editor.lineTextForBufferRow(12)).toBe "};"
|
||||
|
||||
editor.setSelectedBufferRange([[10, 0], [12, 2]])
|
||||
editor.moveLineUp()
|
||||
|
||||
expect(editor.getSelectedBufferRange()).toEqual [[9, 0], [11, 2]]
|
||||
expect(editor.lineTextForBufferRow(9)).toBe ""
|
||||
expect(editor.lineTextForBufferRow(10)).toBe " return sort(Array.apply(this, arguments));"
|
||||
expect(editor.lineTextForBufferRow(11)).toBe "};"
|
||||
expect(editor.lineTextForBufferRow(12)).toBe " };"
|
||||
|
||||
describe "when there are multiple selections", ->
|
||||
describe "when all the selections span different lines", ->
|
||||
describe "when there is no folds", ->
|
||||
@@ -3279,7 +3254,6 @@ describe "TextEditor", ->
|
||||
expect(line).toBe " var ort = function(items) {"
|
||||
expect(editor.getCursorScreenPosition()).toEqual {row: 1, column: 6}
|
||||
expect(changeScreenRangeHandler).toHaveBeenCalled()
|
||||
expect(editor.getLastCursor().isVisible()).toBeTruthy()
|
||||
|
||||
describe "when the cursor is at the beginning of a line", ->
|
||||
it "joins it with the line above", ->
|
||||
@@ -4847,10 +4821,7 @@ describe "TextEditor", ->
|
||||
expect(buffer.getLineCount()).toBe(count - 1)
|
||||
|
||||
describe "when the line being deleted preceeds a fold, and the command is undone", ->
|
||||
# TODO: This seemed to have only been passing due to an accident in the text
|
||||
# buffer implementation. Once we moved selections to a different layer it
|
||||
# broke. We need to revisit our representation of folds and then reenable it.
|
||||
xit "restores the line and preserves the fold", ->
|
||||
it "restores the line and preserves the fold", ->
|
||||
editor.setCursorBufferPosition([4])
|
||||
editor.foldCurrentRow()
|
||||
expect(editor.isFoldedAtScreenRow(4)).toBeTruthy()
|
||||
@@ -5460,8 +5431,8 @@ describe "TextEditor", ->
|
||||
|
||||
tokens = editor.tokensForScreenRow(0)
|
||||
expect(tokens).toEqual [
|
||||
{text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']},
|
||||
{text: ' http://github.com', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']}
|
||||
{text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']},
|
||||
{text: ' http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}
|
||||
]
|
||||
|
||||
waitsForPromise ->
|
||||
@@ -5470,9 +5441,9 @@ describe "TextEditor", ->
|
||||
runs ->
|
||||
tokens = editor.tokensForScreenRow(0)
|
||||
expect(tokens).toEqual [
|
||||
{text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']},
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']}
|
||||
{text: 'http://github.com', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--markup.syntax--underline.syntax--link.syntax--http.syntax--hyperlink']}
|
||||
{text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']},
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}
|
||||
{text: 'http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--markup syntax--underline syntax--link syntax--http syntax--hyperlink']}
|
||||
]
|
||||
|
||||
describe "when the grammar is updated", ->
|
||||
@@ -5485,8 +5456,8 @@ describe "TextEditor", ->
|
||||
|
||||
tokens = editor.tokensForScreenRow(0)
|
||||
expect(tokens).toEqual [
|
||||
{text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']},
|
||||
{text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']}
|
||||
{text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']},
|
||||
{text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}
|
||||
]
|
||||
|
||||
waitsForPromise ->
|
||||
@@ -5495,8 +5466,8 @@ describe "TextEditor", ->
|
||||
runs ->
|
||||
tokens = editor.tokensForScreenRow(0)
|
||||
expect(tokens).toEqual [
|
||||
{text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']},
|
||||
{text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']}
|
||||
{text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']},
|
||||
{text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}
|
||||
]
|
||||
|
||||
waitsForPromise ->
|
||||
@@ -5505,14 +5476,14 @@ describe "TextEditor", ->
|
||||
runs ->
|
||||
tokens = editor.tokensForScreenRow(0)
|
||||
expect(tokens).toEqual [
|
||||
{text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']},
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']},
|
||||
{text: 'SELECT', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--keyword.syntax--other.syntax--DML.syntax--sql']},
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']},
|
||||
{text: '*', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--keyword.syntax--operator.syntax--star.syntax--sql']},
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']},
|
||||
{text: 'FROM', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--keyword.syntax--other.syntax--DML.syntax--sql']},
|
||||
{text: ' OCTOCATS', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']}
|
||||
{text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']},
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']},
|
||||
{text: 'SELECT', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']},
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']},
|
||||
{text: '*', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--operator syntax--star syntax--sql']},
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']},
|
||||
{text: 'FROM', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']},
|
||||
{text: ' OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}
|
||||
]
|
||||
|
||||
describe ".normalizeTabsInBufferRange()", ->
|
||||
@@ -5533,7 +5504,11 @@ describe "TextEditor", ->
|
||||
|
||||
describe ".pageUp/Down()", ->
|
||||
it "moves the cursor down one page length", ->
|
||||
editor.setRowsPerPage(5)
|
||||
editor.update(autoHeight: false)
|
||||
element = editor.getElement()
|
||||
jasmine.attachToDOM(element)
|
||||
element.style.height = element.component.getLineHeight() * 5 + 'px'
|
||||
element.measureDimensions()
|
||||
|
||||
expect(editor.getCursorBufferPosition().row).toBe 0
|
||||
|
||||
@@ -5551,7 +5526,11 @@ describe "TextEditor", ->
|
||||
|
||||
describe ".selectPageUp/Down()", ->
|
||||
it "selects one screen height of text up or down", ->
|
||||
editor.setRowsPerPage(5)
|
||||
editor.update(autoHeight: false)
|
||||
element = editor.getElement()
|
||||
jasmine.attachToDOM(element)
|
||||
element.style.height = element.component.getLineHeight() * 5 + 'px'
|
||||
element.measureDimensions()
|
||||
|
||||
expect(editor.getCursorBufferPosition().row).toBe 0
|
||||
|
||||
@@ -5574,72 +5553,6 @@ describe "TextEditor", ->
|
||||
editor.selectPageUp()
|
||||
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [12, 2]]]
|
||||
|
||||
describe "::setFirstVisibleScreenRow() and ::getFirstVisibleScreenRow()", ->
|
||||
beforeEach ->
|
||||
line = Array(9).join('0123456789')
|
||||
editor.setText([1..100].map(-> line).join('\n'))
|
||||
expect(editor.getLineCount()).toBe 100
|
||||
expect(editor.lineTextForBufferRow(0).length).toBe 80
|
||||
|
||||
describe "when the editor doesn't have a height and lineHeightInPixels", ->
|
||||
it "does not affect the editor's visible row range", ->
|
||||
expect(editor.getVisibleRowRange()).toBeNull()
|
||||
|
||||
editor.setFirstVisibleScreenRow(1)
|
||||
expect(editor.getFirstVisibleScreenRow()).toEqual 1
|
||||
|
||||
editor.setFirstVisibleScreenRow(3)
|
||||
expect(editor.getFirstVisibleScreenRow()).toEqual 3
|
||||
|
||||
expect(editor.getVisibleRowRange()).toBeNull()
|
||||
expect(editor.getLastVisibleScreenRow()).toBeNull()
|
||||
|
||||
describe "when the editor has a height and lineHeightInPixels", ->
|
||||
beforeEach ->
|
||||
editor.update({scrollPastEnd: true})
|
||||
editor.setHeight(100, true)
|
||||
editor.setLineHeightInPixels(10)
|
||||
|
||||
it "updates the editor's visible row range", ->
|
||||
editor.setFirstVisibleScreenRow(2)
|
||||
expect(editor.getFirstVisibleScreenRow()).toEqual 2
|
||||
expect(editor.getLastVisibleScreenRow()).toBe 12
|
||||
expect(editor.getVisibleRowRange()).toEqual [2, 12]
|
||||
|
||||
it "notifies ::onDidChangeFirstVisibleScreenRow observers", ->
|
||||
changeCount = 0
|
||||
editor.onDidChangeFirstVisibleScreenRow -> changeCount++
|
||||
|
||||
editor.setFirstVisibleScreenRow(2)
|
||||
expect(changeCount).toBe 1
|
||||
|
||||
editor.setFirstVisibleScreenRow(2)
|
||||
expect(changeCount).toBe 1
|
||||
|
||||
editor.setFirstVisibleScreenRow(3)
|
||||
expect(changeCount).toBe 2
|
||||
|
||||
it "ensures that the top row is less than the buffer's line count", ->
|
||||
editor.setFirstVisibleScreenRow(102)
|
||||
expect(editor.getFirstVisibleScreenRow()).toEqual 99
|
||||
expect(editor.getVisibleRowRange()).toEqual [99, 99]
|
||||
|
||||
it "ensures that the left column is less than the length of the longest screen line", ->
|
||||
editor.setFirstVisibleScreenRow(10)
|
||||
expect(editor.getFirstVisibleScreenRow()).toEqual 10
|
||||
|
||||
editor.setText("\n\n\n")
|
||||
|
||||
editor.setFirstVisibleScreenRow(10)
|
||||
expect(editor.getFirstVisibleScreenRow()).toEqual 3
|
||||
|
||||
describe "when the 'editor.scrollPastEnd' option is set to false", ->
|
||||
it "ensures that the bottom row is less than the buffer's line count", ->
|
||||
editor.update({scrollPastEnd: false})
|
||||
editor.setFirstVisibleScreenRow(95)
|
||||
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")
|
||||
@@ -5661,6 +5574,12 @@ describe "TextEditor", ->
|
||||
editor.update({scrollPastEnd: false})
|
||||
expect(editor.getScrollPastEnd()).toBe(false)
|
||||
|
||||
it "always returns false when autoHeight is on", ->
|
||||
editor.update({autoHeight: true, scrollPastEnd: true})
|
||||
expect(editor.getScrollPastEnd()).toBe(false)
|
||||
editor.update({autoHeight: false})
|
||||
expect(editor.getScrollPastEnd()).toBe(true)
|
||||
|
||||
describe "auto height", ->
|
||||
it "returns true by default but can be customized", ->
|
||||
editor = new TextEditor
|
||||
@@ -5947,20 +5866,20 @@ describe "TextEditor", ->
|
||||
|
||||
editor.update({showIndentGuide: false})
|
||||
expect(editor.tokensForScreenRow(0)).toEqual [
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'leading-whitespace']},
|
||||
{text: 'foo', scopes: ['syntax--source.syntax--js']}
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']},
|
||||
{text: 'foo', scopes: ['syntax--source syntax--js']}
|
||||
]
|
||||
|
||||
editor.update({showIndentGuide: true})
|
||||
expect(editor.tokensForScreenRow(0)).toEqual [
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'leading-whitespace indent-guide']},
|
||||
{text: 'foo', scopes: ['syntax--source.syntax--js']}
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace indent-guide']},
|
||||
{text: 'foo', scopes: ['syntax--source syntax--js']}
|
||||
]
|
||||
|
||||
editor.setMini(true)
|
||||
expect(editor.tokensForScreenRow(0)).toEqual [
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'leading-whitespace']},
|
||||
{text: 'foo', scopes: ['syntax--source.syntax--js']}
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']},
|
||||
{text: 'foo', scopes: ['syntax--source syntax--js']}
|
||||
]
|
||||
|
||||
describe "when the editor is constructed with the grammar option set", ->
|
||||
|
||||
@@ -17,16 +17,6 @@ describe('TokenizedBufferIterator', () => {
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
grammar: {
|
||||
scopeForId (id) {
|
||||
return {
|
||||
'-1': 'foo', '-2': 'foo',
|
||||
'-3': 'bar', '-4': 'bar',
|
||||
'-5': 'baz', '-6': 'baz'
|
||||
}[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,57 +24,57 @@ describe('TokenizedBufferIterator', () => {
|
||||
|
||||
expect(iterator.seek(Point(0, 0))).toEqual([])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 0))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([257])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([257])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([259])
|
||||
|
||||
expect(iterator.seek(Point(0, 1))).toEqual(['syntax--baz'])
|
||||
expect(iterator.seek(Point(0, 1))).toEqual([261])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([259])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--bar', 'syntax--baz'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--baz'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([259, 261])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([261])
|
||||
|
||||
expect(iterator.seek(Point(0, 3))).toEqual(['syntax--baz'])
|
||||
expect(iterator.seek(Point(0, 3))).toEqual([261])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([259])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--bar', 'syntax--baz'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--baz'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([259, 261])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([261])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 7))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--baz'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([261])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([259])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 7))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getOpenTags()).toEqual([])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([259])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 0))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual([])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([])
|
||||
|
||||
expect(iterator.seek(Point(0, 5))).toEqual(['syntax--baz'])
|
||||
expect(iterator.seek(Point(0, 5))).toEqual([261])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 7))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--baz'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([261])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([259])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 7))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getOpenTags()).toEqual([])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([259])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -97,12 +87,6 @@ describe('TokenizedBufferIterator', () => {
|
||||
text: '',
|
||||
openScopes: []
|
||||
}
|
||||
},
|
||||
|
||||
grammar: {
|
||||
scopeForId () {
|
||||
return 'foo'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,80 +94,17 @@ describe('TokenizedBufferIterator', () => {
|
||||
|
||||
iterator.seek(Point(0, 0))
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 0))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([257])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 0))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([257])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([257])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--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]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
grammar: {
|
||||
scopeForId (id) {
|
||||
if (id === -2 || id === -1) {
|
||||
return 'foo'
|
||||
} else if (id === -3) {
|
||||
return 'qux'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const iterator = new TokenizedBufferIterator(tokenizedBuffer)
|
||||
|
||||
iterator.seek(Point(0, 0))
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 0))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--foo'])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--qux'])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--qux'])
|
||||
expect(iterator.getOpenTags()).toEqual([])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 0))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--foo'])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(2, 0))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getOpenTags()).toEqual([])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([257])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -590,43 +590,56 @@ describe "TokenizedBuffer", ->
|
||||
iterator.seek(Point(0, 0))
|
||||
|
||||
expectedBoundaries = [
|
||||
{position: Point(0, 0), closeTags: [], openTags: ["syntax--source.syntax--js", "syntax--storage.syntax--type.syntax--var.syntax--js"]}
|
||||
{position: Point(0, 3), closeTags: ["syntax--storage.syntax--type.syntax--var.syntax--js"], openTags: []}
|
||||
{position: Point(0, 8), closeTags: [], openTags: ["syntax--keyword.syntax--operator.syntax--assignment.syntax--js"]}
|
||||
{position: Point(0, 9), closeTags: ["syntax--keyword.syntax--operator.syntax--assignment.syntax--js"], openTags: []}
|
||||
{position: Point(0, 10), closeTags: [], openTags: ["syntax--constant.syntax--numeric.syntax--decimal.syntax--js"]}
|
||||
{position: Point(0, 11), closeTags: ["syntax--constant.syntax--numeric.syntax--decimal.syntax--js"], openTags: []}
|
||||
{position: Point(0, 12), closeTags: [], openTags: ["syntax--comment.syntax--block.syntax--js", "syntax--punctuation.syntax--definition.syntax--comment.syntax--js"]}
|
||||
{position: Point(0, 14), closeTags: ["syntax--punctuation.syntax--definition.syntax--comment.syntax--js"], openTags: []}
|
||||
{position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation.syntax--definition.syntax--comment.syntax--js"]}
|
||||
{position: Point(1, 7), closeTags: ["syntax--punctuation.syntax--definition.syntax--comment.syntax--js", "syntax--comment.syntax--block.syntax--js"], openTags: ["syntax--storage.syntax--type.syntax--var.syntax--js"]}
|
||||
{position: Point(1, 10), closeTags: ["syntax--storage.syntax--type.syntax--var.syntax--js"], openTags: []}
|
||||
{position: Point(1, 15), closeTags: [], openTags: ["syntax--keyword.syntax--operator.syntax--assignment.syntax--js"]}
|
||||
{position: Point(1, 16), closeTags: ["syntax--keyword.syntax--operator.syntax--assignment.syntax--js"], openTags: []}
|
||||
{position: Point(1, 17), closeTags: [], openTags: ["syntax--constant.syntax--numeric.syntax--decimal.syntax--js"]}
|
||||
{position: Point(1, 18), closeTags: ["syntax--constant.syntax--numeric.syntax--decimal.syntax--js"], openTags: []}
|
||||
{position: Point(0, 0), closeTags: [], openTags: ["syntax--source syntax--js", "syntax--storage syntax--type syntax--var syntax--js"]}
|
||||
{position: Point(0, 3), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []}
|
||||
{position: Point(0, 8), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]}
|
||||
{position: Point(0, 9), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []}
|
||||
{position: Point(0, 10), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]}
|
||||
{position: Point(0, 11), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []}
|
||||
{position: Point(0, 12), closeTags: [], openTags: ["syntax--comment syntax--block syntax--js", "syntax--punctuation syntax--definition syntax--comment syntax--js"]}
|
||||
{position: Point(0, 14), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js"], openTags: []}
|
||||
{position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js"]}
|
||||
{position: Point(1, 7), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js", "syntax--comment syntax--block syntax--js"], openTags: ["syntax--storage syntax--type syntax--var syntax--js"]}
|
||||
{position: Point(1, 10), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []}
|
||||
{position: Point(1, 15), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]}
|
||||
{position: Point(1, 16), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []}
|
||||
{position: Point(1, 17), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]}
|
||||
{position: Point(1, 18), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []}
|
||||
]
|
||||
|
||||
loop
|
||||
boundary = {
|
||||
position: iterator.getPosition(),
|
||||
closeTags: iterator.getCloseTags(),
|
||||
openTags: iterator.getOpenTags()
|
||||
closeTags: iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId)),
|
||||
openTags: iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))
|
||||
}
|
||||
|
||||
expect(boundary).toEqual(expectedBoundaries.shift())
|
||||
break unless iterator.moveToSuccessor()
|
||||
|
||||
expect(iterator.seek(Point(0, 1))).toEqual(["syntax--source.syntax--js", "syntax--storage.syntax--type.syntax--var.syntax--js"])
|
||||
expect(iterator.seek(Point(0, 1)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js",
|
||||
"syntax--storage syntax--type syntax--var syntax--js"
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.seek(Point(0, 8))).toEqual(["syntax--source.syntax--js"])
|
||||
expect(iterator.seek(Point(0, 8)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js"
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 8))
|
||||
expect(iterator.seek(Point(1, 0))).toEqual(["syntax--source.syntax--js", "syntax--comment.syntax--block.syntax--js"])
|
||||
expect(iterator.seek(Point(1, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js",
|
||||
"syntax--comment syntax--block syntax--js"
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 0))
|
||||
expect(iterator.seek(Point(1, 18))).toEqual(["syntax--source.syntax--js", "syntax--constant.syntax--numeric.syntax--decimal.syntax--js"])
|
||||
expect(iterator.seek(Point(1, 18)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js",
|
||||
"syntax--constant syntax--numeric syntax--decimal syntax--js"
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 18))
|
||||
|
||||
expect(iterator.seek(Point(2, 0))).toEqual(["syntax--source.syntax--js"])
|
||||
expect(iterator.seek(Point(2, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js"
|
||||
])
|
||||
iterator.moveToSuccessor() # ensure we don't infinitely loop (regression test)
|
||||
|
||||
it "does not report columns beyond the length of the line", ->
|
||||
@@ -671,5 +684,5 @@ describe "TokenizedBuffer", ->
|
||||
iterator.seek(Point(1, 0))
|
||||
|
||||
expect(iterator.getPosition()).toEqual([1, 0])
|
||||
expect(iterator.getCloseTags()).toEqual ['syntax--blue.syntax--broken']
|
||||
expect(iterator.getOpenTags()).toEqual ['syntax--yellow.syntax--broken']
|
||||
expect(iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--blue syntax--broken']
|
||||
expect(iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--yellow syntax--broken']
|
||||
|
||||
@@ -5,7 +5,6 @@ describe "ViewRegistry", ->
|
||||
|
||||
beforeEach ->
|
||||
registry = new ViewRegistry
|
||||
registry.initialize()
|
||||
|
||||
afterEach ->
|
||||
registry.clearDocumentRequests()
|
||||
@@ -127,8 +126,6 @@ describe "ViewRegistry", ->
|
||||
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
|
||||
events = []
|
||||
|
||||
registry.pollDocument -> events.push('poll')
|
||||
registry.pollAfterNextUpdate()
|
||||
registry.updateDocument -> events.push('write 1')
|
||||
registry.readDocument ->
|
||||
registry.updateDocument -> events.push('write from read 1')
|
||||
@@ -147,108 +144,20 @@ describe "ViewRegistry", ->
|
||||
'write 2'
|
||||
'read 1'
|
||||
'read 2'
|
||||
'poll'
|
||||
'write from read 1'
|
||||
'write from read 2'
|
||||
]
|
||||
|
||||
it "pauses DOM polling when reads or writes are pending", ->
|
||||
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
|
||||
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
|
||||
events = []
|
||||
|
||||
registry.pollDocument -> events.push('poll')
|
||||
registry.updateDocument -> events.push('write')
|
||||
registry.readDocument -> events.push('read')
|
||||
|
||||
window.dispatchEvent(new UIEvent('resize'))
|
||||
expect(events).toEqual []
|
||||
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual ['write', 'read', 'poll']
|
||||
|
||||
window.dispatchEvent(new UIEvent('resize'))
|
||||
expect(events).toEqual ['write', 'read', 'poll', 'poll']
|
||||
|
||||
it "polls the document after updating when ::pollAfterNextUpdate() has been called", ->
|
||||
events = []
|
||||
registry.pollDocument -> events.push('poll')
|
||||
registry.updateDocument -> events.push('write')
|
||||
registry.readDocument -> events.push('read')
|
||||
frameRequests.shift()()
|
||||
expect(events).toEqual ['write', 'read']
|
||||
|
||||
events = []
|
||||
registry.pollAfterNextUpdate()
|
||||
registry.updateDocument -> events.push('write')
|
||||
registry.readDocument -> events.push('read')
|
||||
frameRequests.shift()()
|
||||
expect(events).toEqual ['write', 'read', 'poll']
|
||||
|
||||
describe "::pollDocument(fn)", ->
|
||||
[testElement, testStyleSheet, disposable1, disposable2, events] = []
|
||||
|
||||
beforeEach ->
|
||||
testElement = document.createElement('div')
|
||||
testStyleSheet = document.createElement('style')
|
||||
testStyleSheet.textContent = 'body {}'
|
||||
jasmineContent = document.getElementById('jasmine-content')
|
||||
jasmineContent.appendChild(testElement)
|
||||
jasmineContent.appendChild(testStyleSheet)
|
||||
|
||||
events = []
|
||||
disposable1 = registry.pollDocument -> events.push('poll 1')
|
||||
disposable2 = registry.pollDocument -> events.push('poll 2')
|
||||
|
||||
it "calls all registered polling functions after document or stylesheet changes until they are disabled via a returned disposable", ->
|
||||
jasmine.useRealClock()
|
||||
expect(events).toEqual []
|
||||
|
||||
testElement.style.width = '400px'
|
||||
|
||||
waitsFor "events to occur in response to DOM mutation", -> events.length > 0
|
||||
|
||||
runs ->
|
||||
expect(events).toEqual ['poll 1', 'poll 2']
|
||||
events.length = 0
|
||||
|
||||
testStyleSheet.textContent = 'body {color: #333;}'
|
||||
|
||||
waitsFor "events to occur in reponse to style sheet mutation", -> events.length > 0
|
||||
|
||||
runs ->
|
||||
expect(events).toEqual ['poll 1', 'poll 2']
|
||||
events.length = 0
|
||||
|
||||
disposable1.dispose()
|
||||
testElement.style.color = '#fff'
|
||||
|
||||
waitsFor "more events to occur in response to DOM mutation", -> events.length > 0
|
||||
|
||||
runs ->
|
||||
expect(events).toEqual ['poll 2']
|
||||
|
||||
it "calls all registered polling functions when the window resizes", ->
|
||||
expect(events).toEqual []
|
||||
|
||||
window.dispatchEvent(new UIEvent('resize'))
|
||||
|
||||
expect(events).toEqual ['poll 1', 'poll 2']
|
||||
|
||||
describe "::getNextUpdatePromise()", ->
|
||||
it "returns a promise that resolves at the end of the next update cycle", ->
|
||||
updateCalled = false
|
||||
readCalled = false
|
||||
pollCalled = false
|
||||
|
||||
waitsFor 'getNextUpdatePromise to resolve', (done) ->
|
||||
registry.getNextUpdatePromise().then ->
|
||||
expect(updateCalled).toBe true
|
||||
expect(readCalled).toBe true
|
||||
expect(pollCalled).toBe true
|
||||
done()
|
||||
|
||||
registry.updateDocument -> updateCalled = true
|
||||
registry.readDocument -> readCalled = true
|
||||
registry.pollDocument -> pollCalled = true
|
||||
registry.pollAfterNextUpdate()
|
||||
|
||||
@@ -230,28 +230,32 @@ describe('WorkspaceElement', () => {
|
||||
editorElement = editor.getElement()
|
||||
})
|
||||
|
||||
it("updates the font-size based on the 'editor.fontSize' config value", () => {
|
||||
it("updates the font-size based on the 'editor.fontSize' config value", async () => {
|
||||
const initialCharWidth = editor.getDefaultCharWidth()
|
||||
expect(getComputedStyle(editorElement).fontSize).toBe(atom.config.get('editor.fontSize') + 'px')
|
||||
|
||||
atom.config.set('editor.fontSize', atom.config.get('editor.fontSize') + 5)
|
||||
await editorElement.component.getNextUpdatePromise()
|
||||
expect(getComputedStyle(editorElement).fontSize).toBe(atom.config.get('editor.fontSize') + 'px')
|
||||
expect(editor.getDefaultCharWidth()).toBeGreaterThan(initialCharWidth)
|
||||
})
|
||||
|
||||
it("updates the font-family based on the 'editor.fontFamily' config value", () => {
|
||||
it("updates the font-family based on the 'editor.fontFamily' config value", async () => {
|
||||
const initialCharWidth = editor.getDefaultCharWidth()
|
||||
let fontFamily = atom.config.get('editor.fontFamily')
|
||||
expect(getComputedStyle(editorElement).fontFamily).toBe(fontFamily)
|
||||
|
||||
atom.config.set('editor.fontFamily', 'sans-serif')
|
||||
fontFamily = atom.config.get('editor.fontFamily')
|
||||
await editorElement.component.getNextUpdatePromise()
|
||||
expect(getComputedStyle(editorElement).fontFamily).toBe(fontFamily)
|
||||
expect(editor.getDefaultCharWidth()).not.toBe(initialCharWidth)
|
||||
})
|
||||
|
||||
it("updates the line-height based on the 'editor.lineHeight' config value", () => {
|
||||
it("updates the line-height based on the 'editor.lineHeight' config value", async () => {
|
||||
const initialLineHeight = editor.getLineHeightInPixels()
|
||||
atom.config.set('editor.lineHeight', '30px')
|
||||
await editorElement.component.getNextUpdatePromise()
|
||||
expect(getComputedStyle(editorElement).lineHeight).toBe(atom.config.get('editor.lineHeight'))
|
||||
expect(editor.getLineHeightInPixels()).not.toBe(initialLineHeight)
|
||||
})
|
||||
|
||||
@@ -2486,7 +2486,7 @@ i = /test/; #FIXME\
|
||||
waitsForPromise(() => atom.workspace.open())
|
||||
})
|
||||
|
||||
it('closes the active pane item, or the active pane if it is empty, or the current window if there is only the empty root pane', async () => {
|
||||
it('closes the active center pane item, or the active center pane if it is empty, or the current window if there is only the empty root pane in the center', async () => {
|
||||
atom.config.set('core.destroyEmptyPanes', false)
|
||||
|
||||
const pane1 = atom.workspace.getActivePane()
|
||||
@@ -2509,6 +2509,7 @@ i = /test/; #FIXME\
|
||||
expect(pane1.getItems().length).toBe(0)
|
||||
expect(atom.workspace.getCenter().getPanes().length).toBe(1)
|
||||
|
||||
// The dock items should not be closed
|
||||
await atom.workspace.open({
|
||||
getTitle: () => 'Permanent Dock Item',
|
||||
element: document.createElement('div'),
|
||||
@@ -2523,17 +2524,66 @@ i = /test/; #FIXME\
|
||||
|
||||
expect(atom.workspace.getLeftDock().getPaneItems().length).toBe(2)
|
||||
atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow()
|
||||
expect(atom.workspace.getLeftDock().getPaneItems().length).toBe(1)
|
||||
atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow()
|
||||
expect(atom.workspace.getLeftDock().getPaneItems().length).toBe(1)
|
||||
expect(atom.close).not.toHaveBeenCalled()
|
||||
|
||||
atom.workspace.getCenter().activate()
|
||||
atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow()
|
||||
expect(atom.close).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('::activateNextPane', () => {
|
||||
describe('when the active workspace pane is inside a dock', () => {
|
||||
it('activates the next pane in the dock', () => {
|
||||
const dock = atom.workspace.getLeftDock()
|
||||
const dockPane1 = dock.getPanes()[0]
|
||||
const dockPane2 = dockPane1.splitRight()
|
||||
|
||||
dockPane2.focus()
|
||||
expect(atom.workspace.getActivePane()).toBe(dockPane2)
|
||||
atom.workspace.activateNextPane()
|
||||
expect(atom.workspace.getActivePane()).toBe(dockPane1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the active workspace pane is inside the workspace center', () => {
|
||||
it('activates the next pane in the workspace center', () => {
|
||||
const center = atom.workspace.getCenter()
|
||||
const centerPane1 = center.getPanes()[0]
|
||||
const centerPane2 = centerPane1.splitRight()
|
||||
|
||||
centerPane2.focus()
|
||||
expect(atom.workspace.getActivePane()).toBe(centerPane2)
|
||||
atom.workspace.activateNextPane()
|
||||
expect(atom.workspace.getActivePane()).toBe(centerPane1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('::activatePreviousPane', () => {
|
||||
describe('when the active workspace pane is inside a dock', () => {
|
||||
it('activates the previous pane in the dock', () => {
|
||||
const dock = atom.workspace.getLeftDock()
|
||||
const dockPane1 = dock.getPanes()[0]
|
||||
const dockPane2 = dockPane1.splitRight()
|
||||
|
||||
dockPane1.focus()
|
||||
expect(atom.workspace.getActivePane()).toBe(dockPane1)
|
||||
atom.workspace.activatePreviousPane()
|
||||
expect(atom.workspace.getActivePane()).toBe(dockPane2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the active workspace pane is inside the workspace center', () => {
|
||||
it('activates the previous pane in the workspace center', () => {
|
||||
const center = atom.workspace.getCenter()
|
||||
const centerPane1 = center.getPanes()[0]
|
||||
const centerPane2 = centerPane1.splitRight()
|
||||
|
||||
centerPane1.focus()
|
||||
expect(atom.workspace.getActivePane()).toBe(centerPane1)
|
||||
atom.workspace.activatePreviousPane()
|
||||
expect(atom.workspace.getActivePane()).toBe(centerPane2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the core.allowPendingPaneItems option is falsey', () => {
|
||||
it('does not open item with `pending: true` option as pending', () => {
|
||||
let pane = null
|
||||
|
||||
@@ -6,7 +6,7 @@ _ = require 'underscore-plus'
|
||||
{deprecate} = require 'grim'
|
||||
{CompositeDisposable, Disposable, Emitter} = require 'event-kit'
|
||||
fs = require 'fs-plus'
|
||||
{mapSourcePosition} = require 'source-map-support'
|
||||
{mapSourcePosition} = require '@atom/source-map-support'
|
||||
Model = require './model'
|
||||
WindowEventHandler = require './window-event-handler'
|
||||
StateStore = require './state-store'
|
||||
@@ -135,6 +135,7 @@ class AtomEnvironment extends Model
|
||||
@deserializers = new DeserializerManager(this)
|
||||
@deserializeTimings = {}
|
||||
@views = new ViewRegistry(this)
|
||||
TextEditor.setScheduler(@views)
|
||||
@notifications = new NotificationManager
|
||||
@updateProcessEnv ?= updateProcessEnv # For testing
|
||||
|
||||
@@ -208,8 +209,6 @@ class AtomEnvironment extends Model
|
||||
@getStorageFolder().clear()
|
||||
@stateStore.clear()
|
||||
|
||||
@views.initialize()
|
||||
|
||||
ConfigSchema.projectHome = {
|
||||
type: 'string',
|
||||
default: path.join(fs.getHomeDirectory(), 'github'),
|
||||
@@ -251,9 +250,13 @@ class AtomEnvironment extends Model
|
||||
@attachSaveStateListeners()
|
||||
@windowEventHandler.initialize(@window, @document)
|
||||
|
||||
didChangeStyles = @didChangeStyles.bind(this)
|
||||
@disposables.add(@styles.onDidAddStyleElement(didChangeStyles))
|
||||
@disposables.add(@styles.onDidUpdateStyleElement(didChangeStyles))
|
||||
@disposables.add(@styles.onDidRemoveStyleElement(didChangeStyles))
|
||||
|
||||
@observeAutoHideMenuBar()
|
||||
|
||||
@history.initialize(@window.localStorage)
|
||||
@disposables.add @applicationDelegate.onDidChangeHistoryManager(=> @history.loadState())
|
||||
|
||||
preloadPackages: ->
|
||||
@@ -261,7 +264,7 @@ class AtomEnvironment extends Model
|
||||
|
||||
attachSaveStateListeners: ->
|
||||
saveState = _.debounce((=>
|
||||
window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded
|
||||
@window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded
|
||||
), @saveStateDebounceInterval)
|
||||
@document.addEventListener('mousedown', saveState, true)
|
||||
@document.addEventListener('keydown', saveState, true)
|
||||
@@ -677,11 +680,8 @@ class AtomEnvironment extends Model
|
||||
# Call this method when establishing a real application window.
|
||||
startEditorWindow: ->
|
||||
@unloaded = false
|
||||
updateProcessEnvPromise = @updateProcessEnv(@getLoadSettings().env)
|
||||
updateProcessEnvPromise.then =>
|
||||
@shellEnvironmentLoaded = true
|
||||
@emitter.emit('loaded-shell-environment')
|
||||
@packages.triggerActivationHook('core:loaded-shell-environment')
|
||||
|
||||
updateProcessEnvPromise = @updateProcessEnvAndTriggerHooks()
|
||||
|
||||
loadStatePromise = @loadState().then (state) =>
|
||||
@windowDimensions = state?.windowDimensions
|
||||
@@ -800,6 +800,17 @@ class AtomEnvironment extends Model
|
||||
@windowEventHandler?.unsubscribe()
|
||||
@windowEventHandler = null
|
||||
|
||||
didChangeStyles: (styleElement) ->
|
||||
TextEditor.didUpdateStyles()
|
||||
if styleElement.textContent.indexOf('scrollbar') >= 0
|
||||
TextEditor.didUpdateScrollbarStyles()
|
||||
|
||||
updateProcessEnvAndTriggerHooks: ->
|
||||
@updateProcessEnv(@getLoadSettings().env).then =>
|
||||
@shellEnvironmentLoaded = true
|
||||
@emitter.emit('loaded-shell-environment')
|
||||
@packages.triggerActivationHook('core:loaded-shell-environment')
|
||||
|
||||
###
|
||||
Section: Messaging the User
|
||||
###
|
||||
@@ -901,13 +912,17 @@ class AtomEnvironment extends Model
|
||||
@project.addPath(folder) for folder in projectPaths
|
||||
|
||||
attemptRestoreProjectStateForPaths: (state, projectPaths, filesToOpen = []) ->
|
||||
paneItemIsEmptyUnnamedTextEditor = (item) ->
|
||||
return false unless item instanceof TextEditor
|
||||
return false if item.getPath() or item.isModified()
|
||||
center = @workspace.getCenter()
|
||||
windowIsUnused = =>
|
||||
for container in @workspace.getPaneContainers()
|
||||
for item in container.getPaneItems()
|
||||
if item instanceof TextEditor
|
||||
return false if item.getPath() or item.isModified()
|
||||
else
|
||||
return false if container is center
|
||||
true
|
||||
|
||||
windowIsUnused = @workspace.getPaneItems().every(paneItemIsEmptyUnnamedTextEditor)
|
||||
if windowIsUnused
|
||||
if windowIsUnused()
|
||||
@restoreStateIntoThisEnvironment(state)
|
||||
Promise.all (@workspace.open(file) for file in filesToOpen)
|
||||
else
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
var path = require('path')
|
||||
var fs = require('fs-plus')
|
||||
var sourceMapSupport = require('source-map-support')
|
||||
var sourceMapSupport = require('@atom/source-map-support')
|
||||
|
||||
var PackageTranspilationRegistry = require('./package-transpilation-registry')
|
||||
var CSON = null
|
||||
@@ -115,6 +115,18 @@ function writeCachedJavascript (relativeCachePath, code) {
|
||||
var INLINE_SOURCE_MAP_REGEXP = /\/\/[#@]\s*sourceMappingURL=([^'"\n]+)\s*$/mg
|
||||
|
||||
exports.install = function (resourcesPath, nodeRequire) {
|
||||
const snapshotSourceMapConsumer = {
|
||||
originalPositionFor ({line, column}) {
|
||||
const {relativePath, row} = snapshotResult.translateSnapshotRow(line)
|
||||
return {
|
||||
column,
|
||||
line: row,
|
||||
source: path.join(resourcesPath, 'app', 'static', relativePath),
|
||||
name: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceMapSupport.install({
|
||||
handleUncaughtExceptions: false,
|
||||
|
||||
@@ -123,10 +135,7 @@ exports.install = function (resourcesPath, nodeRequire) {
|
||||
// code from our cache directory.
|
||||
retrieveSourceMap: function (filePath) {
|
||||
if (filePath === '<embedded>') {
|
||||
return {
|
||||
map: snapshotResult.sourceMap, // eslint-disable-line no-undef
|
||||
url: path.join(resourcesPath, 'app', 'static', 'index.js')
|
||||
}
|
||||
return {map: snapshotSourceMapConsumer}
|
||||
}
|
||||
|
||||
if (!cacheDirectory || !fs.isFileSync(filePath)) {
|
||||
|
||||
@@ -34,6 +34,16 @@ const configSchema = {
|
||||
|
||||
description: 'List of names of installed packages which are not loaded at startup.'
|
||||
},
|
||||
versionPinnedPackages: {
|
||||
type: 'array',
|
||||
default: [],
|
||||
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
|
||||
description: 'List of names of installed packages which are not automatically updated.'
|
||||
},
|
||||
customFileTypes: {
|
||||
type: 'object',
|
||||
default: {},
|
||||
|
||||
@@ -4,7 +4,7 @@ module.exports = function (extra) {
|
||||
productName: 'Atom',
|
||||
companyName: 'GitHub',
|
||||
submitURL: 'https://crashreporter.atom.io',
|
||||
autoSubmit: false,
|
||||
uploadToServer: false,
|
||||
extra: extra
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,20 +12,14 @@ EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g
|
||||
# of a {DisplayMarker}.
|
||||
module.exports =
|
||||
class Cursor extends Model
|
||||
showCursorOnSelection: null
|
||||
screenPosition: null
|
||||
bufferPosition: null
|
||||
goalColumn: null
|
||||
visible: true
|
||||
|
||||
# Instantiated by a {TextEditor}
|
||||
constructor: ({@editor, @marker, @showCursorOnSelection, id}) ->
|
||||
constructor: ({@editor, @marker, id}) ->
|
||||
@emitter = new Emitter
|
||||
|
||||
@showCursorOnSelection ?= true
|
||||
|
||||
@assignId(id)
|
||||
@updateVisibility()
|
||||
|
||||
destroy: ->
|
||||
@marker.destroy()
|
||||
@@ -57,15 +51,6 @@ class Cursor extends Model
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
|
||||
# Public: Calls your `callback` when the cursor's visibility has changed
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `visibility` {Boolean}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeVisibility: (callback) ->
|
||||
@emitter.on 'did-change-visibility', callback
|
||||
|
||||
###
|
||||
Section: Managing Cursor Position
|
||||
###
|
||||
@@ -568,21 +553,6 @@ class Cursor extends Model
|
||||
Section: Visibility
|
||||
###
|
||||
|
||||
# Public: Sets whether the cursor is visible.
|
||||
setVisible: (visible) ->
|
||||
if @visible isnt visible
|
||||
@visible = visible
|
||||
@emitter.emit 'did-change-visibility', @visible
|
||||
|
||||
# Public: Returns the visibility of the cursor.
|
||||
isVisible: -> @visible
|
||||
|
||||
updateVisibility: ->
|
||||
if @showCursorOnSelection
|
||||
@setVisible(true)
|
||||
else
|
||||
@setVisible(@marker.getBufferRange().isEmpty())
|
||||
|
||||
###
|
||||
Section: Comparing to another cursor
|
||||
###
|
||||
@@ -599,9 +569,6 @@ class Cursor extends Model
|
||||
Section: Utilities
|
||||
###
|
||||
|
||||
# Public: Prevents this cursor from causing scrolling.
|
||||
clearAutoscroll: ->
|
||||
|
||||
# Public: Deselects the current selection.
|
||||
clearSelection: (options) ->
|
||||
@selection?.clear(options)
|
||||
@@ -651,11 +618,6 @@ class Cursor extends Model
|
||||
Section: Private
|
||||
###
|
||||
|
||||
setShowCursorOnSelection: (value) ->
|
||||
if value isnt @showCursorOnSelection
|
||||
@showCursorOnSelection = value
|
||||
@updateVisibility()
|
||||
|
||||
getNonWordCharacters: ->
|
||||
@editor.getNonWordCharacters(@getScopeDescriptor().getScopesArray())
|
||||
|
||||
@@ -668,7 +630,8 @@ class Cursor extends Model
|
||||
{row, column} = @getScreenPosition()
|
||||
new Range(new Point(row, column), new Point(row, column + 1))
|
||||
|
||||
autoscroll: (options) ->
|
||||
autoscroll: (options = {}) ->
|
||||
options.clip = false
|
||||
@editor.scrollToScreenRange(@getScreenRange(), options)
|
||||
|
||||
getBeginningOfNextParagraphBufferPosition: ->
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
module.exports =
|
||||
class CursorsComponent
|
||||
oldState: null
|
||||
|
||||
constructor: ->
|
||||
@cursorNodesById = {}
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add('cursors')
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
newState = state.content
|
||||
@oldState ?= {cursors: {}}
|
||||
|
||||
# update blink class
|
||||
if newState.cursorsVisible isnt @oldState.cursorsVisible
|
||||
if newState.cursorsVisible
|
||||
@domNode.classList.remove 'blink-off'
|
||||
else
|
||||
@domNode.classList.add 'blink-off'
|
||||
@oldState.cursorsVisible = newState.cursorsVisible
|
||||
|
||||
# remove cursors
|
||||
for id of @oldState.cursors
|
||||
unless newState.cursors[id]?
|
||||
@cursorNodesById[id].remove()
|
||||
delete @cursorNodesById[id]
|
||||
delete @oldState.cursors[id]
|
||||
|
||||
# add or update cursors
|
||||
for id, cursorState of newState.cursors
|
||||
unless @oldState.cursors[id]?
|
||||
cursorNode = document.createElement('div')
|
||||
cursorNode.classList.add('cursor')
|
||||
@cursorNodesById[id] = cursorNode
|
||||
@domNode.appendChild(cursorNode)
|
||||
@updateCursorNode(id, cursorState)
|
||||
|
||||
return
|
||||
|
||||
updateCursorNode: (id, newCursorState) ->
|
||||
cursorNode = @cursorNodesById[id]
|
||||
oldCursorState = (@oldState.cursors[id] ?= {})
|
||||
|
||||
if newCursorState.top isnt oldCursorState.top or newCursorState.left isnt oldCursorState.left
|
||||
cursorNode.style['-webkit-transform'] = "translate(#{newCursorState.left}px, #{newCursorState.top}px)"
|
||||
oldCursorState.top = newCursorState.top
|
||||
oldCursorState.left = newCursorState.left
|
||||
|
||||
if newCursorState.height isnt oldCursorState.height
|
||||
cursorNode.style.height = newCursorState.height + 'px'
|
||||
oldCursorState.height = newCursorState.height
|
||||
|
||||
if newCursorState.width isnt oldCursorState.width
|
||||
cursorNode.style.width = newCursorState.width + 'px'
|
||||
oldCursorState.width = newCursorState.width
|
||||
@@ -1,119 +0,0 @@
|
||||
# This class represents a gutter other than the 'line-numbers' gutter.
|
||||
# The contents of this gutter may be specified by Decorations.
|
||||
|
||||
module.exports =
|
||||
class CustomGutterComponent
|
||||
constructor: ({@gutter, @views}) ->
|
||||
@decorationNodesById = {}
|
||||
@decorationItemsById = {}
|
||||
@visible = true
|
||||
|
||||
@domNode = @gutter.getElement()
|
||||
@decorationsNode = @domNode.firstChild
|
||||
# Clear the contents in case the domNode is being reused.
|
||||
@decorationsNode.innerHTML = ''
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
hideNode: ->
|
||||
if @visible
|
||||
@domNode.style.display = 'none'
|
||||
@visible = false
|
||||
|
||||
showNode: ->
|
||||
if not @visible
|
||||
@domNode.style.removeProperty('display')
|
||||
@visible = true
|
||||
|
||||
# `state` is a subset of the TextEditorPresenter state that is specific
|
||||
# to this line number gutter.
|
||||
updateSync: (state) ->
|
||||
@oldDimensionsAndBackgroundState ?= {}
|
||||
setDimensionsAndBackground(@oldDimensionsAndBackgroundState, state.styles, @decorationsNode)
|
||||
|
||||
@oldDecorationPositionState ?= {}
|
||||
decorationState = state.content
|
||||
|
||||
updatedDecorationIds = new Set
|
||||
for decorationId, decorationInfo of decorationState
|
||||
updatedDecorationIds.add(decorationId)
|
||||
existingDecoration = @decorationNodesById[decorationId]
|
||||
if existingDecoration
|
||||
@updateDecorationNode(existingDecoration, decorationId, decorationInfo)
|
||||
else
|
||||
newNode = @buildDecorationNode(decorationId, decorationInfo)
|
||||
@decorationNodesById[decorationId] = newNode
|
||||
@decorationsNode.appendChild(newNode)
|
||||
|
||||
for decorationId, decorationNode of @decorationNodesById
|
||||
if not updatedDecorationIds.has(decorationId)
|
||||
decorationNode.remove()
|
||||
delete @decorationNodesById[decorationId]
|
||||
delete @decorationItemsById[decorationId]
|
||||
delete @oldDecorationPositionState[decorationId]
|
||||
|
||||
###
|
||||
Section: Private Methods
|
||||
###
|
||||
|
||||
# Builds and returns an HTMLElement to represent the specified decoration.
|
||||
buildDecorationNode: (decorationId, decorationInfo) ->
|
||||
@oldDecorationPositionState[decorationId] = {}
|
||||
newNode = document.createElement('div')
|
||||
newNode.style.position = 'absolute'
|
||||
@updateDecorationNode(newNode, decorationId, decorationInfo)
|
||||
newNode
|
||||
|
||||
# Updates the existing HTMLNode with the new decoration info. Attempts to
|
||||
# minimize changes to the DOM.
|
||||
updateDecorationNode: (node, decorationId, newDecorationInfo) ->
|
||||
oldPositionState = @oldDecorationPositionState[decorationId]
|
||||
|
||||
if oldPositionState.top isnt newDecorationInfo.top + 'px'
|
||||
node.style.top = newDecorationInfo.top + 'px'
|
||||
oldPositionState.top = newDecorationInfo.top + 'px'
|
||||
|
||||
if oldPositionState.height isnt newDecorationInfo.height + 'px'
|
||||
node.style.height = newDecorationInfo.height + 'px'
|
||||
oldPositionState.height = newDecorationInfo.height + 'px'
|
||||
|
||||
if newDecorationInfo.class and not node.classList.contains(newDecorationInfo.class)
|
||||
node.className = 'decoration'
|
||||
node.classList.add(newDecorationInfo.class)
|
||||
else if not newDecorationInfo.class
|
||||
node.className = 'decoration'
|
||||
|
||||
@setDecorationItem(newDecorationInfo.item, newDecorationInfo.height, decorationId, node)
|
||||
|
||||
# Sets the decorationItem on the decorationNode.
|
||||
# If `decorationItem` is undefined, the decorationNode's child item will be cleared.
|
||||
setDecorationItem: (newItem, decorationHeight, decorationId, decorationNode) ->
|
||||
if newItem isnt @decorationItemsById[decorationId]
|
||||
while decorationNode.firstChild
|
||||
decorationNode.removeChild(decorationNode.firstChild)
|
||||
delete @decorationItemsById[decorationId]
|
||||
|
||||
if newItem
|
||||
newItemNode = null
|
||||
if newItem instanceof HTMLElement
|
||||
newItemNode = newItem
|
||||
else
|
||||
newItemNode = newItem.element
|
||||
|
||||
newItemNode.style.height = decorationHeight + 'px'
|
||||
decorationNode.appendChild(newItemNode)
|
||||
@decorationItemsById[decorationId] = newItem
|
||||
|
||||
setDimensionsAndBackground = (oldState, newState, domNode) ->
|
||||
if newState.scrollHeight isnt oldState.scrollHeight
|
||||
domNode.style.height = newState.scrollHeight + 'px'
|
||||
oldState.scrollHeight = newState.scrollHeight
|
||||
|
||||
if newState.scrollTop isnt oldState.scrollTop
|
||||
domNode.style['-webkit-transform'] = "translate3d(0px, #{-newState.scrollTop}px, 0px)"
|
||||
oldState.scrollTop = newState.scrollTop
|
||||
|
||||
if newState.backgroundColor isnt oldState.backgroundColor
|
||||
domNode.style.backgroundColor = newState.backgroundColor
|
||||
oldState.backgroundColor = newState.backgroundColor
|
||||
@@ -1,191 +0,0 @@
|
||||
{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) ->
|
||||
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 layerId of @decorationCountsByLayerId
|
||||
layer = @displayLayer.getMarkerLayer(layerId)
|
||||
for marker in layer.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) ->
|
||||
if marker.isDestroyed()
|
||||
error = new Error("Cannot decorate a destroyed marker")
|
||||
error.metadata = {markerLayerIsDestroyed: marker.layer.isDestroyed()}
|
||||
if marker.destroyStackTrace?
|
||||
error.metadata.destroyStackTrace = marker.destroyStackTrace
|
||||
if marker.bufferMarker?.destroyStackTrace?
|
||||
error.metadata.destroyStackTrace = marker.bufferMarker?.destroyStackTrace
|
||||
throw error
|
||||
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) ->
|
||||
throw new Error("Cannot decorate a destroyed marker layer") if markerLayer.isDestroyed()
|
||||
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]
|
||||
|
||||
didDestroyMarkerDecoration: (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]
|
||||
289
src/decoration-manager.js
Normal file
289
src/decoration-manager.js
Normal file
@@ -0,0 +1,289 @@
|
||||
const {Emitter} = require('event-kit')
|
||||
const Decoration = require('./decoration')
|
||||
const LayerDecoration = require('./layer-decoration')
|
||||
|
||||
module.exports =
|
||||
class DecorationManager {
|
||||
constructor (editor) {
|
||||
this.editor = editor
|
||||
this.displayLayer = this.editor.displayLayer
|
||||
|
||||
this.emitter = new Emitter()
|
||||
this.decorationCountsByLayer = new Map()
|
||||
this.markerDecorationCountsByLayer = new Map()
|
||||
this.decorationsByMarker = new Map()
|
||||
this.layerDecorationsByMarkerLayer = new Map()
|
||||
this.overlayDecorations = new Set()
|
||||
this.layerUpdateDisposablesByLayer = new WeakMap()
|
||||
}
|
||||
|
||||
observeDecorations (callback) {
|
||||
const decorations = this.getDecorations()
|
||||
for (let i = 0; i < decorations.length; i++) {
|
||||
callback(decorations[i])
|
||||
}
|
||||
return this.onDidAddDecoration(callback)
|
||||
}
|
||||
|
||||
onDidAddDecoration (callback) {
|
||||
return this.emitter.on('did-add-decoration', callback)
|
||||
}
|
||||
|
||||
onDidRemoveDecoration (callback) {
|
||||
return this.emitter.on('did-remove-decoration', callback)
|
||||
}
|
||||
|
||||
onDidUpdateDecorations (callback) {
|
||||
return this.emitter.on('did-update-decorations', callback)
|
||||
}
|
||||
|
||||
getDecorations (propertyFilter) {
|
||||
let allDecorations = []
|
||||
|
||||
this.decorationsByMarker.forEach((decorations) => {
|
||||
decorations.forEach((decoration) => allDecorations.push(decoration))
|
||||
})
|
||||
if (propertyFilter != null) {
|
||||
allDecorations = allDecorations.filter(function (decoration) {
|
||||
for (let key in propertyFilter) {
|
||||
const value = propertyFilter[key]
|
||||
if (decoration.properties[key] !== value) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return allDecorations
|
||||
}
|
||||
|
||||
getLineDecorations (propertyFilter) {
|
||||
return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('line'))
|
||||
}
|
||||
|
||||
getLineNumberDecorations (propertyFilter) {
|
||||
return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('line-number'))
|
||||
}
|
||||
|
||||
getHighlightDecorations (propertyFilter) {
|
||||
return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('highlight'))
|
||||
}
|
||||
|
||||
getOverlayDecorations (propertyFilter) {
|
||||
const result = []
|
||||
result.push(...Array.from(this.overlayDecorations))
|
||||
if (propertyFilter != null) {
|
||||
return result.filter(function (decoration) {
|
||||
for (let key in propertyFilter) {
|
||||
const value = propertyFilter[key]
|
||||
if (decoration.properties[key] !== value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
decorationPropertiesByMarkerForScreenRowRange (startScreenRow, endScreenRow) {
|
||||
const decorationPropertiesByMarker = new Map()
|
||||
|
||||
this.decorationCountsByLayer.forEach((count, markerLayer) => {
|
||||
const markers = markerLayer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow - 1]})
|
||||
const layerDecorations = this.layerDecorationsByMarkerLayer.get(markerLayer)
|
||||
const hasMarkerDecorations = this.markerDecorationCountsByLayer.get(markerLayer) > 0
|
||||
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
const marker = markers[i]
|
||||
if (!marker.isValid()) continue
|
||||
|
||||
let decorationPropertiesForMarker = decorationPropertiesByMarker.get(marker)
|
||||
if (decorationPropertiesForMarker == null) {
|
||||
decorationPropertiesForMarker = []
|
||||
decorationPropertiesByMarker.set(marker, decorationPropertiesForMarker)
|
||||
}
|
||||
|
||||
if (layerDecorations) {
|
||||
layerDecorations.forEach((layerDecoration) => {
|
||||
const properties = layerDecoration.getPropertiesForMarker(marker) || layerDecoration.getProperties()
|
||||
decorationPropertiesForMarker.push(properties)
|
||||
})
|
||||
}
|
||||
|
||||
if (hasMarkerDecorations) {
|
||||
const decorationsForMarker = this.decorationsByMarker.get(marker)
|
||||
if (decorationsForMarker) {
|
||||
decorationsForMarker.forEach((decoration) => {
|
||||
decorationPropertiesForMarker.push(decoration.getProperties())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return decorationPropertiesByMarker
|
||||
}
|
||||
|
||||
decorationsForScreenRowRange (startScreenRow, endScreenRow) {
|
||||
const decorationsByMarkerId = {}
|
||||
for (const layer of this.decorationCountsByLayer.keys()) {
|
||||
for (const marker of layer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) {
|
||||
const decorations = this.decorationsByMarker.get(marker)
|
||||
if (decorations) {
|
||||
decorationsByMarkerId[marker.id] = Array.from(decorations)
|
||||
}
|
||||
}
|
||||
}
|
||||
return decorationsByMarkerId
|
||||
}
|
||||
|
||||
decorationsStateForScreenRowRange (startScreenRow, endScreenRow) {
|
||||
const decorationsState = {}
|
||||
|
||||
for (const layer of this.decorationCountsByLayer.keys()) {
|
||||
for (const marker of layer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) {
|
||||
if (marker.isValid()) {
|
||||
const screenRange = marker.getScreenRange()
|
||||
const bufferRange = marker.getBufferRange()
|
||||
const rangeIsReversed = marker.isReversed()
|
||||
|
||||
const decorations = this.decorationsByMarker.get(marker)
|
||||
if (decorations) {
|
||||
decorations.forEach((decoration) => {
|
||||
decorationsState[decoration.id] = {
|
||||
properties: decoration.properties,
|
||||
screenRange,
|
||||
bufferRange,
|
||||
rangeIsReversed
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const layerDecorations = this.layerDecorationsByMarkerLayer.get(layer)
|
||||
if (layerDecorations) {
|
||||
layerDecorations.forEach((layerDecoration) => {
|
||||
const properties = layerDecoration.getPropertiesForMarker(marker) || layerDecoration.getProperties()
|
||||
decorationsState[`${layerDecoration.id}-${marker.id}`] = {
|
||||
properties,
|
||||
screenRange,
|
||||
bufferRange,
|
||||
rangeIsReversed
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decorationsState
|
||||
}
|
||||
|
||||
decorateMarker (marker, decorationParams) {
|
||||
if (marker.isDestroyed()) {
|
||||
const error = new Error('Cannot decorate a destroyed marker')
|
||||
error.metadata = {markerLayerIsDestroyed: marker.layer.isDestroyed()}
|
||||
if (marker.destroyStackTrace != null) {
|
||||
error.metadata.destroyStackTrace = marker.destroyStackTrace
|
||||
}
|
||||
if (marker.bufferMarker != null && marker.bufferMarker.destroyStackTrace != null) {
|
||||
error.metadata.destroyStackTrace = marker.bufferMarker.destroyStackTrace
|
||||
}
|
||||
throw error
|
||||
}
|
||||
marker = this.displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id)
|
||||
const decoration = new Decoration(marker, this, decorationParams)
|
||||
let decorationsForMarker = this.decorationsByMarker.get(marker)
|
||||
if (!decorationsForMarker) {
|
||||
decorationsForMarker = new Set()
|
||||
this.decorationsByMarker.set(marker, decorationsForMarker)
|
||||
}
|
||||
decorationsForMarker.add(decoration)
|
||||
if (decoration.isType('overlay')) this.overlayDecorations.add(decoration)
|
||||
this.observeDecoratedLayer(marker.layer, true)
|
||||
this.editor.didAddDecoration(decoration)
|
||||
this.emitDidUpdateDecorations()
|
||||
this.emitter.emit('did-add-decoration', decoration)
|
||||
return decoration
|
||||
}
|
||||
|
||||
decorateMarkerLayer (markerLayer, decorationParams) {
|
||||
if (markerLayer.isDestroyed()) {
|
||||
throw new Error('Cannot decorate a destroyed marker layer')
|
||||
}
|
||||
markerLayer = this.displayLayer.getMarkerLayer(markerLayer.id)
|
||||
const decoration = new LayerDecoration(markerLayer, this, decorationParams)
|
||||
let layerDecorations = this.layerDecorationsByMarkerLayer.get(markerLayer)
|
||||
if (layerDecorations == null) {
|
||||
layerDecorations = new Set()
|
||||
this.layerDecorationsByMarkerLayer.set(markerLayer, layerDecorations)
|
||||
}
|
||||
layerDecorations.add(decoration)
|
||||
this.observeDecoratedLayer(markerLayer, false)
|
||||
this.emitDidUpdateDecorations()
|
||||
return decoration
|
||||
}
|
||||
|
||||
emitDidUpdateDecorations () {
|
||||
this.editor.scheduleComponentUpdate()
|
||||
this.emitter.emit('did-update-decorations')
|
||||
}
|
||||
|
||||
decorationDidChangeType (decoration) {
|
||||
if (decoration.isType('overlay')) {
|
||||
this.overlayDecorations.add(decoration)
|
||||
} else {
|
||||
this.overlayDecorations.delete(decoration)
|
||||
}
|
||||
}
|
||||
|
||||
didDestroyMarkerDecoration (decoration) {
|
||||
const {marker} = decoration
|
||||
const decorations = this.decorationsByMarker.get(marker)
|
||||
if (decorations && decorations.has(decoration)) {
|
||||
decorations.delete(decoration)
|
||||
if (decorations.size === 0) this.decorationsByMarker.delete(marker)
|
||||
this.overlayDecorations.delete(decoration)
|
||||
this.unobserveDecoratedLayer(marker.layer, true)
|
||||
this.emitter.emit('did-remove-decoration', decoration)
|
||||
this.emitDidUpdateDecorations()
|
||||
}
|
||||
}
|
||||
|
||||
didDestroyLayerDecoration (decoration) {
|
||||
const {markerLayer} = decoration
|
||||
const decorations = this.layerDecorationsByMarkerLayer.get(markerLayer)
|
||||
|
||||
if (decorations && decorations.has(decoration)) {
|
||||
decorations.delete(decoration)
|
||||
if (decorations.size === 0) {
|
||||
this.layerDecorationsByMarkerLayer.delete(markerLayer)
|
||||
}
|
||||
this.unobserveDecoratedLayer(markerLayer, true)
|
||||
this.emitDidUpdateDecorations()
|
||||
}
|
||||
}
|
||||
|
||||
observeDecoratedLayer (layer, isMarkerDecoration) {
|
||||
const newCount = (this.decorationCountsByLayer.get(layer) || 0) + 1
|
||||
this.decorationCountsByLayer.set(layer, newCount)
|
||||
if (newCount === 1) {
|
||||
this.layerUpdateDisposablesByLayer.set(layer, layer.onDidUpdate(this.emitDidUpdateDecorations.bind(this)))
|
||||
}
|
||||
if (isMarkerDecoration) {
|
||||
this.markerDecorationCountsByLayer.set(layer, (this.markerDecorationCountsByLayer.get(layer) || 0) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
unobserveDecoratedLayer (layer, isMarkerDecoration) {
|
||||
const newCount = this.decorationCountsByLayer.get(layer) - 1
|
||||
if (newCount === 0) {
|
||||
this.layerUpdateDisposablesByLayer.get(layer).dispose()
|
||||
this.decorationCountsByLayer.delete(layer)
|
||||
} else {
|
||||
this.decorationCountsByLayer.set(layer, newCount)
|
||||
}
|
||||
if (isMarkerDecoration) {
|
||||
this.markerDecorationCountsByLayer.set(this.markerDecorationCountsByLayer.get(layer) - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ class Decoration
|
||||
@properties = translateDecorationParamsOldToNew(newProperties)
|
||||
if newProperties.type?
|
||||
@decorationManager.decorationDidChangeType(this)
|
||||
@decorationManager.scheduleUpdateDecorationsEvent()
|
||||
@decorationManager.emitDidUpdateDecorations()
|
||||
@emitter.emit 'did-change-properties', {oldProperties, newProperties}
|
||||
|
||||
###
|
||||
@@ -171,9 +171,8 @@ class Decoration
|
||||
true
|
||||
|
||||
flash: (klass, duration=500) ->
|
||||
@properties.flashCount ?= 0
|
||||
@properties.flashCount++
|
||||
@properties.flashRequested = true
|
||||
@properties.flashClass = klass
|
||||
@properties.flashDuration = duration
|
||||
@decorationManager.scheduleUpdateDecorationsEvent()
|
||||
@decorationManager.emitDidUpdateDecorations()
|
||||
@emitter.emit 'did-flash'
|
||||
|
||||
12
src/dock.js
12
src/dock.js
@@ -151,7 +151,7 @@ module.exports = class Dock {
|
||||
|
||||
this.state = nextState
|
||||
this.render(this.state)
|
||||
if (didHide) this.didHide()
|
||||
if (didHide) this.didHide(this)
|
||||
}
|
||||
|
||||
render (state) {
|
||||
@@ -626,6 +626,16 @@ module.exports = class Dock {
|
||||
return this.paneContainer.getActivePane()
|
||||
}
|
||||
|
||||
// Extended: Make the next pane active.
|
||||
activateNextPane () {
|
||||
return this.paneContainer.activateNextPane()
|
||||
}
|
||||
|
||||
// Extended: Make the previous pane active.
|
||||
activatePreviousPane () {
|
||||
return this.paneContainer.activatePreviousPane()
|
||||
}
|
||||
|
||||
paneForURI (uri) {
|
||||
return this.paneContainer.paneForURI(uri)
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
module.exports =
|
||||
class DOMElementPool {
|
||||
constructor () {
|
||||
this.managedElements = new Set()
|
||||
this.freeElementsByTagName = new Map()
|
||||
this.freedElements = new Set()
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.managedElements.clear()
|
||||
this.freedElements.clear()
|
||||
this.freeElementsByTagName.clear()
|
||||
}
|
||||
|
||||
buildElement (tagName, className) {
|
||||
const elements = this.freeElementsByTagName.get(tagName)
|
||||
let element = elements ? elements.pop() : null
|
||||
if (element) {
|
||||
for (let dataId in element.dataset) { delete element.dataset[dataId] }
|
||||
element.removeAttribute('style')
|
||||
if (className) {
|
||||
element.className = className
|
||||
} else {
|
||||
element.removeAttribute('class')
|
||||
}
|
||||
while (element.firstChild) {
|
||||
element.removeChild(element.firstChild)
|
||||
}
|
||||
this.freedElements.delete(element)
|
||||
} else {
|
||||
element = document.createElement(tagName)
|
||||
if (className) {
|
||||
element.className = className
|
||||
}
|
||||
this.managedElements.add(element)
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
buildText (textContent) {
|
||||
const elements = this.freeElementsByTagName.get('#text')
|
||||
let element = elements ? elements.pop() : null
|
||||
if (element) {
|
||||
element.textContent = textContent
|
||||
this.freedElements.delete(element)
|
||||
} else {
|
||||
element = document.createTextNode(textContent)
|
||||
this.managedElements.add(element)
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
freeElementAndDescendants (element) {
|
||||
this.free(element)
|
||||
element.remove()
|
||||
}
|
||||
|
||||
freeDescendants (element) {
|
||||
while (element.firstChild) {
|
||||
this.free(element.firstChild)
|
||||
element.removeChild(element.firstChild)
|
||||
}
|
||||
}
|
||||
|
||||
free (element) {
|
||||
if (element == null) { throw new Error('The element cannot be null or undefined.') }
|
||||
if (!this.managedElements.has(element)) return
|
||||
if (this.freedElements.has(element)) {
|
||||
atom.assert(false, 'The element has already been freed!', {
|
||||
content: element instanceof window.Text ? element.textContent : element.outerHTML
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const tagName = element.nodeName.toLowerCase()
|
||||
let elements = this.freeElementsByTagName.get(tagName)
|
||||
if (!elements) {
|
||||
elements = []
|
||||
this.freeElementsByTagName.set(tagName, elements)
|
||||
}
|
||||
elements.push(element)
|
||||
this.freedElements.add(element)
|
||||
|
||||
for (let i = element.childNodes.length - 1; i >= 0; i--) {
|
||||
const descendant = element.childNodes[i]
|
||||
this.free(descendant)
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/first-mate-helpers.js
Normal file
11
src/first-mate-helpers.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
fromFirstMateScopeId (firstMateScopeId) {
|
||||
let atomScopeId = -firstMateScopeId
|
||||
if ((atomScopeId & 1) === 0) atomScopeId--
|
||||
return atomScopeId + 256
|
||||
},
|
||||
|
||||
toFirstMateScopeId (atomScopeId) {
|
||||
return -(atomScopeId - 256)
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
CustomGutterComponent = require './custom-gutter-component'
|
||||
LineNumberGutterComponent = require './line-number-gutter-component'
|
||||
|
||||
# The GutterContainerComponent manages the GutterComponents of a particular
|
||||
# TextEditorComponent.
|
||||
|
||||
module.exports =
|
||||
class GutterContainerComponent
|
||||
constructor: ({@onLineNumberGutterMouseDown, @editor, @domElementPool, @views}) ->
|
||||
# An array of objects of the form: {name: {String}, component: {Object}}
|
||||
@gutterComponents = []
|
||||
@gutterComponentsByGutterName = {}
|
||||
@lineNumberGutterComponent = null
|
||||
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add('gutter-container')
|
||||
@domNode.style.display = 'flex'
|
||||
|
||||
destroy: ->
|
||||
for {component} in @gutterComponents
|
||||
component.destroy?()
|
||||
return
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
getLineNumberGutterComponent: ->
|
||||
@lineNumberGutterComponent
|
||||
|
||||
updateSync: (state) ->
|
||||
# The GutterContainerComponent expects the gutters to be sorted in the order
|
||||
# they should appear.
|
||||
newState = state.gutters
|
||||
|
||||
newGutterComponents = []
|
||||
newGutterComponentsByGutterName = {}
|
||||
for {gutter, visible, styles, content} in newState
|
||||
gutterComponent = @gutterComponentsByGutterName[gutter.name]
|
||||
if not gutterComponent
|
||||
if gutter.name is 'line-number'
|
||||
gutterComponent = new LineNumberGutterComponent({onMouseDown: @onLineNumberGutterMouseDown, @editor, gutter, @domElementPool, @views})
|
||||
@lineNumberGutterComponent = gutterComponent
|
||||
else
|
||||
gutterComponent = new CustomGutterComponent({gutter, @views})
|
||||
|
||||
if visible then gutterComponent.showNode() else gutterComponent.hideNode()
|
||||
# Pass the gutter only the state that it needs.
|
||||
if gutter.name is 'line-number'
|
||||
# For ease of use in the line number gutter component, set the shared
|
||||
# 'styles' as a field under the 'content'.
|
||||
gutterSubstate = _.clone(content)
|
||||
gutterSubstate.styles = styles
|
||||
else
|
||||
# Custom gutter 'content' is keyed on gutter name, so we cannot set
|
||||
# 'styles' as a subfield directly under it.
|
||||
gutterSubstate = {content, styles}
|
||||
gutterComponent.updateSync(gutterSubstate)
|
||||
|
||||
newGutterComponents.push({
|
||||
name: gutter.name,
|
||||
component: gutterComponent,
|
||||
})
|
||||
newGutterComponentsByGutterName[gutter.name] = gutterComponent
|
||||
|
||||
@reorderGutters(newGutterComponents, newGutterComponentsByGutterName)
|
||||
|
||||
@gutterComponents = newGutterComponents
|
||||
@gutterComponentsByGutterName = newGutterComponentsByGutterName
|
||||
|
||||
###
|
||||
Section: Private Methods
|
||||
###
|
||||
|
||||
reorderGutters: (newGutterComponents, newGutterComponentsByGutterName) ->
|
||||
# First, insert new gutters into the DOM.
|
||||
indexInOldGutters = 0
|
||||
oldGuttersLength = @gutterComponents.length
|
||||
|
||||
for gutterComponentDescription in newGutterComponents
|
||||
gutterComponent = gutterComponentDescription.component
|
||||
gutterName = gutterComponentDescription.name
|
||||
|
||||
if @gutterComponentsByGutterName[gutterName]
|
||||
# If the gutter existed previously, we first try to move the cursor to
|
||||
# the point at which it occurs in the previous gutters.
|
||||
matchingGutterFound = false
|
||||
while indexInOldGutters < oldGuttersLength
|
||||
existingGutterComponentDescription = @gutterComponents[indexInOldGutters]
|
||||
existingGutterComponent = existingGutterComponentDescription.component
|
||||
indexInOldGutters++
|
||||
if existingGutterComponent is gutterComponent
|
||||
matchingGutterFound = true
|
||||
break
|
||||
if not matchingGutterFound
|
||||
# If we've reached this point, the gutter previously existed, but its
|
||||
# position has moved. Remove it from the DOM and re-insert it.
|
||||
gutterComponent.getDomNode().remove()
|
||||
@domNode.appendChild(gutterComponent.getDomNode())
|
||||
|
||||
else
|
||||
if indexInOldGutters is oldGuttersLength
|
||||
@domNode.appendChild(gutterComponent.getDomNode())
|
||||
else
|
||||
@domNode.insertBefore(gutterComponent.getDomNode(), @domNode.children[indexInOldGutters])
|
||||
indexInOldGutters += 1
|
||||
|
||||
# Remove any gutters that were not present in the new gutters state.
|
||||
for gutterComponentDescription in @gutterComponents
|
||||
if not newGutterComponentsByGutterName[gutterComponentDescription.name]
|
||||
gutterComponent = gutterComponentDescription.component
|
||||
gutterComponent.getDomNode().remove()
|
||||
@@ -8,6 +8,9 @@ class GutterContainer
|
||||
@textEditor = textEditor
|
||||
@emitter = new Emitter
|
||||
|
||||
scheduleComponentUpdate: ->
|
||||
@textEditor.scheduleComponentUpdate()
|
||||
|
||||
destroy: ->
|
||||
# Create a copy, because `Gutter::destroy` removes the gutter from
|
||||
# GutterContainer's @gutters.
|
||||
@@ -36,6 +39,7 @@ class GutterContainer
|
||||
break
|
||||
if not inserted
|
||||
@gutters.push newGutter
|
||||
@scheduleComponentUpdate()
|
||||
@emitter.emit 'did-add-gutter', newGutter
|
||||
return newGutter
|
||||
|
||||
@@ -67,6 +71,7 @@ class GutterContainer
|
||||
index = @gutters.indexOf(gutter)
|
||||
if index > -1
|
||||
@gutters.splice(index, 1)
|
||||
@scheduleComponentUpdate()
|
||||
@emitter.emit 'did-remove-gutter', gutter.name
|
||||
else
|
||||
throw new Error 'The given gutter cannot be removed because it is not ' +
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{Emitter} = require 'event-kit'
|
||||
CustomGutterComponent = null
|
||||
|
||||
DefaultPriority = -100
|
||||
|
||||
@@ -28,19 +29,6 @@ class Gutter
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.dispose()
|
||||
|
||||
getElement: ->
|
||||
unless @element?
|
||||
@element = document.createElement('div')
|
||||
@element.classList.add('gutter')
|
||||
@element.setAttribute('gutter-name', @name)
|
||||
childNode = document.createElement('div')
|
||||
if @name is 'line-number'
|
||||
childNode.classList.add('line-numbers')
|
||||
else
|
||||
childNode.classList.add('custom-decorations')
|
||||
@element.appendChild(childNode)
|
||||
@element
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
@@ -70,12 +58,14 @@ class Gutter
|
||||
hide: ->
|
||||
if @visible
|
||||
@visible = false
|
||||
@gutterContainer.scheduleComponentUpdate()
|
||||
@emitter.emit 'did-change-visible', this
|
||||
|
||||
# Essential: Show the gutter.
|
||||
show: ->
|
||||
if not @visible
|
||||
@visible = true
|
||||
@gutterContainer.scheduleComponentUpdate()
|
||||
@emitter.emit 'did-change-visible', this
|
||||
|
||||
# Essential: Determine whether the gutter is visible.
|
||||
@@ -100,3 +90,6 @@ class Gutter
|
||||
# Returns a {Decoration} object
|
||||
decorateMarker: (marker, options) ->
|
||||
@gutterContainer.addGutterDecoration(this, marker, options)
|
||||
|
||||
getElement: ->
|
||||
@element ?= document.createElement('div')
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
RegionStyleProperties = ['top', 'left', 'right', 'width', 'height']
|
||||
SpaceRegex = /\s+/
|
||||
|
||||
module.exports =
|
||||
class HighlightsComponent
|
||||
oldState: null
|
||||
|
||||
constructor: (@domElementPool) ->
|
||||
@highlightNodesById = {}
|
||||
@regionNodesByHighlightId = {}
|
||||
|
||||
@domNode = @domElementPool.buildElement("div", "highlights")
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
newState = state.highlights
|
||||
@oldState ?= {}
|
||||
|
||||
# remove highlights
|
||||
for id of @oldState
|
||||
unless newState[id]?
|
||||
@domElementPool.freeElementAndDescendants(@highlightNodesById[id])
|
||||
delete @highlightNodesById[id]
|
||||
delete @regionNodesByHighlightId[id]
|
||||
delete @oldState[id]
|
||||
|
||||
# add or update highlights
|
||||
for id, highlightState of newState
|
||||
unless @oldState[id]?
|
||||
highlightNode = @domElementPool.buildElement("div", "highlight")
|
||||
@highlightNodesById[id] = highlightNode
|
||||
@regionNodesByHighlightId[id] = {}
|
||||
@domNode.appendChild(highlightNode)
|
||||
@updateHighlightNode(id, highlightState)
|
||||
|
||||
return
|
||||
|
||||
updateHighlightNode: (id, newHighlightState) ->
|
||||
highlightNode = @highlightNodesById[id]
|
||||
oldHighlightState = (@oldState[id] ?= {regions: [], flashCount: 0})
|
||||
|
||||
# update class
|
||||
if newHighlightState.class isnt oldHighlightState.class
|
||||
if oldHighlightState.class?
|
||||
if SpaceRegex.test(oldHighlightState.class)
|
||||
highlightNode.classList.remove(oldHighlightState.class.split(SpaceRegex)...)
|
||||
else
|
||||
highlightNode.classList.remove(oldHighlightState.class)
|
||||
|
||||
if SpaceRegex.test(newHighlightState.class)
|
||||
highlightNode.classList.add(newHighlightState.class.split(SpaceRegex)...)
|
||||
else
|
||||
highlightNode.classList.add(newHighlightState.class)
|
||||
|
||||
oldHighlightState.class = newHighlightState.class
|
||||
|
||||
@updateHighlightRegions(id, newHighlightState)
|
||||
@flashHighlightNodeIfRequested(id, newHighlightState)
|
||||
|
||||
updateHighlightRegions: (id, newHighlightState) ->
|
||||
oldHighlightState = @oldState[id]
|
||||
highlightNode = @highlightNodesById[id]
|
||||
|
||||
# remove regions
|
||||
while oldHighlightState.regions.length > newHighlightState.regions.length
|
||||
oldHighlightState.regions.pop()
|
||||
@domElementPool.freeElementAndDescendants(@regionNodesByHighlightId[id][oldHighlightState.regions.length])
|
||||
delete @regionNodesByHighlightId[id][oldHighlightState.regions.length]
|
||||
|
||||
# add or update regions
|
||||
for newRegionState, i in newHighlightState.regions
|
||||
unless oldHighlightState.regions[i]?
|
||||
oldHighlightState.regions[i] = {}
|
||||
regionNode = @domElementPool.buildElement("div", "region")
|
||||
# This prevents highlights at the tiles boundaries to be hidden by the
|
||||
# subsequent tile. When this happens, subpixel anti-aliasing gets
|
||||
# disabled.
|
||||
regionNode.style.boxSizing = "border-box"
|
||||
regionNode.classList.add(newHighlightState.deprecatedRegionClass) if newHighlightState.deprecatedRegionClass?
|
||||
@regionNodesByHighlightId[id][i] = regionNode
|
||||
highlightNode.appendChild(regionNode)
|
||||
|
||||
oldRegionState = oldHighlightState.regions[i]
|
||||
regionNode = @regionNodesByHighlightId[id][i]
|
||||
|
||||
for property in RegionStyleProperties
|
||||
if newRegionState[property] isnt oldRegionState[property]
|
||||
oldRegionState[property] = newRegionState[property]
|
||||
if newRegionState[property]?
|
||||
regionNode.style[property] = newRegionState[property] + 'px'
|
||||
else
|
||||
regionNode.style[property] = ''
|
||||
|
||||
return
|
||||
|
||||
flashHighlightNodeIfRequested: (id, newHighlightState) ->
|
||||
oldHighlightState = @oldState[id]
|
||||
if newHighlightState.needsFlash and oldHighlightState.flashCount isnt newHighlightState.flashCount
|
||||
highlightNode = @highlightNodesById[id]
|
||||
|
||||
addFlashClass = =>
|
||||
highlightNode.classList.add(newHighlightState.flashClass)
|
||||
oldHighlightState.flashClass = newHighlightState.flashClass
|
||||
@flashTimeoutId = setTimeout(removeFlashClass, newHighlightState.flashDuration)
|
||||
|
||||
removeFlashClass = =>
|
||||
highlightNode.classList.remove(oldHighlightState.flashClass)
|
||||
oldHighlightState.flashClass = null
|
||||
clearTimeout(@flashTimeoutId)
|
||||
|
||||
if oldHighlightState.flashClass?
|
||||
removeFlashClass()
|
||||
requestAnimationFrame(addFlashClass)
|
||||
else
|
||||
addFlashClass()
|
||||
|
||||
oldHighlightState.flashCount = newHighlightState.flashCount
|
||||
@@ -17,10 +17,6 @@ export class HistoryManager {
|
||||
this.disposables.add(project.onDidChangePaths((projectPaths) => this.addProject(projectPaths)))
|
||||
}
|
||||
|
||||
initialize (localStorage) {
|
||||
this.localStorage = localStorage
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.disposables.dispose()
|
||||
}
|
||||
@@ -98,10 +94,6 @@ export class HistoryManager {
|
||||
|
||||
async loadState () {
|
||||
let history = await this.stateStore.load('history-manager')
|
||||
if (!history) {
|
||||
history = JSON.parse(this.localStorage.getItem('history'))
|
||||
}
|
||||
|
||||
if (history && history.projects) {
|
||||
this.projects = history.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened)))
|
||||
this.didChangeProjects({reloaded: true})
|
||||
|
||||
@@ -59,6 +59,7 @@ if global.isGeneratingSnapshot
|
||||
|
||||
clipboard = new Clipboard
|
||||
TextEditor.setClipboard(clipboard)
|
||||
TextEditor.viewForItem = (item) -> atom.views.getView(item)
|
||||
|
||||
global.atom = new AtomEnvironment({
|
||||
clipboard,
|
||||
|
||||
@@ -54,6 +54,7 @@ export default async function () {
|
||||
|
||||
const clipboard = new Clipboard()
|
||||
TextEditor.setClipboard(clipboard)
|
||||
TextEditor.viewForItem = (item) => atom.views.getView(item)
|
||||
|
||||
const applicationDelegate = new ApplicationDelegate()
|
||||
const environmentParams = {
|
||||
|
||||
@@ -70,6 +70,7 @@ module.exports = ({blobStore}) ->
|
||||
|
||||
clipboard = new Clipboard
|
||||
TextEditor.setClipboard(clipboard)
|
||||
TextEditor.viewForItem = (item) -> atom.views.getView(item)
|
||||
|
||||
testRunner = require(testRunnerPath)
|
||||
legacyTestRunner = require(legacyTestRunnerPath)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
module.exports =
|
||||
class InputComponent
|
||||
constructor: (@domNode) ->
|
||||
|
||||
updateSync: (state) ->
|
||||
@oldState ?= {}
|
||||
newState = state.hiddenInput
|
||||
|
||||
if newState.top isnt @oldState.top
|
||||
@domNode.style.top = newState.top + 'px'
|
||||
@oldState.top = newState.top
|
||||
|
||||
if newState.left isnt @oldState.left
|
||||
@domNode.style.left = newState.left + 'px'
|
||||
@oldState.left = newState.left
|
||||
|
||||
if newState.width isnt @oldState.width
|
||||
@domNode.style.width = newState.width + 'px'
|
||||
@oldState.width = newState.width
|
||||
|
||||
if newState.height isnt @oldState.height
|
||||
@domNode.style.height = newState.height + 'px'
|
||||
@oldState.height = newState.height
|
||||
@@ -189,7 +189,7 @@ class LanguageMode
|
||||
# row is a comment.
|
||||
isLineCommentedAtBufferRow: (bufferRow) ->
|
||||
return false unless 0 <= bufferRow <= @editor.getLastBufferRow()
|
||||
@editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment()
|
||||
@editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() ? false
|
||||
|
||||
# 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
|
||||
|
||||
@@ -9,7 +9,7 @@ class LayerDecoration
|
||||
@id = nextId()
|
||||
@destroyed = false
|
||||
@markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy()
|
||||
@overridePropertiesByMarkerId = {}
|
||||
@overridePropertiesByMarker = null
|
||||
|
||||
# Essential: Destroys the decoration.
|
||||
destroy: ->
|
||||
@@ -42,7 +42,7 @@ class LayerDecoration
|
||||
setProperties: (newProperties) ->
|
||||
return if @destroyed
|
||||
@properties = newProperties
|
||||
@decorationManager.scheduleUpdateDecorationsEvent()
|
||||
@decorationManager.emitDidUpdateDecorations()
|
||||
|
||||
# Essential: Override the decoration properties for a specific marker.
|
||||
#
|
||||
@@ -52,8 +52,13 @@ class LayerDecoration
|
||||
# Pass `null` to clear the override.
|
||||
setPropertiesForMarker: (marker, properties) ->
|
||||
return if @destroyed
|
||||
@overridePropertiesByMarker ?= new Map()
|
||||
marker = @markerLayer.getMarker(marker.id)
|
||||
if properties?
|
||||
@overridePropertiesByMarkerId[marker.id] = properties
|
||||
@overridePropertiesByMarker.set(marker, properties)
|
||||
else
|
||||
delete @overridePropertiesByMarkerId[marker.id]
|
||||
@decorationManager.scheduleUpdateDecorationsEvent()
|
||||
@overridePropertiesByMarker.delete(marker)
|
||||
@decorationManager.emitDidUpdateDecorations()
|
||||
|
||||
getPropertiesForMarker: (marker) ->
|
||||
@overridePropertiesByMarker?.get(marker)
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
TiledComponent = require './tiled-component'
|
||||
LineNumbersTileComponent = require './line-numbers-tile-component'
|
||||
|
||||
module.exports =
|
||||
class LineNumberGutterComponent extends TiledComponent
|
||||
dummyLineNumberNode: null
|
||||
|
||||
constructor: ({@onMouseDown, @editor, @gutter, @domElementPool, @views}) ->
|
||||
@visible = true
|
||||
|
||||
@dummyLineNumberComponent = LineNumbersTileComponent.createDummy(@domElementPool)
|
||||
|
||||
@domNode = @gutter.getElement()
|
||||
@lineNumbersNode = @domNode.firstChild
|
||||
@lineNumbersNode.innerHTML = ''
|
||||
|
||||
@domNode.addEventListener 'click', @onClick
|
||||
@domNode.addEventListener 'mousedown', @onMouseDown
|
||||
|
||||
destroy: ->
|
||||
@domNode.removeEventListener 'click', @onClick
|
||||
@domNode.removeEventListener 'mousedown', @onMouseDown
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
hideNode: ->
|
||||
if @visible
|
||||
@domNode.style.display = 'none'
|
||||
@visible = false
|
||||
|
||||
showNode: ->
|
||||
if not @visible
|
||||
@domNode.style.removeProperty('display')
|
||||
@visible = true
|
||||
|
||||
buildEmptyState: ->
|
||||
{
|
||||
tiles: {}
|
||||
styles: {}
|
||||
}
|
||||
|
||||
getNewState: (state) -> state
|
||||
|
||||
getTilesNode: -> @lineNumbersNode
|
||||
|
||||
beforeUpdateSync: (state) ->
|
||||
@appendDummyLineNumber() unless @dummyLineNumberNode?
|
||||
|
||||
if @newState.styles.maxHeight isnt @oldState.styles.maxHeight
|
||||
@lineNumbersNode.style.height = @newState.styles.maxHeight + 'px'
|
||||
@oldState.maxHeight = @newState.maxHeight
|
||||
|
||||
if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor
|
||||
@lineNumbersNode.style.backgroundColor = @newState.styles.backgroundColor
|
||||
@oldState.styles.backgroundColor = @newState.styles.backgroundColor
|
||||
|
||||
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
|
||||
@updateDummyLineNumber()
|
||||
@oldState.styles = {}
|
||||
@oldState.maxLineNumberDigits = @newState.maxLineNumberDigits
|
||||
|
||||
buildComponentForTile: (id) -> new LineNumbersTileComponent({id, @domElementPool})
|
||||
|
||||
shouldRecreateAllTilesOnUpdate: ->
|
||||
@newState.continuousReflow
|
||||
|
||||
###
|
||||
Section: Private Methods
|
||||
###
|
||||
|
||||
# This dummy line number element holds the gutter to the appropriate width,
|
||||
# since the real line numbers are absolutely positioned for performance reasons.
|
||||
appendDummyLineNumber: ->
|
||||
@dummyLineNumberComponent.newState = @newState
|
||||
@dummyLineNumberNode = @dummyLineNumberComponent.buildLineNumberNode({bufferRow: -1})
|
||||
@lineNumbersNode.appendChild(@dummyLineNumberNode)
|
||||
|
||||
updateDummyLineNumber: ->
|
||||
@dummyLineNumberComponent.newState = @newState
|
||||
@dummyLineNumberComponent.setLineNumberInnerNodes(0, false, @dummyLineNumberNode)
|
||||
|
||||
onMouseDown: (event) =>
|
||||
{target} = event
|
||||
lineNumber = target.parentNode
|
||||
|
||||
unless target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
|
||||
@onMouseDown(event)
|
||||
|
||||
onClick: (event) =>
|
||||
{target} = event
|
||||
lineNumber = target.parentNode
|
||||
|
||||
if target.classList.contains('icon-right')
|
||||
bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row'))
|
||||
if lineNumber.classList.contains('folded')
|
||||
@editor.unfoldBufferRow(bufferRow)
|
||||
else if lineNumber.classList.contains('foldable')
|
||||
@editor.foldBufferRow(bufferRow)
|
||||
@@ -1,157 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
module.exports =
|
||||
class LineNumbersTileComponent
|
||||
@createDummy: (domElementPool) ->
|
||||
new LineNumbersTileComponent({id: -1, domElementPool})
|
||||
|
||||
constructor: ({@id, @domElementPool}) ->
|
||||
@lineNumberNodesById = {}
|
||||
@domNode = @domElementPool.buildElement("div")
|
||||
@domNode.style.position = "absolute"
|
||||
@domNode.style.display = "block"
|
||||
@domNode.style.top = 0 # Cover the space occupied by a dummy lineNumber
|
||||
|
||||
destroy: ->
|
||||
@domElementPool.freeElementAndDescendants(@domNode)
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
@newState = state
|
||||
unless @oldState
|
||||
@oldState = {tiles: {}, styles: {}}
|
||||
@oldState.tiles[@id] = {lineNumbers: {}}
|
||||
|
||||
@newTileState = @newState.tiles[@id]
|
||||
@oldTileState = @oldState.tiles[@id]
|
||||
|
||||
if @newTileState.display isnt @oldTileState.display
|
||||
@domNode.style.display = @newTileState.display
|
||||
@oldTileState.display = @newTileState.display
|
||||
|
||||
if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor
|
||||
@domNode.style.backgroundColor = @newState.styles.backgroundColor
|
||||
@oldState.styles.backgroundColor = @newState.styles.backgroundColor
|
||||
|
||||
if @newTileState.height isnt @oldTileState.height
|
||||
@domNode.style.height = @newTileState.height + 'px'
|
||||
@oldTileState.height = @newTileState.height
|
||||
|
||||
if @newTileState.top isnt @oldTileState.top
|
||||
@domNode.style['-webkit-transform'] = "translate3d(0, #{@newTileState.top}px, 0px)"
|
||||
@oldTileState.top = @newTileState.top
|
||||
|
||||
if @newTileState.zIndex isnt @oldTileState.zIndex
|
||||
@domNode.style.zIndex = @newTileState.zIndex
|
||||
@oldTileState.zIndex = @newTileState.zIndex
|
||||
|
||||
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
|
||||
for id, node of @lineNumberNodesById
|
||||
@domElementPool.freeElementAndDescendants(node)
|
||||
|
||||
@oldState.tiles[@id] = {lineNumbers: {}}
|
||||
@oldTileState = @oldState.tiles[@id]
|
||||
@lineNumberNodesById = {}
|
||||
@oldState.maxLineNumberDigits = @newState.maxLineNumberDigits
|
||||
|
||||
@updateLineNumbers()
|
||||
|
||||
updateLineNumbers: ->
|
||||
newLineNumberIds = null
|
||||
newLineNumberNodes = null
|
||||
|
||||
for id, lineNumberState of @oldTileState.lineNumbers
|
||||
unless @newTileState.lineNumbers.hasOwnProperty(id)
|
||||
@domElementPool.freeElementAndDescendants(@lineNumberNodesById[id])
|
||||
delete @lineNumberNodesById[id]
|
||||
delete @oldTileState.lineNumbers[id]
|
||||
|
||||
for id, lineNumberState of @newTileState.lineNumbers
|
||||
if @oldTileState.lineNumbers.hasOwnProperty(id)
|
||||
@updateLineNumberNode(id, lineNumberState)
|
||||
else
|
||||
newLineNumberIds ?= []
|
||||
newLineNumberNodes ?= []
|
||||
newLineNumberIds.push(id)
|
||||
newLineNumberNodes.push(@buildLineNumberNode(lineNumberState))
|
||||
@oldTileState.lineNumbers[id] = _.clone(lineNumberState)
|
||||
|
||||
return unless newLineNumberIds?
|
||||
|
||||
for id, i in newLineNumberIds
|
||||
lineNumberNode = newLineNumberNodes[i]
|
||||
@lineNumberNodesById[id] = lineNumberNode
|
||||
if nextNode = @findNodeNextTo(lineNumberNode)
|
||||
@domNode.insertBefore(lineNumberNode, nextNode)
|
||||
else
|
||||
@domNode.appendChild(lineNumberNode)
|
||||
|
||||
findNodeNextTo: (node) ->
|
||||
for nextNode in @domNode.children
|
||||
return nextNode if @screenRowForNode(node) < @screenRowForNode(nextNode)
|
||||
return
|
||||
|
||||
screenRowForNode: (node) -> parseInt(node.dataset.screenRow)
|
||||
|
||||
buildLineNumberNode: (lineNumberState) ->
|
||||
{screenRow, bufferRow, softWrapped, blockDecorationsHeight} = lineNumberState
|
||||
|
||||
className = @buildLineNumberClassName(lineNumberState)
|
||||
lineNumberNode = @domElementPool.buildElement("div", className)
|
||||
lineNumberNode.dataset.screenRow = screenRow
|
||||
lineNumberNode.dataset.bufferRow = bufferRow
|
||||
lineNumberNode.style.marginTop = blockDecorationsHeight + "px"
|
||||
|
||||
@setLineNumberInnerNodes(bufferRow, softWrapped, lineNumberNode)
|
||||
lineNumberNode
|
||||
|
||||
setLineNumberInnerNodes: (bufferRow, softWrapped, lineNumberNode) ->
|
||||
@domElementPool.freeDescendants(lineNumberNode)
|
||||
|
||||
{maxLineNumberDigits} = @newState
|
||||
|
||||
if softWrapped
|
||||
lineNumber = "•"
|
||||
else
|
||||
lineNumber = (bufferRow + 1).toString()
|
||||
padding = _.multiplyString("\u00a0", maxLineNumberDigits - lineNumber.length)
|
||||
|
||||
textNode = @domElementPool.buildText(padding + lineNumber)
|
||||
iconRight = @domElementPool.buildElement("div", "icon-right")
|
||||
|
||||
lineNumberNode.appendChild(textNode)
|
||||
lineNumberNode.appendChild(iconRight)
|
||||
|
||||
updateLineNumberNode: (lineNumberId, newLineNumberState) ->
|
||||
oldLineNumberState = @oldTileState.lineNumbers[lineNumberId]
|
||||
node = @lineNumberNodesById[lineNumberId]
|
||||
|
||||
unless oldLineNumberState.foldable is newLineNumberState.foldable and _.isEqual(oldLineNumberState.decorationClasses, newLineNumberState.decorationClasses)
|
||||
node.className = @buildLineNumberClassName(newLineNumberState)
|
||||
oldLineNumberState.foldable = newLineNumberState.foldable
|
||||
oldLineNumberState.decorationClasses = _.clone(newLineNumberState.decorationClasses)
|
||||
|
||||
unless oldLineNumberState.screenRow is newLineNumberState.screenRow and oldLineNumberState.bufferRow is newLineNumberState.bufferRow
|
||||
@setLineNumberInnerNodes(newLineNumberState.bufferRow, newLineNumberState.softWrapped, node)
|
||||
node.dataset.screenRow = newLineNumberState.screenRow
|
||||
node.dataset.bufferRow = newLineNumberState.bufferRow
|
||||
oldLineNumberState.screenRow = newLineNumberState.screenRow
|
||||
oldLineNumberState.bufferRow = newLineNumberState.bufferRow
|
||||
|
||||
unless oldLineNumberState.blockDecorationsHeight is newLineNumberState.blockDecorationsHeight
|
||||
node.style.marginTop = newLineNumberState.blockDecorationsHeight + "px"
|
||||
oldLineNumberState.blockDecorationsHeight = newLineNumberState.blockDecorationsHeight
|
||||
|
||||
buildLineNumberClassName: ({bufferRow, foldable, decorationClasses, softWrapped}) ->
|
||||
className = "line-number"
|
||||
className += " " + decorationClasses.join(' ') if decorationClasses?
|
||||
className += " foldable" if foldable and not softWrapped
|
||||
className
|
||||
|
||||
lineNumberNodeForScreenRow: (screenRow) ->
|
||||
for id, lineNumberState of @oldTileState.lineNumbers
|
||||
if lineNumberState.screenRow is screenRow
|
||||
return @lineNumberNodesById[id]
|
||||
null
|
||||
@@ -1,109 +0,0 @@
|
||||
CursorsComponent = require './cursors-component'
|
||||
LinesTileComponent = require './lines-tile-component'
|
||||
TiledComponent = require './tiled-component'
|
||||
|
||||
module.exports =
|
||||
class LinesComponent extends TiledComponent
|
||||
placeholderTextDiv: null
|
||||
|
||||
constructor: ({@views, @presenter, @domElementPool, @assert}) ->
|
||||
@DummyLineNode = document.createElement('div')
|
||||
@DummyLineNode.className = 'line'
|
||||
@DummyLineNode.style.position = 'absolute'
|
||||
@DummyLineNode.style.visibility = 'hidden'
|
||||
@DummyLineNode.appendChild(document.createElement('span'))
|
||||
@DummyLineNode.appendChild(document.createElement('span'))
|
||||
@DummyLineNode.appendChild(document.createElement('span'))
|
||||
@DummyLineNode.appendChild(document.createElement('span'))
|
||||
@DummyLineNode.children[0].textContent = 'x'
|
||||
@DummyLineNode.children[1].textContent = '我'
|
||||
@DummyLineNode.children[2].textContent = 'ハ'
|
||||
@DummyLineNode.children[3].textContent = '세'
|
||||
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add('lines')
|
||||
@tilesNode = document.createElement("div")
|
||||
# Create a new stacking context, so that tiles z-index does not interfere
|
||||
# with other visual elements.
|
||||
@tilesNode.style.isolation = "isolate"
|
||||
@tilesNode.style.zIndex = 0
|
||||
@domNode.appendChild(@tilesNode)
|
||||
|
||||
@cursorsComponent = new CursorsComponent
|
||||
@domNode.appendChild(@cursorsComponent.getDomNode())
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
shouldRecreateAllTilesOnUpdate: ->
|
||||
@newState.continuousReflow
|
||||
|
||||
beforeUpdateSync: (state) ->
|
||||
if @newState.maxHeight isnt @oldState.maxHeight
|
||||
@domNode.style.height = @newState.maxHeight + 'px'
|
||||
@oldState.maxHeight = @newState.maxHeight
|
||||
|
||||
if @newState.backgroundColor isnt @oldState.backgroundColor
|
||||
@domNode.style.backgroundColor = @newState.backgroundColor
|
||||
@oldState.backgroundColor = @newState.backgroundColor
|
||||
|
||||
afterUpdateSync: (state) ->
|
||||
if @newState.placeholderText isnt @oldState.placeholderText
|
||||
@placeholderTextDiv?.remove()
|
||||
if @newState.placeholderText?
|
||||
@placeholderTextDiv = document.createElement('div')
|
||||
@placeholderTextDiv.classList.add('placeholder-text')
|
||||
@placeholderTextDiv.textContent = @newState.placeholderText
|
||||
@domNode.appendChild(@placeholderTextDiv)
|
||||
@oldState.placeholderText = @newState.placeholderText
|
||||
|
||||
# Removing and updating block decorations needs to be done in two different
|
||||
# steps, so that the same decoration node can be moved from one tile to
|
||||
# another in the same animation frame.
|
||||
for component in @getComponents()
|
||||
component.removeDeletedBlockDecorations()
|
||||
for component in @getComponents()
|
||||
component.updateBlockDecorations()
|
||||
|
||||
@cursorsComponent.updateSync(state)
|
||||
|
||||
buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert, @views})
|
||||
|
||||
buildEmptyState: ->
|
||||
{tiles: {}}
|
||||
|
||||
getNewState: (state) ->
|
||||
state.content
|
||||
|
||||
getTilesNode: -> @tilesNode
|
||||
|
||||
measureLineHeightAndDefaultCharWidth: ->
|
||||
@domNode.appendChild(@DummyLineNode)
|
||||
|
||||
lineHeightInPixels = @DummyLineNode.getBoundingClientRect().height
|
||||
defaultCharWidth = @DummyLineNode.children[0].getBoundingClientRect().width
|
||||
doubleWidthCharWidth = @DummyLineNode.children[1].getBoundingClientRect().width
|
||||
halfWidthCharWidth = @DummyLineNode.children[2].getBoundingClientRect().width
|
||||
koreanCharWidth = @DummyLineNode.children[3].getBoundingClientRect().width
|
||||
|
||||
@domNode.removeChild(@DummyLineNode)
|
||||
|
||||
@presenter.setLineHeight(lineHeightInPixels)
|
||||
@presenter.setBaseCharacterWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
|
||||
|
||||
measureBlockDecorations: ->
|
||||
for component in @getComponents()
|
||||
component.measureBlockDecorations()
|
||||
return
|
||||
|
||||
lineIdForScreenRow: (screenRow) ->
|
||||
tile = @presenter.tileForRow(screenRow)
|
||||
@getComponentForTile(tile)?.lineIdForScreenRow(screenRow)
|
||||
|
||||
lineNodeForScreenRow: (screenRow) ->
|
||||
tile = @presenter.tileForRow(screenRow)
|
||||
@getComponentForTile(tile)?.lineNodeForScreenRow(screenRow)
|
||||
|
||||
textNodesForScreenRow: (screenRow) ->
|
||||
tile = @presenter.tileForRow(screenRow)
|
||||
@getComponentForTile(tile)?.textNodesForScreenRow(screenRow)
|
||||
@@ -1,401 +0,0 @@
|
||||
const HighlightsComponent = require('./highlights-component')
|
||||
const ZERO_WIDTH_NBSP = '\ufeff'
|
||||
|
||||
module.exports = class LinesTileComponent {
|
||||
constructor ({presenter, id, domElementPool, assert, views}) {
|
||||
this.id = id
|
||||
this.presenter = presenter
|
||||
this.views = views
|
||||
this.domElementPool = domElementPool
|
||||
this.assert = assert
|
||||
this.lineNodesByLineId = {}
|
||||
this.screenRowsByLineId = {}
|
||||
this.lineIdsByScreenRow = {}
|
||||
this.textNodesByLineId = {}
|
||||
this.blockDecorationNodesByLineIdAndDecorationId = {}
|
||||
this.domNode = this.domElementPool.buildElement('div')
|
||||
this.domNode.style.position = 'absolute'
|
||||
this.domNode.style.display = 'block'
|
||||
this.highlightsComponent = new HighlightsComponent(this.domElementPool)
|
||||
this.domNode.appendChild(this.highlightsComponent.getDomNode())
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.removeLineNodes()
|
||||
this.domElementPool.freeElementAndDescendants(this.domNode)
|
||||
}
|
||||
|
||||
getDomNode () {
|
||||
return this.domNode
|
||||
}
|
||||
|
||||
updateSync (state) {
|
||||
this.newState = state
|
||||
if (this.oldState == null) {
|
||||
this.oldState = {tiles: {}}
|
||||
this.oldState.tiles[this.id] = {lines: {}}
|
||||
}
|
||||
|
||||
this.newTileState = this.newState.tiles[this.id]
|
||||
this.oldTileState = this.oldState.tiles[this.id]
|
||||
|
||||
if (this.newState.backgroundColor !== this.oldState.backgroundColor) {
|
||||
this.domNode.style.backgroundColor = this.newState.backgroundColor
|
||||
this.oldState.backgroundColor = this.newState.backgroundColor
|
||||
}
|
||||
|
||||
if (this.newTileState.zIndex !== this.oldTileState.zIndex) {
|
||||
this.domNode.style.zIndex = this.newTileState.zIndex
|
||||
this.oldTileState.zIndex = this.newTileState.zIndex
|
||||
}
|
||||
|
||||
if (this.newTileState.display !== this.oldTileState.display) {
|
||||
this.domNode.style.display = this.newTileState.display
|
||||
this.oldTileState.display = this.newTileState.display
|
||||
}
|
||||
|
||||
if (this.newTileState.height !== this.oldTileState.height) {
|
||||
this.domNode.style.height = this.newTileState.height + 'px'
|
||||
this.oldTileState.height = this.newTileState.height
|
||||
}
|
||||
|
||||
if (this.newState.width !== this.oldState.width) {
|
||||
this.domNode.style.width = this.newState.width + 'px'
|
||||
this.oldState.width = this.newState.width
|
||||
}
|
||||
|
||||
if (this.newTileState.top !== this.oldTileState.top || this.newTileState.left !== this.oldTileState.left) {
|
||||
this.domNode.style.transform = `translate3d(${this.newTileState.left}px, ${this.newTileState.top}px, 0px)`
|
||||
this.oldTileState.top = this.newTileState.top
|
||||
this.oldTileState.left = this.newTileState.left
|
||||
}
|
||||
|
||||
this.updateLineNodes()
|
||||
this.highlightsComponent.updateSync(this.newTileState)
|
||||
}
|
||||
|
||||
removeLineNodes () {
|
||||
for (const id of Object.keys(this.oldTileState.lines)) {
|
||||
this.removeLineNode(id)
|
||||
}
|
||||
}
|
||||
|
||||
removeLineNode (lineId) {
|
||||
this.domElementPool.freeElementAndDescendants(this.lineNodesByLineId[lineId])
|
||||
for (const decorationId of Object.keys(this.oldTileState.lines[lineId].precedingBlockDecorations)) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
topRulerNode.remove()
|
||||
blockDecorationNode.remove()
|
||||
bottomRulerNode.remove()
|
||||
}
|
||||
for (const decorationId of Object.keys(this.oldTileState.lines[lineId].followingBlockDecorations)) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
topRulerNode.remove()
|
||||
blockDecorationNode.remove()
|
||||
bottomRulerNode.remove()
|
||||
}
|
||||
|
||||
delete this.blockDecorationNodesByLineIdAndDecorationId[lineId]
|
||||
delete this.lineNodesByLineId[lineId]
|
||||
delete this.textNodesByLineId[lineId]
|
||||
delete this.lineIdsByScreenRow[this.screenRowsByLineId[lineId]]
|
||||
delete this.screenRowsByLineId[lineId]
|
||||
delete this.oldTileState.lines[lineId]
|
||||
}
|
||||
|
||||
updateLineNodes () {
|
||||
for (const id of Object.keys(this.oldTileState.lines)) {
|
||||
if (!this.newTileState.lines.hasOwnProperty(id)) {
|
||||
this.removeLineNode(id)
|
||||
}
|
||||
}
|
||||
|
||||
const newLineIds = []
|
||||
const newLineNodes = []
|
||||
for (const id of Object.keys(this.newTileState.lines)) {
|
||||
const lineState = this.newTileState.lines[id]
|
||||
if (this.oldTileState.lines.hasOwnProperty(id)) {
|
||||
this.updateLineNode(id)
|
||||
} else {
|
||||
newLineIds.push(id)
|
||||
newLineNodes.push(this.buildLineNode(id))
|
||||
this.screenRowsByLineId[id] = lineState.screenRow
|
||||
this.lineIdsByScreenRow[lineState.screenRow] = id
|
||||
this.oldTileState.lines[id] = Object.assign({}, lineState)
|
||||
// Avoid assigning state for block decorations, because we need to
|
||||
// process it later when updating the DOM.
|
||||
this.oldTileState.lines[id].precedingBlockDecorations = {}
|
||||
this.oldTileState.lines[id].followingBlockDecorations = {}
|
||||
}
|
||||
}
|
||||
|
||||
while (newLineIds.length > 0) {
|
||||
const id = newLineIds.shift()
|
||||
const lineNode = newLineNodes.shift()
|
||||
this.lineNodesByLineId[id] = lineNode
|
||||
const nextNode = this.findNodeNextTo(lineNode)
|
||||
if (nextNode == null) {
|
||||
this.domNode.appendChild(lineNode)
|
||||
} else {
|
||||
this.domNode.insertBefore(lineNode, nextNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findNodeNextTo (node) {
|
||||
let i = 1 // skip highlights node
|
||||
while (i < this.domNode.children.length) {
|
||||
const nextNode = this.domNode.children[i]
|
||||
if (this.screenRowForNode(node) < this.screenRowForNode(nextNode)) {
|
||||
return nextNode
|
||||
}
|
||||
i++
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
screenRowForNode (node) {
|
||||
return parseInt(node.dataset.screenRow)
|
||||
}
|
||||
|
||||
buildLineNode (id) {
|
||||
const {lineText, tagCodes, screenRow, decorationClasses} = this.newTileState.lines[id]
|
||||
|
||||
const lineNode = this.domElementPool.buildElement('div', 'line')
|
||||
lineNode.dataset.screenRow = screenRow
|
||||
if (decorationClasses != null) {
|
||||
for (const decorationClass of decorationClasses) {
|
||||
lineNode.classList.add(decorationClass)
|
||||
}
|
||||
}
|
||||
|
||||
const textNodes = []
|
||||
let startIndex = 0
|
||||
let openScopeNode = lineNode
|
||||
for (const tagCode of tagCodes) {
|
||||
if (tagCode !== 0) {
|
||||
if (this.presenter.isCloseTagCode(tagCode)) {
|
||||
openScopeNode = openScopeNode.parentElement
|
||||
} else if (this.presenter.isOpenTagCode(tagCode)) {
|
||||
const scope = this.presenter.tagForCode(tagCode)
|
||||
const newScopeNode = this.domElementPool.buildElement('span', scope.replace(/\.+/g, ' '))
|
||||
openScopeNode.appendChild(newScopeNode)
|
||||
openScopeNode = newScopeNode
|
||||
} else {
|
||||
const textNode = this.domElementPool.buildText(lineText.substr(startIndex, tagCode))
|
||||
startIndex += tagCode
|
||||
openScopeNode.appendChild(textNode)
|
||||
textNodes.push(textNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startIndex === 0) {
|
||||
const textNode = this.domElementPool.buildText(' ')
|
||||
lineNode.appendChild(textNode)
|
||||
textNodes.push(textNode)
|
||||
}
|
||||
|
||||
if (lineText.endsWith(this.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.
|
||||
const textNode = this.domElementPool.buildText(ZERO_WIDTH_NBSP)
|
||||
lineNode.appendChild(textNode)
|
||||
textNodes.push(textNode)
|
||||
}
|
||||
|
||||
this.textNodesByLineId[id] = textNodes
|
||||
return lineNode
|
||||
}
|
||||
|
||||
updateLineNode (id) {
|
||||
const oldLineState = this.oldTileState.lines[id]
|
||||
const newLineState = this.newTileState.lines[id]
|
||||
const lineNode = this.lineNodesByLineId[id]
|
||||
const newDecorationClasses = newLineState.decorationClasses
|
||||
const oldDecorationClasses = oldLineState.decorationClasses
|
||||
|
||||
if (oldDecorationClasses != null) {
|
||||
for (const decorationClass of oldDecorationClasses) {
|
||||
if (newDecorationClasses == null || !newDecorationClasses.includes(decorationClass)) {
|
||||
lineNode.classList.remove(decorationClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newDecorationClasses != null) {
|
||||
for (const decorationClass of newDecorationClasses) {
|
||||
if (oldDecorationClasses == null || !oldDecorationClasses.includes(decorationClass)) {
|
||||
lineNode.classList.add(decorationClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oldLineState.decorationClasses = newLineState.decorationClasses
|
||||
|
||||
if (newLineState.screenRow !== oldLineState.screenRow) {
|
||||
lineNode.dataset.screenRow = newLineState.screenRow
|
||||
this.lineIdsByScreenRow[newLineState.screenRow] = id
|
||||
this.screenRowsByLineId[id] = newLineState.screenRow
|
||||
}
|
||||
|
||||
oldLineState.screenRow = newLineState.screenRow
|
||||
}
|
||||
|
||||
removeDeletedBlockDecorations () {
|
||||
for (const lineId of Object.keys(this.newTileState.lines)) {
|
||||
const oldLineState = this.oldTileState.lines[lineId]
|
||||
const newLineState = this.newTileState.lines[lineId]
|
||||
for (const decorationId of Object.keys(oldLineState.precedingBlockDecorations)) {
|
||||
if (!newLineState.precedingBlockDecorations.hasOwnProperty(decorationId)) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
topRulerNode.remove()
|
||||
blockDecorationNode.remove()
|
||||
bottomRulerNode.remove()
|
||||
delete this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
delete oldLineState.precedingBlockDecorations[decorationId]
|
||||
}
|
||||
}
|
||||
for (const decorationId of Object.keys(oldLineState.followingBlockDecorations)) {
|
||||
if (!newLineState.followingBlockDecorations.hasOwnProperty(decorationId)) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
topRulerNode.remove()
|
||||
blockDecorationNode.remove()
|
||||
bottomRulerNode.remove()
|
||||
delete this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
delete oldLineState.followingBlockDecorations[decorationId]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateBlockDecorations () {
|
||||
for (const lineId of Object.keys(this.newTileState.lines)) {
|
||||
const oldLineState = this.oldTileState.lines[lineId]
|
||||
const newLineState = this.newTileState.lines[lineId]
|
||||
const lineNode = this.lineNodesByLineId[lineId]
|
||||
if (!this.blockDecorationNodesByLineIdAndDecorationId.hasOwnProperty(lineId)) {
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId] = {}
|
||||
}
|
||||
for (const decorationId of Object.keys(newLineState.precedingBlockDecorations)) {
|
||||
const oldBlockDecorationState = oldLineState.precedingBlockDecorations[decorationId]
|
||||
const newBlockDecorationState = newLineState.precedingBlockDecorations[decorationId]
|
||||
if (oldBlockDecorationState != null) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
if (oldBlockDecorationState.screenRow !== newBlockDecorationState.screenRow) {
|
||||
topRulerNode.remove()
|
||||
blockDecorationNode.remove()
|
||||
bottomRulerNode.remove()
|
||||
topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(topRulerNode, lineNode)
|
||||
blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(blockDecorationNode, lineNode)
|
||||
bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(bottomRulerNode, lineNode)
|
||||
}
|
||||
} else {
|
||||
const topRulerNode = document.createElement('div')
|
||||
topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(topRulerNode, lineNode)
|
||||
const blockDecorationNode = this.views.getView(newBlockDecorationState.decoration.getProperties().item)
|
||||
blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(blockDecorationNode, lineNode)
|
||||
const bottomRulerNode = document.createElement('div')
|
||||
bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(bottomRulerNode, lineNode)
|
||||
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] =
|
||||
{topRulerNode, blockDecorationNode, bottomRulerNode}
|
||||
}
|
||||
oldLineState.precedingBlockDecorations[decorationId] = Object.assign({}, newBlockDecorationState)
|
||||
}
|
||||
for (const decorationId of Object.keys(newLineState.followingBlockDecorations)) {
|
||||
const oldBlockDecorationState = oldLineState.followingBlockDecorations[decorationId]
|
||||
const newBlockDecorationState = newLineState.followingBlockDecorations[decorationId]
|
||||
if (oldBlockDecorationState != null) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
if (oldBlockDecorationState.screenRow !== newBlockDecorationState.screenRow) {
|
||||
topRulerNode.remove()
|
||||
blockDecorationNode.remove()
|
||||
bottomRulerNode.remove()
|
||||
bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(bottomRulerNode, lineNode.nextSibling)
|
||||
blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(blockDecorationNode, lineNode.nextSibling)
|
||||
topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(topRulerNode, lineNode.nextSibling)
|
||||
}
|
||||
} else {
|
||||
const bottomRulerNode = document.createElement('div')
|
||||
bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(bottomRulerNode, lineNode.nextSibling)
|
||||
const blockDecorationNode = this.views.getView(newBlockDecorationState.decoration.getProperties().item)
|
||||
blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(blockDecorationNode, lineNode.nextSibling)
|
||||
const topRulerNode = document.createElement('div')
|
||||
topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(topRulerNode, lineNode.nextSibling)
|
||||
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] =
|
||||
{topRulerNode, blockDecorationNode, bottomRulerNode}
|
||||
}
|
||||
oldLineState.followingBlockDecorations[decorationId] = Object.assign({}, newBlockDecorationState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
measureBlockDecorations () {
|
||||
for (const lineId of Object.keys(this.newTileState.lines)) {
|
||||
const newLineState = this.newTileState.lines[lineId]
|
||||
|
||||
for (const decorationId of Object.keys(newLineState.precedingBlockDecorations)) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
const width = blockDecorationNode.offsetWidth
|
||||
const height = bottomRulerNode.offsetTop - topRulerNode.offsetTop
|
||||
const {decoration} = newLineState.precedingBlockDecorations[decorationId]
|
||||
this.presenter.setBlockDecorationDimensions(decoration, width, height)
|
||||
}
|
||||
for (const decorationId of Object.keys(newLineState.followingBlockDecorations)) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
const width = blockDecorationNode.offsetWidth
|
||||
const height = bottomRulerNode.offsetTop - topRulerNode.offsetTop
|
||||
const {decoration} = newLineState.followingBlockDecorations[decorationId]
|
||||
this.presenter.setBlockDecorationDimensions(decoration, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lineNodeForScreenRow (screenRow) {
|
||||
return this.lineNodesByLineId[this.lineIdsByScreenRow[screenRow]]
|
||||
}
|
||||
|
||||
lineNodeForLineId (lineId) {
|
||||
return this.lineNodesByLineId[lineId]
|
||||
}
|
||||
|
||||
textNodesForLineId (lineId) {
|
||||
return this.textNodesByLineId[lineId].slice()
|
||||
}
|
||||
|
||||
lineIdForScreenRow (screenRow) {
|
||||
return this.lineIdsByScreenRow[screenRow]
|
||||
}
|
||||
|
||||
textNodesForScreenRow (screenRow) {
|
||||
const textNodes = this.textNodesByLineId[this.lineIdsByScreenRow[screenRow]]
|
||||
if (textNodes == null) {
|
||||
return null
|
||||
} else {
|
||||
return textNodes.slice()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
{Point} = require 'text-buffer'
|
||||
{isPairedCharacter} = require './text-utils'
|
||||
|
||||
module.exports =
|
||||
class LinesYardstick
|
||||
constructor: (@model, @lineNodesProvider, @lineTopIndex) ->
|
||||
@rangeForMeasurement = document.createRange()
|
||||
@invalidateCache()
|
||||
|
||||
invalidateCache: ->
|
||||
@leftPixelPositionCache = {}
|
||||
|
||||
measuredRowForPixelPosition: (pixelPosition) ->
|
||||
targetTop = pixelPosition.top
|
||||
row = Math.floor(targetTop / @model.getLineHeightInPixels())
|
||||
row if 0 <= row
|
||||
|
||||
screenPositionForPixelPosition: (pixelPosition) ->
|
||||
targetTop = pixelPosition.top
|
||||
row = Math.max(0, @lineTopIndex.rowForPixelPosition(targetTop))
|
||||
lineNode = @lineNodesProvider.lineNodeForScreenRow(row)
|
||||
unless lineNode
|
||||
lastScreenRow = @model.getLastScreenRow()
|
||||
if row > lastScreenRow
|
||||
return Point(lastScreenRow, @model.lineLengthForScreenRow(lastScreenRow))
|
||||
else
|
||||
return Point(row, 0)
|
||||
|
||||
targetLeft = pixelPosition.left
|
||||
targetLeft = 0 if targetTop < 0 or targetLeft < 0
|
||||
|
||||
textNodes = @lineNodesProvider.textNodesForScreenRow(row)
|
||||
lineOffset = lineNode.getBoundingClientRect().left
|
||||
targetLeft += lineOffset
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
characterIndex = nextCharIndex
|
||||
break
|
||||
|
||||
textNodeStartColumn = 0
|
||||
textNodeStartColumn += textNodes[i].length for i in [0...textNodeIndex] by 1
|
||||
Point(row, textNodeStartColumn + characterIndex)
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition) ->
|
||||
targetRow = screenPosition.row
|
||||
targetColumn = screenPosition.column
|
||||
|
||||
top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow)
|
||||
left = @leftPixelPositionForScreenPosition(targetRow, targetColumn)
|
||||
|
||||
{top, left}
|
||||
|
||||
leftPixelPositionForScreenPosition: (row, column) ->
|
||||
lineNode = @lineNodesProvider.lineNodeForScreenRow(row)
|
||||
lineId = @lineNodesProvider.lineIdForScreenRow(row)
|
||||
|
||||
if lineNode?
|
||||
if @leftPixelPositionCache[lineId]?[column]?
|
||||
@leftPixelPositionCache[lineId][column]
|
||||
else
|
||||
textNodes = @lineNodesProvider.textNodesForScreenRow(row)
|
||||
textNodeStartColumn = 0
|
||||
for textNode in textNodes
|
||||
textNodeEndColumn = textNodeStartColumn + textNode.textContent.length
|
||||
if textNodeEndColumn > column
|
||||
indexInTextNode = column - textNodeStartColumn
|
||||
break
|
||||
else
|
||||
textNodeStartColumn = textNodeEndColumn
|
||||
|
||||
if textNode?
|
||||
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
|
||||
else
|
||||
0
|
||||
|
||||
clientRectForRange: (textNode, startIndex, endIndex) ->
|
||||
@rangeForMeasurement.setStart(textNode, startIndex)
|
||||
@rangeForMeasurement.setEnd(textNode, endIndex)
|
||||
clientRects = @rangeForMeasurement.getClientRects()
|
||||
if clientRects.length is 1
|
||||
clientRects[0]
|
||||
else
|
||||
@rangeForMeasurement.getBoundingClientRect()
|
||||
@@ -237,8 +237,8 @@ class AtomApplication
|
||||
@on 'application:open-discussions', -> shell.openExternal('https://discuss.atom.io')
|
||||
@on 'application:open-faq', -> shell.openExternal('https://atom.io/faq')
|
||||
@on 'application:open-terms-of-use', -> shell.openExternal('https://atom.io/terms')
|
||||
@on 'application:report-issue', -> shell.openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#submitting-issues')
|
||||
@on 'application:search-issues', -> shell.openExternal('https://github.com/issues?q=+is%3Aissue+user%3Aatom')
|
||||
@on 'application:report-issue', -> shell.openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#reporting-bugs')
|
||||
@on 'application:search-issues', -> shell.openExternal('https://github.com/search?q=+is%3Aissue+user%3Aatom')
|
||||
|
||||
@on 'application:install-update', =>
|
||||
@quitting = true
|
||||
@@ -644,7 +644,7 @@ class AtomApplication
|
||||
openUrl: ({urlToOpen, devMode, safeMode, env}) ->
|
||||
unless @packages?
|
||||
PackageManager = require '../package-manager'
|
||||
@packages = new PackageManager()
|
||||
@packages = new PackageManager({})
|
||||
@packages.initialize
|
||||
configDirPath: process.env.ATOM_HOME
|
||||
devMode: devMode
|
||||
|
||||
@@ -26,17 +26,16 @@ class AtomWindow
|
||||
options =
|
||||
show: false
|
||||
title: 'Atom'
|
||||
# Add an opaque backgroundColor (instead of keeping the default
|
||||
# transparent one) to prevent subpixel anti-aliasing from being disabled.
|
||||
# We believe this is a regression introduced with Electron 0.37.3, and
|
||||
# thus we should remove this as soon as a fix gets released.
|
||||
backgroundColor: "#fff"
|
||||
webPreferences:
|
||||
# Prevent specs from throttling when the window is in the background:
|
||||
# this should result in faster CI builds, and an improvement in the
|
||||
# local development experience when running specs through the UI (which
|
||||
# now won't pause when e.g. minimizing the window).
|
||||
backgroundThrottling: not @isSpec
|
||||
# Disable the `auxclick` feature so that `click` events are triggered in
|
||||
# response to a middle-click.
|
||||
# (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960)
|
||||
disableBlinkFeatures: 'Auxclick'
|
||||
|
||||
# Don't set icon on Windows so the exe's ico will be used as window and
|
||||
# taskbar's icon. See https://github.com/atom/atom/issues/4811 for more.
|
||||
@@ -315,4 +314,4 @@ class AtomWindow
|
||||
copy: -> @browserWindow.copy()
|
||||
|
||||
disableZoom: ->
|
||||
@browserWindow.webContents.setZoomLevelLimits(1, 1)
|
||||
@browserWindow.webContents.setVisualZoomLevelLimits(1, 1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
if (typeof snapshotResult !== 'undefined') {
|
||||
snapshotResult.setGlobals(global, process, global, {}, console, require) // eslint-disable-line no-undef
|
||||
snapshotResult.setGlobals(global, process, global, {}, console, require)
|
||||
}
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
@@ -22,6 +22,8 @@ module.exports = function start (resourcePath, startTime) {
|
||||
const previousConsoleLog = console.log
|
||||
console.log = nslog
|
||||
|
||||
app.commandLine.appendSwitch('enable-experimental-web-platform-features')
|
||||
|
||||
const args = parseCommandLine(process.argv.slice(1))
|
||||
atomPaths.setAtomHome(app.getPath('home'))
|
||||
atomPaths.setUserData(app)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
module.exports =
|
||||
class MarkerObservationWindow
|
||||
constructor: (@decorationManager, @bufferWindow) ->
|
||||
|
||||
setScreenRange: (range) ->
|
||||
@bufferWindow.setRange(@decorationManager.bufferRangeForScreenRange(range))
|
||||
|
||||
setBufferRange: (range) ->
|
||||
@bufferWindow.setRange(range)
|
||||
|
||||
destroy: ->
|
||||
@bufferWindow.destroy()
|
||||
@@ -83,7 +83,7 @@ class NativeCompileCache {
|
||||
compiledWrapper = compilationResult.result
|
||||
}
|
||||
|
||||
let args = [moduleSelf.exports, require, moduleSelf, filename, dirname, process, global]
|
||||
let args = [moduleSelf.exports, require, moduleSelf, filename, dirname, process, global, Buffer]
|
||||
return compiledWrapper.apply(moduleSelf.exports, args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
module.exports = class OffScreenBlockDecorationsComponent {
|
||||
constructor ({presenter, views}) {
|
||||
this.presenter = presenter
|
||||
this.views = views
|
||||
this.newState = {offScreenBlockDecorations: {}, width: 0}
|
||||
this.oldState = {offScreenBlockDecorations: {}, width: 0}
|
||||
this.domNode = document.createElement('div')
|
||||
this.domNode.style.visibility = 'hidden'
|
||||
this.domNode.style.position = 'absolute'
|
||||
this.blockDecorationNodesById = {}
|
||||
}
|
||||
|
||||
getDomNode () {
|
||||
return this.domNode
|
||||
}
|
||||
|
||||
updateSync (state) {
|
||||
this.newState = state.content
|
||||
|
||||
if (this.newState.width !== this.oldState.width) {
|
||||
this.domNode.style.width = `${this.newState.width}px`
|
||||
this.oldState.width = this.newState.width
|
||||
}
|
||||
|
||||
for (const id of Object.keys(this.oldState.offScreenBlockDecorations)) {
|
||||
if (!this.newState.offScreenBlockDecorations.hasOwnProperty(id)) {
|
||||
const {topRuler, blockDecoration, bottomRuler} = this.blockDecorationNodesById[id]
|
||||
topRuler.remove()
|
||||
blockDecoration.remove()
|
||||
bottomRuler.remove()
|
||||
delete this.blockDecorationNodesById[id]
|
||||
delete this.oldState.offScreenBlockDecorations[id]
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of Object.keys(this.newState.offScreenBlockDecorations)) {
|
||||
const decoration = this.newState.offScreenBlockDecorations[id]
|
||||
if (!this.oldState.offScreenBlockDecorations.hasOwnProperty(id)) {
|
||||
const topRuler = document.createElement('div')
|
||||
this.domNode.appendChild(topRuler)
|
||||
const blockDecoration = this.views.getView(decoration.getProperties().item)
|
||||
this.domNode.appendChild(blockDecoration)
|
||||
const bottomRuler = document.createElement('div')
|
||||
this.domNode.appendChild(bottomRuler)
|
||||
|
||||
this.blockDecorationNodesById[id] = {topRuler, blockDecoration, bottomRuler}
|
||||
}
|
||||
|
||||
this.oldState.offScreenBlockDecorations[id] = decoration
|
||||
}
|
||||
}
|
||||
|
||||
measureBlockDecorations () {
|
||||
for (const id of Object.keys(this.blockDecorationNodesById)) {
|
||||
const {topRuler, blockDecoration, bottomRuler} = this.blockDecorationNodesById[id]
|
||||
const width = blockDecoration.offsetWidth
|
||||
const height = bottomRuler.offsetTop - topRuler.offsetTop
|
||||
const decoration = this.newState.offScreenBlockDecorations[id]
|
||||
this.presenter.setBlockDecorationDimensions(decoration, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
ElementResizeDetector = require('element-resize-detector')
|
||||
elementResizeDetector = null
|
||||
|
||||
module.exports =
|
||||
class OverlayManager
|
||||
constructor: (@presenter, @container, @views) ->
|
||||
@@ -12,6 +15,7 @@ class OverlayManager
|
||||
unless state.content.overlays.hasOwnProperty(id)
|
||||
delete @overlaysById[id]
|
||||
overlayNode.remove()
|
||||
elementResizeDetector.uninstall(overlayNode)
|
||||
|
||||
shouldUpdateOverlay: (decorationId, overlay) ->
|
||||
cachedOverlay = @overlaysById[decorationId]
|
||||
@@ -19,10 +23,6 @@ class OverlayManager
|
||||
cachedOverlay.pixelPosition?.top isnt overlay.pixelPosition?.top or
|
||||
cachedOverlay.pixelPosition?.left isnt overlay.pixelPosition?.left
|
||||
|
||||
measureOverlays: ->
|
||||
for decorationId, {itemView} of @overlaysById
|
||||
@measureOverlay(decorationId, itemView)
|
||||
|
||||
measureOverlay: (decorationId, itemView) ->
|
||||
contentMargin = parseInt(getComputedStyle(itemView)['margin-left']) ? 0
|
||||
@presenter.setOverlayDimensions(decorationId, itemView.offsetWidth, itemView.offsetHeight, contentMargin)
|
||||
@@ -33,13 +33,20 @@ class OverlayManager
|
||||
unless overlayNode = cachedOverlay?.overlayNode
|
||||
overlayNode = document.createElement('atom-overlay')
|
||||
overlayNode.classList.add(klass) if klass?
|
||||
elementResizeDetector ?= ElementResizeDetector({strategy: 'scroll'})
|
||||
elementResizeDetector.listenTo(overlayNode, =>
|
||||
if overlayNode.parentElement?
|
||||
@measureOverlay(decorationId, itemView)
|
||||
)
|
||||
@container.appendChild(overlayNode)
|
||||
@overlaysById[decorationId] = cachedOverlay = {overlayNode, itemView}
|
||||
|
||||
# The same node may be used in more than one overlay. This steals the node
|
||||
# back if it has been displayed in another overlay.
|
||||
overlayNode.appendChild(itemView) if overlayNode.childNodes.length is 0
|
||||
overlayNode.appendChild(itemView) unless overlayNode.contains(itemView)
|
||||
|
||||
cachedOverlay.pixelPosition = pixelPosition
|
||||
overlayNode.style.top = pixelPosition.top + 'px'
|
||||
overlayNode.style.left = pixelPosition.left + 'px'
|
||||
|
||||
@measureOverlay(decorationId, itemView)
|
||||
|
||||
@@ -73,6 +73,8 @@ class PackageManager
|
||||
@loadedPackages = {}
|
||||
@preloadedPackages = {}
|
||||
@packageStates = {}
|
||||
@packagesCache = packageJSON._atomPackages ? {}
|
||||
@packageDependencies = packageJSON.packageDependencies ? {}
|
||||
@triggeredActivationHooks.clear()
|
||||
|
||||
###
|
||||
|
||||
@@ -29,6 +29,13 @@ class PackageTranspilationRegistry {
|
||||
|
||||
removeTranspilerConfigForPath (packagePath) {
|
||||
delete this.configByPackagePath[packagePath]
|
||||
const packagePathWithSep = packagePath.endsWith(path.sep)
|
||||
? path.join(packagePath) : path.join(packagePath) + path.sep
|
||||
Object.keys(this.specByFilePath).forEach(filePath => {
|
||||
if (path.join(filePath).startsWith(packagePathWithSep)) {
|
||||
delete this.specByFilePath[filePath]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Wraps the transpiler in an object with the same interface
|
||||
|
||||
@@ -751,7 +751,7 @@ class Package
|
||||
"installed-packages:#{@name}:#{@metadata.version}:build-error"
|
||||
|
||||
getIncompatibleNativeModulesStorageKey: ->
|
||||
electronVersion = process.versions['electron'] ? process.versions['atom-shell']
|
||||
electronVersion = process.versions.electron
|
||||
"installed-packages:#{@name}:#{@metadata.version}:electron-#{electronVersion}:incompatible-native-modules"
|
||||
|
||||
getCanDeferMainModuleRequireStorageKey: ->
|
||||
|
||||
@@ -27,7 +27,7 @@ class PaneElement extends HTMLElement
|
||||
|
||||
subscribeToDOMEvents: ->
|
||||
handleFocus = (event) =>
|
||||
@model.focus() unless @isActivating or @contains(event.relatedTarget)
|
||||
@model.focus() unless @isActivating or @model.isDestroyed() or @contains(event.relatedTarget)
|
||||
if event.target is this and view = @getActiveView()
|
||||
view.focus()
|
||||
event.stopPropagation()
|
||||
@@ -67,7 +67,7 @@ class PaneElement extends HTMLElement
|
||||
|
||||
activated: ->
|
||||
@isActivating = true
|
||||
@focus()
|
||||
@focus() unless @hasFocus() # Don't steal focus from children.
|
||||
@isActivating = false
|
||||
|
||||
activeStatusChanged: (active) ->
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
module.exports =
|
||||
class ScrollbarComponent
|
||||
constructor: ({@orientation, @onScroll}) ->
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add "#{@orientation}-scrollbar"
|
||||
@domNode.style['-webkit-transform'] = 'translateZ(0)' # See atom/atom#3559
|
||||
@domNode.style.left = 0 if @orientation is 'horizontal'
|
||||
|
||||
@contentNode = document.createElement('div')
|
||||
@contentNode.classList.add "scrollbar-content"
|
||||
@domNode.appendChild(@contentNode)
|
||||
|
||||
@domNode.addEventListener 'scroll', @onScrollCallback
|
||||
|
||||
destroy: ->
|
||||
@domNode.removeEventListener 'scroll', @onScrollCallback
|
||||
@onScroll = null
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
@oldState ?= {}
|
||||
switch @orientation
|
||||
when 'vertical'
|
||||
@newState = state.verticalScrollbar
|
||||
@updateVertical()
|
||||
when 'horizontal'
|
||||
@newState = state.horizontalScrollbar
|
||||
@updateHorizontal()
|
||||
|
||||
if @newState.visible isnt @oldState.visible
|
||||
if @newState.visible
|
||||
@domNode.style.display = ''
|
||||
else
|
||||
@domNode.style.display = 'none'
|
||||
@oldState.visible = @newState.visible
|
||||
|
||||
updateVertical: ->
|
||||
if @newState.width isnt @oldState.width
|
||||
@domNode.style.width = @newState.width + 'px'
|
||||
@oldState.width = @newState.width
|
||||
|
||||
if @newState.bottom isnt @oldState.bottom
|
||||
@domNode.style.bottom = @newState.bottom + 'px'
|
||||
@oldState.bottom = @newState.bottom
|
||||
|
||||
if @newState.scrollHeight isnt @oldState.scrollHeight
|
||||
@contentNode.style.height = @newState.scrollHeight + 'px'
|
||||
@oldState.scrollHeight = @newState.scrollHeight
|
||||
|
||||
if @newState.scrollTop isnt @oldState.scrollTop
|
||||
@domNode.scrollTop = @newState.scrollTop
|
||||
@oldState.scrollTop = @newState.scrollTop
|
||||
|
||||
updateHorizontal: ->
|
||||
if @newState.height isnt @oldState.height
|
||||
@domNode.style.height = @newState.height + 'px'
|
||||
@oldState.height = @newState.height
|
||||
|
||||
if @newState.right isnt @oldState.right
|
||||
@domNode.style.right = @newState.right + 'px'
|
||||
@oldState.right = @newState.right
|
||||
|
||||
if @newState.scrollWidth isnt @oldState.scrollWidth
|
||||
@contentNode.style.width = @newState.scrollWidth + 'px'
|
||||
@oldState.scrollWidth = @newState.scrollWidth
|
||||
|
||||
if @newState.scrollLeft isnt @oldState.scrollLeft
|
||||
@domNode.scrollLeft = @newState.scrollLeft
|
||||
@oldState.scrollLeft = @newState.scrollLeft
|
||||
|
||||
|
||||
onScrollCallback: =>
|
||||
switch @orientation
|
||||
when 'vertical'
|
||||
@onScroll(@domNode.scrollTop)
|
||||
when 'horizontal'
|
||||
@onScroll(@domNode.scrollLeft)
|
||||
@@ -1,38 +0,0 @@
|
||||
module.exports =
|
||||
class ScrollbarCornerComponent
|
||||
constructor: ->
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add('scrollbar-corner')
|
||||
|
||||
@contentNode = document.createElement('div')
|
||||
@domNode.appendChild(@contentNode)
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
@oldState ?= {}
|
||||
@newState ?= {}
|
||||
|
||||
newHorizontalState = state.horizontalScrollbar
|
||||
newVerticalState = state.verticalScrollbar
|
||||
@newState.visible = newHorizontalState.visible and newVerticalState.visible
|
||||
@newState.height = newHorizontalState.height
|
||||
@newState.width = newVerticalState.width
|
||||
|
||||
if @newState.visible isnt @oldState.visible
|
||||
if @newState.visible
|
||||
@domNode.style.display = ''
|
||||
else
|
||||
@domNode.style.display = 'none'
|
||||
@oldState.visible = @newState.visible
|
||||
|
||||
if @newState.height isnt @oldState.height
|
||||
@domNode.style.height = @newState.height + 'px'
|
||||
@contentNode.style.height = @newState.height + 1 + 'px'
|
||||
@oldState.height = @newState.height
|
||||
|
||||
if @newState.width isnt @oldState.width
|
||||
@domNode.style.width = @newState.width + 'px'
|
||||
@contentNode.style.width = @newState.width + 1 + 'px'
|
||||
@oldState.width = @newState.width
|
||||
@@ -769,8 +769,6 @@ class Selection extends Model
|
||||
{oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e
|
||||
{textChanged} = e
|
||||
|
||||
@cursor.updateVisibility()
|
||||
|
||||
unless oldHeadScreenPosition.isEqual(newHeadScreenPosition)
|
||||
@cursor.goalColumn = null
|
||||
cursorMovedEvent = {
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
{userAgent, taskPath} = process.env
|
||||
handler = null
|
||||
|
||||
setupGlobals = ->
|
||||
global.attachEvent = ->
|
||||
console =
|
||||
warn: -> emit 'task:warn', arguments...
|
||||
log: -> emit 'task:log', arguments...
|
||||
error: -> emit 'task:error', arguments...
|
||||
trace: ->
|
||||
global.__defineGetter__ 'console', -> console
|
||||
|
||||
global.document =
|
||||
createElement: ->
|
||||
setAttribute: ->
|
||||
getElementsByTagName: -> []
|
||||
appendChild: ->
|
||||
documentElement:
|
||||
insertBefore: ->
|
||||
removeChild: ->
|
||||
getElementById: -> {}
|
||||
createComment: -> {}
|
||||
createDocumentFragment: -> {}
|
||||
|
||||
global.emit = (event, args...) ->
|
||||
process.send({event, args})
|
||||
global.navigator = {userAgent}
|
||||
global.window = global
|
||||
|
||||
handleEvents = ->
|
||||
process.on 'uncaughtException', (error) ->
|
||||
console.error(error.message, error.stack)
|
||||
process.on 'message', ({event, args}={}) ->
|
||||
return unless event is 'start'
|
||||
|
||||
isAsync = false
|
||||
async = ->
|
||||
isAsync = true
|
||||
(result) ->
|
||||
emit('task:completed', result)
|
||||
result = handler.bind({async})(args...)
|
||||
emit('task:completed', result) unless isAsync
|
||||
|
||||
setupDeprecations = ->
|
||||
Grim = require 'grim'
|
||||
Grim.on 'updated', ->
|
||||
deprecations = Grim.getDeprecations().map (deprecation) -> deprecation.serialize()
|
||||
Grim.clearDeprecations()
|
||||
emit('task:deprecations', deprecations)
|
||||
|
||||
setupGlobals()
|
||||
handleEvents()
|
||||
setupDeprecations()
|
||||
handler = require(taskPath)
|
||||
68
src/task-bootstrap.js
Normal file
68
src/task-bootstrap.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const {userAgent} = process.env
|
||||
const [compileCachePath, taskPath] = process.argv.slice(2)
|
||||
|
||||
const CompileCache = require('./compile-cache')
|
||||
CompileCache.setCacheDirectory(compileCachePath)
|
||||
CompileCache.install(`${process.resourcesPath}`, require)
|
||||
|
||||
const setupGlobals = function () {
|
||||
global.attachEvent = function () {}
|
||||
const console = {
|
||||
warn () { return global.emit('task:warn', ...arguments) },
|
||||
log () { return global.emit('task:log', ...arguments) },
|
||||
error () { return global.emit('task:error', ...arguments) },
|
||||
trace () {}
|
||||
}
|
||||
global.__defineGetter__('console', () => console)
|
||||
|
||||
global.document = {
|
||||
createElement () {
|
||||
return {
|
||||
setAttribute () {},
|
||||
getElementsByTagName () { return [] },
|
||||
appendChild () {}
|
||||
}
|
||||
},
|
||||
documentElement: {
|
||||
insertBefore () {},
|
||||
removeChild () {}
|
||||
},
|
||||
getElementById () { return {} },
|
||||
createComment () { return {} },
|
||||
createDocumentFragment () { return {} }
|
||||
}
|
||||
|
||||
global.emit = (event, ...args) => process.send({event, args})
|
||||
global.navigator = {userAgent}
|
||||
return (global.window = global)
|
||||
}
|
||||
|
||||
const handleEvents = function () {
|
||||
process.on('uncaughtException', error => console.error(error.message, error.stack))
|
||||
|
||||
return process.on('message', function ({event, args} = {}) {
|
||||
if (event !== 'start') { return }
|
||||
|
||||
let isAsync = false
|
||||
const async = function () {
|
||||
isAsync = true
|
||||
return result => global.emit('task:completed', result)
|
||||
}
|
||||
const result = handler.bind({async})(...args)
|
||||
if (!isAsync) { return global.emit('task:completed', result) }
|
||||
})
|
||||
}
|
||||
|
||||
const setupDeprecations = function () {
|
||||
const Grim = require('grim')
|
||||
return Grim.on('updated', function () {
|
||||
const deprecations = Grim.getDeprecations().map(deprecation => deprecation.serialize())
|
||||
Grim.clearDeprecations()
|
||||
return global.emit('task:deprecations', deprecations)
|
||||
})
|
||||
}
|
||||
|
||||
setupGlobals()
|
||||
handleEvents()
|
||||
setupDeprecations()
|
||||
const handler = require(taskPath)
|
||||
@@ -66,26 +66,11 @@ class Task
|
||||
constructor: (taskPath) ->
|
||||
@emitter = new Emitter
|
||||
|
||||
compileCacheRequire = "require('#{require.resolve('./compile-cache')}')"
|
||||
compileCachePath = require('./compile-cache').getCacheDirectory()
|
||||
taskBootstrapRequire = "require('#{require.resolve('./task-bootstrap')}');"
|
||||
bootstrap = """
|
||||
if (typeof snapshotResult !== 'undefined') {
|
||||
snapshotResult.setGlobals(global, process, global, {}, console, require)
|
||||
}
|
||||
|
||||
CompileCache = #{compileCacheRequire}
|
||||
CompileCache.setCacheDirectory('#{compileCachePath}');
|
||||
CompileCache.install("#{process.resourcesPath}", require)
|
||||
#{taskBootstrapRequire}
|
||||
"""
|
||||
bootstrap = bootstrap.replace(/\\/g, "\\\\")
|
||||
|
||||
taskPath = require.resolve(taskPath)
|
||||
taskPath = taskPath.replace(/\\/g, "\\\\")
|
||||
|
||||
env = _.extend({}, process.env, {taskPath, userAgent: navigator.userAgent})
|
||||
@childProcess = ChildProcess.fork '--eval', [bootstrap], {env, silent: true}
|
||||
env = Object.assign({}, process.env, {userAgent: navigator.userAgent})
|
||||
@childProcess = ChildProcess.fork require.resolve('./task-bootstrap'), [compileCachePath, taskPath], {env, silent: true}
|
||||
|
||||
@on "task:log", -> console.log(arguments...)
|
||||
@on "task:warn", -> console.warn(arguments...)
|
||||
@@ -170,6 +155,9 @@ class Task
|
||||
|
||||
true
|
||||
|
||||
# Public: Cancel the running task and emit an event if it was canceled.
|
||||
#
|
||||
# Returns a {Boolean} indicating whether the task was terminated.
|
||||
cancel: ->
|
||||
didForcefullyTerminate = @terminate()
|
||||
if didForcefullyTerminate
|
||||
|
||||
@@ -1,976 +0,0 @@
|
||||
scrollbarStyle = require 'scrollbar-style'
|
||||
{Range, Point} = require 'text-buffer'
|
||||
{CompositeDisposable} = require 'event-kit'
|
||||
{ipcRenderer} = require 'electron'
|
||||
Grim = require 'grim'
|
||||
|
||||
TextEditorPresenter = require './text-editor-presenter'
|
||||
GutterContainerComponent = require './gutter-container-component'
|
||||
InputComponent = require './input-component'
|
||||
LinesComponent = require './lines-component'
|
||||
OffScreenBlockDecorationsComponent = require './off-screen-block-decorations-component'
|
||||
ScrollbarComponent = require './scrollbar-component'
|
||||
ScrollbarCornerComponent = require './scrollbar-corner-component'
|
||||
OverlayManager = require './overlay-manager'
|
||||
DOMElementPool = require './dom-element-pool'
|
||||
LinesYardstick = require './lines-yardstick'
|
||||
LineTopIndex = require 'line-top-index'
|
||||
|
||||
module.exports =
|
||||
class TextEditorComponent
|
||||
cursorBlinkPeriod: 800
|
||||
cursorBlinkResumeDelay: 100
|
||||
tileSize: 12
|
||||
|
||||
pendingScrollTop: null
|
||||
pendingScrollLeft: null
|
||||
updateRequested: false
|
||||
updatesPaused: false
|
||||
updateRequestedWhilePaused: false
|
||||
heightAndWidthMeasurementRequested: false
|
||||
inputEnabled: true
|
||||
measureScrollbarsWhenShown: true
|
||||
measureLineHeightAndDefaultCharWidthWhenShown: true
|
||||
stylingChangeAnimationFrameRequested: false
|
||||
gutterComponent: null
|
||||
mounted: true
|
||||
initialized: false
|
||||
|
||||
Object.defineProperty @prototype, "domNode",
|
||||
get: -> @domNodeValue
|
||||
set: (domNode) ->
|
||||
@assert domNode?, "TextEditorComponent::domNode was set to null."
|
||||
@domNodeValue = domNode
|
||||
|
||||
constructor: ({@editor, @hostElement, tileSize, @views, @themes, @styles, @assert, hiddenInputElement}) ->
|
||||
@tileSize = tileSize if tileSize?
|
||||
@disposables = new CompositeDisposable
|
||||
|
||||
lineTopIndex = new LineTopIndex({
|
||||
defaultLineHeight: @editor.getLineHeightInPixels()
|
||||
})
|
||||
@presenter = new TextEditorPresenter
|
||||
model: @editor
|
||||
tileSize: tileSize
|
||||
cursorBlinkPeriod: @cursorBlinkPeriod
|
||||
cursorBlinkResumeDelay: @cursorBlinkResumeDelay
|
||||
stoppedScrollingDelay: 200
|
||||
lineTopIndex: lineTopIndex
|
||||
autoHeight: @editor.getAutoHeight()
|
||||
|
||||
@presenter.onDidUpdateState(@requestUpdate)
|
||||
|
||||
@domElementPool = new DOMElementPool
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add('editor-contents--private')
|
||||
|
||||
@overlayManager = new OverlayManager(@presenter, @domNode, @views)
|
||||
|
||||
@scrollViewNode = document.createElement('div')
|
||||
@scrollViewNode.classList.add('scroll-view')
|
||||
@domNode.appendChild(@scrollViewNode)
|
||||
|
||||
@hiddenInputComponent = new InputComponent(hiddenInputElement)
|
||||
@scrollViewNode.appendChild(hiddenInputElement)
|
||||
# Add a getModel method to the hidden input component to make it easy to
|
||||
# access the editor in response to DOM events or when using
|
||||
# document.activeElement.
|
||||
hiddenInputElement.getModel = => @editor
|
||||
|
||||
@linesComponent = new LinesComponent({@presenter, @domElementPool, @assert, @grammars, @views})
|
||||
@scrollViewNode.appendChild(@linesComponent.getDomNode())
|
||||
|
||||
@offScreenBlockDecorationsComponent = new OffScreenBlockDecorationsComponent({@presenter, @views})
|
||||
@scrollViewNode.appendChild(@offScreenBlockDecorationsComponent.getDomNode())
|
||||
|
||||
@linesYardstick = new LinesYardstick(@editor, @linesComponent, lineTopIndex)
|
||||
@presenter.setLinesYardstick(@linesYardstick)
|
||||
|
||||
@horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll})
|
||||
@scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode())
|
||||
|
||||
@verticalScrollbarComponent = new ScrollbarComponent({orientation: 'vertical', onScroll: @onVerticalScroll})
|
||||
@domNode.appendChild(@verticalScrollbarComponent.getDomNode())
|
||||
|
||||
@scrollbarCornerComponent = new ScrollbarCornerComponent
|
||||
@domNode.appendChild(@scrollbarCornerComponent.getDomNode())
|
||||
|
||||
@observeEditor()
|
||||
@listenForDOMEvents()
|
||||
|
||||
@disposables.add @styles.onDidAddStyleElement @onStylesheetsChanged
|
||||
@disposables.add @styles.onDidUpdateStyleElement @onStylesheetsChanged
|
||||
@disposables.add @styles.onDidRemoveStyleElement @onStylesheetsChanged
|
||||
unless @themes.isInitialLoadComplete()
|
||||
@disposables.add @themes.onDidChangeActiveThemes @onAllThemesLoaded
|
||||
@disposables.add scrollbarStyle.onDidChangePreferredScrollbarStyle @refreshScrollbars
|
||||
|
||||
@disposables.add @views.pollDocument(@pollDOM)
|
||||
|
||||
@updateSync()
|
||||
@checkForVisibilityChange()
|
||||
@initialized = true
|
||||
|
||||
destroy: ->
|
||||
@mounted = false
|
||||
@disposables.dispose()
|
||||
@presenter.destroy()
|
||||
@gutterContainerComponent?.destroy()
|
||||
@domElementPool.clear()
|
||||
|
||||
@verticalScrollbarComponent.destroy()
|
||||
@horizontalScrollbarComponent.destroy()
|
||||
|
||||
@onVerticalScroll = null
|
||||
@onHorizontalScroll = null
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: ->
|
||||
@updateSyncPreMeasurement()
|
||||
|
||||
@oldState ?= {width: null}
|
||||
@newState = @presenter.getPostMeasurementState()
|
||||
|
||||
if @editor.getLastSelection()? and not @editor.getLastSelection().isEmpty()
|
||||
@domNode.classList.add('has-selection')
|
||||
else
|
||||
@domNode.classList.remove('has-selection')
|
||||
|
||||
if @newState.focused isnt @oldState.focused
|
||||
@domNode.classList.toggle('is-focused', @newState.focused)
|
||||
|
||||
@performedInitialMeasurement = false if @editor.isDestroyed()
|
||||
|
||||
if @performedInitialMeasurement
|
||||
if @newState.height isnt @oldState.height
|
||||
if @newState.height?
|
||||
@domNode.style.height = @newState.height + 'px'
|
||||
else
|
||||
@domNode.style.height = ''
|
||||
|
||||
if @newState.width isnt @oldState.width
|
||||
if @newState.width?
|
||||
@hostElement.style.width = @newState.width + 'px'
|
||||
else
|
||||
@hostElement.style.width = ''
|
||||
@oldState.width = @newState.width
|
||||
|
||||
if @newState.gutters.length
|
||||
@mountGutterContainerComponent() unless @gutterContainerComponent?
|
||||
@gutterContainerComponent.updateSync(@newState)
|
||||
else
|
||||
@gutterContainerComponent?.getDomNode()?.remove()
|
||||
@gutterContainerComponent = null
|
||||
|
||||
@hiddenInputComponent.updateSync(@newState)
|
||||
@offScreenBlockDecorationsComponent.updateSync(@newState)
|
||||
@linesComponent.updateSync(@newState)
|
||||
@horizontalScrollbarComponent.updateSync(@newState)
|
||||
@verticalScrollbarComponent.updateSync(@newState)
|
||||
@scrollbarCornerComponent.updateSync(@newState)
|
||||
|
||||
@overlayManager?.render(@newState)
|
||||
|
||||
if @clearPoolAfterUpdate
|
||||
@domElementPool.clear()
|
||||
@clearPoolAfterUpdate = false
|
||||
|
||||
if @editor.isAlive()
|
||||
@updateParentViewFocusedClassIfNeeded()
|
||||
@updateParentViewMiniClass()
|
||||
|
||||
updateSyncPreMeasurement: ->
|
||||
@linesComponent.updateSync(@presenter.getPreMeasurementState())
|
||||
|
||||
readAfterUpdateSync: =>
|
||||
@overlayManager?.measureOverlays()
|
||||
@linesComponent.measureBlockDecorations()
|
||||
@offScreenBlockDecorationsComponent.measureBlockDecorations()
|
||||
|
||||
mountGutterContainerComponent: ->
|
||||
@gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool, @views})
|
||||
@domNode.insertBefore(@gutterContainerComponent.getDomNode(), @domNode.firstChild)
|
||||
|
||||
becameVisible: ->
|
||||
@updatesPaused = true
|
||||
# Always invalidate LinesYardstick measurements when the editor becomes
|
||||
# visible again, because content might have been reflowed and measurements
|
||||
# could be outdated.
|
||||
@invalidateMeasurements()
|
||||
@measureScrollbars() if @measureScrollbarsWhenShown
|
||||
@sampleFontStyling()
|
||||
@sampleBackgroundColors()
|
||||
@measureWindowSize()
|
||||
@measureDimensions()
|
||||
@measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown
|
||||
@editor.setVisible(true)
|
||||
@performedInitialMeasurement = true
|
||||
@updatesPaused = false
|
||||
@updateSync() if @canUpdate()
|
||||
|
||||
requestUpdate: =>
|
||||
return unless @canUpdate()
|
||||
|
||||
if @updatesPaused
|
||||
@updateRequestedWhilePaused = true
|
||||
return
|
||||
|
||||
if @hostElement.isUpdatedSynchronously()
|
||||
@updateSync()
|
||||
else unless @updateRequested
|
||||
@updateRequested = true
|
||||
@views.updateDocument =>
|
||||
@updateRequested = false
|
||||
@updateSync() if @canUpdate()
|
||||
@views.readDocument(@readAfterUpdateSync)
|
||||
|
||||
canUpdate: ->
|
||||
@mounted and @editor.isAlive()
|
||||
|
||||
requestAnimationFrame: (fn) ->
|
||||
@updatesPaused = true
|
||||
requestAnimationFrame =>
|
||||
fn()
|
||||
@updatesPaused = false
|
||||
if @updateRequestedWhilePaused and @canUpdate()
|
||||
@updateRequestedWhilePaused = false
|
||||
@requestUpdate()
|
||||
|
||||
getTopmostDOMNode: ->
|
||||
@hostElement
|
||||
|
||||
observeEditor: ->
|
||||
@disposables.add @editor.observeGrammar(@onGrammarChanged)
|
||||
|
||||
listenForDOMEvents: ->
|
||||
@domNode.addEventListener 'mousewheel', @onMouseWheel
|
||||
@domNode.addEventListener 'textInput', @onTextInput
|
||||
@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 macOS. Usually, every keydown event that could cause input is
|
||||
# followed by a corresponding keypress. However, pressing and holding
|
||||
# long enough to open the accented character menu causes additional keydown
|
||||
# events to fire that aren't followed by their own keypress and textInput
|
||||
# events.
|
||||
#
|
||||
# Therefore, we assume the accented character menu has been deployed if,
|
||||
# before observing any keyup event, we observe events in the following
|
||||
# sequence:
|
||||
#
|
||||
# keydown(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:
|
||||
#
|
||||
# User types 's', chromium pops up the completion helper
|
||||
# 1. compositionstart fired
|
||||
# 2. compositionupdate fired; event.data == 's'
|
||||
# User hits arrow keys to move around in completion helper
|
||||
# 3. compositionupdate fired; event.data == 's' for each arry key press
|
||||
# User escape to cancel
|
||||
# 4. compositionend fired
|
||||
# OR User chooses a completion
|
||||
# 4. compositionend fired
|
||||
# 5. textInput fired; event.data == the completion string
|
||||
|
||||
checkpoint = null
|
||||
@domNode.addEventListener 'compositionstart', =>
|
||||
if @openedAccentedCharacterMenu
|
||||
@editor.selectLeft()
|
||||
@openedAccentedCharacterMenu = false
|
||||
checkpoint = @editor.createCheckpoint()
|
||||
@domNode.addEventListener 'compositionupdate', (event) =>
|
||||
@editor.insertText(event.data, select: true)
|
||||
@domNode.addEventListener 'compositionend', (event) =>
|
||||
@editor.revertToCheckpoint(checkpoint)
|
||||
event.target.value = ''
|
||||
|
||||
# Listen for selection changes and store the currently selected text
|
||||
# in the selection clipboard. This is only applicable on Linux.
|
||||
trackSelectionClipboard: ->
|
||||
timeoutId = null
|
||||
writeSelectedTextToSelectionClipboard = =>
|
||||
return if @editor.isDestroyed()
|
||||
if selectedText = @editor.getSelectedText()
|
||||
# This uses ipcRenderer.send instead of clipboard.writeText because
|
||||
# clipboard.writeText is a sync ipcRenderer call on Linux and that
|
||||
# will slow down selections.
|
||||
ipcRenderer.send('write-text-to-selection-clipboard', selectedText)
|
||||
@disposables.add @editor.onDidChangeSelectionRange ->
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(writeSelectedTextToSelectionClipboard)
|
||||
|
||||
onGrammarChanged: =>
|
||||
if @scopedConfigDisposables?
|
||||
@scopedConfigDisposables.dispose()
|
||||
@disposables.remove(@scopedConfigDisposables)
|
||||
|
||||
@scopedConfigDisposables = new CompositeDisposable
|
||||
@disposables.add(@scopedConfigDisposables)
|
||||
|
||||
focused: ->
|
||||
if @mounted
|
||||
@presenter.setFocused(true)
|
||||
|
||||
blurred: ->
|
||||
if @mounted
|
||||
@presenter.setFocused(false)
|
||||
|
||||
onTextInput: (event) =>
|
||||
event.stopPropagation()
|
||||
|
||||
# WARNING: If we call preventDefault on the input of a space character,
|
||||
# then the browser interprets the spacebar keypress as a page-down command,
|
||||
# causing spaces to scroll elements containing editors. This is impossible
|
||||
# to test.
|
||||
event.preventDefault() if event.data isnt ' '
|
||||
|
||||
return unless @isInputEnabled()
|
||||
|
||||
# Workaround of the accented character suggestion feature in macOS.
|
||||
# This will only occur when the user is not composing in IME mode.
|
||||
# When the user selects a modified character from the macOS 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
|
||||
|
||||
@editor.insertText(event.data, groupUndo: true)
|
||||
|
||||
onVerticalScroll: (scrollTop) =>
|
||||
return if @updateRequested or scrollTop is @presenter.getScrollTop()
|
||||
|
||||
animationFramePending = @pendingScrollTop?
|
||||
@pendingScrollTop = scrollTop
|
||||
unless animationFramePending
|
||||
@requestAnimationFrame =>
|
||||
pendingScrollTop = @pendingScrollTop
|
||||
@pendingScrollTop = null
|
||||
@presenter.setScrollTop(pendingScrollTop)
|
||||
@presenter.commitPendingScrollTopPosition()
|
||||
|
||||
onHorizontalScroll: (scrollLeft) =>
|
||||
return if @updateRequested or scrollLeft is @presenter.getScrollLeft()
|
||||
|
||||
animationFramePending = @pendingScrollLeft?
|
||||
@pendingScrollLeft = scrollLeft
|
||||
unless animationFramePending
|
||||
@requestAnimationFrame =>
|
||||
@presenter.setScrollLeft(@pendingScrollLeft)
|
||||
@presenter.commitPendingScrollLeftPosition()
|
||||
@pendingScrollLeft = null
|
||||
|
||||
onMouseWheel: (event) =>
|
||||
# Only scroll in one direction at a time
|
||||
{wheelDeltaX, wheelDeltaY} = event
|
||||
|
||||
if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)
|
||||
# Scrolling horizontally
|
||||
previousScrollLeft = @presenter.getScrollLeft()
|
||||
updatedScrollLeft = previousScrollLeft - Math.round(wheelDeltaX * @editor.getScrollSensitivity() / 100)
|
||||
|
||||
event.preventDefault() if @presenter.canScrollLeftTo(updatedScrollLeft)
|
||||
@presenter.setScrollLeft(updatedScrollLeft)
|
||||
else
|
||||
# Scrolling vertically
|
||||
@presenter.setMouseWheelScreenRow(@screenRowForNode(event.target))
|
||||
previousScrollTop = @presenter.getScrollTop()
|
||||
updatedScrollTop = previousScrollTop - Math.round(wheelDeltaY * @editor.getScrollSensitivity() / 100)
|
||||
|
||||
event.preventDefault() if @presenter.canScrollTopTo(updatedScrollTop)
|
||||
@presenter.setScrollTop(updatedScrollTop)
|
||||
|
||||
onScrollViewScroll: =>
|
||||
if @mounted
|
||||
@scrollViewNode.scrollTop = 0
|
||||
@scrollViewNode.scrollLeft = 0
|
||||
|
||||
onDidChangeScrollTop: (callback) ->
|
||||
@presenter.onDidChangeScrollTop(callback)
|
||||
|
||||
onDidChangeScrollLeft: (callback) ->
|
||||
@presenter.onDidChangeScrollLeft(callback)
|
||||
|
||||
setScrollLeft: (scrollLeft) ->
|
||||
@presenter.setScrollLeft(scrollLeft)
|
||||
|
||||
setScrollRight: (scrollRight) ->
|
||||
@presenter.setScrollRight(scrollRight)
|
||||
|
||||
setScrollTop: (scrollTop) ->
|
||||
@presenter.setScrollTop(scrollTop)
|
||||
|
||||
setScrollBottom: (scrollBottom) ->
|
||||
@presenter.setScrollBottom(scrollBottom)
|
||||
|
||||
getScrollTop: ->
|
||||
@presenter.getScrollTop()
|
||||
|
||||
getScrollLeft: ->
|
||||
@presenter.getScrollLeft()
|
||||
|
||||
getScrollRight: ->
|
||||
@presenter.getScrollRight()
|
||||
|
||||
getScrollBottom: ->
|
||||
@presenter.getScrollBottom()
|
||||
|
||||
getScrollHeight: ->
|
||||
@presenter.getScrollHeight()
|
||||
|
||||
getScrollWidth: ->
|
||||
@presenter.getScrollWidth()
|
||||
|
||||
getMaxScrollTop: ->
|
||||
@presenter.getMaxScrollTop()
|
||||
|
||||
getVerticalScrollbarWidth: ->
|
||||
@presenter.getVerticalScrollbarWidth()
|
||||
|
||||
getHorizontalScrollbarHeight: ->
|
||||
@presenter.getHorizontalScrollbarHeight()
|
||||
|
||||
getVisibleRowRange: ->
|
||||
@presenter.getVisibleRowRange()
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition, clip=true) ->
|
||||
screenPosition = Point.fromObject(screenPosition)
|
||||
screenPosition = @editor.clipScreenPosition(screenPosition) if clip
|
||||
|
||||
unless @presenter.isRowRendered(screenPosition.row)
|
||||
@presenter.setScreenRowsToMeasure([screenPosition.row])
|
||||
|
||||
unless @linesComponent.lineNodeForScreenRow(screenPosition.row)?
|
||||
@updateSyncPreMeasurement()
|
||||
|
||||
pixelPosition = @linesYardstick.pixelPositionForScreenPosition(screenPosition)
|
||||
@presenter.clearScreenRowsToMeasure()
|
||||
pixelPosition
|
||||
|
||||
screenPositionForPixelPosition: (pixelPosition) ->
|
||||
row = @linesYardstick.measuredRowForPixelPosition(pixelPosition)
|
||||
if row? and not @presenter.isRowRendered(row)
|
||||
@presenter.setScreenRowsToMeasure([row])
|
||||
@updateSyncPreMeasurement()
|
||||
|
||||
position = @linesYardstick.screenPositionForPixelPosition(pixelPosition)
|
||||
@presenter.clearScreenRowsToMeasure()
|
||||
position
|
||||
|
||||
pixelRectForScreenRange: (screenRange) ->
|
||||
rowsToMeasure = []
|
||||
unless @presenter.isRowRendered(screenRange.start.row)
|
||||
rowsToMeasure.push(screenRange.start.row)
|
||||
unless @presenter.isRowRendered(screenRange.end.row)
|
||||
rowsToMeasure.push(screenRange.end.row)
|
||||
|
||||
if rowsToMeasure.length > 0
|
||||
@presenter.setScreenRowsToMeasure(rowsToMeasure)
|
||||
@updateSyncPreMeasurement()
|
||||
|
||||
rect = @presenter.absolutePixelRectForScreenRange(screenRange)
|
||||
|
||||
if rowsToMeasure.length > 0
|
||||
@presenter.clearScreenRowsToMeasure()
|
||||
|
||||
rect
|
||||
|
||||
pixelRangeForScreenRange: (screenRange, clip=true) ->
|
||||
{start, end} = Range.fromObject(screenRange)
|
||||
{start: @pixelPositionForScreenPosition(start, clip), end: @pixelPositionForScreenPosition(end, clip)}
|
||||
|
||||
pixelPositionForBufferPosition: (bufferPosition) ->
|
||||
@pixelPositionForScreenPosition(
|
||||
@editor.screenPositionForBufferPosition(bufferPosition)
|
||||
)
|
||||
|
||||
invalidateBlockDecorationDimensions: ->
|
||||
@presenter.invalidateBlockDecorationDimensions(arguments...)
|
||||
|
||||
onMouseDown: (event) =>
|
||||
# Handle middle mouse button on linux platform only (paste clipboard)
|
||||
if event.button is 1 and process.platform is 'linux'
|
||||
if selection = require('./safe-clipboard').readText('selection')
|
||||
screenPosition = @screenPositionForMouseEvent(event)
|
||||
@editor.setCursorScreenPosition(screenPosition, autoscroll: false)
|
||||
@editor.insertText(selection)
|
||||
return
|
||||
|
||||
# Handle mouse down events for left mouse button only
|
||||
# (except middle mouse button on linux platform, see above)
|
||||
unless event.button is 0
|
||||
return
|
||||
|
||||
return if event.target?.classList.contains('horizontal-scrollbar')
|
||||
|
||||
{detail, shiftKey, metaKey, ctrlKey} = event
|
||||
|
||||
# CTRL+click brings up the context menu on macOS, so don't handle those either
|
||||
return if ctrlKey and process.platform is 'darwin'
|
||||
|
||||
# Prevent focusout event on hidden input if editor is already focused
|
||||
event.preventDefault() if @oldState.focused
|
||||
|
||||
screenPosition = @screenPositionForMouseEvent(event)
|
||||
|
||||
if event.target?.classList.contains('fold-marker')
|
||||
bufferPosition = @editor.bufferPositionForScreenPosition(screenPosition)
|
||||
@editor.destroyFoldsIntersectingBufferRange([bufferPosition, bufferPosition])
|
||||
return
|
||||
|
||||
switch detail
|
||||
when 1
|
||||
if shiftKey
|
||||
@editor.selectToScreenPosition(screenPosition)
|
||||
else if metaKey or (ctrlKey and process.platform isnt 'darwin')
|
||||
cursorAtScreenPosition = @editor.getCursorAtScreenPosition(screenPosition)
|
||||
if cursorAtScreenPosition and @editor.hasMultipleCursors()
|
||||
cursorAtScreenPosition.destroy()
|
||||
else
|
||||
@editor.addCursorAtScreenPosition(screenPosition, autoscroll: false)
|
||||
else
|
||||
@editor.setCursorScreenPosition(screenPosition, autoscroll: false)
|
||||
when 2
|
||||
@editor.getLastSelection().selectWord(autoscroll: false)
|
||||
when 3
|
||||
@editor.getLastSelection().selectLine(null, autoscroll: false)
|
||||
|
||||
@handleDragUntilMouseUp (screenPosition) =>
|
||||
@editor.selectToScreenPosition(screenPosition, suppressSelectionMerge: true, autoscroll: false)
|
||||
|
||||
onLineNumberGutterMouseDown: (event) =>
|
||||
return unless event.button is 0 # only handle the left mouse button
|
||||
|
||||
{shiftKey, metaKey, ctrlKey} = event
|
||||
|
||||
if shiftKey
|
||||
@onGutterShiftClick(event)
|
||||
else if metaKey or (ctrlKey and process.platform isnt 'darwin')
|
||||
@onGutterMetaClick(event)
|
||||
else
|
||||
@onGutterClick(event)
|
||||
|
||||
onGutterClick: (event) =>
|
||||
clickedScreenRow = @screenPositionForMouseEvent(event).row
|
||||
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
|
||||
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
|
||||
@editor.setSelectedScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false)
|
||||
@handleGutterDrag(initialScreenRange)
|
||||
|
||||
onGutterMetaClick: (event) =>
|
||||
clickedScreenRow = @screenPositionForMouseEvent(event).row
|
||||
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
|
||||
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
|
||||
@editor.addSelectionForScreenRange(initialScreenRange, autoscroll: false)
|
||||
@handleGutterDrag(initialScreenRange)
|
||||
|
||||
onGutterShiftClick: (event) =>
|
||||
tailScreenPosition = @editor.getLastSelection().getTailScreenPosition()
|
||||
clickedScreenRow = @screenPositionForMouseEvent(event).row
|
||||
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
|
||||
clickedLineScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
|
||||
|
||||
if clickedScreenRow < tailScreenPosition.row
|
||||
@editor.selectToScreenPosition(clickedLineScreenRange.start, suppressSelectionMerge: true, autoscroll: false)
|
||||
else
|
||||
@editor.selectToScreenPosition(clickedLineScreenRange.end, suppressSelectionMerge: true, autoscroll: false)
|
||||
|
||||
@handleGutterDrag(new Range(tailScreenPosition, tailScreenPosition))
|
||||
|
||||
handleGutterDrag: (initialRange) ->
|
||||
@handleDragUntilMouseUp (screenPosition) =>
|
||||
dragRow = screenPosition.row
|
||||
if dragRow < initialRange.start.row
|
||||
startPosition = @editor.clipScreenPosition([dragRow, 0], skipSoftWrapIndentation: true)
|
||||
screenRange = new Range(startPosition, startPosition).union(initialRange)
|
||||
@editor.getLastSelection().setScreenRange(screenRange, reversed: true, autoscroll: false, preserveFolds: true)
|
||||
else
|
||||
endPosition = @editor.clipScreenPosition([dragRow + 1, 0], clipDirection: 'backward')
|
||||
screenRange = new Range(endPosition, endPosition).union(initialRange)
|
||||
@editor.getLastSelection().setScreenRange(screenRange, reversed: false, autoscroll: false, preserveFolds: true)
|
||||
|
||||
onStylesheetsChanged: (styleElement) =>
|
||||
return unless @performedInitialMeasurement
|
||||
return unless @themes.isInitialLoadComplete()
|
||||
|
||||
# This delay prevents the styling from going haywire when stylesheets are
|
||||
# reloaded in dev mode. It seems like a workaround for a browser bug, but
|
||||
# not totally sure.
|
||||
|
||||
unless @stylingChangeAnimationFrameRequested
|
||||
@stylingChangeAnimationFrameRequested = true
|
||||
requestAnimationFrame =>
|
||||
@stylingChangeAnimationFrameRequested = false
|
||||
if @mounted
|
||||
@refreshScrollbars() if not styleElement.sheet? or @containsScrollbarSelector(styleElement.sheet)
|
||||
@handleStylingChange()
|
||||
|
||||
onAllThemesLoaded: =>
|
||||
@refreshScrollbars()
|
||||
@handleStylingChange()
|
||||
|
||||
handleStylingChange: =>
|
||||
@sampleFontStyling()
|
||||
@sampleBackgroundColors()
|
||||
@invalidateMeasurements()
|
||||
|
||||
handleDragUntilMouseUp: (dragHandler) ->
|
||||
dragging = false
|
||||
lastMousePosition = {}
|
||||
animationLoop = =>
|
||||
@requestAnimationFrame =>
|
||||
if dragging and @mounted
|
||||
linesClientRect = @linesComponent.getDomNode().getBoundingClientRect()
|
||||
autoscroll(lastMousePosition, linesClientRect)
|
||||
screenPosition = @screenPositionForMouseEvent(lastMousePosition, linesClientRect)
|
||||
dragHandler(screenPosition)
|
||||
animationLoop()
|
||||
else if not @mounted
|
||||
stopDragging()
|
||||
|
||||
onMouseMove = (event) ->
|
||||
lastMousePosition.clientX = event.clientX
|
||||
lastMousePosition.clientY = event.clientY
|
||||
|
||||
# Start the animation loop when the mouse moves prior to a mouseup event
|
||||
unless dragging
|
||||
dragging = true
|
||||
animationLoop()
|
||||
|
||||
# Stop dragging when cursor enters dev tools because we can't detect mouseup
|
||||
onMouseUp() if event.which is 0
|
||||
|
||||
onMouseUp = (event) =>
|
||||
if dragging
|
||||
stopDragging()
|
||||
@editor.finalizeSelections()
|
||||
@editor.mergeIntersectingSelections()
|
||||
|
||||
stopDragging = ->
|
||||
dragging = false
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('mouseup', onMouseUp)
|
||||
disposables.dispose()
|
||||
|
||||
autoscroll = (mouseClientPosition) =>
|
||||
{top, bottom, left, right} = @scrollViewNode.getBoundingClientRect()
|
||||
top += 30
|
||||
bottom -= 30
|
||||
left += 30
|
||||
right -= 30
|
||||
|
||||
if mouseClientPosition.clientY < top
|
||||
mouseYDelta = top - mouseClientPosition.clientY
|
||||
yDirection = -1
|
||||
else if mouseClientPosition.clientY > bottom
|
||||
mouseYDelta = mouseClientPosition.clientY - bottom
|
||||
yDirection = 1
|
||||
|
||||
if mouseClientPosition.clientX < left
|
||||
mouseXDelta = left - mouseClientPosition.clientX
|
||||
xDirection = -1
|
||||
else if mouseClientPosition.clientX > right
|
||||
mouseXDelta = mouseClientPosition.clientX - right
|
||||
xDirection = 1
|
||||
|
||||
if mouseYDelta?
|
||||
@presenter.setScrollTop(@presenter.getScrollTop() + yDirection * scaleScrollDelta(mouseYDelta))
|
||||
@presenter.commitPendingScrollTopPosition()
|
||||
|
||||
if mouseXDelta?
|
||||
@presenter.setScrollLeft(@presenter.getScrollLeft() + xDirection * scaleScrollDelta(mouseXDelta))
|
||||
@presenter.commitPendingScrollLeftPosition()
|
||||
|
||||
scaleScrollDelta = (scrollDelta) ->
|
||||
Math.pow(scrollDelta / 2, 3) / 280
|
||||
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('mouseup', onMouseUp)
|
||||
disposables = new CompositeDisposable
|
||||
disposables.add(@editor.getBuffer().onWillChange(onMouseUp))
|
||||
disposables.add(@editor.onDidDestroy(stopDragging))
|
||||
|
||||
isVisible: ->
|
||||
# Investigating an exception that occurs here due to ::domNode being null.
|
||||
@assert @domNode?, "TextEditorComponent::domNode was null.", (error) =>
|
||||
error.metadata = {@initialized}
|
||||
|
||||
@domNode? and (@domNode.offsetHeight > 0 or @domNode.offsetWidth > 0)
|
||||
|
||||
pollDOM: =>
|
||||
unless @checkForVisibilityChange()
|
||||
@sampleBackgroundColors()
|
||||
@measureWindowSize()
|
||||
@measureDimensions()
|
||||
@sampleFontStyling()
|
||||
@overlayManager?.measureOverlays()
|
||||
|
||||
checkForVisibilityChange: ->
|
||||
if @isVisible()
|
||||
if @wasVisible
|
||||
false
|
||||
else
|
||||
@becameVisible()
|
||||
@wasVisible = true
|
||||
else
|
||||
@wasVisible = false
|
||||
|
||||
# Measure explicitly-styled height and width and relay them to the model. If
|
||||
# these values aren't explicitly styled, we assume the editor is unconstrained
|
||||
# and use the scrollHeight / scrollWidth as its height and width in
|
||||
# calculations.
|
||||
measureDimensions: ->
|
||||
# If we don't assign autoHeight explicitly, we try to automatically disable
|
||||
# auto-height in certain circumstances. This is legacy behavior that we
|
||||
# would rather not implement, but we can't remove it without risking
|
||||
# breakage currently.
|
||||
unless @editor.autoHeight?
|
||||
{position, top, bottom} = getComputedStyle(@hostElement)
|
||||
hasExplicitTopAndBottom = (position is 'absolute' and top isnt 'auto' and bottom isnt 'auto')
|
||||
hasInlineHeight = @hostElement.style.height.length > 0
|
||||
|
||||
if hasInlineHeight or hasExplicitTopAndBottom
|
||||
if @presenter.autoHeight
|
||||
@presenter.setAutoHeight(false)
|
||||
if hasExplicitTopAndBottom
|
||||
Grim.deprecate("""
|
||||
Assigning editor #{@editor.id}'s height explicitly via `position: 'absolute'` and an assigned `top` and `bottom` implicitly assigns the `autoHeight` property to false on the editor.
|
||||
This behavior is deprecated and will not be supported in the future. Please explicitly assign `autoHeight` on this editor.
|
||||
""")
|
||||
else if hasInlineHeight
|
||||
Grim.deprecate("""
|
||||
Assigning editor #{@editor.id}'s height explicitly via an inline style implicitly assigns the `autoHeight` property to false on the editor.
|
||||
This behavior is deprecated and will not be supported in the future. Please explicitly assign `autoHeight` on this editor.
|
||||
""")
|
||||
else
|
||||
@presenter.setAutoHeight(true)
|
||||
|
||||
if @presenter.autoHeight
|
||||
@presenter.setExplicitHeight(null)
|
||||
else if @hostElement.offsetHeight > 0
|
||||
@presenter.setExplicitHeight(@hostElement.offsetHeight)
|
||||
|
||||
clientWidth = @scrollViewNode.clientWidth
|
||||
paddingLeft = parseInt(getComputedStyle(@scrollViewNode).paddingLeft)
|
||||
clientWidth -= paddingLeft
|
||||
if clientWidth > 0
|
||||
@presenter.setContentFrameWidth(clientWidth)
|
||||
|
||||
@presenter.setGutterWidth(@gutterContainerComponent?.getDomNode().offsetWidth ? 0)
|
||||
@presenter.setBoundingClientRect(@hostElement.getBoundingClientRect())
|
||||
|
||||
measureWindowSize: ->
|
||||
return unless @mounted
|
||||
|
||||
# FIXME: on Ubuntu (via xvfb) `window.innerWidth` reports an incorrect value
|
||||
# when window gets resized through `atom.setWindowDimensions({width:
|
||||
# windowWidth, height: windowHeight})`.
|
||||
@presenter.setWindowSize(window.innerWidth, window.innerHeight)
|
||||
|
||||
sampleFontStyling: =>
|
||||
oldFontSize = @fontSize
|
||||
oldFontFamily = @fontFamily
|
||||
oldLineHeight = @lineHeight
|
||||
|
||||
{@fontSize, @fontFamily, @lineHeight} = getComputedStyle(@getTopmostDOMNode())
|
||||
|
||||
if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight
|
||||
@clearPoolAfterUpdate = true
|
||||
@measureLineHeightAndDefaultCharWidth()
|
||||
@invalidateMeasurements()
|
||||
|
||||
sampleBackgroundColors: (suppressUpdate) ->
|
||||
{backgroundColor} = getComputedStyle(@hostElement)
|
||||
@presenter.setBackgroundColor(backgroundColor)
|
||||
|
||||
lineNumberGutter = @gutterContainerComponent?.getLineNumberGutterComponent()
|
||||
if lineNumberGutter
|
||||
gutterBackgroundColor = getComputedStyle(lineNumberGutter.getDomNode()).backgroundColor
|
||||
@presenter.setGutterBackgroundColor(gutterBackgroundColor)
|
||||
|
||||
measureLineHeightAndDefaultCharWidth: ->
|
||||
if @isVisible()
|
||||
@measureLineHeightAndDefaultCharWidthWhenShown = false
|
||||
@linesComponent.measureLineHeightAndDefaultCharWidth()
|
||||
else
|
||||
@measureLineHeightAndDefaultCharWidthWhenShown = true
|
||||
|
||||
measureScrollbars: ->
|
||||
@measureScrollbarsWhenShown = false
|
||||
|
||||
cornerNode = @scrollbarCornerComponent.getDomNode()
|
||||
originalDisplayValue = cornerNode.style.display
|
||||
|
||||
cornerNode.style.display = 'block'
|
||||
|
||||
width = (cornerNode.offsetWidth - cornerNode.clientWidth) or 15
|
||||
height = (cornerNode.offsetHeight - cornerNode.clientHeight) or 15
|
||||
|
||||
@presenter.setVerticalScrollbarWidth(width)
|
||||
@presenter.setHorizontalScrollbarHeight(height)
|
||||
|
||||
cornerNode.style.display = originalDisplayValue
|
||||
|
||||
containsScrollbarSelector: (stylesheet) ->
|
||||
for rule in stylesheet.cssRules
|
||||
if rule.selectorText?.indexOf('scrollbar') > -1
|
||||
return true
|
||||
false
|
||||
|
||||
refreshScrollbars: =>
|
||||
if @isVisible()
|
||||
@measureScrollbarsWhenShown = false
|
||||
else
|
||||
@measureScrollbarsWhenShown = true
|
||||
return
|
||||
|
||||
verticalNode = @verticalScrollbarComponent.getDomNode()
|
||||
horizontalNode = @horizontalScrollbarComponent.getDomNode()
|
||||
cornerNode = @scrollbarCornerComponent.getDomNode()
|
||||
|
||||
originalVerticalDisplayValue = verticalNode.style.display
|
||||
originalHorizontalDisplayValue = horizontalNode.style.display
|
||||
originalCornerDisplayValue = cornerNode.style.display
|
||||
|
||||
# First, hide all scrollbars in case they are visible so they take on new
|
||||
# styles when they are shown again.
|
||||
verticalNode.style.display = 'none'
|
||||
horizontalNode.style.display = 'none'
|
||||
cornerNode.style.display = 'none'
|
||||
|
||||
# Force a reflow
|
||||
cornerNode.offsetWidth
|
||||
|
||||
# Now measure the new scrollbar dimensions
|
||||
@measureScrollbars()
|
||||
|
||||
# Now restore the display value for all scrollbars, since they were
|
||||
# previously hidden
|
||||
verticalNode.style.display = originalVerticalDisplayValue
|
||||
horizontalNode.style.display = originalHorizontalDisplayValue
|
||||
cornerNode.style.display = originalCornerDisplayValue
|
||||
|
||||
consolidateSelections: (e) ->
|
||||
e.abortKeyBinding() unless @editor.consolidateSelections()
|
||||
|
||||
lineNodeForScreenRow: (screenRow) ->
|
||||
@linesComponent.lineNodeForScreenRow(screenRow)
|
||||
|
||||
lineNumberNodeForScreenRow: (screenRow) ->
|
||||
tileRow = @presenter.tileForRow(screenRow)
|
||||
gutterComponent = @gutterContainerComponent.getLineNumberGutterComponent()
|
||||
tileComponent = gutterComponent.getComponentForTile(tileRow)
|
||||
|
||||
tileComponent?.lineNumberNodeForScreenRow(screenRow)
|
||||
|
||||
tileNodesForLines: ->
|
||||
@linesComponent.getTiles()
|
||||
|
||||
tileNodesForLineNumbers: ->
|
||||
gutterComponent = @gutterContainerComponent.getLineNumberGutterComponent()
|
||||
gutterComponent.getTiles()
|
||||
|
||||
screenRowForNode: (node) ->
|
||||
while node?
|
||||
if screenRow = node.dataset?.screenRow
|
||||
return parseInt(screenRow)
|
||||
node = node.parentElement
|
||||
null
|
||||
|
||||
getFontSize: ->
|
||||
parseInt(getComputedStyle(@getTopmostDOMNode()).fontSize)
|
||||
|
||||
setFontSize: (fontSize) ->
|
||||
@getTopmostDOMNode().style.fontSize = fontSize + 'px'
|
||||
@sampleFontStyling()
|
||||
@invalidateMeasurements()
|
||||
|
||||
getFontFamily: ->
|
||||
getComputedStyle(@getTopmostDOMNode()).fontFamily
|
||||
|
||||
setFontFamily: (fontFamily) ->
|
||||
@getTopmostDOMNode().style.fontFamily = fontFamily
|
||||
@sampleFontStyling()
|
||||
@invalidateMeasurements()
|
||||
|
||||
setLineHeight: (lineHeight) ->
|
||||
@getTopmostDOMNode().style.lineHeight = lineHeight
|
||||
@sampleFontStyling()
|
||||
@invalidateMeasurements()
|
||||
|
||||
invalidateMeasurements: ->
|
||||
@linesYardstick.invalidateCache()
|
||||
@presenter.measurementsChanged()
|
||||
|
||||
screenPositionForMouseEvent: (event, linesClientRect) ->
|
||||
pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect)
|
||||
@screenPositionForPixelPosition(pixelPosition)
|
||||
|
||||
pixelPositionForMouseEvent: (event, linesClientRect) ->
|
||||
{clientX, clientY} = event
|
||||
|
||||
linesClientRect ?= @linesComponent.getDomNode().getBoundingClientRect()
|
||||
top = clientY - linesClientRect.top + @presenter.getRealScrollTop()
|
||||
left = clientX - linesClientRect.left + @presenter.getRealScrollLeft()
|
||||
bottom = linesClientRect.top + @presenter.getRealScrollTop() + linesClientRect.height - clientY
|
||||
right = linesClientRect.left + @presenter.getRealScrollLeft() + linesClientRect.width - clientX
|
||||
|
||||
{top, left, bottom, right}
|
||||
|
||||
getGutterWidth: ->
|
||||
@presenter.getGutterWidth()
|
||||
|
||||
getModel: ->
|
||||
@editor
|
||||
|
||||
isInputEnabled: -> @inputEnabled
|
||||
|
||||
setInputEnabled: (@inputEnabled) -> @inputEnabled
|
||||
|
||||
setContinuousReflow: (continuousReflow) ->
|
||||
@presenter.setContinuousReflow(continuousReflow)
|
||||
|
||||
updateParentViewFocusedClassIfNeeded: ->
|
||||
if @oldState.focused isnt @newState.focused
|
||||
@hostElement.classList.toggle('is-focused', @newState.focused)
|
||||
@oldState.focused = @newState.focused
|
||||
|
||||
updateParentViewMiniClass: ->
|
||||
@hostElement.classList.toggle('mini', @editor.isMini())
|
||||
3968
src/text-editor-component.js
Normal file
3968
src/text-editor-component.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,331 +0,0 @@
|
||||
Grim = require 'grim'
|
||||
{Emitter, CompositeDisposable} = require 'event-kit'
|
||||
TextBuffer = require 'text-buffer'
|
||||
TextEditorComponent = require './text-editor-component'
|
||||
|
||||
class TextEditorElement extends HTMLElement
|
||||
model: null
|
||||
componentDescriptor: null
|
||||
component: null
|
||||
attached: false
|
||||
tileSize: null
|
||||
focusOnAttach: false
|
||||
hasTiledRendering: true
|
||||
logicalDisplayBuffer: true
|
||||
lightDOM: true
|
||||
|
||||
createdCallback: ->
|
||||
# Use globals when the following instance variables aren't set.
|
||||
@themes = atom.themes
|
||||
@workspace = atom.workspace
|
||||
@assert = atom.assert
|
||||
@views = atom.views
|
||||
@styles = atom.styles
|
||||
|
||||
@emitter = new Emitter
|
||||
@subscriptions = new CompositeDisposable
|
||||
|
||||
@hiddenInputElement = document.createElement('input')
|
||||
@hiddenInputElement.classList.add('hidden-input')
|
||||
@hiddenInputElement.setAttribute('tabindex', -1)
|
||||
@hiddenInputElement.setAttribute('data-react-skip-selection-restoration', true)
|
||||
@hiddenInputElement.style['-webkit-transform'] = 'translateZ(0)'
|
||||
@hiddenInputElement.addEventListener 'paste', (event) -> event.preventDefault()
|
||||
|
||||
@addEventListener 'focus', @focused.bind(this)
|
||||
@addEventListener 'blur', @blurred.bind(this)
|
||||
@hiddenInputElement.addEventListener 'focus', @focused.bind(this)
|
||||
@hiddenInputElement.addEventListener 'blur', @inputNodeBlurred.bind(this)
|
||||
|
||||
@classList.add('editor')
|
||||
@setAttribute('tabindex', -1)
|
||||
|
||||
initializeContent: (attributes) ->
|
||||
Object.defineProperty(this, 'shadowRoot', {
|
||||
get: =>
|
||||
Grim.deprecate("""
|
||||
The contents of `atom-text-editor` elements are no longer encapsulated
|
||||
within a shadow DOM boundary. Please, stop using `shadowRoot` and access
|
||||
the editor contents directly instead.
|
||||
""")
|
||||
this
|
||||
})
|
||||
@rootElement = document.createElement('div')
|
||||
@rootElement.classList.add('editor--private')
|
||||
@appendChild(@rootElement)
|
||||
|
||||
attachedCallback: ->
|
||||
@buildModel() unless @getModel()?
|
||||
@assert(@model.isAlive(), "Attaching a view for a destroyed editor")
|
||||
@mountComponent() unless @component?
|
||||
@listenForComponentEvents()
|
||||
@component.checkForVisibilityChange()
|
||||
if @hasFocus()
|
||||
@focused()
|
||||
@emitter.emit("did-attach")
|
||||
|
||||
detachedCallback: ->
|
||||
@unmountComponent()
|
||||
@subscriptions.dispose()
|
||||
@subscriptions = new CompositeDisposable
|
||||
@emitter.emit("did-detach")
|
||||
|
||||
listenForComponentEvents: ->
|
||||
@subscriptions.add @component.onDidChangeScrollTop =>
|
||||
@emitter.emit("did-change-scroll-top", arguments...)
|
||||
@subscriptions.add @component.onDidChangeScrollLeft =>
|
||||
@emitter.emit("did-change-scroll-left", arguments...)
|
||||
|
||||
initialize: (model, {@views, @themes, @workspace, @assert, @styles}) ->
|
||||
throw new Error("Must pass a views parameter when initializing TextEditorElements") unless @views?
|
||||
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 an assert parameter when initializing TextEditorElements") unless @assert?
|
||||
throw new Error("Must pass a styles parameter when initializing TextEditorElements") unless @styles?
|
||||
|
||||
@setModel(model)
|
||||
this
|
||||
|
||||
setModel: (model) ->
|
||||
throw new Error("Model already assigned on TextEditorElement") if @model?
|
||||
return if model.isDestroyed()
|
||||
|
||||
@model = model
|
||||
@model.setUpdatedSynchronously(@isUpdatedSynchronously())
|
||||
@initializeContent()
|
||||
@mountComponent()
|
||||
@addGrammarScopeAttribute()
|
||||
@addMiniAttribute() if @model.isMini()
|
||||
@addEncodingAttribute()
|
||||
@model.onDidChangeGrammar => @addGrammarScopeAttribute()
|
||||
@model.onDidChangeEncoding => @addEncodingAttribute()
|
||||
@model.onDidDestroy => @unmountComponent()
|
||||
@model.onDidChangeMini (mini) => if mini then @addMiniAttribute() else @removeMiniAttribute()
|
||||
@model
|
||||
|
||||
getModel: ->
|
||||
@model ? @buildModel()
|
||||
|
||||
buildModel: ->
|
||||
@setModel(@workspace.buildTextEditor(
|
||||
buffer: new TextBuffer({
|
||||
text: @textContent
|
||||
shouldDestroyOnFileDelete:
|
||||
-> atom.config.get('core.closeDeletedFileTabs')})
|
||||
softWrapped: false
|
||||
tabLength: 2
|
||||
softTabs: true
|
||||
mini: @hasAttribute('mini')
|
||||
lineNumberGutterVisible: not @hasAttribute('gutter-hidden')
|
||||
placeholderText: @getAttribute('placeholder-text')
|
||||
))
|
||||
|
||||
mountComponent: ->
|
||||
@component = new TextEditorComponent(
|
||||
hostElement: this
|
||||
editor: @model
|
||||
tileSize: @tileSize
|
||||
views: @views
|
||||
themes: @themes
|
||||
styles: @styles
|
||||
workspace: @workspace
|
||||
assert: @assert,
|
||||
hiddenInputElement: @hiddenInputElement
|
||||
)
|
||||
@rootElement.appendChild(@component.getDomNode())
|
||||
|
||||
unmountComponent: ->
|
||||
if @component?
|
||||
@component.destroy()
|
||||
@component.getDomNode().remove()
|
||||
@component = null
|
||||
|
||||
focused: (event) ->
|
||||
@component?.focused()
|
||||
@hiddenInputElement.focus()
|
||||
|
||||
blurred: (event) ->
|
||||
if event.relatedTarget is @hiddenInputElement
|
||||
event.stopImmediatePropagation()
|
||||
return
|
||||
@component?.blurred()
|
||||
|
||||
inputNodeBlurred: (event) ->
|
||||
if event.relatedTarget isnt this
|
||||
@dispatchEvent(new FocusEvent('blur', relatedTarget: event.relatedTarget, bubbles: false))
|
||||
|
||||
addGrammarScopeAttribute: ->
|
||||
@dataset.grammar = @model.getGrammar()?.scopeName?.replace(/\./g, ' ')
|
||||
|
||||
addMiniAttribute: ->
|
||||
@setAttributeNode(document.createAttribute("mini"))
|
||||
|
||||
removeMiniAttribute: ->
|
||||
@removeAttribute("mini")
|
||||
|
||||
addEncodingAttribute: ->
|
||||
@dataset.encoding = @model.getEncoding()
|
||||
|
||||
hasFocus: ->
|
||||
this is document.activeElement or @contains(document.activeElement)
|
||||
|
||||
setUpdatedSynchronously: (@updatedSynchronously) ->
|
||||
@model?.setUpdatedSynchronously(@updatedSynchronously)
|
||||
@updatedSynchronously
|
||||
|
||||
isUpdatedSynchronously: -> @updatedSynchronously
|
||||
|
||||
# Extended: Continuously reflows lines and line numbers. (Has performance overhead)
|
||||
#
|
||||
# * `continuousReflow` A {Boolean} indicating whether to keep reflowing or not.
|
||||
setContinuousReflow: (continuousReflow) ->
|
||||
@component?.setContinuousReflow(continuousReflow)
|
||||
|
||||
# Extended: get the width of a character of text displayed in this element.
|
||||
#
|
||||
# Returns a {Number} of pixels.
|
||||
getDefaultCharacterWidth: ->
|
||||
@getModel().getDefaultCharWidth()
|
||||
|
||||
# Extended: Get the maximum scroll top that can be applied to this element.
|
||||
#
|
||||
# Returns a {Number} of pixels.
|
||||
getMaxScrollTop: ->
|
||||
@component?.getMaxScrollTop()
|
||||
|
||||
# Extended: Converts a buffer position to a pixel position.
|
||||
#
|
||||
# * `bufferPosition` An object that represents a buffer position. It can be either
|
||||
# an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
|
||||
#
|
||||
# Returns an {Object} with two values: `top` and `left`, representing the pixel position.
|
||||
pixelPositionForBufferPosition: (bufferPosition) ->
|
||||
@component.pixelPositionForBufferPosition(bufferPosition)
|
||||
|
||||
# Extended: Converts a screen position to a pixel position.
|
||||
#
|
||||
# * `screenPosition` An object that represents a screen position. It can be either
|
||||
# an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
|
||||
#
|
||||
# Returns an {Object} with two values: `top` and `left`, representing the pixel positions.
|
||||
pixelPositionForScreenPosition: (screenPosition) ->
|
||||
@component.pixelPositionForScreenPosition(screenPosition)
|
||||
|
||||
# Extended: Retrieves the number of the row that is visible and currently at the
|
||||
# top of the editor.
|
||||
#
|
||||
# Returns a {Number}.
|
||||
getFirstVisibleScreenRow: ->
|
||||
@getVisibleRowRange()[0]
|
||||
|
||||
# Extended: Retrieves the number of the row that is visible and currently at the
|
||||
# bottom of the editor.
|
||||
#
|
||||
# Returns a {Number}.
|
||||
getLastVisibleScreenRow: ->
|
||||
@getVisibleRowRange()[1]
|
||||
|
||||
# Extended: call the given `callback` when the editor is attached to the DOM.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
onDidAttach: (callback) ->
|
||||
@emitter.on("did-attach", callback)
|
||||
|
||||
# Extended: call the given `callback` when the editor is detached from the DOM.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
onDidDetach: (callback) ->
|
||||
@emitter.on("did-detach", callback)
|
||||
|
||||
onDidChangeScrollTop: (callback) ->
|
||||
@emitter.on("did-change-scroll-top", callback)
|
||||
|
||||
onDidChangeScrollLeft: (callback) ->
|
||||
@emitter.on("did-change-scroll-left", callback)
|
||||
|
||||
setScrollLeft: (scrollLeft) ->
|
||||
@component.setScrollLeft(scrollLeft)
|
||||
|
||||
setScrollRight: (scrollRight) ->
|
||||
@component.setScrollRight(scrollRight)
|
||||
|
||||
setScrollTop: (scrollTop) ->
|
||||
@component.setScrollTop(scrollTop)
|
||||
|
||||
setScrollBottom: (scrollBottom) ->
|
||||
@component.setScrollBottom(scrollBottom)
|
||||
|
||||
# Essential: Scrolls the editor to the top
|
||||
scrollToTop: ->
|
||||
@setScrollTop(0)
|
||||
|
||||
# Essential: Scrolls the editor to the bottom
|
||||
scrollToBottom: ->
|
||||
@setScrollBottom(Infinity)
|
||||
|
||||
getScrollTop: ->
|
||||
@component?.getScrollTop() or 0
|
||||
|
||||
getScrollLeft: ->
|
||||
@component?.getScrollLeft() or 0
|
||||
|
||||
getScrollRight: ->
|
||||
@component?.getScrollRight() or 0
|
||||
|
||||
getScrollBottom: ->
|
||||
@component?.getScrollBottom() or 0
|
||||
|
||||
getScrollHeight: ->
|
||||
@component?.getScrollHeight() or 0
|
||||
|
||||
getScrollWidth: ->
|
||||
@component?.getScrollWidth() or 0
|
||||
|
||||
getVerticalScrollbarWidth: ->
|
||||
@component?.getVerticalScrollbarWidth() or 0
|
||||
|
||||
getHorizontalScrollbarHeight: ->
|
||||
@component?.getHorizontalScrollbarHeight() or 0
|
||||
|
||||
getVisibleRowRange: ->
|
||||
@component?.getVisibleRowRange() or [0, 0]
|
||||
|
||||
intersectsVisibleRowRange: (startRow, endRow) ->
|
||||
[visibleStart, visibleEnd] = @getVisibleRowRange()
|
||||
not (endRow <= visibleStart or visibleEnd <= startRow)
|
||||
|
||||
selectionIntersectsVisibleRowRange: (selection) ->
|
||||
{start, end} = selection.getScreenRange()
|
||||
@intersectsVisibleRowRange(start.row, end.row + 1)
|
||||
|
||||
screenPositionForPixelPosition: (pixelPosition) ->
|
||||
@component.screenPositionForPixelPosition(pixelPosition)
|
||||
|
||||
pixelRectForScreenRange: (screenRange) ->
|
||||
@component.pixelRectForScreenRange(screenRange)
|
||||
|
||||
pixelRangeForScreenRange: (screenRange) ->
|
||||
@component.pixelRangeForScreenRange(screenRange)
|
||||
|
||||
setWidth: (width) ->
|
||||
@style.width = (@component.getGutterWidth() + width) + "px"
|
||||
|
||||
getWidth: ->
|
||||
@offsetWidth - @component.getGutterWidth()
|
||||
|
||||
setHeight: (height) ->
|
||||
@style.height = height + "px"
|
||||
|
||||
getHeight: ->
|
||||
@offsetHeight
|
||||
|
||||
# Experimental: Invalidate the passed block {Decoration} dimensions, forcing
|
||||
# them to be recalculated and the surrounding content to be adjusted on the
|
||||
# next animation frame.
|
||||
#
|
||||
# * {blockDecoration} A {Decoration} representing the block decoration you
|
||||
# want to update the dimensions of.
|
||||
invalidateBlockDecorationDimensions: ->
|
||||
@component.invalidateBlockDecorationDimensions(arguments...)
|
||||
|
||||
module.exports = TextEditorElement = document.registerElement 'atom-text-editor', prototype: TextEditorElement.prototype
|
||||
346
src/text-editor-element.js
Normal file
346
src/text-editor-element.js
Normal file
@@ -0,0 +1,346 @@
|
||||
const {Emitter, Range} = require('atom')
|
||||
const Grim = require('grim')
|
||||
const TextEditorComponent = require('./text-editor-component')
|
||||
const dedent = require('dedent')
|
||||
|
||||
class TextEditorElement extends HTMLElement {
|
||||
initialize (component) {
|
||||
this.component = component
|
||||
return this
|
||||
}
|
||||
|
||||
get shadowRoot () {
|
||||
Grim.deprecate(dedent`
|
||||
The contents of \`atom-text-editor\` elements are no longer encapsulated
|
||||
within a shadow DOM boundary. Please, stop using \`shadowRoot\` and access
|
||||
the editor contents directly instead.
|
||||
`)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
get rootElement () {
|
||||
Grim.deprecate(dedent`
|
||||
The contents of \`atom-text-editor\` elements are no longer encapsulated
|
||||
within a shadow DOM boundary. Please, stop using \`rootElement\` and access
|
||||
the editor contents directly instead.
|
||||
`)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
createdCallback () {
|
||||
this.emitter = new Emitter()
|
||||
this.initialText = this.textContent
|
||||
this.tabIndex = -1
|
||||
this.addEventListener('focus', (event) => this.getComponent().didFocus(event))
|
||||
this.addEventListener('blur', (event) => this.getComponent().didBlur(event))
|
||||
}
|
||||
|
||||
attachedCallback () {
|
||||
this.getComponent().didAttach()
|
||||
this.emitter.emit('did-attach')
|
||||
}
|
||||
|
||||
detachedCallback () {
|
||||
this.emitter.emit('did-detach')
|
||||
this.getComponent().didDetach()
|
||||
}
|
||||
|
||||
attributeChangedCallback (name, oldValue, newValue) {
|
||||
if (this.component) {
|
||||
switch (name) {
|
||||
case 'mini':
|
||||
this.getModel().update({mini: newValue != null})
|
||||
break
|
||||
case 'placeholder-text':
|
||||
this.getModel().update({placeholderText: newValue})
|
||||
break
|
||||
case 'gutter-hidden':
|
||||
this.getModel().update({lineNumberGutterVisible: newValue == null})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extended: Get a promise that resolves the next time the element's DOM
|
||||
// is updated in any way.
|
||||
//
|
||||
// This can be useful when you've made a change to the model and need to
|
||||
// be sure this change has been flushed to the DOM.
|
||||
//
|
||||
// Returns a {Promise}.
|
||||
getNextUpdatePromise () {
|
||||
return this.getComponent().getNextUpdatePromise()
|
||||
}
|
||||
|
||||
getModel () {
|
||||
return this.getComponent().props.model
|
||||
}
|
||||
|
||||
setModel (model) {
|
||||
this.getComponent().update({model})
|
||||
this.updateModelFromAttributes()
|
||||
}
|
||||
|
||||
updateModelFromAttributes () {
|
||||
const props = {mini: this.hasAttribute('mini')}
|
||||
if (this.hasAttribute('placeholder-text')) props.placeholderText = this.getAttribute('placeholder-text')
|
||||
if (this.hasAttribute('gutter-hidden')) props.lineNumberGutterVisible = false
|
||||
|
||||
this.getModel().update(props)
|
||||
if (this.initialText) this.getModel().setText(this.initialText)
|
||||
}
|
||||
|
||||
onDidAttach (callback) {
|
||||
return this.emitter.on('did-attach', callback)
|
||||
}
|
||||
|
||||
onDidDetach (callback) {
|
||||
return this.emitter.on('did-detach', callback)
|
||||
}
|
||||
|
||||
measureDimensions () {
|
||||
this.getComponent().measureDimensions()
|
||||
}
|
||||
|
||||
setWidth (width) {
|
||||
this.style.width = this.getComponent().getGutterContainerWidth() + width + 'px'
|
||||
}
|
||||
|
||||
getWidth () {
|
||||
return this.getComponent().getScrollContainerWidth()
|
||||
}
|
||||
|
||||
setHeight (height) {
|
||||
this.style.height = height + 'px'
|
||||
}
|
||||
|
||||
getHeight () {
|
||||
return this.getComponent().getScrollContainerHeight()
|
||||
}
|
||||
|
||||
onDidChangeScrollLeft (callback) {
|
||||
return this.emitter.on('did-change-scroll-left', callback)
|
||||
}
|
||||
|
||||
onDidChangeScrollTop (callback) {
|
||||
return this.emitter.on('did-change-scroll-top', callback)
|
||||
}
|
||||
|
||||
// Deprecated: get the width of an `x` character displayed in this element.
|
||||
//
|
||||
// Returns a {Number} of pixels.
|
||||
getDefaultCharacterWidth () {
|
||||
return this.getComponent().getBaseCharacterWidth()
|
||||
}
|
||||
|
||||
// Extended: get the width of an `x` character displayed in this element.
|
||||
//
|
||||
// Returns a {Number} of pixels.
|
||||
getBaseCharacterWidth () {
|
||||
return this.getComponent().getBaseCharacterWidth()
|
||||
}
|
||||
|
||||
getMaxScrollTop () {
|
||||
return this.getComponent().getMaxScrollTop()
|
||||
}
|
||||
|
||||
getScrollHeight () {
|
||||
return this.getComponent().getScrollHeight()
|
||||
}
|
||||
|
||||
getScrollWidth () {
|
||||
return this.getComponent().getScrollWidth()
|
||||
}
|
||||
|
||||
getVerticalScrollbarWidth () {
|
||||
return this.getComponent().getVerticalScrollbarWidth()
|
||||
}
|
||||
|
||||
getHorizontalScrollbarHeight () {
|
||||
return this.getComponent().getHorizontalScrollbarHeight()
|
||||
}
|
||||
|
||||
getScrollTop () {
|
||||
return this.getComponent().getScrollTop()
|
||||
}
|
||||
|
||||
setScrollTop (scrollTop) {
|
||||
const component = this.getComponent()
|
||||
component.setScrollTop(scrollTop)
|
||||
component.scheduleUpdate()
|
||||
}
|
||||
|
||||
getScrollBottom () {
|
||||
return this.getComponent().getScrollBottom()
|
||||
}
|
||||
|
||||
setScrollBottom (scrollBottom) {
|
||||
return this.getComponent().setScrollBottom(scrollBottom)
|
||||
}
|
||||
|
||||
getScrollLeft () {
|
||||
return this.getComponent().getScrollLeft()
|
||||
}
|
||||
|
||||
setScrollLeft (scrollLeft) {
|
||||
const component = this.getComponent()
|
||||
component.setScrollLeft(scrollLeft)
|
||||
component.scheduleUpdate()
|
||||
}
|
||||
|
||||
getScrollRight () {
|
||||
return this.getComponent().getScrollRight()
|
||||
}
|
||||
|
||||
setScrollRight (scrollRight) {
|
||||
return this.getComponent().setScrollRight(scrollRight)
|
||||
}
|
||||
|
||||
// Essential: Scrolls the editor to the top.
|
||||
scrollToTop () {
|
||||
this.setScrollTop(0)
|
||||
}
|
||||
|
||||
// Essential: Scrolls the editor to the bottom.
|
||||
scrollToBottom () {
|
||||
this.setScrollTop(Infinity)
|
||||
}
|
||||
|
||||
hasFocus () {
|
||||
return this.getComponent().focused
|
||||
}
|
||||
|
||||
// Extended: Converts a buffer position to a pixel position.
|
||||
//
|
||||
// * `bufferPosition` A {Point}-like object that represents a buffer position.
|
||||
//
|
||||
// Be aware that calling this method with a column that does not translate
|
||||
// to column 0 on screen could cause a synchronous DOM update in order to
|
||||
// measure the requested horizontal pixel position if it isn't already
|
||||
// cached.
|
||||
//
|
||||
// Returns an {Object} with two values: `top` and `left`, representing the
|
||||
// pixel position.
|
||||
pixelPositionForBufferPosition (bufferPosition) {
|
||||
const screenPosition = this.getModel().screenPositionForBufferPosition(bufferPosition)
|
||||
return this.getComponent().pixelPositionForScreenPosition(screenPosition)
|
||||
}
|
||||
|
||||
// Extended: Converts a screen position to a pixel position.
|
||||
//
|
||||
// * `screenPosition` A {Point}-like object that represents a buffer position.
|
||||
//
|
||||
// Be aware that calling this method with a non-zero column value could
|
||||
// cause a synchronous DOM update in order to measure the requested
|
||||
// horizontal pixel position if it isn't already cached.
|
||||
//
|
||||
// Returns an {Object} with two values: `top` and `left`, representing the
|
||||
// pixel position.
|
||||
pixelPositionForScreenPosition (screenPosition) {
|
||||
screenPosition = this.getModel().clipScreenPosition(screenPosition)
|
||||
return this.getComponent().pixelPositionForScreenPosition(screenPosition)
|
||||
}
|
||||
|
||||
screenPositionForPixelPosition (pixelPosition) {
|
||||
return this.getComponent().screenPositionForPixelPosition(pixelPosition)
|
||||
}
|
||||
|
||||
pixelRectForScreenRange (range) {
|
||||
range = Range.fromObject(range)
|
||||
|
||||
const start = this.pixelPositionForScreenPosition(range.start)
|
||||
const end = this.pixelPositionForScreenPosition(range.end)
|
||||
const lineHeight = this.getComponent().getLineHeight()
|
||||
|
||||
return {
|
||||
top: start.top,
|
||||
left: start.left,
|
||||
height: end.top + lineHeight - start.top,
|
||||
width: end.left - start.left
|
||||
}
|
||||
}
|
||||
|
||||
pixelRangeForScreenRange (range) {
|
||||
range = Range.fromObject(range)
|
||||
return {
|
||||
start: this.pixelPositionForScreenPosition(range.start),
|
||||
end: this.pixelPositionForScreenPosition(range.end)
|
||||
}
|
||||
}
|
||||
|
||||
getComponent () {
|
||||
if (!this.component) {
|
||||
this.component = new TextEditorComponent({
|
||||
element: this,
|
||||
mini: this.hasAttribute('mini'),
|
||||
updatedSynchronously: this.updatedSynchronously
|
||||
})
|
||||
this.updateModelFromAttributes()
|
||||
}
|
||||
|
||||
return this.component
|
||||
}
|
||||
|
||||
setUpdatedSynchronously (updatedSynchronously) {
|
||||
this.updatedSynchronously = updatedSynchronously
|
||||
if (this.component) this.component.updatedSynchronously = updatedSynchronously
|
||||
return updatedSynchronously
|
||||
}
|
||||
|
||||
isUpdatedSynchronously () {
|
||||
return this.component ? this.component.updatedSynchronously : this.updatedSynchronously
|
||||
}
|
||||
|
||||
// Experimental: Invalidate the passed block {Decoration}'s dimensions,
|
||||
// forcing them to be recalculated and the surrounding content to be adjusted
|
||||
// on the next animation frame.
|
||||
//
|
||||
// * {blockDecoration} A {Decoration} representing the block decoration you
|
||||
// want to update the dimensions of.
|
||||
invalidateBlockDecorationDimensions () {
|
||||
this.getComponent().invalidateBlockDecorationDimensions(...arguments)
|
||||
}
|
||||
|
||||
setFirstVisibleScreenRow (row) {
|
||||
this.getModel().setFirstVisibleScreenRow(row)
|
||||
}
|
||||
|
||||
getFirstVisibleScreenRow () {
|
||||
return this.getModel().getFirstVisibleScreenRow()
|
||||
}
|
||||
|
||||
getLastVisibleScreenRow () {
|
||||
return this.getModel().getLastVisibleScreenRow()
|
||||
}
|
||||
|
||||
getVisibleRowRange () {
|
||||
return this.getModel().getVisibleRowRange()
|
||||
}
|
||||
|
||||
intersectsVisibleRowRange (startRow, endRow) {
|
||||
return !(
|
||||
endRow <= this.getFirstVisibleScreenRow() ||
|
||||
this.getLastVisibleScreenRow() <= startRow
|
||||
)
|
||||
}
|
||||
|
||||
selectionIntersectsVisibleRowRange (selection) {
|
||||
const {start, end} = selection.getScreenRange()
|
||||
return this.intersectsVisibleRowRange(start.row, end.row + 1)
|
||||
}
|
||||
|
||||
setFirstVisibleScreenColumn (column) {
|
||||
return this.getModel().setFirstVisibleScreenColumn(column)
|
||||
}
|
||||
|
||||
getFirstVisibleScreenColumn () {
|
||||
return this.getModel().getFirstVisibleScreenColumn()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports =
|
||||
document.registerElement('atom-text-editor', {
|
||||
prototype: TextEditorElement.prototype
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,8 @@ Model = require './model'
|
||||
Selection = require './selection'
|
||||
TextMateScopeSelector = require('first-mate').ScopeSelector
|
||||
GutterContainer = require './gutter-container'
|
||||
TextEditorElement = require './text-editor-element'
|
||||
TextEditorComponent = null
|
||||
TextEditorElement = null
|
||||
{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils'
|
||||
|
||||
ZERO_WIDTH_NBSP = '\ufeff'
|
||||
@@ -61,6 +62,20 @@ class TextEditor extends Model
|
||||
@setClipboard: (clipboard) ->
|
||||
@clipboard = clipboard
|
||||
|
||||
@setScheduler: (scheduler) ->
|
||||
TextEditorComponent ?= require './text-editor-component'
|
||||
TextEditorComponent.setScheduler(scheduler)
|
||||
|
||||
@didUpdateStyles: ->
|
||||
TextEditorComponent ?= require './text-editor-component'
|
||||
TextEditorComponent.didUpdateStyles()
|
||||
|
||||
@didUpdateScrollbarStyles: ->
|
||||
TextEditorComponent ?= require './text-editor-component'
|
||||
TextEditorComponent.didUpdateScrollbarStyles()
|
||||
|
||||
@viewForItem: (item) -> item.element ? item
|
||||
|
||||
serializationVersion: 1
|
||||
|
||||
buffer: null
|
||||
@@ -89,6 +104,17 @@ class TextEditor extends Model
|
||||
Object.defineProperty @prototype, "element",
|
||||
get: -> @getElement()
|
||||
|
||||
Object.defineProperty @prototype, "editorElement",
|
||||
get: ->
|
||||
Grim.deprecate("""
|
||||
`TextEditor.prototype.editorElement` has always been private, but now
|
||||
it is gone. Reading the `editorElement` property still returns a
|
||||
reference to the editor element but this field will be removed in a
|
||||
later version of Atom, so we recommend using the `element` property instead.
|
||||
""")
|
||||
|
||||
@getElement()
|
||||
|
||||
Object.defineProperty(@prototype, 'displayBuffer', get: ->
|
||||
Grim.deprecate("""
|
||||
`TextEditor.prototype.displayBuffer` has always been private, but now
|
||||
@@ -128,7 +154,7 @@ class TextEditor extends Model
|
||||
super
|
||||
|
||||
{
|
||||
@softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength,
|
||||
@softTabs, @initialScrollTopRow, @initialScrollLeftColumn, initialLine, initialColumn, tabLength,
|
||||
@softWrapped, @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation,
|
||||
@mini, @placeholderText, lineNumberGutterVisible, @largeFileMode,
|
||||
@assert, grammar, @showInvisibles, @autoHeight, @autoWidth, @scrollPastEnd, @editorWidthInChars,
|
||||
@@ -138,8 +164,6 @@ class TextEditor extends Model
|
||||
} = params
|
||||
|
||||
@assert ?= (condition) -> condition
|
||||
@firstVisibleScreenRow ?= 0
|
||||
@firstVisibleScreenColumn ?= 0
|
||||
@emitter = new Emitter
|
||||
@disposables = new CompositeDisposable
|
||||
@cursors = []
|
||||
@@ -198,7 +222,10 @@ class TextEditor extends Model
|
||||
@selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true)
|
||||
@selectionsMarkerLayer.trackDestructionInOnDidCreateMarkerCallbacks = true
|
||||
|
||||
@decorationManager = new DecorationManager(@displayLayer)
|
||||
@decorationManager = new DecorationManager(this)
|
||||
@decorateMarkerLayer(@selectionsMarkerLayer, type: 'cursor')
|
||||
@decorateCursorLine() unless @isMini()
|
||||
|
||||
@decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'})
|
||||
|
||||
for marker in @selectionsMarkerLayer.getMarkers()
|
||||
@@ -220,13 +247,23 @@ class TextEditor extends Model
|
||||
priority: 0
|
||||
visible: lineNumberGutterVisible
|
||||
|
||||
decorateCursorLine: ->
|
||||
@cursorLineDecorations = [
|
||||
@decorateMarkerLayer(@selectionsMarkerLayer, type: 'line', class: 'cursor-line', onlyEmpty: true),
|
||||
@decorateMarkerLayer(@selectionsMarkerLayer, type: 'line-number', class: 'cursor-line'),
|
||||
@decorateMarkerLayer(@selectionsMarkerLayer, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true)
|
||||
]
|
||||
|
||||
doBackgroundWork: (deadline) =>
|
||||
previousLongestRow = @getApproximateLongestScreenRow()
|
||||
if @displayLayer.doBackgroundWork(deadline)
|
||||
@presenter?.updateVerticalDimensions()
|
||||
@backgroundWorkHandle = requestIdleCallback(@doBackgroundWork)
|
||||
else
|
||||
@backgroundWorkHandle = null
|
||||
|
||||
if @getApproximateLongestScreenRow() isnt previousLongestRow
|
||||
@component?.scheduleUpdate()
|
||||
|
||||
update: (params) ->
|
||||
displayLayerParams = {}
|
||||
|
||||
@@ -292,6 +329,12 @@ class TextEditor extends Model
|
||||
displayLayerParams.invisibles = @getInvisibles()
|
||||
displayLayerParams.softWrapColumn = @getSoftWrapColumn()
|
||||
displayLayerParams.showIndentGuides = @doesShowIndentGuide()
|
||||
if @mini
|
||||
decoration.destroy() for decoration in @cursorLineDecorations
|
||||
@cursorLineDecorations = null
|
||||
else
|
||||
@decorateCursorLine()
|
||||
@component?.scheduleUpdate()
|
||||
|
||||
when 'placeholderText'
|
||||
if value isnt @placeholderText
|
||||
@@ -314,7 +357,6 @@ class TextEditor extends Model
|
||||
when 'showLineNumbers'
|
||||
if value isnt @showLineNumbers
|
||||
@showLineNumbers = value
|
||||
@presenter?.didChangeShowLineNumbers()
|
||||
|
||||
when 'showInvisibles'
|
||||
if value isnt @showInvisibles
|
||||
@@ -339,22 +381,20 @@ class TextEditor extends Model
|
||||
when 'scrollPastEnd'
|
||||
if value isnt @scrollPastEnd
|
||||
@scrollPastEnd = value
|
||||
@presenter?.didChangeScrollPastEnd()
|
||||
@component?.scheduleUpdate()
|
||||
|
||||
when 'autoHeight'
|
||||
if value isnt @autoHeight
|
||||
@autoHeight = value
|
||||
@presenter?.setAutoHeight(@autoHeight)
|
||||
|
||||
when 'autoWidth'
|
||||
if value isnt @autoWidth
|
||||
@autoWidth = value
|
||||
@presenter?.didChangeAutoWidth()
|
||||
|
||||
when 'showCursorOnSelection'
|
||||
if value isnt @showCursorOnSelection
|
||||
@showCursorOnSelection = value
|
||||
cursor.setShowCursorOnSelection(value) for cursor in @getCursors()
|
||||
@component?.scheduleUpdate()
|
||||
|
||||
else
|
||||
if param isnt 'ref' and param isnt 'key'
|
||||
@@ -362,11 +402,14 @@ class TextEditor extends Model
|
||||
|
||||
@displayLayer.reset(displayLayerParams)
|
||||
|
||||
if @editorElement?
|
||||
@editorElement.views.getNextUpdatePromise()
|
||||
if @component?
|
||||
@component.getNextUpdatePromise()
|
||||
else
|
||||
Promise.resolve()
|
||||
|
||||
scheduleComponentUpdate: ->
|
||||
@component?.scheduleUpdate()
|
||||
|
||||
serialize: ->
|
||||
tokenizedBufferState = @tokenizedBuffer.serialize()
|
||||
|
||||
@@ -381,8 +424,8 @@ class TextEditor extends Model
|
||||
displayLayerId: @displayLayer.id
|
||||
selectionsMarkerLayerId: @selectionsMarkerLayer.id
|
||||
|
||||
firstVisibleScreenRow: @getFirstVisibleScreenRow()
|
||||
firstVisibleScreenColumn: @getFirstVisibleScreenColumn()
|
||||
initialScrollTopRow: @getScrollTopRow()
|
||||
initialScrollLeftColumn: @getScrollLeftColumn()
|
||||
|
||||
atomicSoftTabs: @displayLayer.atomicSoftTabs
|
||||
softWrapHangingIndentLength: @displayLayer.softWrapHangingIndent
|
||||
@@ -413,14 +456,17 @@ class TextEditor extends Model
|
||||
@emitter.on 'did-terminate-pending-state', callback
|
||||
|
||||
subscribeToDisplayLayer: ->
|
||||
@disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this)
|
||||
@disposables.add @tokenizedBuffer.onDidChangeGrammar @handleGrammarChange.bind(this)
|
||||
@disposables.add @displayLayer.onDidChangeSync (e) =>
|
||||
@mergeIntersectingSelections()
|
||||
@component?.didChangeDisplayLayer(e)
|
||||
@emitter.emit 'did-change', e
|
||||
@disposables.add @displayLayer.onDidReset =>
|
||||
@mergeIntersectingSelections()
|
||||
@component?.didResetDisplayLayer()
|
||||
@emitter.emit 'did-change', {}
|
||||
@disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this)
|
||||
@disposables.add @selectionsMarkerLayer.onDidUpdate => @component?.didUpdateSelections()
|
||||
|
||||
destroyed: ->
|
||||
@disposables.dispose()
|
||||
@@ -432,8 +478,9 @@ class TextEditor extends Model
|
||||
@gutterContainer.destroy()
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.clear()
|
||||
@editorElement = null
|
||||
@presenter = null
|
||||
@component?.element.component = null
|
||||
@component = null
|
||||
@lineNumberGutter.element = null
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
@@ -688,6 +735,11 @@ class TextEditor extends Model
|
||||
onDidRemoveDecoration: (callback) ->
|
||||
@decorationManager.onDidRemoveDecoration(callback)
|
||||
|
||||
# Called by DecorationManager when a decoration is added.
|
||||
didAddDecoration: (decoration) ->
|
||||
if decoration.isType('block')
|
||||
@component?.didAddBlockDecoration(decoration)
|
||||
|
||||
# Extended: Calls your `callback` when the placeholder text is changed.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
@@ -697,9 +749,6 @@ class TextEditor extends Model
|
||||
onDidChangePlaceholderText: (callback) ->
|
||||
@emitter.on 'did-change-placeholder-text', 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.")
|
||||
|
||||
@@ -735,7 +784,8 @@ class TextEditor extends Model
|
||||
@buffer, selectionsMarkerLayer, softTabs,
|
||||
suppressCursorCreation: true,
|
||||
tabLength: @tokenizedBuffer.getTabLength(),
|
||||
@firstVisibleScreenRow, @firstVisibleScreenColumn,
|
||||
initialScrollTopRow: @getScrollTopRow(),
|
||||
initialScrollLeftColumn: @getScrollLeftColumn(),
|
||||
@assert, displayLayer, grammar: @getGrammar(),
|
||||
@autoWidth, @autoHeight, @showCursorOnSelection
|
||||
})
|
||||
@@ -749,9 +799,6 @@ class TextEditor extends Model
|
||||
|
||||
isMini: -> @mini
|
||||
|
||||
setUpdatedSynchronously: (updatedSynchronously) ->
|
||||
@decorationManager.setUpdatedSynchronously(updatedSynchronously)
|
||||
|
||||
onDidChangeMini: (callback) ->
|
||||
@emitter.on 'did-change-mini', callback
|
||||
|
||||
@@ -971,29 +1018,28 @@ class TextEditor extends Model
|
||||
tokens = []
|
||||
lineTextIndex = 0
|
||||
currentTokenScopes = []
|
||||
{lineText, tagCodes} = @screenLineForScreenRow(screenRow)
|
||||
for tagCode in tagCodes
|
||||
if @displayLayer.isOpenTagCode(tagCode)
|
||||
currentTokenScopes.push(@displayLayer.tagForCode(tagCode))
|
||||
else if @displayLayer.isCloseTagCode(tagCode)
|
||||
{lineText, tags} = @screenLineForScreenRow(screenRow)
|
||||
for tag in tags
|
||||
if @displayLayer.isOpenTag(tag)
|
||||
currentTokenScopes.push(@displayLayer.classNameForTag(tag))
|
||||
else if @displayLayer.isCloseTag(tag)
|
||||
currentTokenScopes.pop()
|
||||
else
|
||||
tokens.push({
|
||||
text: lineText.substr(lineTextIndex, tagCode)
|
||||
text: lineText.substr(lineTextIndex, tag)
|
||||
scopes: currentTokenScopes.slice()
|
||||
})
|
||||
lineTextIndex += tagCode
|
||||
lineTextIndex += tag
|
||||
tokens
|
||||
|
||||
screenLineForScreenRow: (screenRow) ->
|
||||
@displayLayer.getScreenLines(screenRow, screenRow + 1)[0]
|
||||
@displayLayer.getScreenLine(screenRow)
|
||||
|
||||
bufferRowForScreenRow: (screenRow) ->
|
||||
@displayLayer.translateScreenPosition(Point(screenRow, 0)).row
|
||||
|
||||
bufferRowsForScreenRows: (startScreenRow, endScreenRow) ->
|
||||
for screenRow in [startScreenRow..endScreenRow]
|
||||
@bufferRowForScreenRow(screenRow)
|
||||
@displayLayer.bufferRowsForScreenRows(startScreenRow, endScreenRow + 1)
|
||||
|
||||
screenRowForBufferRow: (row) ->
|
||||
@displayLayer.translateBufferPosition(Point(row, 0)).row
|
||||
@@ -1162,7 +1208,7 @@ class TextEditor extends Model
|
||||
|
||||
# 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'
|
||||
lines += @buffer.lineEndingForRow(linesRange.end.row - 2) unless lines[lines.length - 1] is '\n'
|
||||
@buffer.delete(linesRange)
|
||||
@buffer.insert([precedingRow, 0], lines)
|
||||
|
||||
@@ -1751,20 +1797,32 @@ class TextEditor extends Model
|
||||
# * `block` Positions the view associated with the given item before or
|
||||
# after the row of the given `TextEditorMarker`, depending on the `position`
|
||||
# property.
|
||||
# * `cursor` Renders a cursor at the head of the given marker. If multiple
|
||||
# decorations are created for the same marker, their class strings and
|
||||
# style objects are combined into a single cursor. You can use this
|
||||
# decoration type to style existing cursors by passing in their markers
|
||||
# or render artificial cursors that don't actually exist in the model
|
||||
# by passing a marker that isn't actually associated with a cursor.
|
||||
# * `class` This CSS class will be applied to the decorated line number,
|
||||
# line, highlight, or overlay.
|
||||
# * `style` An {Object} containing CSS style properties to apply to the
|
||||
# relevant DOM node. Currently this only works with a `type` of `cursor`.
|
||||
# * `item` (optional) An {HTMLElement} or a model {Object} with a
|
||||
# corresponding view registered. Only applicable to the `gutter`,
|
||||
# `overlay` and `block` types.
|
||||
# `overlay` and `block` decoration types.
|
||||
# * `onlyHead` (optional) If `true`, the decoration will only be applied to
|
||||
# the head of the `DisplayMarker`. Only applicable to the `line` and
|
||||
# `line-number` types.
|
||||
# `line-number` decoration types.
|
||||
# * `onlyEmpty` (optional) If `true`, the decoration will only be applied if
|
||||
# the associated `DisplayMarker` is empty. Only applicable to the `gutter`,
|
||||
# `line`, and `line-number` types.
|
||||
# `line`, and `line-number` decoration types.
|
||||
# * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
|
||||
# if the associated `DisplayMarker` is non-empty. Only applicable to the
|
||||
# `gutter`, `line`, and `line-number` types.
|
||||
# `gutter`, `line`, and `line-number` decoration types.
|
||||
# * `omitEmptyLastRow` (optional) If `false`, the decoration will be applied
|
||||
# to the last row of a non-empty range, even if it ends at column 0.
|
||||
# Defaults to `true`. Only applicable to the `gutter`, `line`, and
|
||||
# `line-number` decoration types.
|
||||
# * `position` (optional) Only applicable to decorations of type `overlay` and `block`.
|
||||
# Controls where the view is positioned relative to the `TextEditorMarker`.
|
||||
# Values can be `'head'` (the default) or `'tail'` for overlay decorations, and
|
||||
@@ -1852,12 +1910,6 @@ class TextEditor extends Model
|
||||
getOverlayDecorations: (propertyFilter) ->
|
||||
@decorationManager.getOverlayDecorations(propertyFilter)
|
||||
|
||||
decorationForId: (id) ->
|
||||
@decorationManager.decorationForId(id)
|
||||
|
||||
decorationsForMarkerId: (id) ->
|
||||
@decorationManager.decorationsForMarkerId(id)
|
||||
|
||||
###
|
||||
Section: Markers
|
||||
###
|
||||
@@ -2096,9 +2148,9 @@ class TextEditor extends Model
|
||||
#
|
||||
# Returns the first matched {Cursor} or undefined
|
||||
getCursorAtScreenPosition: (position) ->
|
||||
for cursor in @cursors
|
||||
return cursor if cursor.getScreenPosition().isEqual(position)
|
||||
undefined
|
||||
if selection = @getSelectionAtScreenPosition(position)
|
||||
if selection.getHeadScreenPosition().isEqual(position)
|
||||
selection.cursor
|
||||
|
||||
# Essential: Get the position of the most recently added cursor in screen
|
||||
# coordinates.
|
||||
@@ -2140,7 +2192,7 @@ class TextEditor extends Model
|
||||
#
|
||||
# Returns a {Cursor}.
|
||||
addCursorAtBufferPosition: (bufferPosition, options) ->
|
||||
@selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'})
|
||||
@selectionsMarkerLayer.markBufferPosition(bufferPosition, Object.assign({invalidate: 'never'}, options))
|
||||
@getLastSelection().cursor.autoscroll() unless options?.autoscroll is false
|
||||
@getLastSelection().cursor
|
||||
|
||||
@@ -2287,14 +2339,12 @@ class TextEditor extends Model
|
||||
cursor = new Cursor(editor: this, marker: marker, showCursorOnSelection: @showCursorOnSelection)
|
||||
@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)
|
||||
cursor
|
||||
|
||||
moveCursors: (fn) ->
|
||||
fn(cursor) for cursor in @getCursors()
|
||||
@mergeCursors()
|
||||
@transact =>
|
||||
fn(cursor) for cursor in @getCursors()
|
||||
@mergeCursors()
|
||||
|
||||
cursorMoved: (event) ->
|
||||
@emitter.emit 'did-change-cursor-position', event
|
||||
@@ -2650,6 +2700,11 @@ class TextEditor extends Model
|
||||
@createLastSelectionIfNeeded()
|
||||
_.last(@selections)
|
||||
|
||||
getSelectionAtScreenPosition: (position) ->
|
||||
markers = @selectionsMarkerLayer.findMarkers(containsScreenPosition: position)
|
||||
if markers.length > 0
|
||||
@cursorsByMarkerId.get(markers[0].id).selection
|
||||
|
||||
# Extended: Get current {Selection}s.
|
||||
#
|
||||
# Returns: An {Array} of {Selection}s.
|
||||
@@ -2808,6 +2863,7 @@ class TextEditor extends Model
|
||||
|
||||
# Called by the selection
|
||||
selectionRangeChanged: (event) ->
|
||||
@component?.didChangeSelectionRange()
|
||||
@emitter.emit 'did-change-selection-range', event
|
||||
|
||||
createLastSelectionIfNeeded: ->
|
||||
@@ -3321,7 +3377,7 @@ class TextEditor extends Model
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isFoldedAtCursorRow: ->
|
||||
@isFoldedAtScreenRow(@getCursorScreenPosition().row)
|
||||
@isFoldedAtBufferRow(@getCursorBufferPosition().row)
|
||||
|
||||
# Extended: Determine whether the given row in buffer coordinates is folded.
|
||||
#
|
||||
@@ -3329,7 +3385,11 @@ class TextEditor extends Model
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isFoldedAtBufferRow: (bufferRow) ->
|
||||
@displayLayer.foldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))).length > 0
|
||||
range = Range(
|
||||
Point(bufferRow, 0),
|
||||
Point(bufferRow, @buffer.lineLengthForRow(bufferRow))
|
||||
)
|
||||
@displayLayer.foldsIntersectingBufferRange(range).length > 0
|
||||
|
||||
# Extended: Determine whether the given row in screen coordinates is folded.
|
||||
#
|
||||
@@ -3379,6 +3439,9 @@ class TextEditor extends Model
|
||||
getGutters: ->
|
||||
@gutterContainer.getGutters()
|
||||
|
||||
getLineNumberGutter: ->
|
||||
@lineNumberGutter
|
||||
|
||||
# Essential: Get the gutter with the given name.
|
||||
#
|
||||
# Returns a {Gutter}, or `null` if no gutter exists for the given name.
|
||||
@@ -3426,7 +3489,9 @@ class TextEditor extends Model
|
||||
@getElement().scrollToBottom()
|
||||
|
||||
scrollToScreenRange: (screenRange, options = {}) ->
|
||||
screenRange = @clipScreenRange(screenRange) if options.clip isnt false
|
||||
scrollEvent = {screenRange, options}
|
||||
@component?.didRequestAutoscroll(scrollEvent)
|
||||
@emitter.emit "did-request-autoscroll", scrollEvent
|
||||
|
||||
getHorizontalScrollbarHeight: ->
|
||||
@@ -3453,9 +3518,12 @@ class TextEditor extends Model
|
||||
|
||||
# Returns the number of rows per page
|
||||
getRowsPerPage: ->
|
||||
Math.max(@rowsPerPage ? 1, 1)
|
||||
|
||||
setRowsPerPage: (@rowsPerPage) ->
|
||||
if @component?
|
||||
clientHeight = @component.getScrollContainerClientHeight()
|
||||
lineHeight = @component.getLineHeight()
|
||||
Math.max(1, Math.ceil(clientHeight / lineHeight))
|
||||
else
|
||||
1
|
||||
|
||||
###
|
||||
Section: Config
|
||||
@@ -3483,7 +3551,11 @@ class TextEditor extends Model
|
||||
# Experimental: Does this editor allow scrolling past the last line?
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
getScrollPastEnd: -> @scrollPastEnd
|
||||
getScrollPastEnd: ->
|
||||
if @getAutoHeight()
|
||||
false
|
||||
else
|
||||
@scrollPastEnd
|
||||
|
||||
# Experimental: How fast does the editor scroll in response to mouse wheel
|
||||
# movements?
|
||||
@@ -3543,7 +3615,17 @@ class TextEditor extends Model
|
||||
|
||||
# Get the Element for the editor.
|
||||
getElement: ->
|
||||
@editorElement ?= new TextEditorElement().initialize(this, atom)
|
||||
if @component?
|
||||
@component.element
|
||||
else
|
||||
TextEditorComponent ?= require('./text-editor-component')
|
||||
TextEditorElement ?= require('./text-editor-element')
|
||||
new TextEditorComponent({
|
||||
model: this,
|
||||
updatedSynchronously: TextEditorElement.prototype.updatedSynchronously,
|
||||
@initialScrollTopRow, @initialScrollLeftColumn
|
||||
})
|
||||
@component.element
|
||||
|
||||
# Essential: Retrieves the greyed out placeholder of a mini editor.
|
||||
#
|
||||
@@ -3600,66 +3682,51 @@ class TextEditor extends Model
|
||||
@doubleWidthCharWidth = doubleWidthCharWidth
|
||||
@halfWidthCharWidth = halfWidthCharWidth
|
||||
@koreanCharWidth = koreanCharWidth
|
||||
@displayLayer.reset({}) if @isSoftWrapped() and @getEditorWidthInChars()?
|
||||
if @isSoftWrapped()
|
||||
@displayLayer.reset({
|
||||
softWrapColumn: @getSoftWrapColumn()
|
||||
})
|
||||
defaultCharWidth
|
||||
|
||||
setHeight: (height, reentrant=false) ->
|
||||
if reentrant
|
||||
@height = height
|
||||
else
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.")
|
||||
@getElement().setHeight(height)
|
||||
setHeight: (height) ->
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.")
|
||||
@getElement().setHeight(height)
|
||||
|
||||
getHeight: ->
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.")
|
||||
@height
|
||||
@getElement().getHeight()
|
||||
|
||||
getAutoHeight: -> @autoHeight ? true
|
||||
|
||||
getAutoWidth: -> @autoWidth ? false
|
||||
|
||||
setWidth: (width, reentrant=false) ->
|
||||
if reentrant
|
||||
@update({width})
|
||||
@width
|
||||
else
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.")
|
||||
@getElement().setWidth(width)
|
||||
setWidth: (width) ->
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.")
|
||||
@getElement().setWidth(width)
|
||||
|
||||
getWidth: ->
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.")
|
||||
@width
|
||||
@getElement().getWidth()
|
||||
|
||||
# Experimental: Scroll the editor such that the given screen row is at the
|
||||
# top of the visible area.
|
||||
setFirstVisibleScreenRow: (screenRow, fromView) ->
|
||||
unless fromView
|
||||
maxScreenRow = @getScreenLineCount() - 1
|
||||
unless @scrollPastEnd
|
||||
if @height? and @lineHeightInPixels?
|
||||
maxScreenRow -= Math.floor(@height / @lineHeightInPixels)
|
||||
screenRow = Math.max(Math.min(screenRow, maxScreenRow), 0)
|
||||
# Use setScrollTopRow instead of this method
|
||||
setFirstVisibleScreenRow: (screenRow) ->
|
||||
@setScrollTopRow(screenRow)
|
||||
|
||||
unless screenRow is @firstVisibleScreenRow
|
||||
@firstVisibleScreenRow = screenRow
|
||||
@emitter.emit 'did-change-first-visible-screen-row', screenRow unless fromView
|
||||
|
||||
getFirstVisibleScreenRow: -> @firstVisibleScreenRow
|
||||
getFirstVisibleScreenRow: ->
|
||||
@getElement().component.getFirstVisibleRow()
|
||||
|
||||
getLastVisibleScreenRow: ->
|
||||
if @height? and @lineHeightInPixels?
|
||||
Math.min(@firstVisibleScreenRow + Math.floor(@height / @lineHeightInPixels), @getScreenLineCount() - 1)
|
||||
else
|
||||
null
|
||||
@getElement().component.getLastVisibleRow()
|
||||
|
||||
getVisibleRowRange: ->
|
||||
if lastVisibleScreenRow = @getLastVisibleScreenRow()
|
||||
[@firstVisibleScreenRow, lastVisibleScreenRow]
|
||||
else
|
||||
null
|
||||
[@getFirstVisibleScreenRow(), @getLastVisibleScreenRow()]
|
||||
|
||||
setFirstVisibleScreenColumn: (@firstVisibleScreenColumn) ->
|
||||
getFirstVisibleScreenColumn: -> @firstVisibleScreenColumn
|
||||
# Use setScrollLeftColumn instead of this method
|
||||
setFirstVisibleScreenColumn: (column) ->
|
||||
@setScrollLeftColumn(column)
|
||||
|
||||
getFirstVisibleScreenColumn: ->
|
||||
@getElement().component.getFirstVisibleColumn()
|
||||
|
||||
getScrollTop: ->
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.")
|
||||
@@ -3716,6 +3783,18 @@ class TextEditor extends Model
|
||||
|
||||
@getElement().getMaxScrollTop()
|
||||
|
||||
getScrollTopRow: ->
|
||||
@getElement().component.getScrollTopRow()
|
||||
|
||||
setScrollTopRow: (scrollTopRow) ->
|
||||
@getElement().component.setScrollTopRow(scrollTopRow)
|
||||
|
||||
getScrollLeftColumn: ->
|
||||
@getElement().component.getScrollLeftColumn()
|
||||
|
||||
setScrollLeftColumn: (scrollLeftColumn) ->
|
||||
@getElement().component.setScrollLeftColumn(scrollLeftColumn)
|
||||
|
||||
intersectsVisibleRowRange: (startRow, endRow) ->
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.")
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
module.exports =
|
||||
class TiledComponent
|
||||
updateSync: (state) ->
|
||||
@newState = @getNewState(state)
|
||||
@oldState ?= @buildEmptyState()
|
||||
|
||||
@beforeUpdateSync?(state)
|
||||
|
||||
@removeTileNodes() if @shouldRecreateAllTilesOnUpdate?()
|
||||
@updateTileNodes()
|
||||
|
||||
@afterUpdateSync?(state)
|
||||
|
||||
removeTileNodes: ->
|
||||
@removeTileNode(tileRow) for tileRow of @oldState.tiles
|
||||
return
|
||||
|
||||
removeTileNode: (tileRow) ->
|
||||
@componentsByTileId[tileRow].destroy()
|
||||
delete @componentsByTileId[tileRow]
|
||||
delete @oldState.tiles[tileRow]
|
||||
|
||||
updateTileNodes: ->
|
||||
@componentsByTileId ?= {}
|
||||
|
||||
for tileRow of @oldState.tiles
|
||||
unless @newState.tiles.hasOwnProperty(tileRow)
|
||||
@removeTileNode(tileRow)
|
||||
|
||||
for tileRow, tileState of @newState.tiles
|
||||
if @oldState.tiles.hasOwnProperty(tileRow)
|
||||
component = @componentsByTileId[tileRow]
|
||||
else
|
||||
component = @componentsByTileId[tileRow] = @buildComponentForTile(tileRow)
|
||||
|
||||
@getTilesNode().appendChild(component.getDomNode())
|
||||
@oldState.tiles[tileRow] = Object.assign({}, tileState)
|
||||
|
||||
component.updateSync(@newState)
|
||||
|
||||
return
|
||||
|
||||
getComponentForTile: (tileRow) ->
|
||||
@componentsByTileId[tileRow]
|
||||
|
||||
getComponents: ->
|
||||
for _, component of @componentsByTileId
|
||||
component
|
||||
|
||||
getTiles: ->
|
||||
@getComponents().map((component) -> component.getDomNode())
|
||||
@@ -1,118 +1,104 @@
|
||||
const {Point} = require('text-buffer')
|
||||
const {fromFirstMateScopeId} = require('./first-mate-helpers')
|
||||
|
||||
module.exports = class TokenizedBufferIterator {
|
||||
constructor (tokenizedBuffer) {
|
||||
this.tokenizedBuffer = tokenizedBuffer
|
||||
this.openTags = null
|
||||
this.closeTags = null
|
||||
this.containingTags = null
|
||||
this.openScopeIds = null
|
||||
this.closeScopeIds = null
|
||||
}
|
||||
|
||||
seek (position) {
|
||||
this.openTags = []
|
||||
this.closeTags = []
|
||||
this.openScopeIds = []
|
||||
this.closeScopeIds = []
|
||||
this.tagIndex = null
|
||||
|
||||
const currentLine = this.tokenizedBuffer.tokenizedLineForRow(position.row)
|
||||
this.currentTags = currentLine.tags
|
||||
this.currentLineOpenTags = currentLine.openScopes
|
||||
this.currentLineTags = currentLine.tags
|
||||
this.currentLineLength = currentLine.text.length
|
||||
this.containingTags = this.currentLineOpenTags.map((id) => this.scopeForId(id))
|
||||
const containingScopeIds = currentLine.openScopes.map((id) => fromFirstMateScopeId(id))
|
||||
|
||||
let currentColumn = 0
|
||||
for (let [index, tag] of this.currentTags.entries()) {
|
||||
for (let index = 0; index < this.currentLineTags.length; index++) {
|
||||
const tag = this.currentLineTags[index]
|
||||
if (tag >= 0) {
|
||||
if (currentColumn >= position.column) {
|
||||
this.tagIndex = index
|
||||
break
|
||||
} else {
|
||||
currentColumn += tag
|
||||
while (this.closeTags.length > 0) {
|
||||
this.closeTags.shift()
|
||||
this.containingTags.pop()
|
||||
while (this.closeScopeIds.length > 0) {
|
||||
this.closeScopeIds.shift()
|
||||
containingScopeIds.pop()
|
||||
}
|
||||
while (this.openTags.length > 0) {
|
||||
const openTag = this.openTags.shift()
|
||||
this.containingTags.push(openTag)
|
||||
while (this.openScopeIds.length > 0) {
|
||||
const openTag = this.openScopeIds.shift()
|
||||
containingScopeIds.push(openTag)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const scopeName = this.scopeForId(tag)
|
||||
if (tag % 2 === 0) {
|
||||
if (this.openTags.length > 0) {
|
||||
const scopeId = fromFirstMateScopeId(tag)
|
||||
if ((tag & 1) === 0) {
|
||||
if (this.openScopeIds.length > 0) {
|
||||
if (currentColumn >= position.column) {
|
||||
this.tagIndex = index
|
||||
break
|
||||
} else {
|
||||
while (this.closeTags.length > 0) {
|
||||
this.closeTags.shift()
|
||||
this.containingTags.pop()
|
||||
while (this.closeScopeIds.length > 0) {
|
||||
this.closeScopeIds.shift()
|
||||
containingScopeIds.pop()
|
||||
}
|
||||
while (this.openTags.length > 0) {
|
||||
const openTag = this.openTags.shift()
|
||||
this.containingTags.push(openTag)
|
||||
while (this.openScopeIds.length > 0) {
|
||||
const openTag = this.openScopeIds.shift()
|
||||
containingScopeIds.push(openTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.closeTags.push(scopeName)
|
||||
this.closeScopeIds.push(scopeId)
|
||||
} else {
|
||||
this.openTags.push(scopeName)
|
||||
this.openScopeIds.push(scopeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.tagIndex == null) {
|
||||
this.tagIndex = this.currentTags.length
|
||||
this.tagIndex = this.currentLineTags.length
|
||||
}
|
||||
this.position = Point(position.row, Math.min(this.currentLineLength, currentColumn))
|
||||
return this.containingTags.slice()
|
||||
return containingScopeIds
|
||||
}
|
||||
|
||||
moveToSuccessor () {
|
||||
for (let tag of this.closeTags) { // eslint-disable-line no-unused-vars
|
||||
this.containingTags.pop()
|
||||
}
|
||||
for (let tag of this.openTags) {
|
||||
this.containingTags.push(tag)
|
||||
}
|
||||
this.openTags = []
|
||||
this.closeTags = []
|
||||
this.openScopeIds = []
|
||||
this.closeScopeIds = []
|
||||
while (true) {
|
||||
if (this.tagIndex === this.currentTags.length) {
|
||||
if (this.tagIndex === this.currentLineTags.length) {
|
||||
if (this.isAtTagBoundary()) {
|
||||
break
|
||||
} else if (this.shouldMoveToNextLine) {
|
||||
this.moveToNextLine()
|
||||
this.openTags = this.currentLineOpenTags.map((id) => this.scopeForId(id))
|
||||
this.shouldMoveToNextLine = false
|
||||
} else if (this.nextLineHasMismatchedContainingTags()) {
|
||||
this.closeTags = this.containingTags.slice().reverse()
|
||||
this.containingTags = []
|
||||
this.shouldMoveToNextLine = true
|
||||
} else if (!this.moveToNextLine()) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
const tag = this.currentTags[this.tagIndex]
|
||||
const tag = this.currentLineTags[this.tagIndex]
|
||||
if (tag >= 0) {
|
||||
if (this.isAtTagBoundary()) {
|
||||
break
|
||||
} else {
|
||||
this.position = Point(this.position.row, Math.min(
|
||||
this.currentLineLength,
|
||||
this.position.column + this.currentTags[this.tagIndex]
|
||||
this.position.column + this.currentLineTags[this.tagIndex]
|
||||
))
|
||||
}
|
||||
} else {
|
||||
const scopeName = this.scopeForId(tag)
|
||||
if (tag % 2 === 0) {
|
||||
if (this.openTags.length > 0) {
|
||||
const scopeId = fromFirstMateScopeId(tag)
|
||||
if ((tag & 1) === 0) {
|
||||
if (this.openScopeIds.length > 0) {
|
||||
break
|
||||
} else {
|
||||
this.closeTags.push(scopeName)
|
||||
this.closeScopeIds.push(scopeId)
|
||||
}
|
||||
} else {
|
||||
this.openTags.push(scopeName)
|
||||
this.openScopeIds.push(scopeId)
|
||||
}
|
||||
}
|
||||
this.tagIndex++
|
||||
@@ -125,24 +111,12 @@ module.exports = class TokenizedBufferIterator {
|
||||
return this.position
|
||||
}
|
||||
|
||||
getCloseTags () {
|
||||
return this.closeTags.slice()
|
||||
getCloseScopeIds () {
|
||||
return this.closeScopeIds.slice()
|
||||
}
|
||||
|
||||
getOpenTags () {
|
||||
return this.openTags.slice()
|
||||
}
|
||||
|
||||
nextLineHasMismatchedContainingTags () {
|
||||
const line = this.tokenizedBuffer.tokenizedLineForRow(this.position.row + 1)
|
||||
if (line == null) {
|
||||
return false
|
||||
} else {
|
||||
return (
|
||||
this.containingTags.length !== line.openScopes.length ||
|
||||
this.containingTags.some((tag, i) => tag !== this.scopeForId(line.openScopes[i]))
|
||||
)
|
||||
}
|
||||
getOpenScopeIds () {
|
||||
return this.openScopeIds.slice()
|
||||
}
|
||||
|
||||
moveToNextLine () {
|
||||
@@ -151,24 +125,14 @@ module.exports = class TokenizedBufferIterator {
|
||||
if (tokenizedLine == null) {
|
||||
return false
|
||||
} else {
|
||||
this.currentTags = tokenizedLine.tags
|
||||
this.currentLineTags = tokenizedLine.tags
|
||||
this.currentLineLength = tokenizedLine.text.length
|
||||
this.currentLineOpenTags = tokenizedLine.openScopes
|
||||
this.tagIndex = 0
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
isAtTagBoundary () {
|
||||
return this.closeTags.length > 0 || this.openTags.length > 0
|
||||
}
|
||||
|
||||
scopeForId (id) {
|
||||
const scope = this.tokenizedBuffer.grammar.scopeForId(id)
|
||||
if (scope) {
|
||||
return `syntax--${scope.replace(/\./g, '.syntax--')}`
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return this.closeScopeIds.length > 0 || this.openScopeIds.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ TokenIterator = require './token-iterator'
|
||||
ScopeDescriptor = require './scope-descriptor'
|
||||
TokenizedBufferIterator = require './tokenized-buffer-iterator'
|
||||
NullGrammar = require './null-grammar'
|
||||
{toFirstMateScopeId} = require './first-mate-helpers'
|
||||
|
||||
prefixedScopes = new Map()
|
||||
|
||||
module.exports =
|
||||
class TokenizedBuffer extends Model
|
||||
@@ -46,6 +49,19 @@ class TokenizedBuffer extends Model
|
||||
buildIterator: ->
|
||||
new TokenizedBufferIterator(this)
|
||||
|
||||
classNameForScopeId: (id) ->
|
||||
scope = @grammar.scopeForId(toFirstMateScopeId(id))
|
||||
if scope
|
||||
prefixedScope = prefixedScopes.get(scope)
|
||||
if prefixedScope
|
||||
prefixedScope
|
||||
else
|
||||
prefixedScope = "syntax--#{scope.replace(/\./g, ' syntax--')}"
|
||||
prefixedScopes.set(scope, prefixedScope)
|
||||
prefixedScope
|
||||
else
|
||||
null
|
||||
|
||||
getInvalidatedRanges: ->
|
||||
[]
|
||||
|
||||
@@ -252,7 +268,7 @@ class TokenizedBuffer extends Model
|
||||
buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) ->
|
||||
lineEnding = @buffer.lineEndingForRow(row)
|
||||
{tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false)
|
||||
new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator})
|
||||
new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator, @grammar})
|
||||
|
||||
tokenizedLineForRow: (bufferRow) ->
|
||||
if 0 <= bufferRow <= @buffer.getLastRow()
|
||||
@@ -262,7 +278,7 @@ class TokenizedBuffer extends Model
|
||||
text = @buffer.lineForRow(bufferRow)
|
||||
lineEnding = @buffer.lineEndingForRow(bufferRow)
|
||||
tags = [@grammar.startIdForScope(@grammar.scopeName), text.length, @grammar.endIdForScope(@grammar.scopeName)]
|
||||
@tokenizedLines[bufferRow] = new TokenizedLine({openScopes: [], text, tags, lineEnding, @tokenIterator})
|
||||
@tokenizedLines[bufferRow] = new TokenizedLine({openScopes: [], text, tags, lineEnding, @tokenIterator, @grammar})
|
||||
|
||||
tokenizedLinesForRows: (startRow, endRow) ->
|
||||
for row in [startRow..endRow] by 1
|
||||
@@ -328,17 +344,16 @@ class TokenizedBuffer extends Model
|
||||
@indentLevelForLine(line)
|
||||
|
||||
indentLevelForLine: (line) ->
|
||||
if match = line.match(/^[\t ]+/)
|
||||
indentLength = 0
|
||||
for character in match[0]
|
||||
if character is '\t'
|
||||
indentLength += @getTabLength() - (indentLength % @getTabLength())
|
||||
else
|
||||
indentLength++
|
||||
indentLength = 0
|
||||
for char in line
|
||||
if char is '\t'
|
||||
indentLength += @getTabLength() - (indentLength % @getTabLength())
|
||||
else if char is ' '
|
||||
indentLength++
|
||||
else
|
||||
break
|
||||
|
||||
indentLength / @getTabLength()
|
||||
else
|
||||
0
|
||||
indentLength / @getTabLength()
|
||||
|
||||
scopeDescriptorForPosition: (position) ->
|
||||
{row, column} = @buffer.clipPosition(Point.fromObject(position))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user