Merge branch 'master' into wl-build-on-node-7

This commit is contained in:
Wliu
2017-05-30 15:40:02 -04:00
109 changed files with 9796 additions and 15940 deletions

View File

@@ -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 projectit'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"/>
![atom-packages](https://cloud.githubusercontent.com/assets/69169/10472281/84fc9792-71d3-11e5-9fd1-19da717df079.png)
@@ -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 &mdash; 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]` &mdash; 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.

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,6 +29,7 @@
| [Exception Reporting](https://github.com/atom/exception-reporting) | [![macOS Build Status](https://travis-ci.org/atom/exception-reporting.svg?branch=master)](https://travis-ci.org/atom/exception-reporting) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/i0pla7qbpv7celg2/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/exception-reporting/branch/master) | [![Dependency Status](https://david-dm.org/atom/exception-reporting.svg)](https://david-dm.org/atom/exception-reporting) |
| [Find and Replace](https://github.com/atom/find-and-replace) | [![macOS Build Status](https://travis-ci.org/atom/find-and-replace.svg?branch=master)](https://travis-ci.org/atom/find-and-replace) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/6w4baiiq5mw4nxky/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/find-and-replace/branch/master) | [![Dependency Status](https://david-dm.org/atom/find-and-replace.svg)](https://david-dm.org/atom/find-and-replace) |
| [Fuzzy Finder](https://github.com/atom/fuzzy-finder) | [![macOS Build Status](https://travis-ci.org/atom/fuzzy-finder.svg?branch=master)](https://travis-ci.org/atom/fuzzy-finder) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/b4b2dg5n9r1wdqad/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/fuzzy-finder/branch/master) | [![Dependency Status](https://david-dm.org/atom/fuzzy-finder.svg)](https://david-dm.org/atom/fuzzy-finder) |
| [GitHub](https://github.com/atom/github) | [![macOS Build Status](https://travis-ci.com/atom/github.svg?token=RwrCnzpsZN5oEq5S5p7V&branch=master)](https://travis-ci.com/atom/github) | [![Windows Build status](https://ci.appveyor.com/api/projects/status/psctk8vrva49dseb/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/github/branch/master) | [![Dependency Status](https://david-dm.org/atom/github.svg)](https://david-dm.org/atom/github) |
| [Git Diff](https://github.com/atom/git-diff) | [![macOS Build Status](https://travis-ci.org/atom/git-diff.svg?branch=master)](https://travis-ci.org/atom/git-diff) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/9auj52cs0vso66nv/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/git-diff/branch/master) | [![Dependency Status](https://david-dm.org/atom/git-diff.svg)](https://david-dm.org/atom/git-diff) |
| [Go to Line](https://github.com/atom/go-to-line) | [![macOS Build Status](https://travis-ci.org/atom/go-to-line.svg?branch=master)](https://travis-ci.org/atom/go-to-line) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/qf0isc8ulw4wxi0b/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/go-to-line/branch/master) | [![Dependency Status](https://david-dm.org/atom/go-to-line.svg)](https://david-dm.org/atom/go-to-line) |
| [Grammar Selector](https://github.com/atom/grammar-selector) | [![macOS Build Status](https://travis-ci.org/atom/grammar-selector.svg?branch=master)](https://travis-ci.org/atom/grammar-selector) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/pg8qss03bfh4ngqm/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/grammar-selector/branch/master) | [![Dependency Status](https://david-dm.org/atom/grammar-selector.svg)](https://david-dm.org/atom/grammar-selector) |
@@ -40,7 +41,7 @@
| [Markdown Preview](https://github.com/atom/markdown-preview) | [![macOS Build Status](https://travis-ci.org/atom/markdown-preview.svg?branch=master)](https://travis-ci.org/atom/markdown-preview) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/bvh0evhh4v6w9b29/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/markdown-preview/branch/master) | [![Dependency Status](https://david-dm.org/atom/markdown-preview.svg)](https://david-dm.org/atom/markdown-preview) |
| [Metrics](https://github.com/atom/metrics) | [![macOS Build Status](https://travis-ci.org/atom/metrics.svg?branch=master)](https://travis-ci.org/atom/metrics) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/b5doi205xl3iex04/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/metrics/branch/master) | [![Dependency Status](https://david-dm.org/atom/metrics.svg)](https://david-dm.org/atom/metrics) |
| [Notifications](https://github.com/atom/notifications) | [![macOS Build Status](https://travis-ci.org/atom/notifications.svg?branch=master)](https://travis-ci.org/atom/notifications) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ps3p8tj2okw57x0e/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/notifications/branch/master) | [![Dependency Status](https://david-dm.org/atom/notifications.svg)](https://david-dm.org/atom/notifications) |
| [Open on Github](https://github.com/atom/open-on-github) | [![macOS Build Status](https://travis-ci.org/atom/open-on-github.svg?branch=master)](https://travis-ci.org/atom/open-on-github) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ccl6na4qsna5wncr/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/open-on-github/branch/master) | [![Dependency Status](https://david-dm.org/atom/open-on-github.svg)](https://david-dm.org/atom/open-on-github) |
| [Open on GitHub](https://github.com/atom/open-on-github) | [![macOS Build Status](https://travis-ci.org/atom/open-on-github.svg?branch=master)](https://travis-ci.org/atom/open-on-github) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ccl6na4qsna5wncr/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/open-on-github/branch/master) | [![Dependency Status](https://david-dm.org/atom/open-on-github.svg)](https://david-dm.org/atom/open-on-github) |
| [Package Generator](https://github.com/atom/package-generator) | [![macOS Build Status](https://travis-ci.org/atom/package-generator.svg?branch=master)](https://travis-ci.org/atom/package-generator)| [![Windows Build Status](https://ci.appveyor.com/api/projects/status/7t1i4hdmljhigp9u/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/package-generator/branch/master) | [![Dependency Status](https://david-dm.org/atom/package-generator.svg)](https://david-dm.org/atom/package-generator) |
| [Settings View](https://github.com/atom/settings-view) | [![macOS Build Status](https://travis-ci.org/atom/settings-view.svg?branch=master)](https://travis-ci.org/atom/settings-view) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/hatgxg6k2g3grafq/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/settings-view/branch/master) | [![Dependency Status](https://david-dm.org/atom/settings-view.svg)](https://david-dm.org/atom/settings-view) |
| [Snippets](https://github.com/atom/snippets) | [![macOS Build Status](https://travis-ci.org/atom/snippets.svg?branch=master)](https://travis-ci.org/atom/snippets) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/8hlc0onofkgbxw53/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/snippets/branch/master) | [![Dependency Status](https://david-dm.org/atom/snippets.svg)](https://david-dm.org/atom/snippets) |
@@ -109,6 +110,7 @@
| [Sass](https://github.com/atom/language-sass) | [![macOS Build Status](https://travis-ci.org/atom/language-sass.svg?branch=master)](https://travis-ci.org/atom/language-sass) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/g7p16vainm4iuoot/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-sass/branch/master) |
| [ShellScript](https://github.com/atom/language-shellscript) | [![macOS Build Status](https://travis-ci.org/atom/language-shellscript.svg?branch=master)](https://travis-ci.org/atom/language-shellscript) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/p4um3lowgrg8y0ty/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-shellscript/branch/master) |
| [SQL](https://github.com/atom/language-sql) | [![macOS Build Status](https://travis-ci.org/atom/language-sql.svg?branch=master)](https://travis-ci.org/atom/language-sql) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/ji31ouk5ehs4jdu1/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-sql/branch/master) |
| [Text](https://github.com/atom/language-text) | [![macOS Build Status](https://travis-ci.org/atom/language-text.svg?branch=master)](https://travis-ci.org/atom/language-text) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/psnekekg8lon67dw/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-text/branch/master) |
| [TODO](https://github.com/atom/language-todo) | [![macOS Build Status](https://travis-ci.org/atom/language-todo.svg?branch=master)](https://travis-ci.org/atom/language-todo) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/gcgb9m7h146lv6qp/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-todo/branch/master) |
| [TOML](https://github.com/atom/language-toml) | [![macOS Build Status](https://travis-ci.org/atom/language-toml.svg?branch=master)](https://travis-ci.org/atom/language-toml) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/kohao3fjyk6xv0sc/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-toml/branch/master) |
| [XML](https://github.com/atom/language-xml) | [![macOS Build Status](https://travis-ci.org/atom/language-xml.svg?branch=master)](https://travis-ci.org/atom/language-xml) | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/m5f6rn74a6h3q5uq/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/language-xml/branch/master) |

View File

@@ -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"
]
}
}

View 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}
}

View File

@@ -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')
}

View File

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

View File

@@ -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()
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

@@ -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'})

View File

@@ -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()
})
})
})

View File

@@ -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()
})
})

View File

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

View File

@@ -1,3 +1,3 @@
'use 6to6';
module.exports = async function hello() {}
module.exports = async function* hello() {}

View File

@@ -0,0 +1,4 @@
module.exports = function initialize() {
global.reachedUrlMain = true;
return Promise.resolve();
};

View File

@@ -0,0 +1,5 @@
{
"name": "package-with-url-main",
"version": "1.0.0",
"urlMain": "./index.js"
}

View File

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

View File

@@ -3,7 +3,9 @@ GutterContainer = require '../src/gutter-container'
describe 'GutterContainer', ->
gutterContainer = null
fakeTextEditor = {}
fakeTextEditor = {
scheduleComponentUpdate: ->
}
beforeEach ->
gutterContainer = new GutterContainer fakeTextEditor

View File

@@ -1,7 +1,9 @@
Gutter = require '../src/gutter'
describe 'Gutter', ->
fakeGutterContainer = {}
fakeGutterContainer = {
scheduleComponentUpdate: ->
}
name = 'name'
describe '::hide', ->

View File

@@ -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'])
})

View File

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

View File

@@ -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>) &amp;= <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]

View File

@@ -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 () {

View File

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

View File

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

View File

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

View 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

View File

@@ -19,7 +19,7 @@ describe('TextEditorRegistry', function () {
packageManager: {deferredActivationHooks: null}
})
editor = new TextEditor()
editor = new TextEditor({autoHeight: false})
})
afterEach(function () {

View File

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

View File

@@ -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([])
})
})
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: {},

View File

@@ -4,7 +4,7 @@ module.exports = function (extra) {
productName: 'Atom',
companyName: 'GitHub',
submitURL: 'https://crashreporter.atom.io',
autoSubmit: false,
uploadToServer: false,
extra: extra
})
}

View File

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

View File

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

View File

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

View File

@@ -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
View 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)
}
}
}

View File

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

View File

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

View File

@@ -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
View 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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -73,6 +73,8 @@ class PackageManager
@loadedPackages = {}
@preloadedPackages = {}
@packageStates = {}
@packagesCache = packageJSON._atomPackages ? {}
@packageDependencies = packageJSON.packageDependencies ? {}
@triggeredActivationHooks.clear()
###

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -769,8 +769,6 @@ class Selection extends Model
{oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e
{textChanged} = e
@cursor.updateVisibility()
unless oldHeadScreenPosition.isEqual(newHeadScreenPosition)
@cursor.goalColumn = null
cursorMovedEvent = {

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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.")

View File

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

View File

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

View File

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