mirror of
https://github.com/atom/atom.git
synced 2026-04-06 03:02:13 -04:00
Merge branch 'master' into wl-async-save-dialog
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
language: python
|
||||
|
||||
python:
|
||||
- "2.7.13"
|
||||
|
||||
git:
|
||||
depth: 10
|
||||
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -65,7 +65,7 @@ Atom is intentionally very modular. Nearly every non-editor UI element you inter
|
||||
|
||||

|
||||
|
||||
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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
20
README.md
20
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"url": "https://github.com/atom/atom.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"atom-package-manager": "1.18.11"
|
||||
"atom-package-manager": "1.19.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
|
||||

|
||||
|
||||
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).
|
||||
|
||||
@@ -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/)
|
||||
@@ -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'
|
||||
|
||||
97
package.json
97
package.json
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -592,7 +592,7 @@ describe('AtomEnvironment', () => {
|
||||
const promise = new Promise((r) => { resolve = r })
|
||||
envLoaded = () => {
|
||||
resolve()
|
||||
promise
|
||||
return promise
|
||||
}
|
||||
atomEnvironment = new AtomEnvironment({
|
||||
applicationDelegate: atom.applicationDelegate,
|
||||
|
||||
@@ -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
|
||||
|
||||
1
spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/fake-parser.js
vendored
Normal file
1
spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/fake-parser.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
exports.isFakeTreeSitterParser = true
|
||||
14
spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/some-language.cson
vendored
Normal file
14
spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/some-language.cson
vendored
Normal 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'
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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'])
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
|
||||
77
spec/syntax-scope-map-spec.js
Normal file
77
spec/syntax-scope-map-spec.js
Normal 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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
560
spec/tree-sitter-language-mode-spec.js
Normal file
560
spec/tree-sitter-language-mode-spec.js
Normal 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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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
364
src/application-delegate.js
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
225
src/main-process/application-menu.js
Normal file
225
src/main-process/application-menu.js
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
1376
src/main-process/atom-application.js
Normal file
1376
src/main-process/atom-application.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
54
src/main-process/atom-protocol-handler.js
Normal file
54
src/main-process/atom-protocol-handler.js
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
432
src/main-process/atom-window.js
Normal file
432
src/main-process/atom-window.js
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
178
src/syntax-scope-map.js
Normal 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}'`)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
72
src/tree-sitter-grammar.js
Normal file
72
src/tree-sitter-grammar.js
Normal 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()
|
||||
}
|
||||
}
|
||||
532
src/tree-sitter-language-mode.js
Normal file
532
src/tree-sitter-language-mode.js
Normal 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]
|
||||
})
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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%; }
|
||||
|
||||
Reference in New Issue
Block a user