Merge branch 'master' into wl-async-save-dialog

This commit is contained in:
Wliu
2018-01-07 20:55:16 -05:00
60 changed files with 5152 additions and 2842 deletions

View File

@@ -1,3 +1,8 @@
language: python
python:
- "2.7.13"
git:
depth: 10

View File

@@ -40,7 +40,7 @@ Project maintainers who do not follow or enforce the Code of Conduct in good fai
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/4/

View File

@@ -65,7 +65,7 @@ Atom is intentionally very modular. Nearly every non-editor UI element you inter
![atom-packages](https://cloud.githubusercontent.com/assets/69169/10472281/84fc9792-71d3-11e5-9fd1-19da717df079.png)
To get a sense for the packages that are bundled with Atom, you can go to Settings > Packages within Atom and take a look at the Core Packages section.
To get a sense for the packages that are bundled with Atom, you can go to `Settings` > `Packages` within Atom and take a look at the Core Packages section.
Here's a list of the big ones:
@@ -80,8 +80,8 @@ Here's a list of the big ones:
* [autocomplete-plus](https://github.com/atom/autocomplete-plus) - autocompletions shown while typing. Some languages have additional packages for autocompletion functionality, such as [autocomplete-html](https://github.com/atom/autocomplete-html).
* [git-diff](https://github.com/atom/git-diff) - Git change indicators shown in the editor's gutter.
* [language-javascript](https://github.com/atom/language-javascript) - all bundled languages are packages too, and each one has a separate package `language-[name]`. Use these for feedback on syntax highlighting issues that only appear for a specific language.
* [one-dark-ui](https://github.com/atom/one-dark-ui) - the default UI styling for anything but the text editor. UI theme packages (i.e. packages with a `-ui` suffix) provide only styling and it's possible that a bundled package is responsible for a UI issue. There are other other bundled UI themes, such as [one-light-ui](https://github.com/atom/one-light-ui).
* [one-dark-syntax](https://github.com/atom/one-dark-syntax) - the default syntax highlighting styles applied for all languages. There are other other bundled syntax themes, such as [solarized-dark-syntax](https://github.com/atom/solarized-dark-syntax). You should use these packages for reporting issues that appear in many languages, but disappear if you change to another syntax theme.
* [one-dark-ui](https://github.com/atom/one-dark-ui) - the default UI styling for anything but the text editor. UI theme packages (i.e. packages with a `-ui` suffix) provide only styling and it's possible that a bundled package is responsible for a UI issue. There are other bundled UI themes, such as [one-light-ui](https://github.com/atom/one-light-ui).
* [one-dark-syntax](https://github.com/atom/one-dark-syntax) - the default syntax highlighting styles applied for all languages. There are other bundled syntax themes, such as [solarized-dark-syntax](https://github.com/atom/solarized-dark-syntax). You should use these packages for reporting issues that appear in many languages, but disappear if you change to another syntax theme.
* [apm](https://github.com/atom/apm) - the `apm` command line tool (Atom Package Manager). You should use this repository for any contributions related to the `apm` tool and to publishing packages.
* [atom.io](https://github.com/atom/atom.io) - the repository for feedback on the [Atom.io website](https://atom.io) and the [Atom.io package API](https://github.com/atom/atom/blob/master/docs/apm-rest-api.md) used by [apm](https://github.com/atom/apm).

View File

@@ -9,8 +9,8 @@ Do you want to ask a question? Are you looking for support? The Atom message boa
### Prerequisites
* [ ] Put an X between the brackets on this line if you have done all of the following:
* Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode
* Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/
* Reproduced the problem in Safe Mode: https://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode
* Followed all applicable steps in the debugging guide: https://flight-manual.atom.io/hacking-atom/sections/debugging/
* Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq
* Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom
* Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages

View File

@@ -1,4 +1,4 @@
Copyright (c) 2011-2017 GitHub Inc.
Copyright (c) 2011-2018 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

View File

@@ -27,6 +27,20 @@ We must be able to understand the design of your change from this description. I
<!-- What are the possible side-effects or negative impacts of the code change? -->
### Verification Process
<!--
What process did you follow to verify that your change has the desired effects?
- How did you verify that all new functionality works as expected?
- How did you verify that all changed functionality works as expected?
- How did you verify that the change has not introduced any regressions?
Describe the actions you performed (e.g., buttons you clicked, text you typed, commands you ran, etc.), and describe the results you observed.
-->
### Applicable Issues
<!-- Enter any applicable Issues here -->

View File

@@ -42,27 +42,11 @@ The `.zip` version will not automatically update.
Using [Chocolatey](https://chocolatey.org)? Run `cinst Atom` to install the latest version of Atom.
### Debian based (Debian, Ubuntu, Linux Mint)
### Linux
Atom is only available for 64-bit Linux systems.
1. Download `atom-amd64.deb` from the [Atom releases page](https://github.com/atom/atom/releases/latest).
2. Run `sudo dpkg --install atom-amd64.deb` on the downloaded package.
3. Launch Atom using the installed `atom` command.
The Linux version does not currently automatically update so you will need to
repeat these steps to upgrade to future releases.
### RPM based (Red Hat, openSUSE, Fedora, CentOS)
Atom is only available for 64-bit Linux systems.
1. Download `atom.x86_64.rpm` from the [Atom releases page](https://github.com/atom/atom/releases/latest).
2. Run `sudo rpm -i atom.x86_64.rpm` on the downloaded package.
3. Launch Atom using the installed `atom` command.
The Linux version does not currently automatically update so you will need to
repeat these steps to upgrade to future releases.
Configure your distribution's package manager to install and update Atom by following the [Linux installation instructions](https://flight-manual.atom.io/getting-started/sections/installing-atom/#platform-linux) in the Flight Manual. You will also find instructions on how to install Atom's official Linux packages without using a package repository, though you will not get automatic updates after installing Atom this way.
### Archive extraction

View File

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

View File

@@ -2,20 +2,20 @@
![Atom](https://cloud.githubusercontent.com/assets/72919/2874231/3af1db48-d3dd-11e3-98dc-6066f8bc766f.png)
Most of the Atom user and developer documentation is contained in the [Atom Flight Manual](https://github.com/atom/flight-manual.atom.io) repository.
In this directory you can only find very specific build and API level documentation. Some of this may eventually move to the Flight Manual as well.
Most of the Atom user and developer documentation is contained in the [Atom Flight Manual](https://github.com/atom/flight-manual.atom.io).
## Build documentation
Instructions for building Atom on various platforms from source.
* [macOS](./build-instructions/macOS.md)
* [Windows](./build-instructions/windows.md)
* [Linux](./build-instructions/linux.md)
* [FreeBSD](./build-instructions/freebsd.md)
* Moved to [the Flight Manual](https://flight-manual.atom.io/hacking-atom/sections/hacking-on-atom-core/)
* Linux
* macOS
* Windows
## Other documentation here
## Other documentation
* [apm REST API](./apm-rest-api.md)
* [Tips for contributing to packages](./contributing-to-packages.md)
[Native Profiling on macOS](./native-profiling.md)
The other documentation that was listed here previously has been moved to [the Flight Manual](https://flight-manual.atom.io).

View File

@@ -1,285 +1,3 @@
# Atom.io package and update API
This guide describes the web API used by [apm](https://github.com/atom/apm) and
Atom. The vast majority of use cases are met by the `apm` command-line tool,
which does other useful things like incrementing your version in `package.json`
and making sure you have pushed your git tag. In fact, Atom itself shells out to
`apm` rather than hitting the API directly. If you're curious about how Atom
uses `apm`, see the [PackageManager class](https://github.com/atom/settings-view/blob/master/lib/package-manager.coffee)
in the `settings-view` package.
*This API should be considered pre-release and is subject to change (though significant breaking changes are unlikely).*
### Authorization
For calls to the API that require authentication, provide a valid token from your
[Atom.io account page](https://atom.io/account) in the `Authorization` header.
### Media type
All requests that take parameters require `application/json`.
# API Resources
## Packages
### Listing packages
#### GET /api/packages
Parameters:
- **page** (optional)
- **sort** (optional) - One of `downloads`, `created_at`, `updated_at`, `stars`. Defaults to `downloads`
- **direction** (optional) - `asc` or `desc`. Defaults to `desc`. `stars` can only be ordered `desc`
Returns a list of all packages in the following format:
```json
[
{
"releases": {
"latest": "0.6.0"
},
"name": "thedaniel-test-package",
"repository": {
"type": "git",
"url": "https://github.com/thedaniel/test-package"
}
},
...
]
```
Results are paginated 30 at a time, and links to the next and last pages are
provided in the `Link` header:
```
Link: <https://www.atom.io/api/packages?page=1>; rel="self",
<https://www.atom.io/api/packages?page=41>; rel="last",
<https://www.atom.io/api/packages?page=2>; rel="next"
```
By default, results are sorted by download count, descending.
### Searching packages
#### GET /api/packages/search
Parameters:
- **q** (required) - Search query
- **page** (optional)
- **sort** (optional) - One of `downloads`, `created_at`, `updated_at`, `stars`. Defaults to the relevance of the search query.
- **direction** (optional) - `asc` or `desc`. Defaults to `desc`.
Returns results in the same format as [listing packages](#listing-packages).
### Showing package details
#### GET /api/packages/:package_name
Returns package details and versions for a single package
Parameters:
- **engine** (optional) - Only show packages with versions compatible with this
Atom version. Must be valid [SemVer](http://semver.org).
Returns:
```json
{
"releases": {
"latest": "0.6.0"
},
"name": "thedaniel-test-package",
"repository": {
"type": "git",
"url": "https://github.com/thedaniel/test-package"
},
"versions": [
(see single version output below)
...,
]
}
```
### Creating a package
#### POST /api/packages
Create a new package; requires authentication.
The name and version will be fetched from the `package.json`
file in the specified repository. The authenticating user *must* have access
to the indicated repository.
Parameters:
- **repository** - String. The repository containing the plugin, in the form "owner/repo"
Returns:
- **201** - Successfully created, returns created package.
- **400** - Repository is inaccessible, nonexistent, not an atom package. Possible
error messages include:
- That repo does not exist, isn't an atom package, or atombot does not have access
- The package.json at owner/repo isn't valid
- **409** - A package by that name already exists
### Deleting a package
#### DELETE /api/packages/:package_name
Delete a package; requires authentication.
Returns:
- **204** - Success
- **400** - Repository is inaccessible
- **401** - Unauthorized
### Renaming a package
Packages are renamed by publishing a new version with the name changed in `package.json`
See [Creating a new package version](#creating-a-new-package-version) for details.
Requests made to the previous name will forward to the new name.
### Package Versions
#### GET /api/packages/:package_name/versions/:version_name
Returns `package.json` with `dist` key added for e.g. tarball download:
```json
{
"bugs": {
"url": "https://github.com/thedaniel/test-package/issues"
},
"dependencies": {
"async": "~0.2.6",
"pegjs": "~0.7.0",
"season": "~0.13.0"
},
"description": "Expand snippets matching the current prefix with `tab`.",
"dist": {
"tarball": "https://codeload.github.com/..."
},
"engines": {
"atom": "*"
},
"main": "./lib/snippets",
"name": "thedaniel-test-package",
"publishConfig": {
"registry": "https://...",
},
"repository": {
"type": "git",
"url": "https://github.com/thedaniel/test-package.git"
},
"version": "0.6.0"
}
```
### Creating a new package version
#### POST /api/packages/:package_name/versions
Creates a new package version from a git tag; requires authentication. If `rename`
is not `true`, the `name` field in `package.json` *must* match the current package
name.
#### Parameters
- **tag** - A git tag for the version you'd like to create. It's important to note
that the version name will not be taken from the tag, but from the `version`
key in the `package.json` file at that ref. The authenticating user *must* have
access to the package repository.
- **rename** - Boolean indicating whether this version contains a new name for the package.
#### Returns
- **201** - Successfully created. Returns created version.
- **400** - Git tag not found / Repository inaccessible / package.json invalid
- **409** - Version exists
### Deleting a version
#### DELETE /api/packages/:package_name/versions/:version_name
Deletes a package version; requires authentication.
Note that a version cannot be republished with a different tag if it is deleted.
If you need to delete the latest version of a package for e.g. security reasons,
you'll need to increment the version when republishing.
Returns 204 No Content
## Stars
### Listing user stars
#### GET /api/users/:login/stars
List a user's starred packages.
Return value is similar to **GET /api/packages**
#### GET /api/stars
List the authenticated user's starred packages; requires authentication.
Return value is similar to **GET /api/packages**
### Starring a package
#### POST /api/packages/:name/star
Star a package; requires authentication.
Returns a package.
### Unstarring a package
#### DELETE /api/packages/:name/star
Unstar a package; requires authentication.
Returns 204 No Content.
### Listing a package's stargazers
#### GET /api/packages/:name/stargazers
List the users that have starred a package.
Returns a list of user objects:
```json
[
{"login":"aperson"},
{"login":"anotherperson"},
]
```
## Atom updates
### Listing Atom updates
#### GET /api/updates
Atom update feed, following the format expected by [Squirrel](https://github.com/Squirrel/).
Returns:
```json
{
"name": "0.96.0",
"notes": "[HTML release notes]",
"pub_date": "2014-05-19T15:52:06.000Z",
"url": "https://www.atom.io/api/updates/download"
}
```
The information that was here has been moved to [a permanent home inside Atom's Flight Manual.](https://flight-manual.atom.io/atom-server-side-apis/)

View File

@@ -133,6 +133,8 @@
'cmd-ctrl-left': 'editor:move-selection-left'
'cmd-ctrl-right': 'editor:move-selection-right'
'cmd-shift-V': 'editor:paste-without-reformatting'
'alt-up': 'editor:select-larger-syntax-node'
'alt-down': 'editor:select-smaller-syntax-node'
# Emacs
'alt-f': 'editor:move-to-end-of-word'

View File

@@ -1,7 +1,7 @@
{
"name": "atom",
"productName": "Atom",
"version": "1.24.0-dev",
"version": "1.25.0-dev",
"description": "A hackable text editor for the 21st Century.",
"main": "./src/main-process/main.js",
"repository": {
@@ -12,13 +12,13 @@
"url": "https://github.com/atom/atom/issues"
},
"license": "MIT",
"electronVersion": "1.6.15",
"electronVersion": "1.7.10",
"dependencies": {
"@atom/nsfw": "^1.0.18",
"@atom/source-map-support": "^0.3.4",
"async": "0.2.6",
"atom-keymap": "8.2.8",
"atom-select-list": "^0.1.0",
"atom-select-list": "^0.7.0",
"atom-ui": "0.4.1",
"babel-core": "5.8.38",
"cached-run-in-this-context": "0.4.1",
@@ -38,7 +38,7 @@
"fs-plus": "^3.0.1",
"fstream": "0.1.24",
"fuzzaldrin": "^2.1",
"git-utils": "5.1.0",
"git-utils": "5.2.0",
"glob": "^7.1.1",
"grim": "1.5.0",
"jasmine-json": "~0.0",
@@ -70,103 +70,104 @@
"service-hub": "^0.7.4",
"sinon": "1.17.4",
"temp": "^0.8.3",
"text-buffer": "13.9.2",
"text-buffer": "13.10.1",
"tree-sitter": "^0.8.4",
"typescript-simple": "1.0.0",
"underscore-plus": "^1.6.6",
"winreg": "^1.2.1",
"yargs": "^3.23.0"
},
"packageDependencies": {
"atom-dark-syntax": "0.28.0",
"atom-dark-ui": "0.53.0",
"atom-dark-syntax": "0.29.0",
"atom-dark-ui": "0.53.1",
"atom-light-syntax": "0.29.0",
"atom-light-ui": "0.46.0",
"atom-light-ui": "0.46.1",
"base16-tomorrow-dark-theme": "1.5.0",
"base16-tomorrow-light-theme": "1.5.0",
"one-dark-ui": "1.10.8",
"one-light-ui": "1.10.8",
"one-dark-syntax": "1.8.0",
"one-light-syntax": "1.8.0",
"solarized-dark-syntax": "1.1.2",
"solarized-light-syntax": "1.1.2",
"one-dark-ui": "1.10.10",
"one-light-ui": "1.10.10",
"one-dark-syntax": "1.8.2",
"one-light-syntax": "1.8.2",
"solarized-dark-syntax": "1.1.4",
"solarized-light-syntax": "1.1.4",
"about": "1.7.8",
"archive-view": "0.64.1",
"autocomplete-atom-api": "0.10.5",
"autocomplete-css": "0.17.4",
"autocomplete-html": "0.8.3",
"autocomplete-plus": "2.39.0",
"archive-view": "0.64.2",
"autocomplete-atom-api": "0.10.6",
"autocomplete-css": "0.17.5",
"autocomplete-html": "0.8.4",
"autocomplete-plus": "2.40.0",
"autocomplete-snippets": "1.11.2",
"autoflow": "0.29.0",
"autoflow": "0.29.3",
"autosave": "0.24.6",
"background-tips": "0.27.1",
"bookmarks": "0.45.0",
"bracket-matcher": "0.88.0",
"bookmarks": "0.45.1",
"bracket-matcher": "0.88.3",
"command-palette": "0.43.0",
"dalek": "0.2.1",
"deprecation-cop": "0.56.9",
"dev-live-reload": "0.48.1",
"encoding-selector": "0.23.7",
"encoding-selector": "0.23.8",
"exception-reporting": "0.42.0",
"find-and-replace": "0.215.0",
"fuzzy-finder": "1.7.3",
"github": "0.8.3",
"git-diff": "1.3.6",
"fuzzy-finder": "1.7.5",
"github": "0.9.1",
"git-diff": "1.3.7",
"go-to-line": "0.32.1",
"grammar-selector": "0.49.8",
"grammar-selector": "0.49.9",
"image-view": "0.62.4",
"incompatible-packages": "0.27.3",
"keybinding-resolver": "0.38.1",
"line-ending-selector": "0.7.4",
"line-ending-selector": "0.7.5",
"link": "0.31.4",
"markdown-preview": "0.159.18",
"markdown-preview": "0.159.19",
"metrics": "1.2.6",
"notifications": "0.70.2",
"open-on-github": "1.3.1",
"package-generator": "1.3.0",
"settings-view": "0.253.0",
"snippets": "1.1.9",
"spell-check": "0.72.3",
"settings-view": "0.253.2",
"snippets": "1.2.0",
"spell-check": "0.72.5",
"status-bar": "1.8.15",
"styleguide": "0.49.9",
"symbols-view": "0.118.1",
"styleguide": "0.49.10",
"symbols-view": "0.118.2",
"tabs": "0.109.1",
"timecop": "0.36.2",
"tree-view": "0.221.3",
"update-package-dependencies": "0.13.0",
"update-package-dependencies": "0.13.1",
"welcome": "0.36.6",
"whitespace": "0.37.5",
"wrap-guide": "0.40.3",
"language-c": "0.58.1",
"language-c": "0.59.0-3",
"language-clojure": "0.22.5",
"language-coffee-script": "0.49.3",
"language-csharp": "0.14.3",
"language-csharp": "0.14.4",
"language-css": "0.42.8",
"language-gfm": "0.90.2",
"language-gfm": "0.90.3",
"language-git": "0.19.1",
"language-go": "0.44.3",
"language-html": "0.48.3",
"language-go": "0.45.0-4",
"language-html": "0.48.5",
"language-hyperlink": "0.16.3",
"language-java": "0.27.6",
"language-javascript": "0.127.7",
"language-javascript": "0.128.0-4",
"language-json": "0.19.1",
"language-less": "0.34.1",
"language-make": "0.22.3",
"language-mustache": "0.14.4",
"language-objective-c": "0.15.1",
"language-perl": "0.38.1",
"language-php": "0.42.2",
"language-php": "0.43.0",
"language-property-list": "0.9.1",
"language-python": "0.45.5",
"language-python": "0.46.0-2",
"language-ruby": "0.71.4",
"language-ruby-on-rails": "0.25.2",
"language-sass": "0.61.3",
"language-shellscript": "0.25.4",
"language-ruby-on-rails": "0.25.3",
"language-sass": "0.61.4",
"language-shellscript": "0.26.0-3",
"language-source": "0.9.0",
"language-sql": "0.25.8",
"language-sql": "0.25.9",
"language-text": "0.7.3",
"language-todo": "0.29.3",
"language-toml": "0.18.1",
"language-typescript": "0.2.3",
"language-typescript": "0.3.0-3",
"language-xml": "0.35.2",
"language-yaml": "0.31.1"
},

View File

@@ -46,6 +46,7 @@ module.exports = function (packagedAppPath) {
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', 'lodash.isequal', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'node-fetch', 'lib', 'fetch-error.js') ||
relativePath === path.join('..', 'node_modules', 'superstring', 'index.js') ||
relativePath === path.join('..', 'node_modules', 'oniguruma', 'src', 'oniguruma.js') ||
@@ -57,7 +58,8 @@ module.exports = function (packagedAppPath) {
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', 'tmp', 'lib', 'tmp.js') ||
relativePath === path.join('..', 'node_modules', 'tree-sitter', 'index.js')
)
}
}).then((snapshotScript) => {
@@ -85,7 +87,7 @@ module.exports = function (packagedAppPath) {
console.log(`Generating startup blob at "${generatedStartupBlobPath}"`)
childProcess.execFileSync(
path.join(CONFIG.repositoryRootPath, 'script', 'node_modules', 'electron-mksnapshot', 'bin', 'mksnapshot'),
[snapshotScriptPath, '--startup_blob', generatedStartupBlobPath]
['--no-use_ic', snapshotScriptPath, '--startup_blob', generatedStartupBlobPath]
)
let startupBlobDestinationPath

View File

@@ -8,15 +8,15 @@
"colors": "1.1.2",
"csslint": "1.0.2",
"donna": "1.0.16",
"electron-chromedriver": "~1.6",
"electron-chromedriver": "~1.7",
"electron-link": "0.1.2",
"electron-mksnapshot": "~1.6",
"electron-mksnapshot": "~1.7",
"electron-packager": "7.3.0",
"electron-winstaller": "2.6.3",
"fs-admin": "^0.1.5",
"fs-extra": "0.30.0",
"glob": "7.0.3",
"joanna": "0.0.9",
"joanna": "0.0.10",
"klaw-sync": "^1.1.2",
"legal-eagle": "0.14.0",
"lodash.template": "4.4.0",

View File

@@ -592,7 +592,7 @@ describe('AtomEnvironment', () => {
const promise = new Promise((r) => { resolve = r })
envLoaded = () => {
resolve()
promise
return promise
}
atomEnvironment = new AtomEnvironment({
applicationDelegate: atom.applicationDelegate,

View File

@@ -106,6 +106,15 @@ describe "Config", ->
atom.config.set("foo.bar.baz", 1, scopeSelector: ".source.coffee", source: "some-package")
expect(atom.config.get("foo.bar.baz", scope: [".source.coffee"])).toBe 100
describe "when the first component of the scope descriptor matches a legacy scope alias", ->
it "falls back to properties defined for the legacy scope if no value is found for the original scope descriptor", ->
atom.config.addLegacyScopeAlias('javascript', '.source.js')
atom.config.set('foo', 100, scopeSelector: '.source.js')
atom.config.set('foo', 200, scopeSelector: 'javascript for_statement')
expect(atom.config.get('foo', scope: ['javascript', 'for_statement', 'identifier'])).toBe(200)
expect(atom.config.get('foo', scope: ['javascript', 'function', 'identifier'])).toBe(100)
describe ".getAll(keyPath, {scope, sources, excludeSources})", ->
it "reads all of the values for a given key-path", ->
expect(atom.config.set("foo", 41)).toBe true
@@ -130,6 +139,20 @@ describe "Config", ->
{scopeSelector: '*', value: 40}
]
describe "when the first component of the scope descriptor matches a legacy scope alias", ->
it "includes the values defined for the legacy scope", ->
atom.config.addLegacyScopeAlias('javascript', '.source.js')
expect(atom.config.set('foo', 41)).toBe true
expect(atom.config.set('foo', 42, scopeSelector: 'javascript')).toBe true
expect(atom.config.set('foo', 43, scopeSelector: '.source.js')).toBe true
expect(atom.config.getAll('foo', scope: ['javascript'])).toEqual([
{scopeSelector: 'javascript', value: 42},
{scopeSelector: '.js.source', value: 43},
{scopeSelector: '*', value: 41}
])
describe ".set(keyPath, value, {source, scopeSelector})", ->
it "allows a key path's value to be written", ->
expect(atom.config.set("foo.bar.baz", 42)).toBe true

View File

@@ -0,0 +1 @@
exports.isFakeTreeSitterParser = true

View File

@@ -0,0 +1,14 @@
name: 'Some Language'
id: 'some-language'
type: 'tree-sitter'
parser: './fake-parser'
fileTypes: [
'somelang'
]
scopes:
'class > identifier': 'entity.name.type.class'

View File

@@ -1,10 +1,13 @@
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
const dedent = require('dedent')
const path = require('path')
const fs = require('fs-plus')
const temp = require('temp').track()
const TextBuffer = require('text-buffer')
const GrammarRegistry = require('../src/grammar-registry')
const TreeSitterGrammar = require('../src/tree-sitter-grammar')
const FirstMate = require('first-mate')
describe('GrammarRegistry', () => {
let grammarRegistry
@@ -13,8 +16,8 @@ describe('GrammarRegistry', () => {
grammarRegistry = new GrammarRegistry({config: atom.config})
})
describe('.assignLanguageMode(buffer, languageName)', () => {
it('assigns to the buffer a language mode with the given language name', async () => {
describe('.assignLanguageMode(buffer, languageId)', () => {
it('assigns to the buffer a language mode with the given language id', async () => {
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-css/grammars/css.cson'))
@@ -34,7 +37,7 @@ describe('GrammarRegistry', () => {
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.css')
})
describe('when no languageName is passed', () => {
describe('when no languageId is passed', () => {
it('makes the buffer use the null grammar', () => {
grammarRegistry.loadGrammarSync(require.resolve('language-css/grammars/css.cson'))
@@ -48,6 +51,36 @@ describe('GrammarRegistry', () => {
})
})
describe('.grammarForId(languageId)', () => {
it('converts the language id to a text-mate language id when `core.useTreeSitterParsers` is false', () => {
atom.config.set('core.useTreeSitterParsers', false)
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson'))
const grammar = grammarRegistry.grammarForId('javascript')
expect(grammar instanceof FirstMate.Grammar).toBe(true)
expect(grammar.scopeName).toBe('source.js')
grammarRegistry.removeGrammar(grammar)
expect(grammarRegistry.grammarForId('javascript')).toBe(undefined)
})
it('converts the language id to a tree-sitter language id when `core.useTreeSitterParsers` is true', () => {
atom.config.set('core.useTreeSitterParsers', true)
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson'))
const grammar = grammarRegistry.grammarForId('source.js')
expect(grammar instanceof TreeSitterGrammar).toBe(true)
expect(grammar.id).toBe('javascript')
grammarRegistry.removeGrammar(grammar)
expect(grammarRegistry.grammarForId('source.js') instanceof FirstMate.Grammar).toBe(true)
})
})
describe('.autoAssignLanguageMode(buffer)', () => {
it('assigns to the buffer a language mode based on the best available grammar', () => {
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
@@ -78,7 +111,9 @@ describe('GrammarRegistry', () => {
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.c')
})
it('updates the buffer\'s grammar when a more appropriate grammar is added for its path', async () => {
it('updates the buffer\'s grammar when a more appropriate text-mate grammar is added for its path', async () => {
atom.config.set('core.useTreeSitterParsers', false)
const buffer = new TextBuffer()
expect(buffer.getLanguageMode().getLanguageId()).toBe(null)
@@ -87,6 +122,25 @@ describe('GrammarRegistry', () => {
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson'))
expect(buffer.getLanguageMode().getLanguageId()).toBe('source.js')
})
it('updates the buffer\'s grammar when a more appropriate tree-sitter grammar is added for its path', async () => {
atom.config.set('core.useTreeSitterParsers', true)
const buffer = new TextBuffer()
expect(buffer.getLanguageMode().getLanguageId()).toBe(null)
buffer.setPath('test.js')
grammarRegistry.maintainLanguageMode(buffer)
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson'))
expect(buffer.getLanguageMode().getLanguageId()).toBe('javascript')
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
expect(buffer.getLanguageMode().getLanguageId()).toBe('javascript')
})
it('can be overridden by calling .assignLanguageMode', () => {
@@ -226,6 +280,32 @@ describe('GrammarRegistry', () => {
expect(atom.grammars.selectGrammar('/hu.git/config').name).toBe('Null Grammar')
})
describe('when the grammar has a contentRegExp field', () => {
it('favors grammars whose contentRegExp matches a prefix of the file\'s content', () => {
atom.grammars.addGrammar({
id: 'javascript-1',
fileTypes: ['js']
})
atom.grammars.addGrammar({
id: 'flow-javascript',
contentRegExp: new RegExp('//.*@flow'),
fileTypes: ['js']
})
atom.grammars.addGrammar({
id: 'javascript-2',
fileTypes: ['js']
})
const selectedGrammar = atom.grammars.selectGrammar('test.js', dedent`
// Copyright EvilCorp
// @flow
module.exports = function () { return 1 + 1 }
`)
expect(selectedGrammar.id).toBe('flow-javascript')
})
})
it("uses the filePath's shebang line if the grammar cannot be determined by the extension or basename", async () => {
await atom.packages.activatePackage('language-javascript')
await atom.packages.activatePackage('language-ruby')
@@ -335,14 +415,38 @@ describe('GrammarRegistry', () => {
await atom.packages.activatePackage('language-javascript')
expect(atom.grammars.selectGrammar('foo.rb', '#!/usr/bin/env node').scopeName).toBe('source.ruby')
})
describe('tree-sitter vs text-mate', () => {
it('favors a text-mate grammar over a tree-sitter grammar when `core.useTreeSitterParsers` is false', () => {
atom.config.set('core.useTreeSitterParsers', false)
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson'))
const grammar = grammarRegistry.selectGrammar('test.js')
expect(grammar.scopeName).toBe('source.js')
expect(grammar instanceof FirstMate.Grammar).toBe(true)
})
it('favors a tree-sitter grammar over a text-mate grammar when `core.useTreeSitterParsers` is true', () => {
atom.config.set('core.useTreeSitterParsers', true)
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/javascript.cson'))
grammarRegistry.loadGrammarSync(require.resolve('language-javascript/grammars/tree-sitter-javascript.cson'))
const grammar = grammarRegistry.selectGrammar('test.js')
expect(grammar.id).toBe('javascript')
expect(grammar instanceof TreeSitterGrammar).toBe(true)
})
})
})
describe('.removeGrammar(grammar)', () => {
it("removes the grammar, so it won't be returned by selectGrammar", async () => {
await atom.packages.activatePackage('language-javascript')
const grammar = atom.grammars.selectGrammar('foo.js')
await atom.packages.activatePackage('language-css')
const grammar = atom.grammars.selectGrammar('foo.css')
atom.grammars.removeGrammar(grammar)
expect(atom.grammars.selectGrammar('foo.js').name).not.toBe(grammar.name)
expect(atom.grammars.selectGrammar('foo.css').name).not.toBe(grammar.name)
})
})

View File

@@ -1,19 +1,14 @@
/** @babel */
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers'
import {Emitter, Disposable, CompositeDisposable} from 'event-kit'
import {HistoryManager, HistoryProject} from '../src/history-manager'
import StateStore from '../src/state-store'
const {HistoryManager, HistoryProject} = require('../src/history-manager')
const StateStore = require('../src/state-store')
describe("HistoryManager", () => {
let historyManager, commandRegistry, project, stateStore
let commandDisposable, projectDisposable
beforeEach(async () => {
// Do not clobber recent project history
spyOn(atom.applicationDelegate, 'didChangeHistoryManager')
commandDisposable = jasmine.createSpyObj('Disposable', ['dispose'])
commandRegistry = jasmine.createSpyObj('CommandRegistry', ['add'])
commandRegistry.add.andReturn(commandDisposable)
@@ -185,11 +180,26 @@ describe("HistoryManager", () => {
})
})
describe("saveState" ,() => {
describe("saveState", () => {
let savedHistory
beforeEach(() => {
// historyManager.saveState is spied on globally to prevent specs from
// modifying the shared project history. Since these tests depend on
// saveState, we unspy it but in turn spy on the state store instead
// so that no data is actually stored to it.
jasmine.unspy(historyManager, 'saveState')
spyOn(historyManager.stateStore, 'save').andCallFake((name, history) => {
savedHistory = history
return Promise.resolve()
})
})
it("saves the state", async () => {
await historyManager.addProject(["/save/state"])
await historyManager.saveState()
const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry})
spyOn(historyManager2.stateStore, 'load').andCallFake(name => Promise.resolve(savedHistory))
await historyManager2.loadState()
expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state'])
})

View File

@@ -1,14 +1,13 @@
/** @babel */
import season from 'season'
import dedent from 'dedent'
import electron from 'electron'
import fs from 'fs-plus'
import path from 'path'
import sinon from 'sinon'
import AtomApplication from '../../src/main-process/atom-application'
import parseCommandLine from '../../src/main-process/parse-command-line'
import {timeoutPromise, conditionPromise, emitterEventPromise} from '../async-spec-helpers'
const temp = require('temp').track()
const season = require('season')
const dedent = require('dedent')
const electron = require('electron')
const fs = require('fs-plus')
const path = require('path')
const sinon = require('sinon')
const AtomApplication = require('../../src/main-process/atom-application')
const parseCommandLine = require('../../src/main-process/parse-command-line')
const {timeoutPromise, conditionPromise, emitterEventPromise} = require('../async-spec-helpers')
const ATOM_RESOURCE_PATH = path.resolve(__dirname, '..', '..')
@@ -17,7 +16,7 @@ describe('AtomApplication', function () {
let originalAppQuit, originalShowMessageBox, originalAtomHome, atomApplicationsToDestroy
beforeEach(function () {
beforeEach(() => {
originalAppQuit = electron.app.quit
originalShowMessageBox = electron.dialog.showMessageBox
mockElectronAppQuit()
@@ -34,7 +33,7 @@ describe('AtomApplication', function () {
atomApplicationsToDestroy = []
})
afterEach(async function () {
afterEach(async () => {
process.env.ATOM_HOME = originalAtomHome
for (let atomApplication of atomApplicationsToDestroy) {
await atomApplication.destroy()
@@ -44,8 +43,8 @@ describe('AtomApplication', function () {
electron.dialog.showMessageBox = originalShowMessageBox
})
describe('launch', function () {
it('can open to a specific line number of a file', async function () {
describe('launch', () => {
it('can open to a specific line number of a file', async () => {
const filePath = path.join(makeTempDir(), 'new-file')
fs.writeFileSync(filePath, '1\n2\n3\n4\n')
const atomApplication = buildAtomApplication()
@@ -53,8 +52,8 @@ describe('AtomApplication', function () {
const window = atomApplication.launch(parseCommandLine([filePath + ':3']))
await focusWindow(window)
const cursorRow = await evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (textEditor) {
const cursorRow = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(textEditor => {
sendBackToMainProcess(textEditor.getCursorBufferPosition().row)
})
})
@@ -62,7 +61,7 @@ describe('AtomApplication', function () {
assert.equal(cursorRow, 2)
})
it('can open to a specific line and column of a file', async function () {
it('can open to a specific line and column of a file', async () => {
const filePath = path.join(makeTempDir(), 'new-file')
fs.writeFileSync(filePath, '1\n2\n3\n4\n')
const atomApplication = buildAtomApplication()
@@ -70,8 +69,8 @@ describe('AtomApplication', function () {
const window = atomApplication.launch(parseCommandLine([filePath + ':2:2']))
await focusWindow(window)
const cursorPosition = await evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (textEditor) {
const cursorPosition = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(textEditor => {
sendBackToMainProcess(textEditor.getCursorBufferPosition())
})
})
@@ -79,7 +78,7 @@ describe('AtomApplication', function () {
assert.deepEqual(cursorPosition, {row: 1, column: 1})
})
it('removes all trailing whitespace and colons from the specified path', async function () {
it('removes all trailing whitespace and colons from the specified path', async () => {
let filePath = path.join(makeTempDir(), 'new-file')
fs.writeFileSync(filePath, '1\n2\n3\n4\n')
const atomApplication = buildAtomApplication()
@@ -87,8 +86,8 @@ describe('AtomApplication', function () {
const window = atomApplication.launch(parseCommandLine([filePath + ':: ']))
await focusWindow(window)
const openedPath = await evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (textEditor) {
const openedPath = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(textEditor => {
sendBackToMainProcess(textEditor.getPath())
})
})
@@ -97,7 +96,7 @@ describe('AtomApplication', function () {
})
if (process.platform === 'darwin' || process.platform === 'win32') {
it('positions new windows at an offset distance from the previous window', async function () {
it('positions new windows at an offset distance from the previous window', async () => {
const atomApplication = buildAtomApplication()
const window1 = atomApplication.launch(parseCommandLine([makeTempDir()]))
@@ -115,7 +114,7 @@ describe('AtomApplication', function () {
})
}
it('reuses existing windows when opening paths, but not directories', async function () {
it('reuses existing windows when opening paths, but not directories', async () => {
const dirAPath = makeTempDir("a")
const dirBPath = makeTempDir("b")
const dirCPath = makeTempDir("c")
@@ -127,8 +126,8 @@ describe('AtomApplication', function () {
await emitterEventPromise(window1, 'window:locations-opened')
await focusWindow(window1)
let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (textEditor) {
let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(textEditor => {
sendBackToMainProcess(textEditor.getPath())
})
})
@@ -139,8 +138,8 @@ describe('AtomApplication', function () {
const reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath]))
assert.equal(reusedWindow, window1)
assert.deepEqual(atomApplication.getAllWindows(), [window1])
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) {
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
const subscription = atom.workspace.onDidChangeActivePaneItem(textEditor => {
sendBackToMainProcess(textEditor.getPath())
subscription.dispose()
})
@@ -156,7 +155,7 @@ describe('AtomApplication', function () {
assert.deepEqual(await getTreeViewRootDirectories(window2), [dirCPath])
})
it('adds folders to existing windows when the --add option is used', async function () {
it('adds folders to existing windows when the --add option is used', async () => {
const dirAPath = makeTempDir("a")
const dirBPath = makeTempDir("b")
const dirCPath = makeTempDir("c")
@@ -167,8 +166,8 @@ describe('AtomApplication', function () {
const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'new-file')]))
await focusWindow(window1)
let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (textEditor) {
let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(textEditor => {
sendBackToMainProcess(textEditor.getPath())
})
})
@@ -179,8 +178,8 @@ describe('AtomApplication', function () {
let reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath, '--add']))
assert.equal(reusedWindow, window1)
assert.deepEqual(atomApplication.getAllWindows(), [window1])
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const subscription = atom.workspace.onDidChangeActivePaneItem(function (textEditor) {
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
const subscription = atom.workspace.onDidChangeActivePaneItem(textEditor => {
sendBackToMainProcess(textEditor.getPath())
subscription.dispose()
})
@@ -198,14 +197,14 @@ describe('AtomApplication', function () {
assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirCPath, dirBPath])
})
it('persists window state based on the project directories', async function () {
it('persists window state based on the project directories', async () => {
const tempDirPath = makeTempDir()
const atomApplication = buildAtomApplication()
const nonExistentFilePath = path.join(tempDirPath, 'new-file')
const window1 = atomApplication.launch(parseCommandLine([nonExistentFilePath]))
await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (textEditor) {
await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(textEditor => {
textEditor.insertText('Hello World!')
sendBackToMainProcess(null)
})
@@ -217,7 +216,7 @@ describe('AtomApplication', function () {
// Restore unsaved state when opening the directory itself
const window2 = atomApplication.launch(parseCommandLine([tempDirPath]))
await window2.loadedPromise
const window2Text = await evalInWebContents(window2.browserWindow.webContents, function (sendBackToMainProcess) {
const window2Text = await evalInWebContents(window2.browserWindow.webContents, sendBackToMainProcess => {
const textEditor = atom.workspace.getActiveTextEditor()
textEditor.moveToBottom()
textEditor.insertText(' How are you?')
@@ -231,13 +230,13 @@ describe('AtomApplication', function () {
// Restore unsaved state when opening a path to a non-existent file in the directory
const window3 = atomApplication.launch(parseCommandLine([path.join(tempDirPath, 'another-non-existent-file')]))
await window3.loadedPromise
const window3Texts = await evalInWebContents(window3.browserWindow.webContents, function (sendBackToMainProcess, nonExistentFilePath) {
const window3Texts = await evalInWebContents(window3.browserWindow.webContents, (sendBackToMainProcess, nonExistentFilePath) => {
sendBackToMainProcess(atom.workspace.getTextEditors().map(editor => editor.getText()))
})
assert.include(window3Texts, 'Hello World! How are you?')
})
it('shows all directories in the tree view when multiple directory paths are passed to Atom', async function () {
it('shows all directories in the tree view when multiple directory paths are passed to Atom', async () => {
const dirAPath = makeTempDir("a")
const dirBPath = makeTempDir("b")
const dirBSubdirPath = path.join(dirBPath, 'c')
@@ -250,7 +249,7 @@ describe('AtomApplication', function () {
assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirBPath])
})
it('reuses windows with no project paths to open directories', async function () {
it('reuses windows with no project paths to open directories', async () => {
const tempDirPath = makeTempDir()
const atomApplication = buildAtomApplication()
const window1 = atomApplication.launch(parseCommandLine([]))
@@ -261,18 +260,18 @@ describe('AtomApplication', function () {
await conditionPromise(async () => (await getTreeViewRootDirectories(reusedWindow)).length > 0)
})
it('opens a new window with a single untitled buffer when launched with no path, even if windows already exist', async function () {
it('opens a new window with a single untitled buffer when launched with no path, even if windows already exist', async () => {
const atomApplication = buildAtomApplication()
const window1 = atomApplication.launch(parseCommandLine([]))
await focusWindow(window1)
const window1EditorTitle = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const window1EditorTitle = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle())
})
assert.equal(window1EditorTitle, 'untitled')
const window2 = atomApplication.openWithOptions(parseCommandLine([]))
await focusWindow(window2)
const window2EditorTitle = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const window2EditorTitle = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle())
})
assert.equal(window2EditorTitle, 'untitled')
@@ -280,7 +279,7 @@ describe('AtomApplication', function () {
assert.deepEqual(atomApplication.getAllWindows(), [window2, window1])
})
it('does not open an empty editor when opened with no path if the core.openEmptyEditorOnStart config setting is false', async function () {
it('does not open an empty editor when opened with no path if the core.openEmptyEditorOnStart config setting is false', async () => {
const configPath = path.join(process.env.ATOM_HOME, 'config.cson')
const config = season.readFileSync(configPath)
if (!config['*'].core) config['*'].core = {}
@@ -294,19 +293,19 @@ describe('AtomApplication', function () {
// wait a bit just to make sure we don't pass due to querying the render process before it loads
await timeoutPromise(1000)
const itemCount = await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
const itemCount = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
sendBackToMainProcess(atom.workspace.getActivePane().getItems().length)
})
assert.equal(itemCount, 0)
})
it('opens an empty text editor and loads its parent directory in the tree-view when launched with a new file path', async function () {
it('opens an empty text editor and loads its parent directory in the tree-view when launched with a new file path', async () => {
const atomApplication = buildAtomApplication()
const newFilePath = path.join(makeTempDir(), 'new-file')
const window = atomApplication.launch(parseCommandLine([newFilePath]))
await focusWindow(window)
const {editorTitle, editorText} = await evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) {
atom.workspace.observeTextEditors(function (editor) {
const {editorTitle, editorText} = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.observeTextEditors(editor => {
sendBackToMainProcess({editorTitle: editor.getTitle(), editorText: editor.getText()})
})
})
@@ -315,7 +314,7 @@ describe('AtomApplication', function () {
assert.deepEqual(await getTreeViewRootDirectories(window), [path.dirname(newFilePath)])
})
it('adds a remote directory to the project when launched with a remote directory', async function () {
it('adds a remote directory to the project when launched with a remote directory', async () => {
const packagePath = path.join(__dirname, '..', 'fixtures', 'packages', 'package-with-directory-provider')
const packagesDirPath = path.join(process.env.ATOM_HOME, 'packages')
fs.mkdirSync(packagesDirPath)
@@ -338,13 +337,13 @@ describe('AtomApplication', function () {
assert.deepEqual(directories, [{type: 'FakeRemoteDirectory', path: remotePath}])
function getProjectDirectories () {
return evalInWebContents(window.browserWindow.webContents, function (sendBackToMainProcess) {
return evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
sendBackToMainProcess(atom.project.getDirectories().map(d => ({ type: d.constructor.name, path: d.getPath() })))
})
}
})
it('reopens any previously opened windows when launched with no path', async function () {
it('reopens any previously opened windows when launched with no path', async () => {
if (process.platform === 'win32') return; // Test is too flakey on Windows
const tempDirPath1 = makeTempDir()
@@ -372,7 +371,7 @@ describe('AtomApplication', function () {
assert.deepEqual(await getTreeViewRootDirectories(app2Window2), [tempDirPath2])
})
it('does not reopen any previously opened windows when launched with no path and `core.restorePreviousWindowsOnStart` is no', async function () {
it('does not reopen any previously opened windows when launched with no path and `core.restorePreviousWindowsOnStart` is no', async () => {
const atomApplication1 = buildAtomApplication()
const app1Window1 = atomApplication1.launch(parseCommandLine([makeTempDir()]))
await focusWindow(app1Window1)
@@ -391,30 +390,136 @@ describe('AtomApplication', function () {
assert.deepEqual(app2Window.representedDirectoryPaths, [])
})
describe('when closing the last window', function () {
describe('when the `--wait` flag is passed', () => {
let killedPids, atomApplication, onDidKillProcess
beforeEach(() => {
killedPids = []
onDidKillProcess = null
atomApplication = buildAtomApplication({
killProcess (pid) {
killedPids.push(pid)
if (onDidKillProcess) onDidKillProcess()
}
})
})
it('kills the specified pid after a newly-opened window is closed', async () => {
const window1 = atomApplication.launch(parseCommandLine(['--wait', '--pid', '101']))
await focusWindow(window1)
const [window2] = atomApplication.launch(parseCommandLine(['--new-window', '--wait', '--pid', '102']))
await focusWindow(window2)
assert.deepEqual(killedPids, [])
let processKillPromise = new Promise(resolve => { onDidKillProcess = resolve })
window1.close()
await processKillPromise
assert.deepEqual(killedPids, [101])
processKillPromise = new Promise(resolve => { onDidKillProcess = resolve })
window2.close()
await processKillPromise
assert.deepEqual(killedPids, [101, 102])
})
it('kills the specified pid after a newly-opened file in an existing window is closed', async () => {
const window = atomApplication.launch(parseCommandLine(['--wait', '--pid', '101']))
await focusWindow(window)
const filePath1 = temp.openSync('test').path
const filePath2 = temp.openSync('test').path
fs.writeFileSync(filePath1, 'File 1')
fs.writeFileSync(filePath2, 'File 2')
const reusedWindow = atomApplication.launch(parseCommandLine(['--wait', '--pid', '102', filePath1, filePath2]))
assert.equal(reusedWindow, window)
const activeEditorPath = await evalInWebContents(window.browserWindow.webContents, send => {
const subscription = atom.workspace.onDidChangeActivePaneItem(editor => {
send(editor.getPath())
subscription.dispose()
})
})
assert([filePath1, filePath2].includes(activeEditorPath))
assert.deepEqual(killedPids, [])
await evalInWebContents(window.browserWindow.webContents, send => {
atom.workspace.getActivePaneItem().destroy()
send()
})
await timeoutPromise(100)
assert.deepEqual(killedPids, [])
let processKillPromise = new Promise(resolve => { onDidKillProcess = resolve })
await evalInWebContents(window.browserWindow.webContents, send => {
atom.workspace.getActivePaneItem().destroy()
send()
})
await processKillPromise
assert.deepEqual(killedPids, [102])
processKillPromise = new Promise(resolve => { onDidKillProcess = resolve })
window.close()
await processKillPromise
assert.deepEqual(killedPids, [102, 101])
})
it('kills the specified pid after a newly-opened directory in an existing window is closed', async () => {
const window = atomApplication.launch(parseCommandLine([]))
await focusWindow(window)
const dirPath1 = makeTempDir()
const reusedWindow = atomApplication.launch(parseCommandLine(['--wait', '--pid', '101', dirPath1]))
assert.equal(reusedWindow, window)
assert.deepEqual(await getTreeViewRootDirectories(window), [dirPath1])
assert.deepEqual(killedPids, [])
const dirPath2 = makeTempDir()
await evalInWebContents(window.browserWindow.webContents, (send, dirPath1, dirPath2) => {
atom.project.setPaths([dirPath1, dirPath2])
send()
}, dirPath1, dirPath2)
await timeoutPromise(100)
assert.deepEqual(killedPids, [])
let processKillPromise = new Promise(resolve => { onDidKillProcess = resolve })
await evalInWebContents(window.browserWindow.webContents, (send, dirPath2) => {
atom.project.setPaths([dirPath2])
send()
}, dirPath2)
await processKillPromise
assert.deepEqual(killedPids, [101])
})
})
describe('when closing the last window', () => {
if (process.platform === 'linux' || process.platform === 'win32') {
it('quits the application', async function () {
it('quits the application', async () => {
const atomApplication = buildAtomApplication()
const window = atomApplication.launch(parseCommandLine([path.join(makeTempDir("a"), 'file-a')]))
await focusWindow(window)
window.close()
await window.closedPromise
assert(electron.app.hasQuitted())
await atomApplication.lastBeforeQuitPromise
assert(electron.app.didQuit())
})
} else if (process.platform === 'darwin') {
it('leaves the application open', async function () {
it('leaves the application open', async () => {
const atomApplication = buildAtomApplication()
const window = atomApplication.launch(parseCommandLine([path.join(makeTempDir("a"), 'file-a')]))
await focusWindow(window)
window.close()
await window.closedPromise
assert(!electron.app.hasQuitted())
await timeoutPromise(1000)
assert(!electron.app.didQuit())
})
}
})
describe('when adding or removing project folders', function () {
it('stores the window state immediately', async function () {
describe('when adding or removing project folders', () => {
it('stores the window state immediately', async () => {
const dirA = makeTempDir()
const dirB = makeTempDir()
@@ -441,8 +546,8 @@ describe('AtomApplication', function () {
})
})
describe('when opening atom:// URLs', function () {
it('loads the urlMain file in a new window', async function () {
describe('when opening atom:// URLs', () => {
it('loads the urlMain file in a new window', async () => {
const packagePath = path.join(__dirname, '..', 'fixtures', 'packages', 'package-with-url-main')
const packagesDirPath = path.join(process.env.ATOM_HOME, 'packages')
fs.mkdirSync(packagesDirPath)
@@ -454,7 +559,7 @@ describe('AtomApplication', function () {
let windows = atomApplication.launch(launchOptions)
await windows[0].loadedPromise
let reached = await evalInWebContents(windows[0].browserWindow.webContents, function (sendBackToMainProcess) {
let reached = await evalInWebContents(windows[0].browserWindow.webContents, sendBackToMainProcess => {
sendBackToMainProcess(global.reachedUrlMain)
})
assert.equal(reached, true);
@@ -488,7 +593,7 @@ describe('AtomApplication', function () {
})
})
it('waits until all the windows have saved their state before quitting', async function () {
it('waits until all the windows have saved their state before quitting', async () => {
const dirAPath = makeTempDir("a")
const dirBPath = makeTempDir("b")
const atomApplication = buildAtomApplication()
@@ -497,9 +602,12 @@ describe('AtomApplication', function () {
const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')]))
await focusWindow(window2)
electron.app.quit()
assert(!electron.app.hasQuitted())
await new Promise(process.nextTick)
assert(!electron.app.didQuit())
await Promise.all([window1.lastPrepareToUnloadPromise, window2.lastPrepareToUnloadPromise])
assert(electron.app.hasQuitted())
await new Promise(process.nextTick)
assert(electron.app.didQuit())
})
it('prevents quitting if user cancels when prompted to save an item', async () => {
@@ -507,7 +615,7 @@ describe('AtomApplication', function () {
const window1 = atomApplication.launch(parseCommandLine([]))
const window2 = atomApplication.launch(parseCommandLine([]))
await Promise.all([window1.loadedPromise, window2.loadedPromise])
await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.getActiveTextEditor().insertText('unsaved text')
sendBackToMainProcess()
})
@@ -516,21 +624,21 @@ describe('AtomApplication', function () {
mockElectronShowMessageBox({choice: 1})
electron.app.quit()
await atomApplication.lastBeforeQuitPromise
assert(!electron.app.hasQuitted())
assert(!electron.app.didQuit())
assert.equal(electron.app.quit.callCount, 1) // Ensure choosing "Cancel" doesn't try to quit the electron app more than once (regression)
// Choosing "Don't save"
mockElectronShowMessageBox({choice: 2})
electron.app.quit()
await atomApplication.lastBeforeQuitPromise
assert(electron.app.hasQuitted())
assert(electron.app.didQuit())
})
function buildAtomApplication () {
const atomApplication = new AtomApplication({
function buildAtomApplication (params = {}) {
const atomApplication = new AtomApplication(Object.assign({
resourcePath: ATOM_RESOURCE_PATH,
atomHomeDirPath: process.env.ATOM_HOME
})
atomHomeDirPath: process.env.ATOM_HOME,
}, params))
atomApplicationsToDestroy.push(atomApplication)
return atomApplication
}
@@ -542,40 +650,34 @@ describe('AtomApplication', function () {
}
function mockElectronAppQuit () {
let quitted = false
electron.app.quit = function () {
if (electron.app.quit.callCount) {
electron.app.quit.callCount++
} else {
electron.app.quit.callCount = 1
}
let didQuit = false
let shouldQuit = true
electron.app.emit('before-quit', {preventDefault: () => { shouldQuit = false }})
if (shouldQuit) {
quitted = true
}
}
electron.app.hasQuitted = function () {
return quitted
electron.app.quit = function () {
this.quit.callCount++
let defaultPrevented = false
this.emit('before-quit', {preventDefault() { defaultPrevented = true }})
if (!defaultPrevented) didQuit = true
}
electron.app.quit.callCount = 0
electron.app.didQuit = () => didQuit
}
function mockElectronShowMessageBox ({choice}) {
electron.dialog.showMessageBox = function () {
electron.dialog.showMessageBox = () => {
return choice
}
}
function makeTempDir (name) {
const temp = require('temp').track()
return fs.realpathSync(temp.mkdirSync(name))
}
let channelIdCounter = 0
function evalInWebContents (webContents, source, ...args) {
const channelId = 'eval-result-' + channelIdCounter++
return new Promise(function (resolve) {
return new Promise(resolve => {
electron.ipcMain.on(channelId, receiveResult)
function receiveResult (event, result) {
@@ -587,13 +689,13 @@ describe('AtomApplication', function () {
function sendBackToMainProcess (result) {
require('electron').ipcRenderer.send('${channelId}', result)
}
(${source})(sendBackToMainProcess)
(${source})(sendBackToMainProcess, ${args.map(JSON.stringify).join(', ')})
`)
})
}
function getTreeViewRootDirectories (atomWindow) {
return evalInWebContents(atomWindow.browserWindow.webContents, function (sendBackToMainProcess) {
return evalInWebContents(atomWindow.browserWindow.webContents, sendBackToMainProcess => {
atom.workspace.getLeftDock().observeActivePaneItem((treeView) => {
if (treeView) {
sendBackToMainProcess(
@@ -607,8 +709,8 @@ describe('AtomApplication', function () {
}
function clearElectronSession () {
return new Promise(function (resolve) {
electron.session.defaultSession.clearStorageData(function () {
return new Promise(resolve => {
electron.session.defaultSession.clearStorageData(() => {
// Resolve promise on next tick, otherwise the process stalls. This
// might be a bug in Electron, but it's probably fixed on the newer
// versions.

View File

@@ -66,4 +66,27 @@ describe('NotificationManager', () => {
expect(notification.getType()).toBe('success')
})
})
describe('clearing notifications', () => {
it('clears the notifications when ::clear has been called', () => {
manager.addSuccess('success')
expect(manager.getNotifications().length).toBe(1)
manager.clear()
expect(manager.getNotifications().length).toBe(0)
})
describe('adding events', () => {
let clearSpy
beforeEach(() => {
clearSpy = jasmine.createSpy()
manager.onDidClearNotifications(clearSpy)
})
it('emits an event when the notifications have been cleared', () => {
manager.clear()
expect(clearSpy).toHaveBeenCalled()
})
})
})
})

View File

@@ -1030,6 +1030,13 @@ describe('PackageManager', () => {
expect(atom.grammars.selectGrammar('a.alot').name).toBe('Alot')
expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Alittle')
})
it('loads any tree-sitter grammars defined in the package', async () => {
await atom.packages.activatePackage('package-with-tree-sitter-grammar')
const grammar = atom.grammars.selectGrammar('test.somelang')
expect(grammar.name).toBe('Some Language')
expect(grammar.languageModule.isFakeTreeSitterParser).toBe(true)
})
})
describe('scoped-property loading', () => {

View File

@@ -63,7 +63,7 @@ else
beforeEach ->
# Do not clobber recent project history
spyOn(atom.history, 'saveState').andReturn(Promise.resolve())
spyOn(Object.getPrototypeOf(atom.history), 'saveState').andReturn(Promise.resolve())
atom.project.setPaths([specProjectPath])
@@ -111,7 +111,8 @@ beforeEach ->
new CompositeDisposable(
@emitter.on("did-tokenize", callback),
@onDidChangeGrammar =>
if @buffer.getLanguageMode().tokenizeInBackground.originalValue
languageMode = @buffer.getLanguageMode()
if languageMode.tokenizeInBackground?.originalValue
callback()
)

View File

@@ -0,0 +1,77 @@
const SyntaxScopeMap = require('../src/syntax-scope-map')
describe('SyntaxScopeMap', () => {
it('can match immediate child selectors', () => {
const map = new SyntaxScopeMap({
'a > b > c': 'x',
'b > c': 'y',
'c': 'z'
})
expect(map.get(['a', 'b', 'c'], [0, 0, 0])).toBe('x')
expect(map.get(['d', 'b', 'c'], [0, 0, 0])).toBe('y')
expect(map.get(['d', 'e', 'c'], [0, 0, 0])).toBe('z')
expect(map.get(['e', 'c'], [0, 0, 0])).toBe('z')
expect(map.get(['c'], [0, 0, 0])).toBe('z')
expect(map.get(['d'], [0, 0, 0])).toBe(undefined)
})
it('can match :nth-child pseudo-selectors on leaves', () => {
const map = new SyntaxScopeMap({
'a > b': 'w',
'a > b:nth-child(1)': 'x',
'b': 'y',
'b:nth-child(2)': 'z'
})
expect(map.get(['a', 'b'], [0, 0])).toBe('w')
expect(map.get(['a', 'b'], [0, 1])).toBe('x')
expect(map.get(['a', 'b'], [0, 2])).toBe('w')
expect(map.get(['b'], [0])).toBe('y')
expect(map.get(['b'], [1])).toBe('y')
expect(map.get(['b'], [2])).toBe('z')
})
it('can match :nth-child pseudo-selectors on interior nodes', () => {
const map = new SyntaxScopeMap({
'b:nth-child(1) > c': 'w',
'a > b > c': 'x',
'a > b:nth-child(2) > c': 'y'
})
expect(map.get(['b', 'c'], [0, 0])).toBe(undefined)
expect(map.get(['b', 'c'], [1, 0])).toBe('w')
expect(map.get(['a', 'b', 'c'], [1, 0, 0])).toBe('x')
expect(map.get(['a', 'b', 'c'], [1, 2, 0])).toBe('y')
})
it('allows anonymous tokens to be referred to by their string value', () => {
const map = new SyntaxScopeMap({
'"b"': 'w',
'a > "b"': 'x',
'a > "b":nth-child(1)': 'y'
})
expect(map.get(['b'], [0], true)).toBe(undefined)
expect(map.get(['b'], [0], false)).toBe('w')
expect(map.get(['a', 'b'], [0, 0], false)).toBe('x')
expect(map.get(['a', 'b'], [0, 1], false)).toBe('y')
})
it('supports the wildcard selector', () => {
const map = new SyntaxScopeMap({
'*': 'w',
'a > *': 'x',
'a > *:nth-child(1)': 'y',
'a > *:nth-child(1) > b': 'z'
})
expect(map.get(['b'], [0])).toBe('w')
expect(map.get(['c'], [0])).toBe('w')
expect(map.get(['a', 'b'], [0, 0])).toBe('x')
expect(map.get(['a', 'b'], [0, 1])).toBe('y')
expect(map.get(['a', 'c'], [0, 1])).toBe('y')
expect(map.get(['a', 'c', 'b'], [0, 1, 1])).toBe('z')
expect(map.get(['a', 'c', 'b'], [0, 2, 1])).toBe('w')
})
})

View File

@@ -2840,83 +2840,149 @@ describe('TextEditorComponent', () => {
describe('mouse input', () => {
describe('on the lines', () => {
it('positions the cursor on single-click or when middle/right-clicking', async () => {
for (const button of [0, 1, 2]) {
describe('when there is only one cursor and no selection', () => {
it('positions the cursor on single-click or when middle/right-clicking', async () => {
for (const button of [0, 1, 2]) {
const {component, element, editor} = buildComponent()
const {lineHeight} = component.measurements
editor.setCursorScreenPosition([Infinity, Infinity], {autoscroll: false})
component.didMouseDownOnContent({
detail: 1,
button,
clientX: clientLeftForCharacter(component, 0, 0) - 1,
clientY: clientTopForLine(component, 0) - 1
})
expect(editor.getCursorScreenPosition()).toEqual([0, 0])
const maxRow = editor.getLastScreenRow()
editor.setCursorScreenPosition([Infinity, Infinity], {autoscroll: false})
component.didMouseDownOnContent({
detail: 1,
button,
clientX: clientLeftForCharacter(component, maxRow, editor.lineLengthForScreenRow(maxRow)) + 1,
clientY: clientTopForLine(component, maxRow) + 1
})
expect(editor.getCursorScreenPosition()).toEqual([maxRow, editor.lineLengthForScreenRow(maxRow)])
component.didMouseDownOnContent({
detail: 1,
button,
clientX: clientLeftForCharacter(component, 0, editor.lineLengthForScreenRow(0)) + 1,
clientY: clientTopForLine(component, 0) + lineHeight / 2
})
expect(editor.getCursorScreenPosition()).toEqual([0, editor.lineLengthForScreenRow(0)])
component.didMouseDownOnContent({
detail: 1,
button,
clientX: (clientLeftForCharacter(component, 3, 0) + clientLeftForCharacter(component, 3, 1)) / 2,
clientY: clientTopForLine(component, 1) + lineHeight / 2
})
expect(editor.getCursorScreenPosition()).toEqual([1, 0])
component.didMouseDownOnContent({
detail: 1,
button,
clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 15)) / 2,
clientY: clientTopForLine(component, 3) + lineHeight / 2
})
expect(editor.getCursorScreenPosition()).toEqual([3, 14])
component.didMouseDownOnContent({
detail: 1,
button,
clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 15)) / 2 + 1,
clientY: clientTopForLine(component, 3) + lineHeight / 2
})
expect(editor.getCursorScreenPosition()).toEqual([3, 15])
editor.getBuffer().setTextInRange([[3, 14], [3, 15]], '🐣')
await component.getNextUpdatePromise()
component.didMouseDownOnContent({
detail: 1,
button,
clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 16)) / 2,
clientY: clientTopForLine(component, 3) + lineHeight / 2
})
expect(editor.getCursorScreenPosition()).toEqual([3, 14])
component.didMouseDownOnContent({
detail: 1,
button,
clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 16)) / 2 + 1,
clientY: clientTopForLine(component, 3) + lineHeight / 2
})
expect(editor.getCursorScreenPosition()).toEqual([3, 16])
expect(editor.testAutoscrollRequests).toEqual([])
}
})
})
describe('when there is more than one cursor', () => {
it('does not move the cursor when right-clicking', async () => {
const {component, element, editor} = buildComponent()
const {lineHeight} = component.measurements
editor.setCursorScreenPosition([Infinity, Infinity], {autoscroll: false})
editor.setCursorScreenPosition([5, 17], {autoscroll: false})
editor.addCursorAtScreenPosition([2, 4])
component.didMouseDownOnContent({
detail: 1,
button,
button: 2,
clientX: clientLeftForCharacter(component, 0, 0) - 1,
clientY: clientTopForLine(component, 0) - 1
})
expect(editor.getCursorScreenPosition()).toEqual([0, 0])
expect(editor.getCursorScreenPositions()).toEqual([Point.fromObject([5, 17]), Point.fromObject([2, 4])])
})
const maxRow = editor.getLastScreenRow()
editor.setCursorScreenPosition([Infinity, Infinity], {autoscroll: false})
it('does move the cursor when middle-clicking', async () => {
const {component, element, editor} = buildComponent()
const {lineHeight} = component.measurements
editor.setCursorScreenPosition([5, 17], {autoscroll: false})
editor.addCursorAtScreenPosition([2, 4])
component.didMouseDownOnContent({
detail: 1,
button,
clientX: clientLeftForCharacter(component, maxRow, editor.lineLengthForScreenRow(maxRow)) + 1,
clientY: clientTopForLine(component, maxRow) + 1
button: 1,
clientX: clientLeftForCharacter(component, 0, 0) - 1,
clientY: clientTopForLine(component, 0) - 1
})
expect(editor.getCursorScreenPosition()).toEqual([maxRow, editor.lineLengthForScreenRow(maxRow)])
expect(editor.getCursorScreenPositions()).toEqual([Point.fromObject([0, 0])])
})
})
describe('when there are non-empty selections', () => {
it('does not move the cursor when right-clicking', async () => {
const {component, element, editor} = buildComponent()
const {lineHeight} = component.measurements
editor.setCursorScreenPosition([5, 17], {autoscroll: false})
editor.selectRight(3)
component.didMouseDownOnContent({
detail: 1,
button,
clientX: clientLeftForCharacter(component, 0, editor.lineLengthForScreenRow(0)) + 1,
clientY: clientTopForLine(component, 0) + lineHeight / 2
button: 2,
clientX: clientLeftForCharacter(component, 0, 0) - 1,
clientY: clientTopForLine(component, 0) - 1
})
expect(editor.getCursorScreenPosition()).toEqual([0, editor.lineLengthForScreenRow(0)])
expect(editor.getSelectedScreenRange()).toEqual([[5, 17], [5, 20]])
})
it('does move the cursor when middle-clicking', async () => {
const {component, element, editor} = buildComponent()
const {lineHeight} = component.measurements
editor.setCursorScreenPosition([5, 17], {autoscroll: false})
editor.selectRight(3)
component.didMouseDownOnContent({
detail: 1,
button,
clientX: (clientLeftForCharacter(component, 3, 0) + clientLeftForCharacter(component, 3, 1)) / 2,
clientY: clientTopForLine(component, 1) + lineHeight / 2
button: 1,
clientX: clientLeftForCharacter(component, 0, 0) - 1,
clientY: clientTopForLine(component, 0) - 1
})
expect(editor.getCursorScreenPosition()).toEqual([1, 0])
component.didMouseDownOnContent({
detail: 1,
button,
clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 15)) / 2,
clientY: clientTopForLine(component, 3) + lineHeight / 2
})
expect(editor.getCursorScreenPosition()).toEqual([3, 14])
component.didMouseDownOnContent({
detail: 1,
button,
clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 15)) / 2 + 1,
clientY: clientTopForLine(component, 3) + lineHeight / 2
})
expect(editor.getCursorScreenPosition()).toEqual([3, 15])
editor.getBuffer().setTextInRange([[3, 14], [3, 15]], '🐣')
await component.getNextUpdatePromise()
component.didMouseDownOnContent({
detail: 1,
button,
clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 16)) / 2,
clientY: clientTopForLine(component, 3) + lineHeight / 2
})
expect(editor.getCursorScreenPosition()).toEqual([3, 14])
component.didMouseDownOnContent({
detail: 1,
button,
clientX: (clientLeftForCharacter(component, 3, 14) + clientLeftForCharacter(component, 3, 16)) / 2 + 1,
clientY: clientTopForLine(component, 3) + lineHeight / 2
})
expect(editor.getCursorScreenPosition()).toEqual([3, 16])
expect(editor.testAutoscrollRequests).toEqual([])
}
expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [0, 0]])
})
})
describe('when the input is for the primary mouse button', () => {
@@ -3612,421 +3678,198 @@ describe('TextEditorComponent', () => {
})
describe('keyboard input', () => {
describe('on Chrome 56', () => {
it('handles inserted accented characters via the press-and-hold menu on macOS correctly', async () => {
const {editor, component, element} = buildComponent({text: '', chromeVersion: 56})
editor.insertText('x')
editor.setCursorBufferPosition([0, 1])
it('handles inserted accented characters via the press-and-hold menu on macOS correctly', () => {
const {editor, component, element} = buildComponent({text: '', chromeVersion: 57})
editor.insertText('x')
editor.setCursorBufferPosition([0, 1])
// Simulate holding the A key to open the press-and-hold menu,
// then closing it via ESC.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'Escape'})
component.didKeyup({code: 'Escape'})
expect(editor.getText()).toBe('xa')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xaa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// then closing it via ESC.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'Escape'})
component.didKeyup({code: 'Escape'})
expect(editor.getText()).toBe('xa')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xaa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// then selecting an alternative by typing a number.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'Digit2'})
component.didKeyup({code: 'Digit2'})
component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}})
expect(editor.getText()).toBe('xá')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xáa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// then selecting an alternative by typing a number.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'Digit2'})
component.didKeyup({code: 'Digit2'})
component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}})
expect(editor.getText()).toBe('xá')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xáa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// then selecting an alternative by clicking on it.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}})
expect(editor.getText()).toBe('xá')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xáa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// then selecting an alternative by clicking on it.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}})
expect(editor.getText()).toBe('xá')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xáa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// cycling through the alternatives with the arrows, then selecting one of them with Enter.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'ArrowRight'})
component.didCompositionStart({data: ''})
component.didCompositionUpdate({data: 'à'})
component.getHiddenInput().value = 'à'
component.didKeyup({code: 'ArrowRight'})
await getNextTickPromise()
expect(editor.getText()).toBe('xà')
component.didKeydown({code: 'ArrowRight'})
component.didCompositionUpdate({data: 'á'})
component.getHiddenInput().value = 'á'
component.didKeyup({code: 'ArrowRight'})
await getNextTickPromise()
expect(editor.getText()).toBe('xá')
component.didKeydown({code: 'Enter'})
component.didCompositionUpdate({data: 'á'})
component.getHiddenInput().value = 'á'
component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}})
component.didCompositionEnd({data: 'á', target: component.getHiddenInput()})
component.didKeyup({code: 'Enter'})
await getNextTickPromise()
expect(editor.getText()).toBe('xá')
// Simulate holding the A key to open the press-and-hold menu,
// cycling through the alternatives with the arrows, then selecting one of them with Enter.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'ArrowRight'})
component.didCompositionStart({data: ''})
component.didCompositionUpdate({data: 'à'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xà')
component.didKeydown({code: 'ArrowRight'})
component.didCompositionUpdate({data: 'á'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xá')
component.didKeydown({code: 'Enter'})
component.didCompositionUpdate({data: 'á'})
component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}})
component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput})
component.didKeyup({code: 'Enter'})
expect(editor.getText()).toBe('xá')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xáa')
editor.undo()
expect(editor.getText()).toBe('x')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xáa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// cycling through the alternatives with the arrows, then closing it via ESC.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'ArrowRight'})
component.didCompositionStart({data: ''})
component.didCompositionUpdate({data: 'à'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xà')
component.didKeydown({code: 'ArrowRight'})
component.didCompositionUpdate({data: 'á'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xá')
component.didKeydown({code: 'Escape'})
component.didCompositionUpdate({data: 'a'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput})
component.didKeyup({code: 'Escape'})
expect(editor.getText()).toBe('xa')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xaa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// cycling through the alternatives with the arrows, then closing it via ESC.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'ArrowRight'})
component.didCompositionStart({data: ''})
component.didCompositionUpdate({data: 'à'})
component.getHiddenInput().value = 'à'
component.didKeyup({code: 'ArrowRight'})
await getNextTickPromise()
expect(editor.getText()).toBe('xà')
component.didKeydown({code: 'ArrowRight'})
component.didCompositionUpdate({data: 'á'})
component.getHiddenInput().value = 'á'
component.didKeyup({code: 'ArrowRight'})
await getNextTickPromise()
expect(editor.getText()).toBe('xá')
component.didKeydown({code: 'Escape'})
component.didCompositionUpdate({data: 'a'})
component.getHiddenInput().value = 'a'
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput})
component.didKeyup({code: 'Escape'})
await getNextTickPromise()
expect(editor.getText()).toBe('xa')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xaa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate pressing the O key and holding the A key to open the press-and-hold menu right before releasing the O key,
// cycling through the alternatives with the arrows, then closing it via ESC.
component.didKeydown({code: 'KeyO'})
component.didKeypress({code: 'KeyO'})
component.didTextInput({data: 'o', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyO'})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'ArrowRight'})
component.didCompositionStart({data: ''})
component.didCompositionUpdate({data: 'à'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xoà')
component.didKeydown({code: 'ArrowRight'})
component.didCompositionUpdate({data: 'á'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xoá')
component.didKeydown({code: 'Escape'})
component.didCompositionUpdate({data: 'a'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput})
component.didKeyup({code: 'Escape'})
expect(editor.getText()).toBe('xoa')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate pressing the O key and holding the A key to open the press-and-hold menu right before releasing the O key,
// cycling through the alternatives with the arrows, then closing it via ESC.
component.didKeydown({code: 'KeyO'})
component.didKeypress({code: 'KeyO'})
component.didTextInput({data: 'o', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyO'})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'ArrowRight'})
component.didCompositionStart({data: ''})
component.didCompositionUpdate({data: 'à'})
component.getHiddenInput().value = 'à'
component.didKeyup({code: 'ArrowRight'})
await getNextTickPromise()
expect(editor.getText()).toBe('xoà')
component.didKeydown({code: 'ArrowRight'})
component.didCompositionUpdate({data: 'á'})
component.getHiddenInput().value = 'á'
component.didKeyup({code: 'ArrowRight'})
await getNextTickPromise()
expect(editor.getText()).toBe('xoá')
component.didKeydown({code: 'Escape'})
component.didCompositionUpdate({data: 'a'})
component.getHiddenInput().value = 'a'
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput})
component.didKeyup({code: 'Escape'})
await getNextTickPromise()
expect(editor.getText()).toBe('xoa')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// cycling through the alternatives with the arrows, then closing it by changing focus.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'ArrowRight'})
component.didCompositionStart({data: ''})
component.didCompositionUpdate({data: 'à'})
component.getHiddenInput().value = 'à'
component.didKeyup({code: 'ArrowRight'})
await getNextTickPromise()
expect(editor.getText()).toBe('xà')
component.didKeydown({code: 'ArrowRight'})
component.didCompositionUpdate({data: 'á'})
component.getHiddenInput().value = 'á'
component.didKeyup({code: 'ArrowRight'})
await getNextTickPromise()
expect(editor.getText()).toBe('xá')
component.didCompositionUpdate({data: 'á'})
component.getHiddenInput().value = 'á'
component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}})
component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput})
await getNextTickPromise()
expect(editor.getText()).toBe('xá')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xáa')
editor.undo()
expect(editor.getText()).toBe('x')
})
})
describe('on other versions of Chrome', () => {
it('handles inserted accented characters via the press-and-hold menu on macOS correctly', () => {
const {editor, component, element} = buildComponent({text: '', chromeVersion: 57})
editor.insertText('x')
editor.setCursorBufferPosition([0, 1])
// Simulate holding the A key to open the press-and-hold menu,
// then closing it via ESC.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'Escape'})
component.didKeyup({code: 'Escape'})
expect(editor.getText()).toBe('xa')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xaa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// then selecting an alternative by typing a number.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'Digit2'})
component.didKeyup({code: 'Digit2'})
component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}})
expect(editor.getText()).toBe('xá')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xáa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// then selecting an alternative by clicking on it.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}})
expect(editor.getText()).toBe('xá')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xáa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// cycling through the alternatives with the arrows, then selecting one of them with Enter.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'ArrowRight'})
component.didCompositionStart({data: ''})
component.didCompositionUpdate({data: 'à'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xà')
component.didKeydown({code: 'ArrowRight'})
component.didCompositionUpdate({data: 'á'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xá')
component.didKeydown({code: 'Enter'})
component.didCompositionUpdate({data: 'á'})
component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}})
component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput})
component.didKeyup({code: 'Enter'})
expect(editor.getText()).toBe('xá')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xáa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// cycling through the alternatives with the arrows, then closing it via ESC.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'ArrowRight'})
component.didCompositionStart({data: ''})
component.didCompositionUpdate({data: 'à'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xà')
component.didKeydown({code: 'ArrowRight'})
component.didCompositionUpdate({data: 'á'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xá')
component.didKeydown({code: 'Escape'})
component.didCompositionUpdate({data: 'a'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput})
component.didKeyup({code: 'Escape'})
expect(editor.getText()).toBe('xa')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xaa')
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate pressing the O key and holding the A key to open the press-and-hold menu right before releasing the O key,
// cycling through the alternatives with the arrows, then closing it via ESC.
component.didKeydown({code: 'KeyO'})
component.didKeypress({code: 'KeyO'})
component.didTextInput({data: 'o', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyO'})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'ArrowRight'})
component.didCompositionStart({data: ''})
component.didCompositionUpdate({data: 'à'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xoà')
component.didKeydown({code: 'ArrowRight'})
component.didCompositionUpdate({data: 'á'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xoá')
component.didKeydown({code: 'Escape'})
component.didCompositionUpdate({data: 'a'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didCompositionEnd({data: 'a', target: component.refs.cursorsAndInput.refs.hiddenInput})
component.didKeyup({code: 'Escape'})
expect(editor.getText()).toBe('xoa')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
editor.undo()
expect(editor.getText()).toBe('x')
// Simulate holding the A key to open the press-and-hold menu,
// cycling through the alternatives with the arrows, then closing it by changing focus.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'ArrowRight'})
component.didCompositionStart({data: ''})
component.didCompositionUpdate({data: 'à'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xà')
component.didKeydown({code: 'ArrowRight'})
component.didCompositionUpdate({data: 'á'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xá')
component.didCompositionUpdate({data: 'á'})
component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}})
component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput})
expect(editor.getText()).toBe('xá')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xáa')
editor.undo()
expect(editor.getText()).toBe('x')
})
// Simulate holding the A key to open the press-and-hold menu,
// cycling through the alternatives with the arrows, then closing it by changing focus.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeydown({code: 'KeyA'})
component.didKeydown({code: 'KeyA'})
component.didKeyup({code: 'KeyA'})
component.didKeydown({code: 'ArrowRight'})
component.didCompositionStart({data: ''})
component.didCompositionUpdate({data: 'à'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xà')
component.didKeydown({code: 'ArrowRight'})
component.didCompositionUpdate({data: 'á'})
component.didKeyup({code: 'ArrowRight'})
expect(editor.getText()).toBe('xá')
component.didCompositionUpdate({data: 'á'})
component.didTextInput({data: 'á', stopPropagation: () => {}, preventDefault: () => {}})
component.didCompositionEnd({data: 'á', target: component.refs.cursorsAndInput.refs.hiddenInput})
expect(editor.getText()).toBe('xá')
// Ensure another "a" can be typed correctly.
component.didKeydown({code: 'KeyA'})
component.didKeypress({code: 'KeyA'})
component.didTextInput({data: 'a', stopPropagation: () => {}, preventDefault: () => {}})
component.didKeyup({code: 'KeyA'})
expect(editor.getText()).toBe('xáa')
editor.undo()
expect(editor.getText()).toBe('x')
})
})

View File

@@ -70,12 +70,41 @@ describe('TextEditorElement', () => {
expect(element.getModel().isLineNumberGutterVisible()).toBe(false)
})
it("honors the 'readonly' attribute", async function() {
jasmineContent.innerHTML = "<atom-text-editor readonly>"
const element = jasmineContent.firstChild
expect(element.getComponent().isInputEnabled()).toBe(false)
element.removeAttribute('readonly')
expect(element.getComponent().isInputEnabled()).toBe(true)
element.setAttribute('readonly', true)
expect(element.getComponent().isInputEnabled()).toBe(false)
})
it('honors the text content', () => {
jasmineContent.innerHTML = '<atom-text-editor>testing</atom-text-editor>'
const element = jasmineContent.firstChild
expect(element.getModel().getText()).toBe('testing')
})
describe('tabIndex', () => {
it('uses a default value of -1', () => {
jasmineContent.innerHTML = '<atom-text-editor />'
const element = jasmineContent.firstChild
expect(element.tabIndex).toBe(-1)
expect(element.querySelector('input').tabIndex).toBe(-1)
})
it('uses the custom value when given', () => {
jasmineContent.innerHTML = '<atom-text-editor tabIndex="42" />'
const element = jasmineContent.firstChild
expect(element.tabIndex).toBe(-1)
expect(element.querySelector('input').tabIndex).toBe(42)
})
})
describe('when the model is assigned', () =>
it("adds the 'mini' attribute if .isMini() returns true on the model", async () => {
const element = buildTextEditorElement()

View File

@@ -20,6 +20,17 @@ describe('TextEditor', () => {
await atom.packages.activatePackage('language-javascript')
})
it('generates unique ids for each editor', async () => {
// Deserialized editors are initialized with the serialized id. We can
// initialize an editor with what we expect to be the next id:
const deserialized = new TextEditor({id: editor.id+1})
expect(deserialized.id).toEqual(editor.id+1)
// The id generator should skip the id used up by the deserialized one:
const fresh = new TextEditor()
expect(fresh.id).toNotEqual(deserialized.id)
})
describe('when the editor is deserialized', () => {
it('restores selections and folds based on markers in the buffer', async () => {
editor.setSelectedBufferRange([[1, 2], [3, 4]])
@@ -3496,13 +3507,16 @@ describe('TextEditor', () => {
})
describe("when the undo option is set to 'skip'", () => {
beforeEach(() => editor.setSelectedBufferRange([[1, 2], [1, 2]]))
it('does not undo the skipped operation', () => {
let range = editor.insertText('x')
range = editor.insertText('y', {undo: 'skip'})
it('groups the change with the previous change for purposes of undo and redo', () => {
editor.setSelectedBufferRanges([
[[0, 0], [0, 0]],
[[1, 0], [1, 0]]
])
editor.insertText('x')
editor.insertText('y', {undo: 'skip'})
editor.undo()
expect(buffer.lineForRow(1)).toBe(' yvar sort = function(items) {')
expect(buffer.lineForRow(0)).toBe('var quicksort = function () {')
expect(buffer.lineForRow(1)).toBe(' var sort = function(items) {')
})
})
})

View File

@@ -424,12 +424,12 @@ h2 {
waitsForPromise(() => atom.themes.activateThemes())
})
it('uses the default dark UI and syntax themes and logs a warning', function () {
it('uses the default one-dark UI and syntax themes and logs a warning', function () {
const activeThemeNames = atom.themes.getActiveThemeNames()
expect(console.warn.callCount).toBe(2)
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-dark-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')
expect(activeThemeNames).toContain('one-dark-ui')
expect(activeThemeNames).toContain('one-dark-syntax')
})
})
@@ -459,8 +459,8 @@ h2 {
it('uses the default dark UI and syntax themes', function () {
const activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-dark-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')
expect(activeThemeNames).toContain('one-dark-ui')
expect(activeThemeNames).toContain('one-dark-syntax')
})
})
@@ -471,10 +471,10 @@ h2 {
waitsForPromise(() => atom.themes.activateThemes())
})
it('uses the default dark UI theme', function () {
it('uses the default one-dark UI theme', function () {
const activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-dark-ui')
expect(activeThemeNames).toContain('one-dark-ui')
expect(activeThemeNames).toContain('atom-light-syntax')
})
})
@@ -486,11 +486,11 @@ h2 {
waitsForPromise(() => atom.themes.activateThemes())
})
it('uses the default dark syntax theme', function () {
it('uses the default one-dark syntax theme', function () {
const activeThemeNames = atom.themes.getActiveThemeNames()
expect(activeThemeNames.length).toBe(2)
expect(activeThemeNames).toContain('atom-light-ui')
expect(activeThemeNames).toContain('atom-dark-syntax')
expect(activeThemeNames).toContain('one-dark-syntax')
})
})
})

View File

@@ -0,0 +1,560 @@
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
const dedent = require('dedent')
const TextBuffer = require('text-buffer')
const {Point} = TextBuffer
const TextEditor = require('../src/text-editor')
const TreeSitterGrammar = require('../src/tree-sitter-grammar')
const TreeSitterLanguageMode = require('../src/tree-sitter-language-mode')
const cGrammarPath = require.resolve('language-c/grammars/tree-sitter-c.cson')
const pythonGrammarPath = require.resolve('language-python/grammars/tree-sitter-python.cson')
const jsGrammarPath = require.resolve('language-javascript/grammars/tree-sitter-javascript.cson')
describe('TreeSitterLanguageMode', () => {
let editor, buffer
beforeEach(async () => {
editor = await atom.workspace.open('')
buffer = editor.getBuffer()
})
describe('highlighting', () => {
it('applies the most specific scope mapping to each node in the syntax tree', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {
'program': 'source',
'call_expression > identifier': 'function',
'property_identifier': 'property',
'call_expression > member_expression > property_identifier': 'method'
}
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText('aa.bbb = cc(d.eee());')
expectTokensToEqual(editor, [[
{text: 'aa.', scopes: ['source']},
{text: 'bbb', scopes: ['source', 'property']},
{text: ' = ', scopes: ['source']},
{text: 'cc', scopes: ['source', 'function']},
{text: '(d.', scopes: ['source']},
{text: 'eee', scopes: ['source', 'method']},
{text: '());', scopes: ['source']}
]])
})
it('can start or end multiple scopes at the same position', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {
'program': 'source',
'call_expression': 'call',
'member_expression': 'member',
'identifier': 'variable',
'"("': 'open-paren',
'")"': 'close-paren',
}
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText('a = bb.ccc();')
expectTokensToEqual(editor, [[
{text: 'a', scopes: ['source', 'variable']},
{text: ' = ', scopes: ['source']},
{text: 'bb', scopes: ['source', 'call', 'member', 'variable']},
{text: '.ccc', scopes: ['source', 'call', 'member']},
{text: '(', scopes: ['source', 'call', 'open-paren']},
{text: ')', scopes: ['source', 'call', 'close-paren']},
{text: ';', scopes: ['source']}
]])
})
it('can resume highlighting on a line that starts with whitespace', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {
'call_expression > member_expression > property_identifier': 'function',
'property_identifier': 'member',
'identifier': 'variable'
}
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText('a\n .b();')
expectTokensToEqual(editor, [
[
{text: 'a', scopes: ['variable']},
],
[
{text: ' ', scopes: ['whitespace']},
{text: '.', scopes: []},
{text: 'b', scopes: ['function']},
{text: '();', scopes: []}
]
])
})
it('correctly skips over tokens with zero size', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-c',
scopes: {
'primitive_type': 'type',
'identifier': 'variable',
}
})
const languageMode = new TreeSitterLanguageMode({buffer, grammar})
buffer.setLanguageMode(languageMode)
buffer.setText('int main() {\n int a\n int b;\n}');
editor.screenLineForScreenRow(0)
expect(
languageMode.document.rootNode.descendantForPosition(Point(1, 2), Point(1, 6)).toString()
).toBe('(declaration (primitive_type) (identifier) (MISSING))')
expectTokensToEqual(editor, [
[
{text: 'int', scopes: ['type']},
{text: ' ', scopes: []},
{text: 'main', scopes: ['variable']},
{text: '() {', scopes: []}
],
[
{text: ' ', scopes: ['whitespace']},
{text: 'int', scopes: ['type']},
{text: ' ', scopes: []},
{text: 'a', scopes: ['variable']}
],
[
{text: ' ', scopes: ['whitespace']},
{text: 'int', scopes: ['type']},
{text: ' ', scopes: []},
{text: 'b', scopes: ['variable']},
{text: ';', scopes: []}
],
[
{text: '}', scopes: []}
]
])
})
})
describe('folding', () => {
beforeEach(() => {
editor.displayLayer.reset({foldCharacter: '…'})
})
it('can fold nodes that start and end with specified tokens', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
folds: [
{
start: {type: '{', index: 0},
end: {type: '}', index: -1}
},
{
start: {type: '(', index: 0},
end: {type: ')', index: -1}
}
]
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
module.exports =
class A {
getB (c,
d,
e) {
return this.f(g)
}
}
`)
editor.screenLineForScreenRow(0)
expect(editor.isFoldableAtBufferRow(0)).toBe(false)
expect(editor.isFoldableAtBufferRow(1)).toBe(true)
expect(editor.isFoldableAtBufferRow(2)).toBe(true)
expect(editor.isFoldableAtBufferRow(3)).toBe(false)
expect(editor.isFoldableAtBufferRow(4)).toBe(true)
expect(editor.isFoldableAtBufferRow(5)).toBe(false)
editor.foldBufferRow(2)
expect(getDisplayText(editor)).toBe(dedent `
module.exports =
class A {
getB (…) {
return this.f(g)
}
}
`)
editor.foldBufferRow(4)
expect(getDisplayText(editor)).toBe(dedent `
module.exports =
class A {
getB (…) {…}
}
`)
})
it('can fold nodes of specified types', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
folds: [
// Start the fold after the first child (the opening tag) and end it at the last child
// (the closing tag).
{
type: 'jsx_element',
start: {index: 0},
end: {index: -1}
},
// End the fold at the *second* to last child of the self-closing tag: the `/`.
{
type: 'jsx_self_closing_element',
start: {index: 1},
end: {index: -2}
}
]
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
const element1 = <Element
className='submit'
id='something' />
const element2 = <Element>
<span>hello</span>
<span>world</span>
</Element>
`)
editor.screenLineForScreenRow(0)
expect(editor.isFoldableAtBufferRow(0)).toBe(true)
expect(editor.isFoldableAtBufferRow(1)).toBe(false)
expect(editor.isFoldableAtBufferRow(2)).toBe(false)
expect(editor.isFoldableAtBufferRow(3)).toBe(false)
expect(editor.isFoldableAtBufferRow(4)).toBe(true)
expect(editor.isFoldableAtBufferRow(5)).toBe(false)
editor.foldBufferRow(0)
expect(getDisplayText(editor)).toBe(dedent `
const element1 = <Element…/>
const element2 = <Element>
<span>hello</span>
<span>world</span>
</Element>
`)
editor.foldBufferRow(4)
expect(getDisplayText(editor)).toBe(dedent `
const element1 = <Element…/>
const element2 = <Element>…
</Element>
`)
})
it('can fold entire nodes when no start or end parameters are specified', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
folds: [
// By default, for a node with no children, folds are started at the *end* of the first
// line of a node, and ended at the *beginning* of the last line.
{type: 'comment'}
]
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
/**
* Important
*/
const x = 1 /*
Also important
*/
`)
editor.screenLineForScreenRow(0)
expect(editor.isFoldableAtBufferRow(0)).toBe(true)
expect(editor.isFoldableAtBufferRow(1)).toBe(false)
expect(editor.isFoldableAtBufferRow(2)).toBe(false)
expect(editor.isFoldableAtBufferRow(3)).toBe(true)
expect(editor.isFoldableAtBufferRow(4)).toBe(false)
editor.foldBufferRow(0)
expect(getDisplayText(editor)).toBe(dedent `
/**… */
const x = 1 /*
Also important
*/
`)
editor.foldBufferRow(3)
expect(getDisplayText(editor)).toBe(dedent `
/**… */
const x = 1 /*…*/
`)
})
it('tries each folding strategy for a given node in the order specified', () => {
const grammar = new TreeSitterGrammar(atom.grammars, cGrammarPath, {
parser: 'tree-sitter-c',
folds: [
// If the #ifdef has an `#else` clause, then end the fold there.
{
type: ['preproc_ifdef', 'preproc_elif'],
start: {index: 1},
end: {type: ['preproc_else', 'preproc_elif']}
},
// Otherwise, end the fold at the last child - the `#endif`.
{
type: 'preproc_ifdef',
start: {index: 1},
end: {index: -1}
},
// When folding an `#else` clause, the fold extends to the end of the clause.
{
type: 'preproc_else',
start: {index: 0}
}
]
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
#ifndef FOO_H_
#define FOO_H_
#ifdef _WIN32
#include <windows.h>
const char *path_separator = "\\";
#elif defined MACOS
#include <carbon.h>
const char *path_separator = "/";
#else
#include <dirent.h>
const char *path_separator = "/";
#endif
#endif
`)
editor.screenLineForScreenRow(0)
editor.foldBufferRow(3)
expect(getDisplayText(editor)).toBe(dedent `
#ifndef FOO_H_
#define FOO_H_
#ifdef _WIN32…
#elif defined MACOS
#include <carbon.h>
const char *path_separator = "/";
#else
#include <dirent.h>
const char *path_separator = "/";
#endif
#endif
`)
editor.foldBufferRow(8)
expect(getDisplayText(editor)).toBe(dedent `
#ifndef FOO_H_
#define FOO_H_
#ifdef _WIN32…
#elif defined MACOS…
#else
#include <dirent.h>
const char *path_separator = "/";
#endif
#endif
`)
editor.foldBufferRow(0)
expect(getDisplayText(editor)).toBe(dedent `
#ifndef FOO_H_…
#endif
`)
editor.foldAllAtIndentLevel(1)
expect(getDisplayText(editor)).toBe(dedent `
#ifndef FOO_H_
#define FOO_H_
#ifdef _WIN32…
#elif defined MACOS…
#else…
#endif
#endif
`)
})
describe('when folding a node that ends with a line break', () => {
it('ends the fold at the end of the previous line', () => {
const grammar = new TreeSitterGrammar(atom.grammars, pythonGrammarPath, {
parser: 'tree-sitter-python',
folds: [
{
type: 'function_definition',
start: {type: ':'}
}
]
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
def ab():
print 'a'
print 'b'
def cd():
print 'c'
print 'd'
`)
editor.screenLineForScreenRow(0)
editor.foldBufferRow(0)
expect(getDisplayText(editor)).toBe(dedent `
def ab():…
def cd():
print 'c'
print 'd'
`)
})
})
})
describe('.scopeDescriptorForPosition', () => {
it('returns a scope descriptor representing the given position in the syntax tree', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
id: 'javascript',
parser: 'tree-sitter-javascript'
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText('foo({bar: baz});')
editor.screenLineForScreenRow(0)
expect(editor.scopeDescriptorForBufferPosition({row: 0, column: 6}).getScopesArray()).toEqual([
'javascript',
'program',
'expression_statement',
'call_expression',
'arguments',
'object',
'pair',
'property_identifier'
])
})
})
describe('TextEditor.selectLargerSyntaxNode and .selectSmallerSyntaxNode', () => {
it('expands and contract the selection based on the syntax tree', () => {
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
parser: 'tree-sitter-javascript',
scopes: {'program': 'source'}
})
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
buffer.setText(dedent `
function a (b, c, d) {
eee.f()
g()
}
`)
editor.screenLineForScreenRow(0)
editor.setCursorBufferPosition([1, 3])
editor.selectLargerSyntaxNode()
expect(editor.getSelectedText()).toBe('eee')
editor.selectLargerSyntaxNode()
expect(editor.getSelectedText()).toBe('eee.f')
editor.selectLargerSyntaxNode()
expect(editor.getSelectedText()).toBe('eee.f()')
editor.selectLargerSyntaxNode()
expect(editor.getSelectedText()).toBe('{\n eee.f()\n g()\n}')
editor.selectLargerSyntaxNode()
expect(editor.getSelectedText()).toBe('function a (b, c, d) {\n eee.f()\n g()\n}')
editor.selectSmallerSyntaxNode()
expect(editor.getSelectedText()).toBe('{\n eee.f()\n g()\n}')
editor.selectSmallerSyntaxNode()
expect(editor.getSelectedText()).toBe('eee.f()')
editor.selectSmallerSyntaxNode()
expect(editor.getSelectedText()).toBe('eee.f')
editor.selectSmallerSyntaxNode()
expect(editor.getSelectedText()).toBe('eee')
editor.selectSmallerSyntaxNode()
expect(editor.getSelectedBufferRange()).toEqual([[1, 3], [1, 3]])
})
})
})
function getDisplayText (editor) {
return editor.displayLayer.getText()
}
function expectTokensToEqual (editor, expectedTokenLines) {
const lastRow = editor.getLastScreenRow()
// Assert that the correct tokens are returned regardless of which row
// the highlighting iterator starts on.
for (let startRow = 0; startRow <= lastRow; startRow++) {
editor.displayLayer.clearSpatialIndex()
editor.displayLayer.getScreenLines(startRow, Infinity)
const tokenLines = []
for (let row = startRow; row <= lastRow; row++) {
tokenLines[row] = editor.tokensForScreenRow(row).map(({text, scopes}) => ({
text,
scopes: scopes.map(scope => scope
.split(' ')
.map(className => className.slice('syntax--'.length))
.join(' '))
}))
}
for (let row = startRow; row <= lastRow; row++) {
const tokenLine = tokenLines[row]
const expectedTokenLine = expectedTokenLines[row]
expect(tokenLine.length).toEqual(expectedTokenLine.length)
for (let i = 0; i < tokenLine.length; i++) {
expect(tokenLine[i]).toEqual(expectedTokenLine[i], `Token ${i}, startRow: ${startRow}`)
}
}
}
}

View File

@@ -44,7 +44,15 @@ describe('WindowEventHandler', () => {
})
)
})
describe('resize event', () =>
it('calls storeWindowDimensions', () => {
spyOn(atom, 'storeWindowDimensions')
window.dispatchEvent(new CustomEvent('resize'))
expect(atom.storeWindowDimensions).toHaveBeenCalled()
})
)
describe('window:close event', () =>
it('closes the window', () => {
spyOn(atom, 'close')

View File

@@ -1,298 +0,0 @@
{ipcRenderer, remote, shell} = require 'electron'
ipcHelpers = require './ipc-helpers'
{Disposable} = require 'event-kit'
getWindowLoadSettings = require './get-window-load-settings'
module.exports =
class ApplicationDelegate
getWindowLoadSettings: -> getWindowLoadSettings()
open: (params) ->
ipcRenderer.send('open', params)
pickFolder: (callback) ->
responseChannel = "atom-pick-folder-response"
ipcRenderer.on responseChannel, (event, path) ->
ipcRenderer.removeAllListeners(responseChannel)
callback(path)
ipcRenderer.send("pick-folder", responseChannel)
getCurrentWindow: ->
remote.getCurrentWindow()
closeWindow: ->
ipcHelpers.call('window-method', 'close')
getTemporaryWindowState: ->
ipcHelpers.call('get-temporary-window-state').then (stateJSON) -> JSON.parse(stateJSON)
setTemporaryWindowState: (state) ->
ipcHelpers.call('set-temporary-window-state', JSON.stringify(state))
getWindowSize: ->
[width, height] = remote.getCurrentWindow().getSize()
{width, height}
setWindowSize: (width, height) ->
ipcHelpers.call('set-window-size', width, height)
getWindowPosition: ->
[x, y] = remote.getCurrentWindow().getPosition()
{x, y}
setWindowPosition: (x, y) ->
ipcHelpers.call('set-window-position', x, y)
centerWindow: ->
ipcHelpers.call('center-window')
focusWindow: ->
ipcHelpers.call('focus-window')
showWindow: ->
ipcHelpers.call('show-window')
hideWindow: ->
ipcHelpers.call('hide-window')
reloadWindow: ->
ipcHelpers.call('window-method', 'reload')
restartApplication: ->
ipcRenderer.send("restart-application")
minimizeWindow: ->
ipcHelpers.call('window-method', 'minimize')
isWindowMaximized: ->
remote.getCurrentWindow().isMaximized()
maximizeWindow: ->
ipcHelpers.call('window-method', 'maximize')
unmaximizeWindow: ->
ipcHelpers.call('window-method', 'unmaximize')
isWindowFullScreen: ->
remote.getCurrentWindow().isFullScreen()
setWindowFullScreen: (fullScreen=false) ->
ipcHelpers.call('window-method', 'setFullScreen', fullScreen)
onDidEnterFullScreen: (callback) ->
ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback)
onDidLeaveFullScreen: (callback) ->
ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback)
openWindowDevTools: ->
# Defer DevTools interaction to the next tick, because using them during
# event handling causes some wrong input events to be triggered on
# `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
new Promise(process.nextTick).then(-> ipcHelpers.call('window-method', 'openDevTools'))
closeWindowDevTools: ->
# Defer DevTools interaction to the next tick, because using them during
# event handling causes some wrong input events to be triggered on
# `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
new Promise(process.nextTick).then(-> ipcHelpers.call('window-method', 'closeDevTools'))
toggleWindowDevTools: ->
# Defer DevTools interaction to the next tick, because using them during
# event handling causes some wrong input events to be triggered on
# `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
new Promise(process.nextTick).then(-> ipcHelpers.call('window-method', 'toggleDevTools'))
executeJavaScriptInWindowDevTools: (code) ->
ipcRenderer.send("execute-javascript-in-dev-tools", code)
setWindowDocumentEdited: (edited) ->
ipcHelpers.call('window-method', 'setDocumentEdited', edited)
setRepresentedFilename: (filename) ->
ipcHelpers.call('window-method', 'setRepresentedFilename', filename)
addRecentDocument: (filename) ->
ipcRenderer.send("add-recent-document", filename)
setRepresentedDirectoryPaths: (paths) ->
ipcHelpers.call('window-method', 'setRepresentedDirectoryPaths', paths)
setAutoHideWindowMenuBar: (autoHide) ->
ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide)
setWindowMenuBarVisibility: (visible) ->
remote.getCurrentWindow().setMenuBarVisibility(visible)
getPrimaryDisplayWorkAreaSize: ->
remote.screen.getPrimaryDisplay().workAreaSize
getUserDefault: (key, type) ->
remote.systemPreferences.getUserDefault(key, type)
confirm: ({message, detailedMessage, buttons}) ->
buttons ?= {}
if Array.isArray(buttons)
buttonLabels = buttons
else
buttonLabels = Object.keys(buttons)
chosen = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info'
message: message
detail: detailedMessage
buttons: buttonLabels
normalizeAccessKeys: true
})
if Array.isArray(buttons)
chosen
else
callback = buttons[buttonLabels[chosen]]
callback?()
showMessageDialog: (params) ->
showSaveDialog: (options, callback) ->
if callback?
# Async
@getCurrentWindow().showSaveDialog(options, callback)
else
# Sync
if typeof options is 'string'
options = {defaultPath: options}
@getCurrentWindow().showSaveDialog(options)
playBeepSound: ->
shell.beep()
onDidOpenLocations: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'open-locations'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onUpdateAvailable: (callback) ->
outerCallback = (event, message, detail) ->
# TODO: Yes, this is strange that `onUpdateAvailable` is listening for
# `did-begin-downloading-update`. We currently have no mechanism to know
# if there is an update, so begin of downloading is a good proxy.
callback(detail) if message is 'did-begin-downloading-update'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onDidBeginDownloadingUpdate: (callback) ->
@onUpdateAvailable(callback)
onDidBeginCheckingForUpdate: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'checking-for-update'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onDidCompleteDownloadingUpdate: (callback) ->
outerCallback = (event, message, detail) ->
# TODO: We could rename this event to `did-complete-downloading-update`
callback(detail) if message is 'update-available'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onUpdateNotAvailable: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'update-not-available'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onUpdateError: (callback) ->
outerCallback = (event, message, detail) ->
callback(detail) if message is 'update-error'
ipcRenderer.on('message', outerCallback)
new Disposable ->
ipcRenderer.removeListener('message', outerCallback)
onApplicationMenuCommand: (callback) ->
outerCallback = (event, args...) ->
callback(args...)
ipcRenderer.on('command', outerCallback)
new Disposable ->
ipcRenderer.removeListener('command', outerCallback)
onContextMenuCommand: (callback) ->
outerCallback = (event, args...) ->
callback(args...)
ipcRenderer.on('context-command', outerCallback)
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) ->
ipcRenderer.send('did-prepare-to-unload', shouldUnload)
ipcRenderer.on('prepare-to-unload', outerCallback)
new Disposable ->
ipcRenderer.removeListener('prepare-to-unload', outerCallback)
onDidChangeHistoryManager: (callback) ->
outerCallback = (event, message) ->
callback(event)
ipcRenderer.on('did-change-history-manager', outerCallback)
new Disposable ->
ipcRenderer.removeListener('did-change-history-manager', outerCallback)
didChangeHistoryManager: ->
ipcRenderer.send('did-change-history-manager')
openExternal: (url) ->
shell.openExternal(url)
checkForUpdate: ->
ipcRenderer.send('command', 'application:check-for-update')
restartAndInstallUpdate: ->
ipcRenderer.send('command', 'application:install-update')
getAutoUpdateManagerState: ->
ipcRenderer.sendSync('get-auto-update-manager-state')
getAutoUpdateManagerErrorMessage: ->
ipcRenderer.sendSync('get-auto-update-manager-error')
emitWillSavePath: (path) ->
ipcRenderer.sendSync('will-save-path', path)
emitDidSavePath: (path) ->
ipcRenderer.sendSync('did-save-path', path)
resolveProxy: (requestId, url) ->
ipcRenderer.send('resolve-proxy', requestId, url)
onDidResolveProxy: (callback) ->
outerCallback = (event, requestId, proxy) ->
callback(requestId, proxy)
ipcRenderer.on('did-resolve-proxy', outerCallback)
new Disposable ->
ipcRenderer.removeListener('did-resolve-proxy', outerCallback)

364
src/application-delegate.js Normal file
View File

@@ -0,0 +1,364 @@
const {ipcRenderer, remote, shell} = require('electron')
const ipcHelpers = require('./ipc-helpers')
const {Disposable} = require('event-kit')
const getWindowLoadSettings = require('./get-window-load-settings')
module.exports =
class ApplicationDelegate {
getWindowLoadSettings () { return getWindowLoadSettings() }
open (params) {
return ipcRenderer.send('open', params)
}
pickFolder (callback) {
const responseChannel = 'atom-pick-folder-response'
ipcRenderer.on(responseChannel, function (event, path) {
ipcRenderer.removeAllListeners(responseChannel)
return callback(path)
})
return ipcRenderer.send('pick-folder', responseChannel)
}
getCurrentWindow () {
return remote.getCurrentWindow()
}
closeWindow () {
return ipcHelpers.call('window-method', 'close')
}
async getTemporaryWindowState () {
const stateJSON = await ipcHelpers.call('get-temporary-window-state')
return JSON.parse(stateJSON)
}
setTemporaryWindowState (state) {
return ipcHelpers.call('set-temporary-window-state', JSON.stringify(state))
}
getWindowSize () {
const [width, height] = Array.from(remote.getCurrentWindow().getSize())
return {width, height}
}
setWindowSize (width, height) {
return ipcHelpers.call('set-window-size', width, height)
}
getWindowPosition () {
const [x, y] = Array.from(remote.getCurrentWindow().getPosition())
return {x, y}
}
setWindowPosition (x, y) {
return ipcHelpers.call('set-window-position', x, y)
}
centerWindow () {
return ipcHelpers.call('center-window')
}
focusWindow () {
return ipcHelpers.call('focus-window')
}
showWindow () {
return ipcHelpers.call('show-window')
}
hideWindow () {
return ipcHelpers.call('hide-window')
}
reloadWindow () {
return ipcHelpers.call('window-method', 'reload')
}
restartApplication () {
return ipcRenderer.send('restart-application')
}
minimizeWindow () {
return ipcHelpers.call('window-method', 'minimize')
}
isWindowMaximized () {
return remote.getCurrentWindow().isMaximized()
}
maximizeWindow () {
return ipcHelpers.call('window-method', 'maximize')
}
unmaximizeWindow () {
return ipcHelpers.call('window-method', 'unmaximize')
}
isWindowFullScreen () {
return remote.getCurrentWindow().isFullScreen()
}
setWindowFullScreen (fullScreen = false) {
return ipcHelpers.call('window-method', 'setFullScreen', fullScreen)
}
onDidEnterFullScreen (callback) {
return ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback)
}
onDidLeaveFullScreen (callback) {
return ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback)
}
async openWindowDevTools () {
// Defer DevTools interaction to the next tick, because using them during
// event handling causes some wrong input events to be triggered on
// `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
await new Promise(process.nextTick)
return ipcHelpers.call('window-method', 'openDevTools')
}
async closeWindowDevTools () {
// Defer DevTools interaction to the next tick, because using them during
// event handling causes some wrong input events to be triggered on
// `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
await new Promise(process.nextTick)
return ipcHelpers.call('window-method', 'closeDevTools')
}
async toggleWindowDevTools () {
// Defer DevTools interaction to the next tick, because using them during
// event handling causes some wrong input events to be triggered on
// `TextEditorComponent` (Ref.: https://github.com/atom/atom/issues/9697).
await new Promise(process.nextTick)
return ipcHelpers.call('window-method', 'toggleDevTools')
}
executeJavaScriptInWindowDevTools (code) {
return ipcRenderer.send('execute-javascript-in-dev-tools', code)
}
didClosePathWithWaitSession (path) {
return ipcHelpers.call('window-method', 'didClosePathWithWaitSession', path)
}
setWindowDocumentEdited (edited) {
return ipcHelpers.call('window-method', 'setDocumentEdited', edited)
}
setRepresentedFilename (filename) {
return ipcHelpers.call('window-method', 'setRepresentedFilename', filename)
}
addRecentDocument (filename) {
return ipcRenderer.send('add-recent-document', filename)
}
setRepresentedDirectoryPaths (paths) {
return ipcHelpers.call('window-method', 'setRepresentedDirectoryPaths', paths)
}
setAutoHideWindowMenuBar (autoHide) {
return ipcHelpers.call('window-method', 'setAutoHideMenuBar', autoHide)
}
setWindowMenuBarVisibility (visible) {
return remote.getCurrentWindow().setMenuBarVisibility(visible)
}
getPrimaryDisplayWorkAreaSize () {
return remote.screen.getPrimaryDisplay().workAreaSize
}
getUserDefault (key, type) {
return remote.systemPreferences.getUserDefault(key, type)
}
confirm ({message, detailedMessage, buttons}) {
let buttonLabels
if (!buttons) buttons = {}
if (Array.isArray(buttons)) {
buttonLabels = buttons
} else {
buttonLabels = Object.keys(buttons)
}
const chosen = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message,
detail: detailedMessage,
buttons: buttonLabels,
normalizeAccessKeys: true
})
if (Array.isArray(buttons)) {
return chosen
} else {
const callback = buttons[buttonLabels[chosen]]
return (typeof callback === 'function' ? callback() : undefined)
}
}
showMessageDialog (params) {}
showSaveDialog (options, callback) {
if (typeof callback === 'function') {
// Async
this.getCurrentWindow().showSaveDialog(options, callback)
} else {
// Sync
if (typeof params === 'string') {
options = {defaultPath: options}
}
return this.getCurrentWindow().showSaveDialog(options)
}
}
playBeepSound () {
return shell.beep()
}
onDidOpenLocations (callback) {
const outerCallback = (event, message, detail) => {
if (message === 'open-locations') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
}
onUpdateAvailable (callback) {
const outerCallback = (event, message, detail) => {
// TODO: Yes, this is strange that `onUpdateAvailable` is listening for
// `did-begin-downloading-update`. We currently have no mechanism to know
// if there is an update, so begin of downloading is a good proxy.
if (message === 'did-begin-downloading-update') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
}
onDidBeginDownloadingUpdate (callback) {
return this.onUpdateAvailable(callback)
}
onDidBeginCheckingForUpdate (callback) {
const outerCallback = (event, message, detail) => {
if (message === 'checking-for-update') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
}
onDidCompleteDownloadingUpdate (callback) {
const outerCallback = (event, message, detail) => {
// TODO: We could rename this event to `did-complete-downloading-update`
if (message === 'update-available') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
}
onUpdateNotAvailable (callback) {
const outerCallback = (event, message, detail) => {
if (message === 'update-not-available') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
}
onUpdateError (callback) {
const outerCallback = (event, message, detail) => {
if (message === 'update-error') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
}
onApplicationMenuCommand (handler) {
const outerCallback = (event, ...args) => handler(...args)
ipcRenderer.on('command', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('command', outerCallback))
}
onContextMenuCommand (handler) {
const outerCallback = (event, ...args) => handler(...args)
ipcRenderer.on('context-command', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('context-command', outerCallback))
}
onURIMessage (handler) {
const outerCallback = (event, ...args) => handler(...args)
ipcRenderer.on('uri-message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('uri-message', outerCallback))
}
onDidRequestUnload (callback) {
const outerCallback = async (event, message) => {
const shouldUnload = await callback(event)
ipcRenderer.send('did-prepare-to-unload', shouldUnload)
}
ipcRenderer.on('prepare-to-unload', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('prepare-to-unload', outerCallback))
}
onDidChangeHistoryManager (callback) {
const outerCallback = (event, message) => callback(event)
ipcRenderer.on('did-change-history-manager', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('did-change-history-manager', outerCallback))
}
didChangeHistoryManager () {
return ipcRenderer.send('did-change-history-manager')
}
openExternal (url) {
return shell.openExternal(url)
}
checkForUpdate () {
return ipcRenderer.send('command', 'application:check-for-update')
}
restartAndInstallUpdate () {
return ipcRenderer.send('command', 'application:install-update')
}
getAutoUpdateManagerState () {
return ipcRenderer.sendSync('get-auto-update-manager-state')
}
getAutoUpdateManagerErrorMessage () {
return ipcRenderer.sendSync('get-auto-update-manager-error')
}
emitWillSavePath (path) {
return ipcRenderer.sendSync('will-save-path', path)
}
emitDidSavePath (path) {
return ipcRenderer.sendSync('did-save-path', path)
}
resolveProxy (requestId, url) {
return ipcRenderer.send('resolve-proxy', requestId, url)
}
onDidResolveProxy (callback) {
const outerCallback = (event, requestId, proxy) => callback(requestId, proxy)
ipcRenderer.on('did-resolve-proxy', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('did-resolve-proxy', outerCallback))
}
}

View File

@@ -51,13 +51,15 @@ let nextId = 0
//
// An instance of this class is always available as the `atom` global.
class AtomEnvironment {
/*
Section: Construction and Destruction
Section: Properties
*/
// Call .loadOrCreate instead
constructor (params = {}) {
this.id = (params.id != null) ? params.id : nextId++
// Public: A {Clipboard} instance
this.clipboard = params.clipboard
this.updateProcessEnv = params.updateProcessEnv || updateProcessEnv
this.enablePersistence = params.enablePersistence
@@ -68,25 +70,44 @@ class AtomEnvironment {
this.loadTime = null
this.emitter = new Emitter()
this.disposables = new CompositeDisposable()
this.pathsWithWaitSessions = new Set()
// Public: A {DeserializerManager} instance
this.deserializers = new DeserializerManager(this)
this.deserializeTimings = {}
// Public: A {ViewRegistry} instance
this.views = new ViewRegistry(this)
// Public: A {NotificationManager} instance
this.notifications = new NotificationManager()
this.stateStore = new StateStore('AtomEnvironments', 1)
// Public: A {Config} instance
this.config = new Config({
notificationManager: this.notifications,
enablePersistence: this.enablePersistence
})
this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)})
// Public: A {KeymapManager} instance
this.keymaps = new KeymapManager({notificationManager: this.notifications})
// Public: A {TooltipManager} instance
this.tooltips = new TooltipManager({keymapManager: this.keymaps, viewRegistry: this.views})
// Public: A {CommandRegistry} instance
this.commands = new CommandRegistry()
this.uriHandlerRegistry = new URIHandlerRegistry()
// Public: A {GrammarRegistry} instance
this.grammars = new GrammarRegistry({config: this.config})
// Public: A {StyleManager} instance
this.styles = new StyleManager()
// Public: A {PackageManager} instance
this.packages = new PackageManager({
config: this.config,
styleManager: this.styles,
@@ -98,6 +119,8 @@ class AtomEnvironment {
viewRegistry: this.views,
uriHandlerRegistry: this.uriHandlerRegistry
})
// Public: A {ThemeManager} instance
this.themes = new ThemeManager({
packageManager: this.packages,
config: this.config,
@@ -105,12 +128,18 @@ class AtomEnvironment {
notificationManager: this.notifications,
viewRegistry: this.views
})
// Public: A {MenuManager} instance
this.menu = new MenuManager({keymapManager: this.keymaps, packageManager: this.packages})
// Public: A {ContextMenuManager} instance
this.contextMenu = new ContextMenuManager({keymapManager: this.keymaps})
this.packages.setMenuManager(this.menu)
this.packages.setContextMenuManager(this.contextMenu)
this.packages.setThemeManager(this.themes)
// Public: A {Project} instance
this.project = new Project({
notificationManager: this.notifications,
packageManager: this.packages,
@@ -121,6 +150,7 @@ class AtomEnvironment {
this.commandInstaller = new CommandInstaller(this.applicationDelegate)
this.protocolHandlerInstaller = new ProtocolHandlerInstaller()
// Public: A {TextEditorRegistry} instance
this.textEditors = new TextEditorRegistry({
config: this.config,
grammarRegistry: this.grammars,
@@ -128,6 +158,7 @@ class AtomEnvironment {
packageManager: this.packages
})
// Public: A {Workspace} instance
this.workspace = new Workspace({
config: this.config,
project: this.project,
@@ -157,7 +188,9 @@ class AtomEnvironment {
this.windowEventHandler = new WindowEventHandler({atomEnvironment: this, applicationDelegate: this.applicationDelegate})
// Public: A {HistoryManager} instance
this.history = new HistoryManager({project: this.project, commands: this.commands, stateStore: this.stateStore})
// Keep instances of HistoryManager in sync
this.disposables.add(this.history.onDidChangeProjects(event => {
if (!event.reloaded) this.applicationDelegate.didChangeHistoryManager()
@@ -206,12 +239,13 @@ class AtomEnvironment {
this.themes.initialize({configDirPath: this.configDirPath, resourcePath, safeMode, devMode})
this.commandInstaller.initialize(this.getVersion())
this.protocolHandlerInstaller.initialize(this.config, this.notifications)
this.uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this))
this.autoUpdater.initialize()
this.config.load()
this.protocolHandlerInstaller.initialize(this.config, this.notifications)
this.themes.loadBaseStylesheets()
this.initialStyleElements = this.styles.getSnapshot()
if (params.onlyLoadBaseStyleSheets) this.themes.initialLoadComplete = true
@@ -326,6 +360,7 @@ class AtomEnvironment {
this.grammars.clear()
this.textEditors.clear()
this.views.clear()
this.pathsWithWaitSessions.clear()
}
destroy () {
@@ -789,7 +824,22 @@ class AtomEnvironment {
this.document.body.appendChild(this.workspace.getElement())
if (this.backgroundStylesheet) this.backgroundStylesheet.remove()
this.watchProjectPaths()
let previousProjectPaths = this.project.getPaths()
this.disposables.add(this.project.onDidChangePaths(newPaths => {
for (let path of previousProjectPaths) {
if (this.pathsWithWaitSessions.has(path) && !newPaths.includes(path)) {
this.applicationDelegate.didClosePathWithWaitSession(path)
}
}
previousProjectPaths = newPaths
this.applicationDelegate.setRepresentedDirectoryPaths(newPaths)
}))
this.disposables.add(this.workspace.onDidDestroyPaneItem(({item}) => {
const path = item.getPath && item.getPath()
if (this.pathsWithWaitSessions.has(path)) {
this.applicationDelegate.didClosePathWithWaitSession(path)
}
}))
this.packages.activate()
this.keymaps.loadUserKeymap()
@@ -992,13 +1042,6 @@ class AtomEnvironment {
return this.themes.load()
}
// Notify the browser project of the window's current project path
watchProjectPaths () {
this.disposables.add(this.project.onDidChangePaths(() => {
this.applicationDelegate.setRepresentedDirectoryPaths(this.project.getPaths())
}))
}
setDocumentEdited (edited) {
if (typeof this.applicationDelegate.setWindowDocumentEdited === 'function') {
this.applicationDelegate.setWindowDocumentEdited(edited)
@@ -1012,8 +1055,10 @@ class AtomEnvironment {
}
addProjectFolder () {
this.pickFolder((selectedPaths = []) => {
this.addToProject(selectedPaths)
return new Promise((resolve) => {
this.pickFolder((selectedPaths) => {
this.addToProject(selectedPaths || []).then(resolve)
})
})
}
@@ -1264,8 +1309,9 @@ or use Pane::saveItemAs for programmatic saving.`)
}
}
for (var {pathToOpen, initialLine, initialColumn, forceAddToWindow} of locations) {
if (pathToOpen && (needsProjectPaths || forceAddToWindow)) {
for (const location of locations) {
const {pathToOpen} = location
if (pathToOpen && (needsProjectPaths || location.forceAddToWindow)) {
if (fs.existsSync(pathToOpen)) {
pushFolderToOpen(this.project.getDirectoryForProjectPath(pathToOpen).getPath())
} else if (fs.existsSync(path.dirname(pathToOpen))) {
@@ -1276,8 +1322,10 @@ or use Pane::saveItemAs for programmatic saving.`)
}
if (!fs.isDirectorySync(pathToOpen)) {
fileLocationsToOpen.push({pathToOpen, initialLine, initialColumn})
fileLocationsToOpen.push(location)
}
if (location.hasWaitSession) this.pathsWithWaitSessions.add(pathToOpen)
}
let restoredState = false
@@ -1298,7 +1346,7 @@ or use Pane::saveItemAs for programmatic saving.`)
if (!restoredState) {
const fileOpenPromises = []
for ({pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) {
for (const {pathToOpen, initialLine, initialColumn} of fileLocationsToOpen) {
fileOpenPromises.push(this.workspace && this.workspace.open(pathToOpen, {initialLine, initialColumn}))
}
await Promise.all(fileOpenPromises)

View File

@@ -342,6 +342,11 @@ const configSchema = {
description: 'Emulated with Atom events'
}
]
},
useTreeSitterParsers: {
type: 'boolean',
default: false,
description: 'Use the new Tree-sitter parsing system for supported languages'
}
}
},

View File

@@ -423,6 +423,7 @@ class Config
@configFileHasErrors = false
@transactDepth = 0
@pendingOperations = []
@legacyScopeAliases = {}
@requestLoad = _.debounce =>
@loadUserConfig()
@@ -599,11 +600,22 @@ class Config
# * `value` The value for the key-path
getAll: (keyPath, options) ->
{scope} = options if options?
result = []
if scope?
scopeDescriptor = ScopeDescriptor.fromObject(scope)
result = result.concat @scopedSettingsStore.getAll(scopeDescriptor.getScopeChain(), keyPath, options)
result = @scopedSettingsStore.getAll(
scopeDescriptor.getScopeChain(),
keyPath,
options
)
if legacyScopeDescriptor = @getLegacyScopeDescriptor(scopeDescriptor)
result.push(@scopedSettingsStore.getAll(
legacyScopeDescriptor.getScopeChain(),
keyPath,
options
)...)
else
result = []
if globalValue = @getRawValue(keyPath, options)
result.push(scopeSelector: '*', value: globalValue)
@@ -762,6 +774,12 @@ class Config
finally
@endTransaction()
addLegacyScopeAlias: (languageId, legacyScopeName) ->
@legacyScopeAliases[languageId] = legacyScopeName
removeLegacyScopeAlias: (languageId) ->
delete @legacyScopeAliases[languageId]
###
Section: Internal methods used by core
###
@@ -1145,7 +1163,20 @@ class Config
getRawScopedValue: (scopeDescriptor, keyPath, options) ->
scopeDescriptor = ScopeDescriptor.fromObject(scopeDescriptor)
@scopedSettingsStore.getPropertyValue(scopeDescriptor.getScopeChain(), keyPath, options)
result = @scopedSettingsStore.getPropertyValue(
scopeDescriptor.getScopeChain(),
keyPath,
options
)
if result?
result
else if legacyScopeDescriptor = @getLegacyScopeDescriptor(scopeDescriptor)
@scopedSettingsStore.getPropertyValue(
legacyScopeDescriptor.getScopeChain(),
keyPath,
options
)
observeScopedKeyPath: (scope, keyPath, callback) ->
callback(@get(keyPath, {scope}))
@@ -1160,6 +1191,13 @@ class Config
oldValue = newValue
callback(event)
getLegacyScopeDescriptor: (scopeDescriptor) ->
legacyAlias = @legacyScopeAliases[scopeDescriptor.scopes[0]]
if legacyAlias
scopes = scopeDescriptor.scopes.slice()
scopes[0] = legacyAlias
new ScopeDescriptor({scopes})
# Base schema enforcers. These will coerce raw input into the specified type,
# and will throw an error when the value cannot be coerced. Throwing the error
# will indicate that the value should not be set.

View File

@@ -1,13 +1,16 @@
const _ = require('underscore-plus')
const Grim = require('grim')
const CSON = require('season')
const FirstMate = require('first-mate')
const {Disposable, CompositeDisposable} = require('event-kit')
const TextMateLanguageMode = require('./text-mate-language-mode')
const TreeSitterLanguageMode = require('./tree-sitter-language-mode')
const TreeSitterGrammar = require('./tree-sitter-grammar')
const Token = require('./token')
const fs = require('fs-plus')
const {Point, Range} = require('text-buffer')
const GRAMMAR_SELECTION_RANGE = Range(Point.ZERO, Point(10, 0)).freeze()
const GRAMMAR_TYPE_BONUS = 1000
const PATH_SPLIT_REGEX = new RegExp('[/.]')
// Extended: This class holds the grammars used for tokenizing.
@@ -24,10 +27,13 @@ class GrammarRegistry {
clear () {
this.textmateRegistry.clear()
this.treeSitterGrammarsById = {}
if (this.subscriptions) this.subscriptions.dispose()
this.subscriptions = new CompositeDisposable()
this.languageOverridesByBufferId = new Map()
this.grammarScoresByBuffer = new Map()
this.textMateScopeNamesByTreeSitterLanguageId = new Map()
this.treeSitterLanguageIdsByTextMateScopeName = new Map()
const grammarAddedOrUpdated = this.grammarAddedOrUpdated.bind(this)
this.textmateRegistry.onDidAddGrammar(grammarAddedOrUpdated)
@@ -102,17 +108,18 @@ class GrammarRegistry {
// Extended: Force a {TextBuffer} to use a different grammar than the
// one that would otherwise be selected for it.
//
// * `buffer` The {TextBuffer} whose gramamr will be set.
// * `buffer` The {TextBuffer} whose grammar will be set.
// * `languageId` The {String} id of the desired language.
//
// Returns a {Boolean} that indicates whether the language was successfully
// found.
assignLanguageMode (buffer, languageId) {
if (buffer.getBuffer) buffer = buffer.getBuffer()
languageId = this.normalizeLanguageId(languageId)
let grammar = null
if (languageId != null) {
grammar = this.textmateRegistry.grammarForScopeName(languageId)
grammar = this.grammarForId(languageId)
if (!grammar) return false
this.languageOverridesByBufferId.set(buffer.id, languageId)
} else {
@@ -136,7 +143,7 @@ class GrammarRegistry {
autoAssignLanguageMode (buffer) {
const result = this.selectGrammarWithScore(
buffer.getPath(),
buffer.getTextInRange(GRAMMAR_SELECTION_RANGE)
getGrammarSelectionContent(buffer)
)
this.languageOverridesByBufferId.delete(buffer.id)
this.grammarScoresByBuffer.set(buffer, result.score)
@@ -146,7 +153,11 @@ class GrammarRegistry {
}
languageModeForGrammarAndBuffer (grammar, buffer) {
return new TextMateLanguageMode({grammar, buffer, config: this.config})
if (grammar instanceof TreeSitterGrammar) {
return new TreeSitterLanguageMode({grammar, buffer, config: this.config})
} else {
return new TextMateLanguageMode({grammar, buffer, config: this.config})
}
}
// Extended: Select a grammar for the given file path and file contents.
@@ -165,39 +176,44 @@ class GrammarRegistry {
selectGrammarWithScore (filePath, fileContents) {
let bestMatch = null
let highestScore = -Infinity
for (let grammar of this.textmateRegistry.grammars) {
this.forEachGrammar(grammar => {
const score = this.getGrammarScore(grammar, filePath, fileContents)
if ((score > highestScore) || (bestMatch == null)) {
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)) {
if (contents == null && fs.isFileSync(filePath)) {
contents = fs.readFileSync(filePath, 'utf8')
}
let score = this.getGrammarPathScore(grammar, filePath)
if ((score > 0) && !grammar.bundledPackage) {
if (score > 0 && !grammar.bundledPackage) {
score += 0.125
}
if (this.grammarMatchesContents(grammar, contents)) {
score += 0.25
}
if (score > 0 && this.isGrammarPreferredType(grammar)) {
score += GRAMMAR_TYPE_BONUS
}
return score
}
getGrammarPathScore (grammar, filePath) {
if (!filePath) { return -1 }
if (!filePath) return -1
if (process.platform === 'win32') { filePath = filePath.replace(/\\/g, '/') }
const pathComponents = filePath.toLowerCase().split(PATH_SPLIT_REGEX)
let pathScore = -1
let pathScore = 0
let customFileTypes
if (this.config.get('core.customFileTypes')) {
@@ -225,25 +241,48 @@ class GrammarRegistry {
}
grammarMatchesContents (grammar, contents) {
if ((contents == null) || (grammar.firstLineRegex == null)) { return false }
if (contents == 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
if (grammar.contentRegExp) { // TreeSitter grammars
return grammar.contentRegExp.test(contents)
} else if (grammar.firstLineRegex) { // FirstMate grammars
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'))
} else {
return false
}
const lines = contents.split('\n')
return grammar.firstLineRegex.testSync(lines.slice(0, numberOfNewlinesInRegex + 1).join('\n'))
}
forEachGrammar (callback) {
this.textmateRegistry.grammars.forEach(callback)
for (let grammarId in this.treeSitterGrammarsById) {
callback(this.treeSitterGrammarsById[grammarId])
}
}
grammarForId (languageId) {
languageId = this.normalizeLanguageId(languageId)
return (
this.textmateRegistry.grammarForScopeName(languageId) ||
this.treeSitterGrammarsById[languageId]
)
}
// Deprecated: Get the grammar override for the given file path.
@@ -284,6 +323,8 @@ class GrammarRegistry {
}
grammarAddedOrUpdated (grammar) {
if (grammar.scopeName && !grammar.id) grammar.id = grammar.scopeName
this.grammarScoresByBuffer.forEach((score, buffer) => {
const languageMode = buffer.getLanguageMode()
if (grammar.injectionSelector) {
@@ -295,16 +336,11 @@ class GrammarRegistry {
const languageOverride = this.languageOverridesByBufferId.get(buffer.id)
if ((grammar.scopeName === buffer.getLanguageMode().getLanguageId() ||
grammar.scopeName === languageOverride)) {
if ((grammar.id === buffer.getLanguageMode().getLanguageId() ||
grammar.id === languageOverride)) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
} else if (!languageOverride) {
const score = this.getGrammarScore(
grammar,
buffer.getPath(),
buffer.getTextInRange(GRAMMAR_SELECTION_RANGE)
)
const score = this.getGrammarScore(grammar, buffer.getPath(), getGrammarSelectionContent(buffer))
const currentScore = this.grammarScoresByBuffer.get(buffer)
if (currentScore == null || score > currentScore) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
@@ -348,15 +384,35 @@ class GrammarRegistry {
}
grammarForScopeName (scopeName) {
return this.textmateRegistry.grammarForScopeName(scopeName)
return this.grammarForId(scopeName)
}
addGrammar (grammar) {
return this.textmateRegistry.addGrammar(grammar)
if (grammar instanceof TreeSitterGrammar) {
this.treeSitterGrammarsById[grammar.id] = grammar
if (grammar.legacyScopeName) {
this.config.addLegacyScopeAlias(grammar.id, grammar.legacyScopeName)
this.textMateScopeNamesByTreeSitterLanguageId.set(grammar.id, grammar.legacyScopeName)
this.treeSitterLanguageIdsByTextMateScopeName.set(grammar.legacyScopeName, grammar.id)
}
this.grammarAddedOrUpdated(grammar)
return new Disposable(() => this.removeGrammar(grammar))
} else {
return this.textmateRegistry.addGrammar(grammar)
}
}
removeGrammar (grammar) {
return this.textmateRegistry.removeGrammar(grammar)
if (grammar instanceof TreeSitterGrammar) {
delete this.treeSitterGrammarsById[grammar.id]
if (grammar.legacyScopeName) {
this.config.removeLegacyScopeAlias(grammar.id)
this.textMateScopeNamesByTreeSitterLanguageId.delete(grammar.id)
this.treeSitterLanguageIdsByTextMateScopeName.delete(grammar.legacyScopeName)
}
} else {
return this.textmateRegistry.removeGrammar(grammar)
}
}
removeGrammarForScopeName (scopeName) {
@@ -370,7 +426,11 @@ class GrammarRegistry {
// * `error` An {Error}, may be null.
// * `grammar` A {Grammar} or null if an error occured.
loadGrammar (grammarPath, callback) {
return this.textmateRegistry.loadGrammar(grammarPath, callback)
this.readGrammar(grammarPath, (error, grammar) => {
if (error) return callback(error)
this.addGrammar(grammar)
callback(grammar)
})
}
// Extended: Read a grammar synchronously and add it to this registry.
@@ -379,7 +439,9 @@ class GrammarRegistry {
//
// Returns a {Grammar}.
loadGrammarSync (grammarPath) {
return this.textmateRegistry.loadGrammarSync(grammarPath)
const grammar = this.readGrammarSync(grammarPath)
this.addGrammar(grammar)
return grammar
}
// Extended: Read a grammar asynchronously but don't add it to the registry.
@@ -391,7 +453,15 @@ class GrammarRegistry {
//
// Returns undefined.
readGrammar (grammarPath, callback) {
return this.textmateRegistry.readGrammar(grammarPath, callback)
if (!callback) callback = () => {}
CSON.readFile(grammarPath, (error, params = {}) => {
if (error) return callback(error)
try {
callback(null, this.createGrammar(grammarPath, params))
} catch (error) {
callback(error)
}
})
}
// Extended: Read a grammar synchronously but don't add it to the registry.
@@ -400,11 +470,18 @@ class GrammarRegistry {
//
// Returns a {Grammar}.
readGrammarSync (grammarPath) {
return this.textmateRegistry.readGrammarSync(grammarPath)
return this.createGrammar(grammarPath, CSON.readFileSync(grammarPath) || {})
}
createGrammar (grammarPath, params) {
return this.textmateRegistry.createGrammar(grammarPath, params)
if (params.type === 'tree-sitter') {
return new TreeSitterGrammar(this, grammarPath, params)
} else {
if (typeof params.scopeName !== 'string' || params.scopeName.length === 0) {
throw new Error(`Grammar missing required scopeName property: ${grammarPath}`)
}
return this.textmateRegistry.createGrammar(grammarPath, params)
}
}
// Extended: Get all the grammars in this registry.
@@ -417,4 +494,25 @@ class GrammarRegistry {
scopeForId (id) {
return this.textmateRegistry.scopeForId(id)
}
isGrammarPreferredType (grammar) {
return this.config.get('core.useTreeSitterParsers')
? grammar instanceof TreeSitterGrammar
: grammar instanceof FirstMate.Grammar
}
normalizeLanguageId (languageId) {
if (this.config.get('core.useTreeSitterParsers')) {
return this.treeSitterLanguageIdsByTextMateScopeName.get(languageId) || languageId
} else {
return this.textMateScopeNamesByTreeSitterLanguageId.get(languageId) || languageId
}
}
}
function getGrammarSelectionContent (buffer) {
return buffer.getTextInRange(Range(
Point(0, 0),
buffer.positionForCharacterIndex(1024)
))
}

View File

@@ -1,161 +0,0 @@
{app, Menu} = require 'electron'
_ = require 'underscore-plus'
MenuHelpers = require '../menu-helpers'
# Used to manage the global application menu.
#
# It's created by {AtomApplication} upon instantiation and used to add, remove
# and maintain the state of all menu items.
module.exports =
class ApplicationMenu
constructor: (@version, @autoUpdateManager) ->
@windowTemplates = new WeakMap()
@setActiveTemplate(@getDefaultTemplate())
@autoUpdateManager.on 'state-changed', (state) => @showUpdateMenuItem(state)
# Public: Updates the entire menu with the given keybindings.
#
# window - The BrowserWindow this menu template is associated with.
# template - The Object which describes the menu to display.
# keystrokesByCommand - An Object where the keys are commands and the values
# are Arrays containing the keystroke.
update: (window, template, keystrokesByCommand) ->
@translateTemplate(template, keystrokesByCommand)
@substituteVersion(template)
@windowTemplates.set(window, template)
@setActiveTemplate(template) if window is @lastFocusedWindow
setActiveTemplate: (template) ->
unless _.isEqual(template, @activeTemplate)
@activeTemplate = template
@menu = Menu.buildFromTemplate(_.deepClone(template))
Menu.setApplicationMenu(@menu)
@showUpdateMenuItem(@autoUpdateManager.getState())
# Register a BrowserWindow with this application menu.
addWindow: (window) ->
@lastFocusedWindow ?= window
focusHandler = =>
@lastFocusedWindow = window
if template = @windowTemplates.get(window)
@setActiveTemplate(template)
window.on 'focus', focusHandler
window.once 'closed', =>
@lastFocusedWindow = null if window is @lastFocusedWindow
@windowTemplates.delete(window)
window.removeListener 'focus', focusHandler
@enableWindowSpecificItems(true)
# Flattens the given menu and submenu items into an single Array.
#
# menu - A complete menu configuration object for atom-shell's menu API.
#
# Returns an Array of native menu items.
flattenMenuItems: (menu) ->
items = []
for index, item of menu.items or {}
items.push(item)
items = items.concat(@flattenMenuItems(item.submenu)) if item.submenu
items
# Flattens the given menu template into an single Array.
#
# template - An object describing the menu item.
#
# Returns an Array of native menu items.
flattenMenuTemplate: (template) ->
items = []
for item in template
items.push(item)
items = items.concat(@flattenMenuTemplate(item.submenu)) if item.submenu
items
# Public: Used to make all window related menu items are active.
#
# enable - If true enables all window specific items, if false disables all
# window specific items.
enableWindowSpecificItems: (enable) ->
for item in @flattenMenuItems(@menu)
item.enabled = enable if item.metadata?.windowSpecific
return
# Replaces VERSION with the current version.
substituteVersion: (template) ->
if (item = _.find(@flattenMenuTemplate(template), ({label}) -> label is 'VERSION'))
item.label = "Version #{@version}"
# Sets the proper visible state the update menu items
showUpdateMenuItem: (state) ->
checkForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Check for Update')
checkingForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Checking for Update')
downloadingUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Downloading Update')
installUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Restart and Install Update')
return unless checkForUpdateItem? and checkingForUpdateItem? and downloadingUpdateItem? and installUpdateItem?
checkForUpdateItem.visible = false
checkingForUpdateItem.visible = false
downloadingUpdateItem.visible = false
installUpdateItem.visible = false
switch state
when 'idle', 'error', 'no-update-available'
checkForUpdateItem.visible = true
when 'checking'
checkingForUpdateItem.visible = true
when 'downloading'
downloadingUpdateItem.visible = true
when 'update-available'
installUpdateItem.visible = true
# Default list of menu items.
#
# Returns an Array of menu item Objects.
getDefaultTemplate: ->
[
label: "Atom"
submenu: [
{label: "Check for Update", metadata: {autoUpdate: true}}
{label: 'Reload', accelerator: 'Command+R', click: => @focusedWindow()?.reload()}
{label: 'Close Window', accelerator: 'Command+Shift+W', click: => @focusedWindow()?.close()}
{label: 'Toggle Dev Tools', accelerator: 'Command+Alt+I', click: => @focusedWindow()?.toggleDevTools()}
{label: 'Quit', accelerator: 'Command+Q', click: -> app.quit()}
]
]
focusedWindow: ->
_.find global.atomApplication.getAllWindows(), (atomWindow) -> atomWindow.isFocused()
# Combines a menu template with the appropriate keystroke.
#
# template - An Object conforming to atom-shell's menu api but lacking
# accelerator and click properties.
# keystrokesByCommand - An Object where the keys are commands and the values
# are Arrays containing the keystroke.
#
# Returns a complete menu configuration object for atom-shell's menu API.
translateTemplate: (template, keystrokesByCommand) ->
template.forEach (item) =>
item.metadata ?= {}
if item.command
item.accelerator = @acceleratorForCommand(item.command, keystrokesByCommand)
item.click = -> global.atomApplication.sendCommand(item.command, item.commandDetail)
item.metadata.windowSpecific = true unless /^application:/.test(item.command, item.commandDetail)
@translateTemplate(item.submenu, keystrokesByCommand) if item.submenu
template
# Determine the accelerator for a given command.
#
# command - The name of the command.
# keystrokesByCommand - An Object where the keys are commands and the values
# are Arrays containing the keystroke.
#
# Returns a String containing the keystroke in a format that can be interpreted
# by Electron to provide nice icons where available.
acceleratorForCommand: (command, keystrokesByCommand) ->
firstKeystroke = keystrokesByCommand[command]?[0]
MenuHelpers.acceleratorForKeystroke(firstKeystroke)

View File

@@ -0,0 +1,225 @@
const {app, Menu} = require('electron')
const _ = require('underscore-plus')
const MenuHelpers = require('../menu-helpers')
// Used to manage the global application menu.
//
// It's created by {AtomApplication} upon instantiation and used to add, remove
// and maintain the state of all menu items.
module.exports =
class ApplicationMenu {
constructor (version, autoUpdateManager) {
this.version = version
this.autoUpdateManager = autoUpdateManager
this.windowTemplates = new WeakMap()
this.setActiveTemplate(this.getDefaultTemplate())
this.autoUpdateManager.on('state-changed', state => this.showUpdateMenuItem(state))
}
// Public: Updates the entire menu with the given keybindings.
//
// window - The BrowserWindow this menu template is associated with.
// template - The Object which describes the menu to display.
// keystrokesByCommand - An Object where the keys are commands and the values
// are Arrays containing the keystroke.
update (window, template, keystrokesByCommand) {
this.translateTemplate(template, keystrokesByCommand)
this.substituteVersion(template)
this.windowTemplates.set(window, template)
if (window === this.lastFocusedWindow) return this.setActiveTemplate(template)
}
setActiveTemplate (template) {
if (!_.isEqual(template, this.activeTemplate)) {
this.activeTemplate = template
this.menu = Menu.buildFromTemplate(_.deepClone(template))
Menu.setApplicationMenu(this.menu)
}
return this.showUpdateMenuItem(this.autoUpdateManager.getState())
}
// Register a BrowserWindow with this application menu.
addWindow (window) {
if (this.lastFocusedWindow == null) this.lastFocusedWindow = window
const focusHandler = () => {
this.lastFocusedWindow = window
const template = this.windowTemplates.get(window)
if (template) this.setActiveTemplate(template)
}
window.on('focus', focusHandler)
window.once('closed', () => {
if (window === this.lastFocusedWindow) this.lastFocusedWindow = null
this.windowTemplates.delete(window)
window.removeListener('focus', focusHandler)
})
this.enableWindowSpecificItems(true)
}
// Flattens the given menu and submenu items into an single Array.
//
// menu - A complete menu configuration object for atom-shell's menu API.
//
// Returns an Array of native menu items.
flattenMenuItems (menu) {
const object = menu.items || {}
let items = []
for (let index in object) {
const item = object[index]
items.push(item)
if (item.submenu) items = items.concat(this.flattenMenuItems(item.submenu))
}
return items
}
// Flattens the given menu template into an single Array.
//
// template - An object describing the menu item.
//
// Returns an Array of native menu items.
flattenMenuTemplate (template) {
let items = []
for (let item of template) {
items.push(item)
if (item.submenu) items = items.concat(this.flattenMenuTemplate(item.submenu))
}
return items
}
// Public: Used to make all window related menu items are active.
//
// enable - If true enables all window specific items, if false disables all
// window specific items.
enableWindowSpecificItems (enable) {
for (let item of this.flattenMenuItems(this.menu)) {
if (item.metadata && item.metadata.windowSpecific) item.enabled = enable
}
}
// Replaces VERSION with the current version.
substituteVersion (template) {
let item = this.flattenMenuTemplate(template).find(({label}) => label === 'VERSION')
if (item) item.label = `Version ${this.version}`
}
// Sets the proper visible state the update menu items
showUpdateMenuItem (state) {
const items = this.flattenMenuItems(this.menu)
const checkForUpdateItem = items.find(({label}) => label === 'Check for Update')
const checkingForUpdateItem = items.find(({label}) => label === 'Checking for Update')
const downloadingUpdateItem = items.find(({label}) => label === 'Downloading Update')
const installUpdateItem = items.find(({label}) => label === 'Restart and Install Update')
if (!checkForUpdateItem || !checkingForUpdateItem ||
!downloadingUpdateItem || !installUpdateItem) return
checkForUpdateItem.visible = false
checkingForUpdateItem.visible = false
downloadingUpdateItem.visible = false
installUpdateItem.visible = false
switch (state) {
case 'idle':
case 'error':
case 'no-update-available':
checkForUpdateItem.visible = true
break
case 'checking':
checkingForUpdateItem.visible = true
break
case 'downloading':
downloadingUpdateItem.visible = true
break
case 'update-available':
installUpdateItem.visible = true
break
}
}
// Default list of menu items.
//
// Returns an Array of menu item Objects.
getDefaultTemplate () {
return [{
label: 'Atom',
submenu: [
{
label: 'Check for Update',
metadata: {autoUpdate: true}
},
{
label: 'Reload',
accelerator: 'Command+R',
click: () => {
const window = this.focusedWindow()
if (window) window.reload()
}
},
{
label: 'Close Window',
accelerator: 'Command+Shift+W',
click: () => {
const window = this.focusedWindow()
if (window) window.close()
}
},
{
label: 'Toggle Dev Tools',
accelerator: 'Command+Alt+I',
click: () => {
const window = this.focusedWindow()
if (window) window.toggleDevTools()
}
},
{
label: 'Quit',
accelerator: 'Command+Q',
click: () => app.quit()
}
]
}]
}
focusedWindow () {
return global.atomApplication.getAllWindows().find(window => window.isFocused())
}
// Combines a menu template with the appropriate keystroke.
//
// template - An Object conforming to atom-shell's menu api but lacking
// accelerator and click properties.
// keystrokesByCommand - An Object where the keys are commands and the values
// are Arrays containing the keystroke.
//
// Returns a complete menu configuration object for atom-shell's menu API.
translateTemplate (template, keystrokesByCommand) {
template.forEach(item => {
if (item.metadata == null) item.metadata = {}
if (item.command) {
item.accelerator = this.acceleratorForCommand(item.command, keystrokesByCommand)
item.click = () => global.atomApplication.sendCommand(item.command, item.commandDetail)
if (!/^application:/.test(item.command, item.commandDetail)) {
item.metadata.windowSpecific = true
}
}
if (item.submenu) this.translateTemplate(item.submenu, keystrokesByCommand)
})
return template
}
// Determine the accelerator for a given command.
//
// command - The name of the command.
// keystrokesByCommand - An Object where the keys are commands and the values
// are Arrays containing the keystroke.
//
// Returns a String containing the keystroke in a format that can be interpreted
// by Electron to provide nice icons where available.
acceleratorForCommand (command, keystrokesByCommand) {
const firstKeystroke = keystrokesByCommand[command] && keystrokesByCommand[command][0]
return MenuHelpers.acceleratorForKeystroke(firstKeystroke)
}
}

View File

@@ -1,917 +0,0 @@
AtomWindow = require './atom-window'
ApplicationMenu = require './application-menu'
AtomProtocolHandler = require './atom-protocol-handler'
AutoUpdateManager = require './auto-update-manager'
StorageFolder = require '../storage-folder'
Config = require '../config'
FileRecoveryService = require './file-recovery-service'
ipcHelpers = require '../ipc-helpers'
{BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron'
{CompositeDisposable, Disposable} = require 'event-kit'
fs = require 'fs-plus'
path = require 'path'
os = require 'os'
net = require 'net'
url = require 'url'
{EventEmitter} = require 'events'
_ = require 'underscore-plus'
FindParentDir = null
Resolve = null
ConfigSchema = require '../config-schema'
LocationSuffixRegExp = /(:\d+)(:\d+)?$/
# The application's singleton class.
#
# It's the entry point into the Atom application and maintains the global state
# of the application.
#
module.exports =
class AtomApplication
Object.assign @prototype, EventEmitter.prototype
# Public: The entry point into the Atom application.
@open: (options) ->
unless options.socketPath?
if process.platform is 'win32'
userNameSafe = new Buffer(process.env.USERNAME).toString('base64')
options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{userNameSafe}-#{process.arch}-sock"
else
options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.env.USER}.sock")
# FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely
# take a few seconds to trigger 'error' event, it could be a bug of node
# or atom-shell, before it's fixed we check the existence of socketPath to
# speedup startup.
if (process.platform isnt 'win32' and not fs.existsSync options.socketPath) or options.test or options.benchmark or options.benchmarkTest
new AtomApplication(options).initialize(options)
return
client = net.connect {path: options.socketPath}, ->
client.write JSON.stringify(options), ->
client.end()
app.quit()
client.on 'error', -> new AtomApplication(options).initialize(options)
windows: null
applicationMenu: null
atomProtocolHandler: null
resourcePath: null
version: null
quitting: false
exit: (status) -> app.exit(status)
constructor: (options) ->
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, @logFile, @userDataDir} = options
@socketPath = null if options.test or options.benchmark or options.benchmarkTest
@pidsToOpenWindows = {}
@windowStack = new WindowStack()
@config = new Config({enablePersistence: true})
@config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)}
ConfigSchema.projectHome = {
type: 'string',
default: path.join(fs.getHomeDirectory(), 'github'),
description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.'
}
@config.initialize({configDirPath: process.env.ATOM_HOME, @resourcePath, projectHomeSchema: ConfigSchema.projectHome})
@config.load()
@fileRecoveryService = new FileRecoveryService(path.join(process.env.ATOM_HOME, "recovery"))
@storageFolder = new StorageFolder(process.env.ATOM_HOME)
@autoUpdateManager = new AutoUpdateManager(
@version,
options.test or options.benchmark or options.benchmarkTest,
@config
)
@disposable = new CompositeDisposable
@handleEvents()
# This stuff was previously done in the constructor, but we want to be able to construct this object
# for testing purposes without booting up the world. As you add tests, feel free to move instantiation
# of these various sub-objects into the constructor, but you'll need to remove the side-effects they
# perform during their construction, adding an initialize method that you call here.
initialize: (options) ->
global.atomApplication = this
# DEPRECATED: This can be removed at some point (added in 1.13)
# It converts `useCustomTitleBar: true` to `titleBar: "custom"`
if process.platform is 'darwin' and @config.get('core.useCustomTitleBar')
@config.unset('core.useCustomTitleBar')
@config.set('core.titleBar', 'custom')
@config.onDidChange 'core.titleBar', @promptForRestart.bind(this)
process.nextTick => @autoUpdateManager.initialize()
@applicationMenu = new ApplicationMenu(@version, @autoUpdateManager)
@atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode)
@listenForArgumentsFromNewProcess()
@setupDockMenu()
@launch(options)
destroy: ->
windowsClosePromises = @getAllWindows().map (window) ->
window.close()
window.closedPromise
Promise.all(windowsClosePromises).then(=> @disposable.dispose())
launch: (options) ->
if options.test or options.benchmark or options.benchmarkTest
@openWithOptions(options)
else if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0
if @config.get('core.restorePreviousWindowsOnStart') is 'always'
@loadState(_.deepClone(options))
@openWithOptions(options)
else
@loadState(options) or @openPath(options)
openWithOptions: (options) ->
{
initialPaths, pathsToOpen, executedFrom, urlsToOpen, benchmark,
benchmarkTest, test, pidToKillWhenClosed, devMode, safeMode, newWindow,
logFile, profileStartup, timeout, clearWindowState, addToLastWindow, env
} = options
app.focus()
if test
@runTests({
headless: true, devMode, @resourcePath, executedFrom, pathsToOpen,
logFile, timeout, env
})
else if benchmark or benchmarkTest
@runBenchmarks({headless: true, test: benchmarkTest, @resourcePath, executedFrom, pathsToOpen, timeout, env})
else if pathsToOpen.length > 0
@openPaths({
initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow,
devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env
})
else if urlsToOpen.length > 0
for urlToOpen in urlsToOpen
@openUrl({urlToOpen, devMode, safeMode, env})
else
# Always open a editor window if this is the first instance of Atom.
@openPath({
initialPaths, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup,
clearWindowState, addToLastWindow, env
})
# Public: Removes the {AtomWindow} from the global window list.
removeWindow: (window) ->
@windowStack.removeWindow(window)
if @getAllWindows().length is 0
@applicationMenu?.enableWindowSpecificItems(false)
if process.platform in ['win32', 'linux']
app.quit()
return
@saveState(true) unless window.isSpec
# Public: Adds the {AtomWindow} to the global window list.
addWindow: (window) ->
@windowStack.addWindow(window)
@applicationMenu?.addWindow(window.browserWindow)
window.once 'window:loaded', =>
@autoUpdateManager?.emitUpdateAvailableEvent(window)
unless window.isSpec
focusHandler = => @windowStack.touch(window)
blurHandler = => @saveState(false)
window.browserWindow.on 'focus', focusHandler
window.browserWindow.on 'blur', blurHandler
window.browserWindow.once 'closed', =>
@windowStack.removeWindow(window)
window.browserWindow.removeListener 'focus', focusHandler
window.browserWindow.removeListener 'blur', blurHandler
window.browserWindow.webContents.once 'did-finish-load', => @saveState(false)
getAllWindows: =>
@windowStack.all().slice()
getLastFocusedWindow: (predicate) =>
@windowStack.getLastFocusedWindow(predicate)
# Creates server to listen for additional atom application launches.
#
# You can run the atom command multiple times, but after the first launch
# the other launches will just pass their information to this server and then
# close immediately.
listenForArgumentsFromNewProcess: ->
return unless @socketPath?
@deleteSocketFile()
server = net.createServer (connection) =>
data = ''
connection.on 'data', (chunk) ->
data = data + chunk
connection.on 'end', =>
options = JSON.parse(data)
@openWithOptions(options)
server.listen @socketPath
server.on 'error', (error) -> console.error 'Application server failed', error
deleteSocketFile: ->
return if process.platform is 'win32' or not @socketPath?
if fs.existsSync(@socketPath)
try
fs.unlinkSync(@socketPath)
catch error
# Ignore ENOENT errors in case the file was deleted between the exists
# check and the call to unlink sync. This occurred occasionally on CI
# which is why this check is here.
throw error unless error.code is 'ENOENT'
# Registers basic application commands, non-idempotent.
handleEvents: ->
getLoadSettings = =>
devMode: @focusedWindow()?.devMode
safeMode: @focusedWindow()?.safeMode
@on 'application:quit', -> app.quit()
@on 'application:new-window', -> @openPath(getLoadSettings())
@on 'application:new-file', -> (@focusedWindow() ? this).openPath()
@on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true)
@on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true)
@on 'application:inspect', ({x, y, atomWindow}) ->
atomWindow ?= @focusedWindow()
atomWindow?.browserWindow.inspectElement(x, y)
@on 'application:open-documentation', -> shell.openExternal('http://flight-manual.atom.io/')
@on 'application:open-discussions', -> shell.openExternal('https://discuss.atom.io')
@on 'application:open-faq', -> shell.openExternal('https://atom.io/faq')
@on 'application:open-terms-of-use', -> shell.openExternal('https://atom.io/terms')
@on 'application:report-issue', -> shell.openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#reporting-bugs')
@on 'application:search-issues', -> shell.openExternal('https://github.com/search?q=+is%3Aissue+user%3Aatom')
@on 'application:install-update', =>
@quitting = true
@autoUpdateManager.install()
@on 'application:check-for-update', => @autoUpdateManager.check()
if process.platform is 'darwin'
@on 'application:bring-all-windows-to-front', -> Menu.sendActionToFirstResponder('arrangeInFront:')
@on 'application:hide', -> Menu.sendActionToFirstResponder('hide:')
@on 'application:hide-other-applications', -> Menu.sendActionToFirstResponder('hideOtherApplications:')
@on 'application:minimize', -> Menu.sendActionToFirstResponder('performMiniaturize:')
@on 'application:unhide-all-applications', -> Menu.sendActionToFirstResponder('unhideAllApplications:')
@on 'application:zoom', -> Menu.sendActionToFirstResponder('zoom:')
else
@on 'application:minimize', -> @focusedWindow()?.minimize()
@on 'application:zoom', -> @focusedWindow()?.maximize()
@openPathOnEvent('application:about', 'atom://about')
@openPathOnEvent('application:show-settings', 'atom://config')
@openPathOnEvent('application:open-your-config', 'atom://.atom/config')
@openPathOnEvent('application:open-your-init-script', 'atom://.atom/init-script')
@openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap')
@openPathOnEvent('application:open-your-snippets', 'atom://.atom/snippets')
@openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet')
@openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
@disposable.add ipcHelpers.on app, 'before-quit', (event) =>
resolveBeforeQuitPromise = null
@lastBeforeQuitPromise = new Promise((resolve) -> resolveBeforeQuitPromise = resolve)
if @quitting
resolveBeforeQuitPromise()
else
event.preventDefault()
@quitting = true
windowUnloadPromises = @getAllWindows().map((window) -> window.prepareToUnload())
Promise.all(windowUnloadPromises).then((windowUnloadedResults) ->
didUnloadAllWindows = windowUnloadedResults.every((didUnloadWindow) -> didUnloadWindow)
app.quit() if didUnloadAllWindows
resolveBeforeQuitPromise()
)
@disposable.add ipcHelpers.on app, 'will-quit', =>
@killAllProcesses()
@deleteSocketFile()
@disposable.add ipcHelpers.on app, 'open-file', (event, pathToOpen) =>
event.preventDefault()
@openPath({pathToOpen})
@disposable.add ipcHelpers.on app, 'open-url', (event, urlToOpen) =>
event.preventDefault()
@openUrl({urlToOpen, @devMode, @safeMode})
@disposable.add ipcHelpers.on app, 'activate', (event, hasVisibleWindows) =>
unless hasVisibleWindows
event?.preventDefault()
@emit('application:new-window')
@disposable.add ipcHelpers.on ipcMain, 'restart-application', =>
@restart()
@disposable.add ipcHelpers.on ipcMain, 'resolve-proxy', (event, requestId, url) ->
event.sender.session.resolveProxy url, (proxy) ->
unless event.sender.isDestroyed()
event.sender.send('did-resolve-proxy', requestId, proxy)
@disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) =>
for atomWindow in @getAllWindows()
webContents = atomWindow.browserWindow.webContents
if webContents isnt event.sender
webContents.send('did-change-history-manager')
# A request from the associated render process to open a new render process.
@disposable.add ipcHelpers.on ipcMain, 'open', (event, options) =>
window = @atomWindowForEvent(event)
if options?
if typeof options.pathsToOpen is 'string'
options.pathsToOpen = [options.pathsToOpen]
if options.pathsToOpen?.length > 0
options.window = window
@openPaths(options)
else
new AtomWindow(this, @fileRecoveryService, options)
else
@promptForPathToOpen('all', {window})
@disposable.add ipcHelpers.on ipcMain, 'update-application-menu', (event, template, keystrokesByCommand) =>
win = BrowserWindow.fromWebContents(event.sender)
@applicationMenu?.update(win, template, keystrokesByCommand)
@disposable.add ipcHelpers.on ipcMain, 'run-package-specs', (event, packageSpecPath) =>
@runTests({resourcePath: @devResourcePath, pathsToOpen: [packageSpecPath], headless: false})
@disposable.add ipcHelpers.on ipcMain, 'run-benchmarks', (event, benchmarksPath) =>
@runBenchmarks({resourcePath: @devResourcePath, pathsToOpen: [benchmarksPath], headless: false, test: false})
@disposable.add ipcHelpers.on ipcMain, 'command', (event, command) =>
@emit(command)
@disposable.add ipcHelpers.on ipcMain, 'open-command', (event, command, args...) =>
defaultPath = args[0] if args.length > 0
switch command
when 'application:open' then @promptForPathToOpen('all', getLoadSettings(), defaultPath)
when 'application:open-file' then @promptForPathToOpen('file', getLoadSettings(), defaultPath)
when 'application:open-folder' then @promptForPathToOpen('folder', getLoadSettings(), defaultPath)
else console.log "Invalid open-command received: " + command
@disposable.add ipcHelpers.on ipcMain, 'window-command', (event, command, args...) ->
win = BrowserWindow.fromWebContents(event.sender)
win.emit(command, args...)
@disposable.add ipcHelpers.respondTo 'window-method', (browserWindow, method, args...) =>
@atomWindowForBrowserWindow(browserWindow)?[method](args...)
@disposable.add ipcHelpers.on ipcMain, 'pick-folder', (event, responseChannel) =>
@promptForPath "folder", (selectedPaths) ->
event.sender.send(responseChannel, selectedPaths)
@disposable.add ipcHelpers.respondTo 'set-window-size', (win, width, height) ->
win.setSize(width, height)
@disposable.add ipcHelpers.respondTo 'set-window-position', (win, x, y) ->
win.setPosition(x, y)
@disposable.add ipcHelpers.respondTo 'center-window', (win) ->
win.center()
@disposable.add ipcHelpers.respondTo 'focus-window', (win) ->
win.focus()
@disposable.add ipcHelpers.respondTo 'show-window', (win) ->
win.show()
@disposable.add ipcHelpers.respondTo 'hide-window', (win) ->
win.hide()
@disposable.add ipcHelpers.respondTo 'get-temporary-window-state', (win) ->
win.temporaryState
@disposable.add ipcHelpers.respondTo 'set-temporary-window-state', (win, state) ->
win.temporaryState = state
clipboard = require '../safe-clipboard'
@disposable.add ipcHelpers.on ipcMain, 'write-text-to-selection-clipboard', (event, selectedText) ->
clipboard.writeText(selectedText, 'selection')
@disposable.add ipcHelpers.on ipcMain, 'write-to-stdout', (event, output) ->
process.stdout.write(output)
@disposable.add ipcHelpers.on ipcMain, 'write-to-stderr', (event, output) ->
process.stderr.write(output)
@disposable.add ipcHelpers.on ipcMain, 'add-recent-document', (event, filename) ->
app.addRecentDocument(filename)
@disposable.add ipcHelpers.on ipcMain, 'execute-javascript-in-dev-tools', (event, code) ->
event.sender.devToolsWebContents?.executeJavaScript(code)
@disposable.add ipcHelpers.on ipcMain, 'get-auto-update-manager-state', (event) =>
event.returnValue = @autoUpdateManager.getState()
@disposable.add ipcHelpers.on ipcMain, 'get-auto-update-manager-error', (event) =>
event.returnValue = @autoUpdateManager.getErrorMessage()
@disposable.add ipcHelpers.on ipcMain, 'will-save-path', (event, path) =>
@fileRecoveryService.willSavePath(@atomWindowForEvent(event), path)
event.returnValue = true
@disposable.add ipcHelpers.on ipcMain, 'did-save-path', (event, path) =>
@fileRecoveryService.didSavePath(@atomWindowForEvent(event), path)
event.returnValue = true
@disposable.add ipcHelpers.on ipcMain, 'did-change-paths', =>
@saveState(false)
@disposable.add(@disableZoomOnDisplayChange())
setupDockMenu: ->
if process.platform is 'darwin'
dockMenu = Menu.buildFromTemplate [
{label: 'New Window', click: => @emit('application:new-window')}
]
app.dock.setMenu dockMenu
# Public: Executes the given command.
#
# If it isn't handled globally, delegate to the currently focused window.
#
# command - The string representing the command.
# args - The optional arguments to pass along.
sendCommand: (command, args...) ->
unless @emit(command, args...)
focusedWindow = @focusedWindow()
if focusedWindow?
focusedWindow.sendCommand(command, args...)
else
@sendCommandToFirstResponder(command)
# Public: Executes the given command on the given window.
#
# command - The string representing the command.
# atomWindow - The {AtomWindow} to send the command to.
# args - The optional arguments to pass along.
sendCommandToWindow: (command, atomWindow, args...) ->
unless @emit(command, args...)
if atomWindow?
atomWindow.sendCommand(command, args...)
else
@sendCommandToFirstResponder(command)
# Translates the command into macOS action and sends it to application's first
# responder.
sendCommandToFirstResponder: (command) ->
return false unless process.platform is 'darwin'
switch command
when 'core:undo' then Menu.sendActionToFirstResponder('undo:')
when 'core:redo' then Menu.sendActionToFirstResponder('redo:')
when 'core:copy' then Menu.sendActionToFirstResponder('copy:')
when 'core:cut' then Menu.sendActionToFirstResponder('cut:')
when 'core:paste' then Menu.sendActionToFirstResponder('paste:')
when 'core:select-all' then Menu.sendActionToFirstResponder('selectAll:')
else return false
true
# Public: Open the given path in the focused window when the event is
# triggered.
#
# A new window will be created if there is no currently focused window.
#
# eventName - The event to listen for.
# pathToOpen - The path to open when the event is triggered.
openPathOnEvent: (eventName, pathToOpen) ->
@on eventName, ->
if window = @focusedWindow()
window.openPath(pathToOpen)
else
@openPath({pathToOpen})
# Returns the {AtomWindow} for the given paths.
windowForPaths: (pathsToOpen, devMode) ->
_.find @getAllWindows(), (atomWindow) ->
atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen)
# Returns the {AtomWindow} for the given ipcMain event.
atomWindowForEvent: ({sender}) ->
@atomWindowForBrowserWindow(BrowserWindow.fromWebContents(sender))
atomWindowForBrowserWindow: (browserWindow) ->
@getAllWindows().find((atomWindow) -> atomWindow.browserWindow is browserWindow)
# Public: Returns the currently focused {AtomWindow} or undefined if none.
focusedWindow: ->
_.find @getAllWindows(), (atomWindow) -> atomWindow.isFocused()
# Get the platform-specific window offset for new windows.
getWindowOffsetForCurrentPlatform: ->
offsetByPlatform =
darwin: 22
win32: 26
offsetByPlatform[process.platform] ? 0
# Get the dimensions for opening a new window by cascading as appropriate to
# the platform.
getDimensionsForNewWindow: ->
return if (@focusedWindow() ? @getLastFocusedWindow())?.isMaximized()
dimensions = (@focusedWindow() ? @getLastFocusedWindow())?.getDimensions()
offset = @getWindowOffsetForCurrentPlatform()
if dimensions? and offset?
dimensions.x += offset
dimensions.y += offset
dimensions
# Public: Opens a single path, in an existing window if possible.
#
# options -
# :pathToOpen - The file path to open
# :pidToKillWhenClosed - The integer of the pid to kill
# :newWindow - Boolean of whether this should be opened in a new window.
# :devMode - Boolean to control the opened window's dev mode.
# :safeMode - Boolean to control the opened window's safe mode.
# :profileStartup - Boolean to control creating a profile of the startup time.
# :window - {AtomWindow} to open file paths in.
# :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPath: ({initialPaths, pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env} = {}) ->
@openPaths({initialPaths, pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env})
# Public: Opens multiple paths, in existing windows if possible.
#
# options -
# :pathsToOpen - The array of file paths to open
# :pidToKillWhenClosed - The integer of the pid to kill
# :newWindow - Boolean of whether this should be opened in a new window.
# :devMode - Boolean to control the opened window's dev mode.
# :safeMode - Boolean to control the opened window's safe mode.
# :windowDimensions - Object with height and width keys.
# :window - {AtomWindow} to open file paths in.
# :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPaths: ({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState, addToLastWindow, env}={}) ->
if not pathsToOpen? or pathsToOpen.length is 0
return
env = process.env unless env?
devMode = Boolean(devMode)
safeMode = Boolean(safeMode)
clearWindowState = Boolean(clearWindowState)
locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom, addToLastWindow) for pathToOpen in pathsToOpen)
pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen)
unless pidToKillWhenClosed or newWindow
existingWindow = @windowForPaths(pathsToOpen, devMode)
stats = (fs.statSyncNoException(pathToOpen) for pathToOpen in pathsToOpen)
unless existingWindow?
if currentWindow = window ? @getLastFocusedWindow()
existingWindow = currentWindow if (
addToLastWindow or
currentWindow.devMode is devMode and
(
stats.every((stat) -> stat.isFile?()) or
stats.some((stat) -> stat.isDirectory?() and not currentWindow.hasProjectPath())
)
)
if existingWindow?
openedWindow = existingWindow
openedWindow.openLocations(locationsToOpen)
if openedWindow.isMinimized()
openedWindow.restore()
else
openedWindow.focus()
openedWindow.replaceEnvironment(env)
else
if devMode
try
windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window'))
resourcePath = @devResourcePath
windowInitializationScript ?= require.resolve('../initialize-application-window')
resourcePath ?= @resourcePath
windowDimensions ?= @getDimensionsForNewWindow()
openedWindow = new AtomWindow(this, @fileRecoveryService, {initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env})
openedWindow.focus()
@windowStack.addWindow(openedWindow)
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
openedWindow.browserWindow.once 'closed', =>
@killProcessForWindow(openedWindow)
openedWindow
# Kill all processes associated with opened windows.
killAllProcesses: ->
@killProcess(pid) for pid of @pidsToOpenWindows
return
# Kill process associated with the given opened window.
killProcessForWindow: (openedWindow) ->
for pid, trackedWindow of @pidsToOpenWindows
@killProcess(pid) if trackedWindow is openedWindow
return
# Kill the process with the given pid.
killProcess: (pid) ->
try
parsedPid = parseInt(pid)
process.kill(parsedPid) if isFinite(parsedPid)
catch error
if error.code isnt 'ESRCH'
console.log("Killing process #{pid} failed: #{error.code ? error.message}")
delete @pidsToOpenWindows[pid]
saveState: (allowEmpty=false) ->
return if @quitting
states = []
for window in @getAllWindows()
unless window.isSpec
states.push({initialPaths: window.representedDirectoryPaths})
states.reverse()
if states.length > 0 or allowEmpty
@storageFolder.storeSync('application.json', states)
@emit('application:did-save-state')
loadState: (options) ->
if (@config.get('core.restorePreviousWindowsOnStart') in ['yes', 'always']) and (states = @storageFolder.load('application.json'))?.length > 0
for state in states
@openWithOptions(Object.assign(options, {
initialPaths: state.initialPaths
pathsToOpen: state.initialPaths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
urlsToOpen: []
devMode: @devMode
safeMode: @safeMode
}))
else
null
# Open an atom:// url.
#
# The host of the URL being opened is assumed to be the package name
# responsible for opening the URL. A new window will be created with
# that package's `urlMain` as the bootstrap script.
#
# options -
# :urlToOpen - The atom:// url to open.
# :devMode - Boolean to control the opened window's dev mode.
# :safeMode - Boolean to control the opened window's safe mode.
openUrl: ({urlToOpen, devMode, safeMode, env}) ->
parsedUrl = url.parse(urlToOpen, true)
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, parsedUrl, devMode, safeMode, env)
openPackageUriHandler: (url, parsedUrl, devMode, safeMode, env) ->
bestWindow = null
if parsedUrl.host is 'core'
predicate = require('../core-uri-handlers').windowPredicate(parsedUrl)
bestWindow = @getLastFocusedWindow (win) ->
not win.isSpecWindow() and predicate(win)
bestWindow ?= @getLastFocusedWindow (win) -> not win.isSpecWindow()
if bestWindow?
bestWindow.sendURIMessage url
bestWindow.focus()
else
resourcePath = @resourcePath
if devMode
try
windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window'))
resourcePath = @devResourcePath
windowInitializationScript ?= require.resolve('../initialize-application-window')
windowDimensions = @getDimensionsForNewWindow()
win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env})
@windowStack.addWindow(win)
win.on 'window:loaded', ->
win.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({})
@packages.initialize
configDirPath: process.env.ATOM_HOME
devMode: devMode
resourcePath: @resourcePath
@packages
# Opens up a new {AtomWindow} to run specs within.
#
# options -
# :headless - A Boolean that, if true, will close the window upon
# completion.
# :resourcePath - The path to include specs from.
# :specPath - The directory to load specs from.
# :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages
# and ~/.atom/dev/packages, defaults to false.
runTests: ({headless, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout, env}) ->
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
resourcePath = @resourcePath
timeoutInSeconds = Number.parseFloat(timeout)
unless Number.isNaN(timeoutInSeconds)
timeoutHandler = ->
console.log "The test suite has timed out because it has been running for more than #{timeoutInSeconds} seconds."
process.exit(124) # Use the same exit code as the UNIX timeout util.
setTimeout(timeoutHandler, timeoutInSeconds * 1000)
try
windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-test-window'))
catch error
windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window'))
testPaths = []
if pathsToOpen?
for pathToOpen in pathsToOpen
testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen)))
if testPaths.length is 0
process.stderr.write 'Error: Specify at least one test path\n\n'
process.exit(1)
legacyTestRunnerPath = @resolveLegacyTestRunnerPath()
testRunnerPath = @resolveTestRunnerPath(testPaths[0])
devMode = true
isSpec = true
safeMode ?= false
new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env})
runBenchmarks: ({headless, test, resourcePath, executedFrom, pathsToOpen, env}) ->
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
resourcePath = @resourcePath
try
windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-benchmark-window'))
catch error
windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-benchmark-window'))
benchmarkPaths = []
if pathsToOpen?
for pathToOpen in pathsToOpen
benchmarkPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen)))
if benchmarkPaths.length is 0
process.stderr.write 'Error: Specify at least one benchmark path.\n\n'
process.exit(1)
devMode = true
isSpec = true
safeMode = false
new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, test, isSpec, devMode, benchmarkPaths, safeMode, env})
resolveTestRunnerPath: (testPath) ->
FindParentDir ?= require 'find-parent-dir'
if packageRoot = FindParentDir.sync(testPath, 'package.json')
packageMetadata = require(path.join(packageRoot, 'package.json'))
if packageMetadata.atomTestRunner
Resolve ?= require('resolve')
if testRunnerPath = Resolve.sync(packageMetadata.atomTestRunner, basedir: packageRoot, extensions: Object.keys(require.extensions))
return testRunnerPath
else
process.stderr.write "Error: Could not resolve test runner path '#{packageMetadata.atomTestRunner}'"
process.exit(1)
@resolveLegacyTestRunnerPath()
resolveLegacyTestRunnerPath: ->
try
require.resolve(path.resolve(@devResourcePath, 'spec', 'jasmine-test-runner'))
catch error
require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner'))
locationForPathToOpen: (pathToOpen, executedFrom='', forceAddToWindow) ->
return {pathToOpen} unless pathToOpen
pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
match = pathToOpen.match(LocationSuffixRegExp)
if match?
pathToOpen = pathToOpen.slice(0, -match[0].length)
initialLine = Math.max(0, parseInt(match[1].slice(1)) - 1) if match[1]
initialColumn = Math.max(0, parseInt(match[2].slice(1)) - 1) if match[2]
else
initialLine = initialColumn = null
unless url.parse(pathToOpen).protocol?
pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen))
{pathToOpen, initialLine, initialColumn, forceAddToWindow}
# Opens a native dialog to prompt the user for a path.
#
# Once paths are selected, they're opened in a new or existing {AtomWindow}s.
#
# options -
# :type - A String which specifies the type of the dialog, could be 'file',
# 'folder' or 'all'. The 'all' is only available on macOS.
# :devMode - A Boolean which controls whether any newly opened windows
# should be in dev mode or not.
# :safeMode - A Boolean which controls whether any newly opened windows
# should be in safe mode or not.
# :window - An {AtomWindow} to use for opening a selected file path.
# :path - An optional String which controls the default path to which the
# file dialog opens.
promptForPathToOpen: (type, {devMode, safeMode, window}, path=null) ->
@promptForPath type, ((pathsToOpen) =>
@openPaths({pathsToOpen, devMode, safeMode, window})), path
promptForPath: (type, callback, path) ->
properties =
switch type
when 'file' then ['openFile']
when 'folder' then ['openDirectory']
when 'all' then ['openFile', 'openDirectory']
else throw new Error("#{type} is an invalid type for promptForPath")
# Show the open dialog as child window on Windows and Linux, and as
# independent dialog on macOS. This matches most native apps.
parentWindow =
if process.platform is 'darwin'
null
else
BrowserWindow.getFocusedWindow()
openOptions =
properties: properties.concat(['multiSelections', 'createDirectory'])
title: switch type
when 'file' then 'Open File'
when 'folder' then 'Open Folder'
else 'Open'
# File dialog defaults to project directory of currently active editor
if path?
openOptions.defaultPath = path
dialog.showOpenDialog(parentWindow, openOptions, callback)
promptForRestart: ->
chosen = dialog.showMessageBox BrowserWindow.getFocusedWindow(),
type: 'warning'
title: 'Restart required'
message: "You will need to restart Atom for this change to take effect."
buttons: ['Restart Atom', 'Cancel']
if chosen is 0
@restart()
restart: ->
args = []
args.push("--safe") if @safeMode
args.push("--log-file=#{@logFile}") if @logFile?
args.push("--socket-path=#{@socketPath}") if @socketPath?
args.push("--user-data-dir=#{@userDataDir}") if @userDataDir?
if @devMode
args.push('--dev')
args.push("--resource-path=#{@resourcePath}")
app.relaunch({args})
app.quit()
disableZoomOnDisplayChange: ->
outerCallback = =>
for window in @getAllWindows()
window.disableZoom()
# Set the limits every time a display is added or removed, otherwise the
# configuration gets reset to the default, which allows zooming the
# webframe.
screen.on('display-added', outerCallback)
screen.on('display-removed', outerCallback)
new Disposable ->
screen.removeListener('display-added', outerCallback)
screen.removeListener('display-removed', outerCallback)
class WindowStack
constructor: (@windows = []) ->
addWindow: (window) =>
@removeWindow(window)
@windows.unshift(window)
touch: (window) =>
@addWindow(window)
removeWindow: (window) =>
currentIndex = @windows.indexOf(window)
@windows.splice(currentIndex, 1) if currentIndex > -1
getLastFocusedWindow: (predicate) =>
predicate ?= (win) -> true
@windows.find(predicate)
all: =>
@windows

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +0,0 @@
{protocol} = require 'electron'
fs = require 'fs'
path = require 'path'
# Handles requests with 'atom' protocol.
#
# It's created by {AtomApplication} upon instantiation and is used to create a
# custom resource loader for 'atom://' URLs.
#
# The following directories are searched in order:
# * ~/.atom/assets
# * ~/.atom/dev/packages (unless in safe mode)
# * ~/.atom/packages
# * RESOURCE_PATH/node_modules
#
module.exports =
class AtomProtocolHandler
constructor: (resourcePath, safeMode) ->
@loadPaths = []
unless safeMode
@loadPaths.push(path.join(process.env.ATOM_HOME, 'dev', 'packages'))
@loadPaths.push(path.join(process.env.ATOM_HOME, 'packages'))
@loadPaths.push(path.join(resourcePath, 'node_modules'))
@registerAtomProtocol()
# Creates the 'atom' custom protocol handler.
registerAtomProtocol: ->
protocol.registerFileProtocol 'atom', (request, callback) =>
relativePath = path.normalize(request.url.substr(7))
if relativePath.indexOf('assets/') is 0
assetsPath = path.join(process.env.ATOM_HOME, relativePath)
filePath = assetsPath if fs.statSyncNoException(assetsPath).isFile?()
unless filePath
for loadPath in @loadPaths
filePath = path.join(loadPath, relativePath)
break if fs.statSyncNoException(filePath).isFile?()
callback(filePath)

View File

@@ -0,0 +1,54 @@
const {protocol} = require('electron')
const fs = require('fs')
const path = require('path')
// Handles requests with 'atom' protocol.
//
// It's created by {AtomApplication} upon instantiation and is used to create a
// custom resource loader for 'atom://' URLs.
//
// The following directories are searched in order:
// * ~/.atom/assets
// * ~/.atom/dev/packages (unless in safe mode)
// * ~/.atom/packages
// * RESOURCE_PATH/node_modules
//
module.exports =
class AtomProtocolHandler {
constructor (resourcePath, safeMode) {
this.loadPaths = []
if (!safeMode) {
this.loadPaths.push(path.join(process.env.ATOM_HOME, 'dev', 'packages'))
}
this.loadPaths.push(path.join(process.env.ATOM_HOME, 'packages'))
this.loadPaths.push(path.join(resourcePath, 'node_modules'))
this.registerAtomProtocol()
}
// Creates the 'atom' custom protocol handler.
registerAtomProtocol () {
protocol.registerFileProtocol('atom', (request, callback) => {
const relativePath = path.normalize(request.url.substr(7))
let filePath
if (relativePath.indexOf('assets/') === 0) {
const assetsPath = path.join(process.env.ATOM_HOME, relativePath)
const stat = fs.statSyncNoException(assetsPath)
if (stat && stat.isFile()) filePath = assetsPath
}
if (!filePath) {
for (let loadPath of this.loadPaths) {
filePath = path.join(loadPath, relativePath)
const stat = fs.statSyncNoException(filePath)
if (stat && stat.isFile()) break
}
}
callback(filePath)
})
}
}

View File

@@ -1,329 +0,0 @@
{BrowserWindow, app, dialog, ipcMain} = require 'electron'
path = require 'path'
fs = require 'fs'
url = require 'url'
{EventEmitter} = require 'events'
module.exports =
class AtomWindow
Object.assign @prototype, EventEmitter.prototype
@iconPath: path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
@includeShellLoadTime: true
browserWindow: null
loaded: null
isSpec: null
constructor: (@atomApplication, @fileRecoveryService, settings={}) ->
{@resourcePath, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings
locationsToOpen ?= [{pathToOpen}] if pathToOpen
locationsToOpen ?= []
@loadedPromise = new Promise((@resolveLoadedPromise) =>)
@closedPromise = new Promise((@resolveClosedPromise) =>)
options =
show: false
title: 'Atom'
tabbingIdentifier: 'atom'
webPreferences:
# Prevent specs from throttling when the window is in the background:
# this should result in faster CI builds, and an improvement in the
# local development experience when running specs through the UI (which
# now won't pause when e.g. minimizing the window).
backgroundThrottling: not @isSpec
# Disable the `auxclick` feature so that `click` events are triggered in
# response to a middle-click.
# (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960)
disableBlinkFeatures: 'Auxclick'
# Don't set icon on Windows so the exe's ico will be used as window and
# taskbar's icon. See https://github.com/atom/atom/issues/4811 for more.
if process.platform is 'linux'
options.icon = @constructor.iconPath
if @shouldAddCustomTitleBar()
options.titleBarStyle = 'hidden'
if @shouldAddCustomInsetTitleBar()
options.titleBarStyle = 'hidden-inset'
if @shouldHideTitleBar()
options.frame = false
@browserWindow = new BrowserWindow(options)
@handleEvents()
@loadSettings = Object.assign({}, settings)
@loadSettings.appVersion = app.getVersion()
@loadSettings.resourcePath = @resourcePath
@loadSettings.devMode ?= false
@loadSettings.safeMode ?= false
@loadSettings.atomHome = process.env.ATOM_HOME
@loadSettings.clearWindowState ?= false
@loadSettings.initialPaths ?=
for {pathToOpen} in locationsToOpen when pathToOpen
stat = fs.statSyncNoException(pathToOpen) or null
if stat?.isDirectory()
pathToOpen
else
parentDirectory = path.dirname(pathToOpen)
if stat?.isFile() or fs.existsSync(parentDirectory)
parentDirectory
else
pathToOpen
@loadSettings.initialPaths.sort()
# Only send to the first non-spec window created
if @constructor.includeShellLoadTime and not @isSpec
@constructor.includeShellLoadTime = false
@loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
@representedDirectoryPaths = @loadSettings.initialPaths
@env = @loadSettings.env if @loadSettings.env?
@browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings)
@browserWindow.on 'window:loaded', =>
@disableZoom()
@emit 'window:loaded'
@resolveLoadedPromise()
@browserWindow.on 'window:locations-opened', =>
@emit 'window:locations-opened'
@browserWindow.on 'enter-full-screen', =>
@browserWindow.webContents.send('did-enter-full-screen')
@browserWindow.on 'leave-full-screen', =>
@browserWindow.webContents.send('did-leave-full-screen')
@browserWindow.loadURL url.format
protocol: 'file'
pathname: "#{@resourcePath}/static/index.html"
slashes: true
@browserWindow.showSaveDialog = @showSaveDialog.bind(this)
@browserWindow.focusOnWebView() if @isSpec
@browserWindow.temporaryState = {windowDimensions} if windowDimensions?
hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?)
@openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow()
@atomApplication.addWindow(this)
hasProjectPath: -> @representedDirectoryPaths.length > 0
setupContextMenu: ->
ContextMenu = require './context-menu'
@browserWindow.on 'context-menu', (menuTemplate) =>
new ContextMenu(menuTemplate, this)
containsPaths: (paths) ->
for pathToCheck in paths
return false unless @containsPath(pathToCheck)
true
containsPath: (pathToCheck) ->
@representedDirectoryPaths.some (projectPath) ->
if not projectPath
false
else if not pathToCheck
false
else if pathToCheck is projectPath
true
else if fs.statSyncNoException(pathToCheck).isDirectory?()
false
else if pathToCheck.indexOf(path.join(projectPath, path.sep)) is 0
true
else
false
handleEvents: ->
@browserWindow.on 'close', (event) =>
unless @atomApplication.quitting or @unloading
event.preventDefault()
@unloading = true
@atomApplication.saveState(false)
@prepareToUnload().then (result) =>
@close() if result
@browserWindow.on 'closed', =>
@fileRecoveryService.didCloseWindow(this)
@atomApplication.removeWindow(this)
@resolveClosedPromise()
@browserWindow.on 'unresponsive', =>
return if @isSpec
chosen = dialog.showMessageBox @browserWindow,
type: 'warning'
buttons: ['Force Close', 'Keep Waiting']
message: 'Editor is not responding'
detail: 'The editor is not responding. Would you like to force close it or just keep waiting?'
@browserWindow.destroy() if chosen is 0
@browserWindow.webContents.on 'crashed', =>
if @headless
console.log "Renderer process crashed, exiting"
@atomApplication.exit(100)
return
@fileRecoveryService.didCrashWindow(this)
chosen = dialog.showMessageBox @browserWindow,
type: 'warning'
buttons: ['Close Window', 'Reload', 'Keep It Open']
message: 'The editor has crashed'
detail: 'Please report this issue to https://github.com/atom/atom'
switch chosen
when 0 then @browserWindow.destroy()
when 1 then @browserWindow.reload()
@browserWindow.webContents.on 'will-navigate', (event, url) =>
unless url is @browserWindow.webContents.getURL()
event.preventDefault()
@setupContextMenu()
if @isSpec
# Spec window's web view should always have focus
@browserWindow.on 'blur', =>
@browserWindow.focusOnWebView()
prepareToUnload: ->
if @isSpecWindow()
return Promise.resolve(true)
@lastPrepareToUnloadPromise = new Promise (resolve) =>
callback = (event, result) =>
if BrowserWindow.fromWebContents(event.sender) is @browserWindow
ipcMain.removeListener('did-prepare-to-unload', callback)
unless result
@unloading = false
@atomApplication.quitting = false
resolve(result)
ipcMain.on('did-prepare-to-unload', callback)
@browserWindow.webContents.send('prepare-to-unload')
openPath: (pathToOpen, initialLine, initialColumn) ->
@openLocations([{pathToOpen, initialLine, initialColumn}])
openLocations: (locationsToOpen) ->
@loadedPromise.then => @sendMessage 'open-locations', locationsToOpen
replaceEnvironment: (env) ->
@browserWindow.webContents.send 'environment', env
sendMessage: (message, detail) ->
@browserWindow.webContents.send 'message', message, detail
sendCommand: (command, args...) ->
if @isSpecWindow()
unless @atomApplication.sendCommandToFirstResponder(command)
switch command
when 'window:reload' then @reload()
when 'window:toggle-dev-tools' then @toggleDevTools()
when 'window:close' then @close()
else if @isWebViewFocused()
@sendCommandToBrowserWindow(command, args...)
else
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...
getDimensions: ->
[x, y] = @browserWindow.getPosition()
[width, height] = @browserWindow.getSize()
{x, y, width, height}
shouldAddCustomTitleBar: ->
not @isSpec and
process.platform is 'darwin' and
@atomApplication.config.get('core.titleBar') is 'custom'
shouldAddCustomInsetTitleBar: ->
not @isSpec and
process.platform is 'darwin' and
@atomApplication.config.get('core.titleBar') is 'custom-inset'
shouldHideTitleBar: ->
not @isSpec and
process.platform is 'darwin' and
@atomApplication.config.get('core.titleBar') is 'hidden'
close: -> @browserWindow.close()
focus: -> @browserWindow.focus()
minimize: -> @browserWindow.minimize()
maximize: -> @browserWindow.maximize()
unmaximize: -> @browserWindow.unmaximize()
restore: -> @browserWindow.restore()
setFullScreen: (fullScreen) -> @browserWindow.setFullScreen(fullScreen)
setAutoHideMenuBar: (autoHideMenuBar) -> @browserWindow.setAutoHideMenuBar(autoHideMenuBar)
handlesAtomCommands: ->
not @isSpecWindow() and @isWebViewFocused()
isFocused: -> @browserWindow.isFocused()
isMaximized: -> @browserWindow.isMaximized()
isMinimized: -> @browserWindow.isMinimized()
isWebViewFocused: -> @browserWindow.isWebViewFocused()
isSpecWindow: -> @isSpec
reload: ->
@loadedPromise = new Promise((@resolveLoadedPromise) =>)
@prepareToUnload().then (result) =>
@browserWindow.reload() if result
@loadedPromise
showSaveDialog: (options, callback) ->
options = Object.assign({
title: 'Save File',
defaultPath: @representedDirectoryPaths[0]
}, options)
if callback?
# Async
dialog.showSaveDialog(@browserWindow, options, callback)
else
# Sync
dialog.showSaveDialog(@browserWindow, options)
toggleDevTools: -> @browserWindow.toggleDevTools()
openDevTools: -> @browserWindow.openDevTools()
closeDevTools: -> @browserWindow.closeDevTools()
setDocumentEdited: (documentEdited) -> @browserWindow.setDocumentEdited(documentEdited)
setRepresentedFilename: (representedFilename) -> @browserWindow.setRepresentedFilename(representedFilename)
setRepresentedDirectoryPaths: (@representedDirectoryPaths) ->
@representedDirectoryPaths.sort()
@loadSettings.initialPaths = @representedDirectoryPaths
@browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings)
@atomApplication.saveState()
copy: -> @browserWindow.copy()
disableZoom: ->
@browserWindow.webContents.setVisualZoomLevelLimits(1, 1)

View File

@@ -0,0 +1,432 @@
const {BrowserWindow, app, dialog, ipcMain} = require('electron')
const path = require('path')
const fs = require('fs')
const url = require('url')
const {EventEmitter} = require('events')
const ICON_PATH = path.resolve(__dirname, '..', '..', 'resources', 'atom.png')
let includeShellLoadTime = true
let nextId = 0
module.exports =
class AtomWindow extends EventEmitter {
constructor (atomApplication, fileRecoveryService, settings = {}) {
super()
this.id = nextId++
this.atomApplication = atomApplication
this.fileRecoveryService = fileRecoveryService
this.isSpec = settings.isSpec
this.headless = settings.headless
this.safeMode = settings.safeMode
this.devMode = settings.devMode
this.resourcePath = settings.resourcePath
let {pathToOpen, locationsToOpen} = settings
if (!locationsToOpen && pathToOpen) locationsToOpen = [{pathToOpen}]
if (!locationsToOpen) locationsToOpen = []
this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve })
this.closedPromise = new Promise(resolve => { this.resolveClosedPromise = resolve })
const options = {
show: false,
title: 'Atom',
tabbingIdentifier: 'atom',
webPreferences: {
// Prevent specs from throttling when the window is in the background:
// this should result in faster CI builds, and an improvement in the
// local development experience when running specs through the UI (which
// now won't pause when e.g. minimizing the window).
backgroundThrottling: !this.isSpec,
// Disable the `auxclick` feature so that `click` events are triggered in
// response to a middle-click.
// (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960)
disableBlinkFeatures: 'Auxclick'
}
}
// Don't set icon on Windows so the exe's ico will be used as window and
// taskbar's icon. See https://github.com/atom/atom/issues/4811 for more.
if (process.platform === 'linux') options.icon = ICON_PATH
if (this.shouldAddCustomTitleBar()) options.titleBarStyle = 'hidden'
if (this.shouldAddCustomInsetTitleBar()) options.titleBarStyle = 'hidden-inset'
if (this.shouldHideTitleBar()) options.frame = false
this.browserWindow = new BrowserWindow(options)
this.handleEvents()
this.loadSettings = Object.assign({}, settings)
this.loadSettings.appVersion = app.getVersion()
this.loadSettings.resourcePath = this.resourcePath
this.loadSettings.atomHome = process.env.ATOM_HOME
if (this.loadSettings.devMode == null) this.loadSettings.devMode = false
if (this.loadSettings.safeMode == null) this.loadSettings.safeMode = false
if (this.loadSettings.clearWindowState == null) this.loadSettings.clearWindowState = false
if (!this.loadSettings.initialPaths) {
this.loadSettings.initialPaths = []
for (const {pathToOpen} of locationsToOpen) {
if (!pathToOpen) continue
const stat = fs.statSyncNoException(pathToOpen) || null
if (stat && stat.isDirectory()) {
this.loadSettings.initialPaths.push(pathToOpen)
} else {
const parentDirectory = path.dirname(pathToOpen)
if ((stat && stat.isFile()) || fs.existsSync(parentDirectory)) {
this.loadSettings.initialPaths.push(parentDirectory)
} else {
this.loadSettings.initialPaths.push(pathToOpen)
}
}
}
}
this.loadSettings.initialPaths.sort()
// Only send to the first non-spec window created
if (includeShellLoadTime && !this.isSpec) {
includeShellLoadTime = false
if (!this.loadSettings.shellLoadTime) {
this.loadSettings.shellLoadTime = Date.now() - global.shellStartTime
}
}
this.representedDirectoryPaths = this.loadSettings.initialPaths
if (!this.loadSettings.env) this.env = this.loadSettings.env
this.browserWindow.loadSettingsJSON = JSON.stringify(this.loadSettings)
this.browserWindow.on('window:loaded', () => {
this.disableZoom()
this.emit('window:loaded')
this.resolveLoadedPromise()
})
this.browserWindow.on('window:locations-opened', () => {
this.emit('window:locations-opened')
})
this.browserWindow.on('enter-full-screen', () => {
this.browserWindow.webContents.send('did-enter-full-screen')
})
this.browserWindow.on('leave-full-screen', () => {
this.browserWindow.webContents.send('did-leave-full-screen')
})
this.browserWindow.loadURL(
url.format({
protocol: 'file',
pathname: `${this.resourcePath}/static/index.html`,
slashes: true
})
)
this.browserWindow.showSaveDialog = this.showSaveDialog.bind(this)
if (this.isSpec) this.browserWindow.focusOnWebView()
const hasPathToOpen = !(locationsToOpen.length === 1 && locationsToOpen[0].pathToOpen == null)
if (hasPathToOpen && !this.isSpecWindow()) this.openLocations(locationsToOpen)
}
hasProjectPath () {
return this.representedDirectoryPaths.length > 0
}
setupContextMenu () {
const ContextMenu = require('./context-menu')
this.browserWindow.on('context-menu', menuTemplate => {
return new ContextMenu(menuTemplate, this)
})
}
containsPaths (paths) {
return paths.every(p => this.containsPath(p))
}
containsPath (pathToCheck) {
if (!pathToCheck) return false
const stat = fs.statSyncNoException(pathToCheck)
if (stat && stat.isDirectory()) return false
return this.representedDirectoryPaths.some(projectPath =>
pathToCheck === projectPath || pathToCheck.startsWith(path.join(projectPath, path.sep))
)
}
handleEvents () {
this.browserWindow.on('close', async event => {
if (!this.atomApplication.quitting && !this.unloading) {
event.preventDefault()
this.unloading = true
this.atomApplication.saveState(false)
if (await this.prepareToUnload()) this.close()
}
})
this.browserWindow.on('closed', () => {
this.fileRecoveryService.didCloseWindow(this)
this.atomApplication.removeWindow(this)
this.resolveClosedPromise()
})
this.browserWindow.on('unresponsive', () => {
if (this.isSpec) return
const chosen = dialog.showMessageBox(this.browserWindow, {
type: 'warning',
buttons: ['Force Close', 'Keep Waiting'],
message: 'Editor is not responding',
detail:
'The editor is not responding. Would you like to force close it or just keep waiting?'
})
if (chosen === 0) this.browserWindow.destroy()
})
this.browserWindow.webContents.on('crashed', () => {
if (this.headless) {
console.log('Renderer process crashed, exiting')
this.atomApplication.exit(100)
return
}
this.fileRecoveryService.didCrashWindow(this)
const chosen = dialog.showMessageBox(this.browserWindow, {
type: 'warning',
buttons: ['Close Window', 'Reload', 'Keep It Open'],
message: 'The editor has crashed',
detail: 'Please report this issue to https://github.com/atom/atom'
})
switch (chosen) {
case 0: return this.browserWindow.destroy()
case 1: return this.browserWindow.reload()
}
})
this.browserWindow.webContents.on('will-navigate', (event, url) => {
if (url !== this.browserWindow.webContents.getURL()) event.preventDefault()
})
this.setupContextMenu()
// Spec window's web view should always have focus
if (this.isSpec) this.browserWindow.on('blur', () => this.browserWindow.focusOnWebView())
}
async prepareToUnload () {
if (this.isSpecWindow()) return true
this.lastPrepareToUnloadPromise = new Promise(resolve => {
const callback = (event, result) => {
if (BrowserWindow.fromWebContents(event.sender) === this.browserWindow) {
ipcMain.removeListener('did-prepare-to-unload', callback)
if (!result) {
this.unloading = false
this.atomApplication.quitting = false
}
resolve(result)
}
}
ipcMain.on('did-prepare-to-unload', callback)
this.browserWindow.webContents.send('prepare-to-unload')
})
return this.lastPrepareToUnloadPromise
}
openPath (pathToOpen, initialLine, initialColumn) {
return this.openLocations([{pathToOpen, initialLine, initialColumn}])
}
async openLocations (locationsToOpen) {
await this.loadedPromise
this.sendMessage('open-locations', locationsToOpen)
}
replaceEnvironment (env) {
this.browserWindow.webContents.send('environment', env)
}
sendMessage (message, detail) {
this.browserWindow.webContents.send('message', message, detail)
}
sendCommand (command, ...args) {
if (this.isSpecWindow()) {
if (!this.atomApplication.sendCommandToFirstResponder(command)) {
switch (command) {
case 'window:reload': return this.reload()
case 'window:toggle-dev-tools': return this.toggleDevTools()
case 'window:close': return this.close()
}
}
} else if (this.isWebViewFocused()) {
this.sendCommandToBrowserWindow(command, ...args)
} else if (!this.atomApplication.sendCommandToFirstResponder(command)) {
this.sendCommandToBrowserWindow(command, ...args)
}
}
sendURIMessage (uri) {
this.browserWindow.webContents.send('uri-message', uri)
}
sendCommandToBrowserWindow (command, ...args) {
const action = args[0] && args[0].contextCommand
? 'context-command'
: 'command'
this.browserWindow.webContents.send(action, command, ...args)
}
getDimensions () {
const [x, y] = Array.from(this.browserWindow.getPosition())
const [width, height] = Array.from(this.browserWindow.getSize())
return {x, y, width, height}
}
shouldAddCustomTitleBar () {
return (
!this.isSpec &&
process.platform === 'darwin' &&
this.atomApplication.config.get('core.titleBar') === 'custom'
)
}
shouldAddCustomInsetTitleBar () {
return (
!this.isSpec &&
process.platform === 'darwin' &&
this.atomApplication.config.get('core.titleBar') === 'custom-inset'
)
}
shouldHideTitleBar () {
return (
!this.isSpec &&
process.platform === 'darwin' &&
this.atomApplication.config.get('core.titleBar') === 'hidden'
)
}
close () {
return this.browserWindow.close()
}
focus () {
return this.browserWindow.focus()
}
minimize () {
return this.browserWindow.minimize()
}
maximize () {
return this.browserWindow.maximize()
}
unmaximize () {
return this.browserWindow.unmaximize()
}
restore () {
return this.browserWindow.restore()
}
setFullScreen (fullScreen) {
return this.browserWindow.setFullScreen(fullScreen)
}
setAutoHideMenuBar (autoHideMenuBar) {
return this.browserWindow.setAutoHideMenuBar(autoHideMenuBar)
}
handlesAtomCommands () {
return !this.isSpecWindow() && this.isWebViewFocused()
}
isFocused () {
return this.browserWindow.isFocused()
}
isMaximized () {
return this.browserWindow.isMaximized()
}
isMinimized () {
return this.browserWindow.isMinimized()
}
isWebViewFocused () {
return this.browserWindow.isWebViewFocused()
}
isSpecWindow () {
return this.isSpec
}
reload () {
this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve })
this.prepareToUnload().then(canUnload => {
if (canUnload) this.browserWindow.reload()
})
return this.loadedPromise
}
showSaveDialog (options, callback) {
options = Object.assign({
title: 'Save File',
defaultPath: this.representedDirectoryPaths[0]
}, options)
if (typeof callback === 'function') {
// Async
dialog.showSaveDialog(this.browserWindow, options, callback)
} else {
// Sync
return dialog.showSaveDialog(this.browserWindow, options)
}
}
toggleDevTools () {
return this.browserWindow.toggleDevTools()
}
openDevTools () {
return this.browserWindow.openDevTools()
}
closeDevTools () {
return this.browserWindow.closeDevTools()
}
setDocumentEdited (documentEdited) {
return this.browserWindow.setDocumentEdited(documentEdited)
}
setRepresentedFilename (representedFilename) {
return this.browserWindow.setRepresentedFilename(representedFilename)
}
setRepresentedDirectoryPaths (representedDirectoryPaths) {
this.representedDirectoryPaths = representedDirectoryPaths
this.representedDirectoryPaths.sort()
this.loadSettings.initialPaths = this.representedDirectoryPaths
this.browserWindow.loadSettingsJSON = JSON.stringify(this.loadSettings)
return this.atomApplication.saveState()
}
didClosePathWithWaitSession (path) {
this.atomApplication.windowDidClosePathWithWaitSession(this, path)
}
copy () {
return this.browserWindow.copy()
}
disableZoom () {
return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1)
}
}

View File

@@ -27,6 +27,15 @@ class NotificationManager {
return this.emitter.on('did-add-notification', callback)
}
// Public: Invoke the given callback after the notifications have been cleared.
//
// * `callback` {Function} to be called after the notifications are cleared.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidClearNotifications (callback) {
return this.emitter.on('did-clear-notifications', callback)
}
/*
Section: Adding Notifications
*/
@@ -200,7 +209,9 @@ class NotificationManager {
Section: Managing Notifications
*/
// Public: Clear all the notifications.
clear () {
this.notifications = []
this.emitter.emit('did-clear-notifications')
}
}

View File

@@ -9,8 +9,12 @@ class PaneResizeHandleElement extends HTMLElement
@addEventListener 'mousedown', @resizeStarted.bind(this)
attachedCallback: ->
@isHorizontal = @parentElement.classList.contains("horizontal")
@classList.add if @isHorizontal then 'horizontal' else 'vertical'
# For some reason Chromium 58 is firing the attached callback after the
# element has been detached, so we ignore the callback when a parent element
# can't be found.
if @parentElement
@isHorizontal = @parentElement.classList.contains("horizontal")
@classList.add if @isHorizontal then 'horizontal' else 'vertical'
detachedCallback: ->
@resizeStopped()

View File

@@ -63,7 +63,7 @@ class ProtocolHandlerInstaller {
notification = notifications.addInfo('Register as default atom:// URI handler?', {
dismissable: true,
icon: 'link',
description: 'Atom is not currently set as the defaut handler for atom:// URIs. Would you like Atom to handle ' +
description: 'Atom is not currently set as the default handler for atom:// URIs. Would you like Atom to handle ' +
'atom:// URIs?',
buttons: [
{

View File

@@ -160,6 +160,8 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
'editor:select-to-previous-subword-boundary': -> @selectToPreviousSubwordBoundary()
'editor:select-to-first-character-of-line': -> @selectToFirstCharacterOfLine()
'editor:select-line': -> @selectLinesContainingCursors()
'editor:select-larger-syntax-node': -> @selectLargerSyntaxNode()
'editor:select-smaller-syntax-node': -> @selectSmallerSyntaxNode()
}),
false
)

View File

@@ -39,11 +39,17 @@ class ScopeDescriptor
getScopesArray: -> @scopes
getScopeChain: ->
@scopes
.map (scope) ->
scope = ".#{scope}" unless scope[0] is '.'
scope
.join(' ')
# For backward compatibility, prefix TextMate-style scope names with
# leading dots (e.g. 'source.js' -> '.source.js').
if @scopes[0].includes('.')
result = ''
for scope, i in @scopes
result += ' ' if i > 0
result += '.' if scope[0] isnt '.'
result += scope
result
else
@scopes.join(' ')
toString: ->
@getScopeChain()

178
src/syntax-scope-map.js Normal file
View File

@@ -0,0 +1,178 @@
const parser = require('postcss-selector-parser')
module.exports =
class SyntaxScopeMap {
constructor (scopeNamesBySelector) {
this.namedScopeTable = {}
this.anonymousScopeTable = {}
for (let selector in scopeNamesBySelector) {
this.addSelector(selector, scopeNamesBySelector[selector])
}
setTableDefaults(this.namedScopeTable)
setTableDefaults(this.anonymousScopeTable)
}
addSelector (selector, scopeName) {
parser((parseResult) => {
for (let selectorNode of parseResult.nodes) {
let currentTable = null
let currentIndexValue = null
for (let i = selectorNode.nodes.length - 1; i >= 0; i--) {
const termNode = selectorNode.nodes[i]
switch (termNode.type) {
case 'tag':
if (!currentTable) currentTable = this.namedScopeTable
if (!currentTable[termNode.value]) currentTable[termNode.value] = {}
currentTable = currentTable[termNode.value]
if (currentIndexValue != null) {
if (!currentTable.indices) currentTable.indices = {}
if (!currentTable.indices[currentIndexValue]) currentTable.indices[currentIndexValue] = {}
currentTable = currentTable.indices[currentIndexValue]
currentIndexValue = null
}
break
case 'string':
if (!currentTable) currentTable = this.anonymousScopeTable
const value = termNode.value.slice(1, -1)
if (!currentTable[value]) currentTable[value] = {}
currentTable = currentTable[value]
if (currentIndexValue != null) {
if (!currentTable.indices) currentTable.indices = {}
if (!currentTable.indices[currentIndexValue]) currentTable.indices[currentIndexValue] = {}
currentTable = currentTable.indices[currentIndexValue]
currentIndexValue = null
}
break
case 'universal':
if (currentTable) {
if (!currentTable['*']) currentTable['*'] = {}
currentTable = currentTable['*']
} else {
if (!this.namedScopeTable['*']) {
this.namedScopeTable['*'] = this.anonymousScopeTable['*'] = {}
}
currentTable = this.namedScopeTable['*']
}
if (currentIndexValue != null) {
if (!currentTable.indices) currentTable.indices = {}
if (!currentTable.indices[currentIndexValue]) currentTable.indices[currentIndexValue] = {}
currentTable = currentTable.indices[currentIndexValue]
currentIndexValue = null
}
break
case 'combinator':
if (currentIndexValue != null) {
rejectSelector(selector)
}
if (termNode.value === '>') {
if (!currentTable.parents) currentTable.parents = {}
currentTable = currentTable.parents
} else {
rejectSelector(selector)
}
break
case 'pseudo':
if (termNode.value === ':nth-child') {
currentIndexValue = termNode.nodes[0].nodes[0].value
} else {
rejectSelector(selector)
}
break
default:
rejectSelector(selector)
}
}
currentTable.scopeName = scopeName
}
}).process(selector)
}
get (nodeTypes, childIndices, leafIsNamed = true) {
let result
let i = nodeTypes.length - 1
let currentTable = leafIsNamed
? this.namedScopeTable[nodeTypes[i]]
: this.anonymousScopeTable[nodeTypes[i]]
if (!currentTable) currentTable = this.namedScopeTable['*']
while (currentTable) {
if (currentTable.indices && currentTable.indices[childIndices[i]]) {
currentTable = currentTable.indices[childIndices[i]]
}
if (currentTable.scopeName) {
result = currentTable.scopeName
}
if (i === 0) break
i--
currentTable = currentTable.parents && (
currentTable.parents[nodeTypes[i]] ||
currentTable.parents['*']
)
}
return result
}
}
function setTableDefaults (table) {
const defaultTypeTable = table['*']
for (let type in table) {
let typeTable = table[type]
if (typeTable === defaultTypeTable) continue
if (defaultTypeTable) {
mergeTable(typeTable, defaultTypeTable)
}
if (typeTable.parents) {
setTableDefaults(typeTable.parents)
}
for (let key in typeTable.indices) {
const indexTable = typeTable.indices[key]
mergeTable(indexTable, typeTable, false)
if (indexTable.parents) {
setTableDefaults(indexTable.parents)
}
}
}
}
function mergeTable (table, defaultTable, mergeIndices = true) {
if (mergeIndices && defaultTable.indices) {
if (!table.indices) table.indices = {}
for (let key in defaultTable.indices) {
if (!table.indices[key]) table.indices[key] = {}
mergeTable(table.indices[key], defaultTable.indices[key])
}
}
if (defaultTable.parents) {
if (!table.parents) table.parents = {}
for (let key in defaultTable.parents) {
if (!table.parents[key]) table.parents[key] = {}
mergeTable(table.parents[key], defaultTable.parents[key])
}
}
if (defaultTable.scopeName && !table.scopeName) {
table.scopeName = defaultTable.scopeName
}
}
function rejectSelector (selector) {
throw new TypeError(`Unsupported selector '${selector}'`)
}

View File

@@ -56,7 +56,7 @@ class TextEditorComponent {
this.props = props
if (!props.model) {
props.model = new TextEditor({mini: props.mini})
props.model = new TextEditor({mini: props.mini, readOnly: props.readOnly})
}
this.props.model.component = this
@@ -170,6 +170,7 @@ class TextEditorComponent {
this.textDecorationBoundaries = []
this.pendingScrollTopRow = this.props.initialScrollTopRow
this.pendingScrollLeftColumn = this.props.initialScrollLeftColumn
this.tabIndex = this.props.element && this.props.element.tabIndex ? this.props.element.tabIndex : -1
this.measuredContent = false
this.queryGuttersToRender()
@@ -460,9 +461,13 @@ class TextEditorComponent {
}
}
let attributes = null
let attributes = {}
if (model.isMini()) {
attributes = {mini: ''}
attributes.mini = ''
}
if (!this.isInputEnabled()) {
attributes.readonly = ''
}
const dataset = {encoding: model.getEncoding()}
@@ -677,7 +682,8 @@ class TextEditorComponent {
scrollWidth: this.getScrollWidth(),
decorationsToRender: this.decorationsToRender,
cursorsBlinkedOff: this.cursorsBlinkedOff,
hiddenInputPosition: this.hiddenInputPosition
hiddenInputPosition: this.hiddenInputPosition,
tabIndex: this.tabIndex
})
}
@@ -1712,10 +1718,6 @@ class TextEditorComponent {
return
}
if (this.getChromeVersion() === 56) {
this.getHiddenInput().value = ''
}
this.compositionCheckpoint = this.props.model.createCheckpoint()
if (this.accentedCharacterMenuIsOpen) {
this.props.model.selectLeft()
@@ -1723,16 +1725,7 @@ class TextEditorComponent {
}
didCompositionUpdate (event) {
if (this.getChromeVersion() === 56) {
process.nextTick(() => {
if (this.compositionCheckpoint != null) {
const previewText = this.getHiddenInput().value
this.props.model.insertText(previewText, {select: true})
}
})
} else {
this.props.model.insertText(event.data, {select: true})
}
this.props.model.insertText(event.data, {select: true})
}
didCompositionEnd (event) {
@@ -1758,11 +1751,13 @@ class TextEditorComponent {
const screenPosition = this.screenPositionForMouseEvent(event)
// All clicks should set the cursor position, but only left-clicks should
// have additional logic.
// On macOS, ctrl-click brings up the context menu so also handle that case.
if (button !== 0 || (platform === 'darwin' && ctrlKey)) {
model.setCursorScreenPosition(screenPosition, {autoscroll: false})
// Always set cursor position on middle-click
// Only set cursor position on right-click if there is one cursor with no selection
const ranges = model.getSelectedBufferRanges()
if (button === 1 || (ranges.length === 1 && ranges[0].isEmpty())) {
model.setCursorScreenPosition(screenPosition, {autoscroll: false})
}
// On Linux, pasting happens on middle click. A textInput event with the
// contents of the selection clipboard will be dispatched by the browser
@@ -2962,11 +2957,11 @@ class TextEditorComponent {
}
setInputEnabled (inputEnabled) {
this.props.inputEnabled = inputEnabled
this.props.model.update({readOnly: !inputEnabled})
}
isInputEnabled (inputEnabled) {
return this.props.inputEnabled != null ? this.props.inputEnabled : true
return !this.props.model.isReadOnly()
}
getHiddenInput () {
@@ -3017,7 +3012,7 @@ class DummyScrollbarComponent {
const outerStyle = {
position: 'absolute',
contain: 'strict',
contain: 'content',
zIndex: 1,
willChange: 'transform'
}
@@ -3553,7 +3548,7 @@ class CursorsAndInputComponent {
const {
lineHeight, hiddenInputPosition, didBlurHiddenInput, didFocusHiddenInput,
didPaste, didTextInput, didKeydown, didKeyup, didKeypress,
didCompositionStart, didCompositionUpdate, didCompositionEnd
didCompositionStart, didCompositionUpdate, didCompositionEnd, tabIndex
} = this.props
let top, left
@@ -3581,7 +3576,7 @@ class CursorsAndInputComponent {
compositionupdate: didCompositionUpdate,
compositionend: didCompositionEnd
},
tabIndex: -1,
tabIndex: tabIndex,
style: {
position: 'absolute',
width: '1px',

View File

@@ -32,7 +32,7 @@ class TextEditorElement extends HTMLElement {
createdCallback () {
this.emitter = new Emitter()
this.initialText = this.textContent
this.tabIndex = -1
if (this.tabIndex == null) this.tabIndex = -1
this.addEventListener('focus', (event) => this.getComponent().didFocus(event))
this.addEventListener('blur', (event) => this.getComponent().didBlur(event))
}
@@ -59,6 +59,9 @@ class TextEditorElement extends HTMLElement {
case 'gutter-hidden':
this.getModel().update({lineNumberGutterVisible: newValue == null})
break
case 'readonly':
this.getModel().update({readOnly: newValue != null})
break
}
}
}
@@ -275,7 +278,8 @@ class TextEditorElement extends HTMLElement {
this.component = new TextEditorComponent({
element: this,
mini: this.hasAttribute('mini'),
updatedSynchronously: this.updatedSynchronously
updatedSynchronously: this.updatedSynchronously,
readOnly: this.hasAttribute('readonly')
})
this.updateModelFromAttributes()
}

View File

@@ -119,11 +119,16 @@ class TextEditor {
}
this.id = params.id != null ? params.id : nextId++
if (this.id >= nextId) {
// Ensure that new editors get unique ids:
nextId = this.id + 1
}
this.initialScrollTopRow = params.initialScrollTopRow
this.initialScrollLeftColumn = params.initialScrollLeftColumn
this.decorationManager = params.decorationManager
this.selectionsMarkerLayer = params.selectionsMarkerLayer
this.mini = (params.mini != null) ? params.mini : false
this.readOnly = (params.readOnly != null) ? params.readOnly : false
this.placeholderText = params.placeholderText
this.showLineNumbers = params.showLineNumbers
this.assert = params.assert || (condition => condition)
@@ -400,6 +405,15 @@ class TextEditor {
}
break
case 'readOnly':
if (value !== this.readOnly) {
this.readOnly = value
if (this.component != null) {
this.component.scheduleUpdate()
}
}
break
case 'placeholderText':
if (value !== this.placeholderText) {
this.placeholderText = value
@@ -530,6 +544,7 @@ class TextEditor {
softWrapAtPreferredLineLength: this.softWrapAtPreferredLineLength,
preferredLineLength: this.preferredLineLength,
mini: this.mini,
readOnly: this.readOnly,
editorWidthInChars: this.editorWidthInChars,
width: this.width,
maxScreenLineLength: this.maxScreenLineLength,
@@ -965,6 +980,12 @@ class TextEditor {
isMini () { return this.mini }
setReadOnly (readOnly) {
this.update({readOnly})
}
isReadOnly () { return this.readOnly }
onDidChangeMini (callback) {
return this.emitter.on('did-change-mini', callback)
}
@@ -1309,15 +1330,24 @@ class TextEditor {
insertText (text, options = {}) {
if (!this.emitWillInsertTextEvent(text)) return false
let groupLastChanges = false
if (options.undo === 'skip') {
options = Object.assign({}, options)
delete options.undo
groupLastChanges = true
}
const groupingInterval = options.groupUndo ? this.undoGroupingInterval : 0
if (options.autoIndentNewline == null) options.autoIndentNewline = this.shouldAutoIndent()
if (options.autoDecreaseIndent == null) options.autoDecreaseIndent = this.shouldAutoIndent()
return this.mutateSelectedText(selection => {
const result = this.mutateSelectedText(selection => {
const range = selection.insertText(text, options)
const didInsertEvent = {text, range}
this.emitter.emit('did-insert-text', didInsertEvent)
return range
}, groupingInterval)
if (groupLastChanges) this.buffer.groupLastChanges()
return result
}
// Essential: For each selection, replace the selected text with a newline.
@@ -3053,6 +3083,36 @@ class TextEditor {
return this.expandSelectionsBackward(selection => selection.selectToBeginningOfPreviousParagraph())
}
// Extended: For each selection, select the syntax node that contains
// that selection.
selectLargerSyntaxNode () {
const languageMode = this.buffer.getLanguageMode()
if (!languageMode.getRangeForSyntaxNodeContainingRange) return
this.expandSelectionsForward(selection => {
const currentRange = selection.getBufferRange()
const newRange = languageMode.getRangeForSyntaxNodeContainingRange(currentRange)
if (newRange) {
if (!selection._rangeStack) selection._rangeStack = []
selection._rangeStack.push(currentRange)
selection.setBufferRange(newRange)
}
})
}
// Extended: Undo the effect a preceding call to {::selectLargerSyntaxNode}.
selectSmallerSyntaxNode () {
this.expandSelectionsForward(selection => {
if (selection._rangeStack) {
const lastRange = selection._rangeStack[selection._rangeStack.length - 1]
if (lastRange && selection.getBufferRange().containsRange(lastRange)) {
selection._rangeStack.length--
selection.setBufferRange(lastRange)
}
}
})
}
// Extended: Select the range of the given marker if it is valid.
//
// * `marker` A {DisplayMarker}
@@ -3583,14 +3643,15 @@ class TextEditor {
return this.buffer.getLanguageMode().rootScopeDescriptor
}
// Essential: Get the syntactic scopeDescriptor for the given position in buffer
// Essential: Get the syntactic {ScopeDescriptor} for the given position in buffer
// coordinates. Useful with {Config::get}.
//
// For example, if called with a position inside the parameter list of an
// anonymous CoffeeScript function, the method returns the following array:
// `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]`
// anonymous CoffeeScript function, this method returns a {ScopeDescriptor} with
// the following scopes array:
// `["source.coffee", "meta.function.inline.coffee", "meta.parameters.coffee", "variable.parameter.function.coffee"]`
//
// * `bufferPosition` A {Point} or {Array} of [row, column].
// * `bufferPosition` A {Point} or {Array} of `[row, column]`.
//
// Returns a {ScopeDescriptor}.
scopeDescriptorForBufferPosition (bufferPosition) {
@@ -3838,7 +3899,7 @@ class TextEditor {
// Extended: Fold all foldable lines at the given indent level.
//
// * `level` A {Number}.
// * `level` A {Number} starting at 0.
foldAllAtIndentLevel (level) {
const languageMode = this.buffer.getLanguageMode()
const foldableRanges = (
@@ -4522,8 +4583,7 @@ class TextEditor {
? minBlankIndentLevel
: 0
const tabLength = this.getTabLength()
const indentString = ' '.repeat(tabLength * minIndentLevel)
const indentString = this.buildIndentString(minIndentLevel)
for (let row = start; row <= end; row++) {
const line = this.buffer.lineForRow(row)
if (NON_WHITESPACE_REGEXP.test(line)) {

View File

@@ -74,10 +74,15 @@ class TextMateLanguageMode {
//
// Returns a {Number}.
suggestedIndentForBufferRow (bufferRow, tabLength, options) {
return this._suggestedIndentForTokenizedLineAtBufferRow(
const line = this.buffer.lineForRow(bufferRow)
const tokenizedLine = this.tokenizedLineForRow(bufferRow)
const iterator = tokenizedLine.getTokenIterator()
iterator.next()
const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()})
return this._suggestedIndentForLineWithScopeAtBufferRow(
bufferRow,
this.buffer.lineForRow(bufferRow),
this.tokenizedLineForRow(bufferRow),
line,
scopeDescriptor,
tabLength,
options
)
@@ -90,10 +95,14 @@ class TextMateLanguageMode {
//
// Returns a {Number}.
suggestedIndentForLineAtBufferRow (bufferRow, line, tabLength) {
return this._suggestedIndentForTokenizedLineAtBufferRow(
const tokenizedLine = this.buildTokenizedLineForRowWithText(bufferRow, line)
const iterator = tokenizedLine.getTokenIterator()
iterator.next()
const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()})
return this._suggestedIndentForLineWithScopeAtBufferRow(
bufferRow,
line,
this.buildTokenizedLineForRowWithText(bufferRow, line),
scopeDescriptor,
tabLength
)
}
@@ -111,7 +120,7 @@ class TextMateLanguageMode {
const currentIndentLevel = this.indentLevelForLine(line, tabLength)
if (currentIndentLevel === 0) return
const scopeDescriptor = this.scopeDescriptorForPosition([bufferRow, 0])
const scopeDescriptor = this.scopeDescriptorForPosition(new Point(bufferRow, 0))
const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
if (!decreaseIndentRegex) return
@@ -138,11 +147,7 @@ class TextMateLanguageMode {
return desiredIndentLevel
}
_suggestedIndentForTokenizedLineAtBufferRow (bufferRow, line, tokenizedLine, tabLength, options) {
const iterator = tokenizedLine.getTokenIterator()
iterator.next()
const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()})
_suggestedIndentForLineWithScopeAtBufferRow (bufferRow, line, scopeDescriptor, tabLength, options) {
const increaseIndentRegex = this.increaseIndentRegexForScopeDescriptor(scopeDescriptor)
const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
const decreaseNextIndentRegex = this.decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)

View File

@@ -136,12 +136,12 @@ class ThemeManager {
]
themeNames = _.intersection(themeNames, builtInThemeNames)
if (themeNames.length === 0) {
themeNames = ['atom-dark-syntax', 'atom-dark-ui']
themeNames = ['one-dark-syntax', 'one-dark-ui']
} else if (themeNames.length === 1) {
if (_.endsWith(themeNames[0], '-ui')) {
themeNames.unshift('atom-dark-syntax')
themeNames.unshift('one-dark-syntax')
} else {
themeNames.push('atom-dark-ui')
themeNames.push('one-dark-ui')
}
}
}

View File

@@ -0,0 +1,72 @@
const path = require('path')
const SyntaxScopeMap = require('./syntax-scope-map')
const Module = require('module')
module.exports =
class TreeSitterGrammar {
constructor (registry, filePath, params) {
this.registry = registry
this.id = params.id
this.name = params.name
this.legacyScopeName = params.legacyScopeName
if (params.contentRegExp) this.contentRegExp = new RegExp(params.contentRegExp)
this.folds = params.folds || []
this.commentStrings = {
commentStartString: params.comments && params.comments.start,
commentEndString: params.comments && params.comments.end
}
const scopeSelectors = {}
for (const key in params.scopes || {}) {
scopeSelectors[key] = params.scopes[key]
.split('.')
.map(s => `syntax--${s}`)
.join(' ')
}
this.scopeMap = new SyntaxScopeMap(scopeSelectors)
this.fileTypes = params.fileTypes
// TODO - When we upgrade to a new enough version of node, use `require.resolve`
// with the new `paths` option instead of this private API.
const languageModulePath = Module._resolveFilename(params.parser, {
id: filePath,
filename: filePath,
paths: Module._nodeModulePaths(path.dirname(filePath))
})
this.languageModule = require(languageModulePath)
this.scopesById = new Map()
this.idsByScope = {}
this.nextScopeId = 256 + 1
this.registration = null
}
idForScope (scope) {
let id = this.idsByScope[scope]
if (!id) {
id = this.nextScopeId += 2
this.idsByScope[scope] = id
this.scopesById.set(id, scope)
}
return id
}
classNameForScopeId (id) {
return this.scopesById.get(id)
}
get scopeName () {
return this.id
}
activate () {
this.registration = this.registry.addGrammar(this)
}
deactivate () {
if (this.registration) this.registration.dispose()
}
}

View File

@@ -0,0 +1,532 @@
const {Document} = require('tree-sitter')
const {Point, Range, Emitter} = require('atom')
const ScopeDescriptor = require('./scope-descriptor')
const TokenizedLine = require('./tokenized-line')
const TextMateLanguageMode = require('./text-mate-language-mode')
let nextId = 0
module.exports =
class TreeSitterLanguageMode {
constructor ({buffer, grammar, config}) {
this.id = nextId++
this.buffer = buffer
this.grammar = grammar
this.config = config
this.document = new Document()
this.document.setInput(new TreeSitterTextBufferInput(buffer))
this.document.setLanguage(grammar.languageModule)
this.document.parse()
this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.id]})
this.emitter = new Emitter()
this.isFoldableCache = []
// TODO: Remove this once TreeSitterLanguageMode implements its own auto-indentation system. This
// is temporarily needed in order to delegate to the TextMateLanguageMode's auto-indent system.
this.regexesByPattern = {}
}
getLanguageId () {
return this.grammar.id
}
bufferDidChange ({oldRange, newRange, oldText, newText}) {
const startRow = oldRange.start.row
const oldEndRow = oldRange.end.row
const newEndRow = newRange.end.row
this.isFoldableCache.splice(startRow, oldEndRow - startRow, ...new Array(newEndRow - startRow))
this.document.edit({
startIndex: this.buffer.characterIndexForPosition(oldRange.start),
lengthRemoved: oldText.length,
lengthAdded: newText.length,
startPosition: oldRange.start,
extentRemoved: oldRange.getExtent(),
extentAdded: newRange.getExtent()
})
}
/*
Section - Highlighting
*/
buildHighlightIterator () {
const invalidatedRanges = this.document.parse()
for (let i = 0, n = invalidatedRanges.length; i < n; i++) {
const range = invalidatedRanges[i]
const startRow = range.start.row
const endRow = range.end.row
for (let row = startRow; row < endRow; row++) {
this.isFoldableCache[row] = undefined
}
this.emitter.emit('did-change-highlighting', range)
}
return new TreeSitterHighlightIterator(this)
}
onDidChangeHighlighting (callback) {
return this.emitter.on('did-change-hightlighting', callback)
}
classNameForScopeId (scopeId) {
return this.grammar.classNameForScopeId(scopeId)
}
/*
Section - Commenting
*/
commentStringsForPosition () {
return this.grammar.commentStrings
}
isRowCommented () {
return false
}
/*
Section - Indentation
*/
suggestedIndentForLineAtBufferRow (row, line, tabLength) {
return this._suggestedIndentForLineWithScopeAtBufferRow(
row,
line,
this.rootScopeDescriptor,
tabLength
)
}
suggestedIndentForBufferRow (row, tabLength, options) {
return this._suggestedIndentForLineWithScopeAtBufferRow(
row,
this.buffer.lineForRow(row),
this.rootScopeDescriptor,
tabLength,
options
)
}
indentLevelForLine (line, tabLength = tabLength) {
let indentLength = 0
for (let i = 0, {length} = line; i < length; i++) {
const char = line[i]
if (char === '\t') {
indentLength += tabLength - (indentLength % tabLength)
} else if (char === ' ') {
indentLength++
} else {
break
}
}
return indentLength / tabLength
}
/*
Section - Folding
*/
isFoldableAtRow (row) {
if (this.isFoldableCache[row] != null) return this.isFoldableCache[row]
const result = this.getFoldableRangeContainingPoint(Point(row, Infinity), 0, true) != null
this.isFoldableCache[row] = result
return result
}
getFoldableRanges () {
return this.getFoldableRangesAtIndentLevel(null)
}
getFoldableRangesAtIndentLevel (goalLevel) {
let result = []
let stack = [{node: this.document.rootNode, level: 0}]
while (stack.length > 0) {
const {node, level} = stack.pop()
const range = this.getFoldableRangeForNode(node)
if (range) {
if (goalLevel == null || level === goalLevel) {
let updatedExistingRange = false
for (let i = 0, {length} = result; i < length; i++) {
if (result[i].start.row === range.start.row &&
result[i].end.row === range.end.row) {
result[i] = range
updatedExistingRange = true
break
}
}
if (!updatedExistingRange) result.push(range)
}
}
const parentStartRow = node.startPosition.row
const parentEndRow = node.endPosition.row
for (let children = node.namedChildren, i = 0, {length} = children; i < length; i++) {
const child = children[i]
const {startPosition: childStart, endPosition: childEnd} = child
if (childEnd.row > childStart.row) {
if (childStart.row === parentStartRow && childEnd.row === parentEndRow) {
stack.push({node: child, level: level})
} else {
const childLevel = range && range.containsPoint(childStart) && range.containsPoint(childEnd)
? level + 1
: level
if (childLevel <= goalLevel || goalLevel == null) {
stack.push({node: child, level: childLevel})
}
}
}
}
}
return result.sort((a, b) => a.start.row - b.start.row)
}
getFoldableRangeContainingPoint (point, tabLength, existenceOnly = false) {
let node = this.document.rootNode.descendantForPosition(this.buffer.clipPosition(point))
while (node) {
if (existenceOnly && node.startPosition.row < point.row) break
if (node.endPosition.row > point.row) {
const range = this.getFoldableRangeForNode(node, existenceOnly)
if (range) return range
}
node = node.parent
}
}
getFoldableRangeForNode (node, existenceOnly) {
const {children, type: nodeType} = node
const childCount = children.length
let childTypes
for (var i = 0, {length} = this.grammar.folds; i < length; i++) {
const foldEntry = this.grammar.folds[i]
if (foldEntry.type) {
if (typeof foldEntry.type === 'string') {
if (foldEntry.type !== nodeType) continue
} else {
if (!foldEntry.type.includes(nodeType)) continue
}
}
let foldStart
const startEntry = foldEntry.start
if (startEntry) {
if (startEntry.index != null) {
const child = children[startEntry.index]
if (!child || (startEntry.type && startEntry.type !== child.type)) continue
foldStart = child.endPosition
} else {
if (!childTypes) childTypes = children.map(child => child.type)
const index = typeof startEntry.type === 'string'
? childTypes.indexOf(startEntry.type)
: childTypes.findIndex(type => startEntry.type.includes(type))
if (index === -1) continue
foldStart = children[index].endPosition
}
} else {
foldStart = new Point(node.startPosition.row, Infinity)
}
let foldEnd
const endEntry = foldEntry.end
if (endEntry) {
let foldEndNode
if (endEntry.index != null) {
const index = endEntry.index < 0 ? childCount + endEntry.index : endEntry.index
foldEndNode = children[index]
if (!foldEndNode || (endEntry.type && endEntry.type !== foldEndNode.type)) continue
} else {
if (!childTypes) childTypes = children.map(foldEndNode => foldEndNode.type)
const index = typeof endEntry.type === 'string'
? childTypes.indexOf(endEntry.type)
: childTypes.findIndex(type => endEntry.type.includes(type))
if (index === -1) continue
foldEndNode = children[index]
}
if (foldEndNode.endIndex - foldEndNode.startIndex > 1 && foldEndNode.startPosition.row > foldStart.row) {
foldEnd = new Point(foldEndNode.startPosition.row - 1, Infinity)
} else {
foldEnd = foldEndNode.startPosition
}
} else {
const {endPosition} = node
if (endPosition.column === 0) {
foldEnd = Point(endPosition.row - 1, Infinity)
} else if (childCount > 0) {
foldEnd = endPosition
} else {
foldEnd = Point(endPosition.row, 0)
}
}
return existenceOnly ? true : new Range(foldStart, foldEnd)
}
}
/*
Syntax Tree APIs
*/
getRangeForSyntaxNodeContainingRange (range) {
const startIndex = this.buffer.characterIndexForPosition(range.start)
const endIndex = this.buffer.characterIndexForPosition(range.end)
let node = this.document.rootNode.descendantForIndex(startIndex, endIndex - 1)
while (node && node.startIndex === startIndex && node.endIndex === endIndex) {
node = node.parent
}
if (node) return new Range(node.startPosition, node.endPosition)
}
/*
Section - Backward compatibility shims
*/
tokenizedLineForRow (row) {
return new TokenizedLine({
openScopes: [],
text: this.buffer.lineForRow(row),
tags: [],
ruleStack: [],
lineEnding: this.buffer.lineEndingForRow(row),
tokenIterator: null,
grammar: this.grammar
})
}
scopeDescriptorForPosition (point) {
const result = []
let node = this.document.rootNode.descendantForPosition(point)
// Don't include anonymous token types like '(' because they prevent scope chains
// from being parsed as CSS selectors by the `slick` parser. Other css selector
// parsers like `postcss-selector-parser` do allow arbitrary quoted strings in
// selectors.
if (!node.isNamed) node = node.parent
while (node) {
result.push(node.type)
node = node.parent
}
result.push(this.grammar.id)
return new ScopeDescriptor({scopes: result.reverse()})
}
hasTokenForSelector (scopeSelector) {
return false
}
getGrammar () {
return this.grammar
}
}
class TreeSitterHighlightIterator {
constructor (layer, document) {
this.layer = layer
// Conceptually, the iterator represents a single position in the text. It stores this
// position both as a character index and as a `Point`. This position corresponds to a
// leaf node of the syntax tree, which either contains or follows the iterator's
// textual position. The `currentNode` property represents that leaf node, and
// `currentChildIndex` represents the child index of that leaf node within its parent.
this.currentIndex = null
this.currentPosition = null
this.currentNode = null
this.currentChildIndex = null
// In order to determine which selectors match its current node, the iterator maintains
// a list of the current node's ancestors. Because the selectors can use the `:nth-child`
// pseudo-class, each node's child index is also stored.
this.containingNodeTypes = []
this.containingNodeChildIndices = []
// At any given position, the iterator exposes the list of class names that should be
// *ended* at its current position and the list of class names that should be *started*
// at its current position.
this.closeTags = []
this.openTags = []
}
seek (targetPosition) {
const containingTags = []
this.closeTags.length = 0
this.openTags.length = 0
this.containingNodeTypes.length = 0
this.containingNodeChildIndices.length = 0
this.currentPosition = targetPosition
this.currentIndex = this.layer.buffer.characterIndexForPosition(targetPosition)
var node = this.layer.document.rootNode
var childIndex = -1
var done = false
var nodeContainsTarget = true
do {
this.currentNode = node
this.currentChildIndex = childIndex
if (!nodeContainsTarget) break
this.containingNodeTypes.push(node.type)
this.containingNodeChildIndices.push(childIndex)
const scopeName = this.currentScopeName()
if (scopeName) {
const id = this.layer.grammar.idForScope(scopeName)
if (this.currentIndex === node.startIndex) {
this.openTags.push(id)
} else {
containingTags.push(id)
}
}
done = true
for (var i = 0, {children} = node, childCount = children.length; i < childCount; i++) {
const child = children[i]
if (child.endIndex > this.currentIndex) {
node = child
childIndex = i
done = false
if (child.startIndex > this.currentIndex) nodeContainsTarget = false
break
}
}
} while (!done)
return containingTags
}
moveToSuccessor () {
this.closeTags.length = 0
this.openTags.length = 0
if (!this.currentNode) {
this.currentPosition = {row: Infinity, column: Infinity}
return false
}
do {
if (this.currentIndex < this.currentNode.startIndex) {
this.currentIndex = this.currentNode.startIndex
this.currentPosition = this.currentNode.startPosition
this.pushOpenTag()
this.descendLeft()
} else if (this.currentIndex < this.currentNode.endIndex) {
while (true) {
this.currentIndex = this.currentNode.endIndex
this.currentPosition = this.currentNode.endPosition
this.pushCloseTag()
const {nextSibling} = this.currentNode
if (nextSibling) {
this.currentNode = nextSibling
this.currentChildIndex++
if (this.currentIndex === nextSibling.startIndex) {
this.pushOpenTag()
this.descendLeft()
}
break
} else {
this.currentNode = this.currentNode.parent
this.currentChildIndex = last(this.containingNodeChildIndices)
if (!this.currentNode) break
}
}
} else if (this.currentNode.startIndex < this.currentNode.endIndex) {
this.currentNode = this.currentNode.nextSibling
if (this.currentNode) {
this.currentChildIndex++
this.currentPosition = this.currentNode.startPosition
this.currentIndex = this.currentNode.startIndex
this.pushOpenTag()
this.descendLeft()
}
} else {
this.pushCloseTag()
this.currentNode = this.currentNode.parent
this.currentChildIndex = last(this.containingNodeChildIndices)
}
} while (this.closeTags.length === 0 && this.openTags.length === 0 && this.currentNode)
return true
}
getPosition () {
return this.currentPosition
}
getCloseScopeIds () {
return this.closeTags.slice()
}
getOpenScopeIds () {
return this.openTags.slice()
}
// Private methods
descendLeft () {
let child
while ((child = this.currentNode.firstChild) && this.currentIndex === child.startIndex) {
this.currentNode = child
this.currentChildIndex = 0
this.pushOpenTag()
}
}
currentScopeName () {
return this.layer.grammar.scopeMap.get(
this.containingNodeTypes,
this.containingNodeChildIndices,
this.currentNode.isNamed
)
}
pushCloseTag () {
const scopeName = this.currentScopeName()
if (scopeName) this.closeTags.push(this.layer.grammar.idForScope(scopeName))
this.containingNodeTypes.pop()
this.containingNodeChildIndices.pop()
}
pushOpenTag () {
this.containingNodeTypes.push(this.currentNode.type)
this.containingNodeChildIndices.push(this.currentChildIndex)
const scopeName = this.currentScopeName()
if (scopeName) this.openTags.push(this.layer.grammar.idForScope(scopeName))
}
}
class TreeSitterTextBufferInput {
constructor (buffer) {
this.buffer = buffer
this.seek(0)
}
seek (characterIndex) {
this.position = this.buffer.positionForCharacterIndex(characterIndex)
}
read () {
const endPosition = this.buffer.clipPosition(this.position.traverse({row: 1000, column: 0}))
const text = this.buffer.getTextInRange([this.position, endPosition])
this.position = endPosition
return text
}
}
function last (array) {
return array[array.length - 1]
}
// TODO: Remove this once TreeSitterLanguageMode implements its own auto-indent system.
[
'_suggestedIndentForLineWithScopeAtBufferRow',
'suggestedIndentForEditedBufferRow',
'increaseIndentRegexForScopeDescriptor',
'decreaseIndentRegexForScopeDescriptor',
'decreaseNextIndentRegexForScopeDescriptor',
'regexForPattern'
].forEach(methodName => {
module.exports.prototype[methodName] = TextMateLanguageMode.prototype[methodName]
})

View File

@@ -9,6 +9,7 @@ class WindowEventHandler {
this.handleFocusNext = this.handleFocusNext.bind(this)
this.handleFocusPrevious = this.handleFocusPrevious.bind(this)
this.handleWindowBlur = this.handleWindowBlur.bind(this)
this.handleWindowResize = this.handleWindowResize.bind(this)
this.handleEnterFullScreen = this.handleEnterFullScreen.bind(this)
this.handleLeaveFullScreen = this.handleLeaveFullScreen.bind(this)
this.handleWindowBeforeunload = this.handleWindowBeforeunload.bind(this)
@@ -51,6 +52,7 @@ class WindowEventHandler {
this.addEventListener(this.window, 'beforeunload', this.handleWindowBeforeunload)
this.addEventListener(this.window, 'focus', this.handleWindowFocus)
this.addEventListener(this.window, 'blur', this.handleWindowBlur)
this.addEventListener(this.window, 'resize', this.handleWindowResize)
this.addEventListener(this.document, 'keyup', this.handleDocumentKeyEvent)
this.addEventListener(this.document, 'keydown', this.handleDocumentKeyEvent)
@@ -189,6 +191,10 @@ class WindowEventHandler {
this.atomEnvironment.storeWindowDimensions()
}
handleWindowResize () {
this.atomEnvironment.storeWindowDimensions()
}
handleEnterFullScreen () {
this.document.body.classList.add('fullscreen')
}

View File

@@ -16,9 +16,10 @@ atom-dock {
.atom-dock-inner {
display: flex;
// Keep the area at least a pixel wide so that you have something to hover
// Keep the area at least 2 pixels wide so that you have something to hover
// over to trigger the toggle button affordance even when fullscreen.
&.left, &.right { min-width: 1px; }
// Needs to be 2 pixels to work on Windows when scaled to 150%. See atom/atom #15728
&.left, &.right { min-width: 2px; }
&.bottom { min-height: 1px; }
&.bottom { width: 100%; }