Merge remote-tracking branch 'upstream/master' into tests

This commit is contained in:
Steven Hobson-Campbell 2017-10-25 14:14:12 -07:00
commit bbb331e1f7
129 changed files with 59480 additions and 9912 deletions

15
.github/stale.yml vendored
View File

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

View File

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

View File

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

View File

@ -65,7 +65,7 @@ export default async function ({test, benchmarkPaths}) {
console.log(textualOutput)
}
global.atom.reset()
await global.atom.reset()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
'name': 'Test Ruby'
'scopeName': 'test.rb'
'firstLineMatch': '^\\#!.*(?:\\s|\\/)(?:testruby)(?:$|\\s)'
'fileTypes': [
'rb'
]

View File

@ -0,0 +1,5 @@
module.exports = {
activate: () => null,
deactivate: () => null,
handleURI: () => null,
}

View File

@ -0,0 +1,6 @@
{
"name": "package-with-uri-handler",
"uriHandler": {
"method": "handleURI"
}
}

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View 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, [])
})
})
})

View File

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

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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] = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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 = []
}
}

View File

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

View File

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