mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-19 23:17:16 +03:00
Merge remote-tracking branch 'upstream/master' into tests
This commit is contained in:
commit
bbb331e1f7
15
.github/stale.yml
vendored
15
.github/stale.yml
vendored
@ -13,9 +13,20 @@ exemptLabels:
|
||||
staleLabel: stale
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
Thanks for your contribution!
|
||||
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
recent activity. Because the Atom team treats their issues
|
||||
[as their backlog](https://en.wikipedia.org/wiki/Scrum_(software_development)#Product_backlog), stale issues
|
||||
are closed. If you would like this issue to remain open:
|
||||
|
||||
1. Verify that you can still reproduce the issue in the latest version of Atom
|
||||
1. Comment that the issue is still reproducible and include:
|
||||
* What version of Atom you reproduced the issue on
|
||||
* What OS and version you reproduced the issue on
|
||||
* What steps you followed to reproduce the issue
|
||||
|
||||
Issues that are labeled as triaged will not be automatically marked as stale.
|
||||
# Comment to post when removing the stale label. Set to `false` to disable
|
||||
unmarkComment: false
|
||||
# Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable
|
||||
|
@ -85,7 +85,7 @@ 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](http://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/).
|
||||
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][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, 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)
|
||||
|
||||
@ -199,16 +199,7 @@ If you want to read about using Atom or developing packages in Atom, the [Atom F
|
||||
|
||||
#### Local development
|
||||
|
||||
All packages can be developed locally, by checking out the corresponding repository and registering the package to Atom with `apm`:
|
||||
|
||||
```
|
||||
$ git clone url-to-git-repository
|
||||
$ cd path-to-package/
|
||||
$ apm link -d
|
||||
$ atom -d .
|
||||
```
|
||||
|
||||
By running Atom with the `-d` flag, you signal it to run with development packages installed. `apm link` makes sure that your local repository is loaded by Atom.
|
||||
All packages can be developed locally. For instructions on how to do this, see [Contributing to Official Atom Packages][contributing-to-official-atom-packages] in the [Atom Flight Manual](http://flight-manual.atom.io).
|
||||
|
||||
### Pull Requests
|
||||
|
||||
@ -500,3 +491,4 @@ Please open an issue on `atom/atom` if you have suggestions for new labels, and
|
||||
|
||||
[beginner]:https://github.com/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3Abeginner+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc
|
||||
[help-wanted]:https://github.com/issues?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted+user%3Aatom+sort%3Acomments-desc+-label%3Abeginner
|
||||
[contributing-to-official-atom-packages]:http://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/
|
||||
|
@ -6,6 +6,6 @@
|
||||
"url": "https://github.com/atom/atom.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"atom-package-manager": "1.18.5"
|
||||
"atom-package-manager": "1.18.10"
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ export default async function ({test, benchmarkPaths}) {
|
||||
console.log(textualOutput)
|
||||
}
|
||||
|
||||
global.atom.reset()
|
||||
await global.atom.reset()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,53 +1 @@
|
||||
# Contributing to Official Atom Packages
|
||||
|
||||
If you think you know which package is causing the issue you are reporting, feel
|
||||
free to open up the issue in that specific repository instead. When in doubt
|
||||
just open the issue here but be aware that it may get closed here and reopened
|
||||
in the proper package's repository.
|
||||
|
||||
## Hacking on Packages
|
||||
|
||||
### Cloning
|
||||
|
||||
The first step is creating your own clone.
|
||||
|
||||
For example, if you want to make changes to the `tree-view` package, fork the repo on your github account, then clone it:
|
||||
|
||||
```
|
||||
> git clone git@github.com:your-username/tree-view.git
|
||||
```
|
||||
|
||||
Next install all the dependencies:
|
||||
|
||||
```
|
||||
> cd tree-view
|
||||
> apm install
|
||||
Installing modules ✓
|
||||
```
|
||||
|
||||
Now you can link it to development mode so when you run an Atom window with `atom --dev`, you will use your fork instead of the built in package:
|
||||
|
||||
```
|
||||
> apm link -d
|
||||
```
|
||||
|
||||
### Running in Development Mode
|
||||
|
||||
Editing a package in Atom is a bit of a circular experience: you're using Atom
|
||||
to modify itself. What happens if you temporarily break something? You don't
|
||||
want the version of Atom you're using to edit to become useless in the process.
|
||||
For this reason, you'll only want to load packages in **development mode** while
|
||||
you are working on them. You'll perform your editing in **stable mode**, only
|
||||
switching to development mode to test your changes.
|
||||
|
||||
To open a development mode window, use the "Application: Open Dev" command.
|
||||
You can also run dev mode from the command line with `atom --dev`.
|
||||
|
||||
To load your package in development mode, create a symlink to it in
|
||||
`~/.atom/dev/packages`. This occurs automatically when you clone the package
|
||||
with `apm develop`. You can also run `apm link --dev` and `apm unlink --dev`
|
||||
from the package directory to create and remove dev-mode symlinks.
|
||||
|
||||
### Installing Dependencies
|
||||
|
||||
You'll want to keep dependencies up to date by running `apm update` after pulling any upstream changes.
|
||||
See http://flight-manual.atom.io/hacking-atom/sections/contributing-to-official-atom-packages/
|
||||
|
@ -132,6 +132,7 @@
|
||||
'ctrl-shift-w': 'editor:select-word'
|
||||
'cmd-ctrl-left': 'editor:move-selection-left'
|
||||
'cmd-ctrl-right': 'editor:move-selection-right'
|
||||
'cmd-shift-V': 'editor:paste-without-reformatting'
|
||||
|
||||
# Emacs
|
||||
'alt-f': 'editor:move-to-end-of-word'
|
||||
|
@ -105,6 +105,7 @@
|
||||
'alt-shift-right': 'editor:select-to-next-subword-boundary'
|
||||
'alt-backspace': 'editor:delete-to-beginning-of-subword'
|
||||
'alt-delete': 'editor:delete-to-end-of-subword'
|
||||
'ctrl-shift-V': 'editor:paste-without-reformatting'
|
||||
|
||||
# Sublime Parity
|
||||
'ctrl-a': 'core:select-all'
|
||||
|
@ -110,6 +110,7 @@
|
||||
'alt-shift-right': 'editor:select-to-next-subword-boundary'
|
||||
'alt-backspace': 'editor:delete-to-beginning-of-subword'
|
||||
'alt-delete': 'editor:delete-to-end-of-subword'
|
||||
'ctrl-shift-V': 'editor:paste-without-reformatting'
|
||||
|
||||
# Sublime Parity
|
||||
'ctrl-a': 'core:select-all'
|
||||
|
@ -65,6 +65,7 @@
|
||||
{ label: 'Copy', command: 'core:copy' }
|
||||
{ label: 'Copy Path', command: 'editor:copy-path' }
|
||||
{ label: 'Paste', command: 'core:paste' }
|
||||
{ label: 'Paste Without Reformatting', command: 'editor:paste-without-reformatting' }
|
||||
{ label: 'Select All', command: 'core:select-all' }
|
||||
{ type: 'separator' }
|
||||
{ label: 'Toggle Comments', command: 'editor:toggle-line-comments' }
|
||||
|
@ -38,6 +38,7 @@
|
||||
{ label: 'C&opy', command: 'core:copy' }
|
||||
{ label: 'Copy Pat&h', command: 'editor:copy-path' }
|
||||
{ label: '&Paste', command: 'core:paste' }
|
||||
{ label: 'Paste Without Reformatting', command: 'editor:paste-without-reformatting' }
|
||||
{ label: 'Select &All', command: 'core:select-all' }
|
||||
{ type: 'separator' }
|
||||
{ label: '&Toggle Comments', command: 'editor:toggle-line-comments' }
|
||||
|
@ -46,6 +46,7 @@
|
||||
{ label: '&Copy', command: 'core:copy' }
|
||||
{ label: 'Copy Pat&h', command: 'editor:copy-path' }
|
||||
{ label: '&Paste', command: 'core:paste' }
|
||||
{ label: 'Paste Without Reformatting', command: 'editor:paste-without-reformatting' }
|
||||
{ label: 'Select &All', command: 'core:select-all' }
|
||||
{ type: 'separator' }
|
||||
{ label: '&Toggle Comments', command: 'editor:toggle-line-comments' }
|
||||
|
110
package.json
110
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "atom",
|
||||
"productName": "Atom",
|
||||
"version": "1.21.0-dev",
|
||||
"version": "1.23.0-dev",
|
||||
"description": "A hackable text editor for the 21st Century.",
|
||||
"main": "./src/main-process/main.js",
|
||||
"repository": {
|
||||
@ -12,11 +12,11 @@
|
||||
"url": "https://github.com/atom/atom/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"electronVersion": "1.6.9",
|
||||
"electronVersion": "1.6.15",
|
||||
"dependencies": {
|
||||
"@atom/source-map-support": "^0.3.4",
|
||||
"async": "0.2.6",
|
||||
"atom-keymap": "8.2.4",
|
||||
"atom-keymap": "8.2.8",
|
||||
"atom-select-list": "^0.1.0",
|
||||
"atom-ui": "0.4.1",
|
||||
"babel-core": "5.8.38",
|
||||
@ -26,18 +26,18 @@
|
||||
"clear-cut": "^2.0.2",
|
||||
"coffee-script": "1.11.1",
|
||||
"color": "^0.7.3",
|
||||
"dedent": "^0.6.0",
|
||||
"dedent": "^0.7.0",
|
||||
"devtron": "1.3.0",
|
||||
"etch": "^0.12.6",
|
||||
"event-kit": "^2.3.0",
|
||||
"event-kit": "^2.4.0",
|
||||
"find-parent-dir": "^0.3.0",
|
||||
"first-mate": "7.0.7",
|
||||
"first-mate": "7.0.10",
|
||||
"focus-trap": "^2.3.0",
|
||||
"fs-admin": "^0.1.5",
|
||||
"fs-admin": "^0.1.6",
|
||||
"fs-plus": "^3.0.1",
|
||||
"fstream": "0.1.24",
|
||||
"fuzzaldrin": "^2.1",
|
||||
"git-utils": "5.0.0",
|
||||
"git-utils": "5.1.0",
|
||||
"glob": "^7.1.1",
|
||||
"grim": "1.5.0",
|
||||
"jasmine-json": "~0.0",
|
||||
@ -65,12 +65,12 @@
|
||||
"scandal": "^3.1.0",
|
||||
"scoped-property-store": "^0.17.0",
|
||||
"scrollbar-style": "^3.2",
|
||||
"season": "^6.0.0",
|
||||
"season": "^6.0.2",
|
||||
"semver": "^4.3.3",
|
||||
"service-hub": "^0.7.4",
|
||||
"sinon": "1.17.4",
|
||||
"temp": "^0.8.3",
|
||||
"text-buffer": "13.1.14",
|
||||
"text-buffer": "13.5.8",
|
||||
"typescript-simple": "1.0.0",
|
||||
"underscore-plus": "^1.6.6",
|
||||
"winreg": "^1.2.1",
|
||||
@ -89,86 +89,86 @@
|
||||
"one-light-syntax": "1.8.0",
|
||||
"solarized-dark-syntax": "1.1.2",
|
||||
"solarized-light-syntax": "1.1.2",
|
||||
"about": "1.7.6",
|
||||
"archive-view": "0.63.3",
|
||||
"about": "1.7.8",
|
||||
"archive-view": "0.63.4",
|
||||
"autocomplete-atom-api": "0.10.3",
|
||||
"autocomplete-css": "0.17.3",
|
||||
"autocomplete-html": "0.8.1",
|
||||
"autocomplete-plus": "2.35.8",
|
||||
"autocomplete-snippets": "1.11.1",
|
||||
"autocomplete-html": "0.8.2",
|
||||
"autocomplete-plus": "2.37.0",
|
||||
"autocomplete-snippets": "1.11.2",
|
||||
"autoflow": "0.29.0",
|
||||
"autosave": "0.24.3",
|
||||
"autosave": "0.24.6",
|
||||
"background-tips": "0.27.1",
|
||||
"bookmarks": "0.44.4",
|
||||
"bracket-matcher": "0.87.3",
|
||||
"bracket-matcher": "0.88.0",
|
||||
"command-palette": "0.41.1",
|
||||
"dalek": "0.2.1",
|
||||
"deprecation-cop": "0.56.7",
|
||||
"deprecation-cop": "0.56.9",
|
||||
"dev-live-reload": "0.47.1",
|
||||
"encoding-selector": "0.23.4",
|
||||
"encoding-selector": "0.23.7",
|
||||
"exception-reporting": "0.41.4",
|
||||
"find-and-replace": "0.212.0",
|
||||
"fuzzy-finder": "1.5.8",
|
||||
"github": "0.5.0",
|
||||
"find-and-replace": "0.212.3",
|
||||
"fuzzy-finder": "1.6.1",
|
||||
"github": "0.7.0",
|
||||
"git-diff": "1.3.6",
|
||||
"go-to-line": "0.32.1",
|
||||
"grammar-selector": "0.49.5",
|
||||
"image-view": "0.62.3",
|
||||
"grammar-selector": "0.49.8",
|
||||
"image-view": "0.62.4",
|
||||
"incompatible-packages": "0.27.3",
|
||||
"keybinding-resolver": "0.38.0",
|
||||
"line-ending-selector": "0.7.3",
|
||||
"line-ending-selector": "0.7.4",
|
||||
"link": "0.31.3",
|
||||
"markdown-preview": "0.159.13",
|
||||
"markdown-preview": "0.159.16",
|
||||
"metrics": "1.2.6",
|
||||
"notifications": "0.69.0",
|
||||
"notifications": "0.69.2",
|
||||
"open-on-github": "1.2.1",
|
||||
"package-generator": "1.1.1",
|
||||
"settings-view": "0.251.5",
|
||||
"snippets": "1.1.4",
|
||||
"spell-check": "0.72.2",
|
||||
"status-bar": "1.8.11",
|
||||
"styleguide": "0.49.6",
|
||||
"symbols-view": "0.117.1",
|
||||
"tabs": "0.107.1",
|
||||
"settings-view": "0.252.1",
|
||||
"snippets": "1.1.6",
|
||||
"spell-check": "0.72.3",
|
||||
"status-bar": "1.8.13",
|
||||
"styleguide": "0.49.8",
|
||||
"symbols-view": "0.118.1",
|
||||
"tabs": "0.108.0",
|
||||
"timecop": "0.36.0",
|
||||
"tree-view": "0.217.8",
|
||||
"tree-view": "0.220.0",
|
||||
"update-package-dependencies": "0.12.0",
|
||||
"welcome": "0.36.5",
|
||||
"whitespace": "0.37.2",
|
||||
"whitespace": "0.37.4",
|
||||
"wrap-guide": "0.40.2",
|
||||
"language-c": "0.58.1",
|
||||
"language-clojure": "0.22.4",
|
||||
"language-coffee-script": "0.49.0",
|
||||
"language-csharp": "0.14.2",
|
||||
"language-css": "0.42.5",
|
||||
"language-gfm": "0.90.1",
|
||||
"language-coffee-script": "0.49.2",
|
||||
"language-csharp": "0.14.3",
|
||||
"language-css": "0.42.7",
|
||||
"language-gfm": "0.90.2",
|
||||
"language-git": "0.19.1",
|
||||
"language-go": "0.44.2",
|
||||
"language-html": "0.47.7",
|
||||
"language-hyperlink": "0.16.2",
|
||||
"language-java": "0.27.4",
|
||||
"language-javascript": "0.127.3",
|
||||
"language-go": "0.44.3",
|
||||
"language-html": "0.48.2",
|
||||
"language-hyperlink": "0.16.3",
|
||||
"language-java": "0.27.5",
|
||||
"language-javascript": "0.127.6",
|
||||
"language-json": "0.19.1",
|
||||
"language-less": "0.33.0",
|
||||
"language-less": "0.34.0",
|
||||
"language-make": "0.22.3",
|
||||
"language-mustache": "0.14.1",
|
||||
"language-mustache": "0.14.4",
|
||||
"language-objective-c": "0.15.1",
|
||||
"language-perl": "0.37.0",
|
||||
"language-php": "0.41.0",
|
||||
"language-php": "0.42.2",
|
||||
"language-property-list": "0.9.1",
|
||||
"language-python": "0.45.4",
|
||||
"language-ruby": "0.71.3",
|
||||
"language-python": "0.45.5",
|
||||
"language-ruby": "0.71.4",
|
||||
"language-ruby-on-rails": "0.25.2",
|
||||
"language-sass": "0.61.1",
|
||||
"language-shellscript": "0.25.3",
|
||||
"language-sass": "0.61.2",
|
||||
"language-shellscript": "0.25.4",
|
||||
"language-source": "0.9.0",
|
||||
"language-sql": "0.25.8",
|
||||
"language-text": "0.7.3",
|
||||
"language-todo": "0.29.2",
|
||||
"language-todo": "0.29.3",
|
||||
"language-toml": "0.18.1",
|
||||
"language-typescript": "0.1.0",
|
||||
"language-typescript": "0.2.2",
|
||||
"language-xml": "0.35.2",
|
||||
"language-yaml": "0.30.2"
|
||||
"language-yaml": "0.31.1"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -9,7 +9,7 @@ module.exports = function () {
|
||||
const chromedriverVer = buildMetadata.dependencies['electron-chromedriver']
|
||||
const mksnapshotVer = buildMetadata.dependencies['electron-mksnapshot']
|
||||
|
||||
// Always use tilde on electron-chromedriver so that it can pick up the best patch vesion
|
||||
// Always use tilde on electron-chromedriver so that it can pick up the best patch version
|
||||
if (!chromedriverVer.startsWith('~')) {
|
||||
throw new Error(`electron-chromedriver version in script/package.json should start with a tilde to match latest patch version.`)
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ const fs = require('fs')
|
||||
const path = require('path')
|
||||
const electronLink = require('electron-link')
|
||||
const CONFIG = require('../config')
|
||||
const vm = require('vm')
|
||||
|
||||
module.exports = function (packagedAppPath) {
|
||||
const snapshotScriptPath = path.join(CONFIG.buildOutputPath, 'startup.js')
|
||||
@ -28,47 +27,37 @@ module.exports = function (packagedAppPath) {
|
||||
coreModules.has(modulePath) ||
|
||||
(relativePath.startsWith(path.join('..', 'src')) && relativePath.endsWith('-element.js')) ||
|
||||
relativePath.startsWith(path.join('..', 'node_modules', 'dugite')) ||
|
||||
relativePath.endsWith(path.join('node_modules', 'coffee-script', 'lib', 'coffee-script', 'register.js')) ||
|
||||
relativePath.endsWith(path.join('node_modules', 'fs-extra', 'lib', 'index.js')) ||
|
||||
relativePath.endsWith(path.join('node_modules', 'graceful-fs', 'graceful-fs.js')) ||
|
||||
relativePath.endsWith(path.join('node_modules', 'htmlparser2', 'lib', 'index.js')) ||
|
||||
relativePath.endsWith(path.join('node_modules', 'minimatch', 'minimatch.js')) ||
|
||||
relativePath === path.join('..', 'exports', 'atom.js') ||
|
||||
relativePath === path.join('..', 'src', 'electron-shims.js') ||
|
||||
relativePath === path.join('..', 'src', 'safe-clipboard.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'atom-keymap', 'lib', 'command-event.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'babel-core', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'cached-run-in-this-context', 'lib', 'main.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'coffee-script', 'lib', 'coffee-script', 'register.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'cson-parser', 'node_modules', 'coffee-script', 'lib', 'coffee-script', 'register.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'decompress-zip', 'lib', 'decompress-zip.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'debug', 'node.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'fs-extra', 'lib', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'github', 'node_modules', 'fs-extra', 'lib', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'git-utils', 'lib', 'git.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'git-utils', 'src', 'git.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'glob', 'glob.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'graceful-fs', 'graceful-fs.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'htmlparser2', 'lib', 'index.js') ||
|
||||
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', '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') ||
|
||||
relativePath === path.join('..', 'node_modules', 'less', 'node_modules', 'graceful-fs', 'graceful-fs.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'minimatch', 'minimatch.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'node-fetch', 'lib', 'fetch-error.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'nsfw', 'node_modules', 'fs-extra', 'lib', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'superstring', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'oniguruma', 'src', 'oniguruma.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'request', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'resolve', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'resolve', 'lib', 'core.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'scandal', 'node_modules', 'minimatch', 'minimatch.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'settings-view', 'node_modules', 'glob', 'glob.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'settings-view', 'node_modules', 'minimatch', 'minimatch.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'spellchecker', 'lib', 'spellchecker.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'spelling-manager', 'node_modules', 'natural', 'lib', 'natural', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'tar', 'tar.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'temp', 'lib', 'temp.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'tree-view', 'node_modules', 'minimatch', 'minimatch.js')
|
||||
relativePath === path.join('..', 'node_modules', 'tmp', 'lib', 'tmp.js')
|
||||
)
|
||||
}
|
||||
}).then((snapshotScript) => {
|
||||
@ -76,7 +65,21 @@ module.exports = function (packagedAppPath) {
|
||||
process.stdout.write('\n')
|
||||
|
||||
console.log('Verifying if snapshot can be executed via `mksnapshot`')
|
||||
vm.runInNewContext(snapshotScript, undefined, {filename: snapshotScriptPath, displayErrors: true})
|
||||
const verifySnapshotScriptPath = path.join(CONFIG.repositoryRootPath, 'script', 'verify-snapshot-script')
|
||||
let nodeBundledInElectronPath
|
||||
if (process.platform === 'darwin') {
|
||||
const executableName = CONFIG.channel === 'beta' ? 'Atom Beta' : 'Atom'
|
||||
nodeBundledInElectronPath = path.join(packagedAppPath, 'Contents', 'MacOS', executableName)
|
||||
} else if (process.platform === 'win32') {
|
||||
nodeBundledInElectronPath = path.join(packagedAppPath, 'atom.exe')
|
||||
} else {
|
||||
nodeBundledInElectronPath = path.join(packagedAppPath, 'atom')
|
||||
}
|
||||
childProcess.execFileSync(
|
||||
nodeBundledInElectronPath,
|
||||
[verifySnapshotScriptPath, snapshotScriptPath],
|
||||
{env: Object.assign({}, process.env, {ELECTRON_RUN_AS_NODE: 1})}
|
||||
)
|
||||
|
||||
const generatedStartupBlobPath = path.join(CONFIG.buildOutputPath, 'snapshot_blob.bin')
|
||||
console.log(`Generating startup blob at "${generatedStartupBlobPath}"`)
|
||||
|
@ -71,7 +71,8 @@ const EXCLUDE_REGEXPS_SOURCES = [
|
||||
'node_modules' + escapeRegExp(path.sep) + '.*' + escapeRegExp(path.sep) + 'examples?' + escapeRegExp(path.sep),
|
||||
'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.md$',
|
||||
'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.d\\.ts$',
|
||||
'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.js\\.map$'
|
||||
'node_modules' + escapeRegExp(path.sep) + '.*' + '\\.js\\.map$',
|
||||
'.*' + escapeRegExp(path.sep) + 'test.*\\.html$'
|
||||
]
|
||||
|
||||
// Ignore spec directories in all bundled packages
|
||||
|
@ -25,7 +25,7 @@ module.exports = function () {
|
||||
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
|
||||
// Run `apm install` in the *root* package'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)) {
|
||||
|
@ -9,10 +9,10 @@
|
||||
"csslint": "1.0.2",
|
||||
"donna": "1.0.16",
|
||||
"electron-chromedriver": "~1.6",
|
||||
"electron-link": "0.1.1",
|
||||
"electron-link": "0.1.2",
|
||||
"electron-mksnapshot": "~1.6",
|
||||
"electron-packager": "7.3.0",
|
||||
"electron-winstaller": "2.6.2",
|
||||
"electron-winstaller": "2.6.3",
|
||||
"fs-admin": "^0.1.5",
|
||||
"fs-extra": "0.30.0",
|
||||
"glob": "7.0.3",
|
||||
@ -26,6 +26,7 @@
|
||||
"npm": "5.3.0",
|
||||
"passwd-user": "2.1.0",
|
||||
"pegjs": "0.9.0",
|
||||
"random-seed": "^0.3.0",
|
||||
"season": "5.3.0",
|
||||
"semver": "5.3.0",
|
||||
"standard": "8.4.0",
|
||||
|
6
script/verify-snapshot-script
Executable file
6
script/verify-snapshot-script
Executable file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs')
|
||||
const vm = require('vm')
|
||||
const snapshotScriptPath = process.argv[2]
|
||||
const snapshotScript = fs.readFileSync(snapshotScriptPath, 'utf8')
|
||||
vm.runInNewContext(snapshotScript, undefined, {filename: snapshotScriptPath, displayErrors: true})
|
@ -322,6 +322,44 @@ describe "AtomEnvironment", ->
|
||||
expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain')
|
||||
atom2.destroy()
|
||||
|
||||
describe "deserialization failures", ->
|
||||
|
||||
it "propagates project state restoration failures", ->
|
||||
spyOn(atom.project, 'deserialize').andCallFake ->
|
||||
err = new Error('deserialization failure')
|
||||
err.missingProjectPaths = ['/foo']
|
||||
Promise.reject(err)
|
||||
spyOn(atom.notifications, 'addError')
|
||||
|
||||
waitsForPromise -> atom.deserialize({project: 'should work'})
|
||||
runs ->
|
||||
expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open project directory',
|
||||
{description: 'Project directory `/foo` is no longer on disk.'}
|
||||
|
||||
it "accumulates and reports two errors with one notification", ->
|
||||
spyOn(atom.project, 'deserialize').andCallFake ->
|
||||
err = new Error('deserialization failure')
|
||||
err.missingProjectPaths = ['/foo', '/wat']
|
||||
Promise.reject(err)
|
||||
spyOn(atom.notifications, 'addError')
|
||||
|
||||
waitsForPromise -> atom.deserialize({project: 'should work'})
|
||||
runs ->
|
||||
expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open 2 project directories',
|
||||
{description: 'Project directories `/foo` and `/wat` are no longer on disk.'}
|
||||
|
||||
it "accumulates and reports three+ errors with one notification", ->
|
||||
spyOn(atom.project, 'deserialize').andCallFake ->
|
||||
err = new Error('deserialization failure')
|
||||
err.missingProjectPaths = ['/foo', '/wat', '/stuff', '/things']
|
||||
Promise.reject(err)
|
||||
spyOn(atom.notifications, 'addError')
|
||||
|
||||
waitsForPromise -> atom.deserialize({project: 'should work'})
|
||||
runs ->
|
||||
expect(atom.notifications.addError).toHaveBeenCalledWith 'Unable to open 4 project directories',
|
||||
{description: 'Project directories `/foo`, `/wat`, `/stuff`, and `/things` are no longer on disk.'}
|
||||
|
||||
describe "openInitialEmptyEditorIfNecessary", ->
|
||||
describe "when there are no paths set", ->
|
||||
beforeEach ->
|
||||
|
@ -13,6 +13,8 @@ describe "Config", ->
|
||||
dotAtomPath = temp.path('atom-spec-config')
|
||||
atom.config.configDirPath = dotAtomPath
|
||||
atom.config.enablePersistence = true
|
||||
atom.config.settingsLoaded = true
|
||||
atom.config.pendingOperations = []
|
||||
atom.config.configFilePath = path.join(atom.config.configDirPath, "atom.config.cson")
|
||||
|
||||
afterEach ->
|
||||
@ -877,7 +879,7 @@ describe "Config", ->
|
||||
|
||||
beforeEach ->
|
||||
atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy()
|
||||
spyOn(fs, "existsSync").andCallFake ->
|
||||
spyOn(fs, "makeTreeSync").andCallFake ->
|
||||
error = new Error()
|
||||
error.code = 'EPERM'
|
||||
throw error
|
||||
@ -895,16 +897,15 @@ describe "Config", ->
|
||||
describe ".observeUserConfig()", ->
|
||||
updatedHandler = null
|
||||
|
||||
writeConfigFile = (data) ->
|
||||
previousSetTimeoutCallCount = setTimeout.callCount
|
||||
runs ->
|
||||
fs.writeFileSync(atom.config.configFilePath, data)
|
||||
waitsFor "debounced config file load", ->
|
||||
setTimeout.callCount > previousSetTimeoutCallCount
|
||||
runs ->
|
||||
advanceClock(1000)
|
||||
writeConfigFile = (data, secondsInFuture = 0) ->
|
||||
fs.writeFileSync(atom.config.configFilePath, data)
|
||||
|
||||
future = (Date.now() / 1000) + secondsInFuture
|
||||
fs.utimesSync(atom.config.configFilePath, future, future)
|
||||
|
||||
beforeEach ->
|
||||
jasmine.useRealClock()
|
||||
|
||||
atom.config.setSchema 'foo',
|
||||
type: 'object'
|
||||
properties:
|
||||
@ -920,7 +921,7 @@ describe "Config", ->
|
||||
default: 12
|
||||
|
||||
expect(fs.existsSync(atom.config.configDirPath)).toBeFalsy()
|
||||
fs.writeFileSync atom.config.configFilePath, """
|
||||
writeConfigFile """
|
||||
'*':
|
||||
foo:
|
||||
bar: 'baz'
|
||||
@ -930,26 +931,32 @@ describe "Config", ->
|
||||
scoped: true
|
||||
"""
|
||||
atom.config.loadUserConfig()
|
||||
atom.config.observeUserConfig()
|
||||
updatedHandler = jasmine.createSpy("updatedHandler")
|
||||
atom.config.onDidChange updatedHandler
|
||||
|
||||
waitsForPromise -> atom.config.observeUserConfig()
|
||||
|
||||
runs ->
|
||||
updatedHandler = jasmine.createSpy "updatedHandler"
|
||||
atom.config.onDidChange updatedHandler
|
||||
|
||||
afterEach ->
|
||||
atom.config.unobserveUserConfig()
|
||||
fs.removeSync(dotAtomPath)
|
||||
|
||||
describe "when the config file changes to contain valid cson", ->
|
||||
|
||||
it "updates the config data", ->
|
||||
writeConfigFile("foo: { bar: 'quux', baz: 'bar'}")
|
||||
writeConfigFile "foo: { bar: 'quux', baz: 'bar'}", 2
|
||||
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
runs ->
|
||||
expect(atom.config.get('foo.bar')).toBe 'quux'
|
||||
expect(atom.config.get('foo.baz')).toBe 'bar'
|
||||
|
||||
it "does not fire a change event for paths that did not change", ->
|
||||
atom.config.onDidChange 'foo.bar', noChangeSpy = jasmine.createSpy()
|
||||
atom.config.onDidChange 'foo.bar', noChangeSpy = jasmine.createSpy "unchanged"
|
||||
|
||||
writeConfigFile("foo: { bar: 'baz', baz: 'ok'}")
|
||||
writeConfigFile "foo: { bar: 'baz', baz: 'ok'}", 2
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
runs ->
|
||||
@ -964,15 +971,16 @@ describe "Config", ->
|
||||
items:
|
||||
type: 'string'
|
||||
|
||||
writeConfigFile("foo: { bar: ['baz', 'ok']}")
|
||||
updatedHandler.reset()
|
||||
writeConfigFile "foo: { bar: ['baz', 'ok']}", 4
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
runs -> updatedHandler.reset()
|
||||
|
||||
it "does not fire a change event for paths that did not change", ->
|
||||
noChangeSpy = jasmine.createSpy()
|
||||
noChangeSpy = jasmine.createSpy "unchanged"
|
||||
atom.config.onDidChange('foo.bar', noChangeSpy)
|
||||
|
||||
writeConfigFile("foo: { bar: ['baz', 'ok'], baz: 'another'}")
|
||||
writeConfigFile "foo: { bar: ['baz', 'ok'], baz: 'another'}", 2
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
runs ->
|
||||
@ -989,7 +997,7 @@ describe "Config", ->
|
||||
'*':
|
||||
foo:
|
||||
scoped: false
|
||||
"""
|
||||
""", 2
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
runs ->
|
||||
@ -997,7 +1005,7 @@ describe "Config", ->
|
||||
expect(atom.config.get('foo.scoped', scope: ['.source.ruby'])).toBe false
|
||||
|
||||
it "does not fire a change event for paths that did not change", ->
|
||||
noChangeSpy = jasmine.createSpy()
|
||||
noChangeSpy = jasmine.createSpy "no change"
|
||||
atom.config.onDidChange('foo.scoped', scope: ['.source.ruby'], noChangeSpy)
|
||||
|
||||
writeConfigFile """
|
||||
@ -1007,7 +1015,7 @@ describe "Config", ->
|
||||
'.source.ruby':
|
||||
foo:
|
||||
scoped: true
|
||||
"""
|
||||
""", 2
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
runs ->
|
||||
@ -1017,7 +1025,7 @@ describe "Config", ->
|
||||
|
||||
describe "when the config file changes to omit a setting with a default", ->
|
||||
it "resets the setting back to the default", ->
|
||||
writeConfigFile("foo: { baz: 'new'}")
|
||||
writeConfigFile "foo: { baz: 'new'}", 2
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
runs ->
|
||||
expect(atom.config.get('foo.bar')).toBe 'def'
|
||||
@ -1025,20 +1033,20 @@ describe "Config", ->
|
||||
|
||||
describe "when the config file changes to be empty", ->
|
||||
beforeEach ->
|
||||
writeConfigFile("")
|
||||
updatedHandler.reset()
|
||||
writeConfigFile "", 4
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
it "resets all settings back to the defaults", ->
|
||||
expect(updatedHandler.callCount).toBe 1
|
||||
expect(atom.config.get('foo.bar')).toBe 'def'
|
||||
atom.config.set("hair", "blonde") # trigger a save
|
||||
advanceClock(500)
|
||||
expect(atom.config.save).toHaveBeenCalled()
|
||||
waitsFor 'save', -> atom.config.save.callCount > 0
|
||||
|
||||
describe "when the config file subsequently changes again to contain configuration", ->
|
||||
beforeEach ->
|
||||
updatedHandler.reset()
|
||||
writeConfigFile("foo: bar: 'newVal'")
|
||||
writeConfigFile "foo: bar: 'newVal'", 2
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
it "sets the setting to the value specified in the config file", ->
|
||||
@ -1047,25 +1055,26 @@ describe "Config", ->
|
||||
describe "when the config file changes to contain invalid cson", ->
|
||||
addErrorHandler = null
|
||||
beforeEach ->
|
||||
atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy()
|
||||
writeConfigFile("}}}")
|
||||
atom.notifications.onDidAddNotification addErrorHandler = jasmine.createSpy "error handler"
|
||||
writeConfigFile "}}}", 4
|
||||
waitsFor "error to be logged", -> addErrorHandler.callCount > 0
|
||||
|
||||
it "logs a warning and does not update config data", ->
|
||||
expect(updatedHandler.callCount).toBe 0
|
||||
expect(atom.config.get('foo.bar')).toBe 'baz'
|
||||
|
||||
atom.config.set("hair", "blonde") # trigger a save
|
||||
expect(atom.config.save).not.toHaveBeenCalled()
|
||||
|
||||
describe "when the config file subsequently changes again to contain valid cson", ->
|
||||
beforeEach ->
|
||||
writeConfigFile("foo: bar: 'newVal'")
|
||||
updatedHandler.reset()
|
||||
writeConfigFile "foo: bar: 'newVal'", 6
|
||||
waitsFor 'update event', -> updatedHandler.callCount > 0
|
||||
|
||||
it "updates the config data and resumes saving", ->
|
||||
atom.config.set("hair", "blonde")
|
||||
advanceClock(500)
|
||||
expect(atom.config.save).toHaveBeenCalled()
|
||||
waitsFor 'save', -> atom.config.save.callCount > 0
|
||||
|
||||
describe ".initializeConfigDirectory()", ->
|
||||
beforeEach ->
|
||||
@ -1741,3 +1750,35 @@ describe "Config", ->
|
||||
|
||||
expect(atom.config.set('foo.bar.str_options', 'One')).toBe false
|
||||
expect(atom.config.get('foo.bar.str_options')).toEqual 'two'
|
||||
|
||||
describe "when .set/.unset is called prior to .loadUserConfig", ->
|
||||
beforeEach ->
|
||||
atom.config.settingsLoaded = false
|
||||
fs.writeFileSync atom.config.configFilePath, """
|
||||
'*':
|
||||
foo:
|
||||
bar: 'baz'
|
||||
do:
|
||||
ray: 'me'
|
||||
"""
|
||||
|
||||
it "ensures that early set and unset calls are replayed after the config is loaded from disk", ->
|
||||
atom.config.unset 'foo.bar'
|
||||
atom.config.set 'foo.qux', 'boo'
|
||||
|
||||
expect(atom.config.get('foo.bar')).toBeUndefined()
|
||||
expect(atom.config.get('foo.qux')).toBe 'boo'
|
||||
expect(atom.config.get('do.ray')).toBeUndefined()
|
||||
|
||||
advanceClock 100
|
||||
expect(atom.config.save).not.toHaveBeenCalled()
|
||||
|
||||
atom.config.loadUserConfig()
|
||||
|
||||
advanceClock 100
|
||||
waitsFor -> atom.config.save.callCount > 0
|
||||
|
||||
runs ->
|
||||
expect(atom.config.get('foo.bar')).toBeUndefined()
|
||||
expect(atom.config.get('foo.qux')).toBe 'boo'
|
||||
expect(atom.config.get('do.ray')).toBe 'me'
|
||||
|
@ -1,5 +1,6 @@
|
||||
'name': 'Test Ruby'
|
||||
'scopeName': 'test.rb'
|
||||
'firstLineMatch': '^\\#!.*(?:\\s|\\/)(?:testruby)(?:$|\\s)'
|
||||
'fileTypes': [
|
||||
'rb'
|
||||
]
|
||||
|
5
spec/fixtures/packages/package-with-uri-handler/index.js
vendored
Normal file
5
spec/fixtures/packages/package-with-uri-handler/index.js
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
activate: () => null,
|
||||
deactivate: () => null,
|
||||
handleURI: () => null,
|
||||
}
|
6
spec/fixtures/packages/package-with-uri-handler/package.json
vendored
Normal file
6
spec/fixtures/packages/package-with-uri-handler/package.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "package-with-uri-handler",
|
||||
"uriHandler": {
|
||||
"method": "handleURI"
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
path = require 'path'
|
||||
fs = require 'fs-plus'
|
||||
temp = require('temp').track()
|
||||
{Directory} = require 'pathwatcher'
|
||||
GitRepository = require '../src/git-repository'
|
||||
GitRepositoryProvider = require '../src/git-repository-provider'
|
||||
|
||||
describe "GitRepositoryProvider", ->
|
||||
provider = null
|
||||
|
||||
beforeEach ->
|
||||
provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm)
|
||||
|
||||
afterEach ->
|
||||
if provider?
|
||||
provider.pathToRepository[key].destroy() for key in Object.keys(provider.pathToRepository)
|
||||
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe ".repositoryForDirectory(directory)", ->
|
||||
describe "when specified a Directory with a Git repository", ->
|
||||
it "returns a Promise that resolves to a GitRepository", ->
|
||||
waitsForPromise ->
|
||||
directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git')
|
||||
provider.repositoryForDirectory(directory).then (result) ->
|
||||
expect(result).toBeInstanceOf GitRepository
|
||||
expect(provider.pathToRepository[result.getPath()]).toBeTruthy()
|
||||
expect(result.statusTask).toBeTruthy()
|
||||
expect(result.getType()).toBe 'git'
|
||||
|
||||
it "returns the same GitRepository for different Directory objects in the same repo", ->
|
||||
firstRepo = null
|
||||
secondRepo = null
|
||||
|
||||
waitsForPromise ->
|
||||
directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git')
|
||||
provider.repositoryForDirectory(directory).then (result) -> firstRepo = result
|
||||
|
||||
waitsForPromise ->
|
||||
directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects')
|
||||
provider.repositoryForDirectory(directory).then (result) -> secondRepo = result
|
||||
|
||||
runs ->
|
||||
expect(firstRepo).toBeInstanceOf GitRepository
|
||||
expect(firstRepo).toBe secondRepo
|
||||
|
||||
describe "when specified a Directory without a Git repository", ->
|
||||
it "returns a Promise that resolves to null", ->
|
||||
waitsForPromise ->
|
||||
directory = new Directory temp.mkdirSync('dir')
|
||||
provider.repositoryForDirectory(directory).then (result) ->
|
||||
expect(result).toBe null
|
||||
|
||||
describe "when specified a Directory with an invalid Git repository", ->
|
||||
it "returns a Promise that resolves to null", ->
|
||||
waitsForPromise ->
|
||||
dirPath = temp.mkdirSync('dir')
|
||||
fs.writeFileSync(path.join(dirPath, '.git', 'objects'), '')
|
||||
fs.writeFileSync(path.join(dirPath, '.git', 'HEAD'), '')
|
||||
fs.writeFileSync(path.join(dirPath, '.git', 'refs'), '')
|
||||
|
||||
directory = new Directory dirPath
|
||||
provider.repositoryForDirectory(directory).then (result) ->
|
||||
expect(result).toBe null
|
||||
|
||||
describe "when specified a Directory with a valid gitfile-linked repository", ->
|
||||
it "returns a Promise that resolves to a GitRepository", ->
|
||||
waitsForPromise ->
|
||||
gitDirPath = path.join(__dirname, 'fixtures', 'git', 'master.git')
|
||||
workDirPath = temp.mkdirSync('git-workdir')
|
||||
fs.writeFileSync(path.join(workDirPath, '.git'), 'gitdir: ' + gitDirPath+'\n')
|
||||
|
||||
directory = new Directory workDirPath
|
||||
provider.repositoryForDirectory(directory).then (result) ->
|
||||
expect(result).toBeInstanceOf GitRepository
|
||||
expect(provider.pathToRepository[result.getPath()]).toBeTruthy()
|
||||
expect(result.statusTask).toBeTruthy()
|
||||
expect(result.getType()).toBe 'git'
|
||||
|
||||
describe "when specified a Directory without existsSync()", ->
|
||||
directory = null
|
||||
provider = null
|
||||
beforeEach ->
|
||||
# An implementation of Directory that does not implement existsSync().
|
||||
subdirectory = {}
|
||||
directory =
|
||||
getSubdirectory: ->
|
||||
isRoot: -> true
|
||||
spyOn(directory, "getSubdirectory").andReturn(subdirectory)
|
||||
|
||||
it "returns null", ->
|
||||
repo = provider.repositoryForDirectorySync(directory)
|
||||
expect(repo).toBe null
|
||||
expect(directory.getSubdirectory).toHaveBeenCalledWith(".git")
|
||||
|
||||
it "returns a Promise that resolves to null for the async implementation", ->
|
||||
waitsForPromise ->
|
||||
provider.repositoryForDirectory(directory).then (repo) ->
|
||||
expect(repo).toBe null
|
||||
expect(directory.getSubdirectory).toHaveBeenCalledWith(".git")
|
111
spec/git-repository-provider-spec.js
Normal file
111
spec/git-repository-provider-spec.js
Normal file
@ -0,0 +1,111 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs-plus')
|
||||
const temp = require('temp').track()
|
||||
const {Directory} = require('pathwatcher')
|
||||
const GitRepository = require('../src/git-repository')
|
||||
const GitRepositoryProvider = require('../src/git-repository-provider')
|
||||
const {it, fit, ffit, fffit, beforeEach} = require('./async-spec-helpers')
|
||||
|
||||
describe('GitRepositoryProvider', () => {
|
||||
let provider
|
||||
|
||||
beforeEach(() => {
|
||||
provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (provider) {
|
||||
Object.keys(provider.pathToRepository).forEach(key => {
|
||||
provider.pathToRepository[key].destroy()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('.repositoryForDirectory(directory)', () => {
|
||||
describe('when specified a Directory with a Git repository', () => {
|
||||
it('resolves with a GitRepository', async () => {
|
||||
const directory = new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git'))
|
||||
const result = await provider.repositoryForDirectory(directory)
|
||||
expect(result).toBeInstanceOf(GitRepository)
|
||||
expect(provider.pathToRepository[result.getPath()]).toBeTruthy()
|
||||
expect(result.getType()).toBe('git')
|
||||
|
||||
// Refresh should be started
|
||||
await new Promise(resolve => result.onDidChangeStatuses(resolve))
|
||||
})
|
||||
|
||||
it('resolves with the same GitRepository for different Directory objects in the same repo', async () => {
|
||||
const firstRepo = await provider.repositoryForDirectory(
|
||||
new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git'))
|
||||
)
|
||||
const secondRepo = await provider.repositoryForDirectory(
|
||||
new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects'))
|
||||
)
|
||||
|
||||
expect(firstRepo).toBeInstanceOf(GitRepository)
|
||||
expect(firstRepo).toBe(secondRepo)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when specified a Directory without a Git repository', () => {
|
||||
it('resolves with null', async () => {
|
||||
const directory = new Directory(temp.mkdirSync('dir'))
|
||||
const repo = await provider.repositoryForDirectory(directory)
|
||||
expect(repo).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when specified a Directory with an invalid Git repository', () => {
|
||||
it('resolves with null', async () => {
|
||||
const dirPath = temp.mkdirSync('dir')
|
||||
fs.writeFileSync(path.join(dirPath, '.git', 'objects'), '')
|
||||
fs.writeFileSync(path.join(dirPath, '.git', 'HEAD'), '')
|
||||
fs.writeFileSync(path.join(dirPath, '.git', 'refs'), '')
|
||||
|
||||
const directory = new Directory(dirPath)
|
||||
const repo = await provider.repositoryForDirectory(directory)
|
||||
expect(repo).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when specified a Directory with a valid gitfile-linked repository', () => {
|
||||
it('returns a Promise that resolves to a GitRepository', async () => {
|
||||
const gitDirPath = path.join(__dirname, 'fixtures', 'git', 'master.git')
|
||||
const workDirPath = temp.mkdirSync('git-workdir')
|
||||
fs.writeFileSync(path.join(workDirPath, '.git'), `gitdir: ${gitDirPath}\n`)
|
||||
|
||||
const directory = new Directory(workDirPath)
|
||||
const result = await provider.repositoryForDirectory(directory)
|
||||
expect(result).toBeInstanceOf(GitRepository)
|
||||
expect(provider.pathToRepository[result.getPath()]).toBeTruthy()
|
||||
expect(result.getType()).toBe('git')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when specified a Directory without existsSync()', () => {
|
||||
let directory
|
||||
|
||||
beforeEach(() => {
|
||||
// An implementation of Directory that does not implement existsSync().
|
||||
const subdirectory = {}
|
||||
directory = {
|
||||
getSubdirectory () {},
|
||||
isRoot () { return true }
|
||||
}
|
||||
spyOn(directory, 'getSubdirectory').andReturn(subdirectory)
|
||||
})
|
||||
|
||||
it('returns null', () => {
|
||||
const repo = provider.repositoryForDirectorySync(directory)
|
||||
expect(repo).toBe(null)
|
||||
expect(directory.getSubdirectory).toHaveBeenCalledWith('.git')
|
||||
})
|
||||
|
||||
it('returns a Promise that resolves to null for the async implementation', async () => {
|
||||
const repo = await provider.repositoryForDirectory(directory)
|
||||
expect(repo).toBe(null)
|
||||
expect(directory.getSubdirectory).toHaveBeenCalledWith('.git')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -283,11 +283,15 @@ describe "GitRepository", ->
|
||||
[editor] = []
|
||||
|
||||
beforeEach ->
|
||||
statusRefreshed = false
|
||||
atom.project.setPaths([copyRepository()])
|
||||
atom.project.getRepositories()[0].onDidChangeStatuses -> statusRefreshed = true
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('other.txt').then (o) -> editor = o
|
||||
|
||||
waitsFor 'repo to refresh', -> statusRefreshed
|
||||
|
||||
it "emits a status-changed event when a buffer is saved", ->
|
||||
editor.insertNewline()
|
||||
|
||||
|
@ -22,10 +22,12 @@ describe "the `grammars` global", ->
|
||||
atom.packages.activatePackage('language-git')
|
||||
|
||||
afterEach ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
waitsForPromise ->
|
||||
atom.packages.deactivatePackages()
|
||||
runs ->
|
||||
atom.packages.unloadPackages()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe ".selectGrammar(filePath)", ->
|
||||
it "always returns a grammar", ->
|
||||
@ -118,6 +120,8 @@ describe "the `grammars` global", ->
|
||||
atom.grammars.grammarForScopeName('source.ruby').bundledPackage = true
|
||||
atom.grammars.grammarForScopeName('test.rb').bundledPackage = false
|
||||
|
||||
expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env ruby').scopeName).toBe 'source.ruby'
|
||||
expect(atom.grammars.selectGrammar('test.rb', '#!/usr/bin/env testruby').scopeName).toBe 'test.rb'
|
||||
expect(atom.grammars.selectGrammar('test.rb').scopeName).toBe 'test.rb'
|
||||
|
||||
describe "when there is no file path", ->
|
||||
|
@ -1,64 +0,0 @@
|
||||
Gutter = require '../src/gutter'
|
||||
GutterContainer = require '../src/gutter-container'
|
||||
|
||||
describe 'GutterContainer', ->
|
||||
gutterContainer = null
|
||||
fakeTextEditor = {
|
||||
scheduleComponentUpdate: ->
|
||||
}
|
||||
|
||||
beforeEach ->
|
||||
gutterContainer = new GutterContainer fakeTextEditor
|
||||
|
||||
describe 'when initialized', ->
|
||||
it 'it has no gutters', ->
|
||||
expect(gutterContainer.getGutters().length).toBe 0
|
||||
|
||||
describe '::addGutter', ->
|
||||
it 'creates a new gutter', ->
|
||||
newGutter = gutterContainer.addGutter {'test-gutter', priority: 1}
|
||||
expect(gutterContainer.getGutters()).toEqual [newGutter]
|
||||
expect(newGutter.priority).toBe 1
|
||||
|
||||
it 'throws an error if the provided gutter name is already in use', ->
|
||||
name = 'test-gutter'
|
||||
gutterContainer.addGutter {name}
|
||||
expect(gutterContainer.addGutter.bind(null, {name})).toThrow()
|
||||
|
||||
it 'keeps added gutters sorted by ascending priority', ->
|
||||
gutter1 = gutterContainer.addGutter {name: 'first', priority: 1}
|
||||
gutter3 = gutterContainer.addGutter {name: 'third', priority: 3}
|
||||
gutter2 = gutterContainer.addGutter {name: 'second', priority: 2}
|
||||
expect(gutterContainer.getGutters()).toEqual [gutter1, gutter2, gutter3]
|
||||
|
||||
describe '::removeGutter', ->
|
||||
removedGutters = null
|
||||
|
||||
beforeEach ->
|
||||
gutterContainer = new GutterContainer fakeTextEditor
|
||||
removedGutters = []
|
||||
gutterContainer.onDidRemoveGutter (gutterName) ->
|
||||
removedGutters.push gutterName
|
||||
|
||||
it 'removes the gutter if it is contained by this GutterContainer', ->
|
||||
gutter = gutterContainer.addGutter {'test-gutter'}
|
||||
expect(gutterContainer.getGutters()).toEqual [gutter]
|
||||
gutterContainer.removeGutter gutter
|
||||
expect(gutterContainer.getGutters().length).toBe 0
|
||||
expect(removedGutters).toEqual [gutter.name]
|
||||
|
||||
it 'throws an error if the gutter is not within this GutterContainer', ->
|
||||
fakeOtherTextEditor = {}
|
||||
otherGutterContainer = new GutterContainer fakeOtherTextEditor
|
||||
gutter = new Gutter 'gutter-name', otherGutterContainer
|
||||
expect(gutterContainer.removeGutter.bind(null, gutter)).toThrow()
|
||||
|
||||
describe '::destroy', ->
|
||||
it 'clears its array of gutters and destroys custom gutters', ->
|
||||
newGutter = gutterContainer.addGutter {'test-gutter', priority: 1}
|
||||
newGutterSpy = jasmine.createSpy()
|
||||
newGutter.onDidDestroy(newGutterSpy)
|
||||
|
||||
gutterContainer.destroy()
|
||||
expect(newGutterSpy).toHaveBeenCalled()
|
||||
expect(gutterContainer.getGutters()).toEqual []
|
77
spec/gutter-container-spec.js
Normal file
77
spec/gutter-container-spec.js
Normal file
@ -0,0 +1,77 @@
|
||||
const Gutter = require('../src/gutter')
|
||||
const GutterContainer = require('../src/gutter-container')
|
||||
|
||||
describe('GutterContainer', () => {
|
||||
let gutterContainer = null
|
||||
const fakeTextEditor = {
|
||||
scheduleComponentUpdate () {}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
gutterContainer = new GutterContainer(fakeTextEditor)
|
||||
})
|
||||
|
||||
describe('when initialized', () =>
|
||||
it('it has no gutters', () => {
|
||||
expect(gutterContainer.getGutters().length).toBe(0)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::addGutter', () => {
|
||||
it('creates a new gutter', () => {
|
||||
const newGutter = gutterContainer.addGutter({'test-gutter': 'test-gutter', priority: 1})
|
||||
expect(gutterContainer.getGutters()).toEqual([newGutter])
|
||||
expect(newGutter.priority).toBe(1)
|
||||
})
|
||||
|
||||
it('throws an error if the provided gutter name is already in use', () => {
|
||||
const name = 'test-gutter'
|
||||
gutterContainer.addGutter({name})
|
||||
expect(gutterContainer.addGutter.bind(null, {name})).toThrow()
|
||||
})
|
||||
|
||||
it('keeps added gutters sorted by ascending priority', () => {
|
||||
const gutter1 = gutterContainer.addGutter({name: 'first', priority: 1})
|
||||
const gutter3 = gutterContainer.addGutter({name: 'third', priority: 3})
|
||||
const gutter2 = gutterContainer.addGutter({name: 'second', priority: 2})
|
||||
expect(gutterContainer.getGutters()).toEqual([gutter1, gutter2, gutter3])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::removeGutter', () => {
|
||||
let removedGutters
|
||||
|
||||
beforeEach(function () {
|
||||
gutterContainer = new GutterContainer(fakeTextEditor)
|
||||
removedGutters = []
|
||||
gutterContainer.onDidRemoveGutter(gutterName => removedGutters.push(gutterName))
|
||||
})
|
||||
|
||||
it('removes the gutter if it is contained by this GutterContainer', () => {
|
||||
const gutter = gutterContainer.addGutter({'test-gutter': 'test-gutter'})
|
||||
expect(gutterContainer.getGutters()).toEqual([gutter])
|
||||
gutterContainer.removeGutter(gutter)
|
||||
expect(gutterContainer.getGutters().length).toBe(0)
|
||||
expect(removedGutters).toEqual([gutter.name])
|
||||
})
|
||||
|
||||
it('throws an error if the gutter is not within this GutterContainer', () => {
|
||||
const fakeOtherTextEditor = {}
|
||||
const otherGutterContainer = new GutterContainer(fakeOtherTextEditor)
|
||||
const gutter = new Gutter('gutter-name', otherGutterContainer)
|
||||
expect(gutterContainer.removeGutter.bind(null, gutter)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('::destroy', () =>
|
||||
it('clears its array of gutters and destroys custom gutters', () => {
|
||||
const newGutter = gutterContainer.addGutter({'test-gutter': 'test-gutter', priority: 1})
|
||||
const newGutterSpy = jasmine.createSpy()
|
||||
newGutter.onDidDestroy(newGutterSpy)
|
||||
|
||||
gutterContainer.destroy()
|
||||
expect(newGutterSpy).toHaveBeenCalled()
|
||||
expect(gutterContainer.getGutters()).toEqual([])
|
||||
})
|
||||
)
|
||||
})
|
@ -1,70 +0,0 @@
|
||||
Gutter = require '../src/gutter'
|
||||
|
||||
describe 'Gutter', ->
|
||||
fakeGutterContainer = {
|
||||
scheduleComponentUpdate: ->
|
||||
}
|
||||
name = 'name'
|
||||
|
||||
describe '::hide', ->
|
||||
it 'hides the gutter if it is visible.', ->
|
||||
options =
|
||||
name: name
|
||||
visible: true
|
||||
gutter = new Gutter fakeGutterContainer, options
|
||||
events = []
|
||||
gutter.onDidChangeVisible (gutter) ->
|
||||
events.push gutter.isVisible()
|
||||
|
||||
expect(gutter.isVisible()).toBe true
|
||||
gutter.hide()
|
||||
expect(gutter.isVisible()).toBe false
|
||||
expect(events).toEqual [false]
|
||||
gutter.hide()
|
||||
expect(gutter.isVisible()).toBe false
|
||||
# An event should only be emitted when the visibility changes.
|
||||
expect(events.length).toBe 1
|
||||
|
||||
describe '::show', ->
|
||||
it 'shows the gutter if it is hidden.', ->
|
||||
options =
|
||||
name: name
|
||||
visible: false
|
||||
gutter = new Gutter fakeGutterContainer, options
|
||||
events = []
|
||||
gutter.onDidChangeVisible (gutter) ->
|
||||
events.push gutter.isVisible()
|
||||
|
||||
expect(gutter.isVisible()).toBe false
|
||||
gutter.show()
|
||||
expect(gutter.isVisible()).toBe true
|
||||
expect(events).toEqual [true]
|
||||
gutter.show()
|
||||
expect(gutter.isVisible()).toBe true
|
||||
# An event should only be emitted when the visibility changes.
|
||||
expect(events.length).toBe 1
|
||||
|
||||
describe '::destroy', ->
|
||||
[mockGutterContainer, mockGutterContainerRemovedGutters] = []
|
||||
|
||||
beforeEach ->
|
||||
mockGutterContainerRemovedGutters = []
|
||||
mockGutterContainer = removeGutter: (destroyedGutter) ->
|
||||
mockGutterContainerRemovedGutters.push destroyedGutter
|
||||
|
||||
it 'removes the gutter from its container.', ->
|
||||
gutter = new Gutter mockGutterContainer, {name}
|
||||
gutter.destroy()
|
||||
expect(mockGutterContainerRemovedGutters).toEqual([gutter])
|
||||
|
||||
it 'calls all callbacks registered on ::onDidDestroy.', ->
|
||||
gutter = new Gutter mockGutterContainer, {name}
|
||||
didDestroy = false
|
||||
gutter.onDidDestroy ->
|
||||
didDestroy = true
|
||||
gutter.destroy()
|
||||
expect(didDestroy).toBe true
|
||||
|
||||
it 'does not allow destroying the line-number gutter', ->
|
||||
gutter = new Gutter mockGutterContainer, {name: 'line-number'}
|
||||
expect(gutter.destroy).toThrow()
|
82
spec/gutter-spec.js
Normal file
82
spec/gutter-spec.js
Normal file
@ -0,0 +1,82 @@
|
||||
const Gutter = require('../src/gutter')
|
||||
|
||||
describe('Gutter', () => {
|
||||
const fakeGutterContainer = {
|
||||
scheduleComponentUpdate () {}
|
||||
}
|
||||
const name = 'name'
|
||||
|
||||
describe('::hide', () =>
|
||||
it('hides the gutter if it is visible.', () => {
|
||||
const options = {
|
||||
name,
|
||||
visible: true
|
||||
}
|
||||
const gutter = new Gutter(fakeGutterContainer, options)
|
||||
const events = []
|
||||
gutter.onDidChangeVisible(gutter => events.push(gutter.isVisible()))
|
||||
|
||||
expect(gutter.isVisible()).toBe(true)
|
||||
gutter.hide()
|
||||
expect(gutter.isVisible()).toBe(false)
|
||||
expect(events).toEqual([false])
|
||||
gutter.hide()
|
||||
expect(gutter.isVisible()).toBe(false)
|
||||
// An event should only be emitted when the visibility changes.
|
||||
expect(events.length).toBe(1)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::show', () =>
|
||||
it('shows the gutter if it is hidden.', () => {
|
||||
const options = {
|
||||
name,
|
||||
visible: false
|
||||
}
|
||||
const gutter = new Gutter(fakeGutterContainer, options)
|
||||
const events = []
|
||||
gutter.onDidChangeVisible(gutter => events.push(gutter.isVisible()))
|
||||
|
||||
expect(gutter.isVisible()).toBe(false)
|
||||
gutter.show()
|
||||
expect(gutter.isVisible()).toBe(true)
|
||||
expect(events).toEqual([true])
|
||||
gutter.show()
|
||||
expect(gutter.isVisible()).toBe(true)
|
||||
// An event should only be emitted when the visibility changes.
|
||||
expect(events.length).toBe(1)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::destroy', () => {
|
||||
let mockGutterContainer, mockGutterContainerRemovedGutters
|
||||
|
||||
beforeEach(() => {
|
||||
mockGutterContainerRemovedGutters = []
|
||||
mockGutterContainer = {
|
||||
removeGutter (destroyedGutter) {
|
||||
mockGutterContainerRemovedGutters.push(destroyedGutter)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('removes the gutter from its container.', () => {
|
||||
const gutter = new Gutter(mockGutterContainer, {name})
|
||||
gutter.destroy()
|
||||
expect(mockGutterContainerRemovedGutters).toEqual([gutter])
|
||||
})
|
||||
|
||||
it('calls all callbacks registered on ::onDidDestroy.', () => {
|
||||
const gutter = new Gutter(mockGutterContainer, {name})
|
||||
let didDestroy = false
|
||||
gutter.onDidDestroy(() => { didDestroy = true })
|
||||
gutter.destroy()
|
||||
expect(didDestroy).toBe(true)
|
||||
})
|
||||
|
||||
it('does not allow destroying the line-number gutter', () => {
|
||||
const gutter = new Gutter(mockGutterContainer, {name: 'line-number'})
|
||||
expect(gutter.destroy).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
42
spec/helpers/random.js
Normal file
42
spec/helpers/random.js
Normal file
@ -0,0 +1,42 @@
|
||||
const WORDS = require('./words')
|
||||
const {Point, Range} = require('text-buffer')
|
||||
|
||||
exports.getRandomBufferRange = function getRandomBufferRange (random, buffer) {
|
||||
const endRow = random(buffer.getLineCount())
|
||||
const startRow = random.intBetween(0, endRow)
|
||||
const startColumn = random(buffer.lineForRow(startRow).length + 1)
|
||||
const endColumn = random(buffer.lineForRow(endRow).length + 1)
|
||||
return Range(Point(startRow, startColumn), Point(endRow, endColumn))
|
||||
}
|
||||
|
||||
exports.buildRandomLines = function buildRandomLines (random, maxLines) {
|
||||
const lines = []
|
||||
|
||||
for (let i = 0; i < random(maxLines); i++) {
|
||||
lines.push(buildRandomLine(random))
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function buildRandomLine (random) {
|
||||
const line = []
|
||||
|
||||
for (let i = 0; i < random(5); i++) {
|
||||
const n = random(10)
|
||||
|
||||
if (n < 2) {
|
||||
line.push('\t')
|
||||
} else if (n < 4) {
|
||||
line.push(' ')
|
||||
} else {
|
||||
if (line.length > 0 && !/\s/.test(line[line.length - 1])) {
|
||||
line.push(' ')
|
||||
}
|
||||
|
||||
line.push(WORDS[random(WORDS.length)])
|
||||
}
|
||||
}
|
||||
|
||||
return line.join('')
|
||||
}
|
46891
spec/helpers/words.js
Normal file
46891
spec/helpers/words.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,490 +0,0 @@
|
||||
describe "LanguageMode", ->
|
||||
[editor, buffer, languageMode] = []
|
||||
|
||||
afterEach ->
|
||||
editor.destroy()
|
||||
|
||||
describe "javascript", ->
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('sample.js', autoIndent: false).then (o) ->
|
||||
editor = o
|
||||
{buffer, languageMode} = editor
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-javascript')
|
||||
|
||||
afterEach ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
|
||||
describe ".minIndentLevelForRowRange(startRow, endRow)", ->
|
||||
it "returns the minimum indent level for the given row range", ->
|
||||
expect(languageMode.minIndentLevelForRowRange(4, 7)).toBe 2
|
||||
expect(languageMode.minIndentLevelForRowRange(5, 7)).toBe 2
|
||||
expect(languageMode.minIndentLevelForRowRange(5, 6)).toBe 3
|
||||
expect(languageMode.minIndentLevelForRowRange(9, 11)).toBe 1
|
||||
expect(languageMode.minIndentLevelForRowRange(10, 10)).toBe 0
|
||||
|
||||
describe ".toggleLineCommentsForBufferRows(start, end)", ->
|
||||
it "comments/uncomments lines in the given range", ->
|
||||
languageMode.toggleLineCommentsForBufferRows(4, 7)
|
||||
expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {"
|
||||
expect(buffer.lineForRow(5)).toBe " // current = items.shift();"
|
||||
expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);"
|
||||
expect(buffer.lineForRow(7)).toBe " // }"
|
||||
|
||||
languageMode.toggleLineCommentsForBufferRows(4, 5)
|
||||
expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {"
|
||||
expect(buffer.lineForRow(5)).toBe " current = items.shift();"
|
||||
expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);"
|
||||
expect(buffer.lineForRow(7)).toBe " // }"
|
||||
|
||||
buffer.setText('\tvar i;')
|
||||
languageMode.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(buffer.lineForRow(0)).toBe "\t// var i;"
|
||||
|
||||
buffer.setText('var i;')
|
||||
languageMode.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(buffer.lineForRow(0)).toBe "// var i;"
|
||||
|
||||
buffer.setText(' var i;')
|
||||
languageMode.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(buffer.lineForRow(0)).toBe " // var i;"
|
||||
|
||||
buffer.setText(' ')
|
||||
languageMode.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(buffer.lineForRow(0)).toBe " // "
|
||||
|
||||
buffer.setText(' a\n \n b')
|
||||
languageMode.toggleLineCommentsForBufferRows(0, 2)
|
||||
expect(buffer.lineForRow(0)).toBe " // a"
|
||||
expect(buffer.lineForRow(1)).toBe " // "
|
||||
expect(buffer.lineForRow(2)).toBe " // b"
|
||||
|
||||
buffer.setText(' \n // var i;')
|
||||
languageMode.toggleLineCommentsForBufferRows(0, 1)
|
||||
expect(buffer.lineForRow(0)).toBe ' '
|
||||
expect(buffer.lineForRow(1)).toBe ' var i;'
|
||||
|
||||
describe ".rowRangeForCodeFoldAtBufferRow(bufferRow)", ->
|
||||
it "returns the start/end rows of the foldable region starting at the given row", ->
|
||||
expect(languageMode.rowRangeForCodeFoldAtBufferRow(0)).toEqual [0, 12]
|
||||
expect(languageMode.rowRangeForCodeFoldAtBufferRow(1)).toEqual [1, 9]
|
||||
expect(languageMode.rowRangeForCodeFoldAtBufferRow(2)).toBeNull()
|
||||
expect(languageMode.rowRangeForCodeFoldAtBufferRow(4)).toEqual [4, 7]
|
||||
|
||||
describe ".rowRangeForCommentAtBufferRow(bufferRow)", ->
|
||||
it "returns the start/end rows of the foldable comment starting at the given row", ->
|
||||
buffer.setText("//this is a multi line comment\n//another line")
|
||||
expect(languageMode.rowRangeForCommentAtBufferRow(0)).toEqual [0, 1]
|
||||
expect(languageMode.rowRangeForCommentAtBufferRow(1)).toEqual [0, 1]
|
||||
|
||||
buffer.setText("//this is a multi line comment\n//another line\n//and one more")
|
||||
expect(languageMode.rowRangeForCommentAtBufferRow(0)).toEqual [0, 2]
|
||||
expect(languageMode.rowRangeForCommentAtBufferRow(1)).toEqual [0, 2]
|
||||
|
||||
buffer.setText("//this is a multi line comment\n\n//with an empty line")
|
||||
expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined()
|
||||
expect(languageMode.rowRangeForCommentAtBufferRow(1)).toBeUndefined()
|
||||
expect(languageMode.rowRangeForCommentAtBufferRow(2)).toBeUndefined()
|
||||
|
||||
buffer.setText("//this is a single line comment\n")
|
||||
expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined()
|
||||
expect(languageMode.rowRangeForCommentAtBufferRow(1)).toBeUndefined()
|
||||
|
||||
buffer.setText("//this is a single line comment")
|
||||
expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined()
|
||||
|
||||
describe ".suggestedIndentForBufferRow", ->
|
||||
it "bases indentation off of the previous non-blank line", ->
|
||||
expect(languageMode.suggestedIndentForBufferRow(0)).toBe 0
|
||||
expect(languageMode.suggestedIndentForBufferRow(1)).toBe 1
|
||||
expect(languageMode.suggestedIndentForBufferRow(2)).toBe 2
|
||||
expect(languageMode.suggestedIndentForBufferRow(5)).toBe 3
|
||||
expect(languageMode.suggestedIndentForBufferRow(7)).toBe 2
|
||||
expect(languageMode.suggestedIndentForBufferRow(9)).toBe 1
|
||||
expect(languageMode.suggestedIndentForBufferRow(11)).toBe 1
|
||||
|
||||
it "does not take invisibles into account", ->
|
||||
editor.update({showInvisibles: true})
|
||||
expect(languageMode.suggestedIndentForBufferRow(0)).toBe 0
|
||||
expect(languageMode.suggestedIndentForBufferRow(1)).toBe 1
|
||||
expect(languageMode.suggestedIndentForBufferRow(2)).toBe 2
|
||||
expect(languageMode.suggestedIndentForBufferRow(5)).toBe 3
|
||||
expect(languageMode.suggestedIndentForBufferRow(7)).toBe 2
|
||||
expect(languageMode.suggestedIndentForBufferRow(9)).toBe 1
|
||||
expect(languageMode.suggestedIndentForBufferRow(11)).toBe 1
|
||||
|
||||
describe "rowRangeForParagraphAtBufferRow", ->
|
||||
describe "with code and comments", ->
|
||||
beforeEach ->
|
||||
buffer.setText '''
|
||||
var quicksort = function () {
|
||||
/* Single line comment block */
|
||||
var sort = function(items) {};
|
||||
|
||||
/*
|
||||
A multiline
|
||||
comment is here
|
||||
*/
|
||||
var sort = function(items) {};
|
||||
|
||||
// A comment
|
||||
//
|
||||
// Multiple comment
|
||||
// lines
|
||||
var sort = function(items) {};
|
||||
// comment line after fn
|
||||
|
||||
var nosort = function(items) {
|
||||
return item;
|
||||
}
|
||||
|
||||
};
|
||||
'''
|
||||
|
||||
it "will limit paragraph range to comments", ->
|
||||
range = languageMode.rowRangeForParagraphAtBufferRow(0)
|
||||
expect(range).toEqual [[0, 0], [0, 29]]
|
||||
|
||||
range = languageMode.rowRangeForParagraphAtBufferRow(10)
|
||||
expect(range).toEqual [[10, 0], [10, 14]]
|
||||
range = languageMode.rowRangeForParagraphAtBufferRow(11)
|
||||
expect(range).toBeFalsy()
|
||||
range = languageMode.rowRangeForParagraphAtBufferRow(12)
|
||||
expect(range).toEqual [[12, 0], [13, 10]]
|
||||
|
||||
range = languageMode.rowRangeForParagraphAtBufferRow(14)
|
||||
expect(range).toEqual [[14, 0], [14, 32]]
|
||||
|
||||
range = languageMode.rowRangeForParagraphAtBufferRow(15)
|
||||
expect(range).toEqual [[15, 0], [15, 26]]
|
||||
|
||||
range = languageMode.rowRangeForParagraphAtBufferRow(18)
|
||||
expect(range).toEqual [[17, 0], [19, 3]]
|
||||
|
||||
describe "coffeescript", ->
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('coffee.coffee', autoIndent: false).then (o) ->
|
||||
editor = o
|
||||
{buffer, languageMode} = editor
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-coffee-script')
|
||||
|
||||
afterEach ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
|
||||
describe ".toggleLineCommentsForBufferRows(start, end)", ->
|
||||
it "comments/uncomments lines in the given range", ->
|
||||
languageMode.toggleLineCommentsForBufferRows(4, 6)
|
||||
expect(buffer.lineForRow(4)).toBe " # pivot = items.shift()"
|
||||
expect(buffer.lineForRow(5)).toBe " # left = []"
|
||||
expect(buffer.lineForRow(6)).toBe " # right = []"
|
||||
|
||||
languageMode.toggleLineCommentsForBufferRows(4, 5)
|
||||
expect(buffer.lineForRow(4)).toBe " pivot = items.shift()"
|
||||
expect(buffer.lineForRow(5)).toBe " left = []"
|
||||
expect(buffer.lineForRow(6)).toBe " # right = []"
|
||||
|
||||
it "comments/uncomments lines when empty line", ->
|
||||
languageMode.toggleLineCommentsForBufferRows(4, 7)
|
||||
expect(buffer.lineForRow(4)).toBe " # pivot = items.shift()"
|
||||
expect(buffer.lineForRow(5)).toBe " # left = []"
|
||||
expect(buffer.lineForRow(6)).toBe " # right = []"
|
||||
expect(buffer.lineForRow(7)).toBe " # "
|
||||
|
||||
languageMode.toggleLineCommentsForBufferRows(4, 5)
|
||||
expect(buffer.lineForRow(4)).toBe " pivot = items.shift()"
|
||||
expect(buffer.lineForRow(5)).toBe " left = []"
|
||||
expect(buffer.lineForRow(6)).toBe " # right = []"
|
||||
expect(buffer.lineForRow(7)).toBe " # "
|
||||
|
||||
describe "fold suggestion", ->
|
||||
describe ".rowRangeForCodeFoldAtBufferRow(bufferRow)", ->
|
||||
it "returns the start/end rows of the foldable region starting at the given row", ->
|
||||
expect(languageMode.rowRangeForCodeFoldAtBufferRow(0)).toEqual [0, 20]
|
||||
expect(languageMode.rowRangeForCodeFoldAtBufferRow(1)).toEqual [1, 17]
|
||||
expect(languageMode.rowRangeForCodeFoldAtBufferRow(2)).toBeNull()
|
||||
expect(languageMode.rowRangeForCodeFoldAtBufferRow(19)).toEqual [19, 20]
|
||||
|
||||
describe "css", ->
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('css.css', autoIndent: false).then (o) ->
|
||||
editor = o
|
||||
{buffer, languageMode} = editor
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-css')
|
||||
|
||||
afterEach ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
|
||||
describe ".toggleLineCommentsForBufferRows(start, end)", ->
|
||||
it "comments/uncomments lines in the given range", ->
|
||||
languageMode.toggleLineCommentsForBufferRows(0, 1)
|
||||
expect(buffer.lineForRow(0)).toBe "/*body {"
|
||||
expect(buffer.lineForRow(1)).toBe " font-size: 1234px;*/"
|
||||
expect(buffer.lineForRow(2)).toBe " width: 110%;"
|
||||
expect(buffer.lineForRow(3)).toBe " font-weight: bold !important;"
|
||||
|
||||
languageMode.toggleLineCommentsForBufferRows(2, 2)
|
||||
expect(buffer.lineForRow(0)).toBe "/*body {"
|
||||
expect(buffer.lineForRow(1)).toBe " font-size: 1234px;*/"
|
||||
expect(buffer.lineForRow(2)).toBe " /*width: 110%;*/"
|
||||
expect(buffer.lineForRow(3)).toBe " font-weight: bold !important;"
|
||||
|
||||
languageMode.toggleLineCommentsForBufferRows(0, 1)
|
||||
expect(buffer.lineForRow(0)).toBe "body {"
|
||||
expect(buffer.lineForRow(1)).toBe " font-size: 1234px;"
|
||||
expect(buffer.lineForRow(2)).toBe " /*width: 110%;*/"
|
||||
expect(buffer.lineForRow(3)).toBe " font-weight: bold !important;"
|
||||
|
||||
it "uncomments lines with leading whitespace", ->
|
||||
buffer.setTextInRange([[2, 0], [2, Infinity]], " /*width: 110%;*/")
|
||||
languageMode.toggleLineCommentsForBufferRows(2, 2)
|
||||
expect(buffer.lineForRow(2)).toBe " width: 110%;"
|
||||
|
||||
it "uncomments lines with trailing whitespace", ->
|
||||
buffer.setTextInRange([[2, 0], [2, Infinity]], "/*width: 110%;*/ ")
|
||||
languageMode.toggleLineCommentsForBufferRows(2, 2)
|
||||
expect(buffer.lineForRow(2)).toBe "width: 110%; "
|
||||
|
||||
it "uncomments lines with leading and trailing whitespace", ->
|
||||
buffer.setTextInRange([[2, 0], [2, Infinity]], " /*width: 110%;*/ ")
|
||||
languageMode.toggleLineCommentsForBufferRows(2, 2)
|
||||
expect(buffer.lineForRow(2)).toBe " width: 110%; "
|
||||
|
||||
describe "less", ->
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('sample.less', autoIndent: false).then (o) ->
|
||||
editor = o
|
||||
{buffer, languageMode} = editor
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-less')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-css')
|
||||
|
||||
afterEach ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
|
||||
describe "when commenting lines", ->
|
||||
it "only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart`", ->
|
||||
languageMode.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(buffer.lineForRow(0)).toBe "// @color: #4D926F;"
|
||||
|
||||
describe "xml", ->
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('sample.xml', autoIndent: false).then (o) ->
|
||||
editor = o
|
||||
editor.setText("<!-- test -->")
|
||||
{buffer, languageMode} = editor
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-xml')
|
||||
|
||||
afterEach ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
|
||||
describe "when uncommenting lines", ->
|
||||
it "removes the leading whitespace from the comment end pattern match", ->
|
||||
languageMode.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(buffer.lineForRow(0)).toBe "test"
|
||||
|
||||
describe "folding", ->
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('sample.js', autoIndent: false).then (o) ->
|
||||
editor = o
|
||||
{buffer, languageMode} = editor
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-javascript')
|
||||
|
||||
afterEach ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
|
||||
it "maintains cursor buffer position when a folding/unfolding", ->
|
||||
editor.setCursorBufferPosition([5, 5])
|
||||
languageMode.foldAll()
|
||||
expect(editor.getCursorBufferPosition()).toEqual([5, 5])
|
||||
|
||||
describe ".unfoldAll()", ->
|
||||
it "unfolds every folded line", ->
|
||||
initialScreenLineCount = editor.getScreenLineCount()
|
||||
languageMode.foldBufferRow(0)
|
||||
languageMode.foldBufferRow(1)
|
||||
expect(editor.getScreenLineCount()).toBeLessThan initialScreenLineCount
|
||||
languageMode.unfoldAll()
|
||||
expect(editor.getScreenLineCount()).toBe initialScreenLineCount
|
||||
|
||||
describe ".foldAll()", ->
|
||||
it "folds every foldable line", ->
|
||||
languageMode.foldAll()
|
||||
|
||||
[fold1, fold2, fold3] = languageMode.unfoldAll()
|
||||
expect([fold1.start.row, fold1.end.row]).toEqual [0, 12]
|
||||
expect([fold2.start.row, fold2.end.row]).toEqual [1, 9]
|
||||
expect([fold3.start.row, fold3.end.row]).toEqual [4, 7]
|
||||
|
||||
describe ".foldBufferRow(bufferRow)", ->
|
||||
describe "when bufferRow can be folded", ->
|
||||
it "creates a fold based on the syntactic region starting at the given row", ->
|
||||
languageMode.foldBufferRow(1)
|
||||
[fold] = languageMode.unfoldAll()
|
||||
expect([fold.start.row, fold.end.row]).toEqual [1, 9]
|
||||
|
||||
describe "when bufferRow can't be folded", ->
|
||||
it "searches upward for the first row that begins a syntatic region containing the given buffer row (and folds it)", ->
|
||||
languageMode.foldBufferRow(8)
|
||||
[fold] = languageMode.unfoldAll()
|
||||
expect([fold.start.row, fold.end.row]).toEqual [1, 9]
|
||||
|
||||
describe "when the bufferRow is already folded", ->
|
||||
it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", ->
|
||||
languageMode.foldBufferRow(2)
|
||||
expect(editor.isFoldedAtBufferRow(0)).toBe(false)
|
||||
expect(editor.isFoldedAtBufferRow(1)).toBe(true)
|
||||
|
||||
languageMode.foldBufferRow(1)
|
||||
expect(editor.isFoldedAtBufferRow(0)).toBe(true)
|
||||
|
||||
describe "when the bufferRow is in a multi-line comment", ->
|
||||
it "searches upward and downward for surrounding comment lines and folds them as a single fold", ->
|
||||
buffer.insert([1, 0], " //this is a comment\n // and\n //more docs\n\n//second comment")
|
||||
languageMode.foldBufferRow(1)
|
||||
[fold] = languageMode.unfoldAll()
|
||||
expect([fold.start.row, fold.end.row]).toEqual [1, 3]
|
||||
|
||||
describe "when the bufferRow is a single-line comment", ->
|
||||
it "searches upward for the first row that begins a syntatic region containing the folded row (and folds it)", ->
|
||||
buffer.insert([1, 0], " //this is a single line comment\n")
|
||||
languageMode.foldBufferRow(1)
|
||||
[fold] = languageMode.unfoldAll()
|
||||
expect([fold.start.row, fold.end.row]).toEqual [0, 13]
|
||||
|
||||
describe ".foldAllAtIndentLevel(indentLevel)", ->
|
||||
it "folds blocks of text at the given indentation level", ->
|
||||
languageMode.foldAllAtIndentLevel(0)
|
||||
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" + editor.displayLayer.foldCharacter
|
||||
expect(editor.getLastScreenRow()).toBe 0
|
||||
|
||||
languageMode.foldAllAtIndentLevel(1)
|
||||
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
|
||||
expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {" + editor.displayLayer.foldCharacter
|
||||
expect(editor.getLastScreenRow()).toBe 4
|
||||
|
||||
languageMode.foldAllAtIndentLevel(2)
|
||||
expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {"
|
||||
expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {"
|
||||
expect(editor.lineTextForScreenRow(2)).toBe " if (items.length <= 1) return items;"
|
||||
expect(editor.getLastScreenRow()).toBe 9
|
||||
|
||||
describe "folding with comments", ->
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('sample-with-comments.js', autoIndent: false).then (o) ->
|
||||
editor = o
|
||||
{buffer, languageMode} = editor
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-javascript')
|
||||
|
||||
afterEach ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
|
||||
describe ".unfoldAll()", ->
|
||||
it "unfolds every folded line", ->
|
||||
initialScreenLineCount = editor.getScreenLineCount()
|
||||
languageMode.foldBufferRow(0)
|
||||
languageMode.foldBufferRow(5)
|
||||
expect(editor.getScreenLineCount()).toBeLessThan initialScreenLineCount
|
||||
languageMode.unfoldAll()
|
||||
expect(editor.getScreenLineCount()).toBe initialScreenLineCount
|
||||
|
||||
describe ".foldAll()", ->
|
||||
it "folds every foldable line", ->
|
||||
languageMode.foldAll()
|
||||
|
||||
folds = languageMode.unfoldAll()
|
||||
expect(folds.length).toBe 8
|
||||
expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30]
|
||||
expect([folds[1].start.row, folds[1].end.row]).toEqual [1, 4]
|
||||
expect([folds[2].start.row, folds[2].end.row]).toEqual [5, 27]
|
||||
expect([folds[3].start.row, folds[3].end.row]).toEqual [6, 8]
|
||||
expect([folds[4].start.row, folds[4].end.row]).toEqual [11, 16]
|
||||
expect([folds[5].start.row, folds[5].end.row]).toEqual [17, 20]
|
||||
expect([folds[6].start.row, folds[6].end.row]).toEqual [21, 22]
|
||||
expect([folds[7].start.row, folds[7].end.row]).toEqual [24, 25]
|
||||
|
||||
describe ".foldAllAtIndentLevel()", ->
|
||||
it "folds every foldable range at a given indentLevel", ->
|
||||
languageMode.foldAllAtIndentLevel(2)
|
||||
|
||||
folds = languageMode.unfoldAll()
|
||||
expect(folds.length).toBe 5
|
||||
expect([folds[0].start.row, folds[0].end.row]).toEqual [6, 8]
|
||||
expect([folds[1].start.row, folds[1].end.row]).toEqual [11, 16]
|
||||
expect([folds[2].start.row, folds[2].end.row]).toEqual [17, 20]
|
||||
expect([folds[3].start.row, folds[3].end.row]).toEqual [21, 22]
|
||||
expect([folds[4].start.row, folds[4].end.row]).toEqual [24, 25]
|
||||
|
||||
it "does not fold anything but the indentLevel", ->
|
||||
languageMode.foldAllAtIndentLevel(0)
|
||||
|
||||
folds = languageMode.unfoldAll()
|
||||
expect(folds.length).toBe 1
|
||||
expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30]
|
||||
|
||||
describe ".isFoldableAtBufferRow(bufferRow)", ->
|
||||
it "returns true if the line starts a multi-line comment", ->
|
||||
expect(languageMode.isFoldableAtBufferRow(1)).toBe true
|
||||
expect(languageMode.isFoldableAtBufferRow(6)).toBe true
|
||||
expect(languageMode.isFoldableAtBufferRow(8)).toBe false
|
||||
expect(languageMode.isFoldableAtBufferRow(11)).toBe true
|
||||
expect(languageMode.isFoldableAtBufferRow(15)).toBe false
|
||||
expect(languageMode.isFoldableAtBufferRow(17)).toBe true
|
||||
expect(languageMode.isFoldableAtBufferRow(21)).toBe true
|
||||
expect(languageMode.isFoldableAtBufferRow(24)).toBe true
|
||||
expect(languageMode.isFoldableAtBufferRow(28)).toBe false
|
||||
|
||||
it "returns true for lines that end with a comment and are followed by an indented line", ->
|
||||
expect(languageMode.isFoldableAtBufferRow(5)).toBe true
|
||||
|
||||
it "does not return true for a line in the middle of a comment that's followed by an indented line", ->
|
||||
expect(languageMode.isFoldableAtBufferRow(7)).toBe false
|
||||
editor.buffer.insert([8, 0], ' ')
|
||||
expect(languageMode.isFoldableAtBufferRow(7)).toBe false
|
||||
|
||||
describe "css", ->
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('css.css', autoIndent: true).then (o) ->
|
||||
editor = o
|
||||
{buffer, languageMode} = editor
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-source')
|
||||
atom.packages.activatePackage('language-css')
|
||||
|
||||
afterEach ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
|
||||
describe "suggestedIndentForBufferRow", ->
|
||||
it "does not return negative values (regression)", ->
|
||||
editor.setText('.test {\npadding: 0;\n}')
|
||||
expect(editor.suggestedIndentForBufferRow(2)).toBe 0
|
27
spec/main-process/parse-command-line.test.js
Normal file
27
spec/main-process/parse-command-line.test.js
Normal file
@ -0,0 +1,27 @@
|
||||
/** @babel */
|
||||
|
||||
import parseCommandLine from '../../src/main-process/parse-command-line'
|
||||
|
||||
describe('parseCommandLine', function () {
|
||||
describe('when --uri-handler is not passed', function () {
|
||||
it('parses arguments as normal', function () {
|
||||
const args = parseCommandLine(['-d', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url'])
|
||||
assert.isTrue(args.devMode)
|
||||
assert.isTrue(args.safeMode)
|
||||
assert.isTrue(args.test)
|
||||
assert.deepEqual(args.urlsToOpen, ['atom://test/url', 'atom://other/url'])
|
||||
assert.deepEqual(args.pathsToOpen, ['/some/path'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when --uri-handler is passed', function () {
|
||||
it('ignores other arguments and limits to one URL', function () {
|
||||
const args = parseCommandLine(['-d', '--uri-handler', '--safe', '--test', '/some/path', 'atom://test/url', 'atom://other/url'])
|
||||
assert.isUndefined(args.devMode)
|
||||
assert.isUndefined(args.safeMode)
|
||||
assert.isUndefined(args.test)
|
||||
assert.deepEqual(args.urlsToOpen, ['atom://test/url'])
|
||||
assert.deepEqual(args.pathsToOpen, [])
|
||||
})
|
||||
})
|
||||
})
|
@ -1,57 +0,0 @@
|
||||
NotificationManager = require '../src/notification-manager'
|
||||
|
||||
describe "NotificationManager", ->
|
||||
[manager] = []
|
||||
|
||||
beforeEach ->
|
||||
manager = new NotificationManager
|
||||
|
||||
describe "the atom global", ->
|
||||
it "has a notifications instance", ->
|
||||
expect(atom.notifications instanceof NotificationManager).toBe true
|
||||
|
||||
describe "adding events", ->
|
||||
addSpy = null
|
||||
|
||||
beforeEach ->
|
||||
addSpy = jasmine.createSpy()
|
||||
manager.onDidAddNotification(addSpy)
|
||||
|
||||
it "emits an event when a notification has been added", ->
|
||||
manager.add('error', 'Some error!', icon: 'someIcon')
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
|
||||
notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'error'
|
||||
expect(notification.getMessage()).toBe 'Some error!'
|
||||
expect(notification.getIcon()).toBe 'someIcon'
|
||||
|
||||
it "emits a fatal error ::addFatalError has been called", ->
|
||||
manager.addFatalError('Some error!', icon: 'someIcon')
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'fatal'
|
||||
|
||||
it "emits an error ::addError has been called", ->
|
||||
manager.addError('Some error!', icon: 'someIcon')
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'error'
|
||||
|
||||
it "emits a warning notification ::addWarning has been called", ->
|
||||
manager.addWarning('Something!', icon: 'someIcon')
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'warning'
|
||||
|
||||
it "emits an info notification ::addInfo has been called", ->
|
||||
manager.addInfo('Something!', icon: 'someIcon')
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'info'
|
||||
|
||||
it "emits a success notification ::addSuccess has been called", ->
|
||||
manager.addSuccess('Something!', icon: 'someIcon')
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'success'
|
69
spec/notification-manager-spec.js
Normal file
69
spec/notification-manager-spec.js
Normal file
@ -0,0 +1,69 @@
|
||||
const NotificationManager = require('../src/notification-manager')
|
||||
|
||||
describe('NotificationManager', () => {
|
||||
let manager
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new NotificationManager()
|
||||
})
|
||||
|
||||
describe('the atom global', () =>
|
||||
it('has a notifications instance', () => {
|
||||
expect(atom.notifications instanceof NotificationManager).toBe(true)
|
||||
})
|
||||
)
|
||||
|
||||
describe('adding events', () => {
|
||||
let addSpy
|
||||
|
||||
beforeEach(() => {
|
||||
addSpy = jasmine.createSpy()
|
||||
manager.onDidAddNotification(addSpy)
|
||||
})
|
||||
|
||||
it('emits an event when a notification has been added', () => {
|
||||
manager.add('error', 'Some error!', {icon: 'someIcon'})
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
|
||||
const notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('error')
|
||||
expect(notification.getMessage()).toBe('Some error!')
|
||||
expect(notification.getIcon()).toBe('someIcon')
|
||||
})
|
||||
|
||||
it('emits a fatal error when ::addFatalError has been called', () => {
|
||||
manager.addFatalError('Some error!', {icon: 'someIcon'})
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
const notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('fatal')
|
||||
})
|
||||
|
||||
it('emits an error when ::addError has been called', () => {
|
||||
manager.addError('Some error!', {icon: 'someIcon'})
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
const notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('error')
|
||||
})
|
||||
|
||||
it('emits a warning notification when ::addWarning has been called', () => {
|
||||
manager.addWarning('Something!', {icon: 'someIcon'})
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
const notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('warning')
|
||||
})
|
||||
|
||||
it('emits an info notification when ::addInfo has been called', () => {
|
||||
manager.addInfo('Something!', {icon: 'someIcon'})
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
const notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('info')
|
||||
})
|
||||
|
||||
it('emits a success notification when ::addSuccess has been called', () => {
|
||||
manager.addSuccess('Something!', {icon: 'someIcon'})
|
||||
expect(addSpy).toHaveBeenCalled()
|
||||
const notification = addSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('success')
|
||||
})
|
||||
})
|
||||
})
|
@ -1,60 +0,0 @@
|
||||
Notification = require '../src/notification'
|
||||
|
||||
describe "Notification", ->
|
||||
[notification] = []
|
||||
|
||||
it "throws an error when created with a non-string message", ->
|
||||
expect(-> new Notification('error', null)).toThrow()
|
||||
expect(-> new Notification('error', 3)).toThrow()
|
||||
expect(-> new Notification('error', {})).toThrow()
|
||||
expect(-> new Notification('error', false)).toThrow()
|
||||
expect(-> new Notification('error', [])).toThrow()
|
||||
|
||||
it "throws an error when created with non-object options", ->
|
||||
expect(-> new Notification('error', 'message', 'foo')).toThrow()
|
||||
expect(-> new Notification('error', 'message', 3)).toThrow()
|
||||
expect(-> new Notification('error', 'message', false)).toThrow()
|
||||
expect(-> new Notification('error', 'message', [])).toThrow()
|
||||
|
||||
describe "::getTimestamp()", ->
|
||||
it "returns a Date object", ->
|
||||
notification = new Notification('error', 'message!')
|
||||
expect(notification.getTimestamp() instanceof Date).toBe true
|
||||
|
||||
describe "::getIcon()", ->
|
||||
it "returns a default when no icon specified", ->
|
||||
notification = new Notification('error', 'message!')
|
||||
expect(notification.getIcon()).toBe 'flame'
|
||||
|
||||
it "returns the icon specified", ->
|
||||
notification = new Notification('error', 'message!', icon: 'my-icon')
|
||||
expect(notification.getIcon()).toBe 'my-icon'
|
||||
|
||||
describe "dismissing notifications", ->
|
||||
describe "when the notfication is dismissable", ->
|
||||
it "calls a callback when the notification is dismissed", ->
|
||||
dismissedSpy = jasmine.createSpy()
|
||||
notification = new Notification('error', 'message', dismissable: true)
|
||||
notification.onDidDismiss dismissedSpy
|
||||
|
||||
expect(notification.isDismissable()).toBe true
|
||||
expect(notification.isDismissed()).toBe false
|
||||
|
||||
notification.dismiss()
|
||||
|
||||
expect(dismissedSpy).toHaveBeenCalled()
|
||||
expect(notification.isDismissed()).toBe true
|
||||
|
||||
describe "when the notfication is not dismissable", ->
|
||||
it "does nothing when ::dismiss() is called", ->
|
||||
dismissedSpy = jasmine.createSpy()
|
||||
notification = new Notification('error', 'message')
|
||||
notification.onDidDismiss dismissedSpy
|
||||
|
||||
expect(notification.isDismissable()).toBe false
|
||||
expect(notification.isDismissed()).toBe true
|
||||
|
||||
notification.dismiss()
|
||||
|
||||
expect(dismissedSpy).not.toHaveBeenCalled()
|
||||
expect(notification.isDismissed()).toBe true
|
71
spec/notification-spec.js
Normal file
71
spec/notification-spec.js
Normal file
@ -0,0 +1,71 @@
|
||||
const Notification = require('../src/notification')
|
||||
|
||||
describe('Notification', () => {
|
||||
it('throws an error when created with a non-string message', () => {
|
||||
expect(() => new Notification('error', null)).toThrow()
|
||||
expect(() => new Notification('error', 3)).toThrow()
|
||||
expect(() => new Notification('error', {})).toThrow()
|
||||
expect(() => new Notification('error', false)).toThrow()
|
||||
expect(() => new Notification('error', [])).toThrow()
|
||||
})
|
||||
|
||||
it('throws an error when created with non-object options', () => {
|
||||
expect(() => new Notification('error', 'message', 'foo')).toThrow()
|
||||
expect(() => new Notification('error', 'message', 3)).toThrow()
|
||||
expect(() => new Notification('error', 'message', false)).toThrow()
|
||||
expect(() => new Notification('error', 'message', [])).toThrow()
|
||||
})
|
||||
|
||||
describe('::getTimestamp()', () =>
|
||||
it('returns a Date object', () => {
|
||||
const notification = new Notification('error', 'message!')
|
||||
expect(notification.getTimestamp() instanceof Date).toBe(true)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::getIcon()', () => {
|
||||
it('returns a default when no icon specified', () => {
|
||||
const notification = new Notification('error', 'message!')
|
||||
expect(notification.getIcon()).toBe('flame')
|
||||
})
|
||||
|
||||
it('returns the icon specified', () => {
|
||||
const notification = new Notification('error', 'message!', {icon: 'my-icon'})
|
||||
expect(notification.getIcon()).toBe('my-icon')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dismissing notifications', () => {
|
||||
describe('when the notfication is dismissable', () =>
|
||||
it('calls a callback when the notification is dismissed', () => {
|
||||
const dismissedSpy = jasmine.createSpy()
|
||||
const notification = new Notification('error', 'message', {dismissable: true})
|
||||
notification.onDidDismiss(dismissedSpy)
|
||||
|
||||
expect(notification.isDismissable()).toBe(true)
|
||||
expect(notification.isDismissed()).toBe(false)
|
||||
|
||||
notification.dismiss()
|
||||
|
||||
expect(dismissedSpy).toHaveBeenCalled()
|
||||
expect(notification.isDismissed()).toBe(true)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the notfication is not dismissable', () =>
|
||||
it('does nothing when ::dismiss() is called', () => {
|
||||
const dismissedSpy = jasmine.createSpy()
|
||||
const notification = new Notification('error', 'message')
|
||||
notification.onDidDismiss(dismissedSpy)
|
||||
|
||||
expect(notification.isDismissable()).toBe(false)
|
||||
expect(notification.isDismissed()).toBe(true)
|
||||
|
||||
notification.dismiss()
|
||||
|
||||
expect(dismissedSpy).not.toHaveBeenCalled()
|
||||
expect(notification.isDismissed()).toBe(true)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
File diff suppressed because it is too large
Load Diff
1354
spec/package-manager-spec.js
Normal file
1354
spec/package-manager-spec.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -138,7 +138,8 @@ describe "Package", ->
|
||||
jasmine.attachToDOM(editorElement)
|
||||
|
||||
afterEach ->
|
||||
theme.deactivate() if theme?
|
||||
waitsForPromise ->
|
||||
Promise.resolve(theme.deactivate()) if theme?
|
||||
|
||||
describe "when the theme contains a single style file", ->
|
||||
it "loads and applies css", ->
|
||||
@ -200,8 +201,10 @@ describe "Package", ->
|
||||
|
||||
it "deactivated event fires on .deactivate()", ->
|
||||
theme.onDidDeactivate spy = jasmine.createSpy()
|
||||
theme.deactivate()
|
||||
expect(spy).toHaveBeenCalled()
|
||||
waitsForPromise ->
|
||||
Promise.resolve(theme.deactivate())
|
||||
runs ->
|
||||
expect(spy).toHaveBeenCalled()
|
||||
|
||||
describe ".loadMetadata()", ->
|
||||
[packagePath, metadata] = []
|
||||
|
@ -172,7 +172,7 @@ describe "PaneContainerElement", ->
|
||||
lowerPane = leftPane.splitDown()
|
||||
expectPaneScale [lowerPane, 1], [leftPane, 1], [leftPane.getParent(), 0.5]
|
||||
|
||||
# dynamically close pane, the pane's flexscale will recorver to origin value
|
||||
# dynamically close pane, the pane's flexscale will recover to origin value
|
||||
waitsForPromise -> lowerPane.close()
|
||||
runs -> expectPaneScale [leftPane, 0.5], [rightPane, 1.5]
|
||||
|
||||
|
@ -1,409 +0,0 @@
|
||||
PaneContainer = require '../src/pane-container'
|
||||
Pane = require '../src/pane'
|
||||
|
||||
describe "PaneContainer", ->
|
||||
[confirm, params] = []
|
||||
|
||||
beforeEach ->
|
||||
confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0)
|
||||
params = {
|
||||
location: 'center',
|
||||
config: atom.config,
|
||||
deserializerManager: atom.deserializers
|
||||
applicationDelegate: atom.applicationDelegate,
|
||||
viewRegistry: atom.views
|
||||
}
|
||||
|
||||
describe "serialization", ->
|
||||
[containerA, pane1A, pane2A, pane3A] = []
|
||||
|
||||
beforeEach ->
|
||||
# This is a dummy item to prevent panes from being empty on deserialization
|
||||
class Item
|
||||
atom.deserializers.add(this)
|
||||
@deserialize: -> new this
|
||||
serialize: -> deserializer: 'Item'
|
||||
|
||||
containerA = new PaneContainer(params)
|
||||
pane1A = containerA.getActivePane()
|
||||
pane1A.addItem(new Item)
|
||||
pane2A = pane1A.splitRight(items: [new Item])
|
||||
pane3A = pane2A.splitDown(items: [new Item])
|
||||
pane3A.focus()
|
||||
|
||||
it "preserves the focused pane across serialization", ->
|
||||
expect(pane3A.focused).toBe true
|
||||
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(containerA.serialize(), atom.deserializers)
|
||||
[pane1B, pane2B, pane3B] = containerB.getPanes()
|
||||
expect(pane3B.focused).toBe true
|
||||
|
||||
it "preserves the active pane across serialization, independent of focus", ->
|
||||
pane3A.activate()
|
||||
expect(containerA.getActivePane()).toBe pane3A
|
||||
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(containerA.serialize(), atom.deserializers)
|
||||
[pane1B, pane2B, pane3B] = containerB.getPanes()
|
||||
expect(containerB.getActivePane()).toBe pane3B
|
||||
|
||||
it "makes the first pane active if no pane exists for the activePaneId", ->
|
||||
pane3A.activate()
|
||||
state = containerA.serialize()
|
||||
state.activePaneId = -22
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
expect(containerB.getActivePane()).toBe containerB.getPanes()[0]
|
||||
|
||||
describe "if there are empty panes after deserialization", ->
|
||||
beforeEach ->
|
||||
pane3A.getItems()[0].serialize = -> deserializer: 'Bogus'
|
||||
|
||||
describe "if the 'core.destroyEmptyPanes' config option is false (the default)", ->
|
||||
it "leaves the empty panes intact", ->
|
||||
state = containerA.serialize()
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
[leftPane, column] = containerB.getRoot().getChildren()
|
||||
[topPane, bottomPane] = column.getChildren()
|
||||
|
||||
expect(leftPane.getItems().length).toBe 1
|
||||
expect(topPane.getItems().length).toBe 1
|
||||
expect(bottomPane.getItems().length).toBe 0
|
||||
|
||||
describe "if the 'core.destroyEmptyPanes' config option is true", ->
|
||||
it "removes empty panes on deserialization", ->
|
||||
atom.config.set('core.destroyEmptyPanes', true)
|
||||
|
||||
state = containerA.serialize()
|
||||
containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
[leftPane, rightPane] = containerB.getRoot().getChildren()
|
||||
|
||||
expect(leftPane.getItems().length).toBe 1
|
||||
expect(rightPane.getItems().length).toBe 1
|
||||
|
||||
it "does not allow the root pane to be destroyed", ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().destroy()
|
||||
expect(container.getRoot()).toBeDefined()
|
||||
expect(container.getRoot().isDestroyed()).toBe false
|
||||
|
||||
describe "::getActivePane()", ->
|
||||
[container, pane1, pane2] = []
|
||||
|
||||
beforeEach ->
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
|
||||
it "returns the first pane if no pane has been made active", ->
|
||||
expect(container.getActivePane()).toBe pane1
|
||||
expect(pane1.isActive()).toBe true
|
||||
|
||||
it "returns the most pane on which ::activate() was most recently called", ->
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.activate()
|
||||
expect(container.getActivePane()).toBe pane2
|
||||
expect(pane1.isActive()).toBe false
|
||||
expect(pane2.isActive()).toBe true
|
||||
pane1.activate()
|
||||
expect(container.getActivePane()).toBe pane1
|
||||
expect(pane1.isActive()).toBe true
|
||||
expect(pane2.isActive()).toBe false
|
||||
|
||||
it "returns the next pane if the current active pane is destroyed", ->
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.activate()
|
||||
pane2.destroy()
|
||||
expect(container.getActivePane()).toBe pane1
|
||||
expect(pane1.isActive()).toBe true
|
||||
|
||||
describe "::onDidChangeActivePane()", ->
|
||||
[container, pane1, pane2, observed] = []
|
||||
|
||||
beforeEach ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([new Object, new Object])
|
||||
container.getRoot().splitRight(items: [new Object, new Object])
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidChangeActivePane (pane) -> observed.push(pane)
|
||||
|
||||
it "invokes observers when the active pane changes", ->
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual [pane1, pane2]
|
||||
|
||||
describe "::onDidChangeActivePaneItem()", ->
|
||||
[container, pane1, pane2, observed] = []
|
||||
|
||||
beforeEach ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([new Object, new Object])
|
||||
container.getRoot().splitRight(items: [new Object, new Object])
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidChangeActivePaneItem (item) -> observed.push(item)
|
||||
|
||||
it "invokes observers when the active item of the active pane changes", ->
|
||||
pane2.activateNextItem()
|
||||
pane2.activateNextItem()
|
||||
expect(observed).toEqual [pane2.itemAtIndex(1), pane2.itemAtIndex(0)]
|
||||
|
||||
it "invokes observers when the active pane changes", ->
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual [pane1.itemAtIndex(0), pane2.itemAtIndex(0)]
|
||||
|
||||
describe "::onDidStopChangingActivePaneItem()", ->
|
||||
[container, pane1, pane2, observed] = []
|
||||
|
||||
beforeEach ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([new Object, new Object])
|
||||
container.getRoot().splitRight(items: [new Object, new Object])
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidStopChangingActivePaneItem (item) -> observed.push(item)
|
||||
|
||||
it "invokes observers once when the active item of the active pane changes", ->
|
||||
pane2.activateNextItem()
|
||||
pane2.activateNextItem()
|
||||
expect(observed).toEqual []
|
||||
advanceClock 100
|
||||
expect(observed).toEqual [pane2.itemAtIndex(0)]
|
||||
|
||||
it "invokes observers once when the active pane changes", ->
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual []
|
||||
advanceClock 100
|
||||
expect(observed).toEqual [pane2.itemAtIndex(0)]
|
||||
|
||||
describe "::onDidActivatePane", ->
|
||||
it "invokes observers when a pane is activated (even if it was already active)", ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
activatedPanes = []
|
||||
container.onDidActivatePane (pane) -> activatedPanes.push(pane)
|
||||
|
||||
pane1.activate()
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
pane2.activate()
|
||||
expect(activatedPanes).toEqual([pane1, pane1, pane2, pane2])
|
||||
|
||||
describe "::observePanes()", ->
|
||||
it "invokes observers with all current and future panes", ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.observePanes (pane) -> observed.push(pane)
|
||||
|
||||
pane3 = pane2.splitDown()
|
||||
pane4 = pane2.splitRight()
|
||||
|
||||
expect(observed).toEqual [pane1, pane2, pane3, pane4]
|
||||
|
||||
describe "::observePaneItems()", ->
|
||||
it "invokes observers with all current and future pane items", ->
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([new Object, new Object])
|
||||
container.getRoot().splitRight(items: [new Object])
|
||||
[pane1, pane2] = container.getPanes()
|
||||
observed = []
|
||||
container.observePaneItems (pane) -> observed.push(pane)
|
||||
|
||||
pane3 = pane2.splitDown(items: [new Object])
|
||||
pane3.addItems([new Object, new Object])
|
||||
|
||||
expect(observed).toEqual container.getPaneItems()
|
||||
|
||||
describe "::confirmClose()", ->
|
||||
[container, pane1, pane2] = []
|
||||
|
||||
beforeEach ->
|
||||
class TestItem
|
||||
shouldPromptToSave: -> true
|
||||
getURI: -> 'test'
|
||||
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
[pane1, pane2] = container.getPanes()
|
||||
pane1.addItem(new TestItem)
|
||||
pane2.addItem(new TestItem)
|
||||
|
||||
it "returns true if the user saves all modified files when prompted", ->
|
||||
confirm.andReturn(0)
|
||||
waitsForPromise ->
|
||||
container.confirmClose().then (saved) ->
|
||||
expect(confirm).toHaveBeenCalled()
|
||||
expect(saved).toBeTruthy()
|
||||
|
||||
it "returns false if the user cancels saving any modified file", ->
|
||||
confirm.andReturn(1)
|
||||
waitsForPromise ->
|
||||
container.confirmClose().then (saved) ->
|
||||
expect(confirm).toHaveBeenCalled()
|
||||
expect(saved).toBeFalsy()
|
||||
|
||||
describe "::onDidAddPane(callback)", ->
|
||||
it "invokes the given callback when panes are added", ->
|
||||
container = new PaneContainer(params)
|
||||
events = []
|
||||
container.onDidAddPane (event) ->
|
||||
expect(event.pane in container.getPanes()).toBe true
|
||||
events.push(event)
|
||||
|
||||
pane1 = container.getActivePane()
|
||||
pane2 = pane1.splitRight()
|
||||
pane3 = pane2.splitDown()
|
||||
|
||||
expect(events).toEqual [{pane: pane2}, {pane: pane3}]
|
||||
|
||||
describe "::onWillDestroyPane(callback)", ->
|
||||
it "invokes the given callback before panes or their items are destroyed", ->
|
||||
class TestItem
|
||||
constructor: -> @_isDestroyed = false
|
||||
destroy: -> @_isDestroyed = true
|
||||
isDestroyed: -> @_isDestroyed
|
||||
|
||||
container = new PaneContainer(params)
|
||||
events = []
|
||||
container.onWillDestroyPane (event) ->
|
||||
itemsDestroyed = (item.isDestroyed() for item in event.pane.getItems())
|
||||
events.push([event, itemsDestroyed: itemsDestroyed])
|
||||
|
||||
pane1 = container.getActivePane()
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.addItem(new TestItem)
|
||||
|
||||
pane2.destroy()
|
||||
|
||||
expect(events).toEqual [[{pane: pane2}, itemsDestroyed: [false]]]
|
||||
|
||||
describe "::onDidDestroyPane(callback)", ->
|
||||
it "invokes the given callback when panes are destroyed", ->
|
||||
container = new PaneContainer(params)
|
||||
events = []
|
||||
container.onDidDestroyPane (event) ->
|
||||
expect(event.pane in container.getPanes()).toBe false
|
||||
events.push(event)
|
||||
|
||||
pane1 = container.getActivePane()
|
||||
pane2 = pane1.splitRight()
|
||||
pane3 = pane2.splitDown()
|
||||
|
||||
pane2.destroy()
|
||||
pane3.destroy()
|
||||
|
||||
expect(events).toEqual [{pane: pane2}, {pane: pane3}]
|
||||
|
||||
it "invokes the given callback when the container is destroyed", ->
|
||||
container = new PaneContainer(params)
|
||||
events = []
|
||||
container.onDidDestroyPane (event) ->
|
||||
expect(event.pane in container.getPanes()).toBe false
|
||||
events.push(event)
|
||||
|
||||
pane1 = container.getActivePane()
|
||||
pane2 = pane1.splitRight()
|
||||
pane3 = pane2.splitDown()
|
||||
|
||||
container.destroy()
|
||||
|
||||
expect(events).toEqual [{pane: pane1}, {pane: pane2}, {pane: pane3}]
|
||||
|
||||
describe "::onWillDestroyPaneItem() and ::onDidDestroyPaneItem", ->
|
||||
it "invokes the given callbacks when an item will be destroyed on any pane", ->
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
item1 = new Object
|
||||
item2 = new Object
|
||||
item3 = new Object
|
||||
|
||||
pane1.addItem(item1)
|
||||
events = []
|
||||
container.onWillDestroyPaneItem (event) -> events.push(['will', event])
|
||||
container.onDidDestroyPaneItem (event) -> events.push(['did', event])
|
||||
pane2 = pane1.splitRight(items: [item2, item3])
|
||||
|
||||
pane1.destroyItem(item1)
|
||||
pane2.destroyItem(item3)
|
||||
pane2.destroyItem(item2)
|
||||
|
||||
expect(events).toEqual [
|
||||
['will', {item: item1, pane: pane1, index: 0}]
|
||||
['did', {item: item1, pane: pane1, index: 0}]
|
||||
['will', {item: item3, pane: pane2, index: 1}]
|
||||
['did', {item: item3, pane: pane2, index: 1}]
|
||||
['will', {item: item2, pane: pane2, index: 0}]
|
||||
['did', {item: item2, pane: pane2, index: 0}]
|
||||
]
|
||||
|
||||
describe "::saveAll()", ->
|
||||
it "saves all modified pane items", ->
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
pane2 = pane1.splitRight()
|
||||
|
||||
item1 = {
|
||||
saved: false
|
||||
getURI: -> ''
|
||||
isModified: -> true,
|
||||
save: -> @saved = true
|
||||
}
|
||||
item2 = {
|
||||
saved: false
|
||||
getURI: -> ''
|
||||
isModified: -> false,
|
||||
save: -> @saved = true
|
||||
}
|
||||
item3 = {
|
||||
saved: false
|
||||
getURI: -> ''
|
||||
isModified: -> true,
|
||||
save: -> @saved = true
|
||||
}
|
||||
|
||||
pane1.addItem(item1)
|
||||
pane1.addItem(item2)
|
||||
pane1.addItem(item3)
|
||||
|
||||
container.saveAll()
|
||||
|
||||
expect(item1.saved).toBe true
|
||||
expect(item2.saved).toBe false
|
||||
expect(item3.saved).toBe true
|
||||
|
||||
describe "::moveActiveItemToPane(destPane) and ::copyActiveItemToPane(destPane)", ->
|
||||
[container, pane1, pane2, item1] = []
|
||||
|
||||
beforeEach ->
|
||||
class TestItem
|
||||
constructor: (id) -> @id = id
|
||||
copy: -> new TestItem(@id)
|
||||
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
item1 = new TestItem('1')
|
||||
pane2 = pane1.splitRight(items: [item1])
|
||||
|
||||
describe "::::moveActiveItemToPane(destPane)", ->
|
||||
it "moves active item to given pane and focuses it", ->
|
||||
container.moveActiveItemToPane(pane1)
|
||||
expect(pane1.getActiveItem()).toBe item1
|
||||
|
||||
describe "::::copyActiveItemToPane(destPane)", ->
|
||||
it "copies active item to given pane and focuses it", ->
|
||||
container.copyActiveItemToPane(pane1)
|
||||
expect(container.paneForItem(item1)).toBe pane2
|
||||
expect(pane1.getActiveItem().id).toBe item1.id
|
472
spec/pane-container-spec.js
Normal file
472
spec/pane-container-spec.js
Normal file
@ -0,0 +1,472 @@
|
||||
const PaneContainer = require('../src/pane-container')
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
|
||||
|
||||
describe('PaneContainer', () => {
|
||||
let confirm, params
|
||||
|
||||
beforeEach(() => {
|
||||
confirm = spyOn(atom.applicationDelegate, 'confirm').andReturn(0)
|
||||
params = {
|
||||
location: 'center',
|
||||
config: atom.config,
|
||||
deserializerManager: atom.deserializers,
|
||||
applicationDelegate: atom.applicationDelegate,
|
||||
viewRegistry: atom.views
|
||||
}
|
||||
})
|
||||
|
||||
describe('serialization', () => {
|
||||
let containerA, pane1A, pane2A, pane3A
|
||||
|
||||
beforeEach(() => {
|
||||
// This is a dummy item to prevent panes from being empty on deserialization
|
||||
class Item {
|
||||
static deserialize () { return new (this)() }
|
||||
serialize () { return {deserializer: 'Item'} }
|
||||
}
|
||||
atom.deserializers.add(Item)
|
||||
|
||||
containerA = new PaneContainer(params)
|
||||
pane1A = containerA.getActivePane()
|
||||
pane1A.addItem(new Item())
|
||||
pane2A = pane1A.splitRight({items: [new Item()]})
|
||||
pane3A = pane2A.splitDown({items: [new Item()]})
|
||||
pane3A.focus()
|
||||
})
|
||||
|
||||
it('preserves the focused pane across serialization', () => {
|
||||
expect(pane3A.focused).toBe(true)
|
||||
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(containerA.serialize(), atom.deserializers)
|
||||
const pane3B = containerB.getPanes()[2]
|
||||
expect(pane3B.focused).toBe(true)
|
||||
})
|
||||
|
||||
it('preserves the active pane across serialization, independent of focus', () => {
|
||||
pane3A.activate()
|
||||
expect(containerA.getActivePane()).toBe(pane3A)
|
||||
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(containerA.serialize(), atom.deserializers)
|
||||
const pane3B = containerB.getPanes()[2]
|
||||
expect(containerB.getActivePane()).toBe(pane3B)
|
||||
})
|
||||
|
||||
it('makes the first pane active if no pane exists for the activePaneId', () => {
|
||||
pane3A.activate()
|
||||
const state = containerA.serialize()
|
||||
state.activePaneId = -22
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
expect(containerB.getActivePane()).toBe(containerB.getPanes()[0])
|
||||
})
|
||||
|
||||
describe('if there are empty panes after deserialization', () => {
|
||||
beforeEach(() => {
|
||||
pane3A.getItems()[0].serialize = () => ({deserializer: 'Bogus'})
|
||||
})
|
||||
|
||||
describe("if the 'core.destroyEmptyPanes' config option is false (the default)", () =>
|
||||
it('leaves the empty panes intact', () => {
|
||||
const state = containerA.serialize()
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
const [leftPane, column] = containerB.getRoot().getChildren()
|
||||
const [topPane, bottomPane] = column.getChildren()
|
||||
|
||||
expect(leftPane.getItems().length).toBe(1)
|
||||
expect(topPane.getItems().length).toBe(1)
|
||||
expect(bottomPane.getItems().length).toBe(0)
|
||||
})
|
||||
)
|
||||
|
||||
describe("if the 'core.destroyEmptyPanes' config option is true", () =>
|
||||
it('removes empty panes on deserialization', () => {
|
||||
atom.config.set('core.destroyEmptyPanes', true)
|
||||
|
||||
const state = containerA.serialize()
|
||||
const containerB = new PaneContainer(params)
|
||||
containerB.deserialize(state, atom.deserializers)
|
||||
const [leftPane, rightPane] = containerB.getRoot().getChildren()
|
||||
|
||||
expect(leftPane.getItems().length).toBe(1)
|
||||
expect(rightPane.getItems().length).toBe(1)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not allow the root pane to be destroyed', () => {
|
||||
const container = new PaneContainer(params)
|
||||
container.getRoot().destroy()
|
||||
expect(container.getRoot()).toBeDefined()
|
||||
expect(container.getRoot().isDestroyed()).toBe(false)
|
||||
})
|
||||
|
||||
describe('::getActivePane()', () => {
|
||||
let container, pane1, pane2
|
||||
|
||||
beforeEach(() => {
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
})
|
||||
|
||||
it('returns the first pane if no pane has been made active', () => {
|
||||
expect(container.getActivePane()).toBe(pane1)
|
||||
expect(pane1.isActive()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns the most pane on which ::activate() was most recently called', () => {
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.activate()
|
||||
expect(container.getActivePane()).toBe(pane2)
|
||||
expect(pane1.isActive()).toBe(false)
|
||||
expect(pane2.isActive()).toBe(true)
|
||||
pane1.activate()
|
||||
expect(container.getActivePane()).toBe(pane1)
|
||||
expect(pane1.isActive()).toBe(true)
|
||||
expect(pane2.isActive()).toBe(false)
|
||||
})
|
||||
|
||||
it('returns the next pane if the current active pane is destroyed', () => {
|
||||
pane2 = pane1.splitRight()
|
||||
pane2.activate()
|
||||
pane2.destroy()
|
||||
expect(container.getActivePane()).toBe(pane1)
|
||||
expect(pane1.isActive()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidChangeActivePane()', () => {
|
||||
let container, pane1, pane2, observed
|
||||
|
||||
beforeEach(() => {
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([{}, {}])
|
||||
container.getRoot().splitRight({items: [{}, {}]});
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidChangeActivePane(pane => observed.push(pane))
|
||||
})
|
||||
|
||||
it('invokes observers when the active pane changes', () => {
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual([pane1, pane2])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidChangeActivePaneItem()', () => {
|
||||
let container, pane1, pane2, observed
|
||||
|
||||
beforeEach(() => {
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([{}, {}])
|
||||
container.getRoot().splitRight({items: [{}, {}]});
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidChangeActivePaneItem(item => observed.push(item))
|
||||
})
|
||||
|
||||
it('invokes observers when the active item of the active pane changes', () => {
|
||||
pane2.activateNextItem()
|
||||
pane2.activateNextItem()
|
||||
expect(observed).toEqual([pane2.itemAtIndex(1), pane2.itemAtIndex(0)])
|
||||
})
|
||||
|
||||
it('invokes observers when the active pane changes', () => {
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual([pane1.itemAtIndex(0), pane2.itemAtIndex(0)])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidStopChangingActivePaneItem()', () => {
|
||||
let container, pane1, pane2, observed
|
||||
|
||||
beforeEach(() => {
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().addItems([{}, {}])
|
||||
container.getRoot().splitRight({items: [{}, {}]});
|
||||
[pane1, pane2] = container.getPanes()
|
||||
|
||||
observed = []
|
||||
container.onDidStopChangingActivePaneItem(item => observed.push(item))
|
||||
})
|
||||
|
||||
it('invokes observers once when the active item of the active pane changes', () => {
|
||||
pane2.activateNextItem()
|
||||
pane2.activateNextItem()
|
||||
expect(observed).toEqual([])
|
||||
advanceClock(100)
|
||||
expect(observed).toEqual([pane2.itemAtIndex(0)])
|
||||
})
|
||||
|
||||
it('invokes observers once when the active pane changes', () => {
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
expect(observed).toEqual([])
|
||||
advanceClock(100)
|
||||
expect(observed).toEqual([pane2.itemAtIndex(0)])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidActivatePane', () => {
|
||||
it('invokes observers when a pane is activated (even if it was already active)', () => {
|
||||
const container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
const [pane1, pane2] = container.getPanes()
|
||||
|
||||
const activatedPanes = []
|
||||
container.onDidActivatePane(pane => activatedPanes.push(pane))
|
||||
|
||||
pane1.activate()
|
||||
pane1.activate()
|
||||
pane2.activate()
|
||||
pane2.activate()
|
||||
expect(activatedPanes).toEqual([pane1, pane1, pane2, pane2])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::observePanes()', () => {
|
||||
it('invokes observers with all current and future panes', () => {
|
||||
const container = new PaneContainer(params)
|
||||
container.getRoot().splitRight()
|
||||
const [pane1, pane2] = container.getPanes()
|
||||
|
||||
const observed = []
|
||||
container.observePanes(pane => observed.push(pane))
|
||||
|
||||
const pane3 = pane2.splitDown()
|
||||
const pane4 = pane2.splitRight()
|
||||
|
||||
expect(observed).toEqual([pane1, pane2, pane3, pane4])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::observePaneItems()', () =>
|
||||
it('invokes observers with all current and future pane items', () => {
|
||||
const container = new PaneContainer(params)
|
||||
container.getRoot().addItems([{}, {}])
|
||||
container.getRoot().splitRight({items: [{}]})
|
||||
const pane2 = container.getPanes()[1]
|
||||
const observed = []
|
||||
container.observePaneItems(pane => observed.push(pane))
|
||||
|
||||
const pane3 = pane2.splitDown({items: [{}]})
|
||||
pane3.addItems([{}, {}])
|
||||
|
||||
expect(observed).toEqual(container.getPaneItems())
|
||||
})
|
||||
)
|
||||
|
||||
describe('::confirmClose()', () => {
|
||||
let container, pane1, pane2
|
||||
|
||||
beforeEach(() => {
|
||||
class TestItem {
|
||||
shouldPromptToSave () { return true }
|
||||
getURI () { return 'test' }
|
||||
}
|
||||
|
||||
container = new PaneContainer(params)
|
||||
container.getRoot().splitRight();
|
||||
[pane1, pane2] = container.getPanes()
|
||||
pane1.addItem(new TestItem())
|
||||
pane2.addItem(new TestItem())
|
||||
})
|
||||
|
||||
it('returns true if the user saves all modified files when prompted', async () => {
|
||||
confirm.andReturn(0)
|
||||
const saved = await container.confirmClose()
|
||||
expect(confirm).toHaveBeenCalled()
|
||||
expect(saved).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns false if the user cancels saving any modified file', async () => {
|
||||
confirm.andReturn(1)
|
||||
const saved = await container.confirmClose()
|
||||
expect(confirm).toHaveBeenCalled()
|
||||
expect(saved).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidAddPane(callback)', () => {
|
||||
it('invokes the given callback when panes are added', () => {
|
||||
const container = new PaneContainer(params)
|
||||
const events = []
|
||||
container.onDidAddPane((event) => {
|
||||
expect(container.getPanes().includes(event.pane)).toBe(true)
|
||||
events.push(event)
|
||||
})
|
||||
|
||||
const pane1 = container.getActivePane()
|
||||
const pane2 = pane1.splitRight()
|
||||
const pane3 = pane2.splitDown()
|
||||
|
||||
expect(events).toEqual([{pane: pane2}, {pane: pane3}])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onWillDestroyPane(callback)', () => {
|
||||
it('invokes the given callback before panes or their items are destroyed', () => {
|
||||
class TestItem {
|
||||
constructor () { this._isDestroyed = false }
|
||||
destroy () { this._isDestroyed = true }
|
||||
isDestroyed () { return this._isDestroyed }
|
||||
}
|
||||
|
||||
const container = new PaneContainer(params)
|
||||
const events = []
|
||||
container.onWillDestroyPane((event) => {
|
||||
const itemsDestroyed = event.pane.getItems().map((item) => item.isDestroyed())
|
||||
events.push([event, {itemsDestroyed}])
|
||||
})
|
||||
|
||||
const pane1 = container.getActivePane()
|
||||
const pane2 = pane1.splitRight()
|
||||
pane2.addItem(new TestItem())
|
||||
|
||||
pane2.destroy()
|
||||
|
||||
expect(events).toEqual([[{pane: pane2}, {itemsDestroyed: [false]}]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidDestroyPane(callback)', () => {
|
||||
it('invokes the given callback when panes are destroyed', () => {
|
||||
const container = new PaneContainer(params)
|
||||
const events = []
|
||||
container.onDidDestroyPane((event) => {
|
||||
expect(container.getPanes().includes(event.pane)).toBe(false)
|
||||
events.push(event)
|
||||
})
|
||||
|
||||
const pane1 = container.getActivePane()
|
||||
const pane2 = pane1.splitRight()
|
||||
const pane3 = pane2.splitDown()
|
||||
|
||||
pane2.destroy()
|
||||
pane3.destroy()
|
||||
|
||||
expect(events).toEqual([{pane: pane2}, {pane: pane3}])
|
||||
})
|
||||
|
||||
it('invokes the given callback when the container is destroyed', () => {
|
||||
const container = new PaneContainer(params)
|
||||
const events = []
|
||||
container.onDidDestroyPane((event) => {
|
||||
expect(container.getPanes().includes(event.pane)).toBe(false)
|
||||
events.push(event)
|
||||
})
|
||||
|
||||
const pane1 = container.getActivePane()
|
||||
const pane2 = pane1.splitRight()
|
||||
const pane3 = pane2.splitDown()
|
||||
|
||||
container.destroy()
|
||||
|
||||
expect(events).toEqual([{pane: pane1}, {pane: pane2}, {pane: pane3}])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onWillDestroyPaneItem() and ::onDidDestroyPaneItem', () => {
|
||||
it('invokes the given callbacks when an item will be destroyed on any pane', async () => {
|
||||
const container = new PaneContainer(params)
|
||||
const pane1 = container.getRoot()
|
||||
const item1 = {}
|
||||
const item2 = {}
|
||||
const item3 = {}
|
||||
|
||||
pane1.addItem(item1)
|
||||
const events = []
|
||||
container.onWillDestroyPaneItem(event => events.push(['will', event]))
|
||||
container.onDidDestroyPaneItem(event => events.push(['did', event]))
|
||||
const pane2 = pane1.splitRight({items: [item2, item3]})
|
||||
|
||||
await pane1.destroyItem(item1)
|
||||
await pane2.destroyItem(item3)
|
||||
await pane2.destroyItem(item2)
|
||||
|
||||
expect(events).toEqual([
|
||||
['will', {item: item1, pane: pane1, index: 0}],
|
||||
['did', {item: item1, pane: pane1, index: 0}],
|
||||
['will', {item: item3, pane: pane2, index: 1}],
|
||||
['did', {item: item3, pane: pane2, index: 1}],
|
||||
['will', {item: item2, pane: pane2, index: 0}],
|
||||
['did', {item: item2, pane: pane2, index: 0}]
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::saveAll()', () =>
|
||||
it('saves all modified pane items', async () => {
|
||||
const container = new PaneContainer(params)
|
||||
const pane1 = container.getRoot()
|
||||
pane1.splitRight()
|
||||
|
||||
const item1 = {
|
||||
saved: false,
|
||||
getURI () { return '' },
|
||||
isModified () { return true },
|
||||
save () { this.saved = true }
|
||||
}
|
||||
const item2 = {
|
||||
saved: false,
|
||||
getURI () { return '' },
|
||||
isModified () { return false },
|
||||
save () { this.saved = true }
|
||||
}
|
||||
const item3 = {
|
||||
saved: false,
|
||||
getURI () { return '' },
|
||||
isModified () { return true },
|
||||
save () { this.saved = true }
|
||||
}
|
||||
|
||||
pane1.addItem(item1)
|
||||
pane1.addItem(item2)
|
||||
pane1.addItem(item3)
|
||||
|
||||
container.saveAll()
|
||||
|
||||
expect(item1.saved).toBe(true)
|
||||
expect(item2.saved).toBe(false)
|
||||
expect(item3.saved).toBe(true)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::moveActiveItemToPane(destPane) and ::copyActiveItemToPane(destPane)', () => {
|
||||
let container, pane1, pane2, item1
|
||||
|
||||
beforeEach(() => {
|
||||
class TestItem {
|
||||
constructor (id) { this.id = id }
|
||||
copy () { return new TestItem(this.id) }
|
||||
}
|
||||
|
||||
container = new PaneContainer(params)
|
||||
pane1 = container.getRoot()
|
||||
item1 = new TestItem('1')
|
||||
pane2 = pane1.splitRight({items: [item1]})
|
||||
})
|
||||
|
||||
describe('::::moveActiveItemToPane(destPane)', () =>
|
||||
it('moves active item to given pane and focuses it', () => {
|
||||
container.moveActiveItemToPane(pane1)
|
||||
expect(pane1.getActiveItem()).toBe(item1)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::::copyActiveItemToPane(destPane)', () =>
|
||||
it('copies active item to given pane and focuses it', () => {
|
||||
container.copyActiveItemToPane(pane1)
|
||||
expect(container.paneForItem(item1)).toBe(pane2)
|
||||
expect(pane1.getActiveItem().id).toBe(item1.id)
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
@ -113,6 +113,53 @@ describe "PaneElement", ->
|
||||
expect(paneElement.dataset.activeItemPath).toBeUndefined()
|
||||
expect(paneElement.dataset.activeItemName).toBeUndefined()
|
||||
|
||||
describe "when the path of the item changes", ->
|
||||
[item1, item2] = []
|
||||
|
||||
beforeEach ->
|
||||
item1 = document.createElement('div')
|
||||
item1.path = '/foo/bar.txt'
|
||||
item1.changePathCallbacks = []
|
||||
item1.setPath = (path) ->
|
||||
@path = path
|
||||
callback() for callback in @changePathCallbacks
|
||||
return
|
||||
item1.getPath = -> @path
|
||||
item1.onDidChangePath = (callback) ->
|
||||
@changePathCallbacks.push callback
|
||||
return dispose: =>
|
||||
@changePathCallbacks = @changePathCallbacks.filter (f) -> f isnt callback
|
||||
|
||||
item2 = document.createElement('div')
|
||||
|
||||
pane.addItem(item1)
|
||||
pane.addItem(item2)
|
||||
|
||||
it "changes the file path and file name data attributes on the pane if the active item path is changed", ->
|
||||
|
||||
expect(paneElement.dataset.activeItemPath).toBe '/foo/bar.txt'
|
||||
expect(paneElement.dataset.activeItemName).toBe 'bar.txt'
|
||||
|
||||
item1.setPath "/foo/bar1.txt"
|
||||
|
||||
expect(paneElement.dataset.activeItemPath).toBe '/foo/bar1.txt'
|
||||
expect(paneElement.dataset.activeItemName).toBe 'bar1.txt'
|
||||
|
||||
pane.activateItem(item2)
|
||||
|
||||
expect(paneElement.dataset.activeItemPath).toBeUndefined()
|
||||
expect(paneElement.dataset.activeItemName).toBeUndefined()
|
||||
|
||||
item1.setPath "/foo/bar2.txt"
|
||||
|
||||
expect(paneElement.dataset.activeItemPath).toBeUndefined()
|
||||
expect(paneElement.dataset.activeItemName).toBeUndefined()
|
||||
|
||||
pane.activateItem(item1)
|
||||
|
||||
expect(paneElement.dataset.activeItemPath).toBe '/foo/bar2.txt'
|
||||
expect(paneElement.dataset.activeItemName).toBe 'bar2.txt'
|
||||
|
||||
describe "when an item is removed from the pane", ->
|
||||
describe "when the destroyed item is an element", ->
|
||||
it "removes the item from the itemViews div", ->
|
||||
|
@ -3,7 +3,7 @@ const {Emitter} = require('event-kit')
|
||||
const Grim = require('grim')
|
||||
const Pane = require('../src/pane')
|
||||
const PaneContainer = require('../src/pane-container')
|
||||
const {it, fit, ffit, fffit, beforeEach} = require('./async-spec-helpers')
|
||||
const {it, fit, ffit, fffit, beforeEach, timeoutPromise} = require('./async-spec-helpers')
|
||||
|
||||
describe('Pane', () => {
|
||||
let confirm, showSaveDialog, deserializerDisposable
|
||||
@ -491,16 +491,31 @@ describe('Pane', () => {
|
||||
expect(pane.getActiveItem()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('invokes ::onWillDestroyItem() observers before destroying the item', () => {
|
||||
it('invokes ::onWillDestroyItem() and PaneContainer::onWillDestroyPaneItem observers before destroying the item', async () => {
|
||||
jasmine.useRealClock()
|
||||
pane.container = new PaneContainer({config: atom.config, confirm})
|
||||
const events = []
|
||||
pane.onWillDestroyItem(function (event) {
|
||||
|
||||
pane.onWillDestroyItem(async (event) => {
|
||||
expect(item2.isDestroyed()).toBe(false)
|
||||
events.push(event)
|
||||
await timeoutPromise(50)
|
||||
expect(item2.isDestroyed()).toBe(false)
|
||||
events.push(['will-destroy-item', event])
|
||||
})
|
||||
|
||||
pane.destroyItem(item2)
|
||||
pane.container.onWillDestroyPaneItem(async (event) => {
|
||||
expect(item2.isDestroyed()).toBe(false)
|
||||
await timeoutPromise(50)
|
||||
expect(item2.isDestroyed()).toBe(false)
|
||||
events.push(['will-destroy-pane-item', event])
|
||||
})
|
||||
|
||||
await pane.destroyItem(item2)
|
||||
expect(item2.isDestroyed()).toBe(true)
|
||||
expect(events).toEqual([{item: item2, index: 1}])
|
||||
expect(events).toEqual([
|
||||
['will-destroy-item', {item: item2, index: 1}],
|
||||
['will-destroy-pane-item', {item: item2, index: 1, pane}]
|
||||
])
|
||||
})
|
||||
|
||||
it('invokes ::onWillRemoveItem() observers', () => {
|
||||
|
@ -71,7 +71,7 @@ describe('Panel', () => {
|
||||
expect(spy).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('initially renders panel created with visibile: false', () => {
|
||||
it('initially renders panel created with visible: false', () => {
|
||||
const panel = new Panel({visible: false, item: new TestPanelItem()}, atom.views)
|
||||
const element = panel.getElement()
|
||||
expect(element.style.display).toBe('none')
|
||||
@ -91,7 +91,7 @@ describe('Panel', () => {
|
||||
})
|
||||
|
||||
describe('when a class name is specified', () => {
|
||||
it('initially renders panel created with visibile: false', () => {
|
||||
it('initially renders panel created with visible: false', () => {
|
||||
const panel = new Panel({className: 'some classes', item: new TestPanelItem()}, atom.views)
|
||||
const element = panel.getElement()
|
||||
|
||||
|
@ -1,716 +0,0 @@
|
||||
temp = require('temp').track()
|
||||
TextBuffer = require('text-buffer')
|
||||
Project = require '../src/project'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
{Directory} = require 'pathwatcher'
|
||||
{stopAllWatchers} = require '../src/path-watcher'
|
||||
GitRepository = require '../src/git-repository'
|
||||
|
||||
describe "Project", ->
|
||||
beforeEach ->
|
||||
atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')])
|
||||
|
||||
# Wait for project's service consumers to be asynchronously added
|
||||
waits(1)
|
||||
|
||||
describe "serialization", ->
|
||||
deserializedProject = null
|
||||
|
||||
afterEach ->
|
||||
deserializedProject?.destroy()
|
||||
|
||||
it "does not deserialize paths to non directories", ->
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
state = atom.project.serialize()
|
||||
state.paths.push('/directory/that/does/not/exist')
|
||||
|
||||
waitsForPromise ->
|
||||
deserializedProject.deserialize(state, atom.deserializers)
|
||||
|
||||
runs ->
|
||||
expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths())
|
||||
|
||||
it "does not include unretained buffers in the serialized state", ->
|
||||
waitsForPromise ->
|
||||
atom.project.bufferForPath('a')
|
||||
|
||||
runs ->
|
||||
expect(atom.project.getBuffers().length).toBe 1
|
||||
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
|
||||
waitsForPromise ->
|
||||
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
|
||||
runs ->
|
||||
expect(deserializedProject.getBuffers().length).toBe 0
|
||||
|
||||
it "listens for destroyed events on deserialized buffers and removes them when they are destroyed", ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('a')
|
||||
|
||||
runs ->
|
||||
expect(atom.project.getBuffers().length).toBe 1
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
|
||||
waitsForPromise ->
|
||||
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
|
||||
runs ->
|
||||
expect(deserializedProject.getBuffers().length).toBe 1
|
||||
deserializedProject.getBuffers()[0].destroy()
|
||||
expect(deserializedProject.getBuffers().length).toBe 0
|
||||
|
||||
it "does not deserialize buffers when their path is a directory that exists", ->
|
||||
pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(pathToOpen)
|
||||
|
||||
runs ->
|
||||
expect(atom.project.getBuffers().length).toBe 1
|
||||
fs.mkdirSync(pathToOpen)
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
expect(deserializedProject.getBuffers().length).toBe 0
|
||||
|
||||
it "does not deserialize buffers when their path is inaccessible", ->
|
||||
return if process.platform is 'win32' # chmod not supported on win32
|
||||
pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
fs.writeFileSync(pathToOpen, '')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(pathToOpen)
|
||||
|
||||
runs ->
|
||||
expect(atom.project.getBuffers().length).toBe 1
|
||||
fs.chmodSync(pathToOpen, '000')
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
deserializedProject.deserialize(atom.project.serialize({isUnloading: false}))
|
||||
expect(deserializedProject.getBuffers().length).toBe 0
|
||||
|
||||
it "serializes marker layers and history only if Atom is quitting", ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('a')
|
||||
|
||||
notQuittingProject = null
|
||||
quittingProject = null
|
||||
bufferA = null
|
||||
layerA = null
|
||||
markerA = null
|
||||
|
||||
runs ->
|
||||
bufferA = atom.project.getBuffers()[0]
|
||||
layerA = bufferA.addMarkerLayer(persistent: true)
|
||||
markerA = layerA.markPosition([0, 3])
|
||||
bufferA.append('!')
|
||||
|
||||
waitsForPromise ->
|
||||
notQuittingProject?.destroy()
|
||||
notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
notQuittingProject.deserialize(atom.project.serialize({isUnloading: false})).then ->
|
||||
expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).toBeUndefined()
|
||||
expect(notQuittingProject.getBuffers()[0].undo()).toBe(false)
|
||||
|
||||
waitsForPromise ->
|
||||
quittingProject?.destroy()
|
||||
quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
quittingProject.deserialize(atom.project.serialize({isUnloading: true})).then ->
|
||||
expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id)?.getMarker(markerA.id)).not.toBeUndefined()
|
||||
expect(quittingProject.getBuffers()[0].undo()).toBe(true)
|
||||
|
||||
describe "when an editor is saved and the project has no path", ->
|
||||
it "sets the project's path to the saved file's parent directory", ->
|
||||
tempFile = temp.openSync().path
|
||||
atom.project.setPaths([])
|
||||
expect(atom.project.getPaths()[0]).toBeUndefined()
|
||||
editor = null
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open().then (o) -> editor = o
|
||||
|
||||
waitsForPromise ->
|
||||
editor.saveAs(tempFile)
|
||||
|
||||
runs ->
|
||||
expect(atom.project.getPaths()[0]).toBe path.dirname(tempFile)
|
||||
|
||||
describe "before and after saving a buffer", ->
|
||||
[buffer] = []
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then (o) ->
|
||||
buffer = o
|
||||
buffer.retain()
|
||||
|
||||
afterEach ->
|
||||
buffer.release()
|
||||
|
||||
it "emits save events on the main process", ->
|
||||
spyOn(atom.project.applicationDelegate, 'emitDidSavePath')
|
||||
spyOn(atom.project.applicationDelegate, 'emitWillSavePath')
|
||||
|
||||
waitsForPromise -> buffer.save()
|
||||
|
||||
runs ->
|
||||
expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1)
|
||||
expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath())
|
||||
expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1)
|
||||
expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath())
|
||||
|
||||
describe "when a watch error is thrown from the TextBuffer", ->
|
||||
editor = null
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(require.resolve('./fixtures/dir/a')).then (o) -> editor = o
|
||||
|
||||
it "creates a warning notification", ->
|
||||
atom.notifications.onDidAddNotification noteSpy = jasmine.createSpy()
|
||||
|
||||
error = new Error('SomeError')
|
||||
error.eventType = 'resurrect'
|
||||
editor.buffer.emitter.emit 'will-throw-watch-error',
|
||||
handle: jasmine.createSpy()
|
||||
error: error
|
||||
|
||||
expect(noteSpy).toHaveBeenCalled()
|
||||
|
||||
notification = noteSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe 'warning'
|
||||
expect(notification.getDetail()).toBe 'SomeError'
|
||||
expect(notification.getMessage()).toContain '`resurrect`'
|
||||
expect(notification.getMessage()).toContain path.join('fixtures', 'dir', 'a')
|
||||
|
||||
describe "when a custom repository-provider service is provided", ->
|
||||
[fakeRepositoryProvider, fakeRepository] = []
|
||||
|
||||
beforeEach ->
|
||||
fakeRepository = {destroy: -> null}
|
||||
fakeRepositoryProvider = {
|
||||
repositoryForDirectory: (directory) -> Promise.resolve(fakeRepository)
|
||||
repositoryForDirectorySync: (directory) -> fakeRepository
|
||||
}
|
||||
|
||||
it "uses it to create repositories for any directories that need one", ->
|
||||
projectPath = temp.mkdirSync('atom-project')
|
||||
atom.project.setPaths([projectPath])
|
||||
expect(atom.project.getRepositories()).toEqual [null]
|
||||
|
||||
atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider)
|
||||
waitsFor -> atom.project.repositoryProviders.length > 1
|
||||
runs -> atom.project.getRepositories()[0] is fakeRepository
|
||||
|
||||
it "does not create any new repositories if every directory has a repository", ->
|
||||
repositories = atom.project.getRepositories()
|
||||
expect(repositories.length).toEqual 1
|
||||
expect(repositories[0]).toBeTruthy()
|
||||
|
||||
atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider)
|
||||
waitsFor -> atom.project.repositoryProviders.length > 1
|
||||
runs -> expect(atom.project.getRepositories()).toBe repositories
|
||||
|
||||
it "stops using it to create repositories when the service is removed", ->
|
||||
atom.project.setPaths([])
|
||||
|
||||
disposable = atom.packages.serviceHub.provide("atom.repository-provider", "0.1.0", fakeRepositoryProvider)
|
||||
waitsFor -> atom.project.repositoryProviders.length > 1
|
||||
runs ->
|
||||
disposable.dispose()
|
||||
atom.project.addPath(temp.mkdirSync('atom-project'))
|
||||
expect(atom.project.getRepositories()).toEqual [null]
|
||||
|
||||
describe "when a custom directory-provider service is provided", ->
|
||||
class DummyDirectory
|
||||
constructor: (@path) ->
|
||||
getPath: -> @path
|
||||
getFile: -> {existsSync: -> false}
|
||||
getSubdirectory: -> {existsSync: -> false}
|
||||
isRoot: -> true
|
||||
existsSync: -> @path.endsWith('does-exist')
|
||||
contains: (filePath) -> filePath.startsWith(@path)
|
||||
|
||||
serviceDisposable = null
|
||||
|
||||
beforeEach ->
|
||||
serviceDisposable = atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", {
|
||||
directoryForURISync: (uri) ->
|
||||
if uri.startsWith("ssh://")
|
||||
new DummyDirectory(uri)
|
||||
else
|
||||
null
|
||||
})
|
||||
|
||||
waitsFor ->
|
||||
atom.project.directoryProviders.length > 0
|
||||
|
||||
it "uses the provider's custom directories for any paths that it handles", ->
|
||||
localPath = temp.mkdirSync('local-path')
|
||||
remotePath = "ssh://foreign-directory:8080/does-exist"
|
||||
|
||||
atom.project.setPaths([localPath, remotePath])
|
||||
|
||||
directories = atom.project.getDirectories()
|
||||
expect(directories[0].getPath()).toBe localPath
|
||||
expect(directories[0] instanceof Directory).toBe true
|
||||
expect(directories[1].getPath()).toBe remotePath
|
||||
expect(directories[1] instanceof DummyDirectory).toBe true
|
||||
|
||||
# It does not add new remote paths that do not exist
|
||||
nonExistentRemotePath = "ssh://another-directory:8080/does-not-exist"
|
||||
atom.project.addPath(nonExistentRemotePath)
|
||||
expect(atom.project.getDirectories().length).toBe 2
|
||||
|
||||
# It adds new remote paths if their directories exist.
|
||||
newRemotePath = "ssh://another-directory:8080/does-exist"
|
||||
atom.project.addPath(newRemotePath)
|
||||
directories = atom.project.getDirectories()
|
||||
expect(directories[2].getPath()).toBe newRemotePath
|
||||
expect(directories[2] instanceof DummyDirectory).toBe true
|
||||
|
||||
it "stops using the provider when the service is removed", ->
|
||||
serviceDisposable.dispose()
|
||||
atom.project.setPaths(["ssh://foreign-directory:8080/does-exist"])
|
||||
expect(atom.project.getDirectories().length).toBe(0)
|
||||
|
||||
describe ".open(path)", ->
|
||||
[absolutePath, newBufferHandler] = []
|
||||
|
||||
beforeEach ->
|
||||
absolutePath = require.resolve('./fixtures/dir/a')
|
||||
newBufferHandler = jasmine.createSpy('newBufferHandler')
|
||||
atom.project.onDidAddBuffer(newBufferHandler)
|
||||
|
||||
describe "when given an absolute path that isn't currently open", ->
|
||||
it "returns a new edit session for the given path and emits 'buffer-created'", ->
|
||||
editor = null
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(absolutePath).then (o) -> editor = o
|
||||
|
||||
runs ->
|
||||
expect(editor.buffer.getPath()).toBe absolutePath
|
||||
expect(newBufferHandler).toHaveBeenCalledWith editor.buffer
|
||||
|
||||
describe "when given a relative path that isn't currently opened", ->
|
||||
it "returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", ->
|
||||
editor = null
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(absolutePath).then (o) -> editor = o
|
||||
|
||||
runs ->
|
||||
expect(editor.buffer.getPath()).toBe absolutePath
|
||||
expect(newBufferHandler).toHaveBeenCalledWith editor.buffer
|
||||
|
||||
describe "when passed the path to a buffer that is currently opened", ->
|
||||
it "returns a new edit session containing currently opened buffer", ->
|
||||
editor = null
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(absolutePath).then (o) -> editor = o
|
||||
|
||||
runs ->
|
||||
newBufferHandler.reset()
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open(absolutePath).then ({buffer}) ->
|
||||
expect(buffer).toBe editor.buffer
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('a').then ({buffer}) ->
|
||||
expect(buffer).toBe editor.buffer
|
||||
expect(newBufferHandler).not.toHaveBeenCalled()
|
||||
|
||||
describe "when not passed a path", ->
|
||||
it "returns a new edit session and emits 'buffer-created'", ->
|
||||
editor = null
|
||||
waitsForPromise ->
|
||||
atom.workspace.open().then (o) -> editor = o
|
||||
|
||||
runs ->
|
||||
expect(editor.buffer.getPath()).toBeUndefined()
|
||||
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
|
||||
|
||||
describe ".bufferForPath(path)", ->
|
||||
buffer = null
|
||||
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.project.bufferForPath("a").then (o) ->
|
||||
buffer = o
|
||||
buffer.retain()
|
||||
|
||||
afterEach ->
|
||||
buffer.release()
|
||||
|
||||
describe "when opening a previously opened path", ->
|
||||
it "does not create a new buffer", ->
|
||||
waitsForPromise ->
|
||||
atom.project.bufferForPath("a").then (anotherBuffer) ->
|
||||
expect(anotherBuffer).toBe buffer
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.bufferForPath("b").then (anotherBuffer) ->
|
||||
expect(anotherBuffer).not.toBe buffer
|
||||
|
||||
waitsForPromise ->
|
||||
Promise.all([
|
||||
atom.project.bufferForPath('c'),
|
||||
atom.project.bufferForPath('c')
|
||||
]).then ([buffer1, buffer2]) ->
|
||||
expect(buffer1).toBe(buffer2)
|
||||
|
||||
it "retries loading the buffer if it previously failed", ->
|
||||
waitsForPromise shouldReject: true, ->
|
||||
spyOn(TextBuffer, 'load').andCallFake ->
|
||||
Promise.reject(new Error('Could not open file'))
|
||||
atom.project.bufferForPath('b')
|
||||
|
||||
waitsForPromise shouldReject: false, ->
|
||||
TextBuffer.load.andCallThrough()
|
||||
atom.project.bufferForPath('b')
|
||||
|
||||
it "creates a new buffer if the previous buffer was destroyed", ->
|
||||
buffer.release()
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.bufferForPath("b").then (anotherBuffer) ->
|
||||
expect(anotherBuffer).not.toBe buffer
|
||||
|
||||
describe ".repositoryForDirectory(directory)", ->
|
||||
it "resolves to null when the directory does not have a repository", ->
|
||||
waitsForPromise ->
|
||||
directory = new Directory("/tmp")
|
||||
atom.project.repositoryForDirectory(directory).then (result) ->
|
||||
expect(result).toBeNull()
|
||||
expect(atom.project.repositoryProviders.length).toBeGreaterThan 0
|
||||
expect(atom.project.repositoryPromisesByPath.size).toBe 0
|
||||
|
||||
it "resolves to a GitRepository and is cached when the given directory is a Git repo", ->
|
||||
waitsForPromise ->
|
||||
directory = new Directory(path.join(__dirname, '..'))
|
||||
promise = atom.project.repositoryForDirectory(directory)
|
||||
promise.then (result) ->
|
||||
expect(result).toBeInstanceOf GitRepository
|
||||
dirPath = directory.getRealPathSync()
|
||||
expect(result.getPath()).toBe path.join(dirPath, '.git')
|
||||
|
||||
# Verify that the result is cached.
|
||||
expect(atom.project.repositoryForDirectory(directory)).toBe(promise)
|
||||
|
||||
it "creates a new repository if a previous one with the same directory had been destroyed", ->
|
||||
repository = null
|
||||
directory = new Directory(path.join(__dirname, '..'))
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.repositoryForDirectory(directory).then (repo) -> repository = repo
|
||||
|
||||
runs ->
|
||||
expect(repository.isDestroyed()).toBe(false)
|
||||
repository.destroy()
|
||||
expect(repository.isDestroyed()).toBe(true)
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.repositoryForDirectory(directory).then (repo) -> repository = repo
|
||||
|
||||
runs ->
|
||||
expect(repository.isDestroyed()).toBe(false)
|
||||
|
||||
describe ".setPaths(paths)", ->
|
||||
describe "when path is a file", ->
|
||||
it "sets its path to the files parent directory and updates the root directory", ->
|
||||
filePath = require.resolve('./fixtures/dir/a')
|
||||
atom.project.setPaths([filePath])
|
||||
expect(atom.project.getPaths()[0]).toEqual path.dirname(filePath)
|
||||
expect(atom.project.getDirectories()[0].path).toEqual path.dirname(filePath)
|
||||
|
||||
describe "when path is a directory", ->
|
||||
it "assigns the directories and repositories", ->
|
||||
directory1 = temp.mkdirSync("non-git-repo")
|
||||
directory2 = temp.mkdirSync("git-repo1")
|
||||
directory3 = temp.mkdirSync("git-repo2")
|
||||
|
||||
gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git'))
|
||||
fs.copySync(gitDirPath, path.join(directory2, ".git"))
|
||||
fs.copySync(gitDirPath, path.join(directory3, ".git"))
|
||||
|
||||
atom.project.setPaths([directory1, directory2, directory3])
|
||||
|
||||
[repo1, repo2, repo3] = atom.project.getRepositories()
|
||||
expect(repo1).toBeNull()
|
||||
expect(repo2.getShortHead()).toBe "master"
|
||||
expect(repo2.getPath()).toBe fs.realpathSync(path.join(directory2, ".git"))
|
||||
expect(repo3.getShortHead()).toBe "master"
|
||||
expect(repo3.getPath()).toBe fs.realpathSync(path.join(directory3, ".git"))
|
||||
|
||||
it "calls callbacks registered with ::onDidChangePaths", ->
|
||||
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
|
||||
paths = [ temp.mkdirSync("dir1"), temp.mkdirSync("dir2") ]
|
||||
atom.project.setPaths(paths)
|
||||
|
||||
expect(onDidChangePathsSpy.callCount).toBe 1
|
||||
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths)
|
||||
|
||||
describe "when no paths are given", ->
|
||||
it "clears its path", ->
|
||||
atom.project.setPaths([])
|
||||
expect(atom.project.getPaths()).toEqual []
|
||||
expect(atom.project.getDirectories()).toEqual []
|
||||
|
||||
it "normalizes the path to remove consecutive slashes, ., and .. segments", ->
|
||||
atom.project.setPaths(["#{require.resolve('./fixtures/dir/a')}#{path.sep}b#{path.sep}#{path.sep}.."])
|
||||
expect(atom.project.getPaths()[0]).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
|
||||
expect(atom.project.getDirectories()[0].path).toEqual path.dirname(require.resolve('./fixtures/dir/a'))
|
||||
|
||||
describe ".addPath(path)", ->
|
||||
it "calls callbacks registered with ::onDidChangePaths", ->
|
||||
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
|
||||
[oldPath] = atom.project.getPaths()
|
||||
|
||||
newPath = temp.mkdirSync("dir")
|
||||
atom.project.addPath(newPath)
|
||||
|
||||
expect(onDidChangePathsSpy.callCount).toBe 1
|
||||
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath])
|
||||
|
||||
it "doesn't add redundant paths", ->
|
||||
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
[oldPath] = atom.project.getPaths()
|
||||
|
||||
# Doesn't re-add an existing root directory
|
||||
atom.project.addPath(oldPath)
|
||||
expect(atom.project.getPaths()).toEqual([oldPath])
|
||||
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
|
||||
|
||||
# Doesn't add an entry for a file-path within an existing root directory
|
||||
atom.project.addPath(path.join(oldPath, 'some-file.txt'))
|
||||
expect(atom.project.getPaths()).toEqual([oldPath])
|
||||
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
|
||||
|
||||
# Does add an entry for a directory within an existing directory
|
||||
newPath = path.join(oldPath, "a-dir")
|
||||
atom.project.addPath(newPath)
|
||||
expect(atom.project.getPaths()).toEqual([oldPath, newPath])
|
||||
expect(onDidChangePathsSpy).toHaveBeenCalled()
|
||||
|
||||
it "doesn't add non-existent directories", ->
|
||||
previousPaths = atom.project.getPaths()
|
||||
atom.project.addPath('/this-definitely/does-not-exist')
|
||||
expect(atom.project.getPaths()).toEqual(previousPaths)
|
||||
|
||||
describe ".removePath(path)", ->
|
||||
onDidChangePathsSpy = null
|
||||
|
||||
beforeEach ->
|
||||
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
|
||||
it "removes the directory and repository for the path", ->
|
||||
result = atom.project.removePath(atom.project.getPaths()[0])
|
||||
expect(atom.project.getDirectories()).toEqual([])
|
||||
expect(atom.project.getRepositories()).toEqual([])
|
||||
expect(atom.project.getPaths()).toEqual([])
|
||||
expect(result).toBe true
|
||||
expect(onDidChangePathsSpy).toHaveBeenCalled()
|
||||
|
||||
it "does nothing if the path is not one of the project's root paths", ->
|
||||
originalPaths = atom.project.getPaths()
|
||||
result = atom.project.removePath(originalPaths[0] + "xyz")
|
||||
expect(result).toBe false
|
||||
expect(atom.project.getPaths()).toEqual(originalPaths)
|
||||
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
|
||||
|
||||
it "doesn't destroy the repository if it is shared by another root directory", ->
|
||||
atom.project.setPaths([__dirname, path.join(__dirname, "..", "src")])
|
||||
atom.project.removePath(__dirname)
|
||||
expect(atom.project.getPaths()).toEqual([path.join(__dirname, "..", "src")])
|
||||
expect(atom.project.getRepositories()[0].isSubmodule("src")).toBe false
|
||||
|
||||
it "removes a path that is represented as a URI", ->
|
||||
atom.packages.serviceHub.provide("atom.directory-provider", "0.1.0", {
|
||||
directoryForURISync: (uri) ->
|
||||
{
|
||||
getPath: -> uri
|
||||
getSubdirectory: -> {}
|
||||
isRoot: -> true
|
||||
existsSync: -> true
|
||||
off: ->
|
||||
}
|
||||
})
|
||||
|
||||
ftpURI = "ftp://example.com/some/folder"
|
||||
|
||||
atom.project.setPaths([ftpURI])
|
||||
expect(atom.project.getPaths()).toEqual [ftpURI]
|
||||
|
||||
atom.project.removePath(ftpURI)
|
||||
expect(atom.project.getPaths()).toEqual []
|
||||
|
||||
describe ".onDidChangeFiles()", ->
|
||||
sub = []
|
||||
events = []
|
||||
checkCallback = ->
|
||||
|
||||
beforeEach ->
|
||||
sub = atom.project.onDidChangeFiles (incoming) ->
|
||||
events.push incoming...
|
||||
checkCallback()
|
||||
|
||||
afterEach ->
|
||||
sub.dispose()
|
||||
|
||||
waitForEvents = (paths) ->
|
||||
remaining = new Set(fs.realpathSync(p) for p in paths)
|
||||
new Promise (resolve, reject) ->
|
||||
checkCallback = ->
|
||||
remaining.delete(event.path) for event in events
|
||||
resolve() if remaining.size is 0
|
||||
|
||||
expire = ->
|
||||
checkCallback = ->
|
||||
console.error "Paths not seen:", Array.from(remaining)
|
||||
reject(new Error('Expired before all expected events were delivered.'))
|
||||
|
||||
checkCallback()
|
||||
setTimeout expire, 2000
|
||||
|
||||
it "reports filesystem changes within project paths", ->
|
||||
dirOne = temp.mkdirSync('atom-spec-project-one')
|
||||
fileOne = path.join(dirOne, 'file-one.txt')
|
||||
fileTwo = path.join(dirOne, 'file-two.txt')
|
||||
dirTwo = temp.mkdirSync('atom-spec-project-two')
|
||||
fileThree = path.join(dirTwo, 'file-three.txt')
|
||||
|
||||
# Ensure that all preexisting watchers are stopped
|
||||
waitsForPromise -> stopAllWatchers()
|
||||
|
||||
runs -> atom.project.setPaths([dirOne])
|
||||
waitsForPromise -> atom.project.getWatcherPromise dirOne
|
||||
|
||||
runs ->
|
||||
expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual undefined
|
||||
|
||||
fs.writeFileSync fileThree, "three\n"
|
||||
fs.writeFileSync fileTwo, "two\n"
|
||||
fs.writeFileSync fileOne, "one\n"
|
||||
|
||||
waitsForPromise -> waitForEvents [fileOne, fileTwo]
|
||||
|
||||
runs ->
|
||||
expect(events.some (event) -> event.path is fileThree).toBeFalsy()
|
||||
|
||||
describe ".onDidAddBuffer()", ->
|
||||
it "invokes the callback with added text buffers", ->
|
||||
buffers = []
|
||||
added = []
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
|
||||
.then (o) -> buffers.push(o)
|
||||
|
||||
runs ->
|
||||
expect(buffers.length).toBe 1
|
||||
atom.project.onDidAddBuffer (buffer) -> added.push(buffer)
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
|
||||
.then (o) -> buffers.push(o)
|
||||
|
||||
runs ->
|
||||
expect(buffers.length).toBe 2
|
||||
expect(added).toEqual [buffers[1]]
|
||||
|
||||
describe ".observeBuffers()", ->
|
||||
it "invokes the observer with current and future text buffers", ->
|
||||
buffers = []
|
||||
observed = []
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
|
||||
.then (o) -> buffers.push(o)
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
|
||||
.then (o) -> buffers.push(o)
|
||||
|
||||
runs ->
|
||||
expect(buffers.length).toBe 2
|
||||
atom.project.observeBuffers (buffer) -> observed.push(buffer)
|
||||
expect(observed).toEqual buffers
|
||||
|
||||
waitsForPromise ->
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
|
||||
.then (o) -> buffers.push(o)
|
||||
|
||||
runs ->
|
||||
expect(observed.length).toBe 3
|
||||
expect(buffers.length).toBe 3
|
||||
expect(observed).toEqual buffers
|
||||
|
||||
describe ".relativize(path)", ->
|
||||
it "returns the path, relative to whichever root directory it is inside of", ->
|
||||
atom.project.addPath(temp.mkdirSync("another-path"))
|
||||
|
||||
rootPath = atom.project.getPaths()[0]
|
||||
childPath = path.join(rootPath, "some", "child", "directory")
|
||||
expect(atom.project.relativize(childPath)).toBe path.join("some", "child", "directory")
|
||||
|
||||
rootPath = atom.project.getPaths()[1]
|
||||
childPath = path.join(rootPath, "some", "child", "directory")
|
||||
expect(atom.project.relativize(childPath)).toBe path.join("some", "child", "directory")
|
||||
|
||||
it "returns the given path if it is not in any of the root directories", ->
|
||||
randomPath = path.join("some", "random", "path")
|
||||
expect(atom.project.relativize(randomPath)).toBe randomPath
|
||||
|
||||
describe ".relativizePath(path)", ->
|
||||
it "returns the root path that contains the given path, and the path relativized to that root path", ->
|
||||
atom.project.addPath(temp.mkdirSync("another-path"))
|
||||
|
||||
rootPath = atom.project.getPaths()[0]
|
||||
childPath = path.join(rootPath, "some", "child", "directory")
|
||||
expect(atom.project.relativizePath(childPath)).toEqual [rootPath, path.join("some", "child", "directory")]
|
||||
|
||||
rootPath = atom.project.getPaths()[1]
|
||||
childPath = path.join(rootPath, "some", "child", "directory")
|
||||
expect(atom.project.relativizePath(childPath)).toEqual [rootPath, path.join("some", "child", "directory")]
|
||||
|
||||
describe "when the given path isn't inside of any of the project's path", ->
|
||||
it "returns null for the root path, and the given path unchanged", ->
|
||||
randomPath = path.join("some", "random", "path")
|
||||
expect(atom.project.relativizePath(randomPath)).toEqual [null, randomPath]
|
||||
|
||||
describe "when the given path is a URL", ->
|
||||
it "returns null for the root path, and the given path unchanged", ->
|
||||
url = "http://the-path"
|
||||
expect(atom.project.relativizePath(url)).toEqual [null, url]
|
||||
|
||||
describe "when the given path is inside more than one root folder", ->
|
||||
it "uses the root folder that is closest to the given path", ->
|
||||
atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir'))
|
||||
|
||||
inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt')
|
||||
|
||||
expect(atom.project.getDirectories()[0].contains(inputPath)).toBe true
|
||||
expect(atom.project.getDirectories()[1].contains(inputPath)).toBe true
|
||||
expect(atom.project.relativizePath(inputPath)).toEqual [
|
||||
atom.project.getPaths()[1],
|
||||
path.join('somewhere', 'something.txt')
|
||||
]
|
||||
|
||||
describe ".contains(path)", ->
|
||||
it "returns whether or not the given path is in one of the root directories", ->
|
||||
rootPath = atom.project.getPaths()[0]
|
||||
childPath = path.join(rootPath, "some", "child", "directory")
|
||||
expect(atom.project.contains(childPath)).toBe true
|
||||
|
||||
randomPath = path.join("some", "random", "path")
|
||||
expect(atom.project.contains(randomPath)).toBe false
|
||||
|
||||
describe ".resolvePath(uri)", ->
|
||||
it "normalizes disk drive letter in passed path on #win32", ->
|
||||
expect(atom.project.resolvePath("d:\\file.txt")).toEqual "D:\\file.txt"
|
927
spec/project-spec.js
Normal file
927
spec/project-spec.js
Normal file
@ -0,0 +1,927 @@
|
||||
const temp = require('temp').track()
|
||||
const TextBuffer = require('text-buffer')
|
||||
const Project = require('../src/project')
|
||||
const fs = require('fs-plus')
|
||||
const path = require('path')
|
||||
const {Directory} = require('pathwatcher')
|
||||
const {stopAllWatchers} = require('../src/path-watcher')
|
||||
const GitRepository = require('../src/git-repository')
|
||||
|
||||
describe('Project', () => {
|
||||
beforeEach(() => {
|
||||
const directory = atom.project.getDirectories()[0]
|
||||
const paths = directory ? [directory.resolve('dir')] : [null]
|
||||
atom.project.setPaths(paths)
|
||||
|
||||
// Wait for project's service consumers to be asynchronously added
|
||||
waits(1)
|
||||
})
|
||||
|
||||
describe('serialization', () => {
|
||||
let deserializedProject = null
|
||||
let notQuittingProject = null
|
||||
let quittingProject = null
|
||||
|
||||
afterEach(() => {
|
||||
if (deserializedProject != null) {
|
||||
deserializedProject.destroy()
|
||||
}
|
||||
if (notQuittingProject != null) {
|
||||
notQuittingProject.destroy()
|
||||
}
|
||||
if (quittingProject != null) {
|
||||
quittingProject.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
it("does not deserialize paths to directories that don't exist", () => {
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
const state = atom.project.serialize()
|
||||
state.paths.push('/directory/that/does/not/exist')
|
||||
|
||||
let err = null
|
||||
waitsForPromise(() =>
|
||||
deserializedProject.deserialize(state, atom.deserializers)
|
||||
.catch(e => { err = e })
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
expect(deserializedProject.getPaths()).toEqual(atom.project.getPaths())
|
||||
expect(err.missingProjectPaths).toEqual(['/directory/that/does/not/exist'])
|
||||
})
|
||||
})
|
||||
|
||||
it('does not deserialize paths that are now files', () => {
|
||||
const childPath = path.join(temp.mkdirSync('atom-spec-project'), 'child')
|
||||
fs.mkdirSync(childPath)
|
||||
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
atom.project.setPaths([childPath])
|
||||
const state = atom.project.serialize()
|
||||
|
||||
fs.rmdirSync(childPath)
|
||||
fs.writeFileSync(childPath, 'surprise!\n')
|
||||
|
||||
let err = null
|
||||
waitsForPromise(() =>
|
||||
deserializedProject.deserialize(state, atom.deserializers)
|
||||
.catch(e => { err = e })
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
expect(deserializedProject.getPaths()).toEqual([])
|
||||
expect(err.missingProjectPaths).toEqual([childPath])
|
||||
})
|
||||
})
|
||||
|
||||
it('does not include unretained buffers in the serialized state', () => {
|
||||
waitsForPromise(() => atom.project.bufferForPath('a'))
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.getBuffers().length).toBe(1)
|
||||
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
|
||||
})
|
||||
|
||||
it('listens for destroyed events on deserialized buffers and removes them when they are destroyed', () => {
|
||||
waitsForPromise(() => atom.workspace.open('a'))
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.getBuffers().length).toBe(1)
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => {
|
||||
expect(deserializedProject.getBuffers().length).toBe(1)
|
||||
deserializedProject.getBuffers()[0].destroy()
|
||||
expect(deserializedProject.getBuffers().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not deserialize buffers when their path is now a directory', () => {
|
||||
const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
|
||||
waitsForPromise(() => atom.workspace.open(pathToOpen))
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.getBuffers().length).toBe(1)
|
||||
fs.mkdirSync(pathToOpen)
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
|
||||
})
|
||||
|
||||
it('does not deserialize buffers when their path is inaccessible', () => {
|
||||
if (process.platform === 'win32') { return } // chmod not supported on win32
|
||||
const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
fs.writeFileSync(pathToOpen, '')
|
||||
|
||||
waitsForPromise(() => atom.workspace.open(pathToOpen))
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.getBuffers().length).toBe(1)
|
||||
fs.chmodSync(pathToOpen, '000')
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
|
||||
})
|
||||
|
||||
it('does not deserialize buffers with their path is no longer present', () => {
|
||||
const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
fs.writeFileSync(pathToOpen, '')
|
||||
|
||||
waitsForPromise(() => atom.workspace.open(pathToOpen))
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.getBuffers().length).toBe(1)
|
||||
fs.unlinkSync(pathToOpen)
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => expect(deserializedProject.getBuffers().length).toBe(0))
|
||||
})
|
||||
|
||||
it('deserializes buffers that have never been saved before', () => {
|
||||
const pathToOpen = path.join(temp.mkdirSync('atom-spec-project'), 'file.txt')
|
||||
|
||||
waitsForPromise(() => atom.workspace.open(pathToOpen))
|
||||
|
||||
runs(() => {
|
||||
atom.workspace.getActiveTextEditor().setText('unsaved\n')
|
||||
expect(atom.project.getBuffers().length).toBe(1)
|
||||
|
||||
deserializedProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => deserializedProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => {
|
||||
expect(deserializedProject.getBuffers().length).toBe(1)
|
||||
expect(deserializedProject.getBuffers()[0].getPath()).toBe(pathToOpen)
|
||||
expect(deserializedProject.getBuffers()[0].getText()).toBe('unsaved\n')
|
||||
})
|
||||
})
|
||||
|
||||
it('serializes marker layers and history only if Atom is quitting', () => {
|
||||
waitsForPromise(() => atom.workspace.open('a'))
|
||||
|
||||
let bufferA = null
|
||||
let layerA = null
|
||||
let markerA = null
|
||||
|
||||
runs(() => {
|
||||
bufferA = atom.project.getBuffers()[0]
|
||||
layerA = bufferA.addMarkerLayer({persistent: true})
|
||||
markerA = layerA.markPosition([0, 3])
|
||||
bufferA.append('!')
|
||||
notQuittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => notQuittingProject.deserialize(atom.project.serialize({isUnloading: false})))
|
||||
|
||||
runs(() => {
|
||||
expect(notQuittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).toBeUndefined()
|
||||
expect(notQuittingProject.getBuffers()[0].undo()).toBe(false)
|
||||
quittingProject = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm})
|
||||
})
|
||||
|
||||
waitsForPromise(() => quittingProject.deserialize(atom.project.serialize({isUnloading: true})))
|
||||
|
||||
runs(() => {
|
||||
expect(quittingProject.getBuffers()[0].getMarkerLayer(layerA.id), x => x.getMarker(markerA.id)).not.toBeUndefined()
|
||||
expect(quittingProject.getBuffers()[0].undo()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when an editor is saved and the project has no path', () =>
|
||||
it("sets the project's path to the saved file's parent directory", () => {
|
||||
const tempFile = temp.openSync().path
|
||||
atom.project.setPaths([])
|
||||
expect(atom.project.getPaths()[0]).toBeUndefined()
|
||||
let editor = null
|
||||
|
||||
waitsForPromise(() => atom.workspace.open().then(o => { editor = o }))
|
||||
|
||||
waitsForPromise(() => editor.saveAs(tempFile))
|
||||
|
||||
runs(() => expect(atom.project.getPaths()[0]).toBe(path.dirname(tempFile)))
|
||||
})
|
||||
)
|
||||
|
||||
describe('before and after saving a buffer', () => {
|
||||
let buffer
|
||||
beforeEach(() =>
|
||||
waitsForPromise(() =>
|
||||
atom.project.bufferForPath(path.join(__dirname, 'fixtures', 'sample.js')).then((o) => {
|
||||
buffer = o
|
||||
buffer.retain()
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
afterEach(() => buffer.release())
|
||||
|
||||
it('emits save events on the main process', () => {
|
||||
spyOn(atom.project.applicationDelegate, 'emitDidSavePath')
|
||||
spyOn(atom.project.applicationDelegate, 'emitWillSavePath')
|
||||
|
||||
waitsForPromise(() => buffer.save())
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.applicationDelegate.emitDidSavePath.calls.length).toBe(1)
|
||||
expect(atom.project.applicationDelegate.emitDidSavePath).toHaveBeenCalledWith(buffer.getPath())
|
||||
expect(atom.project.applicationDelegate.emitWillSavePath.calls.length).toBe(1)
|
||||
expect(atom.project.applicationDelegate.emitWillSavePath).toHaveBeenCalledWith(buffer.getPath())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a watch error is thrown from the TextBuffer', () => {
|
||||
let editor = null
|
||||
beforeEach(() =>
|
||||
waitsForPromise(() => atom.workspace.open(require.resolve('./fixtures/dir/a')).then(o => { editor = o }))
|
||||
)
|
||||
|
||||
it('creates a warning notification', () => {
|
||||
let noteSpy
|
||||
atom.notifications.onDidAddNotification(noteSpy = jasmine.createSpy())
|
||||
|
||||
const error = new Error('SomeError')
|
||||
error.eventType = 'resurrect'
|
||||
editor.buffer.emitter.emit('will-throw-watch-error', {
|
||||
handle: jasmine.createSpy(),
|
||||
error
|
||||
}
|
||||
)
|
||||
|
||||
expect(noteSpy).toHaveBeenCalled()
|
||||
|
||||
const notification = noteSpy.mostRecentCall.args[0]
|
||||
expect(notification.getType()).toBe('warning')
|
||||
expect(notification.getDetail()).toBe('SomeError')
|
||||
expect(notification.getMessage()).toContain('`resurrect`')
|
||||
expect(notification.getMessage()).toContain(path.join('fixtures', 'dir', 'a'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a custom repository-provider service is provided', () => {
|
||||
let fakeRepositoryProvider, fakeRepository
|
||||
|
||||
beforeEach(() => {
|
||||
fakeRepository = {destroy () { return null }}
|
||||
fakeRepositoryProvider = {
|
||||
repositoryForDirectory (directory) { return Promise.resolve(fakeRepository) },
|
||||
repositoryForDirectorySync (directory) { return fakeRepository }
|
||||
}
|
||||
})
|
||||
|
||||
it('uses it to create repositories for any directories that need one', () => {
|
||||
const projectPath = temp.mkdirSync('atom-project')
|
||||
atom.project.setPaths([projectPath])
|
||||
expect(atom.project.getRepositories()).toEqual([null])
|
||||
|
||||
atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider)
|
||||
waitsFor(() => atom.project.repositoryProviders.length > 1)
|
||||
runs(() => atom.project.getRepositories()[0] === fakeRepository)
|
||||
})
|
||||
|
||||
it('does not create any new repositories if every directory has a repository', () => {
|
||||
const repositories = atom.project.getRepositories()
|
||||
expect(repositories.length).toEqual(1)
|
||||
expect(repositories[0]).toBeTruthy()
|
||||
|
||||
atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider)
|
||||
waitsFor(() => atom.project.repositoryProviders.length > 1)
|
||||
runs(() => expect(atom.project.getRepositories()).toBe(repositories))
|
||||
})
|
||||
|
||||
it('stops using it to create repositories when the service is removed', () => {
|
||||
atom.project.setPaths([])
|
||||
|
||||
const disposable = atom.packages.serviceHub.provide('atom.repository-provider', '0.1.0', fakeRepositoryProvider)
|
||||
waitsFor(() => atom.project.repositoryProviders.length > 1)
|
||||
runs(() => {
|
||||
disposable.dispose()
|
||||
atom.project.addPath(temp.mkdirSync('atom-project'))
|
||||
expect(atom.project.getRepositories()).toEqual([null])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a custom directory-provider service is provided', () => {
|
||||
class DummyDirectory {
|
||||
constructor (aPath) {
|
||||
this.path = aPath
|
||||
}
|
||||
getPath () { return this.path }
|
||||
getFile () { return {existsSync () { return false }} }
|
||||
getSubdirectory () { return {existsSync () { return false }} }
|
||||
isRoot () { return true }
|
||||
existsSync () { return this.path.endsWith('does-exist') }
|
||||
contains (filePath) { return filePath.startsWith(this.path) }
|
||||
}
|
||||
|
||||
let serviceDisposable = null
|
||||
|
||||
beforeEach(() => {
|
||||
serviceDisposable = atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', {
|
||||
directoryForURISync (uri) {
|
||||
if (uri.startsWith('ssh://')) {
|
||||
return new DummyDirectory(uri)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
waitsFor(() => atom.project.directoryProviders.length > 0)
|
||||
})
|
||||
|
||||
it("uses the provider's custom directories for any paths that it handles", () => {
|
||||
const localPath = temp.mkdirSync('local-path')
|
||||
const remotePath = 'ssh://foreign-directory:8080/does-exist'
|
||||
|
||||
atom.project.setPaths([localPath, remotePath])
|
||||
|
||||
let directories = atom.project.getDirectories()
|
||||
expect(directories[0].getPath()).toBe(localPath)
|
||||
expect(directories[0] instanceof Directory).toBe(true)
|
||||
expect(directories[1].getPath()).toBe(remotePath)
|
||||
expect(directories[1] instanceof DummyDirectory).toBe(true)
|
||||
|
||||
// It does not add new remote paths that do not exist
|
||||
const nonExistentRemotePath = 'ssh://another-directory:8080/does-not-exist'
|
||||
atom.project.addPath(nonExistentRemotePath)
|
||||
expect(atom.project.getDirectories().length).toBe(2)
|
||||
|
||||
// It adds new remote paths if their directories exist.
|
||||
const newRemotePath = 'ssh://another-directory:8080/does-exist'
|
||||
atom.project.addPath(newRemotePath)
|
||||
directories = atom.project.getDirectories()
|
||||
expect(directories[2].getPath()).toBe(newRemotePath)
|
||||
expect(directories[2] instanceof DummyDirectory).toBe(true)
|
||||
})
|
||||
|
||||
it('stops using the provider when the service is removed', () => {
|
||||
serviceDisposable.dispose()
|
||||
atom.project.setPaths(['ssh://foreign-directory:8080/does-exist'])
|
||||
expect(atom.project.getDirectories().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.open(path)', () => {
|
||||
let absolutePath, newBufferHandler
|
||||
|
||||
beforeEach(() => {
|
||||
absolutePath = require.resolve('./fixtures/dir/a')
|
||||
newBufferHandler = jasmine.createSpy('newBufferHandler')
|
||||
atom.project.onDidAddBuffer(newBufferHandler)
|
||||
})
|
||||
|
||||
describe("when given an absolute path that isn't currently open", () =>
|
||||
it("returns a new edit session for the given path and emits 'buffer-created'", () => {
|
||||
let editor = null
|
||||
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
|
||||
|
||||
runs(() => {
|
||||
expect(editor.buffer.getPath()).toBe(absolutePath)
|
||||
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
describe("when given a relative path that isn't currently opened", () =>
|
||||
it("returns a new edit session for the given path (relative to the project root) and emits 'buffer-created'", () => {
|
||||
let editor = null
|
||||
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
|
||||
|
||||
runs(() => {
|
||||
expect(editor.buffer.getPath()).toBe(absolutePath)
|
||||
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
describe('when passed the path to a buffer that is currently opened', () =>
|
||||
it('returns a new edit session containing currently opened buffer', () => {
|
||||
let editor = null
|
||||
|
||||
waitsForPromise(() => atom.workspace.open(absolutePath).then(o => { editor = o }))
|
||||
|
||||
runs(() => newBufferHandler.reset())
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.workspace.open(absolutePath).then(({buffer}) => expect(buffer).toBe(editor.buffer))
|
||||
)
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.workspace.open('a').then(({buffer}) => {
|
||||
expect(buffer).toBe(editor.buffer)
|
||||
expect(newBufferHandler).not.toHaveBeenCalled()
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when not passed a path', () =>
|
||||
it("returns a new edit session and emits 'buffer-created'", () => {
|
||||
let editor = null
|
||||
waitsForPromise(() => atom.workspace.open().then(o => { editor = o }))
|
||||
|
||||
runs(() => {
|
||||
expect(editor.buffer.getPath()).toBeUndefined()
|
||||
expect(newBufferHandler).toHaveBeenCalledWith(editor.buffer)
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('.bufferForPath(path)', () => {
|
||||
let buffer = null
|
||||
|
||||
beforeEach(() =>
|
||||
waitsForPromise(() =>
|
||||
atom.project.bufferForPath('a').then((o) => {
|
||||
buffer = o
|
||||
buffer.retain()
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
afterEach(() => buffer.release())
|
||||
|
||||
describe('when opening a previously opened path', () => {
|
||||
it('does not create a new buffer', () => {
|
||||
waitsForPromise(() =>
|
||||
atom.project.bufferForPath('a').then(anotherBuffer => expect(anotherBuffer).toBe(buffer))
|
||||
)
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer))
|
||||
)
|
||||
|
||||
waitsForPromise(() =>
|
||||
Promise.all([
|
||||
atom.project.bufferForPath('c'),
|
||||
atom.project.bufferForPath('c')
|
||||
]).then(([buffer1, buffer2]) => {
|
||||
expect(buffer1).toBe(buffer2)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('retries loading the buffer if it previously failed', () => {
|
||||
waitsForPromise({shouldReject: true}, () => {
|
||||
spyOn(TextBuffer, 'load').andCallFake(() => Promise.reject(new Error('Could not open file')))
|
||||
return atom.project.bufferForPath('b')
|
||||
})
|
||||
|
||||
waitsForPromise({shouldReject: false}, () => {
|
||||
TextBuffer.load.andCallThrough()
|
||||
return atom.project.bufferForPath('b')
|
||||
})
|
||||
})
|
||||
|
||||
it('creates a new buffer if the previous buffer was destroyed', () => {
|
||||
buffer.release()
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.bufferForPath('b').then(anotherBuffer => expect(anotherBuffer).not.toBe(buffer))
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.repositoryForDirectory(directory)', () => {
|
||||
it('resolves to null when the directory does not have a repository', () =>
|
||||
waitsForPromise(() => {
|
||||
const directory = new Directory('/tmp')
|
||||
return atom.project.repositoryForDirectory(directory).then((result) => {
|
||||
expect(result).toBeNull()
|
||||
expect(atom.project.repositoryProviders.length).toBeGreaterThan(0)
|
||||
expect(atom.project.repositoryPromisesByPath.size).toBe(0)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
it('resolves to a GitRepository and is cached when the given directory is a Git repo', () =>
|
||||
waitsForPromise(() => {
|
||||
const directory = new Directory(path.join(__dirname, '..'))
|
||||
const promise = atom.project.repositoryForDirectory(directory)
|
||||
return promise.then((result) => {
|
||||
expect(result).toBeInstanceOf(GitRepository)
|
||||
const dirPath = directory.getRealPathSync()
|
||||
expect(result.getPath()).toBe(path.join(dirPath, '.git'))
|
||||
|
||||
// Verify that the result is cached.
|
||||
expect(atom.project.repositoryForDirectory(directory)).toBe(promise)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
it('creates a new repository if a previous one with the same directory had been destroyed', () => {
|
||||
let repository = null
|
||||
const directory = new Directory(path.join(__dirname, '..'))
|
||||
|
||||
waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo }))
|
||||
|
||||
runs(() => {
|
||||
expect(repository.isDestroyed()).toBe(false)
|
||||
repository.destroy()
|
||||
expect(repository.isDestroyed()).toBe(true)
|
||||
})
|
||||
|
||||
waitsForPromise(() => atom.project.repositoryForDirectory(directory).then(repo => { repository = repo }))
|
||||
|
||||
runs(() => expect(repository.isDestroyed()).toBe(false))
|
||||
})
|
||||
})
|
||||
|
||||
describe('.setPaths(paths, options)', () => {
|
||||
describe('when path is a file', () =>
|
||||
it("sets its path to the file's parent directory and updates the root directory", () => {
|
||||
const filePath = require.resolve('./fixtures/dir/a')
|
||||
atom.project.setPaths([filePath])
|
||||
expect(atom.project.getPaths()[0]).toEqual(path.dirname(filePath))
|
||||
expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(filePath))
|
||||
})
|
||||
)
|
||||
|
||||
describe('when path is a directory', () => {
|
||||
it('assigns the directories and repositories', () => {
|
||||
const directory1 = temp.mkdirSync('non-git-repo')
|
||||
const directory2 = temp.mkdirSync('git-repo1')
|
||||
const directory3 = temp.mkdirSync('git-repo2')
|
||||
|
||||
const gitDirPath = fs.absolute(path.join(__dirname, 'fixtures', 'git', 'master.git'))
|
||||
fs.copySync(gitDirPath, path.join(directory2, '.git'))
|
||||
fs.copySync(gitDirPath, path.join(directory3, '.git'))
|
||||
|
||||
atom.project.setPaths([directory1, directory2, directory3])
|
||||
|
||||
const [repo1, repo2, repo3] = atom.project.getRepositories()
|
||||
expect(repo1).toBeNull()
|
||||
expect(repo2.getShortHead()).toBe('master')
|
||||
expect(repo2.getPath()).toBe(fs.realpathSync(path.join(directory2, '.git')))
|
||||
expect(repo3.getShortHead()).toBe('master')
|
||||
expect(repo3.getPath()).toBe(fs.realpathSync(path.join(directory3, '.git')))
|
||||
})
|
||||
|
||||
it('calls callbacks registered with ::onDidChangePaths', () => {
|
||||
const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
|
||||
const paths = [ temp.mkdirSync('dir1'), temp.mkdirSync('dir2') ]
|
||||
atom.project.setPaths(paths)
|
||||
|
||||
expect(onDidChangePathsSpy.callCount).toBe(1)
|
||||
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual(paths)
|
||||
})
|
||||
|
||||
it('optionally throws an error with any paths that did not exist', () => {
|
||||
const paths = [temp.mkdirSync('exists0'), '/doesnt-exists/0', temp.mkdirSync('exists1'), '/doesnt-exists/1']
|
||||
|
||||
try {
|
||||
atom.project.setPaths(paths, {mustExist: true})
|
||||
expect('no exception thrown').toBeUndefined()
|
||||
} catch (e) {
|
||||
expect(e.missingProjectPaths).toEqual([paths[1], paths[3]])
|
||||
}
|
||||
|
||||
expect(atom.project.getPaths()).toEqual([paths[0], paths[2]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when no paths are given', () =>
|
||||
it('clears its path', () => {
|
||||
atom.project.setPaths([])
|
||||
expect(atom.project.getPaths()).toEqual([])
|
||||
expect(atom.project.getDirectories()).toEqual([])
|
||||
})
|
||||
)
|
||||
|
||||
it('normalizes the path to remove consecutive slashes, ., and .. segments', () => {
|
||||
atom.project.setPaths([`${require.resolve('./fixtures/dir/a')}${path.sep}b${path.sep}${path.sep}..`])
|
||||
expect(atom.project.getPaths()[0]).toEqual(path.dirname(require.resolve('./fixtures/dir/a')))
|
||||
expect(atom.project.getDirectories()[0].path).toEqual(path.dirname(require.resolve('./fixtures/dir/a')))
|
||||
})
|
||||
})
|
||||
|
||||
describe('.addPath(path, options)', () => {
|
||||
it('calls callbacks registered with ::onDidChangePaths', () => {
|
||||
const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
|
||||
const [oldPath] = atom.project.getPaths()
|
||||
|
||||
const newPath = temp.mkdirSync('dir')
|
||||
atom.project.addPath(newPath)
|
||||
|
||||
expect(onDidChangePathsSpy.callCount).toBe(1)
|
||||
expect(onDidChangePathsSpy.mostRecentCall.args[0]).toEqual([oldPath, newPath])
|
||||
})
|
||||
|
||||
it("doesn't add redundant paths", () => {
|
||||
const onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths spy')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
const [oldPath] = atom.project.getPaths()
|
||||
|
||||
// Doesn't re-add an existing root directory
|
||||
atom.project.addPath(oldPath)
|
||||
expect(atom.project.getPaths()).toEqual([oldPath])
|
||||
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
|
||||
|
||||
// Doesn't add an entry for a file-path within an existing root directory
|
||||
atom.project.addPath(path.join(oldPath, 'some-file.txt'))
|
||||
expect(atom.project.getPaths()).toEqual([oldPath])
|
||||
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
|
||||
|
||||
// Does add an entry for a directory within an existing directory
|
||||
const newPath = path.join(oldPath, 'a-dir')
|
||||
atom.project.addPath(newPath)
|
||||
expect(atom.project.getPaths()).toEqual([oldPath, newPath])
|
||||
expect(onDidChangePathsSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("doesn't add non-existent directories", () => {
|
||||
const previousPaths = atom.project.getPaths()
|
||||
atom.project.addPath('/this-definitely/does-not-exist')
|
||||
expect(atom.project.getPaths()).toEqual(previousPaths)
|
||||
})
|
||||
|
||||
it('optionally throws on non-existent directories', () =>
|
||||
expect(() => atom.project.addPath('/this-definitely/does-not-exist', {mustExist: true})).toThrow()
|
||||
)
|
||||
})
|
||||
|
||||
describe('.removePath(path)', () => {
|
||||
let onDidChangePathsSpy = null
|
||||
|
||||
beforeEach(() => {
|
||||
onDidChangePathsSpy = jasmine.createSpy('onDidChangePaths listener')
|
||||
atom.project.onDidChangePaths(onDidChangePathsSpy)
|
||||
})
|
||||
|
||||
it('removes the directory and repository for the path', () => {
|
||||
const result = atom.project.removePath(atom.project.getPaths()[0])
|
||||
expect(atom.project.getDirectories()).toEqual([])
|
||||
expect(atom.project.getRepositories()).toEqual([])
|
||||
expect(atom.project.getPaths()).toEqual([])
|
||||
expect(result).toBe(true)
|
||||
expect(onDidChangePathsSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("does nothing if the path is not one of the project's root paths", () => {
|
||||
const originalPaths = atom.project.getPaths()
|
||||
const result = atom.project.removePath(originalPaths[0] + 'xyz')
|
||||
expect(result).toBe(false)
|
||||
expect(atom.project.getPaths()).toEqual(originalPaths)
|
||||
expect(onDidChangePathsSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("doesn't destroy the repository if it is shared by another root directory", () => {
|
||||
atom.project.setPaths([__dirname, path.join(__dirname, '..', 'src')])
|
||||
atom.project.removePath(__dirname)
|
||||
expect(atom.project.getPaths()).toEqual([path.join(__dirname, '..', 'src')])
|
||||
expect(atom.project.getRepositories()[0].isSubmodule('src')).toBe(false)
|
||||
})
|
||||
|
||||
it('removes a path that is represented as a URI', () => {
|
||||
atom.packages.serviceHub.provide('atom.directory-provider', '0.1.0', {
|
||||
directoryForURISync (uri) {
|
||||
return {
|
||||
getPath () { return uri },
|
||||
getSubdirectory () { return {} },
|
||||
isRoot () { return true },
|
||||
existsSync () { return true },
|
||||
off () {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const ftpURI = 'ftp://example.com/some/folder'
|
||||
|
||||
atom.project.setPaths([ftpURI])
|
||||
expect(atom.project.getPaths()).toEqual([ftpURI])
|
||||
|
||||
atom.project.removePath(ftpURI)
|
||||
expect(atom.project.getPaths()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('.onDidChangeFiles()', () => {
|
||||
let sub = []
|
||||
const events = []
|
||||
let checkCallback = () => {}
|
||||
|
||||
beforeEach(() => {
|
||||
sub = atom.project.onDidChangeFiles((incoming) => {
|
||||
events.push(...incoming)
|
||||
checkCallback()
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => sub.dispose())
|
||||
|
||||
const waitForEvents = (paths) => {
|
||||
const remaining = new Set(paths.map((p) => fs.realpathSync(p)))
|
||||
return new Promise((resolve, reject) => {
|
||||
checkCallback = () => {
|
||||
for (let event of events) { remaining.delete(event.path) }
|
||||
if (remaining.size === 0) { resolve() }
|
||||
}
|
||||
|
||||
const expire = () => {
|
||||
checkCallback = () => {}
|
||||
console.error('Paths not seen:', remaining)
|
||||
reject(new Error('Expired before all expected events were delivered.'))
|
||||
}
|
||||
|
||||
checkCallback()
|
||||
setTimeout(expire, 2000)
|
||||
})
|
||||
}
|
||||
|
||||
it('reports filesystem changes within project paths', () => {
|
||||
const dirOne = temp.mkdirSync('atom-spec-project-one')
|
||||
const fileOne = path.join(dirOne, 'file-one.txt')
|
||||
const fileTwo = path.join(dirOne, 'file-two.txt')
|
||||
const dirTwo = temp.mkdirSync('atom-spec-project-two')
|
||||
const fileThree = path.join(dirTwo, 'file-three.txt')
|
||||
|
||||
// Ensure that all preexisting watchers are stopped
|
||||
waitsForPromise(() => stopAllWatchers())
|
||||
|
||||
runs(() => atom.project.setPaths([dirOne]))
|
||||
waitsForPromise(() => atom.project.getWatcherPromise(dirOne))
|
||||
|
||||
runs(() => {
|
||||
expect(atom.project.watcherPromisesByPath[dirTwo]).toEqual(undefined)
|
||||
|
||||
fs.writeFileSync(fileThree, 'three\n')
|
||||
fs.writeFileSync(fileTwo, 'two\n')
|
||||
fs.writeFileSync(fileOne, 'one\n')
|
||||
})
|
||||
|
||||
waitsForPromise(() => waitForEvents([fileOne, fileTwo]))
|
||||
|
||||
runs(() => expect(events.some(event => event.path === fileThree)).toBeFalsy())
|
||||
})
|
||||
})
|
||||
|
||||
describe('.onDidAddBuffer()', () =>
|
||||
it('invokes the callback with added text buffers', () => {
|
||||
const buffers = []
|
||||
const added = []
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
|
||||
.then(o => buffers.push(o))
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
expect(buffers.length).toBe(1)
|
||||
atom.project.onDidAddBuffer(buffer => added.push(buffer))
|
||||
})
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
|
||||
.then(o => buffers.push(o))
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
expect(buffers.length).toBe(2)
|
||||
expect(added).toEqual([buffers[1]])
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
describe('.observeBuffers()', () =>
|
||||
it('invokes the observer with current and future text buffers', () => {
|
||||
const buffers = []
|
||||
const observed = []
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/a'))
|
||||
.then(o => buffers.push(o))
|
||||
)
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
|
||||
.then(o => buffers.push(o))
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
expect(buffers.length).toBe(2)
|
||||
atom.project.observeBuffers(buffer => observed.push(buffer))
|
||||
expect(observed).toEqual(buffers)
|
||||
})
|
||||
|
||||
waitsForPromise(() =>
|
||||
atom.project.buildBuffer(require.resolve('./fixtures/dir/b'))
|
||||
.then(o => buffers.push(o))
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
expect(observed.length).toBe(3)
|
||||
expect(buffers.length).toBe(3)
|
||||
expect(observed).toEqual(buffers)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
describe('.relativize(path)', () => {
|
||||
it('returns the path, relative to whichever root directory it is inside of', () => {
|
||||
atom.project.addPath(temp.mkdirSync('another-path'))
|
||||
|
||||
let rootPath = atom.project.getPaths()[0]
|
||||
let childPath = path.join(rootPath, 'some', 'child', 'directory')
|
||||
expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory'))
|
||||
|
||||
rootPath = atom.project.getPaths()[1]
|
||||
childPath = path.join(rootPath, 'some', 'child', 'directory')
|
||||
expect(atom.project.relativize(childPath)).toBe(path.join('some', 'child', 'directory'))
|
||||
})
|
||||
|
||||
it('returns the given path if it is not in any of the root directories', () => {
|
||||
const randomPath = path.join('some', 'random', 'path')
|
||||
expect(atom.project.relativize(randomPath)).toBe(randomPath)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.relativizePath(path)', () => {
|
||||
it('returns the root path that contains the given path, and the path relativized to that root path', () => {
|
||||
atom.project.addPath(temp.mkdirSync('another-path'))
|
||||
|
||||
let rootPath = atom.project.getPaths()[0]
|
||||
let childPath = path.join(rootPath, 'some', 'child', 'directory')
|
||||
expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')])
|
||||
|
||||
rootPath = atom.project.getPaths()[1]
|
||||
childPath = path.join(rootPath, 'some', 'child', 'directory')
|
||||
expect(atom.project.relativizePath(childPath)).toEqual([rootPath, path.join('some', 'child', 'directory')])
|
||||
})
|
||||
|
||||
describe("when the given path isn't inside of any of the project's path", () =>
|
||||
it('returns null for the root path, and the given path unchanged', () => {
|
||||
const randomPath = path.join('some', 'random', 'path')
|
||||
expect(atom.project.relativizePath(randomPath)).toEqual([null, randomPath])
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the given path is a URL', () =>
|
||||
it('returns null for the root path, and the given path unchanged', () => {
|
||||
const url = 'http://the-path'
|
||||
expect(atom.project.relativizePath(url)).toEqual([null, url])
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the given path is inside more than one root folder', () =>
|
||||
it('uses the root folder that is closest to the given path', () => {
|
||||
atom.project.addPath(path.join(atom.project.getPaths()[0], 'a-dir'))
|
||||
|
||||
const inputPath = path.join(atom.project.getPaths()[1], 'somewhere/something.txt')
|
||||
|
||||
expect(atom.project.getDirectories()[0].contains(inputPath)).toBe(true)
|
||||
expect(atom.project.getDirectories()[1].contains(inputPath)).toBe(true)
|
||||
expect(atom.project.relativizePath(inputPath)).toEqual([
|
||||
atom.project.getPaths()[1],
|
||||
path.join('somewhere', 'something.txt')
|
||||
])
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('.contains(path)', () =>
|
||||
it('returns whether or not the given path is in one of the root directories', () => {
|
||||
const rootPath = atom.project.getPaths()[0]
|
||||
const childPath = path.join(rootPath, 'some', 'child', 'directory')
|
||||
expect(atom.project.contains(childPath)).toBe(true)
|
||||
|
||||
const randomPath = path.join('some', 'random', 'path')
|
||||
expect(atom.project.contains(randomPath)).toBe(false)
|
||||
})
|
||||
)
|
||||
|
||||
describe('.resolvePath(uri)', () =>
|
||||
it('normalizes disk drive letter in passed path on #win32', () => {
|
||||
expect(atom.project.resolvePath('d:\\file.txt')).toEqual('D:\\file.txt')
|
||||
})
|
||||
)
|
||||
})
|
@ -222,7 +222,7 @@ describe("ReopenProjectMenuManager", () => {
|
||||
expect(label).toBe('https://launch.pad/apollo/11')
|
||||
})
|
||||
|
||||
it("returns a comma-seperated list of base names if there are multiple", () => {
|
||||
it("returns a comma-separated list of base names if there are multiple", () => {
|
||||
const project = { paths: [ '/var/one', '/usr/bin/two', '/etc/mission/control/three' ] }
|
||||
const label = ReopenProjectMenuManager.createLabel(project)
|
||||
expect(label).toBe('one, two, three')
|
||||
|
@ -103,6 +103,11 @@ describe "Selection", ->
|
||||
selection.insertText("\r\n", autoIndent: true)
|
||||
expect(buffer.lineForRow(2)).toBe " "
|
||||
|
||||
it "does not adjust the indent of trailing lines if preserveTrailingLineIndentation is true", ->
|
||||
selection.setBufferRange [[5, 0], [5, 0]]
|
||||
selection.insertText(' foo\n bar\n', preserveTrailingLineIndentation: true, indentBasis: 1)
|
||||
expect(buffer.lineForRow(6)).toBe(' bar')
|
||||
|
||||
describe ".fold()", ->
|
||||
it "folds the buffer range spanned by the selection", ->
|
||||
selection.setBufferRange([[0, 3], [1, 6]])
|
||||
|
@ -58,7 +58,7 @@ if specPackagePath = FindParentDir.sync(testPaths[0], 'package.json')
|
||||
if specDirectory = FindParentDir.sync(testPaths[0], 'fixtures')
|
||||
specProjectPath = path.join(specDirectory, 'fixtures')
|
||||
else
|
||||
specProjectPath = path.join(__dirname, 'fixtures')
|
||||
specProjectPath = require('os').tmpdir()
|
||||
|
||||
beforeEach ->
|
||||
atom.project.setPaths([specProjectPath])
|
||||
@ -108,10 +108,14 @@ beforeEach ->
|
||||
afterEach ->
|
||||
ensureNoDeprecatedFunctionCalls()
|
||||
ensureNoDeprecatedStylesheets()
|
||||
atom.reset()
|
||||
document.getElementById('jasmine-content').innerHTML = '' unless window.debugContent
|
||||
warnIfLeakingPathSubscriptions()
|
||||
waits(0) # yield to ui thread to make screen update more frequently
|
||||
|
||||
waitsForPromise ->
|
||||
atom.reset()
|
||||
|
||||
runs ->
|
||||
document.getElementById('jasmine-content').innerHTML = '' unless window.debugContent
|
||||
warnIfLeakingPathSubscriptions()
|
||||
waits(0) # yield to ui thread to make screen update more frequently
|
||||
|
||||
warnIfLeakingPathSubscriptions = ->
|
||||
watchedPaths = pathwatcher.getWatchedPaths()
|
||||
|
@ -1,5 +1,7 @@
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers')
|
||||
|
||||
const Random = require('../script/node_modules/random-seed')
|
||||
const {getRandomBufferRange, buildRandomLines} = require('./helpers/random')
|
||||
const TextEditorComponent = require('../src/text-editor-component')
|
||||
const TextEditorElement = require('../src/text-editor-element')
|
||||
const TextEditor = require('../src/text-editor')
|
||||
@ -12,7 +14,6 @@ const electron = require('electron')
|
||||
const clipboard = require('../src/safe-clipboard')
|
||||
|
||||
const SAMPLE_TEXT = fs.readFileSync(path.join(__dirname, 'fixtures', 'sample.js'), 'utf8')
|
||||
const NBSP_CHARACTER = '\u00a0'
|
||||
|
||||
document.registerElement('text-editor-component-test-element', {
|
||||
prototype: Object.create(HTMLElement.prototype, {
|
||||
@ -286,6 +287,31 @@ describe('TextEditorComponent', () => {
|
||||
expect(lineNumberNodeForScreenRow(component, 0).querySelector('.foldable')).toBeNull()
|
||||
})
|
||||
|
||||
it('gracefully handles folds that change the soft-wrap boundary by causing the vertical scrollbar to disappear (regression)', async () => {
|
||||
const text = ('x'.repeat(100) + '\n') + 'y\n'.repeat(28) + ' z\n'.repeat(50)
|
||||
const {component, element, editor} = buildComponent({text, height: 1000, width: 500})
|
||||
|
||||
element.addEventListener('scroll', (event) => {
|
||||
event.stopPropagation()
|
||||
}, true)
|
||||
|
||||
editor.setSoftWrapped(true)
|
||||
jasmine.attachToDOM(element)
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
const firstScreenLineLengthWithVerticalScrollbar = element.querySelector('.line').textContent.length
|
||||
|
||||
setScrollTop(component, 620)
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
editor.foldBufferRow(28)
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
const firstLineElement = element.querySelector('.line')
|
||||
expect(firstLineElement.dataset.screenRow).toBe('0')
|
||||
expect(firstLineElement.textContent.length).toBeGreaterThan(firstScreenLineLengthWithVerticalScrollbar)
|
||||
})
|
||||
|
||||
it('shows the foldable icon on the last screen row of a buffer row that can be folded', async () => {
|
||||
const {component, element, editor} = buildComponent({text: 'abc\n de\nfghijklm\n no', softWrapped: true})
|
||||
await setEditorWidthInCharacters(component, 5)
|
||||
@ -378,35 +404,50 @@ describe('TextEditorComponent', () => {
|
||||
expect(horizontalScrollbar.style.visibility).toBe('')
|
||||
})
|
||||
|
||||
it('updates the bottom/right of dummy scrollbars and client height/width measurements without forgetting the previous scroll top/left when scrollbar styles change', async () => {
|
||||
const {component, element, editor} = buildComponent({height: 100, width: 100})
|
||||
expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(10)
|
||||
expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(10)
|
||||
setScrollTop(component, 20)
|
||||
setScrollLeft(component, 10)
|
||||
await component.getNextUpdatePromise()
|
||||
describe('when scrollbar styles change or the editor element is detached and then reattached', () => {
|
||||
it('updates the bottom/right of dummy scrollbars and client height/width measurements', async () => {
|
||||
const {component, element, editor} = buildComponent({height: 100, width: 100})
|
||||
expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(10)
|
||||
expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(10)
|
||||
setScrollTop(component, 20)
|
||||
setScrollLeft(component, 10)
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
const style = document.createElement('style')
|
||||
style.textContent = '::-webkit-scrollbar { height: 10px; width: 10px; }'
|
||||
jasmine.attachToDOM(style)
|
||||
// Updating scrollbar styles.
|
||||
const style = document.createElement('style')
|
||||
style.textContent = '::-webkit-scrollbar { height: 10px; width: 10px; }'
|
||||
jasmine.attachToDOM(style)
|
||||
TextEditor.didUpdateScrollbarStyles()
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
TextEditor.didUpdateScrollbarStyles()
|
||||
await component.getNextUpdatePromise()
|
||||
expect(getHorizontalScrollbarHeight(component)).toBe(10)
|
||||
expect(getVerticalScrollbarWidth(component)).toBe(10)
|
||||
expect(component.refs.horizontalScrollbar.element.style.right).toBe('10px')
|
||||
expect(component.refs.verticalScrollbar.element.style.bottom).toBe('10px')
|
||||
expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(10)
|
||||
expect(component.refs.verticalScrollbar.element.scrollTop).toBe(20)
|
||||
expect(component.getScrollContainerClientHeight()).toBe(100 - 10)
|
||||
expect(component.getScrollContainerClientWidth()).toBe(100 - component.getGutterContainerWidth() - 10)
|
||||
|
||||
expect(getHorizontalScrollbarHeight(component)).toBe(10)
|
||||
expect(getVerticalScrollbarWidth(component)).toBe(10)
|
||||
expect(component.refs.horizontalScrollbar.element.style.right).toBe('10px')
|
||||
expect(component.refs.verticalScrollbar.element.style.bottom).toBe('10px')
|
||||
expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(10)
|
||||
expect(component.refs.verticalScrollbar.element.scrollTop).toBe(20)
|
||||
expect(component.getScrollContainerClientHeight()).toBe(100 - 10)
|
||||
expect(component.getScrollContainerClientWidth()).toBe(100 - component.getGutterContainerWidth() - 10)
|
||||
// Detaching and re-attaching the editor element.
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
// Ensure we don't throw an error trying to remeasure non-existent scrollbars for mini editors.
|
||||
await editor.update({mini: true})
|
||||
TextEditor.didUpdateScrollbarStyles()
|
||||
component.scheduleUpdate()
|
||||
await component.getNextUpdatePromise()
|
||||
expect(getHorizontalScrollbarHeight(component)).toBe(10)
|
||||
expect(getVerticalScrollbarWidth(component)).toBe(10)
|
||||
expect(component.refs.horizontalScrollbar.element.style.right).toBe('10px')
|
||||
expect(component.refs.verticalScrollbar.element.style.bottom).toBe('10px')
|
||||
expect(component.refs.horizontalScrollbar.element.scrollLeft).toBe(10)
|
||||
expect(component.refs.verticalScrollbar.element.scrollTop).toBe(20)
|
||||
expect(component.getScrollContainerClientHeight()).toBe(100 - 10)
|
||||
expect(component.getScrollContainerClientWidth()).toBe(100 - component.getGutterContainerWidth() - 10)
|
||||
|
||||
// Ensure we don't throw an error trying to remeasure non-existent scrollbars for mini editors.
|
||||
await editor.update({mini: true})
|
||||
TextEditor.didUpdateScrollbarStyles()
|
||||
component.scheduleUpdate()
|
||||
await component.getNextUpdatePromise()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders cursors within the visible row range', async () => {
|
||||
@ -854,6 +895,97 @@ describe('TextEditorComponent', () => {
|
||||
expect(component.getGutterContainerWidth()).toBe(originalGutterContainerWidth)
|
||||
expect(component.getLineNumberGutterWidth()).toBe(originalLineNumberGutterWidth)
|
||||
})
|
||||
|
||||
describe('randomized tests', () => {
|
||||
let originalTimeout
|
||||
|
||||
beforeEach(() => {
|
||||
originalTimeout = jasmine.getEnv().defaultTimeoutInterval
|
||||
jasmine.getEnv().defaultTimeoutInterval = 60 * 1000
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.getEnv().defaultTimeoutInterval = originalTimeout
|
||||
})
|
||||
|
||||
it('renders the visible rows correctly after randomly mutating the editor', async () => {
|
||||
const initialSeed = Date.now()
|
||||
for (var i = 0; i < 20; i++) {
|
||||
let seed = initialSeed + i
|
||||
// seed = 1507224195357
|
||||
const failureMessage = 'Randomized test failed with seed: ' + seed
|
||||
const random = Random(seed)
|
||||
|
||||
const rowsPerTile = random.intBetween(1, 6)
|
||||
const {component, element, editor} = buildComponent({rowsPerTile, autoHeight: false})
|
||||
editor.setSoftWrapped(Boolean(random(2)))
|
||||
await setEditorWidthInCharacters(component, random(20))
|
||||
await setEditorHeightInLines(component, random(10))
|
||||
element.focus()
|
||||
|
||||
for (var j = 0; j < 5; j++) {
|
||||
const k = random(100)
|
||||
const range = getRandomBufferRange(random, editor.buffer)
|
||||
|
||||
if (k < 10) {
|
||||
editor.setSoftWrapped(!editor.isSoftWrapped())
|
||||
} else if (k < 15) {
|
||||
if (random(2)) setEditorWidthInCharacters(component, random(20))
|
||||
if (random(2)) setEditorHeightInLines(component, random(10))
|
||||
} else if (k < 40) {
|
||||
editor.setSelectedBufferRange(range)
|
||||
editor.backspace()
|
||||
} else if (k < 80) {
|
||||
const linesToInsert = buildRandomLines(random, 5)
|
||||
editor.setCursorBufferPosition(range.start)
|
||||
editor.insertText(linesToInsert)
|
||||
} else if (k < 90) {
|
||||
if (random(2)) {
|
||||
editor.foldBufferRange(range)
|
||||
} else {
|
||||
editor.destroyFoldsIntersectingBufferRange(range)
|
||||
}
|
||||
} else if (k < 95) {
|
||||
editor.setSelectedBufferRange(range)
|
||||
} else {
|
||||
if (random(2)) component.setScrollTop(random(component.getScrollHeight()))
|
||||
if (random(2)) component.setScrollLeft(random(component.getScrollWidth()))
|
||||
}
|
||||
|
||||
component.scheduleUpdate()
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
const renderedLines = queryOnScreenLineElements(element).sort((a, b) => a.dataset.screenRow - b.dataset.screenRow)
|
||||
const renderedLineNumbers = queryOnScreenLineNumberElements(element).sort((a, b) => a.dataset.screenRow - b.dataset.screenRow)
|
||||
const renderedStartRow = component.getRenderedStartRow()
|
||||
const expectedLines = editor.displayLayer.getScreenLines(renderedStartRow, component.getRenderedEndRow())
|
||||
|
||||
expect(renderedLines.length).toBe(expectedLines.length, failureMessage)
|
||||
expect(renderedLineNumbers.length).toBe(expectedLines.length, failureMessage)
|
||||
for (let k = 0; k < renderedLines.length; k++) {
|
||||
const expectedLine = expectedLines[k]
|
||||
const expectedText = expectedLine.lineText || ' '
|
||||
|
||||
const renderedLine = renderedLines[k]
|
||||
const renderedLineNumber = renderedLineNumbers[k]
|
||||
let renderedText = renderedLine.textContent
|
||||
// We append zero width NBSPs after folds at the end of the
|
||||
// line in order to support measurement.
|
||||
if (expectedText.endsWith(editor.displayLayer.foldCharacter)) {
|
||||
renderedText = renderedText.substring(0, renderedText.length - 1)
|
||||
}
|
||||
|
||||
expect(renderedText).toBe(expectedText, failureMessage)
|
||||
expect(parseInt(renderedLine.dataset.screenRow)).toBe(renderedStartRow + k, failureMessage)
|
||||
expect(parseInt(renderedLineNumber.dataset.screenRow)).toBe(renderedStartRow + k, failureMessage)
|
||||
}
|
||||
}
|
||||
|
||||
element.remove()
|
||||
editor.destroy()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mini editors', () => {
|
||||
@ -1142,7 +1274,7 @@ describe('TextEditorComponent', () => {
|
||||
expect(component.getScrollTopRow()).toBe(4)
|
||||
expect(component.getScrollTop()).toBe(Math.round(4 * component.getLineHeight()))
|
||||
|
||||
// Preserves the scrollTopRow when sdetached
|
||||
// Preserves the scrollTopRow when detached
|
||||
element.remove()
|
||||
expect(component.getScrollTopRow()).toBe(4)
|
||||
expect(component.getScrollTop()).toBe(Math.round(4 * component.getLineHeight()))
|
||||
@ -1601,7 +1733,7 @@ describe('TextEditorComponent', () => {
|
||||
const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'a'})
|
||||
decoration.flash('b', 10)
|
||||
|
||||
// Flash on initial appearence of highlight
|
||||
// Flash on initial appearance of highlight
|
||||
await component.getNextUpdatePromise()
|
||||
const highlights = element.querySelectorAll('.highlight.a')
|
||||
expect(highlights.length).toBe(1)
|
||||
@ -1764,6 +1896,8 @@ describe('TextEditorComponent', () => {
|
||||
const decoration = editor.decorateMarker(marker, {type: 'overlay', item: overlayElement, class: 'a'})
|
||||
await component.getNextUpdatePromise()
|
||||
|
||||
const overlayComponent = component.overlayComponents.values().next().value
|
||||
|
||||
const overlayWrapper = overlayElement.parentElement
|
||||
expect(overlayWrapper.classList.contains('a')).toBe(true)
|
||||
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
|
||||
@ -1794,12 +1928,12 @@ describe('TextEditorComponent', () => {
|
||||
await setScrollTop(component, 20)
|
||||
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
|
||||
overlayElement.style.height = 60 + 'px'
|
||||
await component.getNextUpdatePromise()
|
||||
await overlayComponent.getNextUpdatePromise()
|
||||
expect(overlayWrapper.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 4))
|
||||
|
||||
// Does not flip the overlay vertically if it would overflow the top of the window
|
||||
overlayElement.style.height = 80 + 'px'
|
||||
await component.getNextUpdatePromise()
|
||||
await overlayComponent.getNextUpdatePromise()
|
||||
expect(overlayWrapper.getBoundingClientRect().top).toBe(clientTopForLine(component, 5))
|
||||
|
||||
// Can update overlay wrapper class
|
||||
@ -2284,6 +2418,27 @@ describe('TextEditorComponent', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('removes block decorations whose markers have been destroyed', async () => {
|
||||
const {editor, component, element} = buildComponent({rowsPerTile: 3})
|
||||
const {marker} = createBlockDecorationAtScreenRow(editor, 2, {height: 5, position: 'before'})
|
||||
await component.getNextUpdatePromise()
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
assertTilesAreSizedAndPositionedCorrectly(component, [
|
||||
{tileStartRow: 0, height: 3 * component.getLineHeight() + 5},
|
||||
{tileStartRow: 3, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 6, height: 3 * component.getLineHeight()}
|
||||
])
|
||||
|
||||
marker.destroy()
|
||||
await component.getNextUpdatePromise()
|
||||
assertLinesAreAlignedWithLineNumbers(component)
|
||||
assertTilesAreSizedAndPositionedCorrectly(component, [
|
||||
{tileStartRow: 0, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 3, height: 3 * component.getLineHeight()},
|
||||
{tileStartRow: 6, height: 3 * component.getLineHeight()}
|
||||
])
|
||||
})
|
||||
|
||||
it('removes block decorations whose markers are invalidated, and adds them back when they become valid again', async () => {
|
||||
const editor = buildEditor({rowsPerTile: 3, autoHeight: false})
|
||||
const {item, decoration, marker} = createBlockDecorationAtScreenRow(editor, 3, {height: 44, position: 'before', invalidate: 'touch'})
|
||||
@ -2388,6 +2543,49 @@ describe('TextEditorComponent', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('does not throw exceptions when destroying a block decoration inside a marker change event (regression)', async () => {
|
||||
const {editor, component} = buildComponent({rowsPerTile: 3})
|
||||
|
||||
const marker = editor.markScreenPosition([2, 0])
|
||||
marker.onDidChange(() => { marker.destroy() })
|
||||
const item = document.createElement('div')
|
||||
editor.decorateMarker(marker, {type: 'block', item})
|
||||
|
||||
await component.getNextUpdatePromise()
|
||||
expect(item.nextSibling).toBe(lineNodeForScreenRow(component, 2))
|
||||
|
||||
marker.setBufferRange([[0, 0], [0, 0]])
|
||||
expect(marker.isDestroyed()).toBe(true)
|
||||
|
||||
await component.getNextUpdatePromise()
|
||||
expect(item.parentElement).toBeNull()
|
||||
})
|
||||
|
||||
it('does not attempt to render block decorations located outside the visible range', async () => {
|
||||
const {editor, component} = buildComponent({autoHeight: false, rowsPerTile: 2})
|
||||
await setEditorHeightInLines(component, 2)
|
||||
expect(component.getRenderedStartRow()).toBe(0)
|
||||
expect(component.getRenderedEndRow()).toBe(4)
|
||||
|
||||
const marker1 = editor.markScreenRange([[3, 0], [5, 0]], {reversed: false})
|
||||
const item1 = document.createElement('div')
|
||||
editor.decorateMarker(marker1, {type: 'block', item: item1})
|
||||
|
||||
const marker2 = editor.markScreenRange([[3, 0], [5, 0]], {reversed: true})
|
||||
const item2 = document.createElement('div')
|
||||
editor.decorateMarker(marker2, {type: 'block', item: item2})
|
||||
|
||||
await component.getNextUpdatePromise()
|
||||
expect(item1.parentElement).toBeNull()
|
||||
expect(item2.nextSibling).toBe(lineNodeForScreenRow(component, 3))
|
||||
|
||||
await setScrollTop(component, 4 * component.getLineHeight())
|
||||
expect(component.getRenderedStartRow()).toBe(4)
|
||||
expect(component.getRenderedEndRow()).toBe(8)
|
||||
expect(item1.nextSibling).toBe(lineNodeForScreenRow(component, 5))
|
||||
expect(item2.parentElement).toBeNull()
|
||||
})
|
||||
|
||||
it('measures block decorations correctly when they are added before the component width has been updated', async () => {
|
||||
{
|
||||
const {editor, component, element} = buildComponent({autoHeight: false, width: 500, attach: false})
|
||||
@ -2706,6 +2904,8 @@ describe('TextEditorComponent', () => {
|
||||
clientY: clientTopForLine(component, 3) + lineHeight / 2
|
||||
})
|
||||
expect(editor.getCursorScreenPosition()).toEqual([3, 16])
|
||||
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('selects words on double-click', () => {
|
||||
@ -2714,6 +2914,7 @@ describe('TextEditorComponent', () => {
|
||||
component.didMouseDownOnContent({detail: 1, button: 0, clientX, clientY})
|
||||
component.didMouseDownOnContent({detail: 2, button: 0, clientX, clientY})
|
||||
expect(editor.getSelectedScreenRange()).toEqual([[1, 13], [1, 21]])
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('selects lines on triple-click', () => {
|
||||
@ -2723,6 +2924,7 @@ describe('TextEditorComponent', () => {
|
||||
component.didMouseDownOnContent({detail: 2, button: 0, clientX, clientY})
|
||||
component.didMouseDownOnContent({detail: 3, button: 0, clientX, clientY})
|
||||
expect(editor.getSelectedScreenRange()).toEqual([[1, 0], [2, 0]])
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('adds or removes cursors when holding cmd or ctrl when single-clicking', () => {
|
||||
@ -2760,7 +2962,7 @@ describe('TextEditorComponent', () => {
|
||||
expect(editor.getCursorScreenPositions()).toEqual([[1, 16]])
|
||||
|
||||
// cmd-clicking within a selection destroys it
|
||||
editor.addSelectionForScreenRange([[2, 10], [2, 15]])
|
||||
editor.addSelectionForScreenRange([[2, 10], [2, 15]], {autoscroll: false})
|
||||
expect(editor.getSelectedScreenRanges()).toEqual([
|
||||
[[1, 16], [1, 16]],
|
||||
[[2, 10], [2, 15]]
|
||||
@ -2790,7 +2992,7 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
// ctrl-click adds cursors on platforms *other* than macOS
|
||||
component.props.platform = 'win32'
|
||||
editor.setCursorScreenPosition([1, 4])
|
||||
editor.setCursorScreenPosition([1, 4], {autoscroll: false})
|
||||
component.didMouseDownOnContent(
|
||||
Object.assign(clientPositionForCharacter(component, 1, 16), {
|
||||
detail: 1,
|
||||
@ -2799,11 +3001,13 @@ describe('TextEditorComponent', () => {
|
||||
})
|
||||
)
|
||||
expect(editor.getCursorScreenPositions()).toEqual([[1, 4], [1, 16]])
|
||||
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('adds word selections when holding cmd or ctrl when double-clicking', () => {
|
||||
const {component, editor} = buildComponent()
|
||||
editor.addCursorAtScreenPosition([1, 16])
|
||||
editor.addCursorAtScreenPosition([1, 16], {autoscroll: false})
|
||||
expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]])
|
||||
|
||||
component.didMouseDownOnContent(
|
||||
@ -2824,11 +3028,12 @@ describe('TextEditorComponent', () => {
|
||||
[[0, 0], [0, 0]],
|
||||
[[1, 13], [1, 21]]
|
||||
])
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('adds line selections when holding cmd or ctrl when triple-clicking', () => {
|
||||
const {component, editor} = buildComponent()
|
||||
editor.addCursorAtScreenPosition([1, 16])
|
||||
editor.addCursorAtScreenPosition([1, 16], {autoscroll: false})
|
||||
expect(editor.getCursorScreenPositions()).toEqual([[0, 0], [1, 16]])
|
||||
|
||||
const {clientX, clientY} = clientPositionForCharacter(component, 1, 16)
|
||||
@ -2840,12 +3045,13 @@ describe('TextEditorComponent', () => {
|
||||
[[0, 0], [0, 0]],
|
||||
[[1, 0], [2, 0]]
|
||||
])
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('expands the last selection on shift-click', () => {
|
||||
const {component, element, editor} = buildComponent()
|
||||
|
||||
editor.setCursorScreenPosition([2, 18])
|
||||
editor.setCursorScreenPosition([2, 18], {autoscroll: false})
|
||||
component.didMouseDownOnContent(Object.assign({
|
||||
detail: 1,
|
||||
button: 0,
|
||||
@ -2862,8 +3068,8 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
// reorients word-wise selections to keep the word selected regardless of
|
||||
// where the subsequent shift-click occurs
|
||||
editor.setCursorScreenPosition([2, 18])
|
||||
editor.getLastSelection().selectWord()
|
||||
editor.setCursorScreenPosition([2, 18], {autoscroll: false})
|
||||
editor.getLastSelection().selectWord({autoscroll: false})
|
||||
component.didMouseDownOnContent(Object.assign({
|
||||
detail: 1,
|
||||
button: 0,
|
||||
@ -2880,8 +3086,8 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
// reorients line-wise selections to keep the word selected regardless of
|
||||
// where the subsequent shift-click occurs
|
||||
editor.setCursorScreenPosition([2, 18])
|
||||
editor.getLastSelection().selectLine()
|
||||
editor.setCursorScreenPosition([2, 18], {autoscroll: false})
|
||||
editor.getLastSelection().selectLine(null, {autoscroll: false})
|
||||
component.didMouseDownOnContent(Object.assign({
|
||||
detail: 1,
|
||||
button: 0,
|
||||
@ -2895,6 +3101,8 @@ describe('TextEditorComponent', () => {
|
||||
shiftKey: true
|
||||
}, clientPositionForCharacter(component, 3, 11)))
|
||||
expect(editor.getSelectedScreenRange()).toEqual([[2, 0], [4, 0]])
|
||||
|
||||
expect(editor.testAutoscrollRequests).toEqual([])
|
||||
})
|
||||
|
||||
it('expands the last selection on drag', () => {
|
||||
@ -3272,9 +3480,9 @@ describe('TextEditorComponent', () => {
|
||||
await component.getNextUpdatePromise()
|
||||
expect(editor.isFoldedAtScreenRow(5)).toBe(true)
|
||||
|
||||
target = element.querySelectorAll('.line-number')[6].querySelector('.icon-right')
|
||||
component.didMouseDownOnLineNumberGutter({target, button: 0, clientY: clientTopForLine(component, 5)})
|
||||
expect(editor.isFoldedAtScreenRow(5)).toBe(false)
|
||||
target = element.querySelectorAll('.line-number')[4].querySelector('.icon-right')
|
||||
component.didMouseDownOnLineNumberGutter({target, button: 0, clientY: clientTopForLine(component, 4)})
|
||||
expect(editor.isFoldedAtScreenRow(4)).toBe(false)
|
||||
})
|
||||
|
||||
it('autoscrolls when dragging near the top or bottom of the gutter', async () => {
|
||||
@ -4216,7 +4424,7 @@ describe('TextEditorComponent', () => {
|
||||
expect(dragEvents).toEqual([])
|
||||
})
|
||||
|
||||
it('calls `didStopDragging` if the buffer changes while dragging', async () => {
|
||||
it('calls `didStopDragging` if the user interacts with the keyboard while dragging', async () => {
|
||||
const {component, editor} = buildComponent()
|
||||
|
||||
let dragging = false
|
||||
@ -4229,8 +4437,14 @@ describe('TextEditorComponent', () => {
|
||||
await getNextAnimationFramePromise()
|
||||
expect(dragging).toBe(true)
|
||||
|
||||
editor.delete()
|
||||
// Buffer changes don't cause dragging to be stopped.
|
||||
editor.insertText('X')
|
||||
expect(dragging).toBe(true)
|
||||
|
||||
// Keyboard interaction prevents users from dragging further.
|
||||
component.didKeydown({code: 'KeyX'})
|
||||
expect(dragging).toBe(false)
|
||||
|
||||
window.dispatchEvent(new MouseEvent('mousemove'))
|
||||
await getNextAnimationFramePromise()
|
||||
expect(dragging).toBe(false)
|
||||
@ -4250,7 +4464,10 @@ function buildEditor (params = {}) {
|
||||
for (const paramName of ['mini', 'autoHeight', 'autoWidth', 'lineNumberGutterVisible', 'showLineNumbers', 'placeholderText', 'softWrapped', 'scrollSensitivity']) {
|
||||
if (params[paramName] != null) editorParams[paramName] = params[paramName]
|
||||
}
|
||||
return new TextEditor(editorParams)
|
||||
const editor = new TextEditor(editorParams)
|
||||
editor.testAutoscrollRequests = []
|
||||
editor.onDidRequestAutoscroll((request) => { editor.testAutoscrollRequests.push(request) })
|
||||
return editor
|
||||
}
|
||||
|
||||
function buildComponent (params = {}) {
|
||||
|
@ -544,6 +544,21 @@ describe('TextEditorRegistry', function () {
|
||||
expect(editor.getSoftWrapColumn()).toBe(80)
|
||||
})
|
||||
|
||||
it('allows for custom definition of maximum soft wrap based on config', async function () {
|
||||
editor.update({
|
||||
softWrapped: false,
|
||||
maxScreenLineLength: 1500,
|
||||
})
|
||||
|
||||
expect(editor.getSoftWrapColumn()).toBe(1500)
|
||||
|
||||
atom.config.set('editor.softWrap', false)
|
||||
atom.config.set('editor.maxScreenLineLength', 500)
|
||||
registry.maintainConfig(editor)
|
||||
await initialPackageActivation
|
||||
expect(editor.getSoftWrapColumn()).toBe(500)
|
||||
})
|
||||
|
||||
it('sets the preferred line length based on the config', async function () {
|
||||
editor.update({preferredLineLength: 80})
|
||||
expect(editor.getPreferredLineLength()).toBe(80)
|
||||
@ -685,7 +700,7 @@ describe('TextEditorRegistry', function () {
|
||||
registry.setGrammarOverride(editor, 'source.c')
|
||||
registry.setGrammarOverride(editor2, 'source.js')
|
||||
|
||||
atom.packages.deactivatePackage('language-javascript')
|
||||
await atom.packages.deactivatePackage('language-javascript')
|
||||
|
||||
const editorCopy = TextEditor.deserialize(editor.serialize(), atom)
|
||||
const editor2Copy = TextEditor.deserialize(editor2.serialize(), atom)
|
||||
|
@ -74,6 +74,16 @@ describe "TextEditor", ->
|
||||
expect(editor2.getInvisibles()).toEqual(editor.getInvisibles())
|
||||
expect(editor2.getEditorWidthInChars()).toBe(editor.getEditorWidthInChars())
|
||||
expect(editor2.displayLayer.tabLength).toBe(editor2.getTabLength())
|
||||
expect(editor2.displayLayer.softWrapColumn).toBe(editor2.getSoftWrapColumn())
|
||||
|
||||
it "ignores buffers with retired IDs", ->
|
||||
editor2 = TextEditor.deserialize(editor.serialize(), {
|
||||
assert: atom.assert,
|
||||
textEditors: atom.textEditors,
|
||||
project: {bufferForIdSync: -> null}
|
||||
})
|
||||
|
||||
expect(editor2).toBeNull()
|
||||
|
||||
describe "when the editor is constructed with the largeFileMode option set to true", ->
|
||||
it "loads the editor but doesn't tokenize", ->
|
||||
@ -145,7 +155,7 @@ describe "TextEditor", ->
|
||||
returnedPromise = editor.update({
|
||||
tabLength: 6, softTabs: false, softWrapped: true, editorWidthInChars: 40,
|
||||
showInvisibles: false, mini: false, lineNumberGutterVisible: false, scrollPastEnd: true,
|
||||
autoHeight: false
|
||||
autoHeight: false, maxScreenLineLength: 1000
|
||||
})
|
||||
|
||||
expect(returnedPromise).toBe(element.component.getNextUpdatePromise())
|
||||
@ -620,7 +630,7 @@ describe "TextEditor", ->
|
||||
expect(editor.getCursorBufferPosition()).toEqual [0, 0]
|
||||
|
||||
describe ".moveToBottom()", ->
|
||||
it "moves the cusor to the bottom of the buffer", ->
|
||||
it "moves the cursor to the bottom of the buffer", ->
|
||||
editor.setCursorScreenPosition [0, 0]
|
||||
editor.addCursorAtScreenPosition [1, 0]
|
||||
editor.moveToBottom()
|
||||
@ -1158,6 +1168,58 @@ describe "TextEditor", ->
|
||||
editor.setCursorBufferPosition([3, 1])
|
||||
expect(editor.getCurrentParagraphBufferRange()).toBeUndefined()
|
||||
|
||||
it 'will limit paragraph range to comments', ->
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-javascript')
|
||||
|
||||
runs ->
|
||||
editor.setGrammar(atom.grammars.grammarForScopeName('source.js'))
|
||||
editor.setText("""
|
||||
var quicksort = function () {
|
||||
/* Single line comment block */
|
||||
var sort = function(items) {};
|
||||
|
||||
/*
|
||||
A multiline
|
||||
comment is here
|
||||
*/
|
||||
var sort = function(items) {};
|
||||
|
||||
// A comment
|
||||
//
|
||||
// Multiple comment
|
||||
// lines
|
||||
var sort = function(items) {};
|
||||
// comment line after fn
|
||||
|
||||
var nosort = function(items) {
|
||||
item;
|
||||
}
|
||||
|
||||
};
|
||||
""")
|
||||
|
||||
paragraphBufferRangeForRow = (row) ->
|
||||
editor.setCursorBufferPosition([row, 0])
|
||||
editor.getLastCursor().getCurrentParagraphBufferRange()
|
||||
|
||||
expect(paragraphBufferRangeForRow(0)).toEqual([[0, 0], [0, 29]])
|
||||
expect(paragraphBufferRangeForRow(1)).toEqual([[1, 0], [1, 33]])
|
||||
expect(paragraphBufferRangeForRow(2)).toEqual([[2, 0], [2, 32]])
|
||||
expect(paragraphBufferRangeForRow(3)).toBeFalsy()
|
||||
expect(paragraphBufferRangeForRow(4)).toEqual([[4, 0], [7, 4]])
|
||||
expect(paragraphBufferRangeForRow(5)).toEqual([[4, 0], [7, 4]])
|
||||
expect(paragraphBufferRangeForRow(6)).toEqual([[4, 0], [7, 4]])
|
||||
expect(paragraphBufferRangeForRow(7)).toEqual([[4, 0], [7, 4]])
|
||||
expect(paragraphBufferRangeForRow(8)).toEqual([[8, 0], [8, 32]])
|
||||
expect(paragraphBufferRangeForRow(9)).toBeFalsy()
|
||||
expect(paragraphBufferRangeForRow(10)).toEqual([[10, 0], [13, 10]])
|
||||
expect(paragraphBufferRangeForRow(11)).toEqual([[10, 0], [13, 10]])
|
||||
expect(paragraphBufferRangeForRow(12)).toEqual([[10, 0], [13, 10]])
|
||||
expect(paragraphBufferRangeForRow(14)).toEqual([[14, 0], [14, 32]])
|
||||
expect(paragraphBufferRangeForRow(15)).toEqual([[15, 0], [15, 26]])
|
||||
expect(paragraphBufferRangeForRow(18)).toEqual([[17, 0], [19, 3]])
|
||||
|
||||
describe "getCursorAtScreenPosition(screenPosition)", ->
|
||||
it "returns the cursor at the given screenPosition", ->
|
||||
cursor1 = editor.addCursorAtScreenPosition([0, 2])
|
||||
@ -1364,7 +1426,7 @@ describe "TextEditor", ->
|
||||
expect(selections[0].getScreenRange()).toEqual [[3, 0], [10, 0]]
|
||||
|
||||
describe ".selectToBeginningOfPreviousParagraph()", ->
|
||||
it "selects from the cursor to the first line of the pevious paragraph", ->
|
||||
it "selects from the cursor to the first line of the previous paragraph", ->
|
||||
editor.setSelectedBufferRange([[3, 0], [4, 5]])
|
||||
editor.addCursorAtScreenPosition([5, 6])
|
||||
editor.selectToScreenPosition([6, 2])
|
||||
@ -1397,7 +1459,7 @@ describe "TextEditor", ->
|
||||
expect(selection1.isReversed()).toBeTruthy()
|
||||
|
||||
describe ".selectToTop()", ->
|
||||
it "selects text from cusor position to the top of the buffer", ->
|
||||
it "selects text from cursor position to the top of the buffer", ->
|
||||
editor.setCursorScreenPosition [11, 2]
|
||||
editor.addCursorAtScreenPosition [10, 0]
|
||||
editor.selectToTop()
|
||||
@ -1407,7 +1469,7 @@ describe "TextEditor", ->
|
||||
expect(editor.getLastSelection().isReversed()).toBeTruthy()
|
||||
|
||||
describe ".selectToBottom()", ->
|
||||
it "selects text from cusor position to the bottom of the buffer", ->
|
||||
it "selects text from cursor position to the bottom of the buffer", ->
|
||||
editor.setCursorScreenPosition [10, 0]
|
||||
editor.addCursorAtScreenPosition [9, 3]
|
||||
editor.selectToBottom()
|
||||
@ -1422,7 +1484,7 @@ describe "TextEditor", ->
|
||||
expect(editor.getLastSelection().getBufferRange()).toEqual buffer.getRange()
|
||||
|
||||
describe ".selectToBeginningOfLine()", ->
|
||||
it "selects text from cusor position to beginning of line", ->
|
||||
it "selects text from cursor position to beginning of line", ->
|
||||
editor.setCursorScreenPosition [12, 2]
|
||||
editor.addCursorAtScreenPosition [11, 3]
|
||||
|
||||
@ -1441,7 +1503,7 @@ describe "TextEditor", ->
|
||||
expect(selection2.isReversed()).toBeTruthy()
|
||||
|
||||
describe ".selectToEndOfLine()", ->
|
||||
it "selects text from cusor position to end of line", ->
|
||||
it "selects text from cursor position to end of line", ->
|
||||
editor.setCursorScreenPosition [12, 0]
|
||||
editor.addCursorAtScreenPosition [11, 3]
|
||||
|
||||
@ -1483,7 +1545,7 @@ describe "TextEditor", ->
|
||||
expect(editor.getSelectedBufferRange()).toEqual [[1, 0], [4, 0]]
|
||||
|
||||
describe ".selectToBeginningOfWord()", ->
|
||||
it "selects text from cusor position to beginning of word", ->
|
||||
it "selects text from cursor position to beginning of word", ->
|
||||
editor.setCursorScreenPosition [0, 13]
|
||||
editor.addCursorAtScreenPosition [3, 49]
|
||||
|
||||
@ -1502,7 +1564,7 @@ describe "TextEditor", ->
|
||||
expect(selection2.isReversed()).toBeTruthy()
|
||||
|
||||
describe ".selectToEndOfWord()", ->
|
||||
it "selects text from cusor position to end of word", ->
|
||||
it "selects text from cursor position to end of word", ->
|
||||
editor.setCursorScreenPosition [0, 4]
|
||||
editor.addCursorAtScreenPosition [3, 48]
|
||||
|
||||
@ -1521,7 +1583,7 @@ describe "TextEditor", ->
|
||||
expect(selection2.isReversed()).toBeFalsy()
|
||||
|
||||
describe ".selectToBeginningOfNextWord()", ->
|
||||
it "selects text from cusor position to beginning of next word", ->
|
||||
it "selects text from cursor position to beginning of next word", ->
|
||||
editor.setCursorScreenPosition [0, 4]
|
||||
editor.addCursorAtScreenPosition [3, 48]
|
||||
|
||||
@ -1800,7 +1862,7 @@ describe "TextEditor", ->
|
||||
editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 3], [5, 5]]])
|
||||
expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [3, 3]], [[3, 3], [5, 5]]]
|
||||
|
||||
it "recyles existing selection instances", ->
|
||||
it "recycles existing selection instances", ->
|
||||
selection = editor.getLastSelection()
|
||||
editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[4, 4], [5, 5]]])
|
||||
|
||||
@ -1849,7 +1911,7 @@ describe "TextEditor", ->
|
||||
editor.setSelectedBufferRanges([[[2, 2], [3, 3]], [[3, 0], [5, 5]]])
|
||||
expect(editor.getSelectedBufferRanges()).toEqual [[[2, 2], [5, 5]]]
|
||||
|
||||
it "recyles existing selection instances", ->
|
||||
it "recycles existing selection instances", ->
|
||||
selection = editor.getLastSelection()
|
||||
editor.setSelectedScreenRanges([[[2, 2], [3, 4]], [[4, 4], [5, 5]]])
|
||||
|
||||
@ -2258,7 +2320,7 @@ describe "TextEditor", ->
|
||||
|
||||
|
||||
describe "when the preceding row consists of folded code", ->
|
||||
it "moves the line above the folded row and preseveres the correct folds", ->
|
||||
it "moves the line above the folded row and perseveres the correct folds", ->
|
||||
expect(editor.lineTextForBufferRow(8)).toBe " return sort(left).concat(pivot).concat(sort(right));"
|
||||
expect(editor.lineTextForBufferRow(9)).toBe " };"
|
||||
|
||||
@ -3517,7 +3579,7 @@ describe "TextEditor", ->
|
||||
expect(buffer.lineForRow(1)).toBe ' var sort = function(items) { if (items.length <= 1) return items;'
|
||||
|
||||
describe "when text is selected", ->
|
||||
it "still deletes all text to begginning of the line", ->
|
||||
it "still deletes all text to beginning of the line", ->
|
||||
editor.setSelectedBufferRanges([[[1, 24], [1, 27]], [[2, 0], [2, 4]]])
|
||||
editor.deleteToBeginningOfLine()
|
||||
expect(buffer.lineForRow(1)).toBe 'ems) {'
|
||||
@ -3704,7 +3766,7 @@ describe "TextEditor", ->
|
||||
describe "when autoIndent is enabled", ->
|
||||
describe "when the cursor's column is less than the suggested level of indentation", ->
|
||||
describe "when 'softTabs' is true (the default)", ->
|
||||
it "moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentaion", ->
|
||||
it "moves the cursor to the end of the leading whitespace and inserts enough whitespace to bring the line to the suggested level of indentation", ->
|
||||
buffer.insert([5, 0], " \n")
|
||||
editor.setCursorBufferPosition [5, 0]
|
||||
editor.indent(autoIndent: true)
|
||||
@ -3727,7 +3789,7 @@ describe "TextEditor", ->
|
||||
expect(buffer.lineForRow(13).length).toBe 8
|
||||
|
||||
describe "when 'softTabs' is false", ->
|
||||
it "moves the cursor to the end of the leading whitespace and inserts enough tabs to bring the line to the suggested level of indentaion", ->
|
||||
it "moves the cursor to the end of the leading whitespace and inserts enough tabs to bring the line to the suggested level of indentation", ->
|
||||
convertToHardTabs(buffer)
|
||||
editor.setSoftTabs(false)
|
||||
buffer.insert([5, 0], "\t\n")
|
||||
@ -4160,6 +4222,19 @@ describe "TextEditor", ->
|
||||
expect(editor.lineTextForBufferRow(3)).toBe(" if (items.length <= 1) return items;")
|
||||
expect(editor.getCursorBufferPosition()).toEqual([3, 13])
|
||||
|
||||
it "respects options that preserve the formatting of the pasted text", ->
|
||||
editor.update({autoIndentOnPaste: true})
|
||||
atom.clipboard.write("a(x);\n b(x);\r\nc(x);\n", indentBasis: 0)
|
||||
editor.setCursorBufferPosition([5, 0])
|
||||
editor.insertText(' ')
|
||||
editor.pasteText({autoIndent: false, preserveTrailingLineIndentation: true, normalizeLineEndings: false})
|
||||
|
||||
expect(editor.lineTextForBufferRow(5)).toBe " a(x);"
|
||||
expect(editor.lineTextForBufferRow(6)).toBe " b(x);"
|
||||
expect(editor.buffer.lineEndingForRow(6)).toBe "\r\n"
|
||||
expect(editor.lineTextForBufferRow(7)).toBe "c(x);"
|
||||
expect(editor.lineTextForBufferRow(8)).toBe " current = items.shift();"
|
||||
|
||||
describe ".indentSelectedRows()", ->
|
||||
describe "when nothing is selected", ->
|
||||
describe "when softTabs is enabled", ->
|
||||
@ -4301,108 +4376,6 @@ describe "TextEditor", ->
|
||||
expect(editor.lineTextForBufferRow(4)).toBe " }"
|
||||
expect(editor.lineTextForBufferRow(5)).toBe " i=1"
|
||||
|
||||
describe ".toggleLineCommentsInSelection()", ->
|
||||
it "toggles comments on the selected lines", ->
|
||||
editor.setSelectedBufferRange([[4, 5], [7, 5]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
|
||||
expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {"
|
||||
expect(buffer.lineForRow(5)).toBe " // current = items.shift();"
|
||||
expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);"
|
||||
expect(buffer.lineForRow(7)).toBe " // }"
|
||||
expect(editor.getSelectedBufferRange()).toEqual [[4, 8], [7, 8]]
|
||||
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {"
|
||||
expect(buffer.lineForRow(5)).toBe " current = items.shift();"
|
||||
expect(buffer.lineForRow(6)).toBe " current < pivot ? left.push(current) : right.push(current);"
|
||||
expect(buffer.lineForRow(7)).toBe " }"
|
||||
|
||||
it "does not comment the last line of a non-empty selection if it ends at column 0", ->
|
||||
editor.setSelectedBufferRange([[4, 5], [7, 0]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {"
|
||||
expect(buffer.lineForRow(5)).toBe " // current = items.shift();"
|
||||
expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);"
|
||||
expect(buffer.lineForRow(7)).toBe " }"
|
||||
|
||||
it "uncomments lines if all lines match the comment regex", ->
|
||||
editor.setSelectedBufferRange([[0, 0], [0, 1]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {"
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(0)).toBe "// // var quicksort = function () {"
|
||||
expect(buffer.lineForRow(1)).toBe "// var sort = function(items) {"
|
||||
expect(buffer.lineForRow(2)).toBe "// if (items.length <= 1) return items;"
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {"
|
||||
expect(buffer.lineForRow(1)).toBe " var sort = function(items) {"
|
||||
expect(buffer.lineForRow(2)).toBe " if (items.length <= 1) return items;"
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [0, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(0)).toBe "var quicksort = function () {"
|
||||
|
||||
it "uncomments commented lines separated by an empty line", ->
|
||||
editor.setSelectedBufferRange([[0, 0], [1, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(0)).toBe "// var quicksort = function () {"
|
||||
expect(buffer.lineForRow(1)).toBe "// var sort = function(items) {"
|
||||
|
||||
buffer.insert([0, Infinity], '\n')
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(0)).toBe "var quicksort = function () {"
|
||||
expect(buffer.lineForRow(1)).toBe ""
|
||||
expect(buffer.lineForRow(2)).toBe " var sort = function(items) {"
|
||||
|
||||
it "preserves selection emptiness", ->
|
||||
editor.setCursorBufferPosition([4, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.getLastSelection().isEmpty()).toBeTruthy()
|
||||
|
||||
it "does not explode if the current language mode has no comment regex", ->
|
||||
editor = new TextEditor(buffer: new TextBuffer(text: 'hello'))
|
||||
editor.setSelectedBufferRange([[0, 0], [0, 5]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe "hello"
|
||||
|
||||
it "does nothing for empty lines and null grammar", ->
|
||||
runs ->
|
||||
editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar'))
|
||||
editor.setCursorBufferPosition([10, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.buffer.lineForRow(10)).toBe ""
|
||||
|
||||
it "uncomments when the line lacks the trailing whitespace in the comment regex", ->
|
||||
editor.setCursorBufferPosition([10, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
|
||||
expect(buffer.lineForRow(10)).toBe "// "
|
||||
expect(editor.getSelectedBufferRange()).toEqual [[10, 3], [10, 3]]
|
||||
editor.backspace()
|
||||
expect(buffer.lineForRow(10)).toBe "//"
|
||||
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(10)).toBe ""
|
||||
expect(editor.getSelectedBufferRange()).toEqual [[10, 0], [10, 0]]
|
||||
|
||||
it "uncomments when the line has leading whitespace", ->
|
||||
editor.setCursorBufferPosition([10, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
|
||||
expect(buffer.lineForRow(10)).toBe "// "
|
||||
editor.moveToBeginningOfLine()
|
||||
editor.insertText(" ")
|
||||
editor.setSelectedBufferRange([[10, 0], [10, 0]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(buffer.lineForRow(10)).toBe " "
|
||||
|
||||
describe ".undo() and .redo()", ->
|
||||
it "undoes/redoes the last change", ->
|
||||
editor.insertText("foo")
|
||||
@ -4820,7 +4793,7 @@ describe "TextEditor", ->
|
||||
expect(buffer.lineForRow(6)).toBe(line7)
|
||||
expect(buffer.getLineCount()).toBe(count - 1)
|
||||
|
||||
describe "when the line being deleted preceeds a fold, and the command is undone", ->
|
||||
describe "when the line being deleted precedes a fold, and the command is undone", ->
|
||||
it "restores the line and preserves the fold", ->
|
||||
editor.setCursorBufferPosition([4])
|
||||
editor.foldCurrentRow()
|
||||
@ -4992,7 +4965,7 @@ describe "TextEditor", ->
|
||||
editor.insertText('\n')
|
||||
expect(editor.indentationForBufferRow(2)).toBe editor.indentationForBufferRow(1) + 1
|
||||
|
||||
describe "when the line preceding the newline does't add a level of indentation", ->
|
||||
describe "when the line preceding the newline doesn't add a level of indentation", ->
|
||||
it "indents the new line to the same level as the preceding line", ->
|
||||
editor.setCursorBufferPosition([5, 14])
|
||||
editor.insertText('\n')
|
||||
@ -5262,37 +5235,6 @@ describe "TextEditor", ->
|
||||
[[6, 3], [6, 4]],
|
||||
])
|
||||
|
||||
describe ".shouldPromptToSave()", ->
|
||||
it "returns true when buffer changed", ->
|
||||
jasmine.unspy(editor, 'shouldPromptToSave')
|
||||
expect(editor.shouldPromptToSave()).toBeFalsy()
|
||||
buffer.setText('changed')
|
||||
expect(editor.shouldPromptToSave()).toBeTruthy()
|
||||
|
||||
it "returns false when an edit session's buffer is in use by more than one session", ->
|
||||
jasmine.unspy(editor, 'shouldPromptToSave')
|
||||
buffer.setText('changed')
|
||||
|
||||
editor2 = null
|
||||
waitsForPromise ->
|
||||
atom.workspace.getActivePane().splitRight()
|
||||
atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor2 = o
|
||||
|
||||
runs ->
|
||||
expect(editor.shouldPromptToSave()).toBeFalsy()
|
||||
editor2.destroy()
|
||||
expect(editor.shouldPromptToSave()).toBeTruthy()
|
||||
|
||||
it "returns false when close of a window requested and edit session opened inside project", ->
|
||||
jasmine.unspy(editor, 'shouldPromptToSave')
|
||||
buffer.setText('changed')
|
||||
expect(editor.shouldPromptToSave(windowCloseRequested: true, projectHasPaths: true)).toBeFalsy()
|
||||
|
||||
it "returns true when close of a window requested and edit session opened without project", ->
|
||||
jasmine.unspy(editor, 'shouldPromptToSave')
|
||||
buffer.setText('changed')
|
||||
expect(editor.shouldPromptToSave(windowCloseRequested: true, projectHasPaths: false)).toBeTruthy()
|
||||
|
||||
describe "when the editor contains surrogate pair characters", ->
|
||||
it "correctly backspaces over them", ->
|
||||
editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97')
|
||||
@ -5918,3 +5860,11 @@ describe "TextEditor", ->
|
||||
describe "::getElement", ->
|
||||
it "returns an element", ->
|
||||
expect(editor.getElement() instanceof HTMLElement).toBe(true)
|
||||
|
||||
describe 'setMaxScreenLineLength', ->
|
||||
it "sets the maximum line length in the editor before soft wrapping is forced", ->
|
||||
expect(editor.getSoftWrapColumn()).toBe(500)
|
||||
editor.update({
|
||||
maxScreenLineLength: 1500
|
||||
})
|
||||
expect(editor.getSoftWrapColumn()).toBe(1500)
|
||||
|
541
spec/text-editor-spec.js
Normal file
541
spec/text-editor-spec.js
Normal file
@ -0,0 +1,541 @@
|
||||
const fs = require('fs')
|
||||
const temp = require('temp').track()
|
||||
const {Point, Range} = require('text-buffer')
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
|
||||
const TextBuffer = require('text-buffer')
|
||||
const TextEditor = require('../src/text-editor')
|
||||
|
||||
describe('TextEditor', () => {
|
||||
let editor
|
||||
|
||||
afterEach(() => {
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
describe('.shouldPromptToSave()', () => {
|
||||
beforeEach(async () => {
|
||||
editor = await atom.workspace.open('sample.js')
|
||||
jasmine.unspy(editor, 'shouldPromptToSave')
|
||||
})
|
||||
|
||||
it('returns true when buffer has unsaved changes', () => {
|
||||
expect(editor.shouldPromptToSave()).toBeFalsy()
|
||||
editor.setText('changed')
|
||||
expect(editor.shouldPromptToSave()).toBeTruthy()
|
||||
})
|
||||
|
||||
it("returns false when an editor's buffer is in use by more than one buffer", async () => {
|
||||
editor.setText('changed')
|
||||
|
||||
atom.workspace.getActivePane().splitRight()
|
||||
const editor2 = await atom.workspace.open('sample.js', {autoIndent: false})
|
||||
expect(editor.shouldPromptToSave()).toBeFalsy()
|
||||
|
||||
editor2.destroy()
|
||||
expect(editor.shouldPromptToSave()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns true when the window is closing if the file has changed on disk', async () => {
|
||||
jasmine.useRealClock()
|
||||
|
||||
editor.setText('initial stuff')
|
||||
await editor.saveAs(temp.openSync('test-file').path)
|
||||
|
||||
editor.setText('other stuff')
|
||||
fs.writeFileSync(editor.getPath(), 'new stuff')
|
||||
expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: true})).toBeFalsy()
|
||||
|
||||
await new Promise(resolve => editor.onDidConflict(resolve))
|
||||
expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: true})).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns false when the window is closing and the project has one or more directory paths', () => {
|
||||
editor.setText('changed')
|
||||
expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: true})).toBeFalsy()
|
||||
})
|
||||
|
||||
it('returns false when the window is closing and the project has no directory paths', () => {
|
||||
editor.setText('changed')
|
||||
expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: false})).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('.toggleLineCommentsInSelection()', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-javascript')
|
||||
editor = await atom.workspace.open('sample.js')
|
||||
})
|
||||
|
||||
it('toggles comments on the selected lines', () => {
|
||||
editor.setSelectedBufferRange([[4, 5], [7, 5]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' // }')
|
||||
expect(editor.getSelectedBufferRange()).toEqual([[4, 8], [7, 8]])
|
||||
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' current < pivot ? left.push(current) : right.push(current);')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' }')
|
||||
})
|
||||
|
||||
it('does not comment the last line of a non-empty selection if it ends at column 0', () => {
|
||||
editor.setSelectedBufferRange([[4, 5], [7, 0]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' }')
|
||||
})
|
||||
|
||||
it('uncomments lines if all lines match the comment regex', () => {
|
||||
editor.setSelectedBufferRange([[0, 0], [0, 1]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('// var quicksort = function () {')
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('// // var quicksort = function () {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe('// var sort = function(items) {')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe('// if (items.length <= 1) return items;')
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('// var quicksort = function () {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe(' var sort = function(items) {')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' if (items.length <= 1) return items;')
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [0, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {')
|
||||
})
|
||||
|
||||
it('uncomments commented lines separated by an empty line', () => {
|
||||
editor.setSelectedBufferRange([[0, 0], [1, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('// var quicksort = function () {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe('// var sort = function(items) {')
|
||||
|
||||
editor.getBuffer().insert([0, Infinity], '\n')
|
||||
|
||||
editor.setSelectedBufferRange([[0, 0], [2, Infinity]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('var quicksort = function () {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe('')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' var sort = function(items) {')
|
||||
})
|
||||
|
||||
it('preserves selection emptiness', () => {
|
||||
editor.setCursorBufferPosition([4, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.getLastSelection().isEmpty()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('does not explode if the current language mode has no comment regex', () => {
|
||||
const editor = new TextEditor({buffer: new TextBuffer({text: 'hello'})})
|
||||
editor.setSelectedBufferRange([[0, 0], [0, 5]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('hello')
|
||||
})
|
||||
|
||||
it('does nothing for empty lines and null grammar', () => {
|
||||
editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar'))
|
||||
editor.setCursorBufferPosition([10, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(10)).toBe('')
|
||||
})
|
||||
|
||||
it('uncomments when the line lacks the trailing whitespace in the comment regex', () => {
|
||||
editor.setCursorBufferPosition([10, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
|
||||
expect(editor.lineTextForBufferRow(10)).toBe('// ')
|
||||
expect(editor.getSelectedBufferRange()).toEqual([[10, 3], [10, 3]])
|
||||
editor.backspace()
|
||||
expect(editor.lineTextForBufferRow(10)).toBe('//')
|
||||
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(10)).toBe('')
|
||||
expect(editor.getSelectedBufferRange()).toEqual([[10, 0], [10, 0]])
|
||||
})
|
||||
|
||||
it('uncomments when the line has leading whitespace', () => {
|
||||
editor.setCursorBufferPosition([10, 0])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
|
||||
expect(editor.lineTextForBufferRow(10)).toBe('// ')
|
||||
editor.moveToBeginningOfLine()
|
||||
editor.insertText(' ')
|
||||
editor.setSelectedBufferRange([[10, 0], [10, 0]])
|
||||
editor.toggleLineCommentsInSelection()
|
||||
expect(editor.lineTextForBufferRow(10)).toBe(' ')
|
||||
})
|
||||
})
|
||||
|
||||
describe('.toggleLineCommentsForBufferRows', () => {
|
||||
describe('xml', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-xml')
|
||||
editor = await atom.workspace.open('test.xml')
|
||||
editor.setText('<!-- test -->')
|
||||
})
|
||||
|
||||
it('removes the leading whitespace from the comment end pattern match when uncommenting lines', () => {
|
||||
editor.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('test')
|
||||
})
|
||||
})
|
||||
|
||||
describe('less', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-less')
|
||||
await atom.packages.activatePackage('language-css')
|
||||
editor = await atom.workspace.open('sample.less')
|
||||
})
|
||||
|
||||
it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart` when commenting lines', () => {
|
||||
editor.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;')
|
||||
})
|
||||
})
|
||||
|
||||
describe('css', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-css')
|
||||
editor = await atom.workspace.open('css.css')
|
||||
})
|
||||
|
||||
it('comments/uncomments lines in the given range', () => {
|
||||
editor.toggleLineCommentsForBufferRows(0, 1)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('/* body {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px; */')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;')
|
||||
expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;')
|
||||
|
||||
editor.toggleLineCommentsForBufferRows(2, 2)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('/* body {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px; */')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' /* width: 110%; */')
|
||||
expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;')
|
||||
|
||||
editor.toggleLineCommentsForBufferRows(0, 1)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('body {')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' /* width: 110%; */')
|
||||
expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;')
|
||||
})
|
||||
|
||||
it('uncomments lines with leading whitespace', () => {
|
||||
editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /* width: 110%; */')
|
||||
editor.toggleLineCommentsForBufferRows(2, 2)
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;')
|
||||
})
|
||||
|
||||
it('uncomments lines with trailing whitespace', () => {
|
||||
editor.setTextInBufferRange([[2, 0], [2, Infinity]], '/* width: 110%; */ ')
|
||||
editor.toggleLineCommentsForBufferRows(2, 2)
|
||||
expect(editor.lineTextForBufferRow(2)).toBe('width: 110%; ')
|
||||
})
|
||||
|
||||
it('uncomments lines with leading and trailing whitespace', () => {
|
||||
editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /* width: 110%; */ ')
|
||||
editor.toggleLineCommentsForBufferRows(2, 2)
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%; ')
|
||||
})
|
||||
})
|
||||
|
||||
describe('coffeescript', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-coffee-script')
|
||||
editor = await atom.workspace.open('coffee.coffee')
|
||||
})
|
||||
|
||||
it('comments/uncomments lines in the given range', () => {
|
||||
editor.toggleLineCommentsForBufferRows(4, 6)
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' # left = []')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' # right = []')
|
||||
|
||||
editor.toggleLineCommentsForBufferRows(4, 5)
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' left = []')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' # right = []')
|
||||
})
|
||||
|
||||
it('comments/uncomments empty lines', () => {
|
||||
editor.toggleLineCommentsForBufferRows(4, 7)
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' # left = []')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' # right = []')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' # ')
|
||||
|
||||
editor.toggleLineCommentsForBufferRows(4, 5)
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' left = []')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' # right = []')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' # ')
|
||||
})
|
||||
})
|
||||
|
||||
describe('javascript', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-javascript')
|
||||
editor = await atom.workspace.open('sample.js')
|
||||
})
|
||||
|
||||
it('comments/uncomments lines in the given range', () => {
|
||||
editor.toggleLineCommentsForBufferRows(4, 7)
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' // }')
|
||||
|
||||
editor.toggleLineCommentsForBufferRows(4, 5)
|
||||
expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {')
|
||||
expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();')
|
||||
expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);')
|
||||
expect(editor.lineTextForBufferRow(7)).toBe(' // }')
|
||||
|
||||
editor.setText('\tvar i;')
|
||||
editor.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('\t// var i;')
|
||||
|
||||
editor.setText('var i;')
|
||||
editor.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe('// var i;')
|
||||
|
||||
editor.setText(' var i;')
|
||||
editor.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe(' // var i;')
|
||||
|
||||
editor.setText(' ')
|
||||
editor.toggleLineCommentsForBufferRows(0, 0)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe(' // ')
|
||||
|
||||
editor.setText(' a\n \n b')
|
||||
editor.toggleLineCommentsForBufferRows(0, 2)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe(' // a')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe(' // ')
|
||||
expect(editor.lineTextForBufferRow(2)).toBe(' // b')
|
||||
|
||||
editor.setText(' \n // var i;')
|
||||
editor.toggleLineCommentsForBufferRows(0, 1)
|
||||
expect(editor.lineTextForBufferRow(0)).toBe(' ')
|
||||
expect(editor.lineTextForBufferRow(1)).toBe(' var i;')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('folding', () => {
|
||||
beforeEach(async () => {
|
||||
await atom.packages.activatePackage('language-javascript')
|
||||
})
|
||||
|
||||
it('maintains cursor buffer position when a folding/unfolding', async () => {
|
||||
editor = await atom.workspace.open('sample.js', {autoIndent: false})
|
||||
editor.setCursorBufferPosition([5, 5])
|
||||
editor.foldAll()
|
||||
expect(editor.getCursorBufferPosition()).toEqual([5, 5])
|
||||
})
|
||||
|
||||
describe('.unfoldAll()', () => {
|
||||
it('unfolds every folded line', async () => {
|
||||
editor = await atom.workspace.open('sample.js', {autoIndent: false})
|
||||
|
||||
const initialScreenLineCount = editor.getScreenLineCount()
|
||||
editor.foldBufferRow(0)
|
||||
editor.foldBufferRow(1)
|
||||
expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount)
|
||||
editor.unfoldAll()
|
||||
expect(editor.getScreenLineCount()).toBe(initialScreenLineCount)
|
||||
})
|
||||
|
||||
it('unfolds every folded line with comments', async () => {
|
||||
editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false})
|
||||
|
||||
const initialScreenLineCount = editor.getScreenLineCount()
|
||||
editor.foldBufferRow(0)
|
||||
editor.foldBufferRow(5)
|
||||
expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount)
|
||||
editor.unfoldAll()
|
||||
expect(editor.getScreenLineCount()).toBe(initialScreenLineCount)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.foldAll()', () => {
|
||||
it('folds every foldable line', async () => {
|
||||
editor = await atom.workspace.open('sample.js', {autoIndent: false})
|
||||
|
||||
editor.foldAll()
|
||||
const [fold1, fold2, fold3] = editor.unfoldAll()
|
||||
expect([fold1.start.row, fold1.end.row]).toEqual([0, 12])
|
||||
expect([fold2.start.row, fold2.end.row]).toEqual([1, 9])
|
||||
expect([fold3.start.row, fold3.end.row]).toEqual([4, 7])
|
||||
})
|
||||
|
||||
it('works with multi-line comments', async () => {
|
||||
editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false})
|
||||
|
||||
editor.foldAll()
|
||||
const folds = editor.unfoldAll()
|
||||
expect(folds.length).toBe(8)
|
||||
expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30])
|
||||
expect([folds[1].start.row, folds[1].end.row]).toEqual([1, 4])
|
||||
expect([folds[2].start.row, folds[2].end.row]).toEqual([5, 27])
|
||||
expect([folds[3].start.row, folds[3].end.row]).toEqual([6, 8])
|
||||
expect([folds[4].start.row, folds[4].end.row]).toEqual([11, 16])
|
||||
expect([folds[5].start.row, folds[5].end.row]).toEqual([17, 20])
|
||||
expect([folds[6].start.row, folds[6].end.row]).toEqual([21, 22])
|
||||
expect([folds[7].start.row, folds[7].end.row]).toEqual([24, 25])
|
||||
})
|
||||
})
|
||||
|
||||
describe('.foldBufferRow(bufferRow)', () => {
|
||||
beforeEach(async () => {
|
||||
editor = await atom.workspace.open('sample.js')
|
||||
})
|
||||
|
||||
describe('when bufferRow can be folded', () => {
|
||||
it('creates a fold based on the syntactic region starting at the given row', () => {
|
||||
editor.foldBufferRow(1)
|
||||
const [fold] = editor.unfoldAll()
|
||||
expect([fold.start.row, fold.end.row]).toEqual([1, 9])
|
||||
})
|
||||
})
|
||||
|
||||
describe("when bufferRow can't be folded", () => {
|
||||
it('searches upward for the first row that begins a syntactic region containing the given buffer row (and folds it)', () => {
|
||||
editor.foldBufferRow(8)
|
||||
const [fold] = editor.unfoldAll()
|
||||
expect([fold.start.row, fold.end.row]).toEqual([1, 9])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the bufferRow is already folded', () => {
|
||||
it('searches upward for the first row that begins a syntactic region containing the folded row (and folds it)', () => {
|
||||
editor.foldBufferRow(2)
|
||||
expect(editor.isFoldedAtBufferRow(0)).toBe(false)
|
||||
expect(editor.isFoldedAtBufferRow(1)).toBe(true)
|
||||
|
||||
editor.foldBufferRow(1)
|
||||
expect(editor.isFoldedAtBufferRow(0)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the bufferRow is in a multi-line comment', () => {
|
||||
it('searches upward and downward for surrounding comment lines and folds them as a single fold', () => {
|
||||
editor.buffer.insert([1, 0], ' //this is a comment\n // and\n //more docs\n\n//second comment')
|
||||
editor.foldBufferRow(1)
|
||||
const [fold] = editor.unfoldAll()
|
||||
expect([fold.start.row, fold.end.row]).toEqual([1, 3])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the bufferRow is a single-line comment', () => {
|
||||
it('searches upward for the first row that begins a syntactic region containing the folded row (and folds it)', () => {
|
||||
editor.buffer.insert([1, 0], ' //this is a single line comment\n')
|
||||
editor.foldBufferRow(1)
|
||||
const [fold] = editor.unfoldAll()
|
||||
expect([fold.start.row, fold.end.row]).toEqual([0, 13])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.foldCurrentRow()', () => {
|
||||
it('creates a fold at the location of the last cursor', async () => {
|
||||
editor = await atom.workspace.open()
|
||||
editor.setText('\nif (x) {\n y()\n}')
|
||||
editor.setCursorBufferPosition([1, 0])
|
||||
expect(editor.getScreenLineCount()).toBe(4)
|
||||
editor.foldCurrentRow()
|
||||
expect(editor.getScreenLineCount()).toBe(3)
|
||||
})
|
||||
|
||||
it('does nothing when the current row cannot be folded', async () => {
|
||||
editor = await atom.workspace.open()
|
||||
editor.setText('var x;\nx++\nx++')
|
||||
editor.setCursorBufferPosition([0, 0])
|
||||
expect(editor.getScreenLineCount()).toBe(3)
|
||||
editor.foldCurrentRow()
|
||||
expect(editor.getScreenLineCount()).toBe(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.foldAllAtIndentLevel(indentLevel)', () => {
|
||||
it('folds blocks of text at the given indentation level', async () => {
|
||||
editor = await atom.workspace.open('sample.js', {autoIndent: false})
|
||||
|
||||
editor.foldAllAtIndentLevel(0)
|
||||
expect(editor.lineTextForScreenRow(0)).toBe(`var quicksort = function () {${editor.displayLayer.foldCharacter}`)
|
||||
expect(editor.getLastScreenRow()).toBe(0)
|
||||
|
||||
editor.foldAllAtIndentLevel(1)
|
||||
expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {')
|
||||
expect(editor.lineTextForScreenRow(1)).toBe(` var sort = function(items) {${editor.displayLayer.foldCharacter}`)
|
||||
expect(editor.getLastScreenRow()).toBe(4)
|
||||
|
||||
editor.foldAllAtIndentLevel(2)
|
||||
expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {')
|
||||
expect(editor.lineTextForScreenRow(1)).toBe(' var sort = function(items) {')
|
||||
expect(editor.lineTextForScreenRow(2)).toBe(' if (items.length <= 1) return items;')
|
||||
expect(editor.getLastScreenRow()).toBe(9)
|
||||
})
|
||||
|
||||
it('folds every foldable range at a given indentLevel', async () => {
|
||||
editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false})
|
||||
|
||||
editor.foldAllAtIndentLevel(2)
|
||||
const folds = editor.unfoldAll()
|
||||
expect(folds.length).toBe(5)
|
||||
expect([folds[0].start.row, folds[0].end.row]).toEqual([6, 8])
|
||||
expect([folds[1].start.row, folds[1].end.row]).toEqual([11, 16])
|
||||
expect([folds[2].start.row, folds[2].end.row]).toEqual([17, 20])
|
||||
expect([folds[3].start.row, folds[3].end.row]).toEqual([21, 22])
|
||||
expect([folds[4].start.row, folds[4].end.row]).toEqual([24, 25])
|
||||
})
|
||||
|
||||
it('does not fold anything but the indentLevel', async () => {
|
||||
editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false})
|
||||
|
||||
editor.foldAllAtIndentLevel(0)
|
||||
const folds = editor.unfoldAll()
|
||||
expect(folds.length).toBe(1)
|
||||
expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30])
|
||||
})
|
||||
})
|
||||
|
||||
describe('.isFoldableAtBufferRow(bufferRow)', () => {
|
||||
it('returns true if the line starts a multi-line comment', async () => {
|
||||
editor = await atom.workspace.open('sample-with-comments.js')
|
||||
|
||||
expect(editor.isFoldableAtBufferRow(1)).toBe(true)
|
||||
expect(editor.isFoldableAtBufferRow(6)).toBe(true)
|
||||
expect(editor.isFoldableAtBufferRow(8)).toBe(false)
|
||||
expect(editor.isFoldableAtBufferRow(11)).toBe(true)
|
||||
expect(editor.isFoldableAtBufferRow(15)).toBe(false)
|
||||
expect(editor.isFoldableAtBufferRow(17)).toBe(true)
|
||||
expect(editor.isFoldableAtBufferRow(21)).toBe(true)
|
||||
expect(editor.isFoldableAtBufferRow(24)).toBe(true)
|
||||
expect(editor.isFoldableAtBufferRow(28)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true for lines that end with a comment and are followed by an indented line', async () => {
|
||||
editor = await atom.workspace.open('sample-with-comments.js')
|
||||
|
||||
expect(editor.isFoldableAtBufferRow(5)).toBe(true)
|
||||
})
|
||||
|
||||
it("does not return true for a line in the middle of a comment that's followed by an indented line", async () => {
|
||||
editor = await atom.workspace.open('sample-with-comments.js')
|
||||
|
||||
expect(editor.isFoldableAtBufferRow(7)).toBe(false)
|
||||
editor.buffer.insert([8, 0], ' ')
|
||||
expect(editor.isFoldableAtBufferRow(7)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -8,9 +8,11 @@ describe "atom.themes", ->
|
||||
spyOn(console, 'warn')
|
||||
|
||||
afterEach ->
|
||||
atom.themes.deactivateThemes()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
waitsForPromise ->
|
||||
atom.themes.deactivateThemes()
|
||||
runs ->
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe "theme getters and setters", ->
|
||||
beforeEach ->
|
||||
|
@ -1,688 +0,0 @@
|
||||
NullGrammar = require '../src/null-grammar'
|
||||
TokenizedBuffer = require '../src/tokenized-buffer'
|
||||
{Point} = TextBuffer = require 'text-buffer'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
describe "TokenizedBuffer", ->
|
||||
[tokenizedBuffer, buffer] = []
|
||||
|
||||
beforeEach ->
|
||||
# enable async tokenization
|
||||
TokenizedBuffer.prototype.chunkSize = 5
|
||||
jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-javascript')
|
||||
|
||||
afterEach ->
|
||||
tokenizedBuffer?.destroy()
|
||||
|
||||
startTokenizing = (tokenizedBuffer) ->
|
||||
tokenizedBuffer.setVisible(true)
|
||||
|
||||
fullyTokenize = (tokenizedBuffer) ->
|
||||
tokenizedBuffer.setVisible(true)
|
||||
advanceClock() while tokenizedBuffer.firstInvalidRow()?
|
||||
|
||||
describe "serialization", ->
|
||||
describe "when the underlying buffer has a path", ->
|
||||
beforeEach ->
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-coffee-script')
|
||||
|
||||
it "deserializes it searching among the buffers in the current project", ->
|
||||
tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
|
||||
tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
|
||||
expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
|
||||
|
||||
describe "when the underlying buffer has no path", ->
|
||||
beforeEach ->
|
||||
buffer = atom.project.bufferForPathSync(null)
|
||||
|
||||
it "deserializes it searching among the buffers in the current project", ->
|
||||
tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
|
||||
tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
|
||||
expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
|
||||
|
||||
describe "when the buffer is destroyed", ->
|
||||
beforeEach ->
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
startTokenizing(tokenizedBuffer)
|
||||
|
||||
it "stops tokenization", ->
|
||||
tokenizedBuffer.destroy()
|
||||
spyOn(tokenizedBuffer, 'tokenizeNextChunk')
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled()
|
||||
|
||||
describe "when the buffer contains soft-tabs", ->
|
||||
beforeEach ->
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
startTokenizing(tokenizedBuffer)
|
||||
|
||||
afterEach ->
|
||||
tokenizedBuffer.destroy()
|
||||
buffer.release()
|
||||
|
||||
describe "on construction", ->
|
||||
it "tokenizes lines chunk at a time in the background", ->
|
||||
line0 = tokenizedBuffer.tokenizedLines[0]
|
||||
expect(line0).toBeUndefined()
|
||||
|
||||
line11 = tokenizedBuffer.tokenizedLines[11]
|
||||
expect(line11).toBeUndefined()
|
||||
|
||||
# tokenize chunk 1
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[0].ruleStack?).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[4].ruleStack?).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined()
|
||||
|
||||
# tokenize chunk 2
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[5].ruleStack?).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[9].ruleStack?).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined()
|
||||
|
||||
# tokenize last chunk
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[10].ruleStack?).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[12].ruleStack?).toBeTruthy()
|
||||
|
||||
describe "when the buffer is partially tokenized", ->
|
||||
beforeEach ->
|
||||
# tokenize chunk 1 only
|
||||
advanceClock()
|
||||
|
||||
describe "when there is a buffer change inside the tokenized region", ->
|
||||
describe "when lines are added", ->
|
||||
it "pushes the invalid rows down", ->
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe 5
|
||||
buffer.insert([1, 0], '\n\n')
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe 7
|
||||
|
||||
describe "when lines are removed", ->
|
||||
it "pulls the invalid rows up", ->
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe 5
|
||||
buffer.delete([[1, 0], [3, 0]])
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe 2
|
||||
|
||||
describe "when the change invalidates all the lines before the current invalid region", ->
|
||||
it "retokenizes the invalidated lines and continues into the valid region", ->
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe 5
|
||||
buffer.insert([2, 0], '/*')
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe 3
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe 8
|
||||
|
||||
describe "when there is a buffer change surrounding an invalid row", ->
|
||||
it "pushes the invalid row to the end of the change", ->
|
||||
buffer.setTextInRange([[4, 0], [6, 0]], "\n\n\n")
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe 8
|
||||
|
||||
describe "when there is a buffer change inside an invalid region", ->
|
||||
it "does not attempt to tokenize the lines in the change, and preserves the existing invalid row", ->
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe 5
|
||||
buffer.setTextInRange([[6, 0], [7, 0]], "\n\n\n")
|
||||
expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined()
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe 5
|
||||
|
||||
describe "when the buffer is fully tokenized", ->
|
||||
beforeEach ->
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
describe "when there is a buffer change that is smaller than the chunk size", ->
|
||||
describe "when lines are updated, but none are added or removed", ->
|
||||
it "updates tokens to reflect the change", ->
|
||||
buffer.setTextInRange([[0, 0], [2, 0]], "foo()\n7\n")
|
||||
|
||||
expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual(value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual(value: '7', scopes: ['source.js', 'constant.numeric.decimal.js'])
|
||||
# line 2 is unchanged
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
|
||||
|
||||
describe "when the change invalidates the tokenization of subsequent lines", ->
|
||||
it "schedules the invalidated lines to be tokenized in the background", ->
|
||||
buffer.insert([5, 30], '/* */')
|
||||
buffer.insert([2, 0], '/*')
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js']
|
||||
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
|
||||
|
||||
it "resumes highlighting with the state of the previous line", ->
|
||||
buffer.insert([0, 0], '/*')
|
||||
buffer.insert([5, 0], '*/')
|
||||
|
||||
buffer.insert([1, 0], 'var ')
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
|
||||
|
||||
describe "when lines are both updated and removed", ->
|
||||
it "updates tokens to reflect the change", ->
|
||||
buffer.setTextInRange([[1, 0], [3, 0]], "foo()")
|
||||
|
||||
# previous line 0 remains
|
||||
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual(value: 'var', scopes: ['source.js', 'storage.type.var.js'])
|
||||
|
||||
# previous line 3 should be combined with input to form line 1
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual(value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
|
||||
|
||||
# lines below deleted regions should be shifted upward
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.comparison.js'])
|
||||
|
||||
describe "when the change invalidates the tokenization of subsequent lines", ->
|
||||
it "schedules the invalidated lines to be tokenized in the background", ->
|
||||
buffer.insert([5, 30], '/* */')
|
||||
buffer.setTextInRange([[2, 0], [3, 0]], '/*')
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js']
|
||||
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
|
||||
|
||||
describe "when lines are both updated and inserted", ->
|
||||
it "updates tokens to reflect the change", ->
|
||||
buffer.setTextInRange([[1, 0], [2, 0]], "foo()\nbar()\nbaz()\nquux()")
|
||||
|
||||
# previous line 0 remains
|
||||
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual( value: 'var', scopes: ['source.js', 'storage.type.var.js'])
|
||||
|
||||
# 3 new lines inserted
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual(value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[0]).toEqual(value: 'bar', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0]).toEqual(value: 'baz', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js'])
|
||||
|
||||
# previous line 2 is joined with quux() on line 4
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual(value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js'])
|
||||
|
||||
# previous line 3 is pushed down to become line 5
|
||||
expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js'])
|
||||
|
||||
describe "when the change invalidates the tokenization of subsequent lines", ->
|
||||
it "schedules the invalidated lines to be tokenized in the background", ->
|
||||
buffer.insert([5, 30], '/* */')
|
||||
buffer.insert([2, 0], '/*\nabcde\nabcder')
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js']
|
||||
|
||||
advanceClock() # tokenize invalidated lines in background
|
||||
expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe ['source.js', 'comment.block.js']
|
||||
|
||||
describe "when there is an insertion that is larger than the chunk size", ->
|
||||
it "tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background", ->
|
||||
commentBlock = _.multiplyString("// a comment\n", tokenizedBuffer.chunkSize + 2)
|
||||
buffer.insert([0, 0], commentBlock)
|
||||
expect(tokenizedBuffer.tokenizedLines[0].ruleStack?).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[4].ruleStack?).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined()
|
||||
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[5].ruleStack?).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[6].ruleStack?).toBeTruthy()
|
||||
|
||||
it "does not break out soft tabs across a scope boundary", ->
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-gfm')
|
||||
|
||||
runs ->
|
||||
tokenizedBuffer.setTabLength(4)
|
||||
tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md'))
|
||||
buffer.setText(' <![]()\n ')
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
length = 0
|
||||
for tag in tokenizedBuffer.tokenizedLines[1].tags
|
||||
length += tag if tag > 0
|
||||
|
||||
expect(length).toBe 4
|
||||
|
||||
describe "when the buffer contains hard-tabs", ->
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-coffee-script')
|
||||
|
||||
runs ->
|
||||
buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2})
|
||||
startTokenizing(tokenizedBuffer)
|
||||
|
||||
afterEach ->
|
||||
tokenizedBuffer.destroy()
|
||||
buffer.release()
|
||||
|
||||
describe "when the buffer is fully tokenized", ->
|
||||
beforeEach ->
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
describe "when the grammar is tokenized", ->
|
||||
it "emits the `tokenized` event", ->
|
||||
editor = null
|
||||
tokenizedHandler = jasmine.createSpy("tokenized handler")
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('sample.js').then (o) -> editor = o
|
||||
|
||||
runs ->
|
||||
tokenizedBuffer = editor.tokenizedBuffer
|
||||
tokenizedBuffer.onDidTokenize tokenizedHandler
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedHandler.callCount).toBe(1)
|
||||
|
||||
it "doesn't re-emit the `tokenized` event when it is re-tokenized", ->
|
||||
editor = null
|
||||
tokenizedHandler = jasmine.createSpy("tokenized handler")
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('sample.js').then (o) -> editor = o
|
||||
|
||||
runs ->
|
||||
tokenizedBuffer = editor.tokenizedBuffer
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
tokenizedBuffer.onDidTokenize tokenizedHandler
|
||||
editor.getBuffer().insert([0, 0], "'")
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedHandler).not.toHaveBeenCalled()
|
||||
|
||||
describe "when the grammar is updated because a grammar it includes is activated", ->
|
||||
it "re-emits the `tokenized` event", ->
|
||||
editor = null
|
||||
tokenizedBuffer = null
|
||||
tokenizedHandler = jasmine.createSpy("tokenized handler")
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('coffee.coffee').then (o) -> editor = o
|
||||
|
||||
runs ->
|
||||
tokenizedBuffer = editor.tokenizedBuffer
|
||||
tokenizedBuffer.onDidTokenize tokenizedHandler
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
tokenizedHandler.reset()
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-coffee-script')
|
||||
|
||||
runs ->
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedHandler.callCount).toBe(1)
|
||||
|
||||
it "retokenizes the buffer", ->
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-ruby-on-rails')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-ruby')
|
||||
|
||||
runs ->
|
||||
buffer = atom.project.bufferForPathSync()
|
||||
buffer.setText "<div class='name'><%= User.find(2).full_name %></div>"
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
{tokens} = tokenizedBuffer.tokenizedLines[0]
|
||||
expect(tokens[0]).toEqual value: "<div class='name'>", scopes: ["text.html.ruby"]
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-html')
|
||||
|
||||
runs ->
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
{tokens} = tokenizedBuffer.tokenizedLines[0]
|
||||
expect(tokens[0]).toEqual value: '<', scopes: ["text.html.ruby", "meta.tag.block.any.html", "punctuation.definition.tag.begin.html"]
|
||||
|
||||
describe ".tokenForPosition(position)", ->
|
||||
afterEach ->
|
||||
tokenizedBuffer.destroy()
|
||||
buffer.release()
|
||||
|
||||
it "returns the correct token (regression)", ->
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual ["source.js"]
|
||||
expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual ["source.js"]
|
||||
expect(tokenizedBuffer.tokenForPosition([1, 2]).scopes).toEqual ["source.js", "storage.type.var.js"]
|
||||
|
||||
describe ".bufferRangeForScopeAtPosition(selector, position)", ->
|
||||
beforeEach ->
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
describe "when the selector does not match the token at the position", ->
|
||||
it "returns a falsy value", ->
|
||||
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.bogus', [0, 1])).toBeUndefined()
|
||||
|
||||
describe "when the selector matches a single token at the position", ->
|
||||
it "returns the range covered by the token", ->
|
||||
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 1])).toEqual [[0, 0], [0, 3]]
|
||||
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 3])).toEqual [[0, 0], [0, 3]]
|
||||
|
||||
describe "when the selector matches a run of multiple tokens at the position", ->
|
||||
it "returns the range covered by all contigous tokens (within a single line)", ->
|
||||
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual [[1, 6], [1, 28]]
|
||||
|
||||
describe ".indentLevelForRow(row)", ->
|
||||
beforeEach ->
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
describe "when the line is non-empty", ->
|
||||
it "has an indent level based on the leading whitespace on the line", ->
|
||||
expect(tokenizedBuffer.indentLevelForRow(0)).toBe 0
|
||||
expect(tokenizedBuffer.indentLevelForRow(1)).toBe 1
|
||||
expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2
|
||||
buffer.insert([2, 0], ' ')
|
||||
expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2.5
|
||||
|
||||
describe "when the line is empty", ->
|
||||
it "assumes the indentation level of the first non-empty line below or above if one exists", ->
|
||||
buffer.insert([12, 0], ' ')
|
||||
buffer.insert([12, Infinity], '\n\n')
|
||||
expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
|
||||
expect(tokenizedBuffer.indentLevelForRow(14)).toBe 2
|
||||
|
||||
buffer.insert([1, Infinity], '\n\n')
|
||||
expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2
|
||||
expect(tokenizedBuffer.indentLevelForRow(3)).toBe 2
|
||||
|
||||
buffer.setText('\n\n\n')
|
||||
expect(tokenizedBuffer.indentLevelForRow(1)).toBe 0
|
||||
|
||||
describe "when the changed lines are surrounded by whitespace-only lines", ->
|
||||
it "updates the indentLevel of empty lines that precede the change", ->
|
||||
expect(tokenizedBuffer.indentLevelForRow(12)).toBe 0
|
||||
|
||||
buffer.insert([12, 0], '\n')
|
||||
buffer.insert([13, 0], ' ')
|
||||
expect(tokenizedBuffer.indentLevelForRow(12)).toBe 1
|
||||
|
||||
it "updates empty line indent guides when the empty line is the last line", ->
|
||||
buffer.insert([12, 2], '\n')
|
||||
|
||||
# The newline and the tab need to be in two different operations to surface the bug
|
||||
buffer.insert([12, 0], ' ')
|
||||
expect(tokenizedBuffer.indentLevelForRow(13)).toBe 1
|
||||
|
||||
buffer.insert([12, 0], ' ')
|
||||
expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
|
||||
expect(tokenizedBuffer.tokenizedLines[14]).not.toBeDefined()
|
||||
|
||||
it "updates the indentLevel of empty lines surrounding a change that inserts lines", ->
|
||||
buffer.insert([7, 0], '\n\n')
|
||||
buffer.insert([5, 0], '\n\n')
|
||||
expect(tokenizedBuffer.indentLevelForRow(5)).toBe 3
|
||||
expect(tokenizedBuffer.indentLevelForRow(6)).toBe 3
|
||||
expect(tokenizedBuffer.indentLevelForRow(9)).toBe 3
|
||||
expect(tokenizedBuffer.indentLevelForRow(10)).toBe 3
|
||||
expect(tokenizedBuffer.indentLevelForRow(11)).toBe 2
|
||||
|
||||
buffer.setTextInRange([[7, 0], [8, 65]], ' one\n two\n three\n four')
|
||||
expect(tokenizedBuffer.indentLevelForRow(5)).toBe 4
|
||||
expect(tokenizedBuffer.indentLevelForRow(6)).toBe 4
|
||||
expect(tokenizedBuffer.indentLevelForRow(11)).toBe 4
|
||||
expect(tokenizedBuffer.indentLevelForRow(12)).toBe 4
|
||||
expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2
|
||||
|
||||
it "updates the indentLevel of empty lines surrounding a change that removes lines", ->
|
||||
buffer.insert([7, 0], '\n\n')
|
||||
buffer.insert([5, 0], '\n\n')
|
||||
buffer.setTextInRange([[7, 0], [8, 65]], ' ok')
|
||||
expect(tokenizedBuffer.indentLevelForRow(5)).toBe 2
|
||||
expect(tokenizedBuffer.indentLevelForRow(6)).toBe 2
|
||||
expect(tokenizedBuffer.indentLevelForRow(7)).toBe 2 # new text
|
||||
expect(tokenizedBuffer.indentLevelForRow(8)).toBe 2
|
||||
expect(tokenizedBuffer.indentLevelForRow(9)).toBe 2
|
||||
expect(tokenizedBuffer.indentLevelForRow(10)).toBe 2 # }
|
||||
|
||||
describe "::isFoldableAtRow(row)", ->
|
||||
beforeEach ->
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
buffer.insert [10, 0], " // multi-line\n // comment\n // block\n"
|
||||
buffer.insert [0, 0], "// multi-line\n// comment\n// block\n"
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
it "includes the first line of multi-line comments", ->
|
||||
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe true
|
||||
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true # because of indent
|
||||
expect(tokenizedBuffer.isFoldableAtRow(13)).toBe true
|
||||
expect(tokenizedBuffer.isFoldableAtRow(14)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(15)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(16)).toBe false
|
||||
|
||||
buffer.insert([0, Infinity], '\n')
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe true
|
||||
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe false
|
||||
|
||||
buffer.undo()
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe true
|
||||
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true # because of indent
|
||||
|
||||
it "includes non-comment lines that precede an increase in indentation", ->
|
||||
buffer.insert([2, 0], ' ') # commented lines preceding an indent aren't foldable
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true
|
||||
expect(tokenizedBuffer.isFoldableAtRow(4)).toBe true
|
||||
expect(tokenizedBuffer.isFoldableAtRow(5)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe true
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
|
||||
|
||||
buffer.insert([7, 0], ' ')
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
|
||||
|
||||
buffer.undo()
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe true
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
|
||||
|
||||
buffer.insert([7, 0], " \n x\n")
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
|
||||
|
||||
buffer.insert([9, 0], " ")
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false
|
||||
|
||||
describe "::tokenizedLineForRow(row)", ->
|
||||
it "returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", ->
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
grammar = atom.grammars.grammarForScopeName('source.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
|
||||
line0 = buffer.lineForRow(0)
|
||||
|
||||
jsScopeStartId = grammar.startIdForScope(grammar.scopeName)
|
||||
jsScopeEndId = grammar.endIdForScope(grammar.scopeName)
|
||||
startTokenizing(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([jsScopeStartId, line0.length, jsScopeEndId])
|
||||
advanceClock(1)
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).not.toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).not.toEqual([jsScopeStartId, line0.length, jsScopeEndId])
|
||||
|
||||
nullScopeStartId = NullGrammar.startIdForScope(NullGrammar.scopeName)
|
||||
nullScopeEndId = NullGrammar.endIdForScope(NullGrammar.scopeName)
|
||||
tokenizedBuffer.setGrammar(NullGrammar)
|
||||
startTokenizing(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
|
||||
advanceClock(1)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
|
||||
|
||||
it "returns undefined if the requested row is outside the buffer range", ->
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
grammar = atom.grammars.grammarForScopeName('source.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(999)).toBeUndefined()
|
||||
|
||||
describe "when the buffer is configured with the null grammar", ->
|
||||
it "does not actually tokenize using the grammar", ->
|
||||
spyOn(NullGrammar, 'tokenizeLine').andCallThrough()
|
||||
buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar')
|
||||
buffer.setText('a\nb\nc')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2})
|
||||
tokenizeCallback = jasmine.createSpy('onDidTokenize')
|
||||
tokenizedBuffer.onDidTokenize(tokenizeCallback)
|
||||
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined()
|
||||
expect(tokenizeCallback.callCount).toBe(0)
|
||||
expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled()
|
||||
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined()
|
||||
expect(tokenizeCallback.callCount).toBe(0)
|
||||
expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled()
|
||||
|
||||
describe "text decoration layer API", ->
|
||||
describe "iterator", ->
|
||||
it "iterates over the syntactic scope boundaries", ->
|
||||
buffer = new TextBuffer(text: "var foo = 1 /*\nhello*/var bar = 2\n")
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName("source.js"), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
iterator = tokenizedBuffer.buildIterator()
|
||||
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--begin syntax--js"]}
|
||||
{position: Point(0, 14), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js"], openTags: []}
|
||||
{position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js"]}
|
||||
{position: Point(1, 7), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js", "syntax--comment syntax--block syntax--js"], openTags: ["syntax--storage syntax--type syntax--var syntax--js"]}
|
||||
{position: Point(1, 10), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []}
|
||||
{position: Point(1, 15), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]}
|
||||
{position: Point(1, 16), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []}
|
||||
{position: Point(1, 17), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]}
|
||||
{position: Point(1, 18), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []}
|
||||
]
|
||||
|
||||
loop
|
||||
boundary = {
|
||||
position: iterator.getPosition(),
|
||||
closeTags: iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId)),
|
||||
openTags: iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))
|
||||
}
|
||||
|
||||
expect(boundary).toEqual(expectedBoundaries.shift())
|
||||
break unless iterator.moveToSuccessor()
|
||||
|
||||
expect(iterator.seek(Point(0, 1)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js",
|
||||
"syntax--storage syntax--type syntax--var syntax--js"
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.seek(Point(0, 8)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js"
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 8))
|
||||
expect(iterator.seek(Point(1, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js",
|
||||
"syntax--comment syntax--block syntax--js"
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 0))
|
||||
expect(iterator.seek(Point(1, 18)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js",
|
||||
"syntax--constant syntax--numeric syntax--decimal syntax--js"
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 18))
|
||||
|
||||
expect(iterator.seek(Point(2, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js"
|
||||
])
|
||||
iterator.moveToSuccessor() # ensure we don't infinitely loop (regression test)
|
||||
|
||||
it "does not report columns beyond the length of the line", ->
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-coffee-script')
|
||||
|
||||
runs ->
|
||||
buffer = new TextBuffer(text: "# hello\n# world")
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName("source.coffee"), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
iterator = tokenizedBuffer.buildIterator()
|
||||
iterator.seek(Point(0, 0))
|
||||
iterator.moveToSuccessor()
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition().column).toBe(7)
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition().column).toBe(0)
|
||||
|
||||
iterator.seek(Point(0, 7))
|
||||
expect(iterator.getPosition().column).toBe(7)
|
||||
|
||||
iterator.seek(Point(0, 8))
|
||||
expect(iterator.getPosition().column).toBe(7)
|
||||
|
||||
it "correctly terminates scopes at the beginning of the line (regression)", ->
|
||||
grammar = atom.grammars.createGrammar('test', {
|
||||
'scopeName': 'text.broken'
|
||||
'name': 'Broken grammar'
|
||||
'patterns': [
|
||||
{'begin': 'start', 'end': '(?=end)', 'name': 'blue.broken'},
|
||||
{'match': '.', 'name': 'yellow.broken'}
|
||||
]
|
||||
})
|
||||
|
||||
buffer = new TextBuffer(text: 'start x\nend x\nx')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
iterator = tokenizedBuffer.buildIterator()
|
||||
iterator.seek(Point(1, 0))
|
||||
|
||||
expect(iterator.getPosition()).toEqual([1, 0])
|
||||
expect(iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--blue syntax--broken']
|
||||
expect(iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--yellow syntax--broken']
|
904
spec/tokenized-buffer-spec.js
Normal file
904
spec/tokenized-buffer-spec.js
Normal file
@ -0,0 +1,904 @@
|
||||
const NullGrammar = require('../src/null-grammar')
|
||||
const TokenizedBuffer = require('../src/tokenized-buffer')
|
||||
const TextBuffer = require('text-buffer')
|
||||
const {Point, Range} = TextBuffer
|
||||
const _ = require('underscore-plus')
|
||||
const dedent = require('dedent')
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
|
||||
const {ScopedSettingsDelegate} = require('../src/text-editor-registry')
|
||||
|
||||
describe('TokenizedBuffer', () => {
|
||||
let tokenizedBuffer, buffer
|
||||
|
||||
beforeEach(async () => {
|
||||
// enable async tokenization
|
||||
TokenizedBuffer.prototype.chunkSize = 5
|
||||
jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground')
|
||||
await atom.packages.activatePackage('language-javascript')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
buffer && buffer.destroy()
|
||||
tokenizedBuffer && tokenizedBuffer.destroy()
|
||||
})
|
||||
|
||||
function startTokenizing (tokenizedBuffer) {
|
||||
tokenizedBuffer.setVisible(true)
|
||||
}
|
||||
|
||||
function fullyTokenize (tokenizedBuffer) {
|
||||
tokenizedBuffer.setVisible(true)
|
||||
while (tokenizedBuffer.firstInvalidRow() != null) {
|
||||
advanceClock()
|
||||
}
|
||||
}
|
||||
|
||||
describe('serialization', () => {
|
||||
describe('when the underlying buffer has a path', () => {
|
||||
beforeEach(async () => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
await atom.packages.activatePackage('language-coffee-script')
|
||||
})
|
||||
|
||||
it('deserializes it searching among the buffers in the current project', () => {
|
||||
const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
|
||||
const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
|
||||
expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the underlying buffer has no path', () => {
|
||||
beforeEach(() => buffer = atom.project.bufferForPathSync(null))
|
||||
|
||||
it('deserializes it searching among the buffers in the current project', () => {
|
||||
const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2})
|
||||
const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom)
|
||||
expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('tokenizing', () => {
|
||||
describe('when the buffer is destroyed', () => {
|
||||
beforeEach(() => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
startTokenizing(tokenizedBuffer)
|
||||
})
|
||||
|
||||
it('stops tokenization', () => {
|
||||
tokenizedBuffer.destroy()
|
||||
spyOn(tokenizedBuffer, 'tokenizeNextChunk')
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the buffer contains soft-tabs', () => {
|
||||
beforeEach(() => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
startTokenizing(tokenizedBuffer)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
tokenizedBuffer.destroy()
|
||||
buffer.release()
|
||||
})
|
||||
|
||||
describe('on construction', () =>
|
||||
it('tokenizes lines chunk at a time in the background', () => {
|
||||
const line0 = tokenizedBuffer.tokenizedLines[0]
|
||||
expect(line0).toBeUndefined()
|
||||
|
||||
const line11 = tokenizedBuffer.tokenizedLines[11]
|
||||
expect(line11).toBeUndefined()
|
||||
|
||||
// tokenize chunk 1
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined()
|
||||
|
||||
// tokenize chunk 2
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[9].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined()
|
||||
|
||||
// tokenize last chunk
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[10].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[12].ruleStack != null).toBeTruthy()
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the buffer is partially tokenized', () => {
|
||||
beforeEach(() => {
|
||||
// tokenize chunk 1 only
|
||||
advanceClock()
|
||||
})
|
||||
|
||||
describe('when there is a buffer change inside the tokenized region', () => {
|
||||
describe('when lines are added', () => {
|
||||
it('pushes the invalid rows down', () => {
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
|
||||
buffer.insert([1, 0], '\n\n')
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(7)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when lines are removed', () => {
|
||||
it('pulls the invalid rows up', () => {
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
|
||||
buffer.delete([[1, 0], [3, 0]])
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the change invalidates all the lines before the current invalid region', () => {
|
||||
it('retokenizes the invalidated lines and continues into the valid region', () => {
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
|
||||
buffer.insert([2, 0], '/*')
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(3)
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(8)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is a buffer change surrounding an invalid row', () => {
|
||||
it('pushes the invalid row to the end of the change', () => {
|
||||
buffer.setTextInRange([[4, 0], [6, 0]], '\n\n\n')
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(8)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is a buffer change inside an invalid region', () => {
|
||||
it('does not attempt to tokenize the lines in the change, and preserves the existing invalid row', () => {
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
|
||||
buffer.setTextInRange([[6, 0], [7, 0]], '\n\n\n')
|
||||
expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined()
|
||||
expect(tokenizedBuffer.firstInvalidRow()).toBe(5)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the buffer is fully tokenized', () => {
|
||||
beforeEach(() => fullyTokenize(tokenizedBuffer))
|
||||
|
||||
describe('when there is a buffer change that is smaller than the chunk size', () => {
|
||||
describe('when lines are updated, but none are added or removed', () => {
|
||||
it('updates tokens to reflect the change', () => {
|
||||
buffer.setTextInRange([[0, 0], [2, 0]], 'foo()\n7\n')
|
||||
|
||||
expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual({value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: '7', scopes: ['source.js', 'constant.numeric.decimal.js']})
|
||||
// line 2 is unchanged
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']})
|
||||
})
|
||||
|
||||
describe('when the change invalidates the tokenization of subsequent lines', () => {
|
||||
it('schedules the invalidated lines to be tokenized in the background', () => {
|
||||
buffer.insert([5, 30], '/* */')
|
||||
buffer.insert([2, 0], '/*')
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js'])
|
||||
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
})
|
||||
})
|
||||
|
||||
it('resumes highlighting with the state of the previous line', () => {
|
||||
buffer.insert([0, 0], '/*')
|
||||
buffer.insert([5, 0], '*/')
|
||||
|
||||
buffer.insert([1, 0], 'var ')
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when lines are both updated and removed', () => {
|
||||
it('updates tokens to reflect the change', () => {
|
||||
buffer.setTextInRange([[1, 0], [3, 0]], 'foo()')
|
||||
|
||||
// previous line 0 remains
|
||||
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({value: 'var', scopes: ['source.js', 'storage.type.var.js']})
|
||||
|
||||
// previous line 3 should be combined with input to form line 1
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']})
|
||||
|
||||
// lines below deleted regions should be shifted upward
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'while', scopes: ['source.js', 'keyword.control.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual({value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the change invalidates the tokenization of subsequent lines', () => {
|
||||
it('schedules the invalidated lines to be tokenized in the background', () => {
|
||||
buffer.insert([5, 30], '/* */')
|
||||
buffer.setTextInRange([[2, 0], [3, 0]], '/*')
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js'])
|
||||
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when lines are both updated and inserted', () => {
|
||||
it('updates tokens to reflect the change', () => {
|
||||
buffer.setTextInRange([[1, 0], [2, 0]], 'foo()\nbar()\nbaz()\nquux()')
|
||||
|
||||
// previous line 0 remains
|
||||
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ value: 'var', scopes: ['source.js', 'storage.type.var.js']})
|
||||
|
||||
// 3 new lines inserted
|
||||
expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[0]).toEqual({value: 'bar', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0]).toEqual({value: 'baz', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
|
||||
|
||||
// previous line 2 is joined with quux() on line 4
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual({value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']})
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']})
|
||||
|
||||
// previous line 3 is pushed down to become line 5
|
||||
expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the change invalidates the tokenization of subsequent lines', () => {
|
||||
it('schedules the invalidated lines to be tokenized in the background', () => {
|
||||
buffer.insert([5, 30], '/* */')
|
||||
buffer.insert([2, 0], '/*\nabcde\nabcder')
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js'])
|
||||
|
||||
advanceClock() // tokenize invalidated lines in background
|
||||
expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual(['source.js', 'comment.block.js'])
|
||||
expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe(['source.js', 'comment.block.js'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is an insertion that is larger than the chunk size', () =>
|
||||
it('tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background', () => {
|
||||
const commentBlock = _.multiplyString('// a comment\n', tokenizedBuffer.chunkSize + 2)
|
||||
buffer.insert([0, 0], commentBlock)
|
||||
expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined()
|
||||
|
||||
advanceClock()
|
||||
expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy()
|
||||
expect(tokenizedBuffer.tokenizedLines[6].ruleStack != null).toBeTruthy()
|
||||
})
|
||||
)
|
||||
|
||||
it('does not break out soft tabs across a scope boundary', async () => {
|
||||
await atom.packages.activatePackage('language-gfm')
|
||||
|
||||
tokenizedBuffer.setTabLength(4)
|
||||
tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md'))
|
||||
buffer.setText(' <![]()\n ')
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
let length = 0
|
||||
for (let tag of tokenizedBuffer.tokenizedLines[1].tags) {
|
||||
if (tag > 0) length += tag
|
||||
}
|
||||
|
||||
expect(length).toBe(4)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the buffer contains hard-tabs', () => {
|
||||
beforeEach(async () => {
|
||||
atom.packages.activatePackage('language-coffee-script')
|
||||
|
||||
buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2})
|
||||
startTokenizing(tokenizedBuffer)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
tokenizedBuffer.destroy()
|
||||
buffer.release()
|
||||
})
|
||||
|
||||
describe('when the buffer is fully tokenized', () => {
|
||||
beforeEach(() => fullyTokenize(tokenizedBuffer))
|
||||
})
|
||||
})
|
||||
|
||||
describe('when tokenization completes', () => {
|
||||
it('emits the `tokenized` event', async () => {
|
||||
const editor = await atom.workspace.open('sample.js')
|
||||
|
||||
const tokenizedHandler = jasmine.createSpy('tokenized handler')
|
||||
editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
|
||||
fullyTokenize(editor.tokenizedBuffer)
|
||||
expect(tokenizedHandler.callCount).toBe(1)
|
||||
})
|
||||
|
||||
it("doesn't re-emit the `tokenized` event when it is re-tokenized", async () => {
|
||||
const editor = await atom.workspace.open('sample.js')
|
||||
fullyTokenize(editor.tokenizedBuffer)
|
||||
|
||||
const tokenizedHandler = jasmine.createSpy('tokenized handler')
|
||||
editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
|
||||
editor.getBuffer().insert([0, 0], "'")
|
||||
fullyTokenize(editor.tokenizedBuffer)
|
||||
expect(tokenizedHandler).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the grammar is updated because a grammar it includes is activated', async () => {
|
||||
it('re-emits the `tokenized` event', async () => {
|
||||
const editor = await atom.workspace.open('coffee.coffee')
|
||||
|
||||
const tokenizedHandler = jasmine.createSpy('tokenized handler')
|
||||
editor.tokenizedBuffer.onDidTokenize(tokenizedHandler)
|
||||
fullyTokenize(editor.tokenizedBuffer)
|
||||
tokenizedHandler.reset()
|
||||
|
||||
await atom.packages.activatePackage('language-coffee-script')
|
||||
fullyTokenize(editor.tokenizedBuffer)
|
||||
expect(tokenizedHandler.callCount).toBe(1)
|
||||
})
|
||||
|
||||
it('retokenizes the buffer', async () => {
|
||||
await atom.packages.activatePackage('language-ruby-on-rails')
|
||||
await atom.packages.activatePackage('language-ruby')
|
||||
|
||||
buffer = atom.project.bufferForPathSync()
|
||||
buffer.setText("<div class='name'><%= User.find(2).full_name %></div>")
|
||||
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({
|
||||
value: "<div class='name'>",
|
||||
scopes: ['text.html.ruby']
|
||||
})
|
||||
|
||||
await atom.packages.activatePackage('language-html')
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({
|
||||
value: '<',
|
||||
scopes: ['text.html.ruby', 'meta.tag.block.div.html', 'punctuation.definition.tag.begin.html']
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the buffer is configured with the null grammar', () => {
|
||||
it('does not actually tokenize using the grammar', () => {
|
||||
spyOn(NullGrammar, 'tokenizeLine').andCallThrough()
|
||||
buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar')
|
||||
buffer.setText('a\nb\nc')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2})
|
||||
const tokenizeCallback = jasmine.createSpy('onDidTokenize')
|
||||
tokenizedBuffer.onDidTokenize(tokenizeCallback)
|
||||
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined()
|
||||
expect(tokenizeCallback.callCount).toBe(0)
|
||||
expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled()
|
||||
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined()
|
||||
expect(tokenizeCallback.callCount).toBe(0)
|
||||
expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.tokenForPosition(position)', () => {
|
||||
afterEach(() => {
|
||||
tokenizedBuffer.destroy()
|
||||
buffer.release()
|
||||
})
|
||||
|
||||
it('returns the correct token (regression)', () => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual(['source.js'])
|
||||
expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual(['source.js'])
|
||||
expect(tokenizedBuffer.tokenForPosition([1, 2]).scopes).toEqual(['source.js', 'storage.type.var.js'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('.bufferRangeForScopeAtPosition(selector, position)', () => {
|
||||
beforeEach(() => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
})
|
||||
|
||||
describe('when the selector does not match the token at the position', () =>
|
||||
it('returns a falsy value', () => expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.bogus', [0, 1])).toBeUndefined())
|
||||
)
|
||||
|
||||
describe('when the selector matches a single token at the position', () => {
|
||||
it('returns the range covered by the token', () => {
|
||||
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 1])).toEqual([[0, 0], [0, 3]])
|
||||
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 3])).toEqual([[0, 0], [0, 3]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the selector matches a run of multiple tokens at the position', () => {
|
||||
it('returns the range covered by all contiguous tokens (within a single line)', () => {
|
||||
expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual([[1, 6], [1, 28]])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.tokenizedLineForRow(row)', () => {
|
||||
it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
const grammar = atom.grammars.grammarForScopeName('source.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
|
||||
const line0 = buffer.lineForRow(0)
|
||||
|
||||
const jsScopeStartId = grammar.startIdForScope(grammar.scopeName)
|
||||
const jsScopeEndId = grammar.endIdForScope(grammar.scopeName)
|
||||
startTokenizing(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([jsScopeStartId, line0.length, jsScopeEndId])
|
||||
advanceClock(1)
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).not.toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).not.toEqual([jsScopeStartId, line0.length, jsScopeEndId])
|
||||
|
||||
const nullScopeStartId = NullGrammar.startIdForScope(NullGrammar.scopeName)
|
||||
const nullScopeEndId = NullGrammar.endIdForScope(NullGrammar.scopeName)
|
||||
tokenizedBuffer.setGrammar(NullGrammar)
|
||||
startTokenizing(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined()
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
|
||||
advanceClock(1)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId])
|
||||
})
|
||||
|
||||
it('returns undefined if the requested row is outside the buffer range', () => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
const grammar = atom.grammars.grammarForScopeName('source.js')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
expect(tokenizedBuffer.tokenizedLineForRow(999)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('text decoration layer API', () => {
|
||||
describe('iterator', () => {
|
||||
it('iterates over the syntactic scope boundaries', () => {
|
||||
buffer = new TextBuffer({text: 'var foo = 1 /*\nhello*/var bar = 2\n'})
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
const iterator = tokenizedBuffer.buildIterator()
|
||||
iterator.seek(Point(0, 0))
|
||||
|
||||
const expectedBoundaries = [
|
||||
{position: Point(0, 0), closeTags: [], openTags: ['syntax--source syntax--js', 'syntax--storage syntax--type syntax--var syntax--js']},
|
||||
{position: Point(0, 3), closeTags: ['syntax--storage syntax--type syntax--var syntax--js'], openTags: []},
|
||||
{position: Point(0, 8), closeTags: [], openTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js']},
|
||||
{position: Point(0, 9), closeTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js'], openTags: []},
|
||||
{position: Point(0, 10), closeTags: [], openTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js']},
|
||||
{position: Point(0, 11), closeTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js'], openTags: []},
|
||||
{position: Point(0, 12), closeTags: [], openTags: ['syntax--comment syntax--block syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js']},
|
||||
{position: Point(0, 14), closeTags: ['syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js'], openTags: []},
|
||||
{position: Point(1, 5), closeTags: [], openTags: ['syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js']},
|
||||
{position: Point(1, 7), closeTags: ['syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js', 'syntax--comment syntax--block syntax--js'], openTags: ['syntax--storage syntax--type syntax--var syntax--js']},
|
||||
{position: Point(1, 10), closeTags: ['syntax--storage syntax--type syntax--var syntax--js'], openTags: []},
|
||||
{position: Point(1, 15), closeTags: [], openTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js']},
|
||||
{position: Point(1, 16), closeTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js'], openTags: []},
|
||||
{position: Point(1, 17), closeTags: [], openTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js']},
|
||||
{position: Point(1, 18), closeTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js'], openTags: []}
|
||||
]
|
||||
|
||||
while (true) {
|
||||
const boundary = {
|
||||
position: iterator.getPosition(),
|
||||
closeTags: iterator.getCloseScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId)),
|
||||
openTags: iterator.getOpenScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))
|
||||
}
|
||||
|
||||
expect(boundary).toEqual(expectedBoundaries.shift())
|
||||
if (!iterator.moveToSuccessor()) { break }
|
||||
}
|
||||
|
||||
expect(iterator.seek(Point(0, 1)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
'syntax--source syntax--js',
|
||||
'syntax--storage syntax--type syntax--var syntax--js'
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.seek(Point(0, 8)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
'syntax--source syntax--js'
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 8))
|
||||
expect(iterator.seek(Point(1, 0)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
'syntax--source syntax--js',
|
||||
'syntax--comment syntax--block syntax--js'
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 0))
|
||||
expect(iterator.seek(Point(1, 18)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
'syntax--source syntax--js',
|
||||
'syntax--constant syntax--numeric syntax--decimal syntax--js'
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 18))
|
||||
|
||||
expect(iterator.seek(Point(2, 0)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
'syntax--source syntax--js'
|
||||
])
|
||||
iterator.moveToSuccessor()
|
||||
}) // ensure we don't infinitely loop (regression test)
|
||||
|
||||
it('does not report columns beyond the length of the line', async () => {
|
||||
await atom.packages.activatePackage('language-coffee-script')
|
||||
|
||||
buffer = new TextBuffer({text: '# hello\n# world'})
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
const iterator = tokenizedBuffer.buildIterator()
|
||||
iterator.seek(Point(0, 0))
|
||||
iterator.moveToSuccessor()
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition().column).toBe(7)
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition().column).toBe(0)
|
||||
|
||||
iterator.seek(Point(0, 7))
|
||||
expect(iterator.getPosition().column).toBe(7)
|
||||
|
||||
iterator.seek(Point(0, 8))
|
||||
expect(iterator.getPosition().column).toBe(7)
|
||||
})
|
||||
|
||||
it('correctly terminates scopes at the beginning of the line (regression)', () => {
|
||||
const grammar = atom.grammars.createGrammar('test', {
|
||||
'scopeName': 'text.broken',
|
||||
'name': 'Broken grammar',
|
||||
'patterns': [
|
||||
{'begin': 'start', 'end': '(?=end)', 'name': 'blue.broken'},
|
||||
{'match': '.', 'name': 'yellow.broken'}
|
||||
]
|
||||
})
|
||||
|
||||
buffer = new TextBuffer({text: 'start x\nend x\nx'})
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
|
||||
const iterator = tokenizedBuffer.buildIterator()
|
||||
iterator.seek(Point(1, 0))
|
||||
|
||||
expect(iterator.getPosition()).toEqual([1, 0])
|
||||
expect(iterator.getCloseScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual(['syntax--blue syntax--broken'])
|
||||
expect(iterator.getOpenScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual(['syntax--yellow syntax--broken'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.suggestedIndentForBufferRow', () => {
|
||||
let editor
|
||||
|
||||
describe('javascript', () => {
|
||||
beforeEach(async () => {
|
||||
editor = await atom.workspace.open('sample.js', {autoIndent: false})
|
||||
await atom.packages.activatePackage('language-javascript')
|
||||
})
|
||||
|
||||
it('bases indentation off of the previous non-blank line', () => {
|
||||
expect(editor.suggestedIndentForBufferRow(0)).toBe(0)
|
||||
expect(editor.suggestedIndentForBufferRow(1)).toBe(1)
|
||||
expect(editor.suggestedIndentForBufferRow(2)).toBe(2)
|
||||
expect(editor.suggestedIndentForBufferRow(5)).toBe(3)
|
||||
expect(editor.suggestedIndentForBufferRow(7)).toBe(2)
|
||||
expect(editor.suggestedIndentForBufferRow(9)).toBe(1)
|
||||
expect(editor.suggestedIndentForBufferRow(11)).toBe(1)
|
||||
})
|
||||
|
||||
it('does not take invisibles into account', () => {
|
||||
editor.update({showInvisibles: true})
|
||||
expect(editor.suggestedIndentForBufferRow(0)).toBe(0)
|
||||
expect(editor.suggestedIndentForBufferRow(1)).toBe(1)
|
||||
expect(editor.suggestedIndentForBufferRow(2)).toBe(2)
|
||||
expect(editor.suggestedIndentForBufferRow(5)).toBe(3)
|
||||
expect(editor.suggestedIndentForBufferRow(7)).toBe(2)
|
||||
expect(editor.suggestedIndentForBufferRow(9)).toBe(1)
|
||||
expect(editor.suggestedIndentForBufferRow(11)).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('css', () => {
|
||||
beforeEach(async () => {
|
||||
editor = await atom.workspace.open('css.css', {autoIndent: true})
|
||||
await atom.packages.activatePackage('language-source')
|
||||
await atom.packages.activatePackage('language-css')
|
||||
})
|
||||
|
||||
it('does not return negative values (regression)', () => {
|
||||
editor.setText('.test {\npadding: 0;\n}')
|
||||
expect(editor.suggestedIndentForBufferRow(2)).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('.isFoldableAtRow(row)', () => {
|
||||
beforeEach(() => {
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n')
|
||||
buffer.insert([0, 0], '// multi-line\n// comment\n// block\n')
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2})
|
||||
fullyTokenize(tokenizedBuffer)
|
||||
})
|
||||
|
||||
it('includes the first line of multi-line comments', () => {
|
||||
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) // because of indent
|
||||
expect(tokenizedBuffer.isFoldableAtRow(13)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(14)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(15)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(16)).toBe(false)
|
||||
|
||||
buffer.insert([0, Infinity], '\n')
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(false)
|
||||
|
||||
buffer.undo()
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true)
|
||||
}) // because of indent
|
||||
|
||||
it('includes non-comment lines that precede an increase in indentation', () => {
|
||||
buffer.insert([2, 0], ' ') // commented lines preceding an indent aren't foldable
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(4)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(5)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
|
||||
|
||||
buffer.insert([7, 0], ' ')
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
|
||||
|
||||
buffer.undo()
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
|
||||
|
||||
buffer.insert([7, 0], ' \n x\n')
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
|
||||
|
||||
buffer.insert([9, 0], ' ')
|
||||
|
||||
expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false)
|
||||
expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getFoldableRangesAtIndentLevel', () => {
|
||||
it('returns the ranges that can be folded at the given indent level', () => {
|
||||
buffer = new TextBuffer(dedent `
|
||||
if (a) {
|
||||
b();
|
||||
if (c) {
|
||||
d()
|
||||
if (e) {
|
||||
f()
|
||||
}
|
||||
g()
|
||||
}
|
||||
h()
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer})
|
||||
|
||||
expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2))).toBe(dedent `
|
||||
if (a) {⋯
|
||||
}
|
||||
i()
|
||||
if (j) {⋯
|
||||
}
|
||||
`)
|
||||
|
||||
expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2))).toBe(dedent `
|
||||
if (a) {
|
||||
b();
|
||||
if (c) {⋯
|
||||
}
|
||||
h()
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
|
||||
expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2))).toBe(dedent `
|
||||
if (a) {
|
||||
b();
|
||||
if (c) {
|
||||
d()
|
||||
if (e) {⋯
|
||||
}
|
||||
g()
|
||||
}
|
||||
h()
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getFoldableRanges', () => {
|
||||
it('returns the ranges that can be folded', () => {
|
||||
buffer = new TextBuffer(dedent `
|
||||
if (a) {
|
||||
b();
|
||||
if (c) {
|
||||
d()
|
||||
if (e) {
|
||||
f()
|
||||
}
|
||||
g()
|
||||
}
|
||||
h()
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer})
|
||||
|
||||
expect(tokenizedBuffer.getFoldableRanges(2).map(r => r.toString())).toEqual([
|
||||
...tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2),
|
||||
...tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2),
|
||||
...tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2),
|
||||
].sort((a, b) => (a.start.row - b.start.row) || (a.end.row - b.end.row)).map(r => r.toString()))
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getFoldableRangeContainingPoint', () => {
|
||||
it('returns the range for the smallest fold that contains the given range', () => {
|
||||
buffer = new TextBuffer(dedent `
|
||||
if (a) {
|
||||
b();
|
||||
if (c) {
|
||||
d()
|
||||
if (e) {
|
||||
f()
|
||||
}
|
||||
g()
|
||||
}
|
||||
h()
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
|
||||
tokenizedBuffer = new TokenizedBuffer({buffer})
|
||||
|
||||
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 5), 2)).toBeNull()
|
||||
|
||||
let range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 10), 2)
|
||||
expect(simulateFold([range])).toBe(dedent `
|
||||
if (a) {⋯
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
|
||||
range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity), 2)
|
||||
expect(simulateFold([range])).toBe(dedent `
|
||||
if (a) {⋯
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
|
||||
range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, 20), 2)
|
||||
expect(simulateFold([range])).toBe(dedent `
|
||||
if (a) {
|
||||
b();
|
||||
if (c) {⋯
|
||||
}
|
||||
h()
|
||||
}
|
||||
i()
|
||||
if (j) {
|
||||
k()
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('works for coffee-script', async () => {
|
||||
const editor = await atom.workspace.open('coffee.coffee')
|
||||
await atom.packages.activatePackage('language-coffee-script')
|
||||
buffer = editor.buffer
|
||||
tokenizedBuffer = editor.tokenizedBuffer
|
||||
|
||||
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [20, Infinity]])
|
||||
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [17, Infinity]])
|
||||
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [17, Infinity]])
|
||||
expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(19, Infinity))).toEqual([[19, Infinity], [20, Infinity]])
|
||||
})
|
||||
|
||||
it('works for javascript', async () => {
|
||||
const editor = await atom.workspace.open('sample.js')
|
||||
await atom.packages.activatePackage('language-javascript')
|
||||
buffer = editor.buffer
|
||||
tokenizedBuffer = editor.tokenizedBuffer
|
||||
|
||||
expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [12, Infinity]])
|
||||
expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [9, Infinity]])
|
||||
expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [9, Infinity]])
|
||||
expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(4, Infinity))).toEqual([[4, Infinity], [7, Infinity]])
|
||||
})
|
||||
})
|
||||
|
||||
function simulateFold (ranges) {
|
||||
buffer.transact(() => {
|
||||
for (const range of ranges.reverse()) {
|
||||
buffer.setTextInRange(range, '⋯')
|
||||
}
|
||||
})
|
||||
let text = buffer.getText()
|
||||
buffer.undo()
|
||||
return text
|
||||
}
|
||||
})
|
@ -204,7 +204,7 @@ describe "TooltipManager", ->
|
||||
disposable2.dispose()
|
||||
expect(manager.findTooltips(element).length).toBe(0)
|
||||
|
||||
it "lets us hide tooltips programatically", ->
|
||||
it "lets us hide tooltips programmatically", ->
|
||||
disposable = manager.add element, title: "Title"
|
||||
hover element, ->
|
||||
expect(document.body.querySelector(".tooltip")).not.toBeNull()
|
||||
|
75
spec/uri-handler-registry-spec.js
Normal file
75
spec/uri-handler-registry-spec.js
Normal file
@ -0,0 +1,75 @@
|
||||
/** @babel */
|
||||
|
||||
import url from 'url'
|
||||
|
||||
import {it} from './async-spec-helpers'
|
||||
|
||||
import URIHandlerRegistry from '../src/uri-handler-registry'
|
||||
|
||||
describe('URIHandlerRegistry', () => {
|
||||
let registry
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new URIHandlerRegistry(5)
|
||||
})
|
||||
|
||||
it('handles URIs on a per-host basis', () => {
|
||||
const testPackageSpy = jasmine.createSpy()
|
||||
const otherPackageSpy = jasmine.createSpy()
|
||||
registry.registerHostHandler('test-package', testPackageSpy)
|
||||
registry.registerHostHandler('other-package', otherPackageSpy)
|
||||
|
||||
registry.handleURI('atom://yet-another-package/path')
|
||||
expect(testPackageSpy).not.toHaveBeenCalled()
|
||||
expect(otherPackageSpy).not.toHaveBeenCalled()
|
||||
|
||||
registry.handleURI('atom://test-package/path')
|
||||
expect(testPackageSpy).toHaveBeenCalledWith(url.parse('atom://test-package/path', true), 'atom://test-package/path')
|
||||
expect(otherPackageSpy).not.toHaveBeenCalled()
|
||||
|
||||
registry.handleURI('atom://other-package/path')
|
||||
expect(otherPackageSpy).toHaveBeenCalledWith(url.parse('atom://other-package/path', true), 'atom://other-package/path')
|
||||
})
|
||||
|
||||
it('keeps track of the most recent URIs', () => {
|
||||
const spy1 = jasmine.createSpy()
|
||||
const spy2 = jasmine.createSpy()
|
||||
const changeSpy = jasmine.createSpy()
|
||||
registry.registerHostHandler('one', spy1)
|
||||
registry.registerHostHandler('two', spy2)
|
||||
registry.onHistoryChange(changeSpy)
|
||||
|
||||
const uris = [
|
||||
'atom://one/something?asdf=1',
|
||||
'atom://fake/nothing',
|
||||
'atom://two/other/stuff',
|
||||
'atom://one/more/thing',
|
||||
'atom://two/more/stuff'
|
||||
]
|
||||
|
||||
uris.forEach(u => registry.handleURI(u))
|
||||
|
||||
expect(changeSpy.callCount).toBe(5)
|
||||
expect(registry.getRecentlyHandledURIs()).toEqual(uris.map((u, idx) => {
|
||||
return {id: idx + 1, uri: u, handled: !u.match(/fake/), host: url.parse(u).host}
|
||||
}).reverse())
|
||||
|
||||
registry.handleURI('atom://another/url')
|
||||
expect(changeSpy.callCount).toBe(6)
|
||||
const history = registry.getRecentlyHandledURIs()
|
||||
expect(history.length).toBe(5)
|
||||
expect(history[0].uri).toBe('atom://another/url')
|
||||
expect(history[4].uri).toBe(uris[1])
|
||||
})
|
||||
|
||||
it('refuses to handle bad URLs', () => {
|
||||
[
|
||||
'atom:package/path',
|
||||
'atom:8080://package/path',
|
||||
'user:pass@atom://package/path',
|
||||
'smth://package/path'
|
||||
].forEach(uri => {
|
||||
expect(() => registry.handleURI(uri)).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
@ -1,163 +0,0 @@
|
||||
ViewRegistry = require '../src/view-registry'
|
||||
|
||||
describe "ViewRegistry", ->
|
||||
registry = null
|
||||
|
||||
beforeEach ->
|
||||
registry = new ViewRegistry
|
||||
|
||||
afterEach ->
|
||||
registry.clearDocumentRequests()
|
||||
|
||||
describe "::getView(object)", ->
|
||||
describe "when passed a DOM node", ->
|
||||
it "returns the given DOM node", ->
|
||||
node = document.createElement('div')
|
||||
expect(registry.getView(node)).toBe node
|
||||
|
||||
describe "when passed an object with an element property", ->
|
||||
it "returns the element property if it's an instance of HTMLElement", ->
|
||||
class TestComponent
|
||||
constructor: -> @element = document.createElement('div')
|
||||
|
||||
component = new TestComponent
|
||||
expect(registry.getView(component)).toBe component.element
|
||||
|
||||
describe "when passed an object with a getElement function", ->
|
||||
it "returns the return value of getElement if it's an instance of HTMLElement", ->
|
||||
class TestComponent
|
||||
getElement: ->
|
||||
@myElement ?= document.createElement('div')
|
||||
|
||||
component = new TestComponent
|
||||
expect(registry.getView(component)).toBe component.myElement
|
||||
|
||||
describe "when passed a model object", ->
|
||||
describe "when a view provider is registered matching the object's constructor", ->
|
||||
it "constructs a view element and assigns the model on it", ->
|
||||
class TestModel
|
||||
|
||||
class TestModelSubclass extends TestModel
|
||||
|
||||
class TestView
|
||||
initialize: (@model) -> this
|
||||
|
||||
model = new TestModel
|
||||
|
||||
registry.addViewProvider TestModel, (model) ->
|
||||
new TestView().initialize(model)
|
||||
|
||||
view = registry.getView(model)
|
||||
expect(view instanceof TestView).toBe true
|
||||
expect(view.model).toBe model
|
||||
|
||||
subclassModel = new TestModelSubclass
|
||||
view2 = registry.getView(subclassModel)
|
||||
expect(view2 instanceof TestView).toBe true
|
||||
expect(view2.model).toBe subclassModel
|
||||
|
||||
describe "when a view provider is registered generically, and works with the object", ->
|
||||
it "constructs a view element and assigns the model on it", ->
|
||||
model = {a: 'b'}
|
||||
|
||||
registry.addViewProvider (model) ->
|
||||
if model.a is 'b'
|
||||
element = document.createElement('div')
|
||||
element.className = 'test-element'
|
||||
element
|
||||
|
||||
view = registry.getView({a: 'b'})
|
||||
expect(view.className).toBe 'test-element'
|
||||
|
||||
expect(-> registry.getView({a: 'c'})).toThrow()
|
||||
|
||||
describe "when no view provider is registered for the object's constructor", ->
|
||||
it "throws an exception", ->
|
||||
expect(-> registry.getView(new Object)).toThrow()
|
||||
|
||||
describe "::addViewProvider(providerSpec)", ->
|
||||
it "returns a disposable that can be used to remove the provider", ->
|
||||
class TestModel
|
||||
class TestView
|
||||
initialize: (@model) -> this
|
||||
|
||||
disposable = registry.addViewProvider TestModel, (model) ->
|
||||
new TestView().initialize(model)
|
||||
|
||||
expect(registry.getView(new TestModel) instanceof TestView).toBe true
|
||||
disposable.dispose()
|
||||
expect(-> registry.getView(new TestModel)).toThrow()
|
||||
|
||||
describe "::updateDocument(fn) and ::readDocument(fn)", ->
|
||||
frameRequests = null
|
||||
|
||||
beforeEach ->
|
||||
frameRequests = []
|
||||
spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> frameRequests.push(fn)
|
||||
|
||||
it "performs all pending writes before all pending reads on the next animation frame", ->
|
||||
events = []
|
||||
|
||||
registry.updateDocument -> events.push('write 1')
|
||||
registry.readDocument -> events.push('read 1')
|
||||
registry.readDocument -> events.push('read 2')
|
||||
registry.updateDocument -> events.push('write 2')
|
||||
|
||||
expect(events).toEqual []
|
||||
|
||||
expect(frameRequests.length).toBe 1
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual ['write 1', 'write 2', 'read 1', 'read 2']
|
||||
|
||||
frameRequests = []
|
||||
events = []
|
||||
disposable = registry.updateDocument -> events.push('write 3')
|
||||
registry.updateDocument -> events.push('write 4')
|
||||
registry.readDocument -> events.push('read 3')
|
||||
|
||||
disposable.dispose()
|
||||
|
||||
expect(frameRequests.length).toBe 1
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual ['write 4', 'read 3']
|
||||
|
||||
it "performs writes requested from read callbacks in the same animation frame", ->
|
||||
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
|
||||
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
|
||||
events = []
|
||||
|
||||
registry.updateDocument -> events.push('write 1')
|
||||
registry.readDocument ->
|
||||
registry.updateDocument -> events.push('write from read 1')
|
||||
events.push('read 1')
|
||||
registry.readDocument ->
|
||||
registry.updateDocument -> events.push('write from read 2')
|
||||
events.push('read 2')
|
||||
registry.updateDocument -> events.push('write 2')
|
||||
|
||||
expect(frameRequests.length).toBe 1
|
||||
frameRequests[0]()
|
||||
expect(frameRequests.length).toBe 1
|
||||
|
||||
expect(events).toEqual [
|
||||
'write 1'
|
||||
'write 2'
|
||||
'read 1'
|
||||
'read 2'
|
||||
'write from read 1'
|
||||
'write from read 2'
|
||||
]
|
||||
|
||||
describe "::getNextUpdatePromise()", ->
|
||||
it "returns a promise that resolves at the end of the next update cycle", ->
|
||||
updateCalled = false
|
||||
readCalled = false
|
||||
|
||||
waitsFor 'getNextUpdatePromise to resolve', (done) ->
|
||||
registry.getNextUpdatePromise().then ->
|
||||
expect(updateCalled).toBe true
|
||||
expect(readCalled).toBe true
|
||||
done()
|
||||
|
||||
registry.updateDocument -> updateCalled = true
|
||||
registry.readDocument -> readCalled = true
|
216
spec/view-registry-spec.js
Normal file
216
spec/view-registry-spec.js
Normal file
@ -0,0 +1,216 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const ViewRegistry = require('../src/view-registry')
|
||||
|
||||
describe('ViewRegistry', () => {
|
||||
let registry = null
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new ViewRegistry()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
registry.clearDocumentRequests()
|
||||
})
|
||||
|
||||
describe('::getView(object)', () => {
|
||||
describe('when passed a DOM node', () =>
|
||||
it('returns the given DOM node', () => {
|
||||
const node = document.createElement('div')
|
||||
expect(registry.getView(node)).toBe(node)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when passed an object with an element property', () =>
|
||||
it("returns the element property if it's an instance of HTMLElement", () => {
|
||||
class TestComponent {
|
||||
constructor () {
|
||||
this.element = document.createElement('div')
|
||||
}
|
||||
}
|
||||
|
||||
const component = new TestComponent()
|
||||
expect(registry.getView(component)).toBe(component.element)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when passed an object with a getElement function', () =>
|
||||
it("returns the return value of getElement if it's an instance of HTMLElement", () => {
|
||||
class TestComponent {
|
||||
getElement () {
|
||||
if (this.myElement == null) {
|
||||
this.myElement = document.createElement('div')
|
||||
}
|
||||
return this.myElement
|
||||
}
|
||||
}
|
||||
|
||||
const component = new TestComponent()
|
||||
expect(registry.getView(component)).toBe(component.myElement)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when passed a model object', () => {
|
||||
describe("when a view provider is registered matching the object's constructor", () =>
|
||||
it('constructs a view element and assigns the model on it', () => {
|
||||
class TestModel {}
|
||||
|
||||
class TestModelSubclass extends TestModel {}
|
||||
|
||||
class TestView {
|
||||
initialize (model) {
|
||||
this.model = model
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
const model = new TestModel()
|
||||
|
||||
registry.addViewProvider(TestModel, (model) =>
|
||||
new TestView().initialize(model)
|
||||
)
|
||||
|
||||
const view = registry.getView(model)
|
||||
expect(view instanceof TestView).toBe(true)
|
||||
expect(view.model).toBe(model)
|
||||
|
||||
const subclassModel = new TestModelSubclass()
|
||||
const view2 = registry.getView(subclassModel)
|
||||
expect(view2 instanceof TestView).toBe(true)
|
||||
expect(view2.model).toBe(subclassModel)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when a view provider is registered generically, and works with the object', () =>
|
||||
it('constructs a view element and assigns the model on it', () => {
|
||||
registry.addViewProvider((model) => {
|
||||
if (model.a === 'b') {
|
||||
const element = document.createElement('div')
|
||||
element.className = 'test-element'
|
||||
return element
|
||||
}
|
||||
})
|
||||
|
||||
const view = registry.getView({a: 'b'})
|
||||
expect(view.className).toBe('test-element')
|
||||
|
||||
expect(() => registry.getView({a: 'c'})).toThrow()
|
||||
})
|
||||
)
|
||||
|
||||
describe("when no view provider is registered for the object's constructor", () =>
|
||||
it('throws an exception', () => {
|
||||
expect(() => registry.getView({})).toThrow()
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('::addViewProvider(providerSpec)', () =>
|
||||
it('returns a disposable that can be used to remove the provider', () => {
|
||||
class TestModel {}
|
||||
class TestView {
|
||||
initialize (model) {
|
||||
this.model = model
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
const disposable = registry.addViewProvider(TestModel, (model) =>
|
||||
new TestView().initialize(model)
|
||||
)
|
||||
|
||||
expect(registry.getView(new TestModel()) instanceof TestView).toBe(true)
|
||||
disposable.dispose()
|
||||
expect(() => registry.getView(new TestModel())).toThrow()
|
||||
})
|
||||
)
|
||||
|
||||
describe('::updateDocument(fn) and ::readDocument(fn)', () => {
|
||||
let frameRequests = null
|
||||
|
||||
beforeEach(() => {
|
||||
frameRequests = []
|
||||
spyOn(window, 'requestAnimationFrame').andCallFake(fn => frameRequests.push(fn))
|
||||
})
|
||||
|
||||
it('performs all pending writes before all pending reads on the next animation frame', () => {
|
||||
let events = []
|
||||
|
||||
registry.updateDocument(() => events.push('write 1'))
|
||||
registry.readDocument(() => events.push('read 1'))
|
||||
registry.readDocument(() => events.push('read 2'))
|
||||
registry.updateDocument(() => events.push('write 2'))
|
||||
|
||||
expect(events).toEqual([])
|
||||
|
||||
expect(frameRequests.length).toBe(1)
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual(['write 1', 'write 2', 'read 1', 'read 2'])
|
||||
|
||||
frameRequests = []
|
||||
events = []
|
||||
const disposable = registry.updateDocument(() => events.push('write 3'))
|
||||
registry.updateDocument(() => events.push('write 4'))
|
||||
registry.readDocument(() => events.push('read 3'))
|
||||
|
||||
disposable.dispose()
|
||||
|
||||
expect(frameRequests.length).toBe(1)
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual(['write 4', 'read 3'])
|
||||
})
|
||||
|
||||
it('performs writes requested from read callbacks in the same animation frame', () => {
|
||||
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
|
||||
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
|
||||
const events = []
|
||||
|
||||
registry.updateDocument(() => events.push('write 1'))
|
||||
registry.readDocument(() => {
|
||||
registry.updateDocument(() => events.push('write from read 1'))
|
||||
events.push('read 1')
|
||||
})
|
||||
registry.readDocument(() => {
|
||||
registry.updateDocument(() => events.push('write from read 2'))
|
||||
events.push('read 2')
|
||||
})
|
||||
registry.updateDocument(() => events.push('write 2'))
|
||||
|
||||
expect(frameRequests.length).toBe(1)
|
||||
frameRequests[0]()
|
||||
expect(frameRequests.length).toBe(1)
|
||||
|
||||
expect(events).toEqual([
|
||||
'write 1',
|
||||
'write 2',
|
||||
'read 1',
|
||||
'read 2',
|
||||
'write from read 1',
|
||||
'write from read 2'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::getNextUpdatePromise()', () =>
|
||||
it('returns a promise that resolves at the end of the next update cycle', () => {
|
||||
let updateCalled = false
|
||||
let readCalled = false
|
||||
|
||||
waitsFor('getNextUpdatePromise to resolve', (done) => {
|
||||
registry.getNextUpdatePromise().then(() => {
|
||||
expect(updateCalled).toBe(true)
|
||||
expect(readCalled).toBe(true)
|
||||
done()
|
||||
})
|
||||
|
||||
registry.updateDocument(() => { updateCalled = true })
|
||||
registry.readDocument(() => { readCalled = true })
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
@ -1,209 +0,0 @@
|
||||
KeymapManager = require 'atom-keymap'
|
||||
TextEditor = require '../src/text-editor'
|
||||
WindowEventHandler = require '../src/window-event-handler'
|
||||
{ipcRenderer} = require 'electron'
|
||||
|
||||
describe "WindowEventHandler", ->
|
||||
[windowEventHandler] = []
|
||||
|
||||
beforeEach ->
|
||||
atom.uninstallWindowEventHandler()
|
||||
spyOn(atom, 'hide')
|
||||
initialPath = atom.project.getPaths()[0]
|
||||
spyOn(atom, 'getLoadSettings').andCallFake ->
|
||||
loadSettings = atom.getLoadSettings.originalValue.call(atom)
|
||||
loadSettings.initialPath = initialPath
|
||||
loadSettings
|
||||
atom.project.destroy()
|
||||
windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate})
|
||||
windowEventHandler.initialize(window, document)
|
||||
|
||||
afterEach ->
|
||||
windowEventHandler.unsubscribe()
|
||||
atom.installWindowEventHandler()
|
||||
|
||||
describe "when the window is loaded", ->
|
||||
it "doesn't have .is-blurred on the body tag", ->
|
||||
return if process.platform is 'win32' #Win32TestFailures - can not steal focus
|
||||
expect(document.body.className).not.toMatch("is-blurred")
|
||||
|
||||
describe "when the window is blurred", ->
|
||||
beforeEach ->
|
||||
window.dispatchEvent(new CustomEvent('blur'))
|
||||
|
||||
afterEach ->
|
||||
document.body.classList.remove('is-blurred')
|
||||
|
||||
it "adds the .is-blurred class on the body", ->
|
||||
expect(document.body.className).toMatch("is-blurred")
|
||||
|
||||
describe "when the window is focused again", ->
|
||||
it "removes the .is-blurred class from the body", ->
|
||||
window.dispatchEvent(new CustomEvent('focus'))
|
||||
expect(document.body.className).not.toMatch("is-blurred")
|
||||
|
||||
describe "window:close event", ->
|
||||
it "closes the window", ->
|
||||
spyOn(atom, 'close')
|
||||
window.dispatchEvent(new CustomEvent('window:close'))
|
||||
expect(atom.close).toHaveBeenCalled()
|
||||
|
||||
describe "when a link is clicked", ->
|
||||
it "opens the http/https links in an external application", ->
|
||||
{shell} = require 'electron'
|
||||
spyOn(shell, 'openExternal')
|
||||
|
||||
link = document.createElement('a')
|
||||
linkChild = document.createElement('span')
|
||||
link.appendChild(linkChild)
|
||||
link.href = 'http://github.com'
|
||||
jasmine.attachToDOM(link)
|
||||
fakeEvent = {target: linkChild, currentTarget: link, preventDefault: (->)}
|
||||
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).toHaveBeenCalled()
|
||||
expect(shell.openExternal.argsForCall[0][0]).toBe "http://github.com"
|
||||
shell.openExternal.reset()
|
||||
|
||||
link.href = 'https://github.com'
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).toHaveBeenCalled()
|
||||
expect(shell.openExternal.argsForCall[0][0]).toBe "https://github.com"
|
||||
shell.openExternal.reset()
|
||||
|
||||
link.href = ''
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).not.toHaveBeenCalled()
|
||||
shell.openExternal.reset()
|
||||
|
||||
link.href = '#scroll-me'
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).not.toHaveBeenCalled()
|
||||
|
||||
describe "when a form is submitted", ->
|
||||
it "prevents the default so that the window's URL isn't changed", ->
|
||||
form = document.createElement('form')
|
||||
jasmine.attachToDOM(form)
|
||||
|
||||
defaultPrevented = false
|
||||
event = new CustomEvent('submit', bubbles: true)
|
||||
event.preventDefault = -> defaultPrevented = true
|
||||
form.dispatchEvent(event)
|
||||
expect(defaultPrevented).toBe(true)
|
||||
|
||||
describe "core:focus-next and core:focus-previous", ->
|
||||
describe "when there is no currently focused element", ->
|
||||
it "focuses the element with the lowest/highest tabindex", ->
|
||||
wrapperDiv = document.createElement('div')
|
||||
wrapperDiv.innerHTML = """
|
||||
<div>
|
||||
<button tabindex="2"></button>
|
||||
<input tabindex="1">
|
||||
</div>
|
||||
"""
|
||||
elements = wrapperDiv.firstChild
|
||||
jasmine.attachToDOM(elements)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 1
|
||||
|
||||
document.body.focus()
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 2
|
||||
|
||||
describe "when a tabindex is set on the currently focused element", ->
|
||||
it "focuses the element with the next highest/lowest tabindex, skipping disabled elements", ->
|
||||
wrapperDiv = document.createElement('div')
|
||||
wrapperDiv.innerHTML = """
|
||||
<div>
|
||||
<input tabindex="1">
|
||||
<button tabindex="2"></button>
|
||||
<button tabindex="5"></button>
|
||||
<input tabindex="-1">
|
||||
<input tabindex="3">
|
||||
<button tabindex="7"></button>
|
||||
<input tabindex="9" disabled>
|
||||
</div>
|
||||
"""
|
||||
elements = wrapperDiv.firstChild
|
||||
jasmine.attachToDOM(elements)
|
||||
|
||||
elements.querySelector('[tabindex="1"]').focus()
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 2
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 3
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 5
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 7
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-next", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 1
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 7
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 5
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 3
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 2
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 1
|
||||
|
||||
elements.dispatchEvent(new CustomEvent("core:focus-previous", bubbles: true))
|
||||
expect(document.activeElement.tabIndex).toBe 7
|
||||
|
||||
describe "when keydown events occur on the document", ->
|
||||
it "dispatches the event via the KeymapManager and CommandRegistry", ->
|
||||
dispatchedCommands = []
|
||||
atom.commands.onWillDispatch (command) -> dispatchedCommands.push(command)
|
||||
atom.commands.add '*', 'foo-command': ->
|
||||
atom.keymaps.add 'source-name', '*': {'x': 'foo-command'}
|
||||
|
||||
event = KeymapManager.buildKeydownEvent('x', target: document.createElement('div'))
|
||||
document.dispatchEvent(event)
|
||||
|
||||
expect(dispatchedCommands.length).toBe 1
|
||||
expect(dispatchedCommands[0].type).toBe 'foo-command'
|
||||
|
||||
describe "native key bindings", ->
|
||||
it "correctly dispatches them to active elements with the '.native-key-bindings' class", ->
|
||||
webContentsSpy = jasmine.createSpyObj("webContents", ["copy", "paste"])
|
||||
spyOn(atom.applicationDelegate, "getCurrentWindow").andReturn({
|
||||
webContents: webContentsSpy
|
||||
on: ->
|
||||
})
|
||||
|
||||
nativeKeyBindingsInput = document.createElement("input")
|
||||
nativeKeyBindingsInput.classList.add("native-key-bindings")
|
||||
jasmine.attachToDOM(nativeKeyBindingsInput)
|
||||
nativeKeyBindingsInput.focus()
|
||||
|
||||
atom.dispatchApplicationMenuCommand("core:copy")
|
||||
atom.dispatchApplicationMenuCommand("core:paste")
|
||||
|
||||
expect(webContentsSpy.copy).toHaveBeenCalled()
|
||||
expect(webContentsSpy.paste).toHaveBeenCalled()
|
||||
|
||||
webContentsSpy.copy.reset()
|
||||
webContentsSpy.paste.reset()
|
||||
|
||||
normalInput = document.createElement("input")
|
||||
jasmine.attachToDOM(normalInput)
|
||||
normalInput.focus()
|
||||
|
||||
atom.dispatchApplicationMenuCommand("core:copy")
|
||||
atom.dispatchApplicationMenuCommand("core:paste")
|
||||
|
||||
expect(webContentsSpy.copy).not.toHaveBeenCalled()
|
||||
expect(webContentsSpy.paste).not.toHaveBeenCalled()
|
228
spec/window-event-handler-spec.js
Normal file
228
spec/window-event-handler-spec.js
Normal file
@ -0,0 +1,228 @@
|
||||
const KeymapManager = require('atom-keymap')
|
||||
const WindowEventHandler = require('../src/window-event-handler')
|
||||
|
||||
describe('WindowEventHandler', () => {
|
||||
let windowEventHandler
|
||||
|
||||
beforeEach(() => {
|
||||
atom.uninstallWindowEventHandler()
|
||||
spyOn(atom, 'hide')
|
||||
const initialPath = atom.project.getPaths()[0]
|
||||
spyOn(atom, 'getLoadSettings').andCallFake(() => {
|
||||
const loadSettings = atom.getLoadSettings.originalValue.call(atom)
|
||||
loadSettings.initialPath = initialPath
|
||||
return loadSettings
|
||||
})
|
||||
atom.project.destroy()
|
||||
windowEventHandler = new WindowEventHandler({atomEnvironment: atom, applicationDelegate: atom.applicationDelegate})
|
||||
windowEventHandler.initialize(window, document)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
windowEventHandler.unsubscribe()
|
||||
atom.installWindowEventHandler()
|
||||
})
|
||||
|
||||
describe('when the window is loaded', () =>
|
||||
it("doesn't have .is-blurred on the body tag", () => {
|
||||
if (process.platform === 'win32') { return } // Win32TestFailures - can not steal focus
|
||||
expect(document.body.className).not.toMatch('is-blurred')
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the window is blurred', () => {
|
||||
beforeEach(() => window.dispatchEvent(new CustomEvent('blur')))
|
||||
|
||||
afterEach(() => document.body.classList.remove('is-blurred'))
|
||||
|
||||
it('adds the .is-blurred class on the body', () => expect(document.body.className).toMatch('is-blurred'))
|
||||
|
||||
describe('when the window is focused again', () =>
|
||||
it('removes the .is-blurred class from the body', () => {
|
||||
window.dispatchEvent(new CustomEvent('focus'))
|
||||
expect(document.body.className).not.toMatch('is-blurred')
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('window:close event', () =>
|
||||
it('closes the window', () => {
|
||||
spyOn(atom, 'close')
|
||||
window.dispatchEvent(new CustomEvent('window:close'))
|
||||
expect(atom.close).toHaveBeenCalled()
|
||||
})
|
||||
)
|
||||
|
||||
describe('when a link is clicked', () =>
|
||||
it('opens the http/https links in an external application', () => {
|
||||
const {shell} = require('electron')
|
||||
spyOn(shell, 'openExternal')
|
||||
|
||||
const link = document.createElement('a')
|
||||
const linkChild = document.createElement('span')
|
||||
link.appendChild(linkChild)
|
||||
link.href = 'http://github.com'
|
||||
jasmine.attachToDOM(link)
|
||||
const fakeEvent = {target: linkChild, currentTarget: link, preventDefault: () => {}}
|
||||
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).toHaveBeenCalled()
|
||||
expect(shell.openExternal.argsForCall[0][0]).toBe('http://github.com')
|
||||
shell.openExternal.reset()
|
||||
|
||||
link.href = 'https://github.com'
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).toHaveBeenCalled()
|
||||
expect(shell.openExternal.argsForCall[0][0]).toBe('https://github.com')
|
||||
shell.openExternal.reset()
|
||||
|
||||
link.href = ''
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).not.toHaveBeenCalled()
|
||||
shell.openExternal.reset()
|
||||
|
||||
link.href = '#scroll-me'
|
||||
windowEventHandler.handleLinkClick(fakeEvent)
|
||||
expect(shell.openExternal).not.toHaveBeenCalled()
|
||||
})
|
||||
)
|
||||
|
||||
describe('when a form is submitted', () =>
|
||||
it("prevents the default so that the window's URL isn't changed", () => {
|
||||
const form = document.createElement('form')
|
||||
jasmine.attachToDOM(form)
|
||||
|
||||
let defaultPrevented = false
|
||||
const event = new CustomEvent('submit', {bubbles: true})
|
||||
event.preventDefault = () => { defaultPrevented = true }
|
||||
form.dispatchEvent(event)
|
||||
expect(defaultPrevented).toBe(true)
|
||||
})
|
||||
)
|
||||
|
||||
describe('core:focus-next and core:focus-previous', () => {
|
||||
describe('when there is no currently focused element', () =>
|
||||
it('focuses the element with the lowest/highest tabindex', () => {
|
||||
const wrapperDiv = document.createElement('div')
|
||||
wrapperDiv.innerHTML = `
|
||||
<div>
|
||||
<button tabindex="2"></button>
|
||||
<input tabindex="1">
|
||||
</div>
|
||||
`.trim()
|
||||
const elements = wrapperDiv.firstChild
|
||||
jasmine.attachToDOM(elements)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(1)
|
||||
|
||||
document.body.focus()
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(2)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when a tabindex is set on the currently focused element', () =>
|
||||
it('focuses the element with the next highest/lowest tabindex, skipping disabled elements', () => {
|
||||
const wrapperDiv = document.createElement('div')
|
||||
wrapperDiv.innerHTML = `
|
||||
<div>
|
||||
<input tabindex="1">
|
||||
<button tabindex="2"></button>
|
||||
<button tabindex="5"></button>
|
||||
<input tabindex="-1">
|
||||
<input tabindex="3">
|
||||
<button tabindex="7"></button>
|
||||
<input tabindex="9" disabled>
|
||||
</div>
|
||||
`.trim()
|
||||
const elements = wrapperDiv.firstChild
|
||||
jasmine.attachToDOM(elements)
|
||||
|
||||
elements.querySelector('[tabindex="1"]').focus()
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(2)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(3)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(5)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(7)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-next', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(1)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(7)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(5)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(3)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(2)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(1)
|
||||
|
||||
elements.dispatchEvent(new CustomEvent('core:focus-previous', {bubbles: true}))
|
||||
expect(document.activeElement.tabIndex).toBe(7)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
describe('when keydown events occur on the document', () =>
|
||||
it('dispatches the event via the KeymapManager and CommandRegistry', () => {
|
||||
const dispatchedCommands = []
|
||||
atom.commands.onWillDispatch(command => dispatchedCommands.push(command))
|
||||
atom.commands.add('*', {'foo-command': () => {}})
|
||||
atom.keymaps.add('source-name', {'*': {'x': 'foo-command'}})
|
||||
|
||||
const event = KeymapManager.buildKeydownEvent('x', {target: document.createElement('div')})
|
||||
document.dispatchEvent(event)
|
||||
|
||||
expect(dispatchedCommands.length).toBe(1)
|
||||
expect(dispatchedCommands[0].type).toBe('foo-command')
|
||||
})
|
||||
)
|
||||
|
||||
describe('native key bindings', () =>
|
||||
it("correctly dispatches them to active elements with the '.native-key-bindings' class", () => {
|
||||
const webContentsSpy = jasmine.createSpyObj('webContents', ['copy', 'paste'])
|
||||
spyOn(atom.applicationDelegate, 'getCurrentWindow').andReturn({
|
||||
webContents: webContentsSpy,
|
||||
on: () => {}
|
||||
})
|
||||
|
||||
const nativeKeyBindingsInput = document.createElement('input')
|
||||
nativeKeyBindingsInput.classList.add('native-key-bindings')
|
||||
jasmine.attachToDOM(nativeKeyBindingsInput)
|
||||
nativeKeyBindingsInput.focus()
|
||||
|
||||
atom.dispatchApplicationMenuCommand('core:copy')
|
||||
atom.dispatchApplicationMenuCommand('core:paste')
|
||||
|
||||
expect(webContentsSpy.copy).toHaveBeenCalled()
|
||||
expect(webContentsSpy.paste).toHaveBeenCalled()
|
||||
|
||||
webContentsSpy.copy.reset()
|
||||
webContentsSpy.paste.reset()
|
||||
|
||||
const normalInput = document.createElement('input')
|
||||
jasmine.attachToDOM(normalInput)
|
||||
normalInput.focus()
|
||||
|
||||
atom.dispatchApplicationMenuCommand('core:copy')
|
||||
atom.dispatchApplicationMenuCommand('core:paste')
|
||||
|
||||
expect(webContentsSpy.copy).not.toHaveBeenCalled()
|
||||
expect(webContentsSpy.paste).not.toHaveBeenCalled()
|
||||
})
|
||||
)
|
||||
})
|
@ -1585,15 +1585,15 @@ i = /test/; #FIXME\
|
||||
atom2.project.deserialize(atom.project.serialize())
|
||||
atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers)
|
||||
|
||||
expect(atom2.grammars.getGrammars().map(grammar => grammar.name).sort()).toEqual([
|
||||
'CoffeeScript',
|
||||
'CoffeeScript (Literate)',
|
||||
'JSDoc',
|
||||
'JavaScript',
|
||||
'Null Grammar',
|
||||
'Regular Expression Replacement (JavaScript)',
|
||||
'Regular Expressions (JavaScript)',
|
||||
'TODO'
|
||||
expect(atom2.grammars.getGrammars().map(grammar => grammar.scopeName).sort()).toEqual([
|
||||
'source.coffee',
|
||||
'source.js',
|
||||
'source.js.regexp',
|
||||
'source.js.regexp.replacement',
|
||||
'source.jsdoc',
|
||||
'source.litcoffee',
|
||||
'text.plain.null-grammar',
|
||||
'text.todo'
|
||||
])
|
||||
|
||||
atom2.destroy()
|
||||
@ -2773,7 +2773,7 @@ i = /test/; #FIXME\
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the core.allowPendingPaneItems option is falsey', () => {
|
||||
describe('when the core.allowPendingPaneItems option is falsy', () => {
|
||||
it('does not open item with `pending: true` option as pending', () => {
|
||||
let pane = null
|
||||
atom.config.set('core.allowPendingPaneItems', false)
|
||||
|
@ -233,6 +233,14 @@ class ApplicationDelegate
|
||||
new Disposable ->
|
||||
ipcRenderer.removeListener('context-command', outerCallback)
|
||||
|
||||
onURIMessage: (callback) ->
|
||||
outerCallback = (event, args...) ->
|
||||
callback(args...)
|
||||
|
||||
ipcRenderer.on('uri-message', outerCallback)
|
||||
new Disposable ->
|
||||
ipcRenderer.removeListener('uri-message', outerCallback)
|
||||
|
||||
onDidRequestUnload: (callback) ->
|
||||
outerCallback = (event, message) ->
|
||||
callback(event).then (shouldUnload) ->
|
||||
|
@ -22,6 +22,7 @@ Config = require './config'
|
||||
KeymapManager = require './keymap-extensions'
|
||||
TooltipManager = require './tooltip-manager'
|
||||
CommandRegistry = require './command-registry'
|
||||
URIHandlerRegistry = require './uri-handler-registry'
|
||||
GrammarRegistry = require './grammar-registry'
|
||||
{HistoryManager, HistoryProject} = require './history-manager'
|
||||
ReopenProjectMenuManager = require './reopen-project-menu-manager'
|
||||
@ -31,6 +32,7 @@ ThemeManager = require './theme-manager'
|
||||
MenuManager = require './menu-manager'
|
||||
ContextMenuManager = require './context-menu-manager'
|
||||
CommandInstaller = require './command-installer'
|
||||
ProtocolHandlerInstaller = require './protocol-handler-installer'
|
||||
Project = require './project'
|
||||
TitleBar = require './title-bar'
|
||||
Workspace = require './workspace'
|
||||
@ -146,12 +148,14 @@ class AtomEnvironment extends Model
|
||||
@keymaps = new KeymapManager({notificationManager: @notifications})
|
||||
@tooltips = new TooltipManager(keymapManager: @keymaps, viewRegistry: @views)
|
||||
@commands = new CommandRegistry
|
||||
@uriHandlerRegistry = new URIHandlerRegistry
|
||||
@grammars = new GrammarRegistry({@config})
|
||||
@styles = new StyleManager()
|
||||
@packages = new PackageManager({
|
||||
@config, styleManager: @styles,
|
||||
commandRegistry: @commands, keymapManager: @keymaps, notificationManager: @notifications,
|
||||
grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views
|
||||
grammarRegistry: @grammars, deserializerManager: @deserializers, viewRegistry: @views,
|
||||
uriHandlerRegistry: @uriHandlerRegistry
|
||||
})
|
||||
@themes = new ThemeManager({
|
||||
packageManager: @packages, @config, styleManager: @styles,
|
||||
@ -165,6 +169,7 @@ class AtomEnvironment extends Model
|
||||
|
||||
@project = new Project({notificationManager: @notifications, packageManager: @packages, @config, @applicationDelegate})
|
||||
@commandInstaller = new CommandInstaller(@applicationDelegate)
|
||||
@protocolHandlerInstaller = new ProtocolHandlerInstaller()
|
||||
|
||||
@textEditors = new TextEditorRegistry({
|
||||
@config, grammarRegistry: @grammars, assert: @assert.bind(this),
|
||||
@ -231,6 +236,7 @@ class AtomEnvironment extends Model
|
||||
@themes.initialize({@configDirPath, resourcePath, safeMode, devMode})
|
||||
|
||||
@commandInstaller.initialize(@getVersion())
|
||||
@protocolHandlerInstaller.initialize(@config, @notifications)
|
||||
@autoUpdater.initialize()
|
||||
|
||||
@config.load()
|
||||
@ -327,20 +333,14 @@ class AtomEnvironment extends Model
|
||||
|
||||
@contextMenu.clear()
|
||||
|
||||
@packages.reset()
|
||||
|
||||
@workspace.reset(@packages)
|
||||
@registerDefaultOpeners()
|
||||
|
||||
@project.reset(@packages)
|
||||
|
||||
@workspace.subscribeToEvents()
|
||||
|
||||
@grammars.clear()
|
||||
|
||||
@textEditors.clear()
|
||||
|
||||
@views.clear()
|
||||
@packages.reset().then =>
|
||||
@workspace.reset(@packages)
|
||||
@registerDefaultOpeners()
|
||||
@project.reset(@packages)
|
||||
@workspace.subscribeToEvents()
|
||||
@grammars.clear()
|
||||
@textEditors.clear()
|
||||
@views.clear()
|
||||
|
||||
destroy: ->
|
||||
return if not @project
|
||||
@ -355,6 +355,7 @@ class AtomEnvironment extends Model
|
||||
@stylesElement.remove()
|
||||
@config.unobserveUserConfig()
|
||||
@autoUpdater.destroy()
|
||||
@uriHandlerRegistry.destroy()
|
||||
|
||||
@uninstallWindowEventHandler()
|
||||
|
||||
@ -444,7 +445,9 @@ class AtomEnvironment extends Model
|
||||
getVersion: ->
|
||||
@appVersion ?= @getLoadSettings().appVersion
|
||||
|
||||
# Returns the release channel as a {String}. Will return one of `'dev', 'beta', 'stable'`
|
||||
# Public: Gets the release channel of the Atom application.
|
||||
#
|
||||
# Returns the release channel as a {String}. Will return one of `dev`, `beta`, or `stable`.
|
||||
getReleaseChannel: ->
|
||||
version = @getVersion()
|
||||
if version.indexOf('beta') > -1
|
||||
@ -693,6 +696,7 @@ class AtomEnvironment extends Model
|
||||
@disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this)))
|
||||
@disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this)))
|
||||
@disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this)))
|
||||
@disposables.add(@applicationDelegate.onURIMessage(@dispatchURIMessage.bind(this)))
|
||||
@disposables.add @applicationDelegate.onDidRequestUnload =>
|
||||
@saveState({isUnloading: true})
|
||||
.catch(console.error)
|
||||
@ -701,6 +705,11 @@ class AtomEnvironment extends Model
|
||||
windowCloseRequested: true,
|
||||
projectHasPaths: @project.getPaths().length > 0
|
||||
})
|
||||
.then (closing) =>
|
||||
if closing
|
||||
@packages.deactivatePackages().then -> closing
|
||||
else
|
||||
closing
|
||||
|
||||
@listenForUpdates()
|
||||
|
||||
@ -757,7 +766,6 @@ class AtomEnvironment extends Model
|
||||
return if not @project
|
||||
|
||||
@storeWindowBackground()
|
||||
@packages.deactivatePackages()
|
||||
@saveBlobStoreSync()
|
||||
@unloaded = true
|
||||
|
||||
@ -826,6 +834,9 @@ class AtomEnvironment extends Model
|
||||
|
||||
# Essential: A flexible way to open a dialog akin to an alert dialog.
|
||||
#
|
||||
# If the dialog is closed (via `Esc` key or `X` in the top corner) without selecting a button
|
||||
# the first button will be clicked unless a "Cancel" or "No" button is provided.
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# ```coffee
|
||||
@ -843,7 +854,7 @@ class AtomEnvironment extends Model
|
||||
# * `buttons` (optional) Either an array of strings or an object where keys are
|
||||
# button names and the values are callbacks to invoke when clicked.
|
||||
#
|
||||
# Returns the chosen button index {Number} if the buttons option was an array.
|
||||
# Returns the chosen button index {Number} if the buttons option is an array or the return value of the callback if the buttons option is an object.
|
||||
confirm: (params={}) ->
|
||||
@applicationDelegate.confirm(params)
|
||||
|
||||
@ -997,11 +1008,18 @@ class AtomEnvironment extends Model
|
||||
|
||||
@setFullScreen(state.fullScreen)
|
||||
|
||||
missingProjectPaths = []
|
||||
|
||||
@packages.packageStates = state.packageStates ? {}
|
||||
|
||||
startTime = Date.now()
|
||||
if state.project?
|
||||
projectPromise = @project.deserialize(state.project, @deserializers)
|
||||
.catch (err) =>
|
||||
if err.missingProjectPaths?
|
||||
missingProjectPaths.push(err.missingProjectPaths...)
|
||||
else
|
||||
@notifications.addError "Unable to deserialize project", description: err.message, stack: err.stack
|
||||
else
|
||||
projectPromise = Promise.resolve()
|
||||
|
||||
@ -1014,6 +1032,19 @@ class AtomEnvironment extends Model
|
||||
@workspace.deserialize(state.workspace, @deserializers) if state.workspace?
|
||||
@deserializeTimings.workspace = Date.now() - startTime
|
||||
|
||||
if missingProjectPaths.length > 0
|
||||
count = if missingProjectPaths.length is 1 then '' else missingProjectPaths.length + ' '
|
||||
noun = if missingProjectPaths.length is 1 then 'directory' else 'directories'
|
||||
toBe = if missingProjectPaths.length is 1 then 'is' else 'are'
|
||||
escaped = missingProjectPaths.map (projectPath) -> "`#{projectPath}`"
|
||||
group = switch escaped.length
|
||||
when 1 then escaped[0]
|
||||
when 2 then "#{escaped[0]} and #{escaped[1]}"
|
||||
else escaped[..-2].join(", ") + ", and #{escaped[escaped.length - 1]}"
|
||||
|
||||
@notifications.addError "Unable to open #{count}project #{noun}",
|
||||
description: "Project #{noun} #{group} #{toBe} no longer on disk."
|
||||
|
||||
getStateKey: (paths) ->
|
||||
if paths?.length > 0
|
||||
sha1 = crypto.createHash('sha1').update(paths.slice().sort().join("\n")).digest('hex')
|
||||
@ -1068,6 +1099,14 @@ class AtomEnvironment extends Model
|
||||
dispatchContextMenuCommand: (command, args...) ->
|
||||
@commands.dispatch(@contextMenu.activeElement, command, args)
|
||||
|
||||
dispatchURIMessage: (uri) ->
|
||||
if @packages.hasLoadedInitialPackages()
|
||||
@uriHandlerRegistry.handleURI(uri)
|
||||
else
|
||||
sub = @packages.onDidLoadInitialPackages ->
|
||||
sub.dispose()
|
||||
@uriHandlerRegistry.handleURI(uri)
|
||||
|
||||
openLocations: (locations) ->
|
||||
needsProjectPaths = @project?.getPaths().length is 0
|
||||
|
||||
|
18
src/color.js
18
src/color.js
@ -112,27 +112,15 @@ export default class Color {
|
||||
|
||||
function parseColor (colorString) {
|
||||
const color = parseInt(colorString, 10)
|
||||
if (isNaN(color)) {
|
||||
return 0
|
||||
} else {
|
||||
return Math.min(Math.max(color, 0), 255)
|
||||
}
|
||||
return isNaN(color) ? 0 : Math.min(Math.max(color, 0), 255)
|
||||
}
|
||||
|
||||
function parseAlpha (alphaString) {
|
||||
const alpha = parseFloat(alphaString)
|
||||
if (isNaN(alpha)) {
|
||||
return 1
|
||||
} else {
|
||||
return Math.min(Math.max(alpha, 0), 1)
|
||||
}
|
||||
return isNaN(alpha) ? 1 : Math.min(Math.max(alpha, 0), 1)
|
||||
}
|
||||
|
||||
function numberToHexString (number) {
|
||||
const hex = number.toString(16)
|
||||
if (number < 16) {
|
||||
return `0${hex}`
|
||||
} else {
|
||||
return hex
|
||||
}
|
||||
return number < 16 ? `0${hex}` : hex
|
||||
}
|
||||
|
@ -84,20 +84,20 @@ function compileFileAtPath (compiler, filePath, extension) {
|
||||
var sourceCode = fs.readFileSync(filePath, 'utf8')
|
||||
if (compiler.shouldCompile(sourceCode, filePath)) {
|
||||
var cachePath = compiler.getCachePath(sourceCode, filePath)
|
||||
var compiledCode = readCachedJavascript(cachePath)
|
||||
var compiledCode = readCachedJavaScript(cachePath)
|
||||
if (compiledCode != null) {
|
||||
cacheStats[extension].hits++
|
||||
} else {
|
||||
cacheStats[extension].misses++
|
||||
compiledCode = compiler.compile(sourceCode, filePath)
|
||||
writeCachedJavascript(cachePath, compiledCode)
|
||||
writeCachedJavaScript(cachePath, compiledCode)
|
||||
}
|
||||
return compiledCode
|
||||
}
|
||||
return sourceCode
|
||||
}
|
||||
|
||||
function readCachedJavascript (relativeCachePath) {
|
||||
function readCachedJavaScript (relativeCachePath) {
|
||||
var cachePath = path.join(cacheDirectory, relativeCachePath)
|
||||
if (fs.isFileSync(cachePath)) {
|
||||
try {
|
||||
@ -107,7 +107,7 @@ function readCachedJavascript (relativeCachePath) {
|
||||
return null
|
||||
}
|
||||
|
||||
function writeCachedJavascript (relativeCachePath, code) {
|
||||
function writeCachedJavaScript (relativeCachePath, code) {
|
||||
var cachePath = path.join(cacheDirectory, relativeCachePath)
|
||||
fs.writeFileSync(cachePath, code, 'utf8')
|
||||
}
|
||||
@ -153,7 +153,7 @@ exports.install = function (resourcesPath, nodeRequire) {
|
||||
if (!compiler) compiler = COMPILERS['.js']
|
||||
|
||||
try {
|
||||
var fileData = readCachedJavascript(compiler.getCachePath(sourceCode, filePath))
|
||||
var fileData = readCachedJavaScript(compiler.getCachePath(sourceCode, filePath))
|
||||
} catch (error) {
|
||||
console.warn('Error reading compiled file', error.stack)
|
||||
return null
|
||||
|
@ -17,7 +17,7 @@ const configSchema = {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
title: 'Exclude VCS Ignored Paths',
|
||||
description: 'Files and directories ignored by the current project\'s VCS system will be ignored by some packages, such as the fuzzy finder and find and replace. For example, projects using Git have these paths defined in the .gitignore file. Individual packages might have additional config settings for ignoring VCS ignored files and folders.'
|
||||
description: 'Files and directories ignored by the current project\'s VCS will be ignored by some packages, such as the fuzzy finder and find and replace. For example, projects using Git have these paths defined in the .gitignore file. Individual packages might have additional config settings for ignoring VCS ignored files and folders.'
|
||||
},
|
||||
followSymlinks: {
|
||||
type: 'boolean',
|
||||
@ -55,6 +55,25 @@ const configSchema = {
|
||||
}
|
||||
}
|
||||
},
|
||||
uriHandlerRegistration: {
|
||||
type: 'string',
|
||||
default: 'prompt',
|
||||
description: 'When should Atom register itself as the default handler for atom:// URIs',
|
||||
enum: [
|
||||
{
|
||||
value: 'prompt',
|
||||
description: 'Prompt to register Atom as the default atom:// URI handler'
|
||||
},
|
||||
{
|
||||
value: 'always',
|
||||
description: 'Always become the default atom:// URI handler automatically'
|
||||
},
|
||||
{
|
||||
value: 'never',
|
||||
description: 'Never become the default atom:// URI handler'
|
||||
}
|
||||
]
|
||||
},
|
||||
themes: {
|
||||
type: 'array',
|
||||
default: ['one-dark-ui', 'one-dark-syntax'],
|
||||
@ -409,6 +428,12 @@ const configSchema = {
|
||||
minimum: 1,
|
||||
description: 'Identifies the length of a line which is used when wrapping text with the `Soft Wrap At Preferred Line Length` setting enabled, in number of characters.'
|
||||
},
|
||||
maxScreenLineLength: {
|
||||
type: 'integer',
|
||||
default: 500,
|
||||
minimum: 500,
|
||||
description: 'Defines the maximum width of the editor window before soft wrapping is enforced, in number of characters.'
|
||||
},
|
||||
tabLength: {
|
||||
type: 'integer',
|
||||
default: 2,
|
||||
|
@ -4,7 +4,7 @@ fs = require 'fs-plus'
|
||||
CSON = require 'season'
|
||||
path = require 'path'
|
||||
async = require 'async'
|
||||
pathWatcher = require 'pathwatcher'
|
||||
{watchPath} = require './path-watcher'
|
||||
{
|
||||
getValueAtKeyPath, setValueAtKeyPath, deleteValueAtKeyPath,
|
||||
pushKeyPath, splitKeyPath,
|
||||
@ -221,7 +221,7 @@ ScopeDescriptor = require './scope-descriptor'
|
||||
# #### object / Grouping other types
|
||||
#
|
||||
# A config setting with the type `object` allows grouping a set of config
|
||||
# settings. The group will be visualy separated and has its own group headline.
|
||||
# settings. The group will be visually separated and has its own group headline.
|
||||
# The sub options must be listed under a `properties` key.
|
||||
#
|
||||
# ```coffee
|
||||
@ -417,17 +417,24 @@ class Config
|
||||
@defaultSettings = {}
|
||||
@settings = {}
|
||||
@scopedSettingsStore = new ScopedPropertyStore
|
||||
|
||||
@settingsLoaded = false
|
||||
@savePending = false
|
||||
@configFileHasErrors = false
|
||||
@transactDepth = 0
|
||||
@savePending = false
|
||||
@requestLoad = _.debounce(@loadUserConfig, 100)
|
||||
@requestSave = =>
|
||||
@savePending = true
|
||||
debouncedSave.call(this)
|
||||
save = =>
|
||||
@pendingOperations = []
|
||||
|
||||
@requestLoad = _.debounce =>
|
||||
@loadUserConfig()
|
||||
, 100
|
||||
|
||||
debouncedSave = _.debounce =>
|
||||
@savePending = false
|
||||
@save()
|
||||
debouncedSave = _.debounce(save, 100)
|
||||
, 100
|
||||
@requestSave = =>
|
||||
@savePending = true
|
||||
debouncedSave()
|
||||
|
||||
shouldNotAccessFileSystem: -> not @enablePersistence
|
||||
|
||||
@ -647,6 +654,10 @@ class Config
|
||||
# * `false` if the value was not able to be coerced to the type specified in the setting's schema.
|
||||
set: ->
|
||||
[keyPath, value, options] = arguments
|
||||
|
||||
unless @settingsLoaded
|
||||
@pendingOperations.push => @set.call(this, keyPath, value, options)
|
||||
|
||||
scopeSelector = options?.scopeSelector
|
||||
source = options?.source
|
||||
shouldSave = options?.save ? true
|
||||
@ -667,7 +678,8 @@ class Config
|
||||
else
|
||||
@setRawValue(keyPath, value)
|
||||
|
||||
@requestSave() if source is @getUserConfigPath() and shouldSave and not @configFileHasErrors
|
||||
if source is @getUserConfigPath() and shouldSave and not @configFileHasErrors and @settingsLoaded
|
||||
@requestSave()
|
||||
true
|
||||
|
||||
# Essential: Restore the setting at `keyPath` to its default value.
|
||||
@ -677,6 +689,9 @@ class Config
|
||||
# * `scopeSelector` (optional) {String}. See {::set}
|
||||
# * `source` (optional) {String}. See {::set}
|
||||
unset: (keyPath, options) ->
|
||||
unless @settingsLoaded
|
||||
@pendingOperations.push => @unset.call(this, keyPath, options)
|
||||
|
||||
{scopeSelector, source} = options ? {}
|
||||
source ?= @getUserConfigPath()
|
||||
|
||||
@ -688,7 +703,8 @@ class Config
|
||||
setValueAtKeyPath(settings, keyPath, undefined)
|
||||
settings = withoutEmptyObjects(settings)
|
||||
@set(null, settings, {scopeSelector, source, priority: @priorityForSource(source)}) if settings?
|
||||
@requestSave()
|
||||
if source is @getUserConfigPath() and not @configFileHasErrors and @settingsLoaded
|
||||
@requestSave()
|
||||
else
|
||||
@scopedSettingsStore.removePropertiesForSourceAndSelector(source, scopeSelector)
|
||||
@emitChangeEvent()
|
||||
@ -848,21 +864,27 @@ class Config
|
||||
|
||||
loadUserConfig: ->
|
||||
return if @shouldNotAccessFileSystem()
|
||||
return if @savePending
|
||||
|
||||
try
|
||||
unless fs.existsSync(@configFilePath)
|
||||
fs.makeTreeSync(path.dirname(@configFilePath))
|
||||
CSON.writeFileSync(@configFilePath, {})
|
||||
CSON.writeFileSync(@configFilePath, {}, {flag: 'wx'}) # fails if file exists
|
||||
catch error
|
||||
@configFileHasErrors = true
|
||||
@notifyFailure("Failed to initialize `#{path.basename(@configFilePath)}`", error.stack)
|
||||
return
|
||||
if error.code isnt 'EEXIST'
|
||||
@configFileHasErrors = true
|
||||
@notifyFailure("Failed to initialize `#{path.basename(@configFilePath)}`", error.stack)
|
||||
return
|
||||
|
||||
try
|
||||
unless @savePending
|
||||
userConfig = CSON.readFileSync(@configFilePath)
|
||||
@resetUserSettings(userConfig)
|
||||
@configFileHasErrors = false
|
||||
userConfig = CSON.readFileSync(@configFilePath)
|
||||
userConfig = {} if userConfig is null
|
||||
|
||||
unless isPlainObject(userConfig)
|
||||
throw new Error("`#{path.basename(@configFilePath)}` must contain valid JSON or CSON")
|
||||
|
||||
@resetUserSettings(userConfig)
|
||||
@configFileHasErrors = false
|
||||
catch error
|
||||
@configFileHasErrors = true
|
||||
message = "Failed to load `#{path.basename(@configFilePath)}`"
|
||||
@ -880,8 +902,10 @@ class Config
|
||||
return if @shouldNotAccessFileSystem()
|
||||
|
||||
try
|
||||
@watchSubscription ?= pathWatcher.watch @configFilePath, (eventType) =>
|
||||
@requestLoad() if eventType is 'change' and @watchSubscription?
|
||||
@watchSubscriptionPromise ?= watchPath @configFilePath, {}, (events) =>
|
||||
for {action} in events
|
||||
if action in ['created', 'modified', 'renamed'] and @watchSubscriptionPromise?
|
||||
@requestLoad()
|
||||
catch error
|
||||
@notifyFailure """
|
||||
Unable to watch path: `#{path.basename(@configFilePath)}`. Make sure you have permissions to
|
||||
@ -890,9 +914,11 @@ class Config
|
||||
[watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path
|
||||
"""
|
||||
|
||||
@watchSubscriptionPromise
|
||||
|
||||
unobserveUserConfig: ->
|
||||
@watchSubscription?.close()
|
||||
@watchSubscription = null
|
||||
@watchSubscriptionPromise?.then (watcher) -> watcher?.dispose()
|
||||
@watchSubscriptionPromise = null
|
||||
|
||||
notifyFailure: (errorMessage, detail) ->
|
||||
@notificationManager?.addError(errorMessage, {detail, dismissable: true})
|
||||
@ -915,11 +941,6 @@ class Config
|
||||
###
|
||||
|
||||
resetUserSettings: (newSettings) ->
|
||||
unless isPlainObject(newSettings)
|
||||
@settings = {}
|
||||
@emitChangeEvent()
|
||||
return
|
||||
|
||||
if newSettings.global?
|
||||
newSettings['*'] = newSettings.global
|
||||
delete newSettings.global
|
||||
@ -932,8 +953,11 @@ class Config
|
||||
|
||||
@transact =>
|
||||
@settings = {}
|
||||
@settingsLoaded = true
|
||||
@set(key, value, save: false) for key, value of newSettings
|
||||
return
|
||||
if @pendingOperations.length
|
||||
op() for op in @pendingOperations
|
||||
@pendingOperations = []
|
||||
|
||||
getRawValue: (keyPath, options) ->
|
||||
unless options?.excludeSources?.indexOf(@getUserConfigPath()) >= 0
|
||||
|
@ -1,659 +0,0 @@
|
||||
{Point, Range} = require 'text-buffer'
|
||||
{Emitter} = require 'event-kit'
|
||||
_ = require 'underscore-plus'
|
||||
Model = require './model'
|
||||
|
||||
EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g
|
||||
|
||||
# Extended: The `Cursor` class represents the little blinking line identifying
|
||||
# where text can be inserted.
|
||||
#
|
||||
# Cursors belong to {TextEditor}s and have some metadata attached in the form
|
||||
# of a {DisplayMarker}.
|
||||
module.exports =
|
||||
class Cursor extends Model
|
||||
screenPosition: null
|
||||
bufferPosition: null
|
||||
goalColumn: null
|
||||
|
||||
# Instantiated by a {TextEditor}
|
||||
constructor: ({@editor, @marker, id}) ->
|
||||
@emitter = new Emitter
|
||||
@assignId(id)
|
||||
|
||||
destroy: ->
|
||||
@marker.destroy()
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Public: Calls your `callback` when the cursor has been moved.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `event` {Object}
|
||||
# * `oldBufferPosition` {Point}
|
||||
# * `oldScreenPosition` {Point}
|
||||
# * `newBufferPosition` {Point}
|
||||
# * `newScreenPosition` {Point}
|
||||
# * `textChanged` {Boolean}
|
||||
# * `Cursor` {Cursor} that triggered the event
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangePosition: (callback) ->
|
||||
@emitter.on 'did-change-position', callback
|
||||
|
||||
# Public: Calls your `callback` when the cursor is destroyed
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Managing Cursor Position
|
||||
###
|
||||
|
||||
# Public: Moves a cursor to a given screen position.
|
||||
#
|
||||
# * `screenPosition` {Array} of two numbers: the screen row, and the screen column.
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `autoscroll` A Boolean which, if `true`, scrolls the {TextEditor} to wherever
|
||||
# the cursor moves to.
|
||||
setScreenPosition: (screenPosition, options={}) ->
|
||||
@changePosition options, =>
|
||||
@marker.setHeadScreenPosition(screenPosition, options)
|
||||
|
||||
# Public: Returns the screen position of the cursor as a {Point}.
|
||||
getScreenPosition: ->
|
||||
@marker.getHeadScreenPosition()
|
||||
|
||||
# Public: Moves a cursor to a given buffer position.
|
||||
#
|
||||
# * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column.
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `autoscroll` {Boolean} indicating whether to autoscroll to the new
|
||||
# position. Defaults to `true` if this is the most recently added cursor,
|
||||
# `false` otherwise.
|
||||
setBufferPosition: (bufferPosition, options={}) ->
|
||||
@changePosition options, =>
|
||||
@marker.setHeadBufferPosition(bufferPosition, options)
|
||||
|
||||
# Public: Returns the current buffer position as an Array.
|
||||
getBufferPosition: ->
|
||||
@marker.getHeadBufferPosition()
|
||||
|
||||
# Public: Returns the cursor's current screen row.
|
||||
getScreenRow: ->
|
||||
@getScreenPosition().row
|
||||
|
||||
# Public: Returns the cursor's current screen column.
|
||||
getScreenColumn: ->
|
||||
@getScreenPosition().column
|
||||
|
||||
# Public: Retrieves the cursor's current buffer row.
|
||||
getBufferRow: ->
|
||||
@getBufferPosition().row
|
||||
|
||||
# Public: Returns the cursor's current buffer column.
|
||||
getBufferColumn: ->
|
||||
@getBufferPosition().column
|
||||
|
||||
# Public: Returns the cursor's current buffer row of text excluding its line
|
||||
# ending.
|
||||
getCurrentBufferLine: ->
|
||||
@editor.lineTextForBufferRow(@getBufferRow())
|
||||
|
||||
# Public: Returns whether the cursor is at the start of a line.
|
||||
isAtBeginningOfLine: ->
|
||||
@getBufferPosition().column is 0
|
||||
|
||||
# Public: Returns whether the cursor is on the line return character.
|
||||
isAtEndOfLine: ->
|
||||
@getBufferPosition().isEqual(@getCurrentLineBufferRange().end)
|
||||
|
||||
###
|
||||
Section: Cursor Position Details
|
||||
###
|
||||
|
||||
# Public: Returns the underlying {DisplayMarker} for the cursor.
|
||||
# Useful with overlay {Decoration}s.
|
||||
getMarker: -> @marker
|
||||
|
||||
# Public: Identifies if the cursor is surrounded by whitespace.
|
||||
#
|
||||
# "Surrounded" here means that the character directly before and after the
|
||||
# cursor are both whitespace.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isSurroundedByWhitespace: ->
|
||||
{row, column} = @getBufferPosition()
|
||||
range = [[row, column - 1], [row, column + 1]]
|
||||
/^\s+$/.test @editor.getTextInBufferRange(range)
|
||||
|
||||
# Public: Returns whether the cursor is currently between a word and non-word
|
||||
# character. The non-word characters are defined by the
|
||||
# `editor.nonWordCharacters` config value.
|
||||
#
|
||||
# This method returns false if the character before or after the cursor is
|
||||
# whitespace.
|
||||
#
|
||||
# Returns a Boolean.
|
||||
isBetweenWordAndNonWord: ->
|
||||
return false if @isAtBeginningOfLine() or @isAtEndOfLine()
|
||||
|
||||
{row, column} = @getBufferPosition()
|
||||
range = [[row, column - 1], [row, column + 1]]
|
||||
[before, after] = @editor.getTextInBufferRange(range)
|
||||
return false if /\s/.test(before) or /\s/.test(after)
|
||||
|
||||
nonWordCharacters = @getNonWordCharacters()
|
||||
nonWordCharacters.includes(before) isnt nonWordCharacters.includes(after)
|
||||
|
||||
# Public: Returns whether this cursor is between a word's start and end.
|
||||
#
|
||||
# * `options` (optional) {Object}
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp}).
|
||||
#
|
||||
# Returns a {Boolean}
|
||||
isInsideWord: (options) ->
|
||||
{row, column} = @getBufferPosition()
|
||||
range = [[row, column], [row, Infinity]]
|
||||
@editor.getTextInBufferRange(range).search(options?.wordRegex ? @wordRegExp()) is 0
|
||||
|
||||
# Public: Returns the indentation level of the current line.
|
||||
getIndentLevel: ->
|
||||
if @editor.getSoftTabs()
|
||||
@getBufferColumn() / @editor.getTabLength()
|
||||
else
|
||||
@getBufferColumn()
|
||||
|
||||
# Public: Retrieves the scope descriptor for the cursor's current position.
|
||||
#
|
||||
# Returns a {ScopeDescriptor}
|
||||
getScopeDescriptor: ->
|
||||
@editor.scopeDescriptorForBufferPosition(@getBufferPosition())
|
||||
|
||||
# Public: Returns true if this cursor has no non-whitespace characters before
|
||||
# its current position.
|
||||
hasPrecedingCharactersOnLine: ->
|
||||
bufferPosition = @getBufferPosition()
|
||||
line = @editor.lineTextForBufferRow(bufferPosition.row)
|
||||
firstCharacterColumn = line.search(/\S/)
|
||||
|
||||
if firstCharacterColumn is -1
|
||||
false
|
||||
else
|
||||
bufferPosition.column > firstCharacterColumn
|
||||
|
||||
# Public: Identifies if this cursor is the last in the {TextEditor}.
|
||||
#
|
||||
# "Last" is defined as the most recently added cursor.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isLastCursor: ->
|
||||
this is @editor.getLastCursor()
|
||||
|
||||
###
|
||||
Section: Moving the Cursor
|
||||
###
|
||||
|
||||
# Public: Moves the cursor up one screen row.
|
||||
#
|
||||
# * `rowCount` (optional) {Number} number of rows to move (default: 1)
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `moveToEndOfSelection` if true, move to the left of the selection if a
|
||||
# selection exists.
|
||||
moveUp: (rowCount=1, {moveToEndOfSelection}={}) ->
|
||||
range = @marker.getScreenRange()
|
||||
if moveToEndOfSelection and not range.isEmpty()
|
||||
{row, column} = range.start
|
||||
else
|
||||
{row, column} = @getScreenPosition()
|
||||
|
||||
column = @goalColumn if @goalColumn?
|
||||
@setScreenPosition({row: row - rowCount, column: column}, skipSoftWrapIndentation: true)
|
||||
@goalColumn = column
|
||||
|
||||
# Public: Moves the cursor down one screen row.
|
||||
#
|
||||
# * `rowCount` (optional) {Number} number of rows to move (default: 1)
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `moveToEndOfSelection` if true, move to the left of the selection if a
|
||||
# selection exists.
|
||||
moveDown: (rowCount=1, {moveToEndOfSelection}={}) ->
|
||||
range = @marker.getScreenRange()
|
||||
if moveToEndOfSelection and not range.isEmpty()
|
||||
{row, column} = range.end
|
||||
else
|
||||
{row, column} = @getScreenPosition()
|
||||
|
||||
column = @goalColumn if @goalColumn?
|
||||
@setScreenPosition({row: row + rowCount, column: column}, skipSoftWrapIndentation: true)
|
||||
@goalColumn = column
|
||||
|
||||
# Public: Moves the cursor left one screen column.
|
||||
#
|
||||
# * `columnCount` (optional) {Number} number of columns to move (default: 1)
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `moveToEndOfSelection` if true, move to the left of the selection if a
|
||||
# selection exists.
|
||||
moveLeft: (columnCount=1, {moveToEndOfSelection}={}) ->
|
||||
range = @marker.getScreenRange()
|
||||
if moveToEndOfSelection and not range.isEmpty()
|
||||
@setScreenPosition(range.start)
|
||||
else
|
||||
{row, column} = @getScreenPosition()
|
||||
|
||||
while columnCount > column and row > 0
|
||||
columnCount -= column
|
||||
column = @editor.lineLengthForScreenRow(--row)
|
||||
columnCount-- # subtract 1 for the row move
|
||||
|
||||
column = column - columnCount
|
||||
@setScreenPosition({row, column}, clipDirection: 'backward')
|
||||
|
||||
# Public: Moves the cursor right one screen column.
|
||||
#
|
||||
# * `columnCount` (optional) {Number} number of columns to move (default: 1)
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `moveToEndOfSelection` if true, move to the right of the selection if a
|
||||
# selection exists.
|
||||
moveRight: (columnCount=1, {moveToEndOfSelection}={}) ->
|
||||
range = @marker.getScreenRange()
|
||||
if moveToEndOfSelection and not range.isEmpty()
|
||||
@setScreenPosition(range.end)
|
||||
else
|
||||
{row, column} = @getScreenPosition()
|
||||
maxLines = @editor.getScreenLineCount()
|
||||
rowLength = @editor.lineLengthForScreenRow(row)
|
||||
columnsRemainingInLine = rowLength - column
|
||||
|
||||
while columnCount > columnsRemainingInLine and row < maxLines - 1
|
||||
columnCount -= columnsRemainingInLine
|
||||
columnCount-- # subtract 1 for the row move
|
||||
|
||||
column = 0
|
||||
rowLength = @editor.lineLengthForScreenRow(++row)
|
||||
columnsRemainingInLine = rowLength
|
||||
|
||||
column = column + columnCount
|
||||
@setScreenPosition({row, column}, clipDirection: 'forward')
|
||||
|
||||
# Public: Moves the cursor to the top of the buffer.
|
||||
moveToTop: ->
|
||||
@setBufferPosition([0, 0])
|
||||
|
||||
# Public: Moves the cursor to the bottom of the buffer.
|
||||
moveToBottom: ->
|
||||
@setBufferPosition(@editor.getEofBufferPosition())
|
||||
|
||||
# Public: Moves the cursor to the beginning of the line.
|
||||
moveToBeginningOfScreenLine: ->
|
||||
@setScreenPosition([@getScreenRow(), 0])
|
||||
|
||||
# Public: Moves the cursor to the beginning of the buffer line.
|
||||
moveToBeginningOfLine: ->
|
||||
@setBufferPosition([@getBufferRow(), 0])
|
||||
|
||||
# Public: Moves the cursor to the beginning of the first character in the
|
||||
# line.
|
||||
moveToFirstCharacterOfLine: ->
|
||||
screenRow = @getScreenRow()
|
||||
screenLineStart = @editor.clipScreenPosition([screenRow, 0], skipSoftWrapIndentation: true)
|
||||
screenLineEnd = [screenRow, Infinity]
|
||||
screenLineBufferRange = @editor.bufferRangeForScreenRange([screenLineStart, screenLineEnd])
|
||||
|
||||
firstCharacterColumn = null
|
||||
@editor.scanInBufferRange /\S/, screenLineBufferRange, ({range, stop}) ->
|
||||
firstCharacterColumn = range.start.column
|
||||
stop()
|
||||
|
||||
if firstCharacterColumn? and firstCharacterColumn isnt @getBufferColumn()
|
||||
targetBufferColumn = firstCharacterColumn
|
||||
else
|
||||
targetBufferColumn = screenLineBufferRange.start.column
|
||||
|
||||
@setBufferPosition([screenLineBufferRange.start.row, targetBufferColumn])
|
||||
|
||||
# Public: Moves the cursor to the end of the line.
|
||||
moveToEndOfScreenLine: ->
|
||||
@setScreenPosition([@getScreenRow(), Infinity])
|
||||
|
||||
# Public: Moves the cursor to the end of the buffer line.
|
||||
moveToEndOfLine: ->
|
||||
@setBufferPosition([@getBufferRow(), Infinity])
|
||||
|
||||
# Public: Moves the cursor to the beginning of the word.
|
||||
moveToBeginningOfWord: ->
|
||||
@setBufferPosition(@getBeginningOfCurrentWordBufferPosition())
|
||||
|
||||
# Public: Moves the cursor to the end of the word.
|
||||
moveToEndOfWord: ->
|
||||
if position = @getEndOfCurrentWordBufferPosition()
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the beginning of the next word.
|
||||
moveToBeginningOfNextWord: ->
|
||||
if position = @getBeginningOfNextWordBufferPosition()
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the previous word boundary.
|
||||
moveToPreviousWordBoundary: ->
|
||||
if position = @getPreviousWordBoundaryBufferPosition()
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the next word boundary.
|
||||
moveToNextWordBoundary: ->
|
||||
if position = @getNextWordBoundaryBufferPosition()
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the previous subword boundary.
|
||||
moveToPreviousSubwordBoundary: ->
|
||||
options = {wordRegex: @subwordRegExp(backwards: true)}
|
||||
if position = @getPreviousWordBoundaryBufferPosition(options)
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the next subword boundary.
|
||||
moveToNextSubwordBoundary: ->
|
||||
options = {wordRegex: @subwordRegExp()}
|
||||
if position = @getNextWordBoundaryBufferPosition(options)
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the beginning of the buffer line, skipping all
|
||||
# whitespace.
|
||||
skipLeadingWhitespace: ->
|
||||
position = @getBufferPosition()
|
||||
scanRange = @getCurrentLineBufferRange()
|
||||
endOfLeadingWhitespace = null
|
||||
@editor.scanInBufferRange /^[ \t]*/, scanRange, ({range}) ->
|
||||
endOfLeadingWhitespace = range.end
|
||||
|
||||
@setBufferPosition(endOfLeadingWhitespace) if endOfLeadingWhitespace.isGreaterThan(position)
|
||||
|
||||
# Public: Moves the cursor to the beginning of the next paragraph
|
||||
moveToBeginningOfNextParagraph: ->
|
||||
if position = @getBeginningOfNextParagraphBufferPosition()
|
||||
@setBufferPosition(position)
|
||||
|
||||
# Public: Moves the cursor to the beginning of the previous paragraph
|
||||
moveToBeginningOfPreviousParagraph: ->
|
||||
if position = @getBeginningOfPreviousParagraphBufferPosition()
|
||||
@setBufferPosition(position)
|
||||
|
||||
###
|
||||
Section: Local Positions and Ranges
|
||||
###
|
||||
|
||||
# Public: Returns buffer position of previous word boundary. It might be on
|
||||
# the current word, or the previous word.
|
||||
#
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp})
|
||||
getPreviousWordBoundaryBufferPosition: (options = {}) ->
|
||||
currentBufferPosition = @getBufferPosition()
|
||||
previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row)
|
||||
scanRange = [[previousNonBlankRow ? 0, 0], currentBufferPosition]
|
||||
|
||||
beginningOfWordPosition = null
|
||||
@editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) ->
|
||||
if range.start.row < currentBufferPosition.row and currentBufferPosition.column > 0
|
||||
# force it to stop at the beginning of each line
|
||||
beginningOfWordPosition = new Point(currentBufferPosition.row, 0)
|
||||
else if range.end.isLessThan(currentBufferPosition)
|
||||
beginningOfWordPosition = range.end
|
||||
else
|
||||
beginningOfWordPosition = range.start
|
||||
|
||||
if not beginningOfWordPosition?.isEqual(currentBufferPosition)
|
||||
stop()
|
||||
|
||||
beginningOfWordPosition or currentBufferPosition
|
||||
|
||||
# Public: Returns buffer position of the next word boundary. It might be on
|
||||
# the current word, or the previous word.
|
||||
#
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp})
|
||||
getNextWordBoundaryBufferPosition: (options = {}) ->
|
||||
currentBufferPosition = @getBufferPosition()
|
||||
scanRange = [currentBufferPosition, @editor.getEofBufferPosition()]
|
||||
|
||||
endOfWordPosition = null
|
||||
@editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) ->
|
||||
if range.start.row > currentBufferPosition.row
|
||||
# force it to stop at the beginning of each line
|
||||
endOfWordPosition = new Point(range.start.row, 0)
|
||||
else if range.start.isGreaterThan(currentBufferPosition)
|
||||
endOfWordPosition = range.start
|
||||
else
|
||||
endOfWordPosition = range.end
|
||||
|
||||
if not endOfWordPosition?.isEqual(currentBufferPosition)
|
||||
stop()
|
||||
|
||||
endOfWordPosition or currentBufferPosition
|
||||
|
||||
# Public: Retrieves the buffer position of where the current word starts.
|
||||
#
|
||||
# * `options` (optional) An {Object} with the following keys:
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp}).
|
||||
# * `includeNonWordCharacters` A {Boolean} indicating whether to include
|
||||
# non-word characters in the default word regex.
|
||||
# Has no effect if wordRegex is set.
|
||||
# * `allowPrevious` A {Boolean} indicating whether the beginning of the
|
||||
# previous word can be returned.
|
||||
#
|
||||
# Returns a {Range}.
|
||||
getBeginningOfCurrentWordBufferPosition: (options = {}) ->
|
||||
allowPrevious = options.allowPrevious ? true
|
||||
currentBufferPosition = @getBufferPosition()
|
||||
previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) ? 0
|
||||
scanRange = [[previousNonBlankRow, 0], currentBufferPosition]
|
||||
|
||||
beginningOfWordPosition = null
|
||||
@editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, matchText, stop}) ->
|
||||
# Ignore 'empty line' matches between '\r' and '\n'
|
||||
return if matchText is '' and range.start.column isnt 0
|
||||
|
||||
if range.start.isLessThan(currentBufferPosition)
|
||||
if range.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious
|
||||
beginningOfWordPosition = range.start
|
||||
stop()
|
||||
|
||||
if beginningOfWordPosition?
|
||||
beginningOfWordPosition
|
||||
else if allowPrevious
|
||||
new Point(0, 0)
|
||||
else
|
||||
currentBufferPosition
|
||||
|
||||
# Public: Retrieves the buffer position of where the current word ends.
|
||||
#
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp})
|
||||
# * `includeNonWordCharacters` A Boolean indicating whether to include
|
||||
# non-word characters in the default word regex. Has no effect if
|
||||
# wordRegex is set.
|
||||
#
|
||||
# Returns a {Range}.
|
||||
getEndOfCurrentWordBufferPosition: (options = {}) ->
|
||||
allowNext = options.allowNext ? true
|
||||
currentBufferPosition = @getBufferPosition()
|
||||
scanRange = [currentBufferPosition, @editor.getEofBufferPosition()]
|
||||
|
||||
endOfWordPosition = null
|
||||
@editor.scanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, matchText, stop}) ->
|
||||
# Ignore 'empty line' matches between '\r' and '\n'
|
||||
return if matchText is '' and range.start.column isnt 0
|
||||
|
||||
if range.end.isGreaterThan(currentBufferPosition)
|
||||
if allowNext or range.start.isLessThanOrEqual(currentBufferPosition)
|
||||
endOfWordPosition = range.end
|
||||
stop()
|
||||
|
||||
endOfWordPosition ? currentBufferPosition
|
||||
|
||||
# Public: Retrieves the buffer position of where the next word starts.
|
||||
#
|
||||
# * `options` (optional) {Object}
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp}).
|
||||
#
|
||||
# Returns a {Range}
|
||||
getBeginningOfNextWordBufferPosition: (options = {}) ->
|
||||
currentBufferPosition = @getBufferPosition()
|
||||
start = if @isInsideWord(options) then @getEndOfCurrentWordBufferPosition(options) else currentBufferPosition
|
||||
scanRange = [start, @editor.getEofBufferPosition()]
|
||||
|
||||
beginningOfNextWordPosition = null
|
||||
@editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) ->
|
||||
beginningOfNextWordPosition = range.start
|
||||
stop()
|
||||
|
||||
beginningOfNextWordPosition or currentBufferPosition
|
||||
|
||||
# Public: Returns the buffer Range occupied by the word located under the cursor.
|
||||
#
|
||||
# * `options` (optional) {Object}
|
||||
# * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
# (default: {::wordRegExp}).
|
||||
getCurrentWordBufferRange: (options={}) ->
|
||||
startOptions = Object.assign(_.clone(options), allowPrevious: false)
|
||||
endOptions = Object.assign(_.clone(options), allowNext: false)
|
||||
new Range(@getBeginningOfCurrentWordBufferPosition(startOptions), @getEndOfCurrentWordBufferPosition(endOptions))
|
||||
|
||||
# Public: Returns the buffer Range for the current line.
|
||||
#
|
||||
# * `options` (optional) {Object}
|
||||
# * `includeNewline` A {Boolean} which controls whether the Range should
|
||||
# include the newline.
|
||||
getCurrentLineBufferRange: (options) ->
|
||||
@editor.bufferRangeForBufferRow(@getBufferRow(), options)
|
||||
|
||||
# Public: Retrieves the range for the current paragraph.
|
||||
#
|
||||
# A paragraph is defined as a block of text surrounded by empty lines or comments.
|
||||
#
|
||||
# Returns a {Range}.
|
||||
getCurrentParagraphBufferRange: ->
|
||||
@editor.languageMode.rowRangeForParagraphAtBufferRow(@getBufferRow())
|
||||
|
||||
# Public: Returns the characters preceding the cursor in the current word.
|
||||
getCurrentWordPrefix: ->
|
||||
@editor.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()])
|
||||
|
||||
###
|
||||
Section: Visibility
|
||||
###
|
||||
|
||||
###
|
||||
Section: Comparing to another cursor
|
||||
###
|
||||
|
||||
# Public: Compare this cursor's buffer position to another cursor's buffer position.
|
||||
#
|
||||
# See {Point::compare} for more details.
|
||||
#
|
||||
# * `otherCursor`{Cursor} to compare against
|
||||
compare: (otherCursor) ->
|
||||
@getBufferPosition().compare(otherCursor.getBufferPosition())
|
||||
|
||||
###
|
||||
Section: Utilities
|
||||
###
|
||||
|
||||
# Public: Deselects the current selection.
|
||||
clearSelection: (options) ->
|
||||
@selection?.clear(options)
|
||||
|
||||
# Public: Get the RegExp used by the cursor to determine what a "word" is.
|
||||
#
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `includeNonWordCharacters` A {Boolean} indicating whether to include
|
||||
# non-word characters in the regex. (default: true)
|
||||
#
|
||||
# Returns a {RegExp}.
|
||||
wordRegExp: (options) ->
|
||||
nonWordCharacters = _.escapeRegExp(@getNonWordCharacters())
|
||||
source = "^[\t ]*$|[^\\s#{nonWordCharacters}]+"
|
||||
if options?.includeNonWordCharacters ? true
|
||||
source += "|" + "[#{nonWordCharacters}]+"
|
||||
new RegExp(source, "g")
|
||||
|
||||
# Public: Get the RegExp used by the cursor to determine what a "subword" is.
|
||||
#
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `backwards` A {Boolean} indicating whether to look forwards or backwards
|
||||
# for the next subword. (default: false)
|
||||
#
|
||||
# Returns a {RegExp}.
|
||||
subwordRegExp: (options={}) ->
|
||||
nonWordCharacters = @getNonWordCharacters()
|
||||
lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF'
|
||||
uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE'
|
||||
snakeCamelSegment = "[#{uppercaseLetters}]?[#{lowercaseLetters}]+"
|
||||
segments = [
|
||||
"^[\t ]+",
|
||||
"[\t ]+$",
|
||||
"[#{uppercaseLetters}]+(?![#{lowercaseLetters}])",
|
||||
"\\d+"
|
||||
]
|
||||
if options.backwards
|
||||
segments.push("#{snakeCamelSegment}_*")
|
||||
segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+\\s*")
|
||||
else
|
||||
segments.push("_*#{snakeCamelSegment}")
|
||||
segments.push("\\s*[#{_.escapeRegExp(nonWordCharacters)}]+")
|
||||
segments.push("_+")
|
||||
new RegExp(segments.join("|"), "g")
|
||||
|
||||
###
|
||||
Section: Private
|
||||
###
|
||||
|
||||
getNonWordCharacters: ->
|
||||
@editor.getNonWordCharacters(@getScopeDescriptor().getScopesArray())
|
||||
|
||||
changePosition: (options, fn) ->
|
||||
@clearSelection(autoscroll: false)
|
||||
fn()
|
||||
@autoscroll() if options.autoscroll ? @isLastCursor()
|
||||
|
||||
getScreenRange: ->
|
||||
{row, column} = @getScreenPosition()
|
||||
new Range(new Point(row, column), new Point(row, column + 1))
|
||||
|
||||
autoscroll: (options = {}) ->
|
||||
options.clip = false
|
||||
@editor.scrollToScreenRange(@getScreenRange(), options)
|
||||
|
||||
getBeginningOfNextParagraphBufferPosition: ->
|
||||
start = @getBufferPosition()
|
||||
eof = @editor.getEofBufferPosition()
|
||||
scanRange = [start, eof]
|
||||
|
||||
{row, column} = eof
|
||||
position = new Point(row, column - 1)
|
||||
|
||||
@editor.scanInBufferRange EmptyLineRegExp, scanRange, ({range, stop}) ->
|
||||
position = range.start.traverse(Point(1, 0))
|
||||
stop() unless position.isEqual(start)
|
||||
position
|
||||
|
||||
getBeginningOfPreviousParagraphBufferPosition: ->
|
||||
start = @getBufferPosition()
|
||||
|
||||
{row, column} = start
|
||||
scanRange = [[row-1, column], [0, 0]]
|
||||
position = new Point(0, 0)
|
||||
@editor.backwardsScanInBufferRange EmptyLineRegExp, scanRange, ({range, stop}) ->
|
||||
position = range.start.traverse(Point(1, 0))
|
||||
stop() unless position.isEqual(start)
|
||||
position
|
754
src/cursor.js
Normal file
754
src/cursor.js
Normal file
@ -0,0 +1,754 @@
|
||||
const {Point, Range} = require('text-buffer')
|
||||
const {Emitter} = require('event-kit')
|
||||
const _ = require('underscore-plus')
|
||||
const Model = require('./model')
|
||||
|
||||
const EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g
|
||||
|
||||
// Extended: The `Cursor` class represents the little blinking line identifying
|
||||
// where text can be inserted.
|
||||
//
|
||||
// Cursors belong to {TextEditor}s and have some metadata attached in the form
|
||||
// of a {DisplayMarker}.
|
||||
module.exports =
|
||||
class Cursor extends Model {
|
||||
// Instantiated by a {TextEditor}
|
||||
constructor (params) {
|
||||
super(params)
|
||||
this.editor = params.editor
|
||||
this.marker = params.marker
|
||||
this.emitter = new Emitter()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.marker.destroy()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Public: Calls your `callback` when the cursor has been moved.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
// * `event` {Object}
|
||||
// * `oldBufferPosition` {Point}
|
||||
// * `oldScreenPosition` {Point}
|
||||
// * `newBufferPosition` {Point}
|
||||
// * `newScreenPosition` {Point}
|
||||
// * `textChanged` {Boolean}
|
||||
// * `cursor` {Cursor} that triggered the event
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangePosition (callback) {
|
||||
return this.emitter.on('did-change-position', callback)
|
||||
}
|
||||
|
||||
// Public: Calls your `callback` when the cursor is destroyed
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy (callback) {
|
||||
return this.emitter.once('did-destroy', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Managing Cursor Position
|
||||
*/
|
||||
|
||||
// Public: Moves a cursor to a given screen position.
|
||||
//
|
||||
// * `screenPosition` {Array} of two numbers: the screen row, and the screen column.
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `autoscroll` A Boolean which, if `true`, scrolls the {TextEditor} to wherever
|
||||
// the cursor moves to.
|
||||
setScreenPosition (screenPosition, options = {}) {
|
||||
this.changePosition(options, () => {
|
||||
this.marker.setHeadScreenPosition(screenPosition, options)
|
||||
})
|
||||
}
|
||||
|
||||
// Public: Returns the screen position of the cursor as a {Point}.
|
||||
getScreenPosition () {
|
||||
return this.marker.getHeadScreenPosition()
|
||||
}
|
||||
|
||||
// Public: Moves a cursor to a given buffer position.
|
||||
//
|
||||
// * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column.
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `autoscroll` {Boolean} indicating whether to autoscroll to the new
|
||||
// position. Defaults to `true` if this is the most recently added cursor,
|
||||
// `false` otherwise.
|
||||
setBufferPosition (bufferPosition, options = {}) {
|
||||
this.changePosition(options, () => {
|
||||
this.marker.setHeadBufferPosition(bufferPosition, options)
|
||||
})
|
||||
}
|
||||
|
||||
// Public: Returns the current buffer position as an Array.
|
||||
getBufferPosition () {
|
||||
return this.marker.getHeadBufferPosition()
|
||||
}
|
||||
|
||||
// Public: Returns the cursor's current screen row.
|
||||
getScreenRow () {
|
||||
return this.getScreenPosition().row
|
||||
}
|
||||
|
||||
// Public: Returns the cursor's current screen column.
|
||||
getScreenColumn () {
|
||||
return this.getScreenPosition().column
|
||||
}
|
||||
|
||||
// Public: Retrieves the cursor's current buffer row.
|
||||
getBufferRow () {
|
||||
return this.getBufferPosition().row
|
||||
}
|
||||
|
||||
// Public: Returns the cursor's current buffer column.
|
||||
getBufferColumn () {
|
||||
return this.getBufferPosition().column
|
||||
}
|
||||
|
||||
// Public: Returns the cursor's current buffer row of text excluding its line
|
||||
// ending.
|
||||
getCurrentBufferLine () {
|
||||
return this.editor.lineTextForBufferRow(this.getBufferRow())
|
||||
}
|
||||
|
||||
// Public: Returns whether the cursor is at the start of a line.
|
||||
isAtBeginningOfLine () {
|
||||
return this.getBufferPosition().column === 0
|
||||
}
|
||||
|
||||
// Public: Returns whether the cursor is on the line return character.
|
||||
isAtEndOfLine () {
|
||||
return this.getBufferPosition().isEqual(this.getCurrentLineBufferRange().end)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Cursor Position Details
|
||||
*/
|
||||
|
||||
// Public: Returns the underlying {DisplayMarker} for the cursor.
|
||||
// Useful with overlay {Decoration}s.
|
||||
getMarker () { return this.marker }
|
||||
|
||||
// Public: Identifies if the cursor is surrounded by whitespace.
|
||||
//
|
||||
// "Surrounded" here means that the character directly before and after the
|
||||
// cursor are both whitespace.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isSurroundedByWhitespace () {
|
||||
const {row, column} = this.getBufferPosition()
|
||||
const range = [[row, column - 1], [row, column + 1]]
|
||||
return /^\s+$/.test(this.editor.getTextInBufferRange(range))
|
||||
}
|
||||
|
||||
// Public: Returns whether the cursor is currently between a word and non-word
|
||||
// character. The non-word characters are defined by the
|
||||
// `editor.nonWordCharacters` config value.
|
||||
//
|
||||
// This method returns false if the character before or after the cursor is
|
||||
// whitespace.
|
||||
//
|
||||
// Returns a Boolean.
|
||||
isBetweenWordAndNonWord () {
|
||||
if (this.isAtBeginningOfLine() || this.isAtEndOfLine()) return false
|
||||
|
||||
const {row, column} = this.getBufferPosition()
|
||||
const range = [[row, column - 1], [row, column + 1]]
|
||||
const text = this.editor.getTextInBufferRange(range)
|
||||
if (/\s/.test(text[0]) || /\s/.test(text[1])) return false
|
||||
|
||||
const nonWordCharacters = this.getNonWordCharacters()
|
||||
return nonWordCharacters.includes(text[0]) !== nonWordCharacters.includes(text[1])
|
||||
}
|
||||
|
||||
// Public: Returns whether this cursor is between a word's start and end.
|
||||
//
|
||||
// * `options` (optional) {Object}
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp}).
|
||||
//
|
||||
// Returns a {Boolean}
|
||||
isInsideWord (options) {
|
||||
const {row, column} = this.getBufferPosition()
|
||||
const range = [[row, column], [row, Infinity]]
|
||||
const text = this.editor.getTextInBufferRange(range)
|
||||
return text.search((options && options.wordRegex) || this.wordRegExp()) === 0
|
||||
}
|
||||
|
||||
// Public: Returns the indentation level of the current line.
|
||||
getIndentLevel () {
|
||||
if (this.editor.getSoftTabs()) {
|
||||
return this.getBufferColumn() / this.editor.getTabLength()
|
||||
} else {
|
||||
return this.getBufferColumn()
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Retrieves the scope descriptor for the cursor's current position.
|
||||
//
|
||||
// Returns a {ScopeDescriptor}
|
||||
getScopeDescriptor () {
|
||||
return this.editor.scopeDescriptorForBufferPosition(this.getBufferPosition())
|
||||
}
|
||||
|
||||
// Public: Returns true if this cursor has no non-whitespace characters before
|
||||
// its current position.
|
||||
hasPrecedingCharactersOnLine () {
|
||||
const bufferPosition = this.getBufferPosition()
|
||||
const line = this.editor.lineTextForBufferRow(bufferPosition.row)
|
||||
const firstCharacterColumn = line.search(/\S/)
|
||||
|
||||
if (firstCharacterColumn === -1) {
|
||||
return false
|
||||
} else {
|
||||
return bufferPosition.column > firstCharacterColumn
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Identifies if this cursor is the last in the {TextEditor}.
|
||||
//
|
||||
// "Last" is defined as the most recently added cursor.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isLastCursor () {
|
||||
return this === this.editor.getLastCursor()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Moving the Cursor
|
||||
*/
|
||||
|
||||
// Public: Moves the cursor up one screen row.
|
||||
//
|
||||
// * `rowCount` (optional) {Number} number of rows to move (default: 1)
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `moveToEndOfSelection` if true, move to the left of the selection if a
|
||||
// selection exists.
|
||||
moveUp (rowCount = 1, {moveToEndOfSelection} = {}) {
|
||||
let row, column
|
||||
const range = this.marker.getScreenRange()
|
||||
if (moveToEndOfSelection && !range.isEmpty()) {
|
||||
({row, column} = range.start)
|
||||
} else {
|
||||
({row, column} = this.getScreenPosition())
|
||||
}
|
||||
|
||||
if (this.goalColumn != null) column = this.goalColumn
|
||||
this.setScreenPosition({row: row - rowCount, column}, {skipSoftWrapIndentation: true})
|
||||
this.goalColumn = column
|
||||
}
|
||||
|
||||
// Public: Moves the cursor down one screen row.
|
||||
//
|
||||
// * `rowCount` (optional) {Number} number of rows to move (default: 1)
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `moveToEndOfSelection` if true, move to the left of the selection if a
|
||||
// selection exists.
|
||||
moveDown (rowCount = 1, {moveToEndOfSelection} = {}) {
|
||||
let row, column
|
||||
const range = this.marker.getScreenRange()
|
||||
if (moveToEndOfSelection && !range.isEmpty()) {
|
||||
({row, column} = range.end)
|
||||
} else {
|
||||
({row, column} = this.getScreenPosition())
|
||||
}
|
||||
|
||||
if (this.goalColumn != null) column = this.goalColumn
|
||||
this.setScreenPosition({row: row + rowCount, column}, {skipSoftWrapIndentation: true})
|
||||
this.goalColumn = column
|
||||
}
|
||||
|
||||
// Public: Moves the cursor left one screen column.
|
||||
//
|
||||
// * `columnCount` (optional) {Number} number of columns to move (default: 1)
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `moveToEndOfSelection` if true, move to the left of the selection if a
|
||||
// selection exists.
|
||||
moveLeft (columnCount = 1, {moveToEndOfSelection} = {}) {
|
||||
const range = this.marker.getScreenRange()
|
||||
if (moveToEndOfSelection && !range.isEmpty()) {
|
||||
this.setScreenPosition(range.start)
|
||||
} else {
|
||||
let {row, column} = this.getScreenPosition()
|
||||
|
||||
while (columnCount > column && row > 0) {
|
||||
columnCount -= column
|
||||
column = this.editor.lineLengthForScreenRow(--row)
|
||||
columnCount-- // subtract 1 for the row move
|
||||
}
|
||||
|
||||
column = column - columnCount
|
||||
this.setScreenPosition({row, column}, {clipDirection: 'backward'})
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Moves the cursor right one screen column.
|
||||
//
|
||||
// * `columnCount` (optional) {Number} number of columns to move (default: 1)
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `moveToEndOfSelection` if true, move to the right of the selection if a
|
||||
// selection exists.
|
||||
moveRight (columnCount = 1, {moveToEndOfSelection} = {}) {
|
||||
const range = this.marker.getScreenRange()
|
||||
if (moveToEndOfSelection && !range.isEmpty()) {
|
||||
this.setScreenPosition(range.end)
|
||||
} else {
|
||||
let {row, column} = this.getScreenPosition()
|
||||
const maxLines = this.editor.getScreenLineCount()
|
||||
let rowLength = this.editor.lineLengthForScreenRow(row)
|
||||
let columnsRemainingInLine = rowLength - column
|
||||
|
||||
while (columnCount > columnsRemainingInLine && row < maxLines - 1) {
|
||||
columnCount -= columnsRemainingInLine
|
||||
columnCount-- // subtract 1 for the row move
|
||||
|
||||
column = 0
|
||||
rowLength = this.editor.lineLengthForScreenRow(++row)
|
||||
columnsRemainingInLine = rowLength
|
||||
}
|
||||
|
||||
column = column + columnCount
|
||||
this.setScreenPosition({row, column}, {clipDirection: 'forward'})
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the top of the buffer.
|
||||
moveToTop () {
|
||||
this.setBufferPosition([0, 0])
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the bottom of the buffer.
|
||||
moveToBottom () {
|
||||
this.setBufferPosition(this.editor.getEofBufferPosition())
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the line.
|
||||
moveToBeginningOfScreenLine () {
|
||||
this.setScreenPosition([this.getScreenRow(), 0])
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the buffer line.
|
||||
moveToBeginningOfLine () {
|
||||
this.setBufferPosition([this.getBufferRow(), 0])
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the first character in the
|
||||
// line.
|
||||
moveToFirstCharacterOfLine () {
|
||||
let targetBufferColumn
|
||||
const screenRow = this.getScreenRow()
|
||||
const screenLineStart = this.editor.clipScreenPosition([screenRow, 0], {skipSoftWrapIndentation: true})
|
||||
const screenLineEnd = [screenRow, Infinity]
|
||||
const screenLineBufferRange = this.editor.bufferRangeForScreenRange([screenLineStart, screenLineEnd])
|
||||
|
||||
let firstCharacterColumn = null
|
||||
this.editor.scanInBufferRange(/\S/, screenLineBufferRange, ({range, stop}) => {
|
||||
firstCharacterColumn = range.start.column
|
||||
stop()
|
||||
})
|
||||
|
||||
if (firstCharacterColumn != null && firstCharacterColumn !== this.getBufferColumn()) {
|
||||
targetBufferColumn = firstCharacterColumn
|
||||
} else {
|
||||
targetBufferColumn = screenLineBufferRange.start.column
|
||||
}
|
||||
|
||||
this.setBufferPosition([screenLineBufferRange.start.row, targetBufferColumn])
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the end of the line.
|
||||
moveToEndOfScreenLine () {
|
||||
this.setScreenPosition([this.getScreenRow(), Infinity])
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the end of the buffer line.
|
||||
moveToEndOfLine () {
|
||||
this.setBufferPosition([this.getBufferRow(), Infinity])
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the word.
|
||||
moveToBeginningOfWord () {
|
||||
this.setBufferPosition(this.getBeginningOfCurrentWordBufferPosition())
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the end of the word.
|
||||
moveToEndOfWord () {
|
||||
const position = this.getEndOfCurrentWordBufferPosition()
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the next word.
|
||||
moveToBeginningOfNextWord () {
|
||||
const position = this.getBeginningOfNextWordBufferPosition()
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the previous word boundary.
|
||||
moveToPreviousWordBoundary () {
|
||||
const position = this.getPreviousWordBoundaryBufferPosition()
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the next word boundary.
|
||||
moveToNextWordBoundary () {
|
||||
const position = this.getNextWordBoundaryBufferPosition()
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the previous subword boundary.
|
||||
moveToPreviousSubwordBoundary () {
|
||||
const options = {wordRegex: this.subwordRegExp({backwards: true})}
|
||||
const position = this.getPreviousWordBoundaryBufferPosition(options)
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the next subword boundary.
|
||||
moveToNextSubwordBoundary () {
|
||||
const options = {wordRegex: this.subwordRegExp()}
|
||||
const position = this.getNextWordBoundaryBufferPosition(options)
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the buffer line, skipping all
|
||||
// whitespace.
|
||||
skipLeadingWhitespace () {
|
||||
const position = this.getBufferPosition()
|
||||
const scanRange = this.getCurrentLineBufferRange()
|
||||
let endOfLeadingWhitespace = null
|
||||
this.editor.scanInBufferRange(/^[ \t]*/, scanRange, ({range}) => {
|
||||
endOfLeadingWhitespace = range.end
|
||||
})
|
||||
|
||||
if (endOfLeadingWhitespace.isGreaterThan(position)) this.setBufferPosition(endOfLeadingWhitespace)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the next paragraph
|
||||
moveToBeginningOfNextParagraph () {
|
||||
const position = this.getBeginningOfNextParagraphBufferPosition()
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
// Public: Moves the cursor to the beginning of the previous paragraph
|
||||
moveToBeginningOfPreviousParagraph () {
|
||||
const position = this.getBeginningOfPreviousParagraphBufferPosition()
|
||||
if (position) this.setBufferPosition(position)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Local Positions and Ranges
|
||||
*/
|
||||
|
||||
// Public: Returns buffer position of previous word boundary. It might be on
|
||||
// the current word, or the previous word.
|
||||
//
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp})
|
||||
getPreviousWordBoundaryBufferPosition (options = {}) {
|
||||
const currentBufferPosition = this.getBufferPosition()
|
||||
const previousNonBlankRow = this.editor.buffer.previousNonBlankRow(currentBufferPosition.row)
|
||||
const scanRange = [[previousNonBlankRow || 0, 0], currentBufferPosition]
|
||||
|
||||
let beginningOfWordPosition
|
||||
this.editor.backwardsScanInBufferRange(options.wordRegex || this.wordRegExp(), scanRange, ({range, stop}) => {
|
||||
if (range.start.row < currentBufferPosition.row && currentBufferPosition.column > 0) {
|
||||
// force it to stop at the beginning of each line
|
||||
beginningOfWordPosition = new Point(currentBufferPosition.row, 0)
|
||||
} else if (range.end.isLessThan(currentBufferPosition)) {
|
||||
beginningOfWordPosition = range.end
|
||||
} else {
|
||||
beginningOfWordPosition = range.start
|
||||
}
|
||||
|
||||
if (!beginningOfWordPosition.isEqual(currentBufferPosition)) stop()
|
||||
})
|
||||
|
||||
return beginningOfWordPosition || currentBufferPosition
|
||||
}
|
||||
|
||||
// Public: Returns buffer position of the next word boundary. It might be on
|
||||
// the current word, or the previous word.
|
||||
//
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp})
|
||||
getNextWordBoundaryBufferPosition (options = {}) {
|
||||
const currentBufferPosition = this.getBufferPosition()
|
||||
const scanRange = [currentBufferPosition, this.editor.getEofBufferPosition()]
|
||||
|
||||
let endOfWordPosition
|
||||
this.editor.scanInBufferRange((options.wordRegex != null ? options.wordRegex : this.wordRegExp()), scanRange, function ({range, stop}) {
|
||||
if (range.start.row > currentBufferPosition.row) {
|
||||
// force it to stop at the beginning of each line
|
||||
endOfWordPosition = new Point(range.start.row, 0)
|
||||
} else if (range.start.isGreaterThan(currentBufferPosition)) {
|
||||
endOfWordPosition = range.start
|
||||
} else {
|
||||
endOfWordPosition = range.end
|
||||
}
|
||||
|
||||
if (!endOfWordPosition.isEqual(currentBufferPosition)) stop()
|
||||
})
|
||||
|
||||
return endOfWordPosition || currentBufferPosition
|
||||
}
|
||||
|
||||
// Public: Retrieves the buffer position of where the current word starts.
|
||||
//
|
||||
// * `options` (optional) An {Object} with the following keys:
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp}).
|
||||
// * `includeNonWordCharacters` A {Boolean} indicating whether to include
|
||||
// non-word characters in the default word regex.
|
||||
// Has no effect if wordRegex is set.
|
||||
// * `allowPrevious` A {Boolean} indicating whether the beginning of the
|
||||
// previous word can be returned.
|
||||
//
|
||||
// Returns a {Range}.
|
||||
getBeginningOfCurrentWordBufferPosition (options = {}) {
|
||||
const allowPrevious = options.allowPrevious !== false
|
||||
const position = this.getBufferPosition()
|
||||
|
||||
const scanRange = allowPrevious
|
||||
? new Range(new Point(position.row - 1, 0), position)
|
||||
: new Range(new Point(position.row, 0), position)
|
||||
|
||||
const ranges = this.editor.buffer.findAllInRangeSync(
|
||||
options.wordRegex || this.wordRegExp(),
|
||||
scanRange
|
||||
)
|
||||
|
||||
let result
|
||||
for (let range of ranges) {
|
||||
if (position.isLessThanOrEqual(range.start)) break
|
||||
if (allowPrevious || position.isLessThanOrEqual(range.end)) result = Point.fromObject(range.start)
|
||||
}
|
||||
|
||||
return result || (allowPrevious ? new Point(0, 0) : position)
|
||||
}
|
||||
|
||||
// Public: Retrieves the buffer position of where the current word ends.
|
||||
//
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp})
|
||||
// * `includeNonWordCharacters` A Boolean indicating whether to include
|
||||
// non-word characters in the default word regex. Has no effect if
|
||||
// wordRegex is set.
|
||||
//
|
||||
// Returns a {Range}.
|
||||
getEndOfCurrentWordBufferPosition (options = {}) {
|
||||
const allowNext = options.allowNext !== false
|
||||
const position = this.getBufferPosition()
|
||||
|
||||
const scanRange = allowNext
|
||||
? new Range(position, new Point(position.row + 2, 0))
|
||||
: new Range(position, new Point(position.row, Infinity))
|
||||
|
||||
const ranges = this.editor.buffer.findAllInRangeSync(
|
||||
options.wordRegex || this.wordRegExp(),
|
||||
scanRange
|
||||
)
|
||||
|
||||
for (let range of ranges) {
|
||||
if (position.isLessThan(range.start) && !allowNext) break
|
||||
if (position.isLessThan(range.end)) return Point.fromObject(range.end)
|
||||
}
|
||||
|
||||
return allowNext ? this.editor.getEofBufferPosition() : position
|
||||
}
|
||||
|
||||
// Public: Retrieves the buffer position of where the next word starts.
|
||||
//
|
||||
// * `options` (optional) {Object}
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp}).
|
||||
//
|
||||
// Returns a {Range}
|
||||
getBeginningOfNextWordBufferPosition (options = {}) {
|
||||
const currentBufferPosition = this.getBufferPosition()
|
||||
const start = this.isInsideWord(options) ? this.getEndOfCurrentWordBufferPosition(options) : currentBufferPosition
|
||||
const scanRange = [start, this.editor.getEofBufferPosition()]
|
||||
|
||||
let beginningOfNextWordPosition
|
||||
this.editor.scanInBufferRange(options.wordRegex || this.wordRegExp(), scanRange, ({range, stop}) => {
|
||||
beginningOfNextWordPosition = range.start
|
||||
stop()
|
||||
})
|
||||
|
||||
return beginningOfNextWordPosition || currentBufferPosition
|
||||
}
|
||||
|
||||
// Public: Returns the buffer Range occupied by the word located under the cursor.
|
||||
//
|
||||
// * `options` (optional) {Object}
|
||||
// * `wordRegex` A {RegExp} indicating what constitutes a "word"
|
||||
// (default: {::wordRegExp}).
|
||||
getCurrentWordBufferRange (options = {}) {
|
||||
const position = this.getBufferPosition()
|
||||
const ranges = this.editor.buffer.findAllInRangeSync(
|
||||
options.wordRegex || this.wordRegExp(),
|
||||
new Range(new Point(position.row, 0), new Point(position.row, Infinity))
|
||||
)
|
||||
const range = ranges.find(range =>
|
||||
range.end.column >= position.column && range.start.column <= position.column
|
||||
)
|
||||
return range ? Range.fromObject(range) : new Range(position, position)
|
||||
}
|
||||
|
||||
// Public: Returns the buffer Range for the current line.
|
||||
//
|
||||
// * `options` (optional) {Object}
|
||||
// * `includeNewline` A {Boolean} which controls whether the Range should
|
||||
// include the newline.
|
||||
getCurrentLineBufferRange (options) {
|
||||
return this.editor.bufferRangeForBufferRow(this.getBufferRow(), options)
|
||||
}
|
||||
|
||||
// Public: Retrieves the range for the current paragraph.
|
||||
//
|
||||
// A paragraph is defined as a block of text surrounded by empty lines or comments.
|
||||
//
|
||||
// Returns a {Range}.
|
||||
getCurrentParagraphBufferRange () {
|
||||
return this.editor.rowRangeForParagraphAtBufferRow(this.getBufferRow())
|
||||
}
|
||||
|
||||
// Public: Returns the characters preceding the cursor in the current word.
|
||||
getCurrentWordPrefix () {
|
||||
return this.editor.getTextInBufferRange([this.getBeginningOfCurrentWordBufferPosition(), this.getBufferPosition()])
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Visibility
|
||||
*/
|
||||
|
||||
/*
|
||||
Section: Comparing to another cursor
|
||||
*/
|
||||
|
||||
// Public: Compare this cursor's buffer position to another cursor's buffer position.
|
||||
//
|
||||
// See {Point::compare} for more details.
|
||||
//
|
||||
// * `otherCursor`{Cursor} to compare against
|
||||
compare (otherCursor) {
|
||||
return this.getBufferPosition().compare(otherCursor.getBufferPosition())
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Utilities
|
||||
*/
|
||||
|
||||
// Public: Deselects the current selection.
|
||||
clearSelection (options) {
|
||||
if (this.selection) this.selection.clear(options)
|
||||
}
|
||||
|
||||
// Public: Get the RegExp used by the cursor to determine what a "word" is.
|
||||
//
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `includeNonWordCharacters` A {Boolean} indicating whether to include
|
||||
// non-word characters in the regex. (default: true)
|
||||
//
|
||||
// Returns a {RegExp}.
|
||||
wordRegExp (options) {
|
||||
const nonWordCharacters = _.escapeRegExp(this.getNonWordCharacters())
|
||||
let source = `^[\t\r ]*$|[^\\s${nonWordCharacters}]+`
|
||||
if (!options || options.includeNonWordCharacters !== false) {
|
||||
source += `|${`[${nonWordCharacters}]+`}`
|
||||
}
|
||||
return new RegExp(source, 'g')
|
||||
}
|
||||
|
||||
// Public: Get the RegExp used by the cursor to determine what a "subword" is.
|
||||
//
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `backwards` A {Boolean} indicating whether to look forwards or backwards
|
||||
// for the next subword. (default: false)
|
||||
//
|
||||
// Returns a {RegExp}.
|
||||
subwordRegExp (options = {}) {
|
||||
const nonWordCharacters = this.getNonWordCharacters()
|
||||
const lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF'
|
||||
const uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE'
|
||||
const snakeCamelSegment = `[${uppercaseLetters}]?[${lowercaseLetters}]+`
|
||||
const segments = [
|
||||
'^[\t ]+',
|
||||
'[\t ]+$',
|
||||
`[${uppercaseLetters}]+(?![${lowercaseLetters}])`,
|
||||
'\\d+'
|
||||
]
|
||||
if (options.backwards) {
|
||||
segments.push(`${snakeCamelSegment}_*`)
|
||||
segments.push(`[${_.escapeRegExp(nonWordCharacters)}]+\\s*`)
|
||||
} else {
|
||||
segments.push(`_*${snakeCamelSegment}`)
|
||||
segments.push(`\\s*[${_.escapeRegExp(nonWordCharacters)}]+`)
|
||||
}
|
||||
segments.push('_+')
|
||||
return new RegExp(segments.join('|'), 'g')
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Private
|
||||
*/
|
||||
|
||||
getNonWordCharacters () {
|
||||
return this.editor.getNonWordCharacters(this.getScopeDescriptor().getScopesArray())
|
||||
}
|
||||
|
||||
changePosition (options, fn) {
|
||||
this.clearSelection({autoscroll: false})
|
||||
fn()
|
||||
const autoscroll = (options && options.autoscroll != null)
|
||||
? options.autoscroll
|
||||
: this.isLastCursor()
|
||||
if (autoscroll) this.autoscroll()
|
||||
}
|
||||
|
||||
getScreenRange () {
|
||||
const {row, column} = this.getScreenPosition()
|
||||
return new Range(new Point(row, column), new Point(row, column + 1))
|
||||
}
|
||||
|
||||
autoscroll (options = {}) {
|
||||
options.clip = false
|
||||
this.editor.scrollToScreenRange(this.getScreenRange(), options)
|
||||
}
|
||||
|
||||
getBeginningOfNextParagraphBufferPosition () {
|
||||
const start = this.getBufferPosition()
|
||||
const eof = this.editor.getEofBufferPosition()
|
||||
const scanRange = [start, eof]
|
||||
|
||||
const {row, column} = eof
|
||||
let position = new Point(row, column - 1)
|
||||
|
||||
this.editor.scanInBufferRange(EmptyLineRegExp, scanRange, ({range, stop}) => {
|
||||
position = range.start.traverse(Point(1, 0))
|
||||
if (!position.isEqual(start)) stop()
|
||||
})
|
||||
return position
|
||||
}
|
||||
|
||||
getBeginningOfPreviousParagraphBufferPosition () {
|
||||
const start = this.getBufferPosition()
|
||||
|
||||
const {row, column} = start
|
||||
const scanRange = [[row - 1, column], [0, 0]]
|
||||
let position = new Point(0, 0)
|
||||
this.editor.backwardsScanInBufferRange(EmptyLineRegExp, scanRange, ({range, stop}) => {
|
||||
position = range.start.traverse(Point(1, 0))
|
||||
if (!position.isEqual(start)) stop()
|
||||
})
|
||||
return position
|
||||
}
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
{Emitter} = require 'event-kit'
|
||||
|
||||
idCounter = 0
|
||||
nextId = -> idCounter++
|
||||
|
||||
# Applies changes to a decorationsParam {Object} to make it possible to
|
||||
# differentiate decorations on custom gutters versus the line-number gutter.
|
||||
translateDecorationParamsOldToNew = (decorationParams) ->
|
||||
if decorationParams.type is 'line-number'
|
||||
decorationParams.gutterName = 'line-number'
|
||||
decorationParams
|
||||
|
||||
# Essential: Represents a decoration that follows a {DisplayMarker}. A decoration is
|
||||
# basically a visual representation of a marker. It allows you to add CSS
|
||||
# classes to line numbers in the gutter, lines, and add selection-line regions
|
||||
# around marked ranges of text.
|
||||
#
|
||||
# {Decoration} objects are not meant to be created directly, but created with
|
||||
# {TextEditor::decorateMarker}. eg.
|
||||
#
|
||||
# ```coffee
|
||||
# range = editor.getSelectedBufferRange() # any range you like
|
||||
# marker = editor.markBufferRange(range)
|
||||
# decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'})
|
||||
# ```
|
||||
#
|
||||
# Best practice for destroying the decoration is by destroying the {DisplayMarker}.
|
||||
#
|
||||
# ```coffee
|
||||
# marker.destroy()
|
||||
# ```
|
||||
#
|
||||
# You should only use {Decoration::destroy} when you still need or do not own
|
||||
# the marker.
|
||||
module.exports =
|
||||
class Decoration
|
||||
# Private: Check if the `decorationProperties.type` matches `type`
|
||||
#
|
||||
# * `decorationProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}`
|
||||
# * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also
|
||||
# be an {Array} of {String}s, where it will return true if the decoration's
|
||||
# type matches any in the array.
|
||||
#
|
||||
# Returns {Boolean}
|
||||
# Note: 'line-number' is a special subtype of the 'gutter' type. I.e., a
|
||||
# 'line-number' is a 'gutter', but a 'gutter' is not a 'line-number'.
|
||||
@isType: (decorationProperties, type) ->
|
||||
# 'line-number' is a special case of 'gutter'.
|
||||
if _.isArray(decorationProperties.type)
|
||||
return true if type in decorationProperties.type
|
||||
if type is 'gutter'
|
||||
return true if 'line-number' in decorationProperties.type
|
||||
return false
|
||||
else
|
||||
if type is 'gutter'
|
||||
return true if decorationProperties.type in ['gutter', 'line-number']
|
||||
else
|
||||
type is decorationProperties.type
|
||||
|
||||
###
|
||||
Section: Construction and Destruction
|
||||
###
|
||||
|
||||
constructor: (@marker, @decorationManager, properties) ->
|
||||
@emitter = new Emitter
|
||||
@id = nextId()
|
||||
@setProperties properties
|
||||
@destroyed = false
|
||||
@markerDestroyDisposable = @marker.onDidDestroy => @destroy()
|
||||
|
||||
# Essential: Destroy this marker decoration.
|
||||
#
|
||||
# You can also destroy the marker if you own it, which will destroy this
|
||||
# decoration.
|
||||
destroy: ->
|
||||
return if @destroyed
|
||||
@markerDestroyDisposable.dispose()
|
||||
@markerDestroyDisposable = null
|
||||
@destroyed = true
|
||||
@decorationManager.didDestroyMarkerDecoration(this)
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.dispose()
|
||||
|
||||
isDestroyed: -> @destroyed
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Essential: When the {Decoration} is updated via {Decoration::update}.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `event` {Object}
|
||||
# * `oldProperties` {Object} the old parameters the decoration used to have
|
||||
# * `newProperties` {Object} the new parameters the decoration now has
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeProperties: (callback) ->
|
||||
@emitter.on 'did-change-properties', callback
|
||||
|
||||
# Essential: Invoke the given callback when the {Decoration} is destroyed
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Decoration Details
|
||||
###
|
||||
|
||||
# Essential: An id unique across all {Decoration} objects
|
||||
getId: -> @id
|
||||
|
||||
# Essential: Returns the marker associated with this {Decoration}
|
||||
getMarker: -> @marker
|
||||
|
||||
# Public: Check if this decoration is of type `type`
|
||||
#
|
||||
# * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also
|
||||
# be an {Array} of {String}s, where it will return true if the decoration's
|
||||
# type matches any in the array.
|
||||
#
|
||||
# Returns {Boolean}
|
||||
isType: (type) ->
|
||||
Decoration.isType(@properties, type)
|
||||
|
||||
###
|
||||
Section: Properties
|
||||
###
|
||||
|
||||
# Essential: Returns the {Decoration}'s properties.
|
||||
getProperties: ->
|
||||
@properties
|
||||
|
||||
# Essential: Update the marker with new Properties. Allows you to change the decoration's class.
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# ```coffee
|
||||
# decoration.update({type: 'line-number', class: 'my-new-class'})
|
||||
# ```
|
||||
#
|
||||
# * `newProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}`
|
||||
setProperties: (newProperties) ->
|
||||
return if @destroyed
|
||||
oldProperties = @properties
|
||||
@properties = translateDecorationParamsOldToNew(newProperties)
|
||||
if newProperties.type?
|
||||
@decorationManager.decorationDidChangeType(this)
|
||||
@decorationManager.emitDidUpdateDecorations()
|
||||
@emitter.emit 'did-change-properties', {oldProperties, newProperties}
|
||||
|
||||
###
|
||||
Section: Utility
|
||||
###
|
||||
|
||||
inspect: ->
|
||||
"<Decoration #{@id}>"
|
||||
|
||||
###
|
||||
Section: Private methods
|
||||
###
|
||||
|
||||
matchesPattern: (decorationPattern) ->
|
||||
return false unless decorationPattern?
|
||||
for key, value of decorationPattern
|
||||
return false if @properties[key] isnt value
|
||||
true
|
||||
|
||||
flash: (klass, duration=500) ->
|
||||
@properties.flashRequested = true
|
||||
@properties.flashClass = klass
|
||||
@properties.flashDuration = duration
|
||||
@decorationManager.emitDidUpdateDecorations()
|
||||
@emitter.emit 'did-flash'
|
205
src/decoration.js
Normal file
205
src/decoration.js
Normal file
@ -0,0 +1,205 @@
|
||||
const _ = require('underscore-plus')
|
||||
const {Emitter} = require('event-kit')
|
||||
|
||||
let idCounter = 0
|
||||
const nextId = () => idCounter++
|
||||
|
||||
// Applies changes to a decorationsParam {Object} to make it possible to
|
||||
// differentiate decorations on custom gutters versus the line-number gutter.
|
||||
const translateDecorationParamsOldToNew = function (decorationParams) {
|
||||
if (decorationParams.type === 'line-number') {
|
||||
decorationParams.gutterName = 'line-number'
|
||||
}
|
||||
return decorationParams
|
||||
}
|
||||
|
||||
// Essential: Represents a decoration that follows a {DisplayMarker}. A decoration is
|
||||
// basically a visual representation of a marker. It allows you to add CSS
|
||||
// classes to line numbers in the gutter, lines, and add selection-line regions
|
||||
// around marked ranges of text.
|
||||
//
|
||||
// {Decoration} objects are not meant to be created directly, but created with
|
||||
// {TextEditor::decorateMarker}. eg.
|
||||
//
|
||||
// ```coffee
|
||||
// range = editor.getSelectedBufferRange() # any range you like
|
||||
// marker = editor.markBufferRange(range)
|
||||
// decoration = editor.decorateMarker(marker, {type: 'line', class: 'my-line-class'})
|
||||
// ```
|
||||
//
|
||||
// Best practice for destroying the decoration is by destroying the {DisplayMarker}.
|
||||
//
|
||||
// ```coffee
|
||||
// marker.destroy()
|
||||
// ```
|
||||
//
|
||||
// You should only use {Decoration::destroy} when you still need or do not own
|
||||
// the marker.
|
||||
module.exports =
|
||||
class Decoration {
|
||||
// Private: Check if the `decorationProperties.type` matches `type`
|
||||
//
|
||||
// * `decorationProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}`
|
||||
// * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also
|
||||
// be an {Array} of {String}s, where it will return true if the decoration's
|
||||
// type matches any in the array.
|
||||
//
|
||||
// Returns {Boolean}
|
||||
// Note: 'line-number' is a special subtype of the 'gutter' type. I.e., a
|
||||
// 'line-number' is a 'gutter', but a 'gutter' is not a 'line-number'.
|
||||
static isType (decorationProperties, type) {
|
||||
// 'line-number' is a special case of 'gutter'.
|
||||
if (_.isArray(decorationProperties.type)) {
|
||||
if (decorationProperties.type.includes(type)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (type === 'gutter' && decorationProperties.type.includes('line-number')) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
} else {
|
||||
if (type === 'gutter') {
|
||||
return ['gutter', 'line-number'].includes(decorationProperties.type)
|
||||
} else {
|
||||
return type === decorationProperties.type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Construction and Destruction
|
||||
*/
|
||||
|
||||
constructor (marker, decorationManager, properties) {
|
||||
this.marker = marker
|
||||
this.decorationManager = decorationManager
|
||||
this.emitter = new Emitter()
|
||||
this.id = nextId()
|
||||
this.setProperties(properties)
|
||||
this.destroyed = false
|
||||
this.markerDestroyDisposable = this.marker.onDidDestroy(() => this.destroy())
|
||||
}
|
||||
|
||||
// Essential: Destroy this marker decoration.
|
||||
//
|
||||
// You can also destroy the marker if you own it, which will destroy this
|
||||
// decoration.
|
||||
destroy () {
|
||||
if (this.destroyed) { return }
|
||||
this.markerDestroyDisposable.dispose()
|
||||
this.markerDestroyDisposable = null
|
||||
this.destroyed = true
|
||||
this.decorationManager.didDestroyMarkerDecoration(this)
|
||||
this.emitter.emit('did-destroy')
|
||||
return this.emitter.dispose()
|
||||
}
|
||||
|
||||
isDestroyed () { return this.destroyed }
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Essential: When the {Decoration} is updated via {Decoration::update}.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
// * `event` {Object}
|
||||
// * `oldProperties` {Object} the old parameters the decoration used to have
|
||||
// * `newProperties` {Object} the new parameters the decoration now has
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeProperties (callback) {
|
||||
return this.emitter.on('did-change-properties', callback)
|
||||
}
|
||||
|
||||
// Essential: Invoke the given callback when the {Decoration} is destroyed
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy (callback) {
|
||||
return this.emitter.once('did-destroy', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Decoration Details
|
||||
*/
|
||||
|
||||
// Essential: An id unique across all {Decoration} objects
|
||||
getId () { return this.id }
|
||||
|
||||
// Essential: Returns the marker associated with this {Decoration}
|
||||
getMarker () { return this.marker }
|
||||
|
||||
// Public: Check if this decoration is of type `type`
|
||||
//
|
||||
// * `type` {String} type like `'line-number'`, `'line'`, etc. `type` can also
|
||||
// be an {Array} of {String}s, where it will return true if the decoration's
|
||||
// type matches any in the array.
|
||||
//
|
||||
// Returns {Boolean}
|
||||
isType (type) {
|
||||
return Decoration.isType(this.properties, type)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Properties
|
||||
*/
|
||||
|
||||
// Essential: Returns the {Decoration}'s properties.
|
||||
getProperties () {
|
||||
return this.properties
|
||||
}
|
||||
|
||||
// Essential: Update the marker with new Properties. Allows you to change the decoration's class.
|
||||
//
|
||||
// ## Examples
|
||||
//
|
||||
// ```coffee
|
||||
// decoration.update({type: 'line-number', class: 'my-new-class'})
|
||||
// ```
|
||||
//
|
||||
// * `newProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}`
|
||||
setProperties (newProperties) {
|
||||
if (this.destroyed) { return }
|
||||
const oldProperties = this.properties
|
||||
this.properties = translateDecorationParamsOldToNew(newProperties)
|
||||
if (newProperties.type != null) {
|
||||
this.decorationManager.decorationDidChangeType(this)
|
||||
}
|
||||
this.decorationManager.emitDidUpdateDecorations()
|
||||
return this.emitter.emit('did-change-properties', {oldProperties, newProperties})
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Utility
|
||||
*/
|
||||
|
||||
inspect () {
|
||||
return `<Decoration ${this.id}>`
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Private methods
|
||||
*/
|
||||
|
||||
matchesPattern (decorationPattern) {
|
||||
if (decorationPattern == null) { return false }
|
||||
for (let key in decorationPattern) {
|
||||
const value = decorationPattern[key]
|
||||
if (this.properties[key] !== value) { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
flash (klass, duration) {
|
||||
if (duration == null) { duration = 500 }
|
||||
this.properties.flashRequested = true
|
||||
this.properties.flashClass = klass
|
||||
this.properties.flashDuration = duration
|
||||
this.decorationManager.emitDidUpdateDecorations()
|
||||
return this.emitter.emit('did-flash')
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ class DefaultDirectoryProvider
|
||||
#
|
||||
# Returns:
|
||||
# * {Directory} if the given URI is compatible with this provider.
|
||||
# * `null` if the given URI is not compatibile with this provider.
|
||||
# * `null` if the given URI is not compatible with this provider.
|
||||
directoryForURISync: (uri) ->
|
||||
normalizedPath = @normalizePath(uri)
|
||||
{host} = url.parse(uri)
|
||||
@ -39,7 +39,7 @@ class DefaultDirectoryProvider
|
||||
#
|
||||
# Returns a {Promise} that resolves to:
|
||||
# * {Directory} if the given URI is compatible with this provider.
|
||||
# * `null` if the given URI is not compatibile with this provider.
|
||||
# * `null` if the given URI is not compatible with this provider.
|
||||
directoryForURI: (uri) ->
|
||||
Promise.resolve(@directoryForURISync(uri))
|
||||
|
||||
|
@ -119,7 +119,7 @@ module.exports = class Dock {
|
||||
this.setState({visible: false})
|
||||
}
|
||||
|
||||
// Extended: Toggle the dock's visiblity without changing the {Workspace}'s
|
||||
// Extended: Toggle the dock's visibility without changing the {Workspace}'s
|
||||
// active pane container.
|
||||
toggle () {
|
||||
const state = {visible: !this.state.visible}
|
||||
@ -143,7 +143,7 @@ module.exports = class Dock {
|
||||
// frame to ensure the property is animated (or not) appropriately, however we luck out in this
|
||||
// case because the drag start always happens before the item is dragged into the toggle button.
|
||||
if (nextState.visible !== prevState.visible) {
|
||||
// Never animate toggling visiblity...
|
||||
// Never animate toggling visibility...
|
||||
nextState.shouldAnimate = false
|
||||
} else if (!nextState.visible && nextState.draggingItem && !prevState.draggingItem) {
|
||||
// ...but do animate if you start dragging while the panel is hidden.
|
||||
|
@ -1,496 +0,0 @@
|
||||
{join} = require 'path'
|
||||
|
||||
_ = require 'underscore-plus'
|
||||
{Emitter, Disposable, CompositeDisposable} = require 'event-kit'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
GitUtils = require 'git-utils'
|
||||
|
||||
Task = require './task'
|
||||
|
||||
# Extended: Represents the underlying git operations performed by Atom.
|
||||
#
|
||||
# This class shouldn't be instantiated directly but instead by accessing the
|
||||
# `atom.project` global and calling `getRepositories()`. Note that this will
|
||||
# only be available when the project is backed by a Git repository.
|
||||
#
|
||||
# This class handles submodules automatically by taking a `path` argument to many
|
||||
# of the methods. This `path` argument will determine which underlying
|
||||
# repository is used.
|
||||
#
|
||||
# For a repository with submodules this would have the following outcome:
|
||||
#
|
||||
# ```coffee
|
||||
# repo = atom.project.getRepositories()[0]
|
||||
# repo.getShortHead() # 'master'
|
||||
# repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234'
|
||||
# ```
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# ### Logging the URL of the origin remote
|
||||
#
|
||||
# ```coffee
|
||||
# git = atom.project.getRepositories()[0]
|
||||
# console.log git.getOriginURL()
|
||||
# ```
|
||||
#
|
||||
# ### Requiring in packages
|
||||
#
|
||||
# ```coffee
|
||||
# {GitRepository} = require 'atom'
|
||||
# ```
|
||||
module.exports =
|
||||
class GitRepository
|
||||
@exists: (path) ->
|
||||
if git = @open(path)
|
||||
git.destroy()
|
||||
true
|
||||
else
|
||||
false
|
||||
|
||||
###
|
||||
Section: Construction and Destruction
|
||||
###
|
||||
|
||||
# Public: Creates a new GitRepository instance.
|
||||
#
|
||||
# * `path` The {String} path to the Git repository to open.
|
||||
# * `options` An optional {Object} with the following keys:
|
||||
# * `refreshOnWindowFocus` A {Boolean}, `true` to refresh the index and
|
||||
# statuses when the window is focused.
|
||||
#
|
||||
# Returns a {GitRepository} instance or `null` if the repository could not be opened.
|
||||
@open: (path, options) ->
|
||||
return null unless path
|
||||
try
|
||||
new GitRepository(path, options)
|
||||
catch
|
||||
null
|
||||
|
||||
constructor: (path, options={}) ->
|
||||
@emitter = new Emitter
|
||||
@subscriptions = new CompositeDisposable
|
||||
|
||||
@repo = GitUtils.open(path)
|
||||
unless @repo?
|
||||
throw new Error("No Git repository found searching path: #{path}")
|
||||
|
||||
@statuses = {}
|
||||
@upstream = {ahead: 0, behind: 0}
|
||||
for submodulePath, submoduleRepo of @repo.submodules
|
||||
submoduleRepo.upstream = {ahead: 0, behind: 0}
|
||||
|
||||
{@project, @config, refreshOnWindowFocus} = options
|
||||
|
||||
refreshOnWindowFocus ?= true
|
||||
if refreshOnWindowFocus
|
||||
onWindowFocus = =>
|
||||
@refreshIndex()
|
||||
@refreshStatus()
|
||||
|
||||
window.addEventListener 'focus', onWindowFocus
|
||||
@subscriptions.add new Disposable(-> window.removeEventListener 'focus', onWindowFocus)
|
||||
|
||||
if @project?
|
||||
@project.getBuffers().forEach (buffer) => @subscribeToBuffer(buffer)
|
||||
@subscriptions.add @project.onDidAddBuffer (buffer) => @subscribeToBuffer(buffer)
|
||||
|
||||
# Public: Destroy this {GitRepository} object.
|
||||
#
|
||||
# This destroys any tasks and subscriptions and releases the underlying
|
||||
# libgit2 repository handle. This method is idempotent.
|
||||
destroy: ->
|
||||
if @emitter?
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.dispose()
|
||||
@emitter = null
|
||||
|
||||
if @statusTask?
|
||||
@statusTask.terminate()
|
||||
@statusTask = null
|
||||
|
||||
if @repo?
|
||||
@repo.release()
|
||||
@repo = null
|
||||
|
||||
if @subscriptions?
|
||||
@subscriptions.dispose()
|
||||
@subscriptions = null
|
||||
|
||||
# Public: Returns a {Boolean} indicating if this repository has been destroyed.
|
||||
isDestroyed: ->
|
||||
not @repo?
|
||||
|
||||
# Public: Invoke the given callback when this GitRepository's destroy() method
|
||||
# is invoked.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Public: Invoke the given callback when a specific file's status has
|
||||
# changed. When a file is updated, reloaded, etc, and the status changes, this
|
||||
# will be fired.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `event` {Object}
|
||||
# * `path` {String} the old parameters the decoration used to have
|
||||
# * `pathStatus` {Number} representing the status. This value can be passed to
|
||||
# {::isStatusModified} or {::isStatusNew} to get more information.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeStatus: (callback) ->
|
||||
@emitter.on 'did-change-status', callback
|
||||
|
||||
# Public: Invoke the given callback when a multiple files' statuses have
|
||||
# changed. For example, on window focus, the status of all the paths in the
|
||||
# repo is checked. If any of them have changed, this will be fired. Call
|
||||
# {::getPathStatus(path)} to get the status for your path of choice.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeStatuses: (callback) ->
|
||||
@emitter.on 'did-change-statuses', callback
|
||||
|
||||
###
|
||||
Section: Repository Details
|
||||
###
|
||||
|
||||
# Public: A {String} indicating the type of version control system used by
|
||||
# this repository.
|
||||
#
|
||||
# Returns `"git"`.
|
||||
getType: -> 'git'
|
||||
|
||||
# Public: Returns the {String} path of the repository.
|
||||
getPath: ->
|
||||
@path ?= fs.absolute(@getRepo().getPath())
|
||||
|
||||
# Public: Returns the {String} working directory path of the repository.
|
||||
getWorkingDirectory: -> @getRepo().getWorkingDirectory()
|
||||
|
||||
# Public: Returns true if at the root, false if in a subfolder of the
|
||||
# repository.
|
||||
isProjectAtRoot: ->
|
||||
@projectAtRoot ?= @project?.relativize(@getWorkingDirectory()) is ''
|
||||
|
||||
# Public: Makes a path relative to the repository's working directory.
|
||||
relativize: (path) -> @getRepo().relativize(path)
|
||||
|
||||
# Public: Returns true if the given branch exists.
|
||||
hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")?
|
||||
|
||||
# Public: Retrieves a shortened version of the HEAD reference value.
|
||||
#
|
||||
# This removes the leading segments of `refs/heads`, `refs/tags`, or
|
||||
# `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7
|
||||
# characters.
|
||||
#
|
||||
# * `path` An optional {String} path in the repository to get this information
|
||||
# for, only needed if the repository contains submodules.
|
||||
#
|
||||
# Returns a {String}.
|
||||
getShortHead: (path) -> @getRepo(path).getShortHead()
|
||||
|
||||
# Public: Is the given path a submodule in the repository?
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isSubmodule: (path) ->
|
||||
return false unless path
|
||||
|
||||
repo = @getRepo(path)
|
||||
if repo.isSubmodule(repo.relativize(path))
|
||||
true
|
||||
else
|
||||
# Check if the path is a working directory in a repo that isn't the root.
|
||||
repo isnt @getRepo() and repo.relativize(join(path, 'dir')) is 'dir'
|
||||
|
||||
# Public: Returns the number of commits behind the current branch is from the
|
||||
# its upstream remote branch.
|
||||
#
|
||||
# * `reference` The {String} branch reference name.
|
||||
# * `path` The {String} path in the repository to get this information for,
|
||||
# only needed if the repository contains submodules.
|
||||
getAheadBehindCount: (reference, path) ->
|
||||
@getRepo(path).getAheadBehindCount(reference)
|
||||
|
||||
# Public: Get the cached ahead/behind commit counts for the current branch's
|
||||
# upstream branch.
|
||||
#
|
||||
# * `path` An optional {String} path in the repository to get this information
|
||||
# for, only needed if the repository has submodules.
|
||||
#
|
||||
# Returns an {Object} with the following keys:
|
||||
# * `ahead` The {Number} of commits ahead.
|
||||
# * `behind` The {Number} of commits behind.
|
||||
getCachedUpstreamAheadBehindCount: (path) ->
|
||||
@getRepo(path).upstream ? @upstream
|
||||
|
||||
# Public: Returns the git configuration value specified by the key.
|
||||
#
|
||||
# * `key` The {String} key for the configuration to lookup.
|
||||
# * `path` An optional {String} path in the repository to get this information
|
||||
# for, only needed if the repository has submodules.
|
||||
getConfigValue: (key, path) -> @getRepo(path).getConfigValue(key)
|
||||
|
||||
# Public: Returns the origin url of the repository.
|
||||
#
|
||||
# * `path` (optional) {String} path in the repository to get this information
|
||||
# for, only needed if the repository has submodules.
|
||||
getOriginURL: (path) -> @getConfigValue('remote.origin.url', path)
|
||||
|
||||
# Public: Returns the upstream branch for the current HEAD, or null if there
|
||||
# is no upstream branch for the current HEAD.
|
||||
#
|
||||
# * `path` An optional {String} path in the repo to get this information for,
|
||||
# only needed if the repository contains submodules.
|
||||
#
|
||||
# Returns a {String} branch name such as `refs/remotes/origin/master`.
|
||||
getUpstreamBranch: (path) -> @getRepo(path).getUpstreamBranch()
|
||||
|
||||
# Public: Gets all the local and remote references.
|
||||
#
|
||||
# * `path` An optional {String} path in the repository to get this information
|
||||
# for, only needed if the repository has submodules.
|
||||
#
|
||||
# Returns an {Object} with the following keys:
|
||||
# * `heads` An {Array} of head reference names.
|
||||
# * `remotes` An {Array} of remote reference names.
|
||||
# * `tags` An {Array} of tag reference names.
|
||||
getReferences: (path) -> @getRepo(path).getReferences()
|
||||
|
||||
# Public: Returns the current {String} SHA for the given reference.
|
||||
#
|
||||
# * `reference` The {String} reference to get the target of.
|
||||
# * `path` An optional {String} path in the repo to get the reference target
|
||||
# for. Only needed if the repository contains submodules.
|
||||
getReferenceTarget: (reference, path) ->
|
||||
@getRepo(path).getReferenceTarget(reference)
|
||||
|
||||
###
|
||||
Section: Reading Status
|
||||
###
|
||||
|
||||
# Public: Returns true if the given path is modified.
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `path` is modified.
|
||||
isPathModified: (path) -> @isStatusModified(@getPathStatus(path))
|
||||
|
||||
# Public: Returns true if the given path is new.
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `path` is new.
|
||||
isPathNew: (path) -> @isStatusNew(@getPathStatus(path))
|
||||
|
||||
# Public: Is the given path ignored?
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `path` is ignored.
|
||||
isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path))
|
||||
|
||||
# Public: Get the status of a directory in the repository's working directory.
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns a {Number} representing the status. This value can be passed to
|
||||
# {::isStatusModified} or {::isStatusNew} to get more information.
|
||||
getDirectoryStatus: (directoryPath) ->
|
||||
directoryPath = "#{@relativize(directoryPath)}/"
|
||||
directoryStatus = 0
|
||||
for statusPath, status of @statuses
|
||||
directoryStatus |= status if statusPath.indexOf(directoryPath) is 0
|
||||
directoryStatus
|
||||
|
||||
# Public: Get the status of a single path in the repository.
|
||||
#
|
||||
# * `path` A {String} repository-relative path.
|
||||
#
|
||||
# Returns a {Number} representing the status. This value can be passed to
|
||||
# {::isStatusModified} or {::isStatusNew} to get more information.
|
||||
getPathStatus: (path) ->
|
||||
repo = @getRepo(path)
|
||||
relativePath = @relativize(path)
|
||||
currentPathStatus = @statuses[relativePath] ? 0
|
||||
pathStatus = repo.getStatus(repo.relativize(path)) ? 0
|
||||
pathStatus = 0 if repo.isStatusIgnored(pathStatus)
|
||||
if pathStatus > 0
|
||||
@statuses[relativePath] = pathStatus
|
||||
else
|
||||
delete @statuses[relativePath]
|
||||
if currentPathStatus isnt pathStatus
|
||||
@emitter.emit 'did-change-status', {path, pathStatus}
|
||||
|
||||
pathStatus
|
||||
|
||||
# Public: Get the cached status for the given path.
|
||||
#
|
||||
# * `path` A {String} path in the repository, relative or absolute.
|
||||
#
|
||||
# Returns a status {Number} or null if the path is not in the cache.
|
||||
getCachedPathStatus: (path) ->
|
||||
@statuses[@relativize(path)]
|
||||
|
||||
# Public: Returns true if the given status indicates modification.
|
||||
#
|
||||
# * `status` A {Number} representing the status.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `status` indicates modification.
|
||||
isStatusModified: (status) -> @getRepo().isStatusModified(status)
|
||||
|
||||
# Public: Returns true if the given status indicates a new path.
|
||||
#
|
||||
# * `status` A {Number} representing the status.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the `status` indicates a new path.
|
||||
isStatusNew: (status) -> @getRepo().isStatusNew(status)
|
||||
|
||||
###
|
||||
Section: Retrieving Diffs
|
||||
###
|
||||
|
||||
# Public: Retrieves the number of lines added and removed to a path.
|
||||
#
|
||||
# This compares the working directory contents of the path to the `HEAD`
|
||||
# version.
|
||||
#
|
||||
# * `path` The {String} path to check.
|
||||
#
|
||||
# Returns an {Object} with the following keys:
|
||||
# * `added` The {Number} of added lines.
|
||||
# * `deleted` The {Number} of deleted lines.
|
||||
getDiffStats: (path) ->
|
||||
repo = @getRepo(path)
|
||||
repo.getDiffStats(repo.relativize(path))
|
||||
|
||||
# Public: Retrieves the line diffs comparing the `HEAD` version of the given
|
||||
# path and the given text.
|
||||
#
|
||||
# * `path` The {String} path relative to the repository.
|
||||
# * `text` The {String} to compare against the `HEAD` contents
|
||||
#
|
||||
# Returns an {Array} of hunk {Object}s with the following keys:
|
||||
# * `oldStart` The line {Number} of the old hunk.
|
||||
# * `newStart` The line {Number} of the new hunk.
|
||||
# * `oldLines` The {Number} of lines in the old hunk.
|
||||
# * `newLines` The {Number} of lines in the new hunk
|
||||
getLineDiffs: (path, text) ->
|
||||
# Ignore eol of line differences on windows so that files checked in as
|
||||
# LF don't report every line modified when the text contains CRLF endings.
|
||||
options = ignoreEolWhitespace: process.platform is 'win32'
|
||||
repo = @getRepo(path)
|
||||
repo.getLineDiffs(repo.relativize(path), text, options)
|
||||
|
||||
###
|
||||
Section: Checking Out
|
||||
###
|
||||
|
||||
# Public: Restore the contents of a path in the working directory and index
|
||||
# to the version at `HEAD`.
|
||||
#
|
||||
# This is essentially the same as running:
|
||||
#
|
||||
# ```sh
|
||||
# git reset HEAD -- <path>
|
||||
# git checkout HEAD -- <path>
|
||||
# ```
|
||||
#
|
||||
# * `path` The {String} path to checkout.
|
||||
#
|
||||
# Returns a {Boolean} that's true if the method was successful.
|
||||
checkoutHead: (path) ->
|
||||
repo = @getRepo(path)
|
||||
headCheckedOut = repo.checkoutHead(repo.relativize(path))
|
||||
@getPathStatus(path) if headCheckedOut
|
||||
headCheckedOut
|
||||
|
||||
# Public: Checks out a branch in your repository.
|
||||
#
|
||||
# * `reference` The {String} reference to checkout.
|
||||
# * `create` A {Boolean} value which, if true creates the new reference if
|
||||
# it doesn't exist.
|
||||
#
|
||||
# Returns a Boolean that's true if the method was successful.
|
||||
checkoutReference: (reference, create) ->
|
||||
@getRepo().checkoutReference(reference, create)
|
||||
|
||||
###
|
||||
Section: Private
|
||||
###
|
||||
|
||||
# Subscribes to buffer events.
|
||||
subscribeToBuffer: (buffer) ->
|
||||
getBufferPathStatus = =>
|
||||
if bufferPath = buffer.getPath()
|
||||
@getPathStatus(bufferPath)
|
||||
|
||||
getBufferPathStatus()
|
||||
bufferSubscriptions = new CompositeDisposable
|
||||
bufferSubscriptions.add buffer.onDidSave(getBufferPathStatus)
|
||||
bufferSubscriptions.add buffer.onDidReload(getBufferPathStatus)
|
||||
bufferSubscriptions.add buffer.onDidChangePath(getBufferPathStatus)
|
||||
bufferSubscriptions.add buffer.onDidDestroy =>
|
||||
bufferSubscriptions.dispose()
|
||||
@subscriptions.remove(bufferSubscriptions)
|
||||
@subscriptions.add(bufferSubscriptions)
|
||||
return
|
||||
|
||||
# Subscribes to editor view event.
|
||||
checkoutHeadForEditor: (editor) ->
|
||||
buffer = editor.getBuffer()
|
||||
if filePath = buffer.getPath()
|
||||
@checkoutHead(filePath)
|
||||
buffer.reload()
|
||||
|
||||
# Returns the corresponding {Repository}
|
||||
getRepo: (path) ->
|
||||
if @repo?
|
||||
@repo.submoduleForPath(path) ? @repo
|
||||
else
|
||||
throw new Error("Repository has been destroyed")
|
||||
|
||||
# Reread the index to update any values that have changed since the
|
||||
# last time the index was read.
|
||||
refreshIndex: -> @getRepo().refreshIndex()
|
||||
|
||||
# Refreshes the current git status in an outside process and asynchronously
|
||||
# updates the relevant properties.
|
||||
refreshStatus: ->
|
||||
@handlerPath ?= require.resolve('./repository-status-handler')
|
||||
|
||||
relativeProjectPaths = @project?.getPaths()
|
||||
.map (projectPath) => @relativize(projectPath)
|
||||
.filter (projectPath) -> projectPath.length > 0 and not path.isAbsolute(projectPath)
|
||||
|
||||
@statusTask?.terminate()
|
||||
new Promise (resolve) =>
|
||||
@statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) =>
|
||||
statusesUnchanged = _.isEqual(statuses, @statuses) and
|
||||
_.isEqual(upstream, @upstream) and
|
||||
_.isEqual(branch, @branch) and
|
||||
_.isEqual(submodules, @submodules)
|
||||
|
||||
@statuses = statuses
|
||||
@upstream = upstream
|
||||
@branch = branch
|
||||
@submodules = submodules
|
||||
|
||||
for submodulePath, submoduleRepo of @getRepo().submodules
|
||||
submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0}
|
||||
|
||||
unless statusesUnchanged
|
||||
@emitter.emit 'did-change-statuses'
|
||||
resolve()
|
603
src/git-repository.js
Normal file
603
src/git-repository.js
Normal file
@ -0,0 +1,603 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS104: Avoid inline assignments
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const {join} = require('path')
|
||||
const _ = require('underscore-plus')
|
||||
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
|
||||
const fs = require('fs-plus')
|
||||
const path = require('path')
|
||||
const GitUtils = require('git-utils')
|
||||
|
||||
let nextId = 0
|
||||
|
||||
// Extended: Represents the underlying git operations performed by Atom.
|
||||
//
|
||||
// This class shouldn't be instantiated directly but instead by accessing the
|
||||
// `atom.project` global and calling `getRepositories()`. Note that this will
|
||||
// only be available when the project is backed by a Git repository.
|
||||
//
|
||||
// This class handles submodules automatically by taking a `path` argument to many
|
||||
// of the methods. This `path` argument will determine which underlying
|
||||
// repository is used.
|
||||
//
|
||||
// For a repository with submodules this would have the following outcome:
|
||||
//
|
||||
// ```coffee
|
||||
// repo = atom.project.getRepositories()[0]
|
||||
// repo.getShortHead() # 'master'
|
||||
// repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234'
|
||||
// ```
|
||||
//
|
||||
// ## Examples
|
||||
//
|
||||
// ### Logging the URL of the origin remote
|
||||
//
|
||||
// ```coffee
|
||||
// git = atom.project.getRepositories()[0]
|
||||
// console.log git.getOriginURL()
|
||||
// ```
|
||||
//
|
||||
// ### Requiring in packages
|
||||
//
|
||||
// ```coffee
|
||||
// {GitRepository} = require 'atom'
|
||||
// ```
|
||||
module.exports =
|
||||
class GitRepository {
|
||||
static exists (path) {
|
||||
const git = this.open(path)
|
||||
if (git) {
|
||||
git.destroy()
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Construction and Destruction
|
||||
*/
|
||||
|
||||
// Public: Creates a new GitRepository instance.
|
||||
//
|
||||
// * `path` The {String} path to the Git repository to open.
|
||||
// * `options` An optional {Object} with the following keys:
|
||||
// * `refreshOnWindowFocus` A {Boolean}, `true` to refresh the index and
|
||||
// statuses when the window is focused.
|
||||
//
|
||||
// Returns a {GitRepository} instance or `null` if the repository could not be opened.
|
||||
static open (path, options) {
|
||||
if (!path) { return null }
|
||||
try {
|
||||
return new GitRepository(path, options)
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
constructor (path, options = {}) {
|
||||
this.id = nextId++
|
||||
this.emitter = new Emitter()
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
this.repo = GitUtils.open(path)
|
||||
if (this.repo == null) {
|
||||
throw new Error(`No Git repository found searching path: ${path}`)
|
||||
}
|
||||
|
||||
this.statusRefreshCount = 0
|
||||
this.statuses = {}
|
||||
this.upstream = {ahead: 0, behind: 0}
|
||||
for (let submodulePath in this.repo.submodules) {
|
||||
const submoduleRepo = this.repo.submodules[submodulePath]
|
||||
submoduleRepo.upstream = {ahead: 0, behind: 0}
|
||||
}
|
||||
|
||||
this.project = options.project
|
||||
this.config = options.config
|
||||
|
||||
if (options.refreshOnWindowFocus || options.refreshOnWindowFocus == null) {
|
||||
const onWindowFocus = () => {
|
||||
this.refreshIndex()
|
||||
this.refreshStatus()
|
||||
}
|
||||
|
||||
window.addEventListener('focus', onWindowFocus)
|
||||
this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus)))
|
||||
}
|
||||
|
||||
if (this.project != null) {
|
||||
this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer))
|
||||
this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer)))
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Destroy this {GitRepository} object.
|
||||
//
|
||||
// This destroys any tasks and subscriptions and releases the underlying
|
||||
// libgit2 repository handle. This method is idempotent.
|
||||
destroy () {
|
||||
this.repo = null
|
||||
|
||||
if (this.emitter) {
|
||||
this.emitter.emit('did-destroy')
|
||||
this.emitter.dispose()
|
||||
this.emitter = null
|
||||
}
|
||||
|
||||
if (this.subscriptions) {
|
||||
this.subscriptions.dispose()
|
||||
this.subscriptions = null
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Returns a {Boolean} indicating if this repository has been destroyed.
|
||||
isDestroyed () {
|
||||
return this.repo == null
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback when this GitRepository's destroy() method
|
||||
// is invoked.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy (callback) {
|
||||
return this.emitter.once('did-destroy', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Public: Invoke the given callback when a specific file's status has
|
||||
// changed. When a file is updated, reloaded, etc, and the status changes, this
|
||||
// will be fired.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
// * `event` {Object}
|
||||
// * `path` {String} the old parameters the decoration used to have
|
||||
// * `pathStatus` {Number} representing the status. This value can be passed to
|
||||
// {::isStatusModified} or {::isStatusNew} to get more information.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeStatus (callback) {
|
||||
return this.emitter.on('did-change-status', callback)
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback when a multiple files' statuses have
|
||||
// changed. For example, on window focus, the status of all the paths in the
|
||||
// repo is checked. If any of them have changed, this will be fired. Call
|
||||
// {::getPathStatus(path)} to get the status for your path of choice.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeStatuses (callback) {
|
||||
return this.emitter.on('did-change-statuses', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Repository Details
|
||||
*/
|
||||
|
||||
// Public: A {String} indicating the type of version control system used by
|
||||
// this repository.
|
||||
//
|
||||
// Returns `"git"`.
|
||||
getType () { return 'git' }
|
||||
|
||||
// Public: Returns the {String} path of the repository.
|
||||
getPath () {
|
||||
if (this.path == null) {
|
||||
this.path = fs.absolute(this.getRepo().getPath())
|
||||
}
|
||||
return this.path
|
||||
}
|
||||
|
||||
// Public: Returns the {String} working directory path of the repository.
|
||||
getWorkingDirectory () {
|
||||
return this.getRepo().getWorkingDirectory()
|
||||
}
|
||||
|
||||
// Public: Returns true if at the root, false if in a subfolder of the
|
||||
// repository.
|
||||
isProjectAtRoot () {
|
||||
if (this.projectAtRoot == null) {
|
||||
this.projectAtRoot = this.project && this.project.relativize(this.getWorkingDirectory()) === ''
|
||||
}
|
||||
return this.projectAtRoot
|
||||
}
|
||||
|
||||
// Public: Makes a path relative to the repository's working directory.
|
||||
relativize (path) {
|
||||
return this.getRepo().relativize(path)
|
||||
}
|
||||
|
||||
// Public: Returns true if the given branch exists.
|
||||
hasBranch (branch) {
|
||||
return this.getReferenceTarget(`refs/heads/${branch}`) != null
|
||||
}
|
||||
|
||||
// Public: Retrieves a shortened version of the HEAD reference value.
|
||||
//
|
||||
// This removes the leading segments of `refs/heads`, `refs/tags`, or
|
||||
// `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7
|
||||
// characters.
|
||||
//
|
||||
// * `path` An optional {String} path in the repository to get this information
|
||||
// for, only needed if the repository contains submodules.
|
||||
//
|
||||
// Returns a {String}.
|
||||
getShortHead (path) {
|
||||
return this.getRepo(path).getShortHead()
|
||||
}
|
||||
|
||||
// Public: Is the given path a submodule in the repository?
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isSubmodule (path) {
|
||||
if (!path) return false
|
||||
|
||||
const repo = this.getRepo(path)
|
||||
if (repo.isSubmodule(repo.relativize(path))) {
|
||||
return true
|
||||
} else {
|
||||
// Check if the path is a working directory in a repo that isn't the root.
|
||||
return repo !== this.getRepo() && repo.relativize(join(path, 'dir')) === 'dir'
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Returns the number of commits behind the current branch is from the
|
||||
// its upstream remote branch.
|
||||
//
|
||||
// * `reference` The {String} branch reference name.
|
||||
// * `path` The {String} path in the repository to get this information for,
|
||||
// only needed if the repository contains submodules.
|
||||
getAheadBehindCount (reference, path) {
|
||||
return this.getRepo(path).getAheadBehindCount(reference)
|
||||
}
|
||||
|
||||
// Public: Get the cached ahead/behind commit counts for the current branch's
|
||||
// upstream branch.
|
||||
//
|
||||
// * `path` An optional {String} path in the repository to get this information
|
||||
// for, only needed if the repository has submodules.
|
||||
//
|
||||
// Returns an {Object} with the following keys:
|
||||
// * `ahead` The {Number} of commits ahead.
|
||||
// * `behind` The {Number} of commits behind.
|
||||
getCachedUpstreamAheadBehindCount (path) {
|
||||
return this.getRepo(path).upstream || this.upstream
|
||||
}
|
||||
|
||||
// Public: Returns the git configuration value specified by the key.
|
||||
//
|
||||
// * `key` The {String} key for the configuration to lookup.
|
||||
// * `path` An optional {String} path in the repository to get this information
|
||||
// for, only needed if the repository has submodules.
|
||||
getConfigValue (key, path) {
|
||||
return this.getRepo(path).getConfigValue(key)
|
||||
}
|
||||
|
||||
// Public: Returns the origin url of the repository.
|
||||
//
|
||||
// * `path` (optional) {String} path in the repository to get this information
|
||||
// for, only needed if the repository has submodules.
|
||||
getOriginURL (path) {
|
||||
return this.getConfigValue('remote.origin.url', path)
|
||||
}
|
||||
|
||||
// Public: Returns the upstream branch for the current HEAD, or null if there
|
||||
// is no upstream branch for the current HEAD.
|
||||
//
|
||||
// * `path` An optional {String} path in the repo to get this information for,
|
||||
// only needed if the repository contains submodules.
|
||||
//
|
||||
// Returns a {String} branch name such as `refs/remotes/origin/master`.
|
||||
getUpstreamBranch (path) {
|
||||
return this.getRepo(path).getUpstreamBranch()
|
||||
}
|
||||
|
||||
// Public: Gets all the local and remote references.
|
||||
//
|
||||
// * `path` An optional {String} path in the repository to get this information
|
||||
// for, only needed if the repository has submodules.
|
||||
//
|
||||
// Returns an {Object} with the following keys:
|
||||
// * `heads` An {Array} of head reference names.
|
||||
// * `remotes` An {Array} of remote reference names.
|
||||
// * `tags` An {Array} of tag reference names.
|
||||
getReferences (path) {
|
||||
return this.getRepo(path).getReferences()
|
||||
}
|
||||
|
||||
// Public: Returns the current {String} SHA for the given reference.
|
||||
//
|
||||
// * `reference` The {String} reference to get the target of.
|
||||
// * `path` An optional {String} path in the repo to get the reference target
|
||||
// for. Only needed if the repository contains submodules.
|
||||
getReferenceTarget (reference, path) {
|
||||
return this.getRepo(path).getReferenceTarget(reference)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Reading Status
|
||||
*/
|
||||
|
||||
// Public: Returns true if the given path is modified.
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `path` is modified.
|
||||
isPathModified (path) {
|
||||
return this.isStatusModified(this.getPathStatus(path))
|
||||
}
|
||||
|
||||
// Public: Returns true if the given path is new.
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `path` is new.
|
||||
isPathNew (path) {
|
||||
return this.isStatusNew(this.getPathStatus(path))
|
||||
}
|
||||
|
||||
// Public: Is the given path ignored?
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `path` is ignored.
|
||||
isPathIgnored (path) {
|
||||
return this.getRepo().isIgnored(this.relativize(path))
|
||||
}
|
||||
|
||||
// Public: Get the status of a directory in the repository's working directory.
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns a {Number} representing the status. This value can be passed to
|
||||
// {::isStatusModified} or {::isStatusNew} to get more information.
|
||||
getDirectoryStatus (directoryPath) {
|
||||
directoryPath = `${this.relativize(directoryPath)}/`
|
||||
let directoryStatus = 0
|
||||
for (let statusPath in this.statuses) {
|
||||
const status = this.statuses[statusPath]
|
||||
if (statusPath.startsWith(directoryPath)) directoryStatus |= status
|
||||
}
|
||||
return directoryStatus
|
||||
}
|
||||
|
||||
// Public: Get the status of a single path in the repository.
|
||||
//
|
||||
// * `path` A {String} repository-relative path.
|
||||
//
|
||||
// Returns a {Number} representing the status. This value can be passed to
|
||||
// {::isStatusModified} or {::isStatusNew} to get more information.
|
||||
getPathStatus (path) {
|
||||
const repo = this.getRepo(path)
|
||||
const relativePath = this.relativize(path)
|
||||
const currentPathStatus = this.statuses[relativePath] || 0
|
||||
let pathStatus = repo.getStatus(repo.relativize(path)) || 0
|
||||
if (repo.isStatusIgnored(pathStatus)) pathStatus = 0
|
||||
if (pathStatus > 0) {
|
||||
this.statuses[relativePath] = pathStatus
|
||||
} else {
|
||||
delete this.statuses[relativePath]
|
||||
}
|
||||
if (currentPathStatus !== pathStatus) {
|
||||
this.emitter.emit('did-change-status', {path, pathStatus})
|
||||
}
|
||||
|
||||
return pathStatus
|
||||
}
|
||||
|
||||
// Public: Get the cached status for the given path.
|
||||
//
|
||||
// * `path` A {String} path in the repository, relative or absolute.
|
||||
//
|
||||
// Returns a status {Number} or null if the path is not in the cache.
|
||||
getCachedPathStatus (path) {
|
||||
return this.statuses[this.relativize(path)]
|
||||
}
|
||||
|
||||
// Public: Returns true if the given status indicates modification.
|
||||
//
|
||||
// * `status` A {Number} representing the status.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `status` indicates modification.
|
||||
isStatusModified (status) { return this.getRepo().isStatusModified(status) }
|
||||
|
||||
// Public: Returns true if the given status indicates a new path.
|
||||
//
|
||||
// * `status` A {Number} representing the status.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the `status` indicates a new path.
|
||||
isStatusNew (status) {
|
||||
return this.getRepo().isStatusNew(status)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Retrieving Diffs
|
||||
*/
|
||||
|
||||
// Public: Retrieves the number of lines added and removed to a path.
|
||||
//
|
||||
// This compares the working directory contents of the path to the `HEAD`
|
||||
// version.
|
||||
//
|
||||
// * `path` The {String} path to check.
|
||||
//
|
||||
// Returns an {Object} with the following keys:
|
||||
// * `added` The {Number} of added lines.
|
||||
// * `deleted` The {Number} of deleted lines.
|
||||
getDiffStats (path) {
|
||||
const repo = this.getRepo(path)
|
||||
return repo.getDiffStats(repo.relativize(path))
|
||||
}
|
||||
|
||||
// Public: Retrieves the line diffs comparing the `HEAD` version of the given
|
||||
// path and the given text.
|
||||
//
|
||||
// * `path` The {String} path relative to the repository.
|
||||
// * `text` The {String} to compare against the `HEAD` contents
|
||||
//
|
||||
// Returns an {Array} of hunk {Object}s with the following keys:
|
||||
// * `oldStart` The line {Number} of the old hunk.
|
||||
// * `newStart` The line {Number} of the new hunk.
|
||||
// * `oldLines` The {Number} of lines in the old hunk.
|
||||
// * `newLines` The {Number} of lines in the new hunk
|
||||
getLineDiffs (path, text) {
|
||||
// Ignore eol of line differences on windows so that files checked in as
|
||||
// LF don't report every line modified when the text contains CRLF endings.
|
||||
const options = {ignoreEolWhitespace: process.platform === 'win32'}
|
||||
const repo = this.getRepo(path)
|
||||
return repo.getLineDiffs(repo.relativize(path), text, options)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Checking Out
|
||||
*/
|
||||
|
||||
// Public: Restore the contents of a path in the working directory and index
|
||||
// to the version at `HEAD`.
|
||||
//
|
||||
// This is essentially the same as running:
|
||||
//
|
||||
// ```sh
|
||||
// git reset HEAD -- <path>
|
||||
// git checkout HEAD -- <path>
|
||||
// ```
|
||||
//
|
||||
// * `path` The {String} path to checkout.
|
||||
//
|
||||
// Returns a {Boolean} that's true if the method was successful.
|
||||
checkoutHead (path) {
|
||||
const repo = this.getRepo(path)
|
||||
const headCheckedOut = repo.checkoutHead(repo.relativize(path))
|
||||
if (headCheckedOut) this.getPathStatus(path)
|
||||
return headCheckedOut
|
||||
}
|
||||
|
||||
// Public: Checks out a branch in your repository.
|
||||
//
|
||||
// * `reference` The {String} reference to checkout.
|
||||
// * `create` A {Boolean} value which, if true creates the new reference if
|
||||
// it doesn't exist.
|
||||
//
|
||||
// Returns a Boolean that's true if the method was successful.
|
||||
checkoutReference (reference, create) {
|
||||
return this.getRepo().checkoutReference(reference, create)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Private
|
||||
*/
|
||||
|
||||
// Subscribes to buffer events.
|
||||
subscribeToBuffer (buffer) {
|
||||
const getBufferPathStatus = () => {
|
||||
const bufferPath = buffer.getPath()
|
||||
if (bufferPath) this.getPathStatus(bufferPath)
|
||||
}
|
||||
|
||||
getBufferPathStatus()
|
||||
const bufferSubscriptions = new CompositeDisposable()
|
||||
bufferSubscriptions.add(buffer.onDidSave(getBufferPathStatus))
|
||||
bufferSubscriptions.add(buffer.onDidReload(getBufferPathStatus))
|
||||
bufferSubscriptions.add(buffer.onDidChangePath(getBufferPathStatus))
|
||||
bufferSubscriptions.add(buffer.onDidDestroy(() => {
|
||||
bufferSubscriptions.dispose()
|
||||
return this.subscriptions.remove(bufferSubscriptions)
|
||||
}))
|
||||
this.subscriptions.add(bufferSubscriptions)
|
||||
}
|
||||
|
||||
// Subscribes to editor view event.
|
||||
checkoutHeadForEditor (editor) {
|
||||
const buffer = editor.getBuffer()
|
||||
const bufferPath = buffer.getPath()
|
||||
if (bufferPath) {
|
||||
this.checkoutHead(bufferPath)
|
||||
return buffer.reload()
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the corresponding {Repository}
|
||||
getRepo (path) {
|
||||
if (this.repo) {
|
||||
return this.repo.submoduleForPath(path) || this.repo
|
||||
} else {
|
||||
throw new Error('Repository has been destroyed')
|
||||
}
|
||||
}
|
||||
|
||||
// Reread the index to update any values that have changed since the
|
||||
// last time the index was read.
|
||||
refreshIndex () {
|
||||
return this.getRepo().refreshIndex()
|
||||
}
|
||||
|
||||
// Refreshes the current git status in an outside process and asynchronously
|
||||
// updates the relevant properties.
|
||||
async refreshStatus () {
|
||||
const statusRefreshCount = ++this.statusRefreshCount
|
||||
const repo = this.getRepo()
|
||||
|
||||
const relativeProjectPaths = this.project && this.project.getPaths()
|
||||
.map(projectPath => this.relativize(projectPath))
|
||||
.filter(projectPath => (projectPath.length > 0) && !path.isAbsolute(projectPath))
|
||||
|
||||
const branch = await repo.getHeadAsync()
|
||||
const upstream = await repo.getAheadBehindCountAsync()
|
||||
|
||||
const statuses = {}
|
||||
const repoStatus = relativeProjectPaths.length > 0
|
||||
? await repo.getStatusAsync(relativeProjectPaths)
|
||||
: await repo.getStatusAsync()
|
||||
for (let filePath in repoStatus) {
|
||||
statuses[filePath] = repoStatus[filePath]
|
||||
}
|
||||
|
||||
const submodules = {}
|
||||
for (let submodulePath in repo.submodules) {
|
||||
const submoduleRepo = repo.submodules[submodulePath]
|
||||
submodules[submodulePath] = {
|
||||
branch: await submoduleRepo.getHeadAsync(),
|
||||
upstream: await submoduleRepo.getAheadBehindCountAsync()
|
||||
}
|
||||
|
||||
const workingDirectoryPath = submoduleRepo.getWorkingDirectory()
|
||||
const submoduleStatus = await submoduleRepo.getStatusAsync()
|
||||
for (let filePath in submoduleStatus) {
|
||||
const absolutePath = path.join(workingDirectoryPath, filePath)
|
||||
const relativizePath = repo.relativize(absolutePath)
|
||||
statuses[relativizePath] = submoduleStatus[filePath]
|
||||
}
|
||||
}
|
||||
|
||||
if (this.statusRefreshCount !== statusRefreshCount || this.isDestroyed()) return
|
||||
|
||||
const statusesUnchanged =
|
||||
_.isEqual(branch, this.branch) &&
|
||||
_.isEqual(statuses, this.statuses) &&
|
||||
_.isEqual(upstream, this.upstream) &&
|
||||
_.isEqual(submodules, this.submodules)
|
||||
|
||||
this.branch = branch
|
||||
this.statuses = statuses
|
||||
this.upstream = upstream
|
||||
this.submodules = submodules
|
||||
|
||||
for (let submodulePath in repo.submodules) {
|
||||
repo.submodules[submodulePath].upstream = submodules[submodulePath].upstream
|
||||
}
|
||||
|
||||
if (!statusesUnchanged) this.emitter.emit('did-change-statuses')
|
||||
}
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
FirstMate = require 'first-mate'
|
||||
Token = require './token'
|
||||
fs = require 'fs-plus'
|
||||
Grim = require 'grim'
|
||||
|
||||
PathSplitRegex = new RegExp("[/.]")
|
||||
|
||||
# Extended: Syntax class holding the grammars used for tokenizing.
|
||||
#
|
||||
# An instance of this class is always available as the `atom.grammars` global.
|
||||
#
|
||||
# The Syntax class also contains properties for things such as the
|
||||
# language-specific comment regexes. See {::getProperty} for more details.
|
||||
module.exports =
|
||||
class GrammarRegistry extends FirstMate.GrammarRegistry
|
||||
constructor: ({@config}={}) ->
|
||||
super(maxTokensPerLine: 100, maxLineLength: 1000)
|
||||
|
||||
createToken: (value, scopes) -> new Token({value, scopes})
|
||||
|
||||
# Extended: Select a grammar for the given file path and file contents.
|
||||
#
|
||||
# This picks the best match by checking the file path and contents against
|
||||
# each grammar.
|
||||
#
|
||||
# * `filePath` A {String} file path.
|
||||
# * `fileContents` A {String} of text for the file path.
|
||||
#
|
||||
# Returns a {Grammar}, never null.
|
||||
selectGrammar: (filePath, fileContents) ->
|
||||
@selectGrammarWithScore(filePath, fileContents).grammar
|
||||
|
||||
selectGrammarWithScore: (filePath, fileContents) ->
|
||||
bestMatch = null
|
||||
highestScore = -Infinity
|
||||
for grammar in @grammars
|
||||
score = @getGrammarScore(grammar, filePath, fileContents)
|
||||
if score > highestScore or not bestMatch?
|
||||
bestMatch = grammar
|
||||
highestScore = score
|
||||
{grammar: bestMatch, score: highestScore}
|
||||
|
||||
# Extended: Returns a {Number} representing how well the grammar matches the
|
||||
# `filePath` and `contents`.
|
||||
getGrammarScore: (grammar, filePath, contents) ->
|
||||
contents = fs.readFileSync(filePath, 'utf8') if not contents? and fs.isFileSync(filePath)
|
||||
|
||||
score = @getGrammarPathScore(grammar, filePath)
|
||||
if score > 0 and not grammar.bundledPackage
|
||||
score += 0.25
|
||||
if @grammarMatchesContents(grammar, contents)
|
||||
score += 0.125
|
||||
score
|
||||
|
||||
getGrammarPathScore: (grammar, filePath) ->
|
||||
return -1 unless filePath
|
||||
filePath = filePath.replace(/\\/g, '/') if process.platform is 'win32'
|
||||
|
||||
pathComponents = filePath.toLowerCase().split(PathSplitRegex)
|
||||
pathScore = -1
|
||||
|
||||
fileTypes = grammar.fileTypes
|
||||
if customFileTypes = @config.get('core.customFileTypes')?[grammar.scopeName]
|
||||
fileTypes = fileTypes.concat(customFileTypes)
|
||||
|
||||
for fileType, i in fileTypes
|
||||
fileTypeComponents = fileType.toLowerCase().split(PathSplitRegex)
|
||||
pathSuffix = pathComponents[-fileTypeComponents.length..-1]
|
||||
if _.isEqual(pathSuffix, fileTypeComponents)
|
||||
pathScore = Math.max(pathScore, fileType.length)
|
||||
if i >= grammar.fileTypes.length
|
||||
pathScore += 0.5
|
||||
|
||||
pathScore
|
||||
|
||||
grammarMatchesContents: (grammar, contents) ->
|
||||
return false unless contents? and grammar.firstLineRegex?
|
||||
|
||||
escaped = false
|
||||
numberOfNewlinesInRegex = 0
|
||||
for character in grammar.firstLineRegex.source
|
||||
switch character
|
||||
when '\\'
|
||||
escaped = not escaped
|
||||
when 'n'
|
||||
numberOfNewlinesInRegex++ if escaped
|
||||
escaped = false
|
||||
else
|
||||
escaped = false
|
||||
lines = contents.split('\n')
|
||||
grammar.firstLineRegex.testSync(lines[0..numberOfNewlinesInRegex].join('\n'))
|
||||
|
||||
# Deprecated: Get the grammar override for the given file path.
|
||||
#
|
||||
# * `filePath` A {String} file path.
|
||||
#
|
||||
# Returns a {String} such as `"source.js"`.
|
||||
grammarOverrideForPath: (filePath) ->
|
||||
Grim.deprecate 'Use atom.textEditors.getGrammarOverride(editor) instead'
|
||||
if editor = getEditorForPath(filePath)
|
||||
atom.textEditors.getGrammarOverride(editor)
|
||||
|
||||
# Deprecated: Set the grammar override for the given file path.
|
||||
#
|
||||
# * `filePath` A non-empty {String} file path.
|
||||
# * `scopeName` A {String} such as `"source.js"`.
|
||||
#
|
||||
# Returns undefined
|
||||
setGrammarOverrideForPath: (filePath, scopeName) ->
|
||||
Grim.deprecate 'Use atom.textEditors.setGrammarOverride(editor, scopeName) instead'
|
||||
if editor = getEditorForPath(filePath)
|
||||
atom.textEditors.setGrammarOverride(editor, scopeName)
|
||||
return
|
||||
|
||||
# Deprecated: Remove the grammar override for the given file path.
|
||||
#
|
||||
# * `filePath` A {String} file path.
|
||||
#
|
||||
# Returns undefined.
|
||||
clearGrammarOverrideForPath: (filePath) ->
|
||||
Grim.deprecate 'Use atom.textEditors.clearGrammarOverride(editor) instead'
|
||||
if editor = getEditorForPath(filePath)
|
||||
atom.textEditors.clearGrammarOverride(editor)
|
||||
return
|
||||
|
||||
getEditorForPath = (filePath) ->
|
||||
if filePath?
|
||||
atom.workspace.getTextEditors().find (editor) ->
|
||||
editor.getPath() is filePath
|
171
src/grammar-registry.js
Normal file
171
src/grammar-registry.js
Normal file
@ -0,0 +1,171 @@
|
||||
const _ = require('underscore-plus')
|
||||
const FirstMate = require('first-mate')
|
||||
const Token = require('./token')
|
||||
const fs = require('fs-plus')
|
||||
const Grim = require('grim')
|
||||
|
||||
const PathSplitRegex = new RegExp('[/.]')
|
||||
|
||||
// Extended: Syntax class holding the grammars used for tokenizing.
|
||||
//
|
||||
// An instance of this class is always available as the `atom.grammars` global.
|
||||
//
|
||||
// The Syntax class also contains properties for things such as the
|
||||
// language-specific comment regexes. See {::getProperty} for more details.
|
||||
module.exports =
|
||||
class GrammarRegistry extends FirstMate.GrammarRegistry {
|
||||
constructor ({config} = {}) {
|
||||
super({maxTokensPerLine: 100, maxLineLength: 1000})
|
||||
this.config = config
|
||||
}
|
||||
|
||||
createToken (value, scopes) {
|
||||
return new Token({value, scopes})
|
||||
}
|
||||
|
||||
// Extended: Select a grammar for the given file path and file contents.
|
||||
//
|
||||
// This picks the best match by checking the file path and contents against
|
||||
// each grammar.
|
||||
//
|
||||
// * `filePath` A {String} file path.
|
||||
// * `fileContents` A {String} of text for the file path.
|
||||
//
|
||||
// Returns a {Grammar}, never null.
|
||||
selectGrammar (filePath, fileContents) {
|
||||
return this.selectGrammarWithScore(filePath, fileContents).grammar
|
||||
}
|
||||
|
||||
selectGrammarWithScore (filePath, fileContents) {
|
||||
let bestMatch = null
|
||||
let highestScore = -Infinity
|
||||
for (let grammar of this.grammars) {
|
||||
const score = this.getGrammarScore(grammar, filePath, fileContents)
|
||||
if ((score > highestScore) || (bestMatch == null)) {
|
||||
bestMatch = grammar
|
||||
highestScore = score
|
||||
}
|
||||
}
|
||||
return {grammar: bestMatch, score: highestScore}
|
||||
}
|
||||
|
||||
// Extended: Returns a {Number} representing how well the grammar matches the
|
||||
// `filePath` and `contents`.
|
||||
getGrammarScore (grammar, filePath, contents) {
|
||||
if ((contents == null) && fs.isFileSync(filePath)) {
|
||||
contents = fs.readFileSync(filePath, 'utf8')
|
||||
}
|
||||
|
||||
let score = this.getGrammarPathScore(grammar, filePath)
|
||||
if ((score > 0) && !grammar.bundledPackage) {
|
||||
score += 0.125
|
||||
}
|
||||
if (this.grammarMatchesContents(grammar, contents)) {
|
||||
score += 0.25
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
getGrammarPathScore (grammar, filePath) {
|
||||
if (!filePath) { return -1 }
|
||||
if (process.platform === 'win32') { filePath = filePath.replace(/\\/g, '/') }
|
||||
|
||||
const pathComponents = filePath.toLowerCase().split(PathSplitRegex)
|
||||
let pathScore = -1
|
||||
|
||||
let customFileTypes
|
||||
if (this.config.get('core.customFileTypes')) {
|
||||
customFileTypes = this.config.get('core.customFileTypes')[grammar.scopeName]
|
||||
}
|
||||
|
||||
let { fileTypes } = grammar
|
||||
if (customFileTypes) {
|
||||
fileTypes = fileTypes.concat(customFileTypes)
|
||||
}
|
||||
|
||||
for (let i = 0; i < fileTypes.length; i++) {
|
||||
const fileType = fileTypes[i]
|
||||
const fileTypeComponents = fileType.toLowerCase().split(PathSplitRegex)
|
||||
const pathSuffix = pathComponents.slice(-fileTypeComponents.length)
|
||||
if (_.isEqual(pathSuffix, fileTypeComponents)) {
|
||||
pathScore = Math.max(pathScore, fileType.length)
|
||||
if (i >= grammar.fileTypes.length) {
|
||||
pathScore += 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pathScore
|
||||
}
|
||||
|
||||
grammarMatchesContents (grammar, contents) {
|
||||
if ((contents == null) || (grammar.firstLineRegex == null)) { return false }
|
||||
|
||||
let escaped = false
|
||||
let numberOfNewlinesInRegex = 0
|
||||
for (let character of grammar.firstLineRegex.source) {
|
||||
switch (character) {
|
||||
case '\\':
|
||||
escaped = !escaped
|
||||
break
|
||||
case 'n':
|
||||
if (escaped) { numberOfNewlinesInRegex++ }
|
||||
escaped = false
|
||||
break
|
||||
default:
|
||||
escaped = false
|
||||
}
|
||||
}
|
||||
const lines = contents.split('\n')
|
||||
return grammar.firstLineRegex.testSync(lines.slice(0, numberOfNewlinesInRegex + 1).join('\n'))
|
||||
}
|
||||
|
||||
// Deprecated: Get the grammar override for the given file path.
|
||||
//
|
||||
// * `filePath` A {String} file path.
|
||||
//
|
||||
// Returns a {String} such as `"source.js"`.
|
||||
grammarOverrideForPath (filePath) {
|
||||
Grim.deprecate('Use atom.textEditors.getGrammarOverride(editor) instead')
|
||||
|
||||
const editor = getEditorForPath(filePath)
|
||||
if (editor) {
|
||||
return atom.textEditors.getGrammarOverride(editor)
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Set the grammar override for the given file path.
|
||||
//
|
||||
// * `filePath` A non-empty {String} file path.
|
||||
// * `scopeName` A {String} such as `"source.js"`.
|
||||
//
|
||||
// Returns undefined.
|
||||
setGrammarOverrideForPath (filePath, scopeName) {
|
||||
Grim.deprecate('Use atom.textEditors.setGrammarOverride(editor, scopeName) instead')
|
||||
|
||||
const editor = getEditorForPath(filePath)
|
||||
if (editor) {
|
||||
atom.textEditors.setGrammarOverride(editor, scopeName)
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: Remove the grammar override for the given file path.
|
||||
//
|
||||
// * `filePath` A {String} file path.
|
||||
//
|
||||
// Returns undefined.
|
||||
clearGrammarOverrideForPath (filePath) {
|
||||
Grim.deprecate('Use atom.textEditors.clearGrammarOverride(editor) instead')
|
||||
|
||||
const editor = getEditorForPath(filePath)
|
||||
if (editor) {
|
||||
atom.textEditors.clearGrammarOverride(editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getEditorForPath (filePath) {
|
||||
if (filePath != null) {
|
||||
return atom.workspace.getTextEditors().find(editor => editor.getPath() === filePath)
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
{Emitter} = require 'event-kit'
|
||||
Gutter = require './gutter'
|
||||
|
||||
module.exports =
|
||||
class GutterContainer
|
||||
constructor: (textEditor) ->
|
||||
@gutters = []
|
||||
@textEditor = textEditor
|
||||
@emitter = new Emitter
|
||||
|
||||
scheduleComponentUpdate: ->
|
||||
@textEditor.scheduleComponentUpdate()
|
||||
|
||||
destroy: ->
|
||||
# Create a copy, because `Gutter::destroy` removes the gutter from
|
||||
# GutterContainer's @gutters.
|
||||
guttersToDestroy = @gutters.slice(0)
|
||||
for gutter in guttersToDestroy
|
||||
gutter.destroy() if gutter.name isnt 'line-number'
|
||||
@gutters = []
|
||||
@emitter.dispose()
|
||||
|
||||
addGutter: (options) ->
|
||||
options = options ? {}
|
||||
gutterName = options.name
|
||||
if gutterName is null
|
||||
throw new Error('A name is required to create a gutter.')
|
||||
if @gutterWithName(gutterName)
|
||||
throw new Error('Tried to create a gutter with a name that is already in use.')
|
||||
newGutter = new Gutter(this, options)
|
||||
|
||||
inserted = false
|
||||
# Insert the gutter into the gutters array, sorted in ascending order by 'priority'.
|
||||
# This could be optimized, but there are unlikely to be many gutters.
|
||||
for i in [0...@gutters.length]
|
||||
if @gutters[i].priority >= newGutter.priority
|
||||
@gutters.splice(i, 0, newGutter)
|
||||
inserted = true
|
||||
break
|
||||
if not inserted
|
||||
@gutters.push newGutter
|
||||
@scheduleComponentUpdate()
|
||||
@emitter.emit 'did-add-gutter', newGutter
|
||||
return newGutter
|
||||
|
||||
getGutters: ->
|
||||
@gutters.slice()
|
||||
|
||||
gutterWithName: (name) ->
|
||||
for gutter in @gutters
|
||||
if gutter.name is name then return gutter
|
||||
null
|
||||
|
||||
observeGutters: (callback) ->
|
||||
callback(gutter) for gutter in @getGutters()
|
||||
@onDidAddGutter callback
|
||||
|
||||
onDidAddGutter: (callback) ->
|
||||
@emitter.on 'did-add-gutter', callback
|
||||
|
||||
onDidRemoveGutter: (callback) ->
|
||||
@emitter.on 'did-remove-gutter', callback
|
||||
|
||||
###
|
||||
Section: Private Methods
|
||||
###
|
||||
|
||||
# Processes the destruction of the gutter. Throws an error if this gutter is
|
||||
# not within this gutterContainer.
|
||||
removeGutter: (gutter) ->
|
||||
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 ' +
|
||||
'within this GutterContainer.'
|
||||
|
||||
# The public interface is Gutter::decorateMarker or TextEditor::decorateMarker.
|
||||
addGutterDecoration: (gutter, marker, options) ->
|
||||
if gutter.name is 'line-number'
|
||||
options.type = 'line-number'
|
||||
else
|
||||
options.type = 'gutter'
|
||||
options.gutterName = gutter.name
|
||||
@textEditor.decorateMarker(marker, options)
|
108
src/gutter-container.js
Normal file
108
src/gutter-container.js
Normal file
@ -0,0 +1,108 @@
|
||||
const {Emitter} = require('event-kit')
|
||||
const Gutter = require('./gutter')
|
||||
|
||||
module.exports = class GutterContainer {
|
||||
constructor (textEditor) {
|
||||
this.gutters = []
|
||||
this.textEditor = textEditor
|
||||
this.emitter = new Emitter()
|
||||
}
|
||||
|
||||
scheduleComponentUpdate () {
|
||||
this.textEditor.scheduleComponentUpdate()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
// Create a copy, because `Gutter::destroy` removes the gutter from
|
||||
// GutterContainer's @gutters.
|
||||
const guttersToDestroy = this.gutters.slice(0)
|
||||
for (let gutter of guttersToDestroy) {
|
||||
if (gutter.name !== 'line-number') { gutter.destroy() }
|
||||
}
|
||||
this.gutters = []
|
||||
this.emitter.dispose()
|
||||
}
|
||||
|
||||
addGutter (options) {
|
||||
options = options || {}
|
||||
const gutterName = options.name
|
||||
if (gutterName === null) {
|
||||
throw new Error('A name is required to create a gutter.')
|
||||
}
|
||||
if (this.gutterWithName(gutterName)) {
|
||||
throw new Error('Tried to create a gutter with a name that is already in use.')
|
||||
}
|
||||
const newGutter = new Gutter(this, options)
|
||||
|
||||
let inserted = false
|
||||
// Insert the gutter into the gutters array, sorted in ascending order by 'priority'.
|
||||
// This could be optimized, but there are unlikely to be many gutters.
|
||||
for (let i = 0; i < this.gutters.length; i++) {
|
||||
if (this.gutters[i].priority >= newGutter.priority) {
|
||||
this.gutters.splice(i, 0, newGutter)
|
||||
inserted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!inserted) {
|
||||
this.gutters.push(newGutter)
|
||||
}
|
||||
this.scheduleComponentUpdate()
|
||||
this.emitter.emit('did-add-gutter', newGutter)
|
||||
return newGutter
|
||||
}
|
||||
|
||||
getGutters () {
|
||||
return this.gutters.slice()
|
||||
}
|
||||
|
||||
gutterWithName (name) {
|
||||
for (let gutter of this.gutters) {
|
||||
if (gutter.name === name) { return gutter }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
observeGutters (callback) {
|
||||
for (let gutter of this.getGutters()) { callback(gutter) }
|
||||
return this.onDidAddGutter(callback)
|
||||
}
|
||||
|
||||
onDidAddGutter (callback) {
|
||||
return this.emitter.on('did-add-gutter', callback)
|
||||
}
|
||||
|
||||
onDidRemoveGutter (callback) {
|
||||
return this.emitter.on('did-remove-gutter', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Private Methods
|
||||
*/
|
||||
|
||||
// Processes the destruction of the gutter. Throws an error if this gutter is
|
||||
// not within this gutterContainer.
|
||||
removeGutter (gutter) {
|
||||
const index = this.gutters.indexOf(gutter)
|
||||
if (index > -1) {
|
||||
this.gutters.splice(index, 1)
|
||||
this.scheduleComponentUpdate()
|
||||
this.emitter.emit('did-remove-gutter', gutter.name)
|
||||
} else {
|
||||
throw new Error('The given gutter cannot be removed because it is not ' +
|
||||
'within this GutterContainer.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// The public interface is Gutter::decorateMarker or TextEditor::decorateMarker.
|
||||
addGutterDecoration (gutter, marker, options) {
|
||||
if (gutter.name === 'line-number') {
|
||||
options.type = 'line-number'
|
||||
} else {
|
||||
options.type = 'gutter'
|
||||
}
|
||||
options.gutterName = gutter.name
|
||||
return this.textEditor.decorateMarker(marker, options)
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
{Emitter} = require 'event-kit'
|
||||
CustomGutterComponent = null
|
||||
|
||||
DefaultPriority = -100
|
||||
|
||||
# Extended: Represents a gutter within a {TextEditor}.
|
||||
#
|
||||
# See {TextEditor::addGutter} for information on creating a gutter.
|
||||
module.exports =
|
||||
class Gutter
|
||||
constructor: (gutterContainer, options) ->
|
||||
@gutterContainer = gutterContainer
|
||||
@name = options?.name
|
||||
@priority = options?.priority ? DefaultPriority
|
||||
@visible = options?.visible ? true
|
||||
|
||||
@emitter = new Emitter
|
||||
|
||||
###
|
||||
Section: Gutter Destruction
|
||||
###
|
||||
|
||||
# Essential: Destroys the gutter.
|
||||
destroy: ->
|
||||
if @name is 'line-number'
|
||||
throw new Error('The line-number gutter cannot be destroyed.')
|
||||
else
|
||||
@gutterContainer.removeGutter(this)
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.dispose()
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Essential: Calls your `callback` when the gutter's visibility changes.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `gutter` The gutter whose visibility changed.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeVisible: (callback) ->
|
||||
@emitter.on 'did-change-visible', callback
|
||||
|
||||
# Essential: Calls your `callback` when the gutter is destroyed.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Visibility
|
||||
###
|
||||
|
||||
# Essential: Hide the 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.
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isVisible: ->
|
||||
@visible
|
||||
|
||||
# Essential: Add a decoration that tracks a {DisplayMarker}. When the marker moves,
|
||||
# is invalidated, or is destroyed, the decoration will be updated to reflect
|
||||
# the marker's state.
|
||||
#
|
||||
# ## Arguments
|
||||
#
|
||||
# * `marker` A {DisplayMarker} you want this decoration to follow.
|
||||
# * `decorationParams` An {Object} representing the decoration. It is passed
|
||||
# to {TextEditor::decorateMarker} as its `decorationParams` and so supports
|
||||
# all options documented there.
|
||||
# * `type` __Caveat__: set to `'line-number'` if this is the line-number
|
||||
# gutter, `'gutter'` otherwise. This cannot be overridden.
|
||||
#
|
||||
# Returns a {Decoration} object
|
||||
decorateMarker: (marker, options) ->
|
||||
@gutterContainer.addGutterDecoration(this, marker, options)
|
||||
|
||||
getElement: ->
|
||||
@element ?= document.createElement('div')
|
107
src/gutter.js
Normal file
107
src/gutter.js
Normal file
@ -0,0 +1,107 @@
|
||||
const {Emitter} = require('event-kit')
|
||||
|
||||
const DefaultPriority = -100
|
||||
|
||||
// Extended: Represents a gutter within a {TextEditor}.
|
||||
//
|
||||
// See {TextEditor::addGutter} for information on creating a gutter.
|
||||
module.exports = class Gutter {
|
||||
constructor (gutterContainer, options) {
|
||||
this.gutterContainer = gutterContainer
|
||||
this.name = options && options.name
|
||||
this.priority = (options && options.priority != null) ? options.priority : DefaultPriority
|
||||
this.visible = (options && options.visible != null) ? options.visible : true
|
||||
|
||||
this.emitter = new Emitter()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Gutter Destruction
|
||||
*/
|
||||
|
||||
// Essential: Destroys the gutter.
|
||||
destroy () {
|
||||
if (this.name === 'line-number') {
|
||||
throw new Error('The line-number gutter cannot be destroyed.')
|
||||
} else {
|
||||
this.gutterContainer.removeGutter(this)
|
||||
this.emitter.emit('did-destroy')
|
||||
this.emitter.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Essential: Calls your `callback` when the gutter's visibility changes.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
// * `gutter` The gutter whose visibility changed.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeVisible (callback) {
|
||||
return this.emitter.on('did-change-visible', callback)
|
||||
}
|
||||
|
||||
// Essential: Calls your `callback` when the gutter is destroyed.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy (callback) {
|
||||
return this.emitter.once('did-destroy', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Visibility
|
||||
*/
|
||||
|
||||
// Essential: Hide the gutter.
|
||||
hide () {
|
||||
if (this.visible) {
|
||||
this.visible = false
|
||||
this.gutterContainer.scheduleComponentUpdate()
|
||||
this.emitter.emit('did-change-visible', this)
|
||||
}
|
||||
}
|
||||
|
||||
// Essential: Show the gutter.
|
||||
show () {
|
||||
if (!this.visible) {
|
||||
this.visible = true
|
||||
this.gutterContainer.scheduleComponentUpdate()
|
||||
this.emitter.emit('did-change-visible', this)
|
||||
}
|
||||
}
|
||||
|
||||
// Essential: Determine whether the gutter is visible.
|
||||
//
|
||||
// Returns a {Boolean}.
|
||||
isVisible () {
|
||||
return this.visible
|
||||
}
|
||||
|
||||
// Essential: Add a decoration that tracks a {DisplayMarker}. When the marker moves,
|
||||
// is invalidated, or is destroyed, the decoration will be updated to reflect
|
||||
// the marker's state.
|
||||
//
|
||||
// ## Arguments
|
||||
//
|
||||
// * `marker` A {DisplayMarker} you want this decoration to follow.
|
||||
// * `decorationParams` An {Object} representing the decoration. It is passed
|
||||
// to {TextEditor::decorateMarker} as its `decorationParams` and so supports
|
||||
// all options documented there.
|
||||
// * `type` __Caveat__: set to `'line-number'` if this is the line-number
|
||||
// gutter, `'gutter'` otherwise. This cannot be overridden.
|
||||
//
|
||||
// Returns a {Decoration} object
|
||||
decorateMarker (marker, options) {
|
||||
return this.gutterContainer.addGutterDecoration(this, marker, options)
|
||||
}
|
||||
|
||||
getElement () {
|
||||
if (this.element == null) this.element = document.createElement('div')
|
||||
return this.element
|
||||
}
|
||||
}
|
@ -1,350 +0,0 @@
|
||||
{Range} = require 'text-buffer'
|
||||
_ = require 'underscore-plus'
|
||||
{OnigRegExp} = require 'oniguruma'
|
||||
ScopeDescriptor = require './scope-descriptor'
|
||||
NullGrammar = require './null-grammar'
|
||||
|
||||
module.exports =
|
||||
class LanguageMode
|
||||
# Sets up a `LanguageMode` for the given {TextEditor}.
|
||||
#
|
||||
# editor - The {TextEditor} to associate with
|
||||
constructor: (@editor) ->
|
||||
{@buffer} = @editor
|
||||
@regexesByPattern = {}
|
||||
|
||||
destroy: ->
|
||||
|
||||
toggleLineCommentForBufferRow: (row) ->
|
||||
@toggleLineCommentsForBufferRows(row, row)
|
||||
|
||||
# Wraps the lines between two rows in comments.
|
||||
#
|
||||
# If the language doesn't have comment, nothing happens.
|
||||
#
|
||||
# startRow - The row {Number} to start at
|
||||
# endRow - The row {Number} to end at
|
||||
toggleLineCommentsForBufferRows: (start, end) ->
|
||||
scope = @editor.scopeDescriptorForBufferPosition([start, 0])
|
||||
commentStrings = @editor.getCommentStrings(scope)
|
||||
return unless commentStrings?.commentStartString
|
||||
{commentStartString, commentEndString} = commentStrings
|
||||
|
||||
buffer = @editor.buffer
|
||||
commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?')
|
||||
commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})")
|
||||
|
||||
if commentEndString
|
||||
shouldUncomment = commentStartRegex.testSync(buffer.lineForRow(start))
|
||||
if shouldUncomment
|
||||
commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?')
|
||||
commentEndRegex = new OnigRegExp("(#{commentEndRegexString})(\\s*)$")
|
||||
startMatch = commentStartRegex.searchSync(buffer.lineForRow(start))
|
||||
endMatch = commentEndRegex.searchSync(buffer.lineForRow(end))
|
||||
if startMatch and endMatch
|
||||
buffer.transact ->
|
||||
columnStart = startMatch[1].length
|
||||
columnEnd = columnStart + startMatch[2].length
|
||||
buffer.setTextInRange([[start, columnStart], [start, columnEnd]], "")
|
||||
|
||||
endLength = buffer.lineLengthForRow(end) - endMatch[2].length
|
||||
endColumn = endLength - endMatch[1].length
|
||||
buffer.setTextInRange([[end, endColumn], [end, endLength]], "")
|
||||
else
|
||||
buffer.transact ->
|
||||
indentLength = buffer.lineForRow(start).match(/^\s*/)?[0].length ? 0
|
||||
buffer.insert([start, indentLength], commentStartString)
|
||||
buffer.insert([end, buffer.lineLengthForRow(end)], commentEndString)
|
||||
else
|
||||
allBlank = true
|
||||
allBlankOrCommented = true
|
||||
|
||||
for row in [start..end] by 1
|
||||
line = buffer.lineForRow(row)
|
||||
blank = line?.match(/^\s*$/)
|
||||
|
||||
allBlank = false unless blank
|
||||
allBlankOrCommented = false unless blank or commentStartRegex.testSync(line)
|
||||
|
||||
shouldUncomment = allBlankOrCommented and not allBlank
|
||||
|
||||
if shouldUncomment
|
||||
for row in [start..end] by 1
|
||||
if match = commentStartRegex.searchSync(buffer.lineForRow(row))
|
||||
columnStart = match[1].length
|
||||
columnEnd = columnStart + match[2].length
|
||||
buffer.setTextInRange([[row, columnStart], [row, columnEnd]], "")
|
||||
else
|
||||
if start is end
|
||||
indent = @editor.indentationForBufferRow(start)
|
||||
else
|
||||
indent = @minIndentLevelForRowRange(start, end)
|
||||
indentString = @editor.buildIndentString(indent)
|
||||
tabLength = @editor.getTabLength()
|
||||
indentRegex = new RegExp("(\t|[ ]{#{tabLength}}){#{Math.floor(indent)}}")
|
||||
for row in [start..end] by 1
|
||||
line = buffer.lineForRow(row)
|
||||
if indentLength = line.match(indentRegex)?[0].length
|
||||
buffer.insert([row, indentLength], commentStartString)
|
||||
else
|
||||
buffer.setTextInRange([[row, 0], [row, indentString.length]], indentString + commentStartString)
|
||||
return
|
||||
|
||||
# Folds all the foldable lines in the buffer.
|
||||
foldAll: ->
|
||||
@unfoldAll()
|
||||
foldedRowRanges = {}
|
||||
for currentRow in [0..@buffer.getLastRow()] by 1
|
||||
rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
|
||||
continue unless startRow?
|
||||
continue if foldedRowRanges[rowRange]
|
||||
|
||||
@editor.foldBufferRowRange(startRow, endRow)
|
||||
foldedRowRanges[rowRange] = true
|
||||
return
|
||||
|
||||
# Unfolds all the foldable lines in the buffer.
|
||||
unfoldAll: ->
|
||||
@editor.displayLayer.destroyAllFolds()
|
||||
|
||||
# Fold all comment and code blocks at a given indentLevel
|
||||
#
|
||||
# indentLevel - A {Number} indicating indentLevel; 0 based.
|
||||
foldAllAtIndentLevel: (indentLevel) ->
|
||||
@unfoldAll()
|
||||
foldedRowRanges = {}
|
||||
for currentRow in [0..@buffer.getLastRow()] by 1
|
||||
rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
|
||||
continue unless startRow?
|
||||
continue if foldedRowRanges[rowRange]
|
||||
|
||||
# assumption: startRow will always be the min indent level for the entire range
|
||||
if @editor.indentationForBufferRow(startRow) is indentLevel
|
||||
@editor.foldBufferRowRange(startRow, endRow)
|
||||
foldedRowRanges[rowRange] = true
|
||||
return
|
||||
|
||||
# Given a buffer row, creates a fold at it.
|
||||
#
|
||||
# bufferRow - A {Number} indicating the buffer row
|
||||
#
|
||||
# Returns the new {Fold}.
|
||||
foldBufferRow: (bufferRow) ->
|
||||
for currentRow in [bufferRow..0] by -1
|
||||
[startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? []
|
||||
continue unless startRow? and startRow <= bufferRow <= endRow
|
||||
unless @editor.isFoldedAtBufferRow(startRow)
|
||||
return @editor.foldBufferRowRange(startRow, endRow)
|
||||
|
||||
# Find the row range for a fold at a given bufferRow. Will handle comments
|
||||
# and code.
|
||||
#
|
||||
# bufferRow - A {Number} indicating the buffer row
|
||||
#
|
||||
# Returns an {Array} of the [startRow, endRow]. Returns null if no range.
|
||||
rowRangeForFoldAtBufferRow: (bufferRow) ->
|
||||
rowRange = @rowRangeForCommentAtBufferRow(bufferRow)
|
||||
rowRange ?= @rowRangeForCodeFoldAtBufferRow(bufferRow)
|
||||
rowRange
|
||||
|
||||
rowRangeForCommentAtBufferRow: (bufferRow) ->
|
||||
return unless @editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment()
|
||||
|
||||
startRow = bufferRow
|
||||
endRow = bufferRow
|
||||
|
||||
if bufferRow > 0
|
||||
for currentRow in [bufferRow-1..0] by -1
|
||||
break unless @editor.tokenizedBuffer.tokenizedLines[currentRow]?.isComment()
|
||||
startRow = currentRow
|
||||
|
||||
if bufferRow < @buffer.getLastRow()
|
||||
for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1
|
||||
break unless @editor.tokenizedBuffer.tokenizedLines[currentRow]?.isComment()
|
||||
endRow = currentRow
|
||||
|
||||
return [startRow, endRow] if startRow isnt endRow
|
||||
|
||||
rowRangeForCodeFoldAtBufferRow: (bufferRow) ->
|
||||
return null unless @isFoldableAtBufferRow(bufferRow)
|
||||
|
||||
startIndentLevel = @editor.indentationForBufferRow(bufferRow)
|
||||
scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
|
||||
for row in [(bufferRow + 1)..@editor.getLastBufferRow()] by 1
|
||||
continue if @editor.isBufferRowBlank(row)
|
||||
indentation = @editor.indentationForBufferRow(row)
|
||||
if indentation <= startIndentLevel
|
||||
includeRowInFold = indentation is startIndentLevel and @foldEndRegexForScopeDescriptor(scopeDescriptor)?.searchSync(@editor.lineTextForBufferRow(row))
|
||||
foldEndRow = row if includeRowInFold
|
||||
break
|
||||
|
||||
foldEndRow = row
|
||||
|
||||
[bufferRow, foldEndRow]
|
||||
|
||||
isFoldableAtBufferRow: (bufferRow) ->
|
||||
@editor.tokenizedBuffer.isFoldableAtRow(bufferRow)
|
||||
|
||||
# Returns a {Boolean} indicating whether the line at the given buffer
|
||||
# row is a comment.
|
||||
isLineCommentedAtBufferRow: (bufferRow) ->
|
||||
return false unless 0 <= bufferRow <= @editor.getLastBufferRow()
|
||||
@editor.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
|
||||
# the same type (comments next to source code).
|
||||
rowRangeForParagraphAtBufferRow: (bufferRow) ->
|
||||
scope = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
|
||||
commentStrings = @editor.getCommentStrings(scope)
|
||||
commentStartRegex = null
|
||||
if commentStrings?.commentStartString? and not commentStrings.commentEndString?
|
||||
commentStartRegexString = _.escapeRegExp(commentStrings.commentStartString).replace(/(\s+)$/, '(?:$1)?')
|
||||
commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})")
|
||||
|
||||
filterCommentStart = (line) ->
|
||||
if commentStartRegex?
|
||||
matches = commentStartRegex.searchSync(line)
|
||||
line = line.substring(matches[0].end) if matches?.length
|
||||
line
|
||||
|
||||
return unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(bufferRow)))
|
||||
|
||||
if @isLineCommentedAtBufferRow(bufferRow)
|
||||
isOriginalRowComment = true
|
||||
range = @rowRangeForCommentAtBufferRow(bufferRow)
|
||||
[firstRow, lastRow] = range or [bufferRow, bufferRow]
|
||||
else
|
||||
isOriginalRowComment = false
|
||||
[firstRow, lastRow] = [0, @editor.getLastBufferRow()-1]
|
||||
|
||||
startRow = bufferRow
|
||||
while startRow > firstRow
|
||||
break if @isLineCommentedAtBufferRow(startRow - 1) isnt isOriginalRowComment
|
||||
break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(startRow - 1)))
|
||||
startRow--
|
||||
|
||||
endRow = bufferRow
|
||||
lastRow = @editor.getLastBufferRow()
|
||||
while endRow < lastRow
|
||||
break if @isLineCommentedAtBufferRow(endRow + 1) isnt isOriginalRowComment
|
||||
break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(endRow + 1)))
|
||||
endRow++
|
||||
|
||||
new Range([startRow, 0], [endRow, @editor.lineTextForBufferRow(endRow).length])
|
||||
|
||||
# Given a buffer row, this returns a suggested indentation level.
|
||||
#
|
||||
# The indentation level provided is based on the current {LanguageMode}.
|
||||
#
|
||||
# bufferRow - A {Number} indicating the buffer row
|
||||
#
|
||||
# Returns a {Number}.
|
||||
suggestedIndentForBufferRow: (bufferRow, options) ->
|
||||
line = @buffer.lineForRow(bufferRow)
|
||||
tokenizedLine = @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow)
|
||||
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
|
||||
|
||||
suggestedIndentForLineAtBufferRow: (bufferRow, line, options) ->
|
||||
tokenizedLine = @editor.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line)
|
||||
@suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options)
|
||||
|
||||
suggestedIndentForTokenizedLineAtBufferRow: (bufferRow, line, tokenizedLine, options) ->
|
||||
iterator = tokenizedLine.getTokenIterator()
|
||||
iterator.next()
|
||||
scopeDescriptor = new ScopeDescriptor(scopes: iterator.getScopes())
|
||||
|
||||
increaseIndentRegex = @increaseIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
decreaseIndentRegex = @decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
decreaseNextIndentRegex = @decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
|
||||
if options?.skipBlankLines ? true
|
||||
precedingRow = @buffer.previousNonBlankRow(bufferRow)
|
||||
return 0 unless precedingRow?
|
||||
else
|
||||
precedingRow = bufferRow - 1
|
||||
return 0 if precedingRow < 0
|
||||
|
||||
desiredIndentLevel = @editor.indentationForBufferRow(precedingRow)
|
||||
return desiredIndentLevel unless increaseIndentRegex
|
||||
|
||||
unless @editor.isBufferRowCommented(precedingRow)
|
||||
precedingLine = @buffer.lineForRow(precedingRow)
|
||||
desiredIndentLevel += 1 if increaseIndentRegex?.testSync(precedingLine)
|
||||
desiredIndentLevel -= 1 if decreaseNextIndentRegex?.testSync(precedingLine)
|
||||
|
||||
unless @buffer.isRowBlank(precedingRow)
|
||||
desiredIndentLevel -= 1 if decreaseIndentRegex?.testSync(line)
|
||||
|
||||
Math.max(desiredIndentLevel, 0)
|
||||
|
||||
# Calculate a minimum indent level for a range of lines excluding empty lines.
|
||||
#
|
||||
# startRow - The row {Number} to start at
|
||||
# endRow - The row {Number} to end at
|
||||
#
|
||||
# Returns a {Number} of the indent level of the block of lines.
|
||||
minIndentLevelForRowRange: (startRow, endRow) ->
|
||||
indents = (@editor.indentationForBufferRow(row) for row in [startRow..endRow] by 1 when not @editor.isBufferRowBlank(row))
|
||||
indents = [0] unless indents.length
|
||||
Math.min(indents...)
|
||||
|
||||
# Indents all the rows between two buffer row numbers.
|
||||
#
|
||||
# startRow - The row {Number} to start at
|
||||
# endRow - The row {Number} to end at
|
||||
autoIndentBufferRows: (startRow, endRow) ->
|
||||
@autoIndentBufferRow(row) for row in [startRow..endRow] by 1
|
||||
return
|
||||
|
||||
# Given a buffer row, this indents it.
|
||||
#
|
||||
# bufferRow - The row {Number}.
|
||||
# options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}.
|
||||
autoIndentBufferRow: (bufferRow, options) ->
|
||||
indentLevel = @suggestedIndentForBufferRow(bufferRow, options)
|
||||
@editor.setIndentationForBufferRow(bufferRow, indentLevel, options)
|
||||
|
||||
# Given a buffer row, this decreases the indentation.
|
||||
#
|
||||
# bufferRow - The row {Number}
|
||||
autoDecreaseIndentForBufferRow: (bufferRow) ->
|
||||
scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0])
|
||||
return unless decreaseIndentRegex = @decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
|
||||
line = @buffer.lineForRow(bufferRow)
|
||||
return unless decreaseIndentRegex.testSync(line)
|
||||
|
||||
currentIndentLevel = @editor.indentationForBufferRow(bufferRow)
|
||||
return if currentIndentLevel is 0
|
||||
|
||||
precedingRow = @buffer.previousNonBlankRow(bufferRow)
|
||||
return unless precedingRow?
|
||||
|
||||
precedingLine = @buffer.lineForRow(precedingRow)
|
||||
desiredIndentLevel = @editor.indentationForBufferRow(precedingRow)
|
||||
|
||||
if increaseIndentRegex = @increaseIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
desiredIndentLevel -= 1 unless increaseIndentRegex.testSync(precedingLine)
|
||||
|
||||
if decreaseNextIndentRegex = @decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)
|
||||
desiredIndentLevel -= 1 if decreaseNextIndentRegex.testSync(precedingLine)
|
||||
|
||||
if desiredIndentLevel >= 0 and desiredIndentLevel < currentIndentLevel
|
||||
@editor.setIndentationForBufferRow(bufferRow, desiredIndentLevel)
|
||||
|
||||
cacheRegex: (pattern) ->
|
||||
if pattern
|
||||
@regexesByPattern[pattern] ?= new OnigRegExp(pattern)
|
||||
|
||||
increaseIndentRegexForScopeDescriptor: (scopeDescriptor) ->
|
||||
@cacheRegex(@editor.getIncreaseIndentPattern(scopeDescriptor))
|
||||
|
||||
decreaseIndentRegexForScopeDescriptor: (scopeDescriptor) ->
|
||||
@cacheRegex(@editor.getDecreaseIndentPattern(scopeDescriptor))
|
||||
|
||||
decreaseNextIndentRegexForScopeDescriptor: (scopeDescriptor) ->
|
||||
@cacheRegex(@editor.getDecreaseNextIndentPattern(scopeDescriptor))
|
||||
|
||||
foldEndRegexForScopeDescriptor: (scopeDescriptor) ->
|
||||
@cacheRegex(@editor.getFoldEndPattern(scopeDescriptor))
|
@ -648,6 +648,41 @@ class AtomApplication
|
||||
# :devMode - Boolean to control the opened window's dev mode.
|
||||
# :safeMode - Boolean to control the opened window's safe mode.
|
||||
openUrl: ({urlToOpen, devMode, safeMode, env}) ->
|
||||
parsedUrl = url.parse(urlToOpen)
|
||||
return unless parsedUrl.protocol is "atom:"
|
||||
|
||||
pack = @findPackageWithName(parsedUrl.host, devMode)
|
||||
if pack?.urlMain
|
||||
@openPackageUrlMain(parsedUrl.host, pack.urlMain, urlToOpen, devMode, safeMode, env)
|
||||
else
|
||||
@openPackageUriHandler(urlToOpen, devMode, safeMode, env)
|
||||
|
||||
openPackageUriHandler: (url, devMode, safeMode, env) ->
|
||||
resourcePath = @resourcePath
|
||||
if devMode
|
||||
try
|
||||
windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window'))
|
||||
resourcePath = @devResourcePath
|
||||
|
||||
windowInitializationScript ?= require.resolve('../initialize-application-window')
|
||||
if @lastFocusedWindow?
|
||||
@lastFocusedWindow.sendURIMessage url
|
||||
else
|
||||
windowDimensions = @getDimensionsForNewWindow()
|
||||
@lastFocusedWindow = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env})
|
||||
@lastFocusedWindow.on 'window:loaded', =>
|
||||
@lastFocusedWindow.sendURIMessage url
|
||||
|
||||
findPackageWithName: (packageName, devMode) ->
|
||||
_.find @getPackageManager(devMode).getAvailablePackageMetadata(), ({name}) -> name is packageName
|
||||
|
||||
openPackageUrlMain: (packageName, packageUrlMain, urlToOpen, devMode, safeMode, env) ->
|
||||
packagePath = @getPackageManager(devMode).resolvePackagePath(packageName)
|
||||
windowInitializationScript = path.resolve(packagePath, packageUrlMain)
|
||||
windowDimensions = @getDimensionsForNewWindow()
|
||||
new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env})
|
||||
|
||||
getPackageManager: (devMode) ->
|
||||
unless @packages?
|
||||
PackageManager = require '../package-manager'
|
||||
@packages = new PackageManager({})
|
||||
@ -656,18 +691,8 @@ class AtomApplication
|
||||
devMode: devMode
|
||||
resourcePath: @resourcePath
|
||||
|
||||
packageName = url.parse(urlToOpen).host
|
||||
pack = _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName
|
||||
if pack?
|
||||
if pack.urlMain
|
||||
packagePath = @packages.resolvePackagePath(packageName)
|
||||
windowInitializationScript = path.resolve(packagePath, pack.urlMain)
|
||||
windowDimensions = @getDimensionsForNewWindow()
|
||||
new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env})
|
||||
else
|
||||
console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}"
|
||||
else
|
||||
console.log "Opening unknown url: #{urlToOpen}"
|
||||
@packages
|
||||
|
||||
|
||||
# Opens up a new {AtomWindow} to run specs within.
|
||||
#
|
||||
|
@ -232,6 +232,9 @@ class AtomWindow
|
||||
unless @atomApplication.sendCommandToFirstResponder(command)
|
||||
@sendCommandToBrowserWindow(command, args...)
|
||||
|
||||
sendURIMessage: (uri) ->
|
||||
@browserWindow.webContents.send 'uri-message', uri
|
||||
|
||||
sendCommandToBrowserWindow: (command, args...) ->
|
||||
action = if args[0]?.contextCommand then 'context-command' else 'command'
|
||||
@browserWindow.webContents.send action, command, args...
|
||||
|
@ -19,6 +19,8 @@ module.exports = function parseCommandLine (processArgs) {
|
||||
will be opened in that window. Otherwise, they will be opened in a new
|
||||
window.
|
||||
|
||||
Paths that start with \`atom://\` will be interpreted as URLs.
|
||||
|
||||
Environment Variables:
|
||||
|
||||
ATOM_DEV_RESOURCE_PATH The path from which Atom loads source code in dev mode.
|
||||
@ -56,8 +58,18 @@ module.exports = function parseCommandLine (processArgs) {
|
||||
options.string('user-data-dir')
|
||||
options.boolean('clear-window-state').describe('clear-window-state', 'Delete all Atom environment state.')
|
||||
options.boolean('enable-electron-logging').describe('enable-electron-logging', 'Enable low-level logging messages from Electron.')
|
||||
options.boolean('uri-handler')
|
||||
|
||||
const args = options.argv
|
||||
let args = options.argv
|
||||
|
||||
// If --uri-handler is set, then we parse NOTHING else
|
||||
if (args.uriHandler) {
|
||||
args = {
|
||||
uriHandler: true,
|
||||
'uri-handler': true,
|
||||
_: args._.filter(str => str.startsWith('atom://')).slice(0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (args.help) {
|
||||
process.stdout.write(options.help())
|
||||
@ -76,7 +88,6 @@ module.exports = function parseCommandLine (processArgs) {
|
||||
|
||||
const addToLastWindow = args['add']
|
||||
const safeMode = args['safe']
|
||||
const pathsToOpen = args._
|
||||
const benchmark = args['benchmark']
|
||||
const benchmarkTest = args['benchmark-test']
|
||||
const test = args['test']
|
||||
@ -100,11 +111,20 @@ module.exports = function parseCommandLine (processArgs) {
|
||||
const userDataDir = args['user-data-dir']
|
||||
const profileStartup = args['profile-startup']
|
||||
const clearWindowState = args['clear-window-state']
|
||||
const urlsToOpen = []
|
||||
let pathsToOpen = []
|
||||
let urlsToOpen = []
|
||||
let devMode = args['dev']
|
||||
let devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH || path.join(app.getPath('home'), 'github', 'atom')
|
||||
let resourcePath = null
|
||||
|
||||
for (const path of args._) {
|
||||
if (path.startsWith('atom://')) {
|
||||
urlsToOpen.push(path)
|
||||
} else {
|
||||
pathsToOpen.push(path)
|
||||
}
|
||||
}
|
||||
|
||||
if (args['resource-path']) {
|
||||
devMode = true
|
||||
devResourcePath = args['resource-path']
|
||||
|
@ -239,7 +239,7 @@ class RegistryWatcherNode {
|
||||
this.childPaths.add(path.join(...childPathSegments))
|
||||
}
|
||||
|
||||
// Private: Stop assuming responsbility for a previously assigned child path. If this node is
|
||||
// Private: Stop assuming responsibility for a previously assigned child path. If this node is
|
||||
// removed, the named child path will no longer be allocated a {RegistryWatcherNode}.
|
||||
//
|
||||
// * `childPathSegments` the {Array} of path segments between this node's directory and the no longer
|
||||
@ -323,13 +323,13 @@ class RegistryWatcherNode {
|
||||
}
|
||||
}
|
||||
|
||||
// Private: A {RegisteryNode} traversal result that's returned when neither a directory, its children, nor its parents
|
||||
// Private: A {RegistryNode} traversal result that's returned when neither a directory, its children, nor its parents
|
||||
// are present in the tree.
|
||||
class MissingResult {
|
||||
|
||||
// Private: Instantiate a new {MissingResult}.
|
||||
//
|
||||
// * `lastParent` the final succesfully traversed {RegistryNode}.
|
||||
// * `lastParent` the final successfully traversed {RegistryNode}.
|
||||
constructor (lastParent) {
|
||||
this.lastParent = lastParent
|
||||
}
|
||||
|
@ -1,183 +0,0 @@
|
||||
{Emitter} = require 'event-kit'
|
||||
Notification = require '../src/notification'
|
||||
|
||||
# Public: A notification manager used to create {Notification}s to be shown
|
||||
# to the user.
|
||||
#
|
||||
# An instance of this class is always available as the `atom.notifications`
|
||||
# global.
|
||||
module.exports =
|
||||
class NotificationManager
|
||||
constructor: ->
|
||||
@notifications = []
|
||||
@emitter = new Emitter
|
||||
|
||||
###
|
||||
Section: Events
|
||||
###
|
||||
|
||||
# Public: Invoke the given callback after a notification has been added.
|
||||
#
|
||||
# * `callback` {Function} to be called after the notification is added.
|
||||
# * `notification` The {Notification} that was added.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidAddNotification: (callback) ->
|
||||
@emitter.on 'did-add-notification', callback
|
||||
|
||||
###
|
||||
Section: Adding Notifications
|
||||
###
|
||||
|
||||
# Public: Add a success notification.
|
||||
#
|
||||
# * `message` A {String} message
|
||||
# * `options` (optional) An options {Object} with the following keys:
|
||||
# * `buttons` (optional) An {Array} of {Object} where each {Object} has the
|
||||
# following options:
|
||||
# * `className` (optional) {String} a class name to add to the button's
|
||||
# default class name (`btn btn-success`).
|
||||
# * `onDidClick` (optional) {Function} callback to call when the button
|
||||
# has been clicked. The context will be set to the
|
||||
# {NotificationElement} instance.
|
||||
# * `text` {String} inner text for the button
|
||||
# * `description` (optional) A Markdown {String} containing a longer
|
||||
# description about the notification. By default, this **will not**
|
||||
# preserve newlines and whitespace when it is rendered.
|
||||
# * `detail` (optional) A plain-text {String} containing additional details
|
||||
# about the notification. By default, this **will** preserve newlines
|
||||
# and whitespace when it is rendered.
|
||||
# * `dismissable` (optional) A {Boolean} indicating whether this
|
||||
# notification can be dismissed by the user. Defaults to `false`.
|
||||
# * `icon` (optional) A {String} name of an icon from Octicons to display
|
||||
# in the notification header. Defaults to `'check'`.
|
||||
addSuccess: (message, options) ->
|
||||
@addNotification(new Notification('success', message, options))
|
||||
|
||||
# Public: Add an informational notification.
|
||||
#
|
||||
# * `message` A {String} message
|
||||
# * `options` (optional) An options {Object} with the following keys:
|
||||
# * `buttons` (optional) An {Array} of {Object} where each {Object} has the
|
||||
# following options:
|
||||
# * `className` (optional) {String} a class name to add to the button's
|
||||
# default class name (`btn btn-info`).
|
||||
# * `onDidClick` (optional) {Function} callback to call when the button
|
||||
# has been clicked. The context will be set to the
|
||||
# {NotificationElement} instance.
|
||||
# * `text` {String} inner text for the button
|
||||
# * `description` (optional) A Markdown {String} containing a longer
|
||||
# description about the notification. By default, this **will not**
|
||||
# preserve newlines and whitespace when it is rendered.
|
||||
# * `detail` (optional) A plain-text {String} containing additional details
|
||||
# about the notification. By default, this **will** preserve newlines
|
||||
# and whitespace when it is rendered.
|
||||
# * `dismissable` (optional) A {Boolean} indicating whether this
|
||||
# notification can be dismissed by the user. Defaults to `false`.
|
||||
# * `icon` (optional) A {String} name of an icon from Octicons to display
|
||||
# in the notification header. Defaults to `'info'`.
|
||||
addInfo: (message, options) ->
|
||||
@addNotification(new Notification('info', message, options))
|
||||
|
||||
# Public: Add a warning notification.
|
||||
#
|
||||
# * `message` A {String} message
|
||||
# * `options` (optional) An options {Object} with the following keys:
|
||||
# * `buttons` (optional) An {Array} of {Object} where each {Object} has the
|
||||
# following options:
|
||||
# * `className` (optional) {String} a class name to add to the button's
|
||||
# default class name (`btn btn-warning`).
|
||||
# * `onDidClick` (optional) {Function} callback to call when the button
|
||||
# has been clicked. The context will be set to the
|
||||
# {NotificationElement} instance.
|
||||
# * `text` {String} inner text for the button
|
||||
# * `description` (optional) A Markdown {String} containing a longer
|
||||
# description about the notification. By default, this **will not**
|
||||
# preserve newlines and whitespace when it is rendered.
|
||||
# * `detail` (optional) A plain-text {String} containing additional details
|
||||
# about the notification. By default, this **will** preserve newlines
|
||||
# and whitespace when it is rendered.
|
||||
# * `dismissable` (optional) A {Boolean} indicating whether this
|
||||
# notification can be dismissed by the user. Defaults to `false`.
|
||||
# * `icon` (optional) A {String} name of an icon from Octicons to display
|
||||
# in the notification header. Defaults to `'alert'`.
|
||||
addWarning: (message, options) ->
|
||||
@addNotification(new Notification('warning', message, options))
|
||||
|
||||
# Public: Add an error notification.
|
||||
#
|
||||
# * `message` A {String} message
|
||||
# * `options` (optional) An options {Object} with the following keys:
|
||||
# * `buttons` (optional) An {Array} of {Object} where each {Object} has the
|
||||
# following options:
|
||||
# * `className` (optional) {String} a class name to add to the button's
|
||||
# default class name (`btn btn-error`).
|
||||
# * `onDidClick` (optional) {Function} callback to call when the button
|
||||
# has been clicked. The context will be set to the
|
||||
# {NotificationElement} instance.
|
||||
# * `text` {String} inner text for the button
|
||||
# * `description` (optional) A Markdown {String} containing a longer
|
||||
# description about the notification. By default, this **will not**
|
||||
# preserve newlines and whitespace when it is rendered.
|
||||
# * `detail` (optional) A plain-text {String} containing additional details
|
||||
# about the notification. By default, this **will** preserve newlines
|
||||
# and whitespace when it is rendered.
|
||||
# * `dismissable` (optional) A {Boolean} indicating whether this
|
||||
# notification can be dismissed by the user. Defaults to `false`.
|
||||
# * `icon` (optional) A {String} name of an icon from Octicons to display
|
||||
# in the notification header. Defaults to `'flame'`.
|
||||
# * `stack` (optional) A preformatted {String} with stack trace information
|
||||
# describing the location of the error.
|
||||
addError: (message, options) ->
|
||||
@addNotification(new Notification('error', message, options))
|
||||
|
||||
# Public: Add a fatal error notification.
|
||||
#
|
||||
# * `message` A {String} message
|
||||
# * `options` (optional) An options {Object} with the following keys:
|
||||
# * `buttons` (optional) An {Array} of {Object} where each {Object} has the
|
||||
# following options:
|
||||
# * `className` (optional) {String} a class name to add to the button's
|
||||
# default class name (`btn btn-error`).
|
||||
# * `onDidClick` (optional) {Function} callback to call when the button
|
||||
# has been clicked. The context will be set to the
|
||||
# {NotificationElement} instance.
|
||||
# * `text` {String} inner text for the button
|
||||
# * `description` (optional) A Markdown {String} containing a longer
|
||||
# description about the notification. By default, this **will not**
|
||||
# preserve newlines and whitespace when it is rendered.
|
||||
# * `detail` (optional) A plain-text {String} containing additional details
|
||||
# about the notification. By default, this **will** preserve newlines
|
||||
# and whitespace when it is rendered.
|
||||
# * `dismissable` (optional) A {Boolean} indicating whether this
|
||||
# notification can be dismissed by the user. Defaults to `false`.
|
||||
# * `icon` (optional) A {String} name of an icon from Octicons to display
|
||||
# in the notification header. Defaults to `'bug'`.
|
||||
# * `stack` (optional) A preformatted {String} with stack trace information
|
||||
# describing the location of the error.
|
||||
addFatalError: (message, options) ->
|
||||
@addNotification(new Notification('fatal', message, options))
|
||||
|
||||
add: (type, message, options) ->
|
||||
@addNotification(new Notification(type, message, options))
|
||||
|
||||
addNotification: (notification) ->
|
||||
@notifications.push(notification)
|
||||
@emitter.emit('did-add-notification', notification)
|
||||
notification
|
||||
|
||||
###
|
||||
Section: Getting Notifications
|
||||
###
|
||||
|
||||
# Public: Get all the notifications.
|
||||
#
|
||||
# Returns an {Array} of {Notification}s.
|
||||
getNotifications: -> @notifications.slice()
|
||||
|
||||
###
|
||||
Section: Managing Notifications
|
||||
###
|
||||
|
||||
clear: ->
|
||||
@notifications = []
|
206
src/notification-manager.js
Normal file
206
src/notification-manager.js
Normal file
@ -0,0 +1,206 @@
|
||||
const {Emitter} = require('event-kit')
|
||||
const Notification = require('../src/notification')
|
||||
|
||||
// Public: A notification manager used to create {Notification}s to be shown
|
||||
// to the user.
|
||||
//
|
||||
// An instance of this class is always available as the `atom.notifications`
|
||||
// global.
|
||||
module.exports =
|
||||
class NotificationManager {
|
||||
constructor () {
|
||||
this.notifications = []
|
||||
this.emitter = new Emitter()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Events
|
||||
*/
|
||||
|
||||
// Public: Invoke the given callback after a notification has been added.
|
||||
//
|
||||
// * `callback` {Function} to be called after the notification is added.
|
||||
// * `notification` The {Notification} that was added.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidAddNotification (callback) {
|
||||
return this.emitter.on('did-add-notification', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Adding Notifications
|
||||
*/
|
||||
|
||||
// Public: Add a success notification.
|
||||
//
|
||||
// * `message` A {String} message
|
||||
// * `options` (optional) An options {Object} with the following keys:
|
||||
// * `buttons` (optional) An {Array} of {Object} where each {Object} has
|
||||
// the following options:
|
||||
// * `className` (optional) {String} a class name to add to the button's
|
||||
// default class name (`btn btn-success`).
|
||||
// * `onDidClick` (optional) {Function} callback to call when the button
|
||||
// has been clicked. The context will be set to the
|
||||
// {NotificationElement} instance.
|
||||
// * `text` {String} inner text for the button
|
||||
// * `description` (optional) A Markdown {String} containing a longer
|
||||
// description about the notification. By default, this **will not**
|
||||
// preserve newlines and whitespace when it is rendered.
|
||||
// * `detail` (optional) A plain-text {String} containing additional
|
||||
// details about the notification. By default, this **will** preserve
|
||||
// newlines and whitespace when it is rendered.
|
||||
// * `dismissable` (optional) A {Boolean} indicating whether this
|
||||
// notification can be dismissed by the user. Defaults to `false`.
|
||||
// * `icon` (optional) A {String} name of an icon from Octicons to display
|
||||
// in the notification header. Defaults to `'check'`.
|
||||
//
|
||||
// Returns the {Notification} that was added.
|
||||
addSuccess (message, options) {
|
||||
return this.addNotification(new Notification('success', message, options))
|
||||
}
|
||||
|
||||
// Public: Add an informational notification.
|
||||
//
|
||||
// * `message` A {String} message
|
||||
// * `options` (optional) An options {Object} with the following keys:
|
||||
// * `buttons` (optional) An {Array} of {Object} where each {Object} has
|
||||
// the following options:
|
||||
// * `className` (optional) {String} a class name to add to the button's
|
||||
// default class name (`btn btn-info`).
|
||||
// * `onDidClick` (optional) {Function} callback to call when the button
|
||||
// has been clicked. The context will be set to the
|
||||
// {NotificationElement} instance.
|
||||
// * `text` {String} inner text for the button
|
||||
// * `description` (optional) A Markdown {String} containing a longer
|
||||
// description about the notification. By default, this **will not**
|
||||
// preserve newlines and whitespace when it is rendered.
|
||||
// * `detail` (optional) A plain-text {String} containing additional
|
||||
// details about the notification. By default, this **will** preserve
|
||||
// newlines and whitespace when it is rendered.
|
||||
// * `dismissable` (optional) A {Boolean} indicating whether this
|
||||
// notification can be dismissed by the user. Defaults to `false`.
|
||||
// * `icon` (optional) A {String} name of an icon from Octicons to display
|
||||
// in the notification header. Defaults to `'info'`.
|
||||
//
|
||||
// Returns the {Notification} that was added.
|
||||
addInfo (message, options) {
|
||||
return this.addNotification(new Notification('info', message, options))
|
||||
}
|
||||
|
||||
// Public: Add a warning notification.
|
||||
//
|
||||
// * `message` A {String} message
|
||||
// * `options` (optional) An options {Object} with the following keys:
|
||||
// * `buttons` (optional) An {Array} of {Object} where each {Object} has
|
||||
// the following options:
|
||||
// * `className` (optional) {String} a class name to add to the button's
|
||||
// default class name (`btn btn-warning`).
|
||||
// * `onDidClick` (optional) {Function} callback to call when the button
|
||||
// has been clicked. The context will be set to the
|
||||
// {NotificationElement} instance.
|
||||
// * `text` {String} inner text for the button
|
||||
// * `description` (optional) A Markdown {String} containing a longer
|
||||
// description about the notification. By default, this **will not**
|
||||
// preserve newlines and whitespace when it is rendered.
|
||||
// * `detail` (optional) A plain-text {String} containing additional
|
||||
// details about the notification. By default, this **will** preserve
|
||||
// newlines and whitespace when it is rendered.
|
||||
// * `dismissable` (optional) A {Boolean} indicating whether this
|
||||
// notification can be dismissed by the user. Defaults to `false`.
|
||||
// * `icon` (optional) A {String} name of an icon from Octicons to display
|
||||
// in the notification header. Defaults to `'alert'`.
|
||||
//
|
||||
// Returns the {Notification} that was added.
|
||||
addWarning (message, options) {
|
||||
return this.addNotification(new Notification('warning', message, options))
|
||||
}
|
||||
|
||||
// Public: Add an error notification.
|
||||
//
|
||||
// * `message` A {String} message
|
||||
// * `options` (optional) An options {Object} with the following keys:
|
||||
// * `buttons` (optional) An {Array} of {Object} where each {Object} has
|
||||
// the following options:
|
||||
// * `className` (optional) {String} a class name to add to the button's
|
||||
// default class name (`btn btn-error`).
|
||||
// * `onDidClick` (optional) {Function} callback to call when the button
|
||||
// has been clicked. The context will be set to the
|
||||
// {NotificationElement} instance.
|
||||
// * `text` {String} inner text for the button
|
||||
// * `description` (optional) A Markdown {String} containing a longer
|
||||
// description about the notification. By default, this **will not**
|
||||
// preserve newlines and whitespace when it is rendered.
|
||||
// * `detail` (optional) A plain-text {String} containing additional
|
||||
// details about the notification. By default, this **will** preserve
|
||||
// newlines and whitespace when it is rendered.
|
||||
// * `dismissable` (optional) A {Boolean} indicating whether this
|
||||
// notification can be dismissed by the user. Defaults to `false`.
|
||||
// * `icon` (optional) A {String} name of an icon from Octicons to display
|
||||
// in the notification header. Defaults to `'flame'`.
|
||||
// * `stack` (optional) A preformatted {String} with stack trace
|
||||
// information describing the location of the error.
|
||||
//
|
||||
// Returns the {Notification} that was added.
|
||||
addError (message, options) {
|
||||
return this.addNotification(new Notification('error', message, options))
|
||||
}
|
||||
|
||||
// Public: Add a fatal error notification.
|
||||
//
|
||||
// * `message` A {String} message
|
||||
// * `options` (optional) An options {Object} with the following keys:
|
||||
// * `buttons` (optional) An {Array} of {Object} where each {Object} has
|
||||
// the following options:
|
||||
// * `className` (optional) {String} a class name to add to the button's
|
||||
// default class name (`btn btn-error`).
|
||||
// * `onDidClick` (optional) {Function} callback to call when the button
|
||||
// has been clicked. The context will be set to the
|
||||
// {NotificationElement} instance.
|
||||
// * `text` {String} inner text for the button
|
||||
// * `description` (optional) A Markdown {String} containing a longer
|
||||
// description about the notification. By default, this **will not**
|
||||
// preserve newlines and whitespace when it is rendered.
|
||||
// * `detail` (optional) A plain-text {String} containing additional
|
||||
// details about the notification. By default, this **will** preserve
|
||||
// newlines and whitespace when it is rendered.
|
||||
// * `dismissable` (optional) A {Boolean} indicating whether this
|
||||
// notification can be dismissed by the user. Defaults to `false`.
|
||||
// * `icon` (optional) A {String} name of an icon from Octicons to display
|
||||
// in the notification header. Defaults to `'bug'`.
|
||||
// * `stack` (optional) A preformatted {String} with stack trace
|
||||
// information describing the location of the error.
|
||||
//
|
||||
// Returns the {Notification} that was added.
|
||||
addFatalError (message, options) {
|
||||
return this.addNotification(new Notification('fatal', message, options))
|
||||
}
|
||||
|
||||
add (type, message, options) {
|
||||
return this.addNotification(new Notification(type, message, options))
|
||||
}
|
||||
|
||||
addNotification (notification) {
|
||||
this.notifications.push(notification)
|
||||
this.emitter.emit('did-add-notification', notification)
|
||||
return notification
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Getting Notifications
|
||||
*/
|
||||
|
||||
// Public: Get all the notifications.
|
||||
//
|
||||
// Returns an {Array} of {Notification}s.
|
||||
getNotifications () {
|
||||
return this.notifications.slice()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Managing Notifications
|
||||
*/
|
||||
|
||||
clear () {
|
||||
this.notifications = []
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
{Emitter} = require 'event-kit'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
# Public: A notification to the user containing a message and type.
|
||||
module.exports =
|
||||
class Notification
|
||||
constructor: (@type, @message, @options={}) ->
|
||||
@emitter = new Emitter
|
||||
@timestamp = new Date()
|
||||
@dismissed = true
|
||||
@dismissed = false if @isDismissable()
|
||||
@displayed = false
|
||||
@validate()
|
||||
|
||||
validate: ->
|
||||
if typeof @message isnt 'string'
|
||||
throw new Error("Notification must be created with string message: #{@message}")
|
||||
|
||||
unless _.isObject(@options) and not _.isArray(@options)
|
||||
throw new Error("Notification must be created with an options object: #{@options}")
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Public: Invoke the given callback when the notification is dismissed.
|
||||
#
|
||||
# * `callback` {Function} to be called when the notification is dismissed.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDismiss: (callback) ->
|
||||
@emitter.on 'did-dismiss', callback
|
||||
|
||||
# Public: Invoke the given callback when the notification is displayed.
|
||||
#
|
||||
# * `callback` {Function} to be called when the notification is displayed.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDisplay: (callback) ->
|
||||
@emitter.on 'did-display', callback
|
||||
|
||||
getOptions: -> @options
|
||||
|
||||
###
|
||||
Section: Methods
|
||||
###
|
||||
|
||||
# Public: Returns the {String} type.
|
||||
getType: -> @type
|
||||
|
||||
# Public: Returns the {String} message.
|
||||
getMessage: -> @message
|
||||
|
||||
getTimestamp: -> @timestamp
|
||||
|
||||
getDetail: -> @options.detail
|
||||
|
||||
isEqual: (other) ->
|
||||
@getMessage() is other.getMessage() \
|
||||
and @getType() is other.getType() \
|
||||
and @getDetail() is other.getDetail()
|
||||
|
||||
# Extended: Dismisses the notification, removing it from the UI. Calling this programmatically
|
||||
# will call all callbacks added via `onDidDismiss`.
|
||||
dismiss: ->
|
||||
return unless @isDismissable() and not @isDismissed()
|
||||
@dismissed = true
|
||||
@emitter.emit 'did-dismiss', this
|
||||
|
||||
isDismissed: -> @dismissed
|
||||
|
||||
isDismissable: -> !!@options.dismissable
|
||||
|
||||
wasDisplayed: -> @displayed
|
||||
|
||||
setDisplayed: (@displayed) ->
|
||||
@emitter.emit 'did-display', this
|
||||
|
||||
getIcon: ->
|
||||
return @options.icon if @options.icon?
|
||||
switch @type
|
||||
when 'fatal' then 'bug'
|
||||
when 'error' then 'flame'
|
||||
when 'warning' then 'alert'
|
||||
when 'info' then 'info'
|
||||
when 'success' then 'check'
|
118
src/notification.js
Normal file
118
src/notification.js
Normal file
@ -0,0 +1,118 @@
|
||||
const {Emitter} = require('event-kit')
|
||||
const _ = require('underscore-plus')
|
||||
|
||||
// Public: A notification to the user containing a message and type.
|
||||
module.exports =
|
||||
class Notification {
|
||||
constructor (type, message, options = {}) {
|
||||
this.type = type
|
||||
this.message = message
|
||||
this.options = options
|
||||
this.emitter = new Emitter()
|
||||
this.timestamp = new Date()
|
||||
this.dismissed = true
|
||||
if (this.isDismissable()) this.dismissed = false
|
||||
this.displayed = false
|
||||
this.validate()
|
||||
}
|
||||
|
||||
validate () {
|
||||
if (typeof this.message !== 'string') {
|
||||
throw new Error(`Notification must be created with string message: ${this.message}`)
|
||||
}
|
||||
|
||||
if (!_.isObject(this.options) || _.isArray(this.options)) {
|
||||
throw new Error(`Notification must be created with an options object: ${this.options}`)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Public: Invoke the given callback when the notification is dismissed.
|
||||
//
|
||||
// * `callback` {Function} to be called when the notification is dismissed.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDismiss (callback) {
|
||||
return this.emitter.on('did-dismiss', callback)
|
||||
}
|
||||
|
||||
// Public: Invoke the given callback when the notification is displayed.
|
||||
//
|
||||
// * `callback` {Function} to be called when the notification is displayed.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDisplay (callback) {
|
||||
return this.emitter.on('did-display', callback)
|
||||
}
|
||||
|
||||
getOptions () {
|
||||
return this.options
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Methods
|
||||
*/
|
||||
|
||||
// Public: Returns the {String} type.
|
||||
getType () {
|
||||
return this.type
|
||||
}
|
||||
|
||||
// Public: Returns the {String} message.
|
||||
getMessage () {
|
||||
return this.message
|
||||
}
|
||||
|
||||
getTimestamp () {
|
||||
return this.timestamp
|
||||
}
|
||||
|
||||
getDetail () {
|
||||
return this.options.detail
|
||||
}
|
||||
|
||||
isEqual (other) {
|
||||
return (this.getMessage() === other.getMessage()) &&
|
||||
(this.getType() === other.getType()) &&
|
||||
(this.getDetail() === other.getDetail())
|
||||
}
|
||||
|
||||
// Extended: Dismisses the notification, removing it from the UI. Calling this
|
||||
// programmatically will call all callbacks added via `onDidDismiss`.
|
||||
dismiss () {
|
||||
if (!this.isDismissable() || this.isDismissed()) return
|
||||
this.dismissed = true
|
||||
this.emitter.emit('did-dismiss', this)
|
||||
}
|
||||
|
||||
isDismissed () {
|
||||
return this.dismissed
|
||||
}
|
||||
|
||||
isDismissable () {
|
||||
return !!this.options.dismissable
|
||||
}
|
||||
|
||||
wasDisplayed () {
|
||||
return this.displayed
|
||||
}
|
||||
|
||||
setDisplayed (displayed) {
|
||||
this.displayed = displayed
|
||||
this.emitter.emit('did-display', this)
|
||||
}
|
||||
|
||||
getIcon () {
|
||||
if (this.options.icon != null) return this.options.icon
|
||||
switch (this.type) {
|
||||
case 'fatal': return 'bug'
|
||||
case 'error': return 'flame'
|
||||
case 'warning': return 'alert'
|
||||
case 'info': return 'info'
|
||||
case 'success': return 'check'
|
||||
}
|
||||
}
|
||||
}
|
@ -31,7 +31,8 @@ module.exports = class PackageManager {
|
||||
constructor (params) {
|
||||
({
|
||||
config: this.config, styleManager: this.styleManager, notificationManager: this.notificationManager, keymapManager: this.keymapManager,
|
||||
commandRegistry: this.commandRegistry, grammarRegistry: this.grammarRegistry, deserializerManager: this.deserializerManager, viewRegistry: this.viewRegistry
|
||||
commandRegistry: this.commandRegistry, grammarRegistry: this.grammarRegistry, deserializerManager: this.deserializerManager, viewRegistry: this.viewRegistry,
|
||||
uriHandlerRegistry: this.uriHandlerRegistry
|
||||
} = params)
|
||||
|
||||
this.emitter = new Emitter()
|
||||
@ -77,9 +78,9 @@ module.exports = class PackageManager {
|
||||
this.themeManager = themeManager
|
||||
}
|
||||
|
||||
reset () {
|
||||
async reset () {
|
||||
this.serviceHub.clear()
|
||||
this.deactivatePackages()
|
||||
await this.deactivatePackages()
|
||||
this.loadedPackages = {}
|
||||
this.preloadedPackages = {}
|
||||
this.packageStates = {}
|
||||
@ -647,6 +648,10 @@ module.exports = class PackageManager {
|
||||
})
|
||||
}
|
||||
|
||||
registerURIHandlerForPackage (packageName, handler) {
|
||||
return this.uriHandlerRegistry.registerHostHandler(packageName, handler)
|
||||
}
|
||||
|
||||
// another type of package manager can handle other package types.
|
||||
// See ThemeManager
|
||||
registerPackageActivator (activator, types) {
|
||||
@ -744,21 +749,30 @@ module.exports = class PackageManager {
|
||||
}
|
||||
|
||||
// Deactivate all packages
|
||||
deactivatePackages () {
|
||||
this.config.transact(() => {
|
||||
this.getLoadedPackages().forEach(pack => this.deactivatePackage(pack.name, true))
|
||||
})
|
||||
async deactivatePackages () {
|
||||
await this.config.transactAsync(() =>
|
||||
Promise.all(this.getLoadedPackages().map(pack => this.deactivatePackage(pack.name, true)))
|
||||
)
|
||||
this.unobserveDisabledPackages()
|
||||
this.unobservePackagesWithKeymapsDisabled()
|
||||
}
|
||||
|
||||
// Deactivate the package with the given name
|
||||
deactivatePackage (name, suppressSerialization) {
|
||||
async deactivatePackage (name, suppressSerialization) {
|
||||
const pack = this.getLoadedPackage(name)
|
||||
if (pack == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!suppressSerialization && this.isPackageActive(pack.name)) {
|
||||
this.serializePackage(pack)
|
||||
}
|
||||
pack.deactivate()
|
||||
|
||||
const deactivationResult = pack.deactivate()
|
||||
if (deactivationResult && typeof deactivationResult.then === 'function') {
|
||||
await deactivationResult
|
||||
}
|
||||
|
||||
delete this.activePackages[pack.name]
|
||||
delete this.activatingPackages[pack.name]
|
||||
this.emitter.emit('did-deactivate-package', pack)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user