mirror of
https://github.com/atom/atom.git
synced 2026-01-14 17:38:03 -05:00
Merge branch 'master' into bring-back-asar
This commit is contained in:
10
.travis.yml
10
.travis.yml
@@ -10,7 +10,7 @@ matrix:
|
||||
include:
|
||||
- os: linux
|
||||
dist: trusty
|
||||
env: NODE_VERSION=6.9.4 DISPLAY=:99.0 CXX=g++-6 CC=gcc-6
|
||||
env: NODE_VERSION=6.9.4 DISPLAY=:99.0 CC=clang CXX=clang++ npm_config_clang=1
|
||||
|
||||
sudo: required
|
||||
|
||||
@@ -22,7 +22,7 @@ install:
|
||||
- source /tmp/.nvm/nvm.sh
|
||||
- nvm install $NODE_VERSION
|
||||
- nvm use --delete-prefix $NODE_VERSION
|
||||
- npm install -g npm
|
||||
- npm install -g npm@5.3.0
|
||||
- script/build --create-debian-package --create-rpm-package --compress-artifacts
|
||||
|
||||
script:
|
||||
@@ -51,11 +51,9 @@ addons:
|
||||
- out/atom-amd64.tar.gz
|
||||
target_paths: travis-artifacts/$TRAVIS_BUILD_ID
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- gcc-6
|
||||
- g++-6
|
||||
- build-essential
|
||||
- clang-3.3
|
||||
- fakeroot
|
||||
- git
|
||||
- libsecret-1-dev
|
||||
|
||||
@@ -197,6 +197,19 @@ Both issue lists are sorted by total number of comments. While not perfect, numb
|
||||
|
||||
If you want to read about using Atom or developing packages in Atom, the [Atom Flight Manual](http://flight-manual.atom.io) is free and available online. You can find the source to the manual in [atom/flight-manual.atom.io](https://github.com/atom/flight-manual.atom.io).
|
||||
|
||||
#### Local development
|
||||
|
||||
All packages can be developed locally, by checking out the corresponding repository and registering the package to Atom with `apm`:
|
||||
|
||||
```
|
||||
$ git clone url-to-git-repository
|
||||
$ cd path-to-package/
|
||||
$ apm link -d
|
||||
$ atom -d .
|
||||
```
|
||||
|
||||
By running Atom with the `-d` flag, you signal it to run with development packages installed. `apm link` makes sure that your local repository is loaded by Atom.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
* Fill in [the required template](PULL_REQUEST_TEMPLATE.md)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://circleci.com/gh/atom/atom) [](https://travis-ci.org/atom/atom) [](https://ci.appveyor.com/project/Atom/atom)
|
||||
[](https://david-dm.org/atom/atom)
|
||||
[](http://atom-slack.herokuapp.com/)
|
||||
[](https://atom-slack.herokuapp.com)
|
||||
|
||||
Atom is a hackable text editor for the 21st century, built on [Electron](https://github.com/atom/electron), and based on everything we love about our favorite editors. We designed it to be deeply customizable, but still approachable using the default configuration.
|
||||
|
||||
@@ -16,14 +16,14 @@ By participating, you are expected to uphold this code. Please report unacceptab
|
||||
|
||||
## Documentation
|
||||
|
||||
If you want to read about using Atom or developing packages in Atom, the [Atom Flight Manual](http://flight-manual.atom.io) is free and available online. You can find the source to the manual in [atom/flight-manual.atom.io](https://github.com/atom/flight-manual.atom.io).
|
||||
If you want to read about using Atom or developing packages in Atom, the [Atom Flight Manual](https://flight-manual.atom.io) is free and available online. You can find the source to the manual in [atom/flight-manual.atom.io](https://github.com/atom/flight-manual.atom.io).
|
||||
|
||||
The [API reference](https://atom.io/docs/api) for developing packages is also documented on Atom.io.
|
||||
|
||||
## Installing
|
||||
|
||||
### Prerequisites
|
||||
- [Git](https://git-scm.com/)
|
||||
- [Git](https://git-scm.com)
|
||||
|
||||
### macOS
|
||||
|
||||
@@ -40,7 +40,7 @@ Atom will automatically update when a new release is available.
|
||||
You can also download `atom-windows.zip` (32-bit) or `atom-x64-windows.zip` (64-bit) from the [releases page](https://github.com/atom/atom/releases/latest).
|
||||
The `.zip` version will not automatically update.
|
||||
|
||||
Using [chocolatey](https://chocolatey.org/)? Run `cinst Atom` to install the latest version of Atom.
|
||||
Using [Chocolatey](https://chocolatey.org)? Run `cinst Atom` to install the latest version of Atom.
|
||||
|
||||
### Debian Linux (Ubuntu)
|
||||
|
||||
|
||||
11
SUPPORT.md
Normal file
11
SUPPORT.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Atom Support
|
||||
|
||||
If you're looking for support for Atom there are a lot of options, check out:
|
||||
|
||||
* User Documentation — [The Atom Flight Manual](http://flight-manual.atom.io)
|
||||
* Developer Documentation — [Atom API Documentation](https://atom.io/docs/api/latest)
|
||||
* FAQ — [The Atom FAQ on Discuss](https://discuss.atom.io/c/faq)
|
||||
* Message Board — [Discuss, the official Atom and Electron message board](https://discuss.atom.io)
|
||||
* Chat — [Join the Atom Slack team](http://atom-slack.herokuapp.com/)
|
||||
|
||||
On Discuss and in the Atom Slack team, there are a bunch of helpful community members that should be willing to point you in the right direction.
|
||||
@@ -6,6 +6,6 @@
|
||||
"url": "https://github.com/atom/atom.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"atom-package-manager": "1.18.2"
|
||||
"atom-package-manager": "1.18.4"
|
||||
}
|
||||
}
|
||||
|
||||
44
appveyor.yml
44
appveyor.yml
@@ -18,31 +18,44 @@ platform:
|
||||
environment:
|
||||
global:
|
||||
ATOM_DEV_RESOURCE_PATH: c:\projects\atom
|
||||
TEST_JUNIT_XML_ROOT: c:\projects\junit-test-results
|
||||
NODE_VERSION: 6.9.4
|
||||
|
||||
matrix:
|
||||
- NODE_VERSION: 6.8.0
|
||||
- TASK: test
|
||||
- TASK: installer
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
install:
|
||||
- IF NOT EXIST %TEST_JUNIT_XML_ROOT% MKDIR %TEST_JUNIT_XML_ROOT%
|
||||
- SET PATH=C:\Program Files\Atom\resources\cli;%PATH%
|
||||
- ps: Install-Product node $env:NODE_VERSION $env:PLATFORM
|
||||
- npm install -g npm
|
||||
- npm install -g npm@5.3.0
|
||||
|
||||
build_script:
|
||||
- IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp
|
||||
- SET SQUIRREL_TEMP=C:\sqtemp
|
||||
- CD %APPVEYOR_BUILD_FOLDER%
|
||||
- IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] (
|
||||
script\build.cmd --code-sign --create-windows-installer --compress-artifacts
|
||||
- IF NOT EXIST C:\tmp MKDIR C:\tmp
|
||||
- SET SQUIRREL_TEMP=C:\tmp
|
||||
- IF [%TASK%]==[installer] (
|
||||
IF [%APPVEYOR_REPO_BRANCH:~-9%]==[-releases] (
|
||||
script\build.cmd --code-sign --compress-artifacts --create-windows-installer
|
||||
) ELSE (
|
||||
ECHO Skipping installer and Atom build on non-release branch
|
||||
)
|
||||
) ELSE (
|
||||
ECHO Skipping installer build on non-installer build matrix row &&
|
||||
script\build.cmd --code-sign --compress-artifacts
|
||||
)
|
||||
|
||||
test_script:
|
||||
- script\lint.cmd
|
||||
- script\test.cmd
|
||||
- IF [%TASK%]==[test] (
|
||||
script\lint.cmd &&
|
||||
script\test.cmd
|
||||
) ELSE (
|
||||
ECHO Skipping tests on installer build matrix row
|
||||
)
|
||||
|
||||
deploy: off
|
||||
artifacts:
|
||||
@@ -58,10 +71,17 @@ artifacts:
|
||||
name: atom-full.nupkg
|
||||
|
||||
cache:
|
||||
- '%APPVEYOR_BUILD_FOLDER%\script\node_modules'
|
||||
- '%APPVEYOR_BUILD_FOLDER%\apm\node_modules'
|
||||
- '%APPVEYOR_BUILD_FOLDER%\node_modules'
|
||||
- '%APPVEYOR_BUILD_FOLDER%\electron'
|
||||
- '%USERPROFILE%\.atom\.apm'
|
||||
- '%USERPROFILE%\.atom\compile-cache'
|
||||
- '%USERPROFILE%\.atom\snapshot-cache'
|
||||
|
||||
on_finish:
|
||||
- ps: |
|
||||
$wc = New-Object 'System.Net.WebClient'
|
||||
$endpoint = "https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)"
|
||||
Write-Output "Searching for JUnit XML output beneath $($env:TEST_JUNIT_XML_ROOT)"
|
||||
Get-ChildItem -Path $env:TEST_JUNIT_XML_ROOT -Recurse -File -Name -Include "*.xml" | ForEach-Object {
|
||||
$full = "$($env:TEST_JUNIT_XML_ROOT)\$($_)"
|
||||
Write-Output "Uploading JUnit XML file $($full)"
|
||||
$wc.UploadFile($endpoint, $full)
|
||||
}
|
||||
|
||||
7
atom.sh
7
atom.sh
@@ -31,6 +31,9 @@ while getopts ":wtfvh-:" opt; do
|
||||
foreground|benchmark|benchmark-test|test)
|
||||
EXPECT_OUTPUT=1
|
||||
;;
|
||||
enable-electron-logging)
|
||||
export ELECTRON_ENABLE_LOGGING=1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
w)
|
||||
@@ -50,10 +53,6 @@ if [ $REDIRECT_STDERR ]; then
|
||||
exec 2> /dev/null
|
||||
fi
|
||||
|
||||
if [ $EXPECT_OUTPUT ]; then
|
||||
export ELECTRON_ENABLE_LOGGING=1
|
||||
fi
|
||||
|
||||
if [ $OS == 'Mac' ]; then
|
||||
if [ -L "$0" ]; then
|
||||
SCRIPT="$(readlink "$0")"
|
||||
|
||||
@@ -3,6 +3,7 @@ machine:
|
||||
XCODE_SCHEME: test
|
||||
XCODE_WORKSPACE: test
|
||||
XCODE_PROJECT: test
|
||||
TEST_JUNIT_XML_ROOT: ${CIRCLE_TEST_REPORTS}
|
||||
|
||||
xcode:
|
||||
version: 7.3
|
||||
@@ -18,7 +19,7 @@ dependencies:
|
||||
- curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.3/install.sh | bash
|
||||
- nvm install 6.9.4
|
||||
- nvm use 6.9.4
|
||||
- npm install -g npm
|
||||
- npm install -g npm@5.3.0
|
||||
|
||||
override:
|
||||
- script/build --code-sign --compress-artifacts
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Requirements
|
||||
|
||||
* Node.js 6.x or later (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom)
|
||||
* Node.js 6.9.4 or later (the architecture of node available to the build system will determine whether you build 32-bit or 64-bit Atom)
|
||||
* Python v2.7.x
|
||||
* The python.exe must be available at `%SystemDrive%\Python27\python.exe`. If it is installed elsewhere create a symbolic link to the directory containing the python.exe using: `mklink /d %SystemDrive%\Python27 D:\elsewhere\Python27`
|
||||
* 7zip (7z.exe available from the command line) - for creating distribution zip files
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Open the dev tools with `alt-cmd-i`
|
||||
* Evaluate `process.versions.electron` in the console.
|
||||
* Based on this version, download the appropriate Electron symbols from the [releases](https://github.com/atom/electron/releases) page.
|
||||
* The file name should look like `electron-v0.X.Y-darwin-x64-dsym.zip`.
|
||||
* The file name should look like `electron-v1.X.Y-darwin-x64-dsym.zip`.
|
||||
* Decompress these symbols in your `~/Downloads` directory.
|
||||
* Now create a time profile in Instruments.
|
||||
* Open `Instruments.app`.
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
# 'ctrl-p': 'core:move-down'
|
||||
#
|
||||
# You can find more information about keymaps in these guides:
|
||||
# * http://flight-manual.atom.io/using-atom/sections/basic-customization/#_customizing_keybindings
|
||||
# * http://flight-manual.atom.io/using-atom/sections/basic-customization/#customizing-keybindings
|
||||
# * http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth/
|
||||
#
|
||||
# If you're having trouble with your keybindings not working, try the
|
||||
@@ -29,4 +29,4 @@
|
||||
# This file uses CoffeeScript Object Notation (CSON).
|
||||
# If you are unfamiliar with CSON, you can read more about it in the
|
||||
# Atom Flight Manual:
|
||||
# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson
|
||||
# http://flight-manual.atom.io/using-atom/sections/basic-customization/#configuring-with-cson
|
||||
|
||||
@@ -7,6 +7,7 @@ import BufferedNodeProcess from '../src/buffered-node-process'
|
||||
import BufferedProcess from '../src/buffered-process'
|
||||
import GitRepository from '../src/git-repository'
|
||||
import Notification from '../src/notification'
|
||||
import {watchPath} from '../src/path-watcher'
|
||||
|
||||
const atomExport = {
|
||||
BufferedNodeProcess,
|
||||
@@ -20,7 +21,8 @@ const atomExport = {
|
||||
Directory,
|
||||
Emitter,
|
||||
Disposable,
|
||||
CompositeDisposable
|
||||
CompositeDisposable,
|
||||
watchPath
|
||||
}
|
||||
|
||||
// Shell integration is required by both Squirrel and Settings-View
|
||||
|
||||
88
package.json
88
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "atom",
|
||||
"productName": "Atom",
|
||||
"version": "1.20.0-dev",
|
||||
"version": "1.21.0-dev",
|
||||
"description": "A hackable text editor for the 21st Century.",
|
||||
"main": "./src/main-process/main.js",
|
||||
"repository": {
|
||||
@@ -14,8 +14,9 @@
|
||||
"license": "MIT",
|
||||
"electronVersion": "1.6.9",
|
||||
"dependencies": {
|
||||
"@atom/source-map-support": "^0.3.4",
|
||||
"async": "0.2.6",
|
||||
"atom-keymap": "8.1.2",
|
||||
"atom-keymap": "8.2.3",
|
||||
"atom-select-list": "^0.1.0",
|
||||
"atom-ui": "0.4.1",
|
||||
"babel-core": "5.8.38",
|
||||
@@ -38,6 +39,7 @@
|
||||
"glob": "^7.1.1",
|
||||
"grim": "1.5.0",
|
||||
"jasmine-json": "~0.0",
|
||||
"jasmine-reporters": "1.1.0",
|
||||
"jasmine-tagged": "^1.1.4",
|
||||
"key-path-helpers": "^0.4.0",
|
||||
"less-cache": "1.1.0",
|
||||
@@ -45,8 +47,11 @@
|
||||
"marked": "^0.3.6",
|
||||
"minimatch": "^3.0.3",
|
||||
"mocha": "2.5.1",
|
||||
"mocha-junit-reporter": "^1.13.0",
|
||||
"mocha-multi-reporters": "^1.1.4",
|
||||
"mock-spawn": "^0.2.6",
|
||||
"normalize-package-data": "^2.0.0",
|
||||
"nsfw": "^1.0.15",
|
||||
"nslog": "^3",
|
||||
"oniguruma": "6.2.1",
|
||||
"pathwatcher": "7.1.0",
|
||||
@@ -63,9 +68,8 @@
|
||||
"semver": "^4.3.3",
|
||||
"service-hub": "^0.7.4",
|
||||
"sinon": "1.17.4",
|
||||
"@atom/source-map-support": "^0.3.4",
|
||||
"temp": "^0.8.3",
|
||||
"text-buffer": "13.0.1",
|
||||
"text-buffer": "13.0.9",
|
||||
"typescript-simple": "1.0.0",
|
||||
"underscore-plus": "^1.6.6",
|
||||
"winreg": "^1.2.1",
|
||||
@@ -78,91 +82,91 @@
|
||||
"atom-light-ui": "0.46.0",
|
||||
"base16-tomorrow-dark-theme": "1.5.0",
|
||||
"base16-tomorrow-light-theme": "1.5.0",
|
||||
"one-dark-ui": "1.10.5",
|
||||
"one-light-ui": "1.10.5",
|
||||
"one-dark-syntax": "1.7.1",
|
||||
"one-light-syntax": "1.7.1",
|
||||
"one-dark-ui": "1.10.6",
|
||||
"one-light-ui": "1.10.6",
|
||||
"one-dark-syntax": "1.8.0",
|
||||
"one-light-syntax": "1.8.0",
|
||||
"solarized-dark-syntax": "1.1.2",
|
||||
"solarized-light-syntax": "1.1.2",
|
||||
"about": "1.7.6",
|
||||
"archive-view": "0.63.3",
|
||||
"autocomplete-atom-api": "0.10.1",
|
||||
"autocomplete-css": "0.16.2",
|
||||
"autocomplete-atom-api": "0.10.2",
|
||||
"autocomplete-css": "0.17.2",
|
||||
"autocomplete-html": "0.8.0",
|
||||
"autocomplete-plus": "2.35.5",
|
||||
"autocomplete-plus": "2.35.7",
|
||||
"autocomplete-snippets": "1.11.0",
|
||||
"autoflow": "0.29.0",
|
||||
"autosave": "0.24.3",
|
||||
"background-tips": "0.27.1",
|
||||
"bookmarks": "0.44.4",
|
||||
"bracket-matcher": "0.86.0",
|
||||
"bracket-matcher": "0.87.3",
|
||||
"command-palette": "0.40.4",
|
||||
"dalek": "0.2.1",
|
||||
"deprecation-cop": "0.56.7",
|
||||
"dev-live-reload": "0.47.1",
|
||||
"encoding-selector": "0.23.4",
|
||||
"exception-reporting": "0.41.4",
|
||||
"find-and-replace": "0.208.3",
|
||||
"find-and-replace": "0.209.5",
|
||||
"fuzzy-finder": "1.5.8",
|
||||
"github": "0.3.4",
|
||||
"github": "0.4.2",
|
||||
"git-diff": "1.3.6",
|
||||
"go-to-line": "0.32.1",
|
||||
"grammar-selector": "0.49.5",
|
||||
"image-view": "0.61.2",
|
||||
"image-view": "0.62.3",
|
||||
"incompatible-packages": "0.27.3",
|
||||
"keybinding-resolver": "0.38.0",
|
||||
"line-ending-selector": "0.7.2",
|
||||
"line-ending-selector": "0.7.3",
|
||||
"link": "0.31.3",
|
||||
"markdown-preview": "0.159.12",
|
||||
"metrics": "1.2.5",
|
||||
"markdown-preview": "0.159.13",
|
||||
"metrics": "1.2.6",
|
||||
"notifications": "0.67.2",
|
||||
"open-on-github": "1.2.1",
|
||||
"package-generator": "1.1.1",
|
||||
"settings-view": "0.250.0",
|
||||
"settings-view": "0.251.4",
|
||||
"snippets": "1.1.4",
|
||||
"spell-check": "0.71.4",
|
||||
"spell-check": "0.72.0",
|
||||
"status-bar": "1.8.11",
|
||||
"styleguide": "0.49.6",
|
||||
"symbols-view": "0.116.1",
|
||||
"symbols-view": "0.117.0",
|
||||
"tabs": "0.106.2",
|
||||
"timecop": "0.36.0",
|
||||
"tree-view": "0.217.2",
|
||||
"tree-view": "0.217.6",
|
||||
"update-package-dependencies": "0.12.0",
|
||||
"welcome": "0.36.4",
|
||||
"whitespace": "0.37.1",
|
||||
"welcome": "0.36.5",
|
||||
"whitespace": "0.37.2",
|
||||
"wrap-guide": "0.40.2",
|
||||
"language-c": "0.58.1",
|
||||
"language-clojure": "0.22.3",
|
||||
"language-coffee-script": "0.48.7",
|
||||
"language-clojure": "0.22.4",
|
||||
"language-coffee-script": "0.48.9",
|
||||
"language-csharp": "0.14.2",
|
||||
"language-css": "0.42.2",
|
||||
"language-gfm": "0.89.1",
|
||||
"language-css": "0.42.4",
|
||||
"language-gfm": "0.90.0",
|
||||
"language-git": "0.19.1",
|
||||
"language-go": "0.44.1",
|
||||
"language-go": "0.44.2",
|
||||
"language-html": "0.47.3",
|
||||
"language-hyperlink": "0.16.1",
|
||||
"language-java": "0.27.2",
|
||||
"language-javascript": "0.126.1",
|
||||
"language-hyperlink": "0.16.2",
|
||||
"language-java": "0.27.3",
|
||||
"language-javascript": "0.127.2",
|
||||
"language-json": "0.19.1",
|
||||
"language-less": "0.32.0",
|
||||
"language-less": "0.33.0",
|
||||
"language-make": "0.22.3",
|
||||
"language-mustache": "0.14.1",
|
||||
"language-objective-c": "0.15.1",
|
||||
"language-perl": "0.37.0",
|
||||
"language-php": "0.39.0",
|
||||
"language-php": "0.41.0",
|
||||
"language-property-list": "0.9.1",
|
||||
"language-python": "0.45.3",
|
||||
"language-ruby": "0.71.1",
|
||||
"language-python": "0.45.4",
|
||||
"language-ruby": "0.71.3",
|
||||
"language-ruby-on-rails": "0.25.2",
|
||||
"language-sass": "0.59.0",
|
||||
"language-shellscript": "0.25.1",
|
||||
"language-sass": "0.61.0",
|
||||
"language-shellscript": "0.25.2",
|
||||
"language-source": "0.9.0",
|
||||
"language-sql": "0.25.6",
|
||||
"language-sql": "0.25.8",
|
||||
"language-text": "0.7.3",
|
||||
"language-todo": "0.29.1",
|
||||
"language-todo": "0.29.2",
|
||||
"language-toml": "0.18.1",
|
||||
"language-xml": "0.35.1",
|
||||
"language-yaml": "0.30.0"
|
||||
"language-xml": "0.35.2",
|
||||
"language-yaml": "0.30.1"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -3,18 +3,20 @@
|
||||
SET EXPECT_OUTPUT=
|
||||
SET WAIT=
|
||||
SET PSARGS=%*
|
||||
SET ELECTRON_ENABLE_LOGGING=
|
||||
|
||||
FOR %%a IN (%*) DO (
|
||||
IF /I "%%a"=="-f" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="--foreground" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="-h" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="--help" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="-t" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="--test" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="--benchmark" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="--benchmark-test" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="-v" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="--version" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="-f" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="--foreground" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="-h" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="--help" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="-t" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="--test" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="--benchmark" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="--benchmark-test" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="-v" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="--version" SET EXPECT_OUTPUT=YES
|
||||
IF /I "%%a"=="--enable-electron-logging" SET ELECTRON_ENABLE_LOGGING=YES
|
||||
IF /I "%%a"=="-w" (
|
||||
SET EXPECT_OUTPUT=YES
|
||||
SET WAIT=YES
|
||||
@@ -26,7 +28,6 @@ FOR %%a IN (%*) DO (
|
||||
)
|
||||
|
||||
IF "%EXPECT_OUTPUT%"=="YES" (
|
||||
SET ELECTRON_ENABLE_LOGGING=YES
|
||||
IF "%WAIT%"=="YES" (
|
||||
powershell -noexit "Start-Process -FilePath \"%~dp0\..\..\atom.exe\" -ArgumentList \"--pid=$pid $env:PSARGS\" ; wait-event"
|
||||
exit 0
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const childProcess = require('child_process')
|
||||
const CONFIG = require('./config')
|
||||
const cleanDependencies = require('./lib/clean-dependencies')
|
||||
const deleteMsbuildFromPath = require('./lib/delete-msbuild-from-path')
|
||||
@@ -26,6 +27,11 @@ if (process.platform === 'win32') deleteMsbuildFromPath()
|
||||
|
||||
installScriptDependencies()
|
||||
installApm()
|
||||
childProcess.execFileSync(
|
||||
CONFIG.getApmBinPath(),
|
||||
['--version'],
|
||||
{stdio: 'inherit'}
|
||||
)
|
||||
runApmInstall(CONFIG.repositoryRootPath)
|
||||
|
||||
dependenciesFingerprint.write()
|
||||
|
||||
87
script/build
87
script/build
@@ -10,10 +10,12 @@ require('./bootstrap')
|
||||
require('coffee-script/register')
|
||||
require('colors')
|
||||
|
||||
const path = require('path')
|
||||
const yargs = require('yargs')
|
||||
const argv = yargs
|
||||
.usage('Usage: $0 [options]')
|
||||
.help('help')
|
||||
.describe('existing-binaries', 'Use existing Atom binaries (skip clean/transpile/cache)')
|
||||
.describe('code-sign', 'Code-sign executables (macOS and Windows only)')
|
||||
.describe('create-windows-installer', 'Create installer (Windows only)')
|
||||
.describe('create-debian-package', 'Create .deb package (Linux only)')
|
||||
@@ -52,50 +54,67 @@ process.on('unhandledRejection', function (e) {
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
checkChromedriverVersion()
|
||||
cleanOutputDirectory()
|
||||
copyAssets()
|
||||
transpilePackagesWithCustomTranspilerPaths()
|
||||
transpileBabelPaths()
|
||||
transpileCoffeeScriptPaths()
|
||||
transpileCsonPaths()
|
||||
transpilePegJsPaths()
|
||||
generateModuleCache()
|
||||
prebuildLessCache()
|
||||
generateMetadata()
|
||||
generateAPIDocs()
|
||||
dumpSymbols()
|
||||
const CONFIG = require('./config')
|
||||
let binariesPromise = Promise.resolve()
|
||||
|
||||
if (!argv.existingBinaries) {
|
||||
checkChromedriverVersion()
|
||||
cleanOutputDirectory()
|
||||
copyAssets()
|
||||
transpilePackagesWithCustomTranspilerPaths()
|
||||
transpileBabelPaths()
|
||||
transpileCoffeeScriptPaths()
|
||||
transpileCsonPaths()
|
||||
transpilePegJsPaths()
|
||||
generateModuleCache()
|
||||
prebuildLessCache()
|
||||
generateMetadata()
|
||||
generateAPIDocs()
|
||||
binariesPromise = dumpSymbols()
|
||||
}
|
||||
|
||||
binariesPromise
|
||||
.then(packageApplication)
|
||||
.then(packagedAppPath => generateStartupSnapshot(packagedAppPath).then(() => packagedAppPath))
|
||||
.then(packagedAppPath => {
|
||||
if (process.platform === 'darwin') {
|
||||
if (argv.codeSign) {
|
||||
codeSignOnMac(packagedAppPath)
|
||||
} else {
|
||||
console.log('Skipping code-signing. Specify the --code-sign option to perform code-signing'.gray)
|
||||
}
|
||||
} else if (process.platform === 'win32') {
|
||||
if (argv.createWindowsInstaller) {
|
||||
return createWindowsInstaller(packagedAppPath, argv.codeSign).then(() => packagedAppPath)
|
||||
} else {
|
||||
console.log('Skipping creating installer. Specify the --create-windows-installer option to create a Squirrel-based Windows installer.'.gray)
|
||||
switch (process.platform) {
|
||||
case 'darwin': {
|
||||
if (argv.codeSign) {
|
||||
codeSignOnWindows(packagedAppPath)
|
||||
codeSignOnMac(packagedAppPath)
|
||||
} else {
|
||||
console.log('Skipping code-signing. Specify the --code-sign option to perform code-signing'.gray)
|
||||
}
|
||||
}
|
||||
} else if (process.platform === 'linux') {
|
||||
if (argv.createDebianPackage) {
|
||||
createDebianPackage(packagedAppPath)
|
||||
} else {
|
||||
console.log('Skipping creating debian package. Specify the --create-debian-package option to create it.'.gray)
|
||||
case 'win32': {
|
||||
if (argv.codeSign) {
|
||||
const executablesToSign = [ path.join(packagedAppPath, 'Atom.exe') ]
|
||||
if (argv.createWindowsInstaller) {
|
||||
executablesToSign.push(path.join(__dirname, 'node_modules', 'electron-winstaller', 'vendor', 'Update.exe'))
|
||||
}
|
||||
codeSignOnWindows(executablesToSign)
|
||||
} else {
|
||||
console.log('Skipping code-signing. Specify the --code-sign option to perform code-signing'.gray)
|
||||
}
|
||||
if (argv.createWindowsInstaller) {
|
||||
return createWindowsInstaller(packagedAppPath)
|
||||
.then(() => argv.codeSign && codeSignOnWindows([ path.join(CONFIG.buildOutputPath, 'AtomSetup.exe') ]))
|
||||
.then(() => packagedAppPath)
|
||||
} else {
|
||||
console.log('Skipping creating installer. Specify the --create-windows-installer option to create a Squirrel-based Windows installer.'.gray)
|
||||
}
|
||||
}
|
||||
case 'linux': {
|
||||
if (argv.createDebianPackage) {
|
||||
createDebianPackage(packagedAppPath)
|
||||
} else {
|
||||
console.log('Skipping creating debian package. Specify the --create-debian-package option to create it.'.gray)
|
||||
}
|
||||
|
||||
if (argv.createRpmPackage) {
|
||||
createRpmPackage(packagedAppPath)
|
||||
} else {
|
||||
console.log('Skipping creating rpm package. Specify the --create-rpm-package option to create it.'.gray)
|
||||
if (argv.createRpmPackage) {
|
||||
createRpmPackage(packagedAppPath)
|
||||
} else {
|
||||
console.log('Skipping creating rpm package. Specify the --create-rpm-package option to create it.'.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
6
script/create-installer.cmd
Normal file
6
script/create-installer.cmd
Normal file
@@ -0,0 +1,6 @@
|
||||
@ECHO OFF
|
||||
IF NOT EXIST C:\sqtemp MKDIR C:\sqtemp
|
||||
SET SQUIRREL_TEMP=C:\sqtemp
|
||||
del script\package-lock.json /q
|
||||
del apm\package-lock.json /q
|
||||
script\build.cmd --existing-binaries --code-sign --create-windows-installer
|
||||
@@ -875,10 +875,6 @@
|
||||
"hasDeprecations": true,
|
||||
"latestHasDeprecations": false
|
||||
},
|
||||
"language-typescript": {
|
||||
"hasAlternative": true,
|
||||
"alternative": "atom-typescript"
|
||||
},
|
||||
"laravel-facades": {
|
||||
"version": "<=1.0.0",
|
||||
"hasDeprecations": true,
|
||||
|
||||
@@ -4,10 +4,7 @@ const os = require('os')
|
||||
const path = require('path')
|
||||
const {spawnSync} = require('child_process')
|
||||
|
||||
// This is only used when specifying --code-sign WITHOUT --create-windows-installer
|
||||
// as Squirrel has to take care of code-signing in order to correctly wrap during the building of setup
|
||||
|
||||
module.exports = function (packagedAppPath) {
|
||||
module.exports = function (filesToSign) {
|
||||
if (!process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL && !process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) {
|
||||
console.log('Skipping code signing because the ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable is not defined'.gray)
|
||||
return
|
||||
@@ -19,25 +16,26 @@ module.exports = function (packagedAppPath) {
|
||||
downloadFileFromGithub(process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath)
|
||||
}
|
||||
try {
|
||||
console.log(`Code-signing application at ${packagedAppPath}`)
|
||||
signFile(path.join(packagedAppPath, 'atom.exe'))
|
||||
for (const fileToSign of filesToSign) {
|
||||
console.log(`Code-signing executable at ${fileToSign}`)
|
||||
signFile(fileToSign)
|
||||
}
|
||||
} finally {
|
||||
if (!process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) {
|
||||
console.log(`Deleting certificate at ${certPath}`)
|
||||
fs.removeSync(certPath)
|
||||
}
|
||||
}
|
||||
|
||||
function signFile (filePath) {
|
||||
function signFile (fileToSign) {
|
||||
const signCommand = path.resolve(__dirname, '..', 'node_modules', 'electron-winstaller', 'vendor', 'signtool.exe')
|
||||
const args = [ // Changing any of these should also be done in create-windows-installer.js
|
||||
const args = [
|
||||
'sign',
|
||||
`/f ${certPath}`, // Signing cert file
|
||||
`/p ${process.env.ATOM_WIN_CODE_SIGNING_CERT_PASSWORD}`, // Signing cert password
|
||||
'/fd sha256', // File digest algorithm
|
||||
'/tr http://timestamp.digicert.com', // Time stamp server
|
||||
'/td sha256', // Times stamp algorithm
|
||||
`"${filePath}"`
|
||||
`"${fileToSign}"`
|
||||
]
|
||||
const result = spawnSync(signCommand, args, {stdio: 'inherit', shell: true})
|
||||
if (result.status !== 0) {
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
'use strict'
|
||||
|
||||
const downloadFileFromGithub = require('./download-file-from-github')
|
||||
const electronInstaller = require('electron-winstaller')
|
||||
const fs = require('fs-extra')
|
||||
const glob = require('glob')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const spawnSync = require('./spawn-sync')
|
||||
|
||||
const CONFIG = require('../config')
|
||||
|
||||
module.exports = (packagedAppPath, codeSign) => {
|
||||
module.exports = (packagedAppPath) => {
|
||||
const archSuffix = process.arch === 'ia32' ? '' : '-' + process.arch
|
||||
const options = {
|
||||
appDirectory: packagedAppPath,
|
||||
@@ -23,32 +20,7 @@ module.exports = (packagedAppPath, codeSign) => {
|
||||
setupIcon: path.join(CONFIG.repositoryRootPath, 'resources', 'app-icons', CONFIG.channel, 'atom.ico')
|
||||
}
|
||||
|
||||
const signing = codeSign && (process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL || process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH)
|
||||
let certPath = process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH
|
||||
|
||||
if (signing) {
|
||||
if (!certPath) {
|
||||
certPath = path.join(os.tmpdir(), 'win.p12')
|
||||
downloadFileFromGithub(process.env.ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL, certPath)
|
||||
}
|
||||
|
||||
var signParams = [] // Changing any of these should also be done in code-sign-on-windows.js
|
||||
signParams.push(`/f ${certPath}`) // Signing cert file
|
||||
signParams.push(`/p ${process.env.ATOM_WIN_CODE_SIGNING_CERT_PASSWORD}`) // Signing cert password
|
||||
signParams.push('/fd sha256') // File digest algorithm
|
||||
signParams.push('/tr http://timestamp.digicert.com') // Time stamp server
|
||||
signParams.push('/td sha256') // Times stamp algorithm
|
||||
options.signWithParams = signParams.join(' ')
|
||||
} else {
|
||||
console.log('Skipping code-signing. Specify the --code-sign option and provide a ATOM_WIN_CODE_SIGNING_CERT_DOWNLOAD_URL environment variable to perform code-signing'.gray)
|
||||
}
|
||||
|
||||
const cleanUp = () => {
|
||||
if (fs.existsSync(certPath) && !process.env.ATOM_WIN_CODE_SIGNING_CERT_PATH) {
|
||||
console.log(`Deleting certificate at ${certPath}`)
|
||||
fs.removeSync(certPath)
|
||||
}
|
||||
|
||||
for (let nupkgPath of glob.sync(`${CONFIG.buildOutputPath}/*.nupkg`)) {
|
||||
if (!nupkgPath.includes(CONFIG.appMetadata.version)) {
|
||||
console.log(`Deleting downloaded nupkg for previous version at ${nupkgPath} to prevent it from being stored as an artifact`)
|
||||
@@ -57,24 +29,8 @@ module.exports = (packagedAppPath, codeSign) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Squirrel signs its own copy of the executables but we need them for the portable ZIP
|
||||
const extractSignedExes = () => {
|
||||
if (signing) {
|
||||
for (let nupkgPath of glob.sync(`${CONFIG.buildOutputPath}/*-full.nupkg`)) {
|
||||
if (nupkgPath.includes(CONFIG.appMetadata.version)) {
|
||||
nupkgPath = path.resolve(nupkgPath) // Switch from forward-slash notation
|
||||
console.log(`Extracting signed executables from ${nupkgPath} for use in portable zip`)
|
||||
spawnSync('7z.exe', ['e', nupkgPath, 'lib\\net45\\*.exe', '-aoa', `-o${packagedAppPath}`])
|
||||
spawnSync(process.env.COMSPEC, ['/c', 'move', '/y', path.join(packagedAppPath, 'squirrel.exe'), path.join(packagedAppPath, 'update.exe')])
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Creating Windows Installer for ${packagedAppPath}`)
|
||||
return electronInstaller.createWindowsInstaller(options)
|
||||
.then(extractSignedExes)
|
||||
.then(cleanUp, error => {
|
||||
cleanUp()
|
||||
return Promise.reject(error)
|
||||
|
||||
@@ -39,6 +39,7 @@ module.exports = function (packagedAppPath) {
|
||||
relativePath === path.join('..', 'node_modules', 'decompress-zip', 'lib', 'decompress-zip.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'debug', 'node.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'fs-extra', 'lib', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'github', 'node_modules', 'fs-extra', 'lib', 'index.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'git-utils', 'lib', 'git.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'glob', 'glob.js') ||
|
||||
relativePath === path.join('..', 'node_modules', 'graceful-fs', 'graceful-fs.js') ||
|
||||
|
||||
@@ -9,20 +9,20 @@
|
||||
"csslint": "1.0.2",
|
||||
"donna": "1.0.16",
|
||||
"electron-chromedriver": "~1.6",
|
||||
"electron-link": "0.1.0",
|
||||
"electron-link": "0.1.1",
|
||||
"electron-mksnapshot": "~1.6",
|
||||
"electron-packager": "7.3.0",
|
||||
"electron-winstaller": "2.6.2",
|
||||
"fs-extra": "0.30.0",
|
||||
"glob": "7.0.3",
|
||||
"joanna": "0.0.8",
|
||||
"joanna": "0.0.9",
|
||||
"klaw-sync": "^1.1.2",
|
||||
"legal-eagle": "0.14.0",
|
||||
"lodash.template": "4.4.0",
|
||||
"minidump": "0.9.0",
|
||||
"mkdirp": "0.5.1",
|
||||
"normalize-package-data": "2.3.5",
|
||||
"npm": "3.10.5",
|
||||
"npm": "5.3.0",
|
||||
"passwd-user": "2.1.0",
|
||||
"pegjs": "0.9.0",
|
||||
"runas": "3.1.1",
|
||||
|
||||
46
script/test
46
script/test
@@ -32,18 +32,29 @@ if (process.platform === 'darwin') {
|
||||
throw new Error('Running tests on this platform is not supported.')
|
||||
}
|
||||
|
||||
function prepareEnv (suiteName) {
|
||||
const env = Object.assign({}, process.env)
|
||||
|
||||
if (process.env.TEST_JUNIT_XML_ROOT) {
|
||||
// Tell Jasmine to output this suite's results as a JUnit XML file to a subdirectory of the root, so that a
|
||||
// CI system can interpret it.
|
||||
const outputPath = path.join(process.env.TEST_JUNIT_XML_ROOT, suiteName, 'test-results.xml')
|
||||
env.TEST_JUNIT_XML_PATH = outputPath
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
function runCoreMainProcessTests (callback) {
|
||||
const testPath = path.join(CONFIG.repositoryRootPath, 'spec', 'main-process')
|
||||
const testArguments = [
|
||||
'--resource-path', resourcePath,
|
||||
'--test', '--main-process', testPath
|
||||
]
|
||||
const testEnv = Object.assign({}, prepareEnv('core-main-process'), {ATOM_GITHUB_INLINE_GIT_EXEC: 'true'})
|
||||
|
||||
console.log('Executing core main process tests'.bold.green)
|
||||
const cp = childProcess.spawn(executablePath, testArguments, {
|
||||
stdio: 'inherit',
|
||||
env: Object.assign({}, process.env, {ATOM_GITHUB_INLINE_GIT_EXEC: 'true'})
|
||||
})
|
||||
const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv})
|
||||
cp.on('error', error => { callback(error) })
|
||||
cp.on('close', exitCode => { callback(null, exitCode) })
|
||||
}
|
||||
@@ -54,9 +65,10 @@ function runCoreRenderProcessTests (callback) {
|
||||
'--resource-path', resourcePath,
|
||||
'--test', testPath
|
||||
]
|
||||
const testEnv = prepareEnv('core-render-process')
|
||||
|
||||
console.log('Executing core render process tests'.bold.green)
|
||||
const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit'})
|
||||
const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv})
|
||||
cp.on('error', error => { callback(error) })
|
||||
cp.on('close', exitCode => { callback(null, exitCode) })
|
||||
}
|
||||
@@ -87,10 +99,10 @@ for (let packageName in CONFIG.appMetadata.packageDependencies) {
|
||||
'--resource-path', resourcePath,
|
||||
'--test', testFolder
|
||||
]
|
||||
const testEnv = prepareEnv(`bundled-package-${packageName}`)
|
||||
|
||||
const pkgJsonPath = path.join(repositoryPackagePath, 'package.json')
|
||||
const nodeModulesPath = path.join(repositoryPackagePath, 'node_modules')
|
||||
const nodeModulesBackupPath = path.join(repositoryPackagePath, 'node_modules.bak')
|
||||
let finalize = () => null
|
||||
if (require(pkgJsonPath).atomTestRunner) {
|
||||
console.log(`Installing test runner dependencies for ${packageName}`.bold.green)
|
||||
@@ -105,7 +117,7 @@ for (let packageName in CONFIG.appMetadata.packageDependencies) {
|
||||
} else {
|
||||
console.log(`Executing ${packageName} tests`.bold.green)
|
||||
}
|
||||
const cp = childProcess.spawn(executablePath, testArguments)
|
||||
const cp = childProcess.spawn(executablePath, testArguments, {env: testEnv})
|
||||
let stderrOutput = ''
|
||||
cp.stderr.on('data', data => { stderrOutput += data })
|
||||
cp.stdout.on('data', data => { stderrOutput += data })
|
||||
@@ -127,21 +139,27 @@ for (let packageName in CONFIG.appMetadata.packageDependencies) {
|
||||
function runBenchmarkTests (callback) {
|
||||
const benchmarksPath = path.join(CONFIG.repositoryRootPath, 'benchmarks')
|
||||
const testArguments = ['--benchmark-test', benchmarksPath]
|
||||
const testEnv = prepareEnv('benchmark')
|
||||
|
||||
console.log('Executing benchmark tests'.bold.green)
|
||||
const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit'})
|
||||
const cp = childProcess.spawn(executablePath, testArguments, {stdio: 'inherit', env: testEnv})
|
||||
cp.on('error', error => { callback(error) })
|
||||
cp.on('close', exitCode => { callback(null, exitCode) })
|
||||
}
|
||||
|
||||
let testSuitesToRun = testSuitesForPlatform(process.platform)
|
||||
|
||||
function testSuitesForPlatform(platform) {
|
||||
switch(platform) {
|
||||
case 'darwin': return [runCoreMainProcessTests, runCoreRenderProcessTests, runBenchmarkTests].concat(packageTestSuites)
|
||||
case 'win32': return (process.arch === 'x64') ? [runCoreMainProcessTests, runCoreRenderProcessTests] : [runCoreMainProcessTests]
|
||||
case 'linux': return [runCoreMainProcessTests]
|
||||
default: return []
|
||||
function testSuitesForPlatform (platform) {
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
return [runCoreMainProcessTests, runCoreRenderProcessTests, runBenchmarkTests].concat(packageTestSuites)
|
||||
case 'win32':
|
||||
return (process.arch === 'x64') ? [runCoreMainProcessTests, runCoreRenderProcessTests] : [runCoreMainProcessTests]
|
||||
case 'linux':
|
||||
return [runCoreMainProcessTests]
|
||||
default:
|
||||
console.log(`Unrecognized platform: ${platform}`)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ export function afterEach (fn) {
|
||||
|
||||
['it', 'fit', 'ffit', 'fffit'].forEach(function (name) {
|
||||
module.exports[name] = function (description, fn) {
|
||||
if (fn === undefined) {
|
||||
global[name](description)
|
||||
return
|
||||
}
|
||||
|
||||
global[name](description, function () {
|
||||
const result = fn()
|
||||
if (result instanceof Promise) {
|
||||
@@ -29,7 +34,7 @@ export function afterEach (fn) {
|
||||
}
|
||||
})
|
||||
|
||||
export async function conditionPromise (condition) {
|
||||
export async function conditionPromise (condition) {
|
||||
const startTime = Date.now()
|
||||
|
||||
while (true) {
|
||||
@@ -40,7 +45,7 @@ export async function conditionPromise (condition) {
|
||||
}
|
||||
|
||||
if (Date.now() - startTime > 5000) {
|
||||
throw new Error("Timed out waiting on condition")
|
||||
throw new Error('Timed out waiting on condition')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,3 +77,27 @@ export function emitterEventPromise (emitter, event, timeout = 15000) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function promisify (original) {
|
||||
return function (...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
args.push((err, ...results) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(...results)
|
||||
}
|
||||
})
|
||||
|
||||
return original(...args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function promisifySome (obj, fnNames) {
|
||||
const result = {}
|
||||
for (const fnName of fnNames) {
|
||||
result[fnName] = promisify(obj[fnName])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ StorageFolder = require '../src/storage-folder'
|
||||
|
||||
describe "AtomEnvironment", ->
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe 'window sizing methods', ->
|
||||
describe '::getPosition and ::setPosition', ->
|
||||
|
||||
@@ -86,7 +86,11 @@ describe("AtomPaths", () => {
|
||||
afterEach(() => {
|
||||
delete process.env.ATOM_HOME
|
||||
fs.removeSync(electronUserDataPath)
|
||||
temp.cleanupSync()
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
app.setPath('userData', defaultElectronUserDataPath)
|
||||
})
|
||||
|
||||
|
||||
@@ -19,7 +19,8 @@ describe "Babel transpiler support", ->
|
||||
|
||||
afterEach ->
|
||||
CompileCache.setCacheDirectory(originalCacheDir)
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe 'when a .js file starts with /** @babel */;', ->
|
||||
it "transpiles it using babel", ->
|
||||
|
||||
@@ -21,7 +21,8 @@ describe "CommandInstaller on #darwin", ->
|
||||
spyOn(CommandInstaller::, 'getInstallDirectory').andReturn(installationPath)
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
it "shows an error dialog when installing commands interactively fails", ->
|
||||
appDelegate = jasmine.createSpyObj("appDelegate", ["confirm"])
|
||||
|
||||
@@ -23,7 +23,8 @@ describe 'CompileCache', ->
|
||||
afterEach ->
|
||||
CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
|
||||
CSON.setCacheDir(CompileCache.getCacheDirectory())
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe 'addPathToCache(filePath, atomHome)', ->
|
||||
describe 'when the given file is plain javascript', ->
|
||||
|
||||
@@ -10,7 +10,8 @@ describe "DefaultDirectoryProvider", ->
|
||||
tmp = temp.mkdirSync('atom-spec-default-dir-provider')
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe ".directoryForURISync(uri)", ->
|
||||
it "returns a Directory with a path that matches the uri", ->
|
||||
|
||||
@@ -9,12 +9,15 @@ describe('Dock', () => {
|
||||
it('opens the dock and activates its active pane', () => {
|
||||
jasmine.attachToDOM(atom.workspace.getElement())
|
||||
const dock = atom.workspace.getLeftDock()
|
||||
const didChangeVisibleSpy = jasmine.createSpy()
|
||||
dock.onDidChangeVisible(didChangeVisibleSpy)
|
||||
|
||||
expect(dock.isVisible()).toBe(false)
|
||||
expect(document.activeElement).toBe(atom.workspace.getCenter().getActivePane().getElement())
|
||||
dock.activate()
|
||||
expect(dock.isVisible()).toBe(true)
|
||||
expect(document.activeElement).toBe(dock.getActivePane().getElement())
|
||||
expect(didChangeVisibleSpy).toHaveBeenCalledWith(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,17 +25,24 @@ describe('Dock', () => {
|
||||
it('transfers focus back to the active center pane if the dock had focus', () => {
|
||||
jasmine.attachToDOM(atom.workspace.getElement())
|
||||
const dock = atom.workspace.getLeftDock()
|
||||
const didChangeVisibleSpy = jasmine.createSpy()
|
||||
dock.onDidChangeVisible(didChangeVisibleSpy)
|
||||
|
||||
dock.activate()
|
||||
expect(document.activeElement).toBe(dock.getActivePane().getElement())
|
||||
expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(true)
|
||||
|
||||
dock.hide()
|
||||
expect(document.activeElement).toBe(atom.workspace.getCenter().getActivePane().getElement())
|
||||
expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(false)
|
||||
|
||||
dock.activate()
|
||||
expect(document.activeElement).toBe(dock.getActivePane().getElement())
|
||||
expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(true)
|
||||
|
||||
dock.toggle()
|
||||
expect(document.activeElement).toBe(atom.workspace.getCenter().getActivePane().getElement())
|
||||
expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(false)
|
||||
|
||||
// Don't change focus if the dock was not focused in the first place
|
||||
const modalElement = document.createElement('div')
|
||||
@@ -43,9 +53,11 @@ describe('Dock', () => {
|
||||
|
||||
dock.show()
|
||||
expect(document.activeElement).toBe(modalElement)
|
||||
expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(true)
|
||||
|
||||
dock.hide()
|
||||
expect(document.activeElement).toBe(modalElement)
|
||||
expect(didChangeVisibleSpy.mostRecentCall.args[0]).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ describe "GitRepositoryProvider", ->
|
||||
provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm)
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe ".repositoryForDirectory(directory)", ->
|
||||
describe "when specified a Directory with a Git repository", ->
|
||||
|
||||
@@ -24,7 +24,8 @@ describe "the `grammars` global", ->
|
||||
afterEach ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe ".selectGrammar(filePath)", ->
|
||||
it "always returns a grammar", ->
|
||||
|
||||
@@ -7,6 +7,10 @@ module.exports = ({logFile, headless, testPaths, buildAtomEnvironment}) ->
|
||||
window[key] = value for key, value of require '../vendor/jasmine'
|
||||
require 'jasmine-tagged'
|
||||
|
||||
if process.env.TEST_JUNIT_XML_PATH
|
||||
require 'jasmine-reporters'
|
||||
jasmine.getEnv().addReporter new jasmine.JUnitXmlReporter(process.env.TEST_JUNIT_XML_PATH, true, true)
|
||||
|
||||
# Allow document.title to be assigned in specs without screwing up spec window title
|
||||
documentTitle = null
|
||||
Object.defineProperty document, 'title',
|
||||
|
||||
@@ -14,10 +14,11 @@ const ATOM_RESOURCE_PATH = path.resolve(__dirname, '..', '..')
|
||||
describe('AtomApplication', function () {
|
||||
this.timeout(60 * 1000)
|
||||
|
||||
let originalAppQuit, originalAtomHome, atomApplicationsToDestroy
|
||||
let originalAppQuit, originalShowMessageBox, originalAtomHome, atomApplicationsToDestroy
|
||||
|
||||
beforeEach(function () {
|
||||
originalAppQuit = electron.app.quit
|
||||
originalShowMessageBox = electron.dialog.showMessageBox
|
||||
mockElectronAppQuit()
|
||||
originalAtomHome = process.env.ATOM_HOME
|
||||
process.env.ATOM_HOME = makeTempDir('atom-home')
|
||||
@@ -39,6 +40,7 @@ describe('AtomApplication', function () {
|
||||
}
|
||||
await clearElectronSession()
|
||||
electron.app.quit = originalAppQuit
|
||||
electron.dialog.showMessageBox = originalShowMessageBox
|
||||
})
|
||||
|
||||
describe('launch', function () {
|
||||
@@ -462,20 +464,42 @@ describe('AtomApplication', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('before quitting', function () {
|
||||
it('waits until all the windows have saved their state and then quits', async function () {
|
||||
const dirAPath = makeTempDir("a")
|
||||
const dirBPath = makeTempDir("b")
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'file-a')]))
|
||||
await focusWindow(window1)
|
||||
const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')]))
|
||||
await focusWindow(window2)
|
||||
electron.app.quit()
|
||||
assert(!electron.app.hasQuitted())
|
||||
await Promise.all([window1.lastPrepareToUnloadPromise, window2.lastPrepareToUnloadPromise])
|
||||
assert(electron.app.hasQuitted())
|
||||
it('waits until all the windows have saved their state before quitting', async function () {
|
||||
const dirAPath = makeTempDir("a")
|
||||
const dirBPath = makeTempDir("b")
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'file-a')]))
|
||||
await focusWindow(window1)
|
||||
const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')]))
|
||||
await focusWindow(window2)
|
||||
electron.app.quit()
|
||||
assert(!electron.app.hasQuitted())
|
||||
await Promise.all([window1.lastPrepareToUnloadPromise, window2.lastPrepareToUnloadPromise])
|
||||
assert(electron.app.hasQuitted())
|
||||
})
|
||||
|
||||
it('prevents quitting if user cancels when prompted to save an item', async () => {
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window1 = atomApplication.launch(parseCommandLine([]))
|
||||
const window2 = atomApplication.launch(parseCommandLine([]))
|
||||
await Promise.all([window1.loadedPromise, window2.loadedPromise])
|
||||
await evalInWebContents(window1.browserWindow.webContents, function (sendBackToMainProcess) {
|
||||
atom.workspace.getActiveTextEditor().insertText('unsaved text')
|
||||
sendBackToMainProcess()
|
||||
})
|
||||
|
||||
// Choosing "Cancel"
|
||||
mockElectronShowMessageBox({choice: 1})
|
||||
electron.app.quit()
|
||||
await atomApplication.lastBeforeQuitPromise
|
||||
assert(!electron.app.hasQuitted())
|
||||
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())
|
||||
})
|
||||
|
||||
function buildAtomApplication () {
|
||||
@@ -496,6 +520,12 @@ 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 shouldQuit = true
|
||||
electron.app.emit('before-quit', {preventDefault: () => { shouldQuit = false }})
|
||||
if (shouldQuit) {
|
||||
@@ -507,8 +537,15 @@ describe('AtomApplication', function () {
|
||||
}
|
||||
}
|
||||
|
||||
function mockElectronShowMessageBox ({choice}) {
|
||||
electron.dialog.showMessageBox = function () {
|
||||
return choice
|
||||
}
|
||||
}
|
||||
|
||||
function makeTempDir (name) {
|
||||
return fs.realpathSync(require('temp').mkdirSync(name))
|
||||
const temp = require('temp').track()
|
||||
return fs.realpathSync(temp.mkdirSync(name))
|
||||
}
|
||||
|
||||
let channelIdCounter = 0
|
||||
|
||||
@@ -16,7 +16,11 @@ describe("FileRecoveryService", () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
temp.cleanupSync()
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
})
|
||||
|
||||
describe("when no crash happens during a save", () => {
|
||||
|
||||
@@ -7,7 +7,23 @@ import {assert} from 'chai'
|
||||
export default function (testPaths) {
|
||||
global.assert = assert
|
||||
|
||||
const mocha = new Mocha({reporter: 'spec'})
|
||||
let reporterOptions = {
|
||||
reporterEnabled: 'list'
|
||||
}
|
||||
|
||||
if (process.env.TEST_JUNIT_XML_PATH) {
|
||||
reporterOptions = {
|
||||
reporterEnabled: 'list, mocha-junit-reporter',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
mochaFile: process.env.TEST_JUNIT_XML_PATH
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mocha = new Mocha({
|
||||
reporter: 'mocha-multi-reporters',
|
||||
reporterOptions
|
||||
})
|
||||
for (let testPath of testPaths) {
|
||||
if (fs.isDirectorySync(testPath)) {
|
||||
for (let testFilePath of fs.listTreeSync(testPath)) {
|
||||
|
||||
@@ -9,7 +9,8 @@ describe 'ModuleCache', ->
|
||||
spyOn(Module, '_findPath').andCallThrough()
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
it 'resolves Electron module paths without hitting the filesystem', ->
|
||||
builtins = ModuleCache.cache.builtins
|
||||
|
||||
362
spec/native-watcher-registry-spec.js
Normal file
362
spec/native-watcher-registry-spec.js
Normal file
@@ -0,0 +1,362 @@
|
||||
/** @babel */
|
||||
|
||||
import {it, beforeEach} from './async-spec-helpers'
|
||||
|
||||
import path from 'path'
|
||||
import {Emitter} from 'event-kit'
|
||||
|
||||
import {NativeWatcherRegistry} from '../src/native-watcher-registry'
|
||||
|
||||
function findRootDirectory () {
|
||||
let current = process.cwd()
|
||||
while (true) {
|
||||
let next = path.resolve(current, '..')
|
||||
if (next === current) {
|
||||
return next
|
||||
} else {
|
||||
current = next
|
||||
}
|
||||
}
|
||||
}
|
||||
const ROOT = findRootDirectory()
|
||||
|
||||
function absolute (...parts) {
|
||||
const candidate = path.join(...parts)
|
||||
return path.isAbsolute(candidate) ? candidate : path.join(ROOT, candidate)
|
||||
}
|
||||
|
||||
function parts (fullPath) {
|
||||
return fullPath.split(path.sep).filter(part => part.length > 0)
|
||||
}
|
||||
|
||||
class MockWatcher {
|
||||
constructor (normalizedPath) {
|
||||
this.normalizedPath = normalizedPath
|
||||
this.native = null
|
||||
}
|
||||
|
||||
getNormalizedPathPromise () {
|
||||
return Promise.resolve(this.normalizedPath)
|
||||
}
|
||||
|
||||
attachToNative (native, nativePath) {
|
||||
if (this.normalizedPath.startsWith(nativePath)) {
|
||||
if (this.native) {
|
||||
this.native.attached = this.native.attached.filter(each => each !== this)
|
||||
}
|
||||
this.native = native
|
||||
this.native.attached.push(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MockNative {
|
||||
constructor (name) {
|
||||
this.name = name
|
||||
this.attached = []
|
||||
this.disposed = false
|
||||
this.stopped = false
|
||||
|
||||
this.emitter = new Emitter()
|
||||
}
|
||||
|
||||
reattachTo (newNative, nativePath) {
|
||||
for (const watcher of this.attached) {
|
||||
watcher.attachToNative(newNative, nativePath)
|
||||
}
|
||||
}
|
||||
|
||||
onWillStop (callback) {
|
||||
return this.emitter.on('will-stop', callback)
|
||||
}
|
||||
|
||||
dispose () {
|
||||
this.disposed = true
|
||||
}
|
||||
|
||||
stop () {
|
||||
this.stopped = true
|
||||
this.emitter.emit('will-stop')
|
||||
}
|
||||
}
|
||||
|
||||
describe('NativeWatcherRegistry', function () {
|
||||
let createNative, registry
|
||||
|
||||
beforeEach(function () {
|
||||
registry = new NativeWatcherRegistry(normalizedPath => createNative(normalizedPath))
|
||||
})
|
||||
|
||||
it('attaches a Watcher to a newly created NativeWatcher for a new directory', async function () {
|
||||
const watcher = new MockWatcher(absolute('some', 'path'))
|
||||
const NATIVE = new MockNative('created')
|
||||
createNative = () => NATIVE
|
||||
|
||||
await registry.attach(watcher)
|
||||
|
||||
expect(watcher.native).toBe(NATIVE)
|
||||
})
|
||||
|
||||
it('reuses an existing NativeWatcher on the same directory', async function () {
|
||||
const EXISTING = new MockNative('existing')
|
||||
const existingPath = absolute('existing', 'path')
|
||||
let firstTime = true
|
||||
createNative = () => {
|
||||
if (firstTime) {
|
||||
firstTime = false
|
||||
return EXISTING
|
||||
}
|
||||
|
||||
return new MockNative('nope')
|
||||
}
|
||||
await registry.attach(new MockWatcher(existingPath))
|
||||
|
||||
const watcher = new MockWatcher(existingPath)
|
||||
await registry.attach(watcher)
|
||||
|
||||
expect(watcher.native).toBe(EXISTING)
|
||||
})
|
||||
|
||||
it('attaches to an existing NativeWatcher on a parent directory', async function () {
|
||||
const EXISTING = new MockNative('existing')
|
||||
const parentDir = absolute('existing', 'path')
|
||||
const subDir = path.join(parentDir, 'sub', 'directory')
|
||||
let firstTime = true
|
||||
createNative = () => {
|
||||
if (firstTime) {
|
||||
firstTime = false
|
||||
return EXISTING
|
||||
}
|
||||
|
||||
return new MockNative('nope')
|
||||
}
|
||||
await registry.attach(new MockWatcher(parentDir))
|
||||
|
||||
const watcher = new MockWatcher(subDir)
|
||||
await registry.attach(watcher)
|
||||
|
||||
expect(watcher.native).toBe(EXISTING)
|
||||
})
|
||||
|
||||
it('adopts Watchers from NativeWatchers on child directories', async function () {
|
||||
const parentDir = absolute('existing', 'path')
|
||||
const childDir0 = path.join(parentDir, 'child', 'directory', 'zero')
|
||||
const childDir1 = path.join(parentDir, 'child', 'directory', 'one')
|
||||
const otherDir = absolute('another', 'path')
|
||||
|
||||
const CHILD0 = new MockNative('existing0')
|
||||
const CHILD1 = new MockNative('existing1')
|
||||
const OTHER = new MockNative('existing2')
|
||||
const PARENT = new MockNative('parent')
|
||||
|
||||
createNative = dir => {
|
||||
if (dir === childDir0) {
|
||||
return CHILD0
|
||||
} else if (dir === childDir1) {
|
||||
return CHILD1
|
||||
} else if (dir === otherDir) {
|
||||
return OTHER
|
||||
} else if (dir === parentDir) {
|
||||
return PARENT
|
||||
} else {
|
||||
throw new Error(`Unexpected path: ${dir}`)
|
||||
}
|
||||
}
|
||||
|
||||
const watcher0 = new MockWatcher(childDir0)
|
||||
await registry.attach(watcher0)
|
||||
|
||||
const watcher1 = new MockWatcher(childDir1)
|
||||
await registry.attach(watcher1)
|
||||
|
||||
const watcher2 = new MockWatcher(otherDir)
|
||||
await registry.attach(watcher2)
|
||||
|
||||
expect(watcher0.native).toBe(CHILD0)
|
||||
expect(watcher1.native).toBe(CHILD1)
|
||||
expect(watcher2.native).toBe(OTHER)
|
||||
|
||||
// Consolidate all three watchers beneath the same native watcher on the parent directory
|
||||
const watcher = new MockWatcher(parentDir)
|
||||
await registry.attach(watcher)
|
||||
|
||||
expect(watcher.native).toBe(PARENT)
|
||||
|
||||
expect(watcher0.native).toBe(PARENT)
|
||||
expect(CHILD0.stopped).toBe(true)
|
||||
expect(CHILD0.disposed).toBe(true)
|
||||
|
||||
expect(watcher1.native).toBe(PARENT)
|
||||
expect(CHILD1.stopped).toBe(true)
|
||||
expect(CHILD1.disposed).toBe(true)
|
||||
|
||||
expect(watcher2.native).toBe(OTHER)
|
||||
expect(OTHER.stopped).toBe(false)
|
||||
expect(OTHER.disposed).toBe(false)
|
||||
})
|
||||
|
||||
describe('removing NativeWatchers', function () {
|
||||
it('happens when they stop', async function () {
|
||||
const STOPPED = new MockNative('stopped')
|
||||
const RUNNING = new MockNative('running')
|
||||
|
||||
const stoppedPath = absolute('watcher', 'that', 'will', 'be', 'stopped')
|
||||
const stoppedPathParts = stoppedPath.split(path.sep).filter(part => part.length > 0)
|
||||
const runningPath = absolute('watcher', 'that', 'will', 'continue', 'to', 'exist')
|
||||
const runningPathParts = runningPath.split(path.sep).filter(part => part.length > 0)
|
||||
|
||||
createNative = dir => {
|
||||
if (dir === stoppedPath) {
|
||||
return STOPPED
|
||||
} else if (dir === runningPath) {
|
||||
return RUNNING
|
||||
} else {
|
||||
throw new Error(`Unexpected path: ${dir}`)
|
||||
}
|
||||
}
|
||||
|
||||
const stoppedWatcher = new MockWatcher(stoppedPath)
|
||||
await registry.attach(stoppedWatcher)
|
||||
|
||||
const runningWatcher = new MockWatcher(runningPath)
|
||||
await registry.attach(runningWatcher)
|
||||
|
||||
STOPPED.stop()
|
||||
|
||||
const runningNode = registry.tree.root.lookup(runningPathParts).when({
|
||||
parent: node => node,
|
||||
missing: () => false,
|
||||
children: () => false
|
||||
})
|
||||
expect(runningNode).toBeTruthy()
|
||||
expect(runningNode.getNativeWatcher()).toBe(RUNNING)
|
||||
|
||||
const stoppedNode = registry.tree.root.lookup(stoppedPathParts).when({
|
||||
parent: () => false,
|
||||
missing: () => true,
|
||||
children: () => false
|
||||
})
|
||||
expect(stoppedNode).toBe(true)
|
||||
})
|
||||
|
||||
it('reassigns new child watchers when a parent watcher is stopped', async function () {
|
||||
const CHILD0 = new MockNative('child0')
|
||||
const CHILD1 = new MockNative('child1')
|
||||
const PARENT = new MockNative('parent')
|
||||
|
||||
const parentDir = absolute('parent')
|
||||
const childDir0 = path.join(parentDir, 'child0')
|
||||
const childDir1 = path.join(parentDir, 'child1')
|
||||
|
||||
createNative = dir => {
|
||||
if (dir === parentDir) {
|
||||
return PARENT
|
||||
} else if (dir === childDir0) {
|
||||
return CHILD0
|
||||
} else if (dir === childDir1) {
|
||||
return CHILD1
|
||||
} else {
|
||||
throw new Error(`Unexpected directory ${dir}`)
|
||||
}
|
||||
}
|
||||
|
||||
const parentWatcher = new MockWatcher(parentDir)
|
||||
const childWatcher0 = new MockWatcher(childDir0)
|
||||
const childWatcher1 = new MockWatcher(childDir1)
|
||||
|
||||
await registry.attach(parentWatcher)
|
||||
await Promise.all([
|
||||
registry.attach(childWatcher0),
|
||||
registry.attach(childWatcher1)
|
||||
])
|
||||
|
||||
// All three watchers should share the parent watcher's native watcher.
|
||||
expect(parentWatcher.native).toBe(PARENT)
|
||||
expect(childWatcher0.native).toBe(PARENT)
|
||||
expect(childWatcher1.native).toBe(PARENT)
|
||||
|
||||
// Stopping the parent should detach and recreate the child watchers.
|
||||
PARENT.stop()
|
||||
|
||||
expect(childWatcher0.native).toBe(CHILD0)
|
||||
expect(childWatcher1.native).toBe(CHILD1)
|
||||
|
||||
expect(registry.tree.root.lookup(parts(parentDir)).when({
|
||||
parent: () => false,
|
||||
missing: () => false,
|
||||
children: () => true
|
||||
})).toBe(true)
|
||||
|
||||
expect(registry.tree.root.lookup(parts(childDir0)).when({
|
||||
parent: () => true,
|
||||
missing: () => false,
|
||||
children: () => false
|
||||
})).toBe(true)
|
||||
|
||||
expect(registry.tree.root.lookup(parts(childDir1)).when({
|
||||
parent: () => true,
|
||||
missing: () => false,
|
||||
children: () => false
|
||||
})).toBe(true)
|
||||
})
|
||||
|
||||
it('consolidates children when splitting a parent watcher', async function () {
|
||||
const CHILD0 = new MockNative('child0')
|
||||
const PARENT = new MockNative('parent')
|
||||
|
||||
const parentDir = absolute('parent')
|
||||
const childDir0 = path.join(parentDir, 'child0')
|
||||
const childDir1 = path.join(parentDir, 'child0', 'child1')
|
||||
|
||||
createNative = dir => {
|
||||
if (dir === parentDir) {
|
||||
return PARENT
|
||||
} else if (dir === childDir0) {
|
||||
return CHILD0
|
||||
} else {
|
||||
throw new Error(`Unexpected directory ${dir}`)
|
||||
}
|
||||
}
|
||||
|
||||
const parentWatcher = new MockWatcher(parentDir)
|
||||
const childWatcher0 = new MockWatcher(childDir0)
|
||||
const childWatcher1 = new MockWatcher(childDir1)
|
||||
|
||||
await registry.attach(parentWatcher)
|
||||
await Promise.all([
|
||||
registry.attach(childWatcher0),
|
||||
registry.attach(childWatcher1)
|
||||
])
|
||||
|
||||
// All three watchers should share the parent watcher's native watcher.
|
||||
expect(parentWatcher.native).toBe(PARENT)
|
||||
expect(childWatcher0.native).toBe(PARENT)
|
||||
expect(childWatcher1.native).toBe(PARENT)
|
||||
|
||||
// Stopping the parent should detach and create the child watchers. Both child watchers should
|
||||
// share the same native watcher.
|
||||
PARENT.stop()
|
||||
|
||||
expect(childWatcher0.native).toBe(CHILD0)
|
||||
expect(childWatcher1.native).toBe(CHILD0)
|
||||
|
||||
expect(registry.tree.root.lookup(parts(parentDir)).when({
|
||||
parent: () => false,
|
||||
missing: () => false,
|
||||
children: () => true
|
||||
})).toBe(true)
|
||||
|
||||
expect(registry.tree.root.lookup(parts(childDir0)).when({
|
||||
parent: () => true,
|
||||
missing: () => false,
|
||||
children: () => false
|
||||
})).toBe(true)
|
||||
|
||||
expect(registry.tree.root.lookup(parts(childDir1)).when({
|
||||
parent: () => true,
|
||||
missing: () => false,
|
||||
children: () => false
|
||||
})).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -17,7 +17,8 @@ describe "PackageManager", ->
|
||||
spyOn(ModuleCache, 'add')
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe "::getApmPath()", ->
|
||||
it "returns the path to the apm command", ->
|
||||
|
||||
@@ -551,10 +551,11 @@ describe('Pane', () => {
|
||||
itemURI = 'test'
|
||||
confirm.andReturn(0)
|
||||
|
||||
await pane.destroyItem(item1)
|
||||
const success = await pane.destroyItem(item1)
|
||||
expect(item1.save).toHaveBeenCalled()
|
||||
expect(pane.getItems().includes(item1)).toBe(false)
|
||||
expect(item1.isDestroyed()).toBe(true)
|
||||
expect(success).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -565,11 +566,12 @@ describe('Pane', () => {
|
||||
showSaveDialog.andReturn('/selected/path')
|
||||
confirm.andReturn(0)
|
||||
|
||||
await pane.destroyItem(item1)
|
||||
const success = await pane.destroyItem(item1)
|
||||
expect(showSaveDialog).toHaveBeenCalled()
|
||||
expect(item1.saveAs).toHaveBeenCalledWith('/selected/path')
|
||||
expect(pane.getItems().includes(item1)).toBe(false)
|
||||
expect(item1.isDestroyed()).toBe(true)
|
||||
expect(success).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -578,10 +580,11 @@ describe('Pane', () => {
|
||||
it('removes and destroys the item without saving it', async () => {
|
||||
confirm.andReturn(2)
|
||||
|
||||
await pane.destroyItem(item1)
|
||||
const success = await pane.destroyItem(item1)
|
||||
expect(item1.save).not.toHaveBeenCalled()
|
||||
expect(pane.getItems().includes(item1)).toBe(false)
|
||||
expect(item1.isDestroyed()).toBe(true)
|
||||
expect(success).toBe(true);
|
||||
})
|
||||
})
|
||||
|
||||
@@ -589,19 +592,21 @@ describe('Pane', () => {
|
||||
it('does not save, remove, or destroy the item', async () => {
|
||||
confirm.andReturn(1)
|
||||
|
||||
await pane.destroyItem(item1)
|
||||
const success = await pane.destroyItem(item1)
|
||||
expect(item1.save).not.toHaveBeenCalled()
|
||||
expect(pane.getItems().includes(item1)).toBe(true)
|
||||
expect(item1.isDestroyed()).toBe(false)
|
||||
expect(success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when force=true', () => {
|
||||
it('destroys the item immediately', async () => {
|
||||
await pane.destroyItem(item1, true)
|
||||
const success = await pane.destroyItem(item1, true)
|
||||
expect(item1.save).not.toHaveBeenCalled()
|
||||
expect(pane.getItems().includes(item1)).toBe(false)
|
||||
expect(item1.isDestroyed()).toBe(true)
|
||||
expect(success).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -630,18 +635,20 @@ describe('Pane', () => {
|
||||
})
|
||||
|
||||
describe('when passed a permanent dock item', () => {
|
||||
it("doesn't destroy the item", () => {
|
||||
it("doesn't destroy the item", async () => {
|
||||
spyOn(item1, 'isPermanentDockItem').andReturn(true)
|
||||
pane.destroyItem(item1)
|
||||
const success = await pane.destroyItem(item1)
|
||||
expect(pane.getItems().includes(item1)).toBe(true)
|
||||
expect(item1.isDestroyed()).toBe(false)
|
||||
expect(success).toBe(false);
|
||||
})
|
||||
|
||||
it('destroy the item if force=true', () => {
|
||||
it('destroy the item if force=true', async () => {
|
||||
spyOn(item1, 'isPermanentDockItem').andReturn(true)
|
||||
pane.destroyItem(item1, true)
|
||||
const success = await pane.destroyItem(item1, true)
|
||||
expect(pane.getItems().includes(item1)).toBe(false)
|
||||
expect(item1.isDestroyed()).toBe(true)
|
||||
expect(success).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
186
spec/path-watcher-spec.js
Normal file
186
spec/path-watcher-spec.js
Normal file
@@ -0,0 +1,186 @@
|
||||
/** @babel */
|
||||
|
||||
import {it, beforeEach, afterEach, promisifySome} from './async-spec-helpers'
|
||||
import tempCb from 'temp'
|
||||
import fsCb from 'fs-plus'
|
||||
import path from 'path'
|
||||
|
||||
import {CompositeDisposable} from 'event-kit'
|
||||
import {watchPath, stopAllWatchers} from '../src/path-watcher'
|
||||
|
||||
tempCb.track()
|
||||
|
||||
const fs = promisifySome(fsCb, ['writeFile', 'mkdir', 'symlink', 'appendFile', 'realpath'])
|
||||
const temp = promisifySome(tempCb, ['mkdir'])
|
||||
|
||||
describe('watchPath', function () {
|
||||
let subs
|
||||
|
||||
beforeEach(function () {
|
||||
subs = new CompositeDisposable()
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
subs.dispose()
|
||||
await stopAllWatchers()
|
||||
})
|
||||
|
||||
function waitForChanges (watcher, ...fileNames) {
|
||||
const waiting = new Set(fileNames)
|
||||
let fired = false
|
||||
const relevantEvents = []
|
||||
|
||||
return new Promise(resolve => {
|
||||
const sub = watcher.onDidChange(events => {
|
||||
for (const event of events) {
|
||||
if (waiting.delete(event.path)) {
|
||||
relevantEvents.push(event)
|
||||
}
|
||||
}
|
||||
|
||||
if (!fired && waiting.size === 0) {
|
||||
fired = true
|
||||
resolve(relevantEvents)
|
||||
sub.dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe('watchPath()', function () {
|
||||
it('resolves getStartPromise() when the watcher begins listening', async function () {
|
||||
const rootDir = await temp.mkdir('atom-fsmanager-test-')
|
||||
|
||||
const watcher = watchPath(rootDir, {}, () => {})
|
||||
await watcher.getStartPromise()
|
||||
})
|
||||
|
||||
it('reuses an existing native watcher and resolves getStartPromise immediately if attached to a running watcher', async function () {
|
||||
const rootDir = await temp.mkdir('atom-fsmanager-test-')
|
||||
|
||||
const watcher0 = watchPath(rootDir, {}, () => {})
|
||||
await watcher0.getStartPromise()
|
||||
|
||||
const watcher1 = watchPath(rootDir, {}, () => {})
|
||||
await watcher1.getStartPromise()
|
||||
|
||||
expect(watcher0.native).toBe(watcher1.native)
|
||||
})
|
||||
|
||||
it("reuses existing native watchers even while they're still starting", async function () {
|
||||
const rootDir = await temp.mkdir('atom-fsmanager-test-')
|
||||
|
||||
const watcher0 = watchPath(rootDir, {}, () => {})
|
||||
await watcher0.getAttachedPromise()
|
||||
expect(watcher0.native.isRunning()).toBe(false)
|
||||
|
||||
const watcher1 = watchPath(rootDir, {}, () => {})
|
||||
await watcher1.getAttachedPromise()
|
||||
|
||||
expect(watcher0.native).toBe(watcher1.native)
|
||||
|
||||
await Promise.all([watcher0.getStartPromise(), watcher1.getStartPromise()])
|
||||
})
|
||||
|
||||
it("doesn't attach new watchers to a native watcher that's stopping", async function () {
|
||||
const rootDir = await temp.mkdir('atom-fsmanager-test-')
|
||||
|
||||
const watcher0 = watchPath(rootDir, {}, () => {})
|
||||
await watcher0.getStartPromise()
|
||||
const native0 = watcher0.native
|
||||
|
||||
watcher0.dispose()
|
||||
|
||||
const watcher1 = watchPath(rootDir, {}, () => {})
|
||||
|
||||
expect(watcher1.native).not.toBe(native0)
|
||||
})
|
||||
|
||||
it('reuses an existing native watcher on a parent directory and filters events', async function () {
|
||||
const rootDir = await temp.mkdir('atom-fsmanager-test-').then(fs.realpath)
|
||||
const rootFile = path.join(rootDir, 'rootfile.txt')
|
||||
const subDir = path.join(rootDir, 'subdir')
|
||||
const subFile = path.join(subDir, 'subfile.txt')
|
||||
|
||||
await fs.mkdir(subDir)
|
||||
|
||||
// Keep the watchers alive with an undisposed subscription
|
||||
const rootWatcher = watchPath(rootDir, {}, () => {})
|
||||
const childWatcher = watchPath(subDir, {}, () => {})
|
||||
|
||||
await Promise.all([
|
||||
rootWatcher.getStartPromise(),
|
||||
childWatcher.getStartPromise()
|
||||
])
|
||||
|
||||
expect(rootWatcher.native).toBe(childWatcher.native)
|
||||
expect(rootWatcher.native.isRunning()).toBe(true)
|
||||
|
||||
const firstChanges = Promise.all([
|
||||
waitForChanges(rootWatcher, subFile),
|
||||
waitForChanges(childWatcher, subFile)
|
||||
])
|
||||
|
||||
await fs.writeFile(subFile, 'subfile\n', {encoding: 'utf8'})
|
||||
await firstChanges
|
||||
|
||||
const nextRootEvent = waitForChanges(rootWatcher, rootFile)
|
||||
await fs.writeFile(rootFile, 'rootfile\n', {encoding: 'utf8'})
|
||||
|
||||
await nextRootEvent
|
||||
})
|
||||
|
||||
it('adopts existing child watchers and filters events appropriately to them', async function () {
|
||||
const parentDir = await temp.mkdir('atom-fsmanager-test-').then(fs.realpath)
|
||||
|
||||
// Create the directory tree
|
||||
const rootFile = path.join(parentDir, 'rootfile.txt')
|
||||
const subDir0 = path.join(parentDir, 'subdir0')
|
||||
const subFile0 = path.join(subDir0, 'subfile0.txt')
|
||||
const subDir1 = path.join(parentDir, 'subdir1')
|
||||
const subFile1 = path.join(subDir1, 'subfile1.txt')
|
||||
|
||||
await fs.mkdir(subDir0)
|
||||
await fs.mkdir(subDir1)
|
||||
await Promise.all([
|
||||
fs.writeFile(rootFile, 'rootfile\n', {encoding: 'utf8'}),
|
||||
fs.writeFile(subFile0, 'subfile 0\n', {encoding: 'utf8'}),
|
||||
fs.writeFile(subFile1, 'subfile 1\n', {encoding: 'utf8'})
|
||||
])
|
||||
|
||||
// Begin the child watchers and keep them alive
|
||||
const subWatcher0 = watchPath(subDir0, {}, () => {})
|
||||
const subWatcherChanges0 = waitForChanges(subWatcher0, subFile0)
|
||||
|
||||
const subWatcher1 = watchPath(subDir1, {}, () => {})
|
||||
const subWatcherChanges1 = waitForChanges(subWatcher1, subFile1)
|
||||
|
||||
await Promise.all(
|
||||
[subWatcher0, subWatcher1].map(watcher => watcher.getStartPromise())
|
||||
)
|
||||
expect(subWatcher0.native).not.toBe(subWatcher1.native)
|
||||
|
||||
// Create the parent watcher
|
||||
const parentWatcher = watchPath(parentDir, {}, () => {})
|
||||
const parentWatcherChanges = waitForChanges(parentWatcher, rootFile, subFile0, subFile1)
|
||||
|
||||
await parentWatcher.getStartPromise()
|
||||
|
||||
expect(subWatcher0.native).toBe(parentWatcher.native)
|
||||
expect(subWatcher1.native).toBe(parentWatcher.native)
|
||||
|
||||
// Ensure events are filtered correctly
|
||||
await Promise.all([
|
||||
fs.appendFile(rootFile, 'change\n', {encoding: 'utf8'}),
|
||||
fs.appendFile(subFile0, 'change\n', {encoding: 'utf8'}),
|
||||
fs.appendFile(subFile1, 'change\n', {encoding: 'utf8'})
|
||||
])
|
||||
|
||||
await Promise.all([
|
||||
subWatcherChanges0,
|
||||
subWatcherChanges1,
|
||||
parentWatcherChanges
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ Project = require '../src/project'
|
||||
fs = require 'fs-plus'
|
||||
path = require 'path'
|
||||
{Directory} = require 'pathwatcher'
|
||||
{stopAllWatchers} = require '../src/path-watcher'
|
||||
GitRepository = require '../src/git-repository'
|
||||
|
||||
describe "Project", ->
|
||||
@@ -13,9 +14,6 @@ describe "Project", ->
|
||||
# Wait for project's service consumers to be asynchronously added
|
||||
waits(1)
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
|
||||
describe "serialization", ->
|
||||
deserializedProject = null
|
||||
|
||||
@@ -548,6 +546,59 @@ describe "Project", ->
|
||||
atom.project.removePath(ftpURI)
|
||||
expect(atom.project.getPaths()).toEqual []
|
||||
|
||||
describe ".onDidChangeFiles()", ->
|
||||
sub = []
|
||||
events = []
|
||||
checkCallback = ->
|
||||
|
||||
beforeEach ->
|
||||
sub = atom.project.onDidChangeFiles (incoming) ->
|
||||
events.push incoming...
|
||||
checkCallback()
|
||||
|
||||
afterEach ->
|
||||
sub.dispose()
|
||||
|
||||
waitForEvents = (paths) ->
|
||||
remaining = new Set(fs.realpathSync(p) for p in paths)
|
||||
new Promise (resolve, reject) ->
|
||||
checkCallback = ->
|
||||
remaining.delete(event.path) for event in events
|
||||
resolve() if remaining.size is 0
|
||||
|
||||
expire = ->
|
||||
checkCallback = ->
|
||||
console.error "Paths not seen:", Array.from(remaining)
|
||||
reject(new Error('Expired before all expected events were delivered.'))
|
||||
|
||||
checkCallback()
|
||||
setTimeout expire, 2000
|
||||
|
||||
it "reports filesystem changes within project paths", ->
|
||||
dirOne = temp.mkdirSync('atom-spec-project-one')
|
||||
fileOne = path.join(dirOne, 'file-one.txt')
|
||||
fileTwo = path.join(dirOne, 'file-two.txt')
|
||||
dirTwo = temp.mkdirSync('atom-spec-project-two')
|
||||
fileThree = path.join(dirTwo, 'file-three.txt')
|
||||
|
||||
# Ensure that all preexisting watchers are stopped
|
||||
waitsForPromise -> stopAllWatchers()
|
||||
|
||||
runs -> atom.project.setPaths([dirOne])
|
||||
waitsForPromise -> atom.project.watchersByPath[dirOne].getStartPromise()
|
||||
|
||||
runs ->
|
||||
expect(atom.project.watchersByPath[dirTwo]).toEqual undefined
|
||||
|
||||
fs.writeFileSync fileThree, "three\n"
|
||||
fs.writeFileSync fileTwo, "two\n"
|
||||
fs.writeFileSync fileOne, "one\n"
|
||||
|
||||
waitsForPromise -> waitForEvents [fileOne, fileTwo]
|
||||
|
||||
runs ->
|
||||
expect(events.some (event) -> event.path is fileThree).toBeFalsy()
|
||||
|
||||
describe ".onDidAddBuffer()", ->
|
||||
it "invokes the callback with added text buffers", ->
|
||||
buffers = []
|
||||
|
||||
@@ -64,8 +64,8 @@ beforeEach ->
|
||||
atom.project.setPaths([specProjectPath])
|
||||
|
||||
window.resetTimeouts()
|
||||
spyOn(Date, 'now').andCallFake -> window.now
|
||||
spyOn(_._, "now").andCallFake -> window.now
|
||||
spyOn(Date, 'now').andCallFake(-> window.now)
|
||||
spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout
|
||||
spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout
|
||||
|
||||
@@ -188,8 +188,6 @@ jasmine.useRealClock = ->
|
||||
jasmine.useMockClock = ->
|
||||
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
|
||||
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
|
||||
spyOn(Date, 'now').andCallFake(-> window.now)
|
||||
|
||||
|
||||
addCustomMatchers = (spec) ->
|
||||
spec.addMatchers
|
||||
|
||||
@@ -37,7 +37,8 @@ describe "Windows Squirrel Update", ->
|
||||
WinShell.folderBackgroundContextMenu = new FakeShellOption()
|
||||
|
||||
afterEach ->
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
it "quits the app on all squirrel events", ->
|
||||
app = quit: jasmine.createSpy('quit')
|
||||
|
||||
@@ -15,7 +15,11 @@ describe('StyleManager', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
temp.cleanupSync()
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
|
||||
describe('::addStyleSheet(source, params)', () => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,10 @@ describe('TextEditorElement', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jasmineContent = document.body.querySelector('#jasmine-content')
|
||||
// Force scrollbars to be visible regardless of local system configuration
|
||||
const scrollbarStyle = document.createElement('style')
|
||||
scrollbarStyle.textContent = '::-webkit-scrollbar { -webkit-appearance: none }'
|
||||
jasmine.attachToDOM(scrollbarStyle)
|
||||
})
|
||||
|
||||
function buildTextEditorElement (options = {}) {
|
||||
@@ -199,6 +203,49 @@ describe('TextEditorElement', () => {
|
||||
expect(document.activeElement).toBe(element.querySelector('input'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('if focused when invisible due to a zero height and width', () => {
|
||||
it('focuses the hidden input and does not throw an exception', () => {
|
||||
const parentElement = document.createElement('div')
|
||||
parentElement.style.position = 'absolute'
|
||||
parentElement.style.width = '0px'
|
||||
parentElement.style.height = '0px'
|
||||
|
||||
const element = buildTextEditorElement({attach: false})
|
||||
parentElement.appendChild(element)
|
||||
jasmineContent.appendChild(parentElement)
|
||||
|
||||
element.focus()
|
||||
expect(document.activeElement).toBe(element.component.getHiddenInput())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('::setModel', () => {
|
||||
describe('when the element does not have an editor yet', () => {
|
||||
it('uses the supplied one', () => {
|
||||
const element = buildTextEditorElement({attach: false})
|
||||
const editor = new TextEditor()
|
||||
element.setModel(editor)
|
||||
jasmine.attachToDOM(element)
|
||||
expect(editor.element).toBe(element)
|
||||
expect(element.getModel()).toBe(editor)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the element already has an editor', () => {
|
||||
it('unbinds it and then swaps it with the supplied one', async () => {
|
||||
const element = buildTextEditorElement({attach: true})
|
||||
const previousEditor = element.getModel()
|
||||
expect(previousEditor.element).toBe(element)
|
||||
|
||||
const newEditor = new TextEditor()
|
||||
element.setModel(newEditor)
|
||||
expect(previousEditor.element).not.toBe(element)
|
||||
expect(newEditor.element).toBe(element)
|
||||
expect(element.getModel()).toBe(newEditor)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidAttach and ::onDidDetach', () =>
|
||||
|
||||
@@ -9,7 +9,8 @@ describe "atom.themes", ->
|
||||
|
||||
afterEach ->
|
||||
atom.themes.deactivateThemes()
|
||||
temp.cleanupSync()
|
||||
try
|
||||
temp.cleanupSync()
|
||||
|
||||
describe "theme getters and setters", ->
|
||||
beforeEach ->
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
TitleBar = require '../src/title-bar'
|
||||
|
||||
describe "TitleBar", ->
|
||||
it "updates the title based on document.title when the active pane item changes", ->
|
||||
titleBar = new TitleBar({
|
||||
workspace: atom.workspace,
|
||||
themes: atom.themes,
|
||||
applicationDelegate: atom.applicationDelegate,
|
||||
})
|
||||
|
||||
expect(titleBar.element.querySelector('.title').textContent).toBe document.title
|
||||
initialTitle = document.title
|
||||
|
||||
atom.workspace.getActivePane().activateItem({
|
||||
getTitle: -> 'Test Title'
|
||||
})
|
||||
|
||||
expect(document.title).not.toBe(initialTitle)
|
||||
expect(titleBar.element.querySelector('.title').textContent).toBe document.title
|
||||
|
||||
it "can update the sheet offset for the current window based on its height", ->
|
||||
titleBar = new TitleBar({
|
||||
workspace: atom.workspace,
|
||||
themes: atom.themes,
|
||||
applicationDelegate: atom.applicationDelegate,
|
||||
})
|
||||
expect(->
|
||||
titleBar.updateWindowSheetOffset()
|
||||
).not.toThrow()
|
||||
57
spec/title-bar-spec.js
Normal file
57
spec/title-bar-spec.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const TitleBar = require('../src/title-bar')
|
||||
const temp = require('temp').track()
|
||||
|
||||
describe('TitleBar', () => {
|
||||
it('updates its title when document.title changes', () => {
|
||||
const titleBar = new TitleBar({
|
||||
workspace: atom.workspace,
|
||||
themes: atom.themes,
|
||||
applicationDelegate: atom.applicationDelegate
|
||||
})
|
||||
expect(titleBar.element.querySelector('.title').textContent).toBe(document.title)
|
||||
|
||||
const paneItem = new FakePaneItem('Title 1')
|
||||
atom.workspace.getActivePane().activateItem(paneItem)
|
||||
expect(document.title).toMatch('Title 1')
|
||||
expect(titleBar.element.querySelector('.title').textContent).toBe(document.title)
|
||||
|
||||
paneItem.setTitle('Title 2')
|
||||
expect(document.title).toMatch('Title 2')
|
||||
expect(titleBar.element.querySelector('.title').textContent).toBe(document.title)
|
||||
|
||||
atom.project.setPaths([temp.mkdirSync('project-1')])
|
||||
expect(document.title).toMatch('project-1')
|
||||
expect(titleBar.element.querySelector('.title').textContent).toBe(document.title)
|
||||
})
|
||||
|
||||
it('can update the sheet offset for the current window based on its height', () => {
|
||||
const titleBar = new TitleBar({
|
||||
workspace: atom.workspace,
|
||||
themes: atom.themes,
|
||||
applicationDelegate: atom.applicationDelegate
|
||||
})
|
||||
expect(() => titleBar.updateWindowSheetOffset()).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
class FakePaneItem {
|
||||
constructor (title) {
|
||||
this.title = title
|
||||
}
|
||||
|
||||
getTitle () {
|
||||
return this.title
|
||||
}
|
||||
|
||||
onDidChangeTitle (callback) {
|
||||
this.didChangeTitleCallback = callback
|
||||
return {
|
||||
dispose: () => { this.didChangeTitleCallback = null }
|
||||
}
|
||||
}
|
||||
|
||||
setTitle (title) {
|
||||
this.title = title
|
||||
if (this.didChangeTitleCallback) this.didChangeTitleCallback(title)
|
||||
}
|
||||
}
|
||||
@@ -184,7 +184,7 @@ describe "TokenizedBuffer", ->
|
||||
it "schedules the invalidated lines to be tokenized in the background", ->
|
||||
buffer.insert([5, 30], '/* */')
|
||||
buffer.setTextInRange([[2, 0], [3, 0]], '/*')
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js']
|
||||
|
||||
advanceClock()
|
||||
@@ -214,7 +214,7 @@ describe "TokenizedBuffer", ->
|
||||
it "schedules the invalidated lines to be tokenized in the background", ->
|
||||
buffer.insert([5, 30], '/* */')
|
||||
buffer.insert([2, 0], '/*\nabcde\nabcder')
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js']
|
||||
expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js']
|
||||
@@ -596,10 +596,10 @@ describe "TokenizedBuffer", ->
|
||||
{position: Point(0, 9), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []}
|
||||
{position: Point(0, 10), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]}
|
||||
{position: Point(0, 11), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []}
|
||||
{position: Point(0, 12), closeTags: [], openTags: ["syntax--comment syntax--block syntax--js", "syntax--punctuation syntax--definition syntax--comment syntax--js"]}
|
||||
{position: Point(0, 14), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js"], openTags: []}
|
||||
{position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js"]}
|
||||
{position: Point(1, 7), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js", "syntax--comment syntax--block syntax--js"], openTags: ["syntax--storage syntax--type syntax--var syntax--js"]}
|
||||
{position: Point(0, 12), closeTags: [], openTags: ["syntax--comment syntax--block syntax--js", "syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js"]}
|
||||
{position: Point(0, 14), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js"], openTags: []}
|
||||
{position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js"]}
|
||||
{position: Point(1, 7), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js", "syntax--comment syntax--block syntax--js"], openTags: ["syntax--storage syntax--type syntax--var syntax--js"]}
|
||||
{position: Point(1, 10), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []}
|
||||
{position: Point(1, 15), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]}
|
||||
{position: Point(1, 16), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []}
|
||||
|
||||
@@ -28,7 +28,11 @@ describe('updateProcessEnv(launchEnv)', function () {
|
||||
}
|
||||
process.env = originalProcessEnv
|
||||
process.platform = originalProcessPlatform
|
||||
temp.cleanupSync()
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
|
||||
describe('when the launch environment appears to come from a shell', function () {
|
||||
|
||||
@@ -9,7 +9,13 @@ const {Disposable} = require('event-kit')
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers')
|
||||
|
||||
describe('WorkspaceElement', () => {
|
||||
afterEach(() => { temp.cleanupSync() })
|
||||
afterEach(() => {
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
|
||||
describe('when the workspace element is focused', () => {
|
||||
it('transfers focus to the active pane', () => {
|
||||
|
||||
@@ -25,7 +25,13 @@ describe('Workspace', () => {
|
||||
waitsForPromise(() => atom.workspace.itemLocationStore.clear())
|
||||
})
|
||||
|
||||
afterEach(() => temp.cleanupSync())
|
||||
afterEach(() => {
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
})
|
||||
|
||||
function simulateReload() {
|
||||
waitsForPromise(() => {
|
||||
@@ -1582,6 +1588,7 @@ i = /test/; #FIXME\
|
||||
expect(atom2.grammars.getGrammars().map(grammar => grammar.name).sort()).toEqual([
|
||||
'CoffeeScript',
|
||||
'CoffeeScript (Literate)',
|
||||
'JSDoc',
|
||||
'JavaScript',
|
||||
'Null Grammar',
|
||||
'Regular Expression Replacement (JavaScript)',
|
||||
@@ -2387,6 +2394,22 @@ i = /test/; #FIXME\
|
||||
expect(results[0].replacements).toBe(6)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not discard the multiline flag', () => {
|
||||
const filePath = path.join(projectDir, 'sample.js')
|
||||
fs.copyFileSync(path.join(fixturesDir, 'sample.js'), filePath)
|
||||
|
||||
const results = []
|
||||
waitsForPromise(() =>
|
||||
atom.workspace.replace(/;$/gmi, 'items', [filePath], result => results.push(result))
|
||||
)
|
||||
|
||||
runs(() => {
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].filePath).toBe(filePath)
|
||||
expect(results[0].replacements).toBe(8)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a buffer is already open', () => {
|
||||
|
||||
@@ -143,6 +143,7 @@ class ApplicationDelegate
|
||||
message: message
|
||||
detail: detailedMessage
|
||||
buttons: buttonLabels
|
||||
normalizeAccessKeys: true
|
||||
})
|
||||
|
||||
if _.isArray(buttons)
|
||||
|
||||
@@ -938,8 +938,8 @@ class AtomEnvironment extends Model
|
||||
"Would you like to add the #{nouns} to this window, permanently discarding the saved state, " +
|
||||
"or open the #{nouns} in a new window, restoring the saved state?"
|
||||
buttons: [
|
||||
'Open in new window and recover state'
|
||||
'Add to this window and discard state'
|
||||
'&Open in new window and recover state'
|
||||
'&Add to this window and discard state'
|
||||
]
|
||||
if btn is 0
|
||||
@open
|
||||
|
||||
@@ -308,6 +308,21 @@ const configSchema = {
|
||||
description: 'Warn before opening files larger than this number of megabytes.',
|
||||
type: 'number',
|
||||
default: 40
|
||||
},
|
||||
fileSystemWatcher: {
|
||||
description: 'Choose the underlying implementation used to watch for filesystem changes. Emulating changes will miss any events caused by applications other than Atom, but may help prevent crashes or freezes.',
|
||||
type: 'string',
|
||||
default: 'native',
|
||||
enum: [
|
||||
{
|
||||
value: 'native',
|
||||
description: 'Native operating system APIs'
|
||||
},
|
||||
{
|
||||
value: 'atom',
|
||||
description: 'Emulated with Atom events'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -49,7 +49,7 @@ class Cursor extends Model
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Managing Cursor Position
|
||||
|
||||
@@ -105,7 +105,7 @@ class Decoration
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Decoration Details
|
||||
|
||||
37
src/dock.js
37
src/dock.js
@@ -1,7 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
const _ = require('underscore-plus')
|
||||
const {CompositeDisposable} = require('event-kit')
|
||||
const {CompositeDisposable, Emitter} = require('event-kit')
|
||||
const PaneContainer = require('./pane-container')
|
||||
const TextEditor = require('./text-editor')
|
||||
const Grim = require('grim')
|
||||
@@ -35,7 +35,8 @@ module.exports = class Dock {
|
||||
this.notificationManager = params.notificationManager
|
||||
this.viewRegistry = params.viewRegistry
|
||||
this.didActivate = params.didActivate
|
||||
this.didHide = params.didHide
|
||||
|
||||
this.emitter = new Emitter()
|
||||
|
||||
this.paneContainer = new PaneContainer({
|
||||
location: this.location,
|
||||
@@ -53,6 +54,7 @@ module.exports = class Dock {
|
||||
}
|
||||
|
||||
this.subscriptions = new CompositeDisposable(
|
||||
this.emitter,
|
||||
this.paneContainer.onDidActivatePane(() => {
|
||||
this.show()
|
||||
this.didActivate(this)
|
||||
@@ -135,14 +137,12 @@ module.exports = class Dock {
|
||||
setState (newState) {
|
||||
const prevState = this.state
|
||||
const nextState = Object.assign({}, prevState, newState)
|
||||
let didHide = false
|
||||
|
||||
// Update the `shouldAnimate` state. This needs to be written to the DOM before updating the
|
||||
// class that changes the animated property. Normally we'd have to defer the class change a
|
||||
// frame to ensure the property is animated (or not) appropriately, however we luck out in this
|
||||
// case because the drag start always happens before the item is dragged into the toggle button.
|
||||
if (nextState.visible !== prevState.visible) {
|
||||
didHide = !nextState.visible
|
||||
// Never animate toggling visiblity...
|
||||
nextState.shouldAnimate = false
|
||||
} else if (!nextState.visible && nextState.draggingItem && !prevState.draggingItem) {
|
||||
@@ -152,7 +152,11 @@ module.exports = class Dock {
|
||||
|
||||
this.state = nextState
|
||||
this.render(this.state)
|
||||
if (didHide) this.didHide(this)
|
||||
|
||||
const {visible} = this.state
|
||||
if (visible !== prevState.visible) {
|
||||
this.emitter.emit('did-change-visible', visible)
|
||||
}
|
||||
}
|
||||
|
||||
render (state) {
|
||||
@@ -379,12 +383,31 @@ module.exports = class Dock {
|
||||
})
|
||||
}
|
||||
|
||||
// PaneContainer-delegating methods
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Essential: Invoke the given callback when the visibility of the dock changes.
|
||||
//
|
||||
// * `callback` {Function} to be called when the visibility changes.
|
||||
// * `visible` {Boolean} Is the dock now visible?
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeVisible (callback) {
|
||||
return this.emitter.on('did-change-visible', callback)
|
||||
}
|
||||
|
||||
// Essential: Invoke the given callback with the current and all future visibilities of the dock.
|
||||
//
|
||||
// * `callback` {Function} to be called when the visibility changes.
|
||||
// * `visible` {Boolean} Is the dock now visible?
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
observeVisible (callback) {
|
||||
callback(this.isVisible())
|
||||
return this.onDidChangeVisible(callback)
|
||||
}
|
||||
|
||||
// Essential: Invoke the given callback with all current and future panes items
|
||||
// in the dock.
|
||||
//
|
||||
|
||||
@@ -129,7 +129,7 @@ class GitRepository
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
|
||||
@@ -48,7 +48,7 @@ class Gutter
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Visibility
|
||||
|
||||
@@ -120,7 +120,9 @@ class AtomApplication
|
||||
Promise.all(windowsClosePromises).then(=> @disposable.dispose())
|
||||
|
||||
launch: (options) ->
|
||||
if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0 or options.test or options.benchmark or options.benchmarkTest
|
||||
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)
|
||||
@@ -267,10 +269,19 @@ class AtomApplication
|
||||
@openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
|
||||
|
||||
@disposable.add ipcHelpers.on app, 'before-quit', (event) =>
|
||||
unless @quitting
|
||||
resolveBeforeQuitPromise = null
|
||||
@lastBeforeQuitPromise = new Promise((resolve) -> resolveBeforeQuitPromise = resolve)
|
||||
if @quitting
|
||||
resolveBeforeQuitPromise()
|
||||
else
|
||||
event.preventDefault()
|
||||
@quitting = true
|
||||
Promise.all(@windows.map((window) -> window.prepareToUnload())).then(-> app.quit())
|
||||
windowUnloadPromises = @windows.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()
|
||||
|
||||
@@ -54,14 +54,14 @@ class AtomWindow
|
||||
@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 ?=
|
||||
@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()
|
||||
@@ -72,17 +72,17 @@ class AtomWindow
|
||||
parentDirectory
|
||||
else
|
||||
pathToOpen
|
||||
loadSettings.initialPaths.sort()
|
||||
@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
|
||||
@loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime
|
||||
|
||||
@representedDirectoryPaths = loadSettings.initialPaths
|
||||
@env = loadSettings.env if loadSettings.env?
|
||||
@representedDirectoryPaths = @loadSettings.initialPaths
|
||||
@env = @loadSettings.env if @loadSettings.env?
|
||||
|
||||
@browserWindow.loadSettingsJSON = JSON.stringify(loadSettings)
|
||||
@browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings)
|
||||
|
||||
@browserWindow.on 'window:loaded', =>
|
||||
@disableZoom()
|
||||
@@ -309,6 +309,8 @@ class AtomWindow
|
||||
|
||||
setRepresentedDirectoryPaths: (@representedDirectoryPaths) ->
|
||||
@representedDirectoryPaths.sort()
|
||||
@loadSettings.initialPaths = @representedDirectoryPaths
|
||||
@browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings)
|
||||
@atomApplication.saveState()
|
||||
|
||||
copy: -> @browserWindow.copy()
|
||||
|
||||
@@ -55,6 +55,7 @@ module.exports = function parseCommandLine (processArgs) {
|
||||
options.string('socket-path')
|
||||
options.string('user-data-dir')
|
||||
options.boolean('clear-window-state').describe('clear-window-state', 'Delete all Atom environment state.')
|
||||
options.boolean('enable-electron-logging').describe('enable-electron-logging', 'Enable low-level logging messages from Electron.')
|
||||
|
||||
const args = options.argv
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const {app} = require('electron')
|
||||
const nslog = require('nslog')
|
||||
const path = require('path')
|
||||
const temp = require('temp')
|
||||
const temp = require('temp').track()
|
||||
const parseCommandLine = require('./parse-command-line')
|
||||
const startCrashReporter = require('../crash-reporter-start')
|
||||
const atomPaths = require('../atom-paths')
|
||||
|
||||
436
src/native-watcher-registry.js
Normal file
436
src/native-watcher-registry.js
Normal file
@@ -0,0 +1,436 @@
|
||||
/** @babel */
|
||||
|
||||
const path = require('path')
|
||||
|
||||
// Private: re-join the segments split from an absolute path to form another absolute path.
|
||||
function absolute (...parts) {
|
||||
const candidate = path.join(...parts)
|
||||
return path.isAbsolute(candidate) ? candidate : path.join(path.sep, candidate)
|
||||
}
|
||||
|
||||
// Private: Map userland filesystem watcher subscriptions efficiently to deliver filesystem change notifications to
|
||||
// each watcher with the most efficient coverage of native watchers.
|
||||
//
|
||||
// * If two watchers subscribe to the same directory, use a single native watcher for each.
|
||||
// * Re-use a native watcher watching a parent directory for a watcher on a child directory. If the parent directory
|
||||
// watcher is removed, it will be split into child watchers.
|
||||
// * If any child directories already being watched, stop and replace them with a watcher on the parent directory.
|
||||
//
|
||||
// Uses a trie whose structure mirrors the directory structure.
|
||||
class RegistryTree {
|
||||
|
||||
// Private: Construct a tree with no native watchers.
|
||||
//
|
||||
// * `basePathSegments` the position of this tree's root relative to the filesystem's root as an {Array} of directory
|
||||
// names.
|
||||
// * `createNative` {Function} used to construct new native watchers. It should accept an absolute path as an argument
|
||||
// and return a new {NativeWatcher}.
|
||||
constructor (basePathSegments, createNative) {
|
||||
this.basePathSegments = basePathSegments
|
||||
this.root = new RegistryNode()
|
||||
this.createNative = createNative
|
||||
}
|
||||
|
||||
// Private: Identify the native watcher that should be used to produce events at a watched path, creating a new one
|
||||
// if necessary.
|
||||
//
|
||||
// * `pathSegments` the path to watch represented as an {Array} of directory names relative to this {RegistryTree}'s
|
||||
// root.
|
||||
// * `attachToNative` {Function} invoked with the appropriate native watcher and the absolute path to its watch root.
|
||||
add (pathSegments, attachToNative) {
|
||||
const absolutePathSegments = this.basePathSegments.concat(pathSegments)
|
||||
const absolutePath = absolute(...absolutePathSegments)
|
||||
|
||||
const attachToNew = (childPaths) => {
|
||||
const native = this.createNative(absolutePath)
|
||||
const leaf = new RegistryWatcherNode(native, absolutePathSegments, childPaths)
|
||||
this.root = this.root.insert(pathSegments, leaf)
|
||||
|
||||
const sub = native.onWillStop(() => {
|
||||
sub.dispose()
|
||||
this.root = this.root.remove(pathSegments, this.createNative) || new RegistryNode()
|
||||
})
|
||||
|
||||
attachToNative(native, absolutePath)
|
||||
return native
|
||||
}
|
||||
|
||||
this.root.lookup(pathSegments).when({
|
||||
parent: (parent, remaining) => {
|
||||
// An existing NativeWatcher is watching the same directory or a parent directory of the requested path.
|
||||
// Attach this Watcher to it as a filtering watcher and record it as a dependent child path.
|
||||
const native = parent.getNativeWatcher()
|
||||
parent.addChildPath(remaining)
|
||||
attachToNative(native, absolute(...parent.getAbsolutePathSegments()))
|
||||
},
|
||||
children: children => {
|
||||
// One or more NativeWatchers exist on child directories of the requested path. Create a new native watcher
|
||||
// on the parent directory, note the subscribed child paths, and cleanly stop the child native watchers.
|
||||
const newNative = attachToNew(children.map(child => child.path))
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const childNode = children[i].node
|
||||
const childNative = childNode.getNativeWatcher()
|
||||
childNative.reattachTo(newNative, absolutePath)
|
||||
childNative.dispose()
|
||||
childNative.stop()
|
||||
}
|
||||
},
|
||||
missing: () => attachToNew([])
|
||||
})
|
||||
}
|
||||
|
||||
// Private: Access the root node of the tree.
|
||||
getRoot () {
|
||||
return this.root
|
||||
}
|
||||
|
||||
// Private: Return a {String} representation of this tree's structure for diagnostics and testing.
|
||||
print () {
|
||||
return this.root.print()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Private: Non-leaf node in a {RegistryTree} used by the {NativeWatcherRegistry} to cover the allocated {Watcher}
|
||||
// instances with the most efficient set of {NativeWatcher} instances possible. Each {RegistryNode} maps to a directory
|
||||
// in the filesystem tree.
|
||||
class RegistryNode {
|
||||
|
||||
// Private: Construct a new, empty node representing a node with no watchers.
|
||||
constructor () {
|
||||
this.children = {}
|
||||
}
|
||||
|
||||
// Private: Recursively discover any existing watchers corresponding to a path.
|
||||
//
|
||||
// * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names.
|
||||
//
|
||||
// Returns: A {ParentResult} if the exact requested directory or a parent directory is being watched, a
|
||||
// {ChildrenResult} if one or more child paths are being watched, or a {MissingResult} if no relevant watchers
|
||||
// exist.
|
||||
lookup (pathSegments) {
|
||||
if (pathSegments.length === 0) {
|
||||
return new ChildrenResult(this.leaves([]))
|
||||
}
|
||||
|
||||
const child = this.children[pathSegments[0]]
|
||||
if (child === undefined) {
|
||||
return new MissingResult(this)
|
||||
}
|
||||
|
||||
return child.lookup(pathSegments.slice(1))
|
||||
}
|
||||
|
||||
// Private: Insert a new {RegistryWatcherNode} into the tree, creating new intermediate {RegistryNode} instances as
|
||||
// needed. Any existing children of the watched directory are removed.
|
||||
//
|
||||
// * `pathSegments` filesystem path of the new {Watcher}, already split into an Array of directory names.
|
||||
// * `leaf` initialized {RegistryWatcherNode} to insert
|
||||
//
|
||||
// Returns: The root of a new tree with the {RegistryWatcherNode} inserted at the correct location. Callers should
|
||||
// replace their node references with the returned value.
|
||||
insert (pathSegments, leaf) {
|
||||
if (pathSegments.length === 0) {
|
||||
return leaf
|
||||
}
|
||||
|
||||
const pathKey = pathSegments[0]
|
||||
let child = this.children[pathKey]
|
||||
if (child === undefined) {
|
||||
child = new RegistryNode()
|
||||
}
|
||||
this.children[pathKey] = child.insert(pathSegments.slice(1), leaf)
|
||||
return this
|
||||
}
|
||||
|
||||
// Private: Remove a {RegistryWatcherNode} by its exact watched directory.
|
||||
//
|
||||
// * `pathSegments` absolute pre-split filesystem path of the node to remove.
|
||||
// * `createSplitNative` callback to be invoked with each child path segment {Array} if the {RegistryWatcherNode}
|
||||
// is split into child watchers rather than removed outright. See {RegistryWatcherNode.remove}.
|
||||
//
|
||||
// Returns: The root of a new tree with the {RegistryWatcherNode} removed. Callers should replace their node
|
||||
// references with the returned value.
|
||||
remove (pathSegments, createSplitNative) {
|
||||
if (pathSegments.length === 0) {
|
||||
// Attempt to remove a path with child watchers. Do nothing.
|
||||
return this
|
||||
}
|
||||
|
||||
const pathKey = pathSegments[0]
|
||||
const child = this.children[pathKey]
|
||||
if (child === undefined) {
|
||||
// Attempt to remove a path that isn't watched. Do nothing.
|
||||
return this
|
||||
}
|
||||
|
||||
// Recurse
|
||||
const newChild = child.remove(pathSegments.slice(1), createSplitNative)
|
||||
if (newChild === null) {
|
||||
delete this.children[pathKey]
|
||||
} else {
|
||||
this.children[pathKey] = newChild
|
||||
}
|
||||
|
||||
// Remove this node if all of its children have been removed
|
||||
return Object.keys(this.children).length === 0 ? null : this
|
||||
}
|
||||
|
||||
// Private: Discover all {RegistryWatcherNode} instances beneath this tree node and the child paths
|
||||
// that they are watching.
|
||||
//
|
||||
// * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths.
|
||||
//
|
||||
// Returns: A possibly empty {Array} of `{node, path}` objects describing {RegistryWatcherNode}
|
||||
// instances beneath this node.
|
||||
leaves (prefix) {
|
||||
const results = []
|
||||
for (const p of Object.keys(this.children)) {
|
||||
results.push(...this.children[p].leaves(prefix.concat([p])))
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Private: Return a {String} representation of this subtree for diagnostics and testing.
|
||||
print (indent = 0) {
|
||||
let spaces = ''
|
||||
for (let i = 0; i < indent; i++) {
|
||||
spaces += ' '
|
||||
}
|
||||
|
||||
let result = ''
|
||||
for (const p of Object.keys(this.children)) {
|
||||
result += `${spaces}${p}\n${this.children[p].print(indent + 2)}`
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Leaf node within a {NativeWatcherRegistry} tree. Represents a directory that is covered by a
|
||||
// {NativeWatcher}.
|
||||
class RegistryWatcherNode {
|
||||
|
||||
// Private: Allocate a new node to track a {NativeWatcher}.
|
||||
//
|
||||
// * `nativeWatcher` An existing {NativeWatcher} instance.
|
||||
// * `absolutePathSegments` The absolute path to this {NativeWatcher}'s directory as an {Array} of
|
||||
// path segments.
|
||||
// * `childPaths` {Array} of child directories that are currently the responsibility of this
|
||||
// {NativeWatcher}, if any. Directories are represented as arrays of the path segments between this
|
||||
// node's directory and the watched child path.
|
||||
constructor (nativeWatcher, absolutePathSegments, childPaths) {
|
||||
this.nativeWatcher = nativeWatcher
|
||||
this.absolutePathSegments = absolutePathSegments
|
||||
|
||||
// Store child paths as joined strings so they work as Set members.
|
||||
this.childPaths = new Set()
|
||||
for (let i = 0; i < childPaths.length; i++) {
|
||||
this.childPaths.add(path.join(...childPaths[i]))
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Assume responsibility for a new child path. If this node is removed, it will instead
|
||||
// split into a subtree with a new {RegistryWatcherNode} for each child path.
|
||||
//
|
||||
// * `childPathSegments` the {Array} of path segments between this node's directory and the watched
|
||||
// child directory.
|
||||
addChildPath (childPathSegments) {
|
||||
this.childPaths.add(path.join(...childPathSegments))
|
||||
}
|
||||
|
||||
// Private: Stop assuming responsbility for a previously assigned child path. If this node is
|
||||
// removed, the named child path will no longer be allocated a {RegistryWatcherNode}.
|
||||
//
|
||||
// * `childPathSegments` the {Array} of path segments between this node's directory and the no longer
|
||||
// watched child directory.
|
||||
removeChildPath (childPathSegments) {
|
||||
this.childPaths.delete(path.join(...childPathSegments))
|
||||
}
|
||||
|
||||
// Private: Accessor for the {NativeWatcher}.
|
||||
getNativeWatcher () {
|
||||
return this.nativeWatcher
|
||||
}
|
||||
|
||||
// Private: Return the absolute path watched by this {NativeWatcher} as an {Array} of directory names.
|
||||
getAbsolutePathSegments () {
|
||||
return this.absolutePathSegments
|
||||
}
|
||||
|
||||
// Private: Identify how this watcher relates to a request to watch a directory tree.
|
||||
//
|
||||
// * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names.
|
||||
//
|
||||
// Returns: A {ParentResult} referencing this node.
|
||||
lookup (pathSegments) {
|
||||
return new ParentResult(this, pathSegments)
|
||||
}
|
||||
|
||||
// Private: Remove this leaf node if the watcher's exact path matches. If this node is covering additional
|
||||
// {Watcher} instances on child paths, it will be split into a subtree.
|
||||
//
|
||||
// * `pathSegments` filesystem path of the node to remove.
|
||||
// * `createSplitNative` callback invoked with each {Array} of absolute child path segments to create a native
|
||||
// watcher on a subtree of this node.
|
||||
//
|
||||
// Returns: If `pathSegments` match this watcher's path exactly, returns `null` if this node has no `childPaths`
|
||||
// or a new {RegistryNode} on a newly allocated subtree if it did. If `pathSegments` does not match the watcher's
|
||||
// path, it's an attempt to remove a subnode that doesn't exist, so the remove call has no effect and returns
|
||||
// `this` unaltered.
|
||||
remove (pathSegments, createSplitNative) {
|
||||
if (pathSegments.length !== 0) {
|
||||
return this
|
||||
} else if (this.childPaths.size > 0) {
|
||||
let newSubTree = new RegistryTree(this.absolutePathSegments, createSplitNative)
|
||||
|
||||
for (const childPath of this.childPaths) {
|
||||
const childPathSegments = childPath.split(path.sep)
|
||||
newSubTree.add(childPathSegments, (native, attachmentPath) => {
|
||||
this.nativeWatcher.reattachTo(native, attachmentPath)
|
||||
})
|
||||
}
|
||||
|
||||
return newSubTree.getRoot()
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Discover this {RegistryWatcherNode} instance.
|
||||
//
|
||||
// * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths.
|
||||
//
|
||||
// Returns: An {Array} containing a `{node, path}` object describing this node.
|
||||
leaves (prefix) {
|
||||
return [{node: this, path: prefix}]
|
||||
}
|
||||
|
||||
// Private: Return a {String} representation of this watcher for diagnostics and testing. Indicates the number of
|
||||
// child paths that this node's {NativeWatcher} is responsible for.
|
||||
print (indent = 0) {
|
||||
let result = ''
|
||||
for (let i = 0; i < indent; i++) {
|
||||
result += ' '
|
||||
}
|
||||
result += '[watcher'
|
||||
if (this.childPaths.size > 0) {
|
||||
result += ` +${this.childPaths.size}`
|
||||
}
|
||||
result += ']\n'
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Private: A {RegisteryNode} traversal result that's returned when neither a directory, its children, nor its parents
|
||||
// are present in the tree.
|
||||
class MissingResult {
|
||||
|
||||
// Private: Instantiate a new {MissingResult}.
|
||||
//
|
||||
// * `lastParent` the final succesfully traversed {RegistryNode}.
|
||||
constructor (lastParent) {
|
||||
this.lastParent = lastParent
|
||||
}
|
||||
|
||||
// Private: Dispatch within a map of callback actions.
|
||||
//
|
||||
// * `actions` {Object} containing a `missing` key that maps to a callback to be invoked when no results were returned
|
||||
// by {RegistryNode.lookup}. The callback will be called with the last parent node that was encountered during the
|
||||
// traversal.
|
||||
//
|
||||
// Returns: the result of the `actions` callback.
|
||||
when (actions) {
|
||||
return actions.missing(this.lastParent)
|
||||
}
|
||||
}
|
||||
|
||||
// Private: A {RegistryNode.lookup} traversal result that's returned when a parent or an exact match of the requested
|
||||
// directory is being watched by an existing {RegistryWatcherNode}.
|
||||
class ParentResult {
|
||||
|
||||
// Private: Instantiate a new {ParentResult}.
|
||||
//
|
||||
// * `parent` the {RegistryWatcherNode} that was discovered.
|
||||
// * `remainingPathSegments` an {Array} of the directories that lie between the leaf node's watched directory and
|
||||
// the requested directory. This will be empty for exact matches.
|
||||
constructor (parent, remainingPathSegments) {
|
||||
this.parent = parent
|
||||
this.remainingPathSegments = remainingPathSegments
|
||||
}
|
||||
|
||||
// Private: Dispatch within a map of callback actions.
|
||||
//
|
||||
// * `actions` {Object} containing a `parent` key that maps to a callback to be invoked when a parent of a requested
|
||||
// requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the
|
||||
// {RegistryWatcherNode} instance and an {Array} of the {String} path segments that separate the parent node
|
||||
// and the requested directory.
|
||||
//
|
||||
// Returns: the result of the `actions` callback.
|
||||
when (actions) {
|
||||
return actions.parent(this.parent, this.remainingPathSegments)
|
||||
}
|
||||
}
|
||||
|
||||
// Private: A {RegistryNode.lookup} traversal result that's returned when one or more children of the requested
|
||||
// directory are already being watched.
|
||||
class ChildrenResult {
|
||||
|
||||
// Private: Instantiate a new {ChildrenResult}.
|
||||
//
|
||||
// * `children` {Array} of the {RegistryWatcherNode} instances that were discovered.
|
||||
constructor (children) {
|
||||
this.children = children
|
||||
}
|
||||
|
||||
// Private: Dispatch within a map of callback actions.
|
||||
//
|
||||
// * `actions` {Object} containing a `children` key that maps to a callback to be invoked when a parent of a requested
|
||||
// requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the
|
||||
// {RegistryWatcherNode} instance.
|
||||
//
|
||||
// Returns: the result of the `actions` callback.
|
||||
when (actions) {
|
||||
return actions.children(this.children)
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Track the directories being monitored by native filesystem watchers. Minimize the number of native watchers
|
||||
// allocated to receive events for a desired set of directories by:
|
||||
//
|
||||
// 1. Subscribing to the same underlying {NativeWatcher} when watching the same directory multiple times.
|
||||
// 2. Subscribing to an existing {NativeWatcher} on a parent of a desired directory.
|
||||
// 3. Replacing multiple {NativeWatcher} instances on child directories with a single new {NativeWatcher} on the
|
||||
// parent.
|
||||
class NativeWatcherRegistry {
|
||||
|
||||
// Private: Instantiate an empty registry.
|
||||
//
|
||||
// * `createNative` {Function} that will be called with a normalized filesystem path to create a new native
|
||||
// filesystem watcher.
|
||||
constructor (createNative) {
|
||||
this.tree = new RegistryTree([], createNative)
|
||||
}
|
||||
|
||||
// Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. If a suitable {NativeWatcher} already
|
||||
// exists, it will be attached to the new {Watcher} with an appropriate subpath configuration. Otherwise, the
|
||||
// `createWatcher` callback will be invoked to create a new {NativeWatcher}, which will be registered in the tree
|
||||
// and attached to the watcher.
|
||||
//
|
||||
// If any pre-existing child watchers are removed as a result of this operation, {NativeWatcher.onWillReattach} will
|
||||
// be broadcast on each with the new parent watcher as an event payload to give child watchers a chance to attach to
|
||||
// the new watcher.
|
||||
//
|
||||
// * `watcher` an unattached {Watcher}.
|
||||
async attach (watcher) {
|
||||
const normalizedDirectory = await watcher.getNormalizedPathPromise()
|
||||
const pathSegments = normalizedDirectory.split(path.sep).filter(segment => segment.length > 0)
|
||||
|
||||
this.tree.add(pathSegments, (native, nativePath) => {
|
||||
watcher.attachToNative(native, nativePath)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {NativeWatcherRegistry}
|
||||
@@ -69,7 +69,7 @@ class PaneAxis extends Model
|
||||
@emitter.on 'did-replace-child', fn
|
||||
|
||||
onDidDestroy: (fn) ->
|
||||
@emitter.on 'did-destroy', fn
|
||||
@emitter.once 'did-destroy', fn
|
||||
|
||||
onDidChangeFlexScale: (fn) ->
|
||||
@emitter.on 'did-change-flex-scale', fn
|
||||
|
||||
@@ -170,7 +170,7 @@ class Pane
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
# Public: Invoke the given callback when the value of the {::isActive}
|
||||
# property changes.
|
||||
@@ -621,12 +621,15 @@ class Pane
|
||||
destroyItem: (item, force) ->
|
||||
index = @items.indexOf(item)
|
||||
if index isnt -1
|
||||
return false if not force and @getContainer()?.getLocation() isnt 'center' and item.isPermanentDockItem?()
|
||||
if not force and @getContainer()?.getLocation() isnt 'center' and item.isPermanentDockItem?()
|
||||
return Promise.resolve(false)
|
||||
|
||||
@emitter.emit 'will-destroy-item', {item, index}
|
||||
@container?.willDestroyPaneItem({item, index, pane: this})
|
||||
if force or not item?.shouldPromptToSave?()
|
||||
@removeItem(item, false)
|
||||
item.destroy?()
|
||||
Promise.resolve(true)
|
||||
else
|
||||
@promptToSaveItem(item).then (result) =>
|
||||
if result
|
||||
@@ -637,7 +640,7 @@ class Pane
|
||||
# Public: Destroy all items.
|
||||
destroyItems: ->
|
||||
Promise.all(
|
||||
@getItems().map(@destroyItem.bind(this))
|
||||
@getItems().map((item) => @destroyItem(item))
|
||||
)
|
||||
|
||||
# Public: Destroy all items except for the active item.
|
||||
@@ -645,7 +648,7 @@ class Pane
|
||||
Promise.all(
|
||||
@getItems()
|
||||
.filter((item) => item isnt @activeItem)
|
||||
.map(@destroyItem.bind(this))
|
||||
.map((item) => @destroyItem(item))
|
||||
)
|
||||
|
||||
promptToSaveItem: (item, options={}) ->
|
||||
@@ -662,7 +665,7 @@ class Pane
|
||||
chosen = @applicationDelegate.confirm
|
||||
message: message
|
||||
detailedMessage: "Your changes will be lost if you close this item without saving."
|
||||
buttons: [saveButtonText, "Cancel", "Don't Save"]
|
||||
buttons: [saveButtonText, "Cancel", "&Don't Save"]
|
||||
switch chosen
|
||||
when 0
|
||||
new Promise (resolve) ->
|
||||
@@ -950,7 +953,7 @@ class Pane
|
||||
# Returns a {Promise} that resolves once the pane is either closed, or the
|
||||
# closing has been cancelled.
|
||||
close: ->
|
||||
Promise.all(@getItems().map(@promptToSaveItem.bind(this))).then (results) =>
|
||||
Promise.all(@getItems().map((item) => @promptToSaveItem(item))).then (results) =>
|
||||
@destroy() unless results.includes(false)
|
||||
|
||||
handleSaveError: (error, item) ->
|
||||
@@ -989,4 +992,4 @@ promisify = (callback) ->
|
||||
try
|
||||
Promise.resolve(callback())
|
||||
catch error
|
||||
Promise.reject(error)
|
||||
Promise.reject(error)
|
||||
|
||||
@@ -40,7 +40,7 @@ module.exports = class PanelContainer {
|
||||
}
|
||||
|
||||
onDidDestroy (callback) {
|
||||
return this.emitter.on('did-destroy', callback)
|
||||
return this.emitter.once('did-destroy', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -66,7 +66,7 @@ class Panel {
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy (callback) {
|
||||
return this.emitter.on('did-destroy', callback)
|
||||
return this.emitter.once('did-destroy', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
641
src/path-watcher.js
Normal file
641
src/path-watcher.js
Normal file
@@ -0,0 +1,641 @@
|
||||
/** @babel */
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
|
||||
const nsfw = require('nsfw')
|
||||
const {NativeWatcherRegistry} = require('./native-watcher-registry')
|
||||
|
||||
// Private: Associate native watcher action flags with descriptive String equivalents.
|
||||
const ACTION_MAP = new Map([
|
||||
[nsfw.actions.MODIFIED, 'modified'],
|
||||
[nsfw.actions.CREATED, 'created'],
|
||||
[nsfw.actions.DELETED, 'deleted'],
|
||||
[nsfw.actions.RENAMED, 'renamed']
|
||||
])
|
||||
|
||||
// Private: Possible states of a {NativeWatcher}.
|
||||
const WATCHER_STATE = {
|
||||
STOPPED: Symbol('stopped'),
|
||||
STARTING: Symbol('starting'),
|
||||
RUNNING: Symbol('running'),
|
||||
STOPPING: Symbol('stopping')
|
||||
}
|
||||
|
||||
// Private: Emulate a "filesystem watcher" by subscribing to Atom events like buffers being saved. This will miss
|
||||
// any changes made to files outside of Atom, but it also has no overhead.
|
||||
class AtomBackend {
|
||||
async start (rootPath, eventCallback, errorCallback) {
|
||||
const getRealPath = givenPath => {
|
||||
return new Promise(resolve => {
|
||||
fs.realpath(givenPath, (err, resolvedPath) => {
|
||||
err ? resolve(null) : resolve(resolvedPath)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.subs = new CompositeDisposable()
|
||||
|
||||
this.subs.add(atom.workspace.observeTextEditors(async editor => {
|
||||
let realPath = await getRealPath(editor.getPath())
|
||||
if (!realPath || !realPath.startsWith(rootPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const announce = (action, oldPath) => {
|
||||
const payload = {action, path: realPath}
|
||||
if (oldPath) payload.oldPath = oldPath
|
||||
eventCallback([payload])
|
||||
}
|
||||
|
||||
const buffer = editor.getBuffer()
|
||||
|
||||
this.subs.add(buffer.onDidConflict(() => announce('modified')))
|
||||
this.subs.add(buffer.onDidReload(() => announce('modified')))
|
||||
this.subs.add(buffer.onDidSave(event => {
|
||||
if (event.path === realPath) {
|
||||
announce('modified')
|
||||
} else {
|
||||
const oldPath = realPath
|
||||
realPath = event.path
|
||||
announce('renamed', oldPath)
|
||||
}
|
||||
}))
|
||||
|
||||
this.subs.add(buffer.onDidDelete(() => announce('deleted')))
|
||||
|
||||
this.subs.add(buffer.onDidChangePath(newPath => {
|
||||
if (newPath !== realPath) {
|
||||
const oldPath = realPath
|
||||
realPath = newPath
|
||||
announce('renamed', oldPath)
|
||||
}
|
||||
}))
|
||||
}))
|
||||
|
||||
// Giant-ass brittle hack to hook files (and eventually directories) created from the TreeView.
|
||||
const treeViewPackage = await atom.packages.getLoadedPackage('tree-view')
|
||||
if (!treeViewPackage) return
|
||||
await treeViewPackage.activationPromise
|
||||
const treeViewModule = treeViewPackage.mainModule
|
||||
if (!treeViewModule) return
|
||||
const treeView = treeViewModule.getTreeViewInstance()
|
||||
|
||||
const isOpenInEditor = async eventPath => {
|
||||
const openPaths = await Promise.all(
|
||||
atom.workspace.getTextEditors().map(editor => getRealPath(editor.getPath()))
|
||||
)
|
||||
return openPaths.includes(eventPath)
|
||||
}
|
||||
|
||||
this.subs.add(treeView.onFileCreated(async event => {
|
||||
const realPath = await getRealPath(event.path)
|
||||
if (!realPath) return
|
||||
|
||||
eventCallback([{action: 'added', path: realPath}])
|
||||
}))
|
||||
|
||||
this.subs.add(treeView.onEntryDeleted(async event => {
|
||||
const realPath = await getRealPath(event.path)
|
||||
if (!realPath || isOpenInEditor(realPath)) return
|
||||
|
||||
eventCallback([{action: 'deleted', path: realPath}])
|
||||
}))
|
||||
|
||||
this.subs.add(treeView.onEntryMoved(async event => {
|
||||
const [realNewPath, realOldPath] = await Promise.all([
|
||||
getRealPath(event.newPath),
|
||||
getRealPath(event.initialPath)
|
||||
])
|
||||
if (!realNewPath || !realOldPath || isOpenInEditor(realNewPath) || isOpenInEditor(realOldPath)) return
|
||||
|
||||
eventCallback([{action: 'renamed', path: realNewPath, oldPath: realOldPath}])
|
||||
}))
|
||||
}
|
||||
|
||||
async stop () {
|
||||
this.subs && this.subs.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Implement a native watcher by translating events from an NSFW watcher.
|
||||
class NSFWBackend {
|
||||
async start (rootPath, eventCallback, errorCallback) {
|
||||
const handler = events => {
|
||||
eventCallback(events.map(event => {
|
||||
const action = ACTION_MAP.get(event.action) || `unexpected (${event.action})`
|
||||
const payload = {action}
|
||||
|
||||
if (event.file) {
|
||||
payload.path = path.join(event.directory, event.file)
|
||||
} else {
|
||||
payload.oldPath = path.join(event.directory, event.oldFile)
|
||||
payload.path = path.join(event.directory, event.newFile)
|
||||
}
|
||||
|
||||
return payload
|
||||
}))
|
||||
}
|
||||
|
||||
this.watcher = await nsfw(
|
||||
rootPath,
|
||||
handler,
|
||||
{debounceMS: 100, errorCallback}
|
||||
)
|
||||
|
||||
await this.watcher.start()
|
||||
}
|
||||
|
||||
stop () {
|
||||
return this.watcher.stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Map configuration settings from the feature flag to backend implementations.
|
||||
const BACKENDS = {
|
||||
atom: AtomBackend,
|
||||
native: NSFWBackend
|
||||
}
|
||||
|
||||
// Private: the backend implementation to fall back to if the config setting is invalid.
|
||||
const DEFAULT_BACKEND = BACKENDS.nsfw
|
||||
|
||||
// Private: Interface with and normalize events from a native OS filesystem watcher.
|
||||
class NativeWatcher {
|
||||
|
||||
// Private: Initialize a native watcher on a path.
|
||||
//
|
||||
// Events will not be produced until {start()} is called.
|
||||
constructor (normalizedPath) {
|
||||
this.normalizedPath = normalizedPath
|
||||
this.emitter = new Emitter()
|
||||
this.subs = new CompositeDisposable()
|
||||
|
||||
this.backend = null
|
||||
this.state = WATCHER_STATE.STOPPED
|
||||
|
||||
this.onEvents = this.onEvents.bind(this)
|
||||
this.onError = this.onError.bind(this)
|
||||
|
||||
this.subs.add(atom.config.onDidChange('core.fileSystemWatcher', async () => {
|
||||
if (this.state === WATCHER_STATE.STARTING) {
|
||||
// Wait for this watcher to finish starting.
|
||||
await new Promise(resolve => {
|
||||
const sub = this.onDidStart(() => {
|
||||
sub.dispose()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Re-read the config setting in case it's changed again while we were waiting for the watcher
|
||||
// to start.
|
||||
const Backend = this.getCurrentBackend()
|
||||
if (this.state === WATCHER_STATE.RUNNING && !(this.backend instanceof Backend)) {
|
||||
await this.stop()
|
||||
await this.start()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// Private: Read the `core.fileSystemWatcher` setting to determine the filesystem backend to use.
|
||||
getCurrentBackend () {
|
||||
const setting = atom.config.get('core.fileSystemWatcher')
|
||||
return BACKENDS[setting] || DEFAULT_BACKEND
|
||||
}
|
||||
|
||||
// Private: Begin watching for filesystem events.
|
||||
//
|
||||
// Has no effect if the watcher has already been started.
|
||||
async start () {
|
||||
if (this.state !== WATCHER_STATE.STOPPED) {
|
||||
return
|
||||
}
|
||||
this.state = WATCHER_STATE.STARTING
|
||||
|
||||
const Backend = this.getCurrentBackend()
|
||||
|
||||
this.backend = new Backend()
|
||||
await this.backend.start(this.normalizedPath, this.onEvents, this.onError)
|
||||
|
||||
this.state = WATCHER_STATE.RUNNING
|
||||
this.emitter.emit('did-start')
|
||||
}
|
||||
|
||||
// Private: Return true if the underlying watcher is actively listening for filesystem events.
|
||||
isRunning () {
|
||||
return this.state === WATCHER_STATE.RUNNING
|
||||
}
|
||||
|
||||
// Private: Register a callback to be invoked when the filesystem watcher has been initialized.
|
||||
//
|
||||
// Returns: A {Disposable} to revoke the subscription.
|
||||
onDidStart (callback) {
|
||||
return this.emitter.on('did-start', callback)
|
||||
}
|
||||
|
||||
// Private: Register a callback to be invoked with normalized filesystem events as they arrive. Starts the watcher
|
||||
// automatically if it is not already running. The watcher will be stopped automatically when all subscribers
|
||||
// dispose their subscriptions.
|
||||
//
|
||||
// Returns: A {Disposable} to revoke the subscription.
|
||||
onDidChange (callback) {
|
||||
this.start()
|
||||
|
||||
const sub = this.emitter.on('did-change', callback)
|
||||
return new Disposable(() => {
|
||||
sub.dispose()
|
||||
if (this.emitter.listenerCountForEventName('did-change') === 0) {
|
||||
this.stop()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Private: Register a callback to be invoked when a {Watcher} should attach to a different {NativeWatcher}.
|
||||
//
|
||||
// Returns: A {Disposable} to revoke the subscription.
|
||||
onShouldDetach (callback) {
|
||||
return this.emitter.on('should-detach', callback)
|
||||
}
|
||||
|
||||
// Private: Register a callback to be invoked when a {NativeWatcher} is about to be stopped.
|
||||
//
|
||||
// Returns: A {Disposable} to revoke the subscription.
|
||||
onWillStop (callback) {
|
||||
return this.emitter.on('will-stop', callback)
|
||||
}
|
||||
|
||||
// Private: Register a callback to be invoked when the filesystem watcher has been stopped.
|
||||
//
|
||||
// Returns: A {Disposable} to revoke the subscription.
|
||||
onDidStop (callback) {
|
||||
return this.emitter.on('did-stop', callback)
|
||||
}
|
||||
|
||||
// Private: Register a callback to be invoked with any errors reported from the watcher.
|
||||
//
|
||||
// Returns: A {Disposable} to revoke the subscription.
|
||||
onDidError (callback) {
|
||||
return this.emitter.on('did-error', callback)
|
||||
}
|
||||
|
||||
// Private: Broadcast an `onShouldDetach` event to prompt any {Watcher} instances bound here to attach to a new
|
||||
// {NativeWatcher} instead.
|
||||
//
|
||||
// * `replacement` the new {NativeWatcher} instance that a live {Watcher} instance should reattach to instead.
|
||||
// * `watchedPath` absolute path watched by the new {NativeWatcher}.
|
||||
reattachTo (replacement, watchedPath) {
|
||||
this.emitter.emit('should-detach', {replacement, watchedPath})
|
||||
}
|
||||
|
||||
// Private: Stop the native watcher and release any operating system resources associated with it.
|
||||
//
|
||||
// Has no effect if the watcher is not running.
|
||||
async stop () {
|
||||
if (this.state !== WATCHER_STATE.RUNNING) {
|
||||
return
|
||||
}
|
||||
this.state = WATCHER_STATE.STOPPING
|
||||
this.emitter.emit('will-stop')
|
||||
|
||||
await this.backend.stop()
|
||||
this.state = WATCHER_STATE.STOPPED
|
||||
|
||||
this.emitter.emit('did-stop')
|
||||
}
|
||||
|
||||
// Private: Detach any event subscribers.
|
||||
dispose () {
|
||||
this.emitter.dispose()
|
||||
}
|
||||
|
||||
// Private: Callback function invoked by the native watcher when a debounced group of filesystem events arrive.
|
||||
// Normalize and re-broadcast them to any subscribers.
|
||||
//
|
||||
// * `events` An Array of filesystem events.
|
||||
onEvents (events) {
|
||||
this.emitter.emit('did-change', events)
|
||||
}
|
||||
|
||||
// Private: Callback function invoked by the native watcher when an error occurs.
|
||||
//
|
||||
// * `err` The native filesystem error.
|
||||
onError (err) {
|
||||
this.emitter.emit('did-error', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Extended: Manage a subscription to filesystem events that occur beneath a root directory. Construct these by
|
||||
// calling `watchPath`. To watch for events within active project directories, use {Project::onDidChangeFiles}
|
||||
// instead.
|
||||
//
|
||||
// Multiple PathWatchers may be backed by a single native watcher to conserve operation system resources.
|
||||
//
|
||||
// Call {::dispose} to stop receiving events and, if possible, release underlying resources. A PathWatcher may be
|
||||
// added to a {CompositeDisposable} to manage its lifetime along with other {Disposable} resources like event
|
||||
// subscriptions.
|
||||
//
|
||||
// ```js
|
||||
// const {watchPath} = require('atom')
|
||||
//
|
||||
// const disposable = watchPath('/var/log', {}, events => {
|
||||
// console.log(`Received batch of ${events.length} events.`)
|
||||
// for (const event of events) {
|
||||
// // "created", "modified", "deleted", "renamed"
|
||||
// console.log(`Event action: ${event.action}`)
|
||||
//
|
||||
// // absolute path to the filesystem entry that was touched
|
||||
// console.log(`Event path: ${event.path}`)
|
||||
//
|
||||
// if (event.action === 'renamed') {
|
||||
// console.log(`.. renamed from: ${event.oldPath}`)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// // Immediately stop receiving filesystem events. If this is the last
|
||||
// // watcher, asynchronously release any OS resources required to
|
||||
// // subscribe to these events.
|
||||
// disposable.dispose()
|
||||
// ```
|
||||
//
|
||||
// `watchPath` accepts the following arguments:
|
||||
//
|
||||
// `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch.
|
||||
//
|
||||
// `options` Control the watcher's behavior. Currently a placeholder.
|
||||
//
|
||||
// `eventCallback` {Function} to be called each time a batch of filesystem events is observed. Each event object has
|
||||
// the keys: `action`, a {String} describing the filesystem action that occurred, one of `"created"`, `"modified"`,
|
||||
// `"deleted"`, or `"renamed"`; `path`, a {String} containing the absolute path to the filesystem entry that was acted
|
||||
// upon; for rename events only, `oldPath`, a {String} containing the filesystem entry's former absolute path.
|
||||
class PathWatcher {
|
||||
|
||||
// Private: Instantiate a new PathWatcher. Call {watchPath} instead.
|
||||
//
|
||||
// * `nativeWatcherRegistry` {NativeWatcherRegistry} used to find and consolidate redundant watchers.
|
||||
// * `watchedPath` {String} containing the absolute path to the root of the watched filesystem tree.
|
||||
// * `options` See {watchPath} for options.
|
||||
//
|
||||
constructor (nativeWatcherRegistry, watchedPath, options) {
|
||||
this.watchedPath = watchedPath
|
||||
this.nativeWatcherRegistry = nativeWatcherRegistry
|
||||
|
||||
this.normalizedPath = null
|
||||
this.native = null
|
||||
this.changeCallbacks = new Map()
|
||||
|
||||
this.normalizedPathPromise = new Promise((resolve, reject) => {
|
||||
fs.realpath(watchedPath, (err, real) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
this.normalizedPath = real
|
||||
resolve(real)
|
||||
})
|
||||
})
|
||||
|
||||
this.attachedPromise = new Promise(resolve => {
|
||||
this.resolveAttachedPromise = resolve
|
||||
})
|
||||
this.startPromise = new Promise(resolve => {
|
||||
this.resolveStartPromise = resolve
|
||||
})
|
||||
|
||||
this.emitter = new Emitter()
|
||||
this.subs = new CompositeDisposable()
|
||||
}
|
||||
|
||||
// Private: Return a {Promise} that will resolve with the normalized root path.
|
||||
getNormalizedPathPromise () {
|
||||
return this.normalizedPathPromise
|
||||
}
|
||||
|
||||
// Private: Return a {Promise} that will resolve the first time that this watcher is attached to a native watcher.
|
||||
getAttachedPromise () {
|
||||
return this.attachedPromise
|
||||
}
|
||||
|
||||
// Extended: Return a {Promise} that will resolve when the underlying native watcher is ready to begin sending events.
|
||||
// When testing filesystem watchers, it's important to await this promise before making filesystem changes that you
|
||||
// intend to assert about because there will be a delay between the instantiation of the watcher and the activation
|
||||
// of the underlying OS resources that feed it events.
|
||||
//
|
||||
// ```js
|
||||
// const {watchPath} = require('atom')
|
||||
// const ROOT = path.join(__dirname, 'fixtures')
|
||||
// const FILE = path.join(ROOT, 'filename.txt')
|
||||
//
|
||||
// describe('something', function () {
|
||||
// it("doesn't miss events", async function () {
|
||||
// const watcher = watchPath(ROOT, {}, events => {})
|
||||
// await watcher.getStartPromise()
|
||||
// fs.writeFile(FILE, 'contents\n', err => {
|
||||
// // The watcher is listening and the event should be
|
||||
// // received asyncronously
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
// ```
|
||||
getStartPromise () {
|
||||
return this.startPromise
|
||||
}
|
||||
|
||||
// Private: Attach another {Function} to be called with each batch of filesystem events. See {watchPath} for the
|
||||
// spec of the callback's argument.
|
||||
//
|
||||
// * `callback` {Function} to be called with each batch of filesystem events.
|
||||
//
|
||||
// Returns a {Disposable} that will stop the underlying watcher when all callbacks mapped to it have been disposed.
|
||||
onDidChange (callback) {
|
||||
if (this.native) {
|
||||
const sub = this.native.onDidChange(events => this.onNativeEvents(events, callback))
|
||||
this.changeCallbacks.set(callback, sub)
|
||||
|
||||
this.native.start()
|
||||
} else {
|
||||
// Attach to a new native listener and retry
|
||||
this.nativeWatcherRegistry.attach(this).then(() => {
|
||||
this.onDidChange(callback)
|
||||
})
|
||||
}
|
||||
|
||||
return new Disposable(() => {
|
||||
const sub = this.changeCallbacks.get(callback)
|
||||
this.changeCallbacks.delete(callback)
|
||||
sub.dispose()
|
||||
})
|
||||
}
|
||||
|
||||
// Extended: Invoke a {Function} when any errors related to this watcher are reported.
|
||||
//
|
||||
// * `callback` {Function} to be called when an error occurs.
|
||||
// * `err` An {Error} describing the failure condition.
|
||||
//
|
||||
// Returns a {Disposable}.
|
||||
onDidError (callback) {
|
||||
return this.emitter.on('did-error', callback)
|
||||
}
|
||||
|
||||
// Private: Wire this watcher to an operating system-level native watcher implementation.
|
||||
attachToNative (native) {
|
||||
this.subs.dispose()
|
||||
this.native = native
|
||||
|
||||
if (native.isRunning()) {
|
||||
this.resolveStartPromise()
|
||||
} else {
|
||||
this.subs.add(native.onDidStart(() => {
|
||||
this.resolveStartPromise()
|
||||
}))
|
||||
}
|
||||
|
||||
// Transfer any native event subscriptions to the new NativeWatcher.
|
||||
for (const [callback, formerSub] of this.changeCallbacks) {
|
||||
const newSub = native.onDidChange(events => this.onNativeEvents(events, callback))
|
||||
this.changeCallbacks.set(callback, newSub)
|
||||
formerSub.dispose()
|
||||
}
|
||||
|
||||
this.subs.add(native.onDidError(err => {
|
||||
this.emitter.emit('did-error', err)
|
||||
}))
|
||||
|
||||
this.subs.add(native.onShouldDetach(({replacement, watchedPath}) => {
|
||||
if (replacement !== native && this.normalizedPath.startsWith(watchedPath)) {
|
||||
this.attachToNative(replacement)
|
||||
}
|
||||
}))
|
||||
|
||||
this.subs.add(native.onWillStop(() => {
|
||||
this.subs.dispose()
|
||||
this.native = null
|
||||
}))
|
||||
|
||||
this.resolveAttachedPromise()
|
||||
}
|
||||
|
||||
// Private: Invoked when the attached native watcher creates a batch of native filesystem events. The native watcher's
|
||||
// events may include events for paths above this watcher's root path, so filter them to only include the relevant
|
||||
// ones, then re-broadcast them to our subscribers.
|
||||
onNativeEvents (events, callback) {
|
||||
const filtered = events.filter(event => event.path.startsWith(this.normalizedPath))
|
||||
|
||||
if (filtered.length > 0) {
|
||||
callback(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
// Extended: Unsubscribe all subscribers from filesystem events. Native resources will be release asynchronously,
|
||||
// but this watcher will stop broadcasting events immediately.
|
||||
dispose () {
|
||||
for (const sub of this.changeCallbacks.values()) {
|
||||
sub.dispose()
|
||||
}
|
||||
|
||||
this.emitter.dispose()
|
||||
this.subs.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Globally tracked state used to de-duplicate related [PathWatchers]{PathWatcher}.
|
||||
class PathWatcherManager {
|
||||
|
||||
// Private: Access or lazily initialize the singleton manager instance.
|
||||
//
|
||||
// Returns the one and only {PathWatcherManager}.
|
||||
static instance () {
|
||||
if (!PathWatcherManager.theManager) {
|
||||
PathWatcherManager.theManager = new PathWatcherManager()
|
||||
}
|
||||
return PathWatcherManager.theManager
|
||||
}
|
||||
|
||||
// Private: Initialize global {PathWatcher} state.
|
||||
constructor () {
|
||||
this.live = new Set()
|
||||
this.nativeRegistry = new NativeWatcherRegistry(
|
||||
normalizedPath => {
|
||||
const nativeWatcher = new NativeWatcher(normalizedPath)
|
||||
|
||||
this.live.add(nativeWatcher)
|
||||
const sub = nativeWatcher.onWillStop(() => {
|
||||
this.live.delete(nativeWatcher)
|
||||
sub.dispose()
|
||||
})
|
||||
|
||||
return nativeWatcher
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Private: Create a {PathWatcher} tied to this global state. See {watchPath} for detailed arguments.
|
||||
createWatcher (rootPath, options, eventCallback) {
|
||||
const watcher = new PathWatcher(this.nativeRegistry, rootPath, options)
|
||||
watcher.onDidChange(eventCallback)
|
||||
return watcher
|
||||
}
|
||||
|
||||
// Private: Stop all living watchers.
|
||||
//
|
||||
// Returns a {Promise} that resolves when all native watcher resources are disposed.
|
||||
stopAllWatchers () {
|
||||
return Promise.all(
|
||||
Array.from(this.live, watcher => watcher.stop())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Extended: Invoke a callback with each filesystem event that occurs beneath a specified path. If you only need to
|
||||
// watch events within the project's root paths, use {Project::onDidChangeFiles} instead.
|
||||
//
|
||||
// watchPath handles the efficient re-use of operating system resources across living watchers. Watching the same path
|
||||
// more than once, or the child of a watched path, will re-use the existing native watcher.
|
||||
//
|
||||
// * `rootPath` {String} specifies the absolute path to the root of the filesystem content to watch.
|
||||
// * `options` Control the watcher's behavior.
|
||||
// * `eventCallback` {Function} or other callable to be called each time a batch of filesystem events is observed.
|
||||
// * `events` {Array} of objects that describe the events that have occurred.
|
||||
// * `action` {String} describing the filesystem action that occurred. One of `"created"`, `"modified"`,
|
||||
// `"deleted"`, or `"renamed"`.
|
||||
// * `path` {String} containing the absolute path to the filesystem entry that was acted upon.
|
||||
// * `oldPath` For rename events, {String} containing the filesystem entry's former absolute path.
|
||||
//
|
||||
// Returns a {PathWatcher}. Note that every {PathWatcher} is a {Disposable}, so they can be managed by
|
||||
// [CompositeDisposables]{CompositeDisposable} if desired.
|
||||
//
|
||||
// ```js
|
||||
// const {watchPath} = require('atom')
|
||||
//
|
||||
// const disposable = watchPath('/var/log', {}, events => {
|
||||
// console.log(`Received batch of ${events.length} events.`)
|
||||
// for (const event of events) {
|
||||
// // "created", "modified", "deleted", "renamed"
|
||||
// console.log(`Event action: ${event.action}`)
|
||||
// // absolute path to the filesystem entry that was touched
|
||||
// console.log(`Event path: ${event.path}`)
|
||||
// if (event.action === 'renamed') {
|
||||
// console.log(`.. renamed from: ${event.oldPath}`)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
//
|
||||
// // Immediately stop receiving filesystem events. If this is the last watcher, asynchronously release any OS
|
||||
// // resources required to subscribe to these events.
|
||||
// disposable.dispose()
|
||||
// ```
|
||||
//
|
||||
function watchPath (rootPath, options, eventCallback) {
|
||||
return PathWatcherManager.instance().createWatcher(rootPath, options, eventCallback)
|
||||
}
|
||||
|
||||
// Private: Return a Promise that resolves when all {NativeWatcher} instances associated with a FileSystemManager
|
||||
// have stopped listening. This is useful for `afterEach()` blocks in unit tests.
|
||||
function stopAllWatchers () {
|
||||
return PathWatcherManager.instance().stopAllWatchers()
|
||||
}
|
||||
|
||||
module.exports = {watchPath, stopAllWatchers}
|
||||
@@ -4,6 +4,7 @@ _ = require 'underscore-plus'
|
||||
fs = require 'fs-plus'
|
||||
{Emitter, Disposable} = require 'event-kit'
|
||||
TextBuffer = require 'text-buffer'
|
||||
{watchPath} = require('./path-watcher')
|
||||
|
||||
DefaultDirectoryProvider = require './default-directory-provider'
|
||||
Model = require './model'
|
||||
@@ -28,11 +29,13 @@ class Project extends Model
|
||||
@repositoryPromisesByPath = new Map()
|
||||
@repositoryProviders = [new GitRepositoryProvider(this, config)]
|
||||
@loadPromisesByPath = {}
|
||||
@watchersByPath = {}
|
||||
@consumeServices(packageManager)
|
||||
|
||||
destroyed: ->
|
||||
buffer.destroy() for buffer in @buffers.slice()
|
||||
repository?.destroy() for repository in @repositories.slice()
|
||||
watcher.dispose() for _, watcher in @watchersByPath
|
||||
@rootDirectories = []
|
||||
@repositories = []
|
||||
|
||||
@@ -114,6 +117,43 @@ class Project extends Model
|
||||
callback(buffer) for buffer in @getBuffers()
|
||||
@onDidAddBuffer callback
|
||||
|
||||
# Extended: Invoke a callback when a filesystem change occurs within any open
|
||||
# project path.
|
||||
#
|
||||
# ```js
|
||||
# const disposable = atom.project.onDidChangeFiles(events => {
|
||||
# for (const event of events) {
|
||||
# // "created", "modified", "deleted", or "renamed"
|
||||
# console.log(`Event action: ${event.type}`)
|
||||
#
|
||||
# // absolute path to the filesystem entry that was touched
|
||||
# console.log(`Event path: ${event.path}`)
|
||||
#
|
||||
# if (event.type === 'renamed') {
|
||||
# console.log(`.. renamed from: ${event.oldPath}`)
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# disposable.dispose()
|
||||
# ```
|
||||
#
|
||||
# To watch paths outside of open projects, use the `watchPaths` function instead; see {PathWatcher}.
|
||||
#
|
||||
# * `callback` {Function} to be called with batches of filesystem events reported by
|
||||
# the operating system.
|
||||
# * `events` An {Array} of objects that describe a batch of filesystem events.
|
||||
# * `type` {String} describing the filesystem action that occurred. One of `"created"`,
|
||||
# `"modified"`, `"deleted"`, or `"renamed"`.
|
||||
# * `path` {String} containing the absolute path to the filesystem entry
|
||||
# that was acted upon.
|
||||
# * `oldPath` For rename events, {String} containing the filesystem entry's
|
||||
# former absolute path.
|
||||
#
|
||||
# Returns a {Disposable} to manage this event subscription.
|
||||
onDidChangeFiles: (callback) ->
|
||||
@emitter.on 'did-change-files', callback
|
||||
|
||||
###
|
||||
Section: Accessing the git repository
|
||||
###
|
||||
@@ -172,6 +212,9 @@ class Project extends Model
|
||||
@rootDirectories = []
|
||||
@repositories = []
|
||||
|
||||
watcher.dispose() for _, watcher in @watchersByPath
|
||||
@watchersByPath = {}
|
||||
|
||||
@addPath(projectPath, emitEvent: false) for projectPath in projectPaths
|
||||
|
||||
@emitter.emit 'did-change-paths', projectPaths
|
||||
@@ -186,6 +229,11 @@ class Project extends Model
|
||||
return if existingDirectory.getPath() is directory.getPath()
|
||||
|
||||
@rootDirectories.push(directory)
|
||||
@watchersByPath[directory.getPath()] = watchPath directory.getPath(), {}, (events) =>
|
||||
@emitter.emit 'did-change-files', events
|
||||
|
||||
for root, watcher in @watchersByPath
|
||||
watcher.dispose() unless @rootDirectoryies.includes root
|
||||
|
||||
repo = null
|
||||
for provider in @repositoryProviders
|
||||
@@ -220,6 +268,7 @@ class Project extends Model
|
||||
[removedDirectory] = @rootDirectories.splice(indexToRemove, 1)
|
||||
[removedRepository] = @repositories.splice(indexToRemove, 1)
|
||||
removedRepository?.destroy() unless removedRepository in @repositories
|
||||
@watchersByPath[projectPath]?.dispose()
|
||||
@emitter.emit "did-change-paths", @getPaths()
|
||||
true
|
||||
else
|
||||
|
||||
@@ -54,7 +54,7 @@ class Selection extends Model
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Managing the selection range
|
||||
|
||||
@@ -70,6 +70,7 @@ class TextEditorComponent {
|
||||
this.updateSync = this.updateSync.bind(this)
|
||||
this.didBlurHiddenInput = this.didBlurHiddenInput.bind(this)
|
||||
this.didFocusHiddenInput = this.didFocusHiddenInput.bind(this)
|
||||
this.didPaste = this.didPaste.bind(this)
|
||||
this.didTextInput = this.didTextInput.bind(this)
|
||||
this.didKeydown = this.didKeydown.bind(this)
|
||||
this.didKeyup = this.didKeyup.bind(this)
|
||||
@@ -109,11 +110,14 @@ class TextEditorComponent {
|
||||
this.cursorsBlinking = false
|
||||
this.cursorsBlinkedOff = false
|
||||
this.nextUpdateOnlyBlinksCursors = null
|
||||
this.extraLinesToMeasure = null
|
||||
this.extraRenderedScreenLines = null
|
||||
this.linesToMeasure = new Map()
|
||||
this.extraRenderedScreenLines = new Map()
|
||||
this.horizontalPositionsToMeasure = new Map() // Keys are rows with positions we want to measure, values are arrays of columns to measure
|
||||
this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horiontal pixel positions
|
||||
this.blockDecorationsToMeasure = new Set()
|
||||
this.blockDecorationsByElement = new WeakMap()
|
||||
this.heightsByBlockDecoration = new WeakMap()
|
||||
this.blockDecorationResizeObserver = new ResizeObserver(this.didResizeBlockDecorations.bind(this))
|
||||
this.lineNodesByScreenLineId = new Map()
|
||||
this.textNodesByScreenLineId = new Map()
|
||||
this.overlayComponents = new Set()
|
||||
@@ -172,6 +176,10 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
update (props) {
|
||||
if (props.model !== this.props.model) {
|
||||
this.props.model.component = null
|
||||
props.model.component = this
|
||||
}
|
||||
this.props = props
|
||||
this.scheduleUpdate()
|
||||
}
|
||||
@@ -245,20 +253,17 @@ class TextEditorComponent {
|
||||
|
||||
this.measureBlockDecorations()
|
||||
|
||||
this.measuredContent = false
|
||||
this.updateSyncBeforeMeasuringContent()
|
||||
if (useScheduler === true) {
|
||||
const scheduler = etch.getScheduler()
|
||||
scheduler.readDocument(() => {
|
||||
this.measureContentDuringUpdateSync()
|
||||
this.measuredContent = true
|
||||
scheduler.updateDocument(() => {
|
||||
this.updateSyncAfterMeasuringContent()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.measureContentDuringUpdateSync()
|
||||
this.measuredContent = true
|
||||
this.updateSyncAfterMeasuringContent()
|
||||
}
|
||||
}
|
||||
@@ -312,11 +317,17 @@ class TextEditorComponent {
|
||||
}
|
||||
})
|
||||
|
||||
if (this.resizeBlockDecorationMeasurementsArea) {
|
||||
this.resizeBlockDecorationMeasurementsArea = false
|
||||
this.refs.blockDecorationMeasurementArea.style.width = this.getScrollWidth() + 'px'
|
||||
}
|
||||
|
||||
this.blockDecorationsToMeasure.forEach((decoration) => {
|
||||
const {item} = decoration.getProperties()
|
||||
const decorationElement = TextEditor.viewForItem(item)
|
||||
const {previousSibling, nextSibling} = decorationElement
|
||||
const height = nextSibling.getBoundingClientRect().top - previousSibling.getBoundingClientRect().bottom
|
||||
this.heightsByBlockDecoration.set(decoration, height)
|
||||
this.lineTopIndex.resizeBlock(decoration, height)
|
||||
})
|
||||
|
||||
@@ -329,6 +340,7 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
updateSyncBeforeMeasuringContent () {
|
||||
this.measuredContent = false
|
||||
this.derivedDimensionsCache = {}
|
||||
this.updateModelSoftWrapColumn()
|
||||
if (this.pendingAutoscroll) {
|
||||
@@ -343,6 +355,7 @@ class TextEditorComponent {
|
||||
this.queryLineNumbersToRender()
|
||||
this.queryGuttersToRender()
|
||||
this.queryDecorationsToRender()
|
||||
this.queryExtraScreenLinesToRender()
|
||||
this.shouldRenderDummyScrollbars = !this.remeasureScrollbars
|
||||
etch.updateSync(this)
|
||||
this.updateClassList()
|
||||
@@ -357,8 +370,6 @@ class TextEditorComponent {
|
||||
}
|
||||
const wasHorizontalScrollbarVisible = this.isHorizontalScrollbarVisible()
|
||||
|
||||
this.extraRenderedScreenLines = this.extraLinesToMeasure
|
||||
this.extraLinesToMeasure = null
|
||||
this.measureLongestLineWidth()
|
||||
this.measureHorizontalPositions()
|
||||
this.updateAbsolutePositionedDecorations()
|
||||
@@ -372,6 +383,8 @@ class TextEditorComponent {
|
||||
}
|
||||
this.pendingAutoscroll = null
|
||||
}
|
||||
|
||||
this.measuredContent = true
|
||||
}
|
||||
|
||||
updateSyncAfterMeasuringContent () {
|
||||
@@ -539,6 +552,7 @@ class TextEditorComponent {
|
||||
]
|
||||
} else {
|
||||
children = [
|
||||
this.renderCursorsAndInput(),
|
||||
this.renderBlockDecorationMeasurementArea(),
|
||||
this.renderCharacterMeasurementLine()
|
||||
]
|
||||
@@ -592,21 +606,19 @@ class TextEditorComponent {
|
||||
})
|
||||
}
|
||||
|
||||
if (this.extraLinesToMeasure) {
|
||||
this.extraLinesToMeasure.forEach((screenLine, screenRow) => {
|
||||
if (screenRow < startRow || screenRow >= endRow) {
|
||||
tileNodes.push($(LineComponent, {
|
||||
key: 'extra-' + screenLine.id,
|
||||
screenLine,
|
||||
screenRow,
|
||||
displayLayer,
|
||||
nodePool: this.lineNodesPool,
|
||||
lineNodesByScreenLineId,
|
||||
textNodesByScreenLineId
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
this.extraRenderedScreenLines.forEach((screenLine, screenRow) => {
|
||||
if (screenRow < startRow || screenRow >= endRow) {
|
||||
tileNodes.push($(LineComponent, {
|
||||
key: 'extra-' + screenLine.id,
|
||||
screenLine,
|
||||
screenRow,
|
||||
displayLayer,
|
||||
nodePool: this.lineNodesPool,
|
||||
lineNodesByScreenLineId,
|
||||
textNodesByScreenLineId
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
return $.div({
|
||||
key: 'lineTiles',
|
||||
@@ -629,6 +641,7 @@ class TextEditorComponent {
|
||||
didBlurHiddenInput: this.didBlurHiddenInput,
|
||||
didFocusHiddenInput: this.didFocusHiddenInput,
|
||||
didTextInput: this.didTextInput,
|
||||
didPaste: this.didPaste,
|
||||
didKeydown: this.didKeydown,
|
||||
didKeyup: this.didKeyup,
|
||||
didKeypress: this.didKeypress,
|
||||
@@ -815,12 +828,22 @@ class TextEditorComponent {
|
||||
const longestLineRow = model.getApproximateLongestScreenRow()
|
||||
const longestLine = model.screenLineForScreenRow(longestLineRow)
|
||||
if (longestLine !== this.previousLongestLine) {
|
||||
this.requestExtraLineToMeasure(longestLineRow, longestLine)
|
||||
this.requestLineToMeasure(longestLineRow, longestLine)
|
||||
this.longestLineToMeasure = longestLine
|
||||
this.previousLongestLine = longestLine
|
||||
}
|
||||
}
|
||||
|
||||
queryExtraScreenLinesToRender () {
|
||||
this.extraRenderedScreenLines.clear()
|
||||
this.linesToMeasure.forEach((screenLine, row) => {
|
||||
if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) {
|
||||
this.extraRenderedScreenLines.set(row, screenLine)
|
||||
}
|
||||
})
|
||||
this.linesToMeasure.clear()
|
||||
}
|
||||
|
||||
queryLineNumbersToRender () {
|
||||
const {model} = this.props
|
||||
if (!model.isLineNumberGutterVisible()) return
|
||||
@@ -836,6 +859,7 @@ class TextEditorComponent {
|
||||
const renderedRowCount = this.getRenderedRowCount()
|
||||
|
||||
const bufferRows = model.bufferRowsForScreenRows(startRow, endRow)
|
||||
const screenRows = new Array(renderedRowCount)
|
||||
const keys = new Array(renderedRowCount)
|
||||
const foldableFlags = new Array(renderedRowCount)
|
||||
const softWrappedFlags = new Array(renderedRowCount)
|
||||
@@ -862,6 +886,7 @@ class TextEditorComponent {
|
||||
foldableFlags[i] = false
|
||||
}
|
||||
|
||||
screenRows[i] = row
|
||||
previousBufferRow = bufferRow
|
||||
}
|
||||
|
||||
@@ -869,6 +894,7 @@ class TextEditorComponent {
|
||||
bufferRows.pop()
|
||||
|
||||
this.lineNumbersToRender.bufferRows = bufferRows
|
||||
this.lineNumbersToRender.screenRows = screenRows
|
||||
this.lineNumbersToRender.keys = keys
|
||||
this.lineNumbersToRender.foldableFlags = foldableFlags
|
||||
this.lineNumbersToRender.softWrappedFlags = softWrappedFlags
|
||||
@@ -888,7 +914,7 @@ class TextEditorComponent {
|
||||
renderedScreenLineForRow (row) {
|
||||
return (
|
||||
this.renderedScreenLines[row - this.getRenderedStartRow()] ||
|
||||
(this.extraRenderedScreenLines ? this.extraRenderedScreenLines.get(row) : null)
|
||||
this.extraRenderedScreenLines.get(row)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1105,7 +1131,7 @@ class TextEditorComponent {
|
||||
const height = this.pixelPositionBeforeBlocksForRow(screenRange.end.row + 1) - top
|
||||
|
||||
decorations.push({
|
||||
className: decoration.class,
|
||||
className: 'decoration' + (decoration.class ? ' ' + decoration.class : ''),
|
||||
element: TextEditor.viewForItem(decoration.item),
|
||||
top,
|
||||
height
|
||||
@@ -1391,6 +1417,7 @@ class TextEditorComponent {
|
||||
if (!this.hasInitialMeasurements) this.measureDimensions()
|
||||
this.visible = true
|
||||
this.props.model.setVisible(true)
|
||||
this.resizeBlockDecorationMeasurementsArea = true
|
||||
this.updateSync()
|
||||
this.flushPendingLogicalScrollPosition()
|
||||
}
|
||||
@@ -1423,16 +1450,14 @@ class TextEditorComponent {
|
||||
this.scheduleUpdate()
|
||||
}
|
||||
|
||||
const {hiddenInput} = this.refs.cursorsAndInput.refs
|
||||
hiddenInput.focus()
|
||||
this.getHiddenInput().focus()
|
||||
}
|
||||
|
||||
// Called by TextEditorElement so that this function is always the first
|
||||
// listener to be fired, even if other listeners are bound before creating
|
||||
// the component.
|
||||
didBlur (event) {
|
||||
const {cursorsAndInput} = this.refs
|
||||
if (cursorsAndInput && event.relatedTarget === cursorsAndInput.refs.hiddenInput) {
|
||||
if (event.relatedTarget === this.getHiddenInput()) {
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
}
|
||||
@@ -1533,7 +1558,21 @@ class TextEditorComponent {
|
||||
}
|
||||
}
|
||||
|
||||
didPaste (event) {
|
||||
// On Linux, Chromium translates a middle-button mouse click into a
|
||||
// mousedown event *and* a paste event. Since Atom supports the middle mouse
|
||||
// click as a way of closing a tab, we only want the mousedown event, not
|
||||
// the paste event. And since we don't use the `paste` event for any
|
||||
// behavior in Atom, we can no-op the event to eliminate this issue.
|
||||
// See https://github.com/atom/atom/pull/15183#issue-248432413.
|
||||
if (this.getPlatform() === 'linux') event.preventDefault()
|
||||
}
|
||||
|
||||
didTextInput (event) {
|
||||
// Workaround for Chromium not preventing composition events when
|
||||
// preventDefault is called on the keydown event that precipitated them.
|
||||
if (this.lastKeydown && this.lastKeydown.defaultPrevented) return
|
||||
|
||||
if (!this.isInputEnabled()) return
|
||||
|
||||
event.stopPropagation()
|
||||
@@ -1590,7 +1629,6 @@ class TextEditorComponent {
|
||||
|
||||
didKeypress (event) {
|
||||
this.lastKeydownBeforeKeypress = this.lastKeydown
|
||||
this.lastKeydown = null
|
||||
|
||||
// This cancels the accented character behavior if we type a key normally
|
||||
// with the menu open.
|
||||
@@ -1600,7 +1638,6 @@ class TextEditorComponent {
|
||||
didKeyup (event) {
|
||||
if (this.lastKeydownBeforeKeypress && this.lastKeydownBeforeKeypress.code === event.code) {
|
||||
this.lastKeydownBeforeKeypress = null
|
||||
this.lastKeydown = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1615,6 +1652,10 @@ class TextEditorComponent {
|
||||
// 4. compositionend fired
|
||||
// 5. textInput fired; event.data == the completion string
|
||||
didCompositionStart () {
|
||||
if (this.getChromeVersion() === 56) {
|
||||
this.getHiddenInput().value = ''
|
||||
}
|
||||
|
||||
this.compositionCheckpoint = this.props.model.createCheckpoint()
|
||||
if (this.accentedCharacterMenuIsOpen) {
|
||||
this.props.model.selectLeft()
|
||||
@@ -1622,7 +1663,20 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
didCompositionUpdate (event) {
|
||||
this.props.model.insertText(event.data, {select: true})
|
||||
// Workaround for Chromium not preventing composition events when
|
||||
// preventDefault is called on the keydown event that precipitated them.
|
||||
if (this.lastKeydown && this.lastKeydown.defaultPrevented) return
|
||||
|
||||
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})
|
||||
}
|
||||
}
|
||||
|
||||
didCompositionEnd (event) {
|
||||
@@ -1632,10 +1686,37 @@ class TextEditorComponent {
|
||||
didMouseDownOnContent (event) {
|
||||
const {model} = this.props
|
||||
const {target, button, detail, ctrlKey, shiftKey, metaKey} = event
|
||||
const platform = this.getPlatform()
|
||||
|
||||
// Ignore clicks on block decorations.
|
||||
if (target) {
|
||||
let element = target
|
||||
while (element && element !== this.element) {
|
||||
if (this.blockDecorationsByElement.has(element)) {
|
||||
return
|
||||
}
|
||||
|
||||
element = element.parentElement
|
||||
}
|
||||
}
|
||||
|
||||
// On Linux, position the cursor on middle mouse button click. A
|
||||
// textInput event with the contents of the selection clipboard will be
|
||||
// dispatched by the browser automatically on mouseup.
|
||||
if (platform === 'linux' && button === 1) {
|
||||
const selection = clipboard.readText('selection')
|
||||
const screenPosition = this.screenPositionForMouseEvent(event)
|
||||
model.setCursorScreenPosition(screenPosition, {autoscroll: false})
|
||||
model.insertText(selection)
|
||||
return
|
||||
}
|
||||
|
||||
// Only handle mousedown events for left mouse button (or the middle mouse
|
||||
// button on Linux where it pastes the selection clipboard).
|
||||
if (!(button === 0 || (this.getPlatform() === 'linux' && button === 1))) return
|
||||
if (button !== 0) return
|
||||
|
||||
// Ctrl-click brings up the context menu on macOS
|
||||
if (platform === 'darwin' && ctrlKey) return
|
||||
|
||||
const screenPosition = this.screenPositionForMouseEvent(event)
|
||||
|
||||
@@ -1645,15 +1726,7 @@ class TextEditorComponent {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle middle mouse button only on Linux (paste clipboard)
|
||||
if (this.getPlatform() === 'linux' && button === 1) {
|
||||
const selection = clipboard.readText('selection')
|
||||
model.setCursorScreenPosition(screenPosition, {autoscroll: false})
|
||||
model.insertText(selection)
|
||||
return
|
||||
}
|
||||
|
||||
const addOrRemoveSelection = metaKey || (ctrlKey && this.getPlatform() !== 'darwin')
|
||||
const addOrRemoveSelection = metaKey || (ctrlKey && platform !== 'darwin')
|
||||
|
||||
switch (detail) {
|
||||
case 1:
|
||||
@@ -2010,7 +2083,7 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
measureCharacterDimensions () {
|
||||
this.measurements.lineHeight = this.refs.characterMeasurementLine.getBoundingClientRect().height
|
||||
this.measurements.lineHeight = Math.max(1, this.refs.characterMeasurementLine.getBoundingClientRect().height)
|
||||
this.measurements.baseCharacterWidth = this.refs.normalWidthCharacterSpan.getBoundingClientRect().width
|
||||
this.measurements.doubleWidthCharacterWidth = this.refs.doubleWidthCharacterSpan.getBoundingClientRect().width
|
||||
this.measurements.halfWidthCharacterWidth = this.refs.halfWidthCharacterSpan.getBoundingClientRect().width
|
||||
@@ -2089,29 +2162,24 @@ class TextEditorComponent {
|
||||
}
|
||||
}
|
||||
|
||||
requestExtraLineToMeasure (row, screenLine) {
|
||||
if (!this.extraLinesToMeasure) this.extraLinesToMeasure = new Map()
|
||||
this.extraLinesToMeasure.set(row, screenLine)
|
||||
requestLineToMeasure (row, screenLine) {
|
||||
this.linesToMeasure.set(row, screenLine)
|
||||
}
|
||||
|
||||
requestHorizontalMeasurement (row, column) {
|
||||
if (column === 0) return
|
||||
|
||||
if (row < this.getRenderedStartRow() || row >= this.getRenderedEndRow()) {
|
||||
const screenLine = this.props.model.screenLineForScreenRow(row)
|
||||
if (screenLine) {
|
||||
this.requestExtraLineToMeasure(row, screenLine)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
const screenLine = this.props.model.screenLineForScreenRow(row)
|
||||
if (screenLine) {
|
||||
this.requestLineToMeasure(row, screenLine)
|
||||
|
||||
let columns = this.horizontalPositionsToMeasure.get(row)
|
||||
if (columns == null) {
|
||||
columns = []
|
||||
this.horizontalPositionsToMeasure.set(row, columns)
|
||||
let columns = this.horizontalPositionsToMeasure.get(row)
|
||||
if (columns == null) {
|
||||
columns = []
|
||||
this.horizontalPositionsToMeasure.set(row, columns)
|
||||
}
|
||||
columns.push(column)
|
||||
}
|
||||
columns.push(column)
|
||||
}
|
||||
|
||||
measureHorizontalPositions () {
|
||||
@@ -2224,7 +2292,7 @@ class TextEditorComponent {
|
||||
|
||||
let screenLine = this.renderedScreenLineForRow(row)
|
||||
if (!screenLine) {
|
||||
this.requestExtraLineToMeasure(row, model.screenLineForScreenRow(row))
|
||||
this.requestLineToMeasure(row, model.screenLineForScreenRow(row))
|
||||
this.updateSyncBeforeMeasuringContent()
|
||||
this.measureContentDuringUpdateSync()
|
||||
screenLine = this.renderedScreenLineForRow(row)
|
||||
@@ -2341,11 +2409,14 @@ class TextEditorComponent {
|
||||
|
||||
didAddBlockDecoration (decoration) {
|
||||
const marker = decoration.getMarker()
|
||||
const {position} = decoration.getProperties()
|
||||
const {item, position} = decoration.getProperties()
|
||||
const element = TextEditor.viewForItem(item)
|
||||
const row = marker.getHeadScreenPosition().row
|
||||
this.lineTopIndex.insertBlock(decoration, row, 0, position === 'after')
|
||||
|
||||
this.blockDecorationsToMeasure.add(decoration)
|
||||
this.blockDecorationsByElement.set(element, decoration)
|
||||
this.blockDecorationResizeObserver.observe(element)
|
||||
|
||||
const didUpdateDisposable = marker.bufferMarker.onDidChange((e) => {
|
||||
if (!e.textChanged) {
|
||||
@@ -2355,6 +2426,9 @@ class TextEditorComponent {
|
||||
})
|
||||
const didDestroyDisposable = decoration.onDidDestroy(() => {
|
||||
this.blockDecorationsToMeasure.delete(decoration)
|
||||
this.heightsByBlockDecoration.delete(decoration)
|
||||
this.blockDecorationsByElement.delete(element)
|
||||
this.blockDecorationResizeObserver.unobserve(element)
|
||||
this.lineTopIndex.removeBlock(decoration)
|
||||
didUpdateDisposable.dispose()
|
||||
didDestroyDisposable.dispose()
|
||||
@@ -2362,6 +2436,19 @@ class TextEditorComponent {
|
||||
})
|
||||
}
|
||||
|
||||
didResizeBlockDecorations (entries) {
|
||||
if (!this.visible) return
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const {target, contentRect} = entries[i]
|
||||
const decoration = this.blockDecorationsByElement.get(target)
|
||||
const previousHeight = this.heightsByBlockDecoration.get(decoration)
|
||||
if (this.element.contains(target) && contentRect.height !== previousHeight) {
|
||||
this.invalidateBlockDecorationDimensions(decoration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invalidateBlockDecorationDimensions (decoration) {
|
||||
this.blockDecorationsToMeasure.add(decoration)
|
||||
this.scheduleUpdate()
|
||||
@@ -2753,9 +2840,17 @@ class TextEditorComponent {
|
||||
return this.props.inputEnabled != null ? this.props.inputEnabled : true
|
||||
}
|
||||
|
||||
getHiddenInput () {
|
||||
return this.refs.cursorsAndInput.refs.hiddenInput
|
||||
}
|
||||
|
||||
getPlatform () {
|
||||
return this.props.platform || process.platform
|
||||
}
|
||||
|
||||
getChromeVersion () {
|
||||
return this.props.chromeVersion || parseInt(process.versions.chrome)
|
||||
}
|
||||
}
|
||||
|
||||
class DummyScrollbarComponent {
|
||||
@@ -2796,20 +2891,22 @@ class DummyScrollbarComponent {
|
||||
outerStyle.bottom = 0
|
||||
outerStyle.left = 0
|
||||
outerStyle.right = right + 'px'
|
||||
outerStyle.height = '20px'
|
||||
outerStyle.height = '15px'
|
||||
outerStyle.overflowY = 'hidden'
|
||||
outerStyle.overflowX = this.props.forceScrollbarVisible ? 'scroll' : 'auto'
|
||||
innerStyle.height = '20px'
|
||||
outerStyle.cursor = 'default'
|
||||
innerStyle.height = '15px'
|
||||
innerStyle.width = (this.props.scrollWidth || 0) + 'px'
|
||||
} else {
|
||||
let bottom = (this.props.horizontalScrollbarHeight || 0)
|
||||
outerStyle.right = 0
|
||||
outerStyle.top = 0
|
||||
outerStyle.bottom = bottom + 'px'
|
||||
outerStyle.width = '20px'
|
||||
outerStyle.width = '15px'
|
||||
outerStyle.overflowX = 'hidden'
|
||||
outerStyle.overflowY = this.props.forceScrollbarVisible ? 'scroll' : 'auto'
|
||||
innerStyle.width = '20px'
|
||||
outerStyle.cursor = 'default'
|
||||
innerStyle.width = '15px'
|
||||
innerStyle.height = (this.props.scrollHeight || 0) + 'px'
|
||||
}
|
||||
|
||||
@@ -2915,7 +3012,7 @@ class GutterContainerComponent {
|
||||
if (!isLineNumberGutterVisible) return null
|
||||
|
||||
if (hasInitialMeasurements) {
|
||||
const {maxDigits, keys, bufferRows, softWrappedFlags, foldableFlags} = lineNumbersToRender
|
||||
const {maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags} = lineNumbersToRender
|
||||
return $(LineNumberGutterComponent, {
|
||||
ref: 'lineNumberGutter',
|
||||
element: gutter.getElement(),
|
||||
@@ -2926,6 +3023,7 @@ class GutterContainerComponent {
|
||||
maxDigits: maxDigits,
|
||||
keys: keys,
|
||||
bufferRows: bufferRows,
|
||||
screenRows: screenRows,
|
||||
softWrappedFlags: softWrappedFlags,
|
||||
foldableFlags: foldableFlags,
|
||||
decorations: decorationsToRender.lineNumbers,
|
||||
@@ -2967,7 +3065,7 @@ class LineNumberGutterComponent {
|
||||
render () {
|
||||
const {
|
||||
rootComponent, showLineNumbers, height, width, lineHeight, startRow, endRow, rowsPerTile,
|
||||
maxDigits, keys, bufferRows, softWrappedFlags, foldableFlags, decorations
|
||||
maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags, decorations
|
||||
} = this.props
|
||||
|
||||
let children = null
|
||||
@@ -2984,6 +3082,7 @@ class LineNumberGutterComponent {
|
||||
const softWrapped = softWrappedFlags[j]
|
||||
const foldable = foldableFlags[j]
|
||||
const bufferRow = bufferRows[j]
|
||||
const screenRow = screenRows[j]
|
||||
|
||||
let className = 'line-number'
|
||||
if (foldable) className = className + ' foldable'
|
||||
@@ -3002,6 +3101,7 @@ class LineNumberGutterComponent {
|
||||
className,
|
||||
width,
|
||||
bufferRow,
|
||||
screenRow,
|
||||
number,
|
||||
nodePool: this.nodePool
|
||||
}
|
||||
@@ -3115,12 +3215,13 @@ class LineNumberGutterComponent {
|
||||
|
||||
class LineNumberComponent {
|
||||
constructor (props) {
|
||||
const {className, width, marginTop, bufferRow, number, nodePool} = props
|
||||
const {className, width, marginTop, bufferRow, screenRow, number, nodePool} = props
|
||||
this.props = props
|
||||
const style = {width: width + 'px'}
|
||||
if (marginTop != null) style.marginTop = marginTop + 'px'
|
||||
this.element = nodePool.getElement('DIV', className, style)
|
||||
this.element.dataset.bufferRow = bufferRow
|
||||
this.element.dataset.screenRow = screenRow
|
||||
if (number) this.element.appendChild(nodePool.getTextNode(number))
|
||||
this.element.appendChild(nodePool.getElement('DIV', 'icon-right', null))
|
||||
}
|
||||
@@ -3131,8 +3232,10 @@ class LineNumberComponent {
|
||||
}
|
||||
|
||||
update (props) {
|
||||
const {nodePool, className, width, marginTop, number} = props
|
||||
const {nodePool, className, width, marginTop, bufferRow, screenRow, number} = props
|
||||
|
||||
if (this.props.bufferRow !== bufferRow) this.element.dataset.bufferRow = bufferRow
|
||||
if (this.props.screenRow !== screenRow) this.element.dataset.screenRow = screenRow
|
||||
if (this.props.className !== className) this.element.className = className
|
||||
if (this.props.width !== width) this.element.style.width = width + 'px'
|
||||
if (this.props.marginTop !== marginTop) {
|
||||
@@ -3217,7 +3320,10 @@ class CustomGutterDecorationComponent {
|
||||
this.element.style.top = top + 'px'
|
||||
this.element.style.height = height + 'px'
|
||||
if (className != null) this.element.className = className
|
||||
if (element != null) this.element.appendChild(element)
|
||||
if (element != null) {
|
||||
this.element.appendChild(element)
|
||||
element.style.height = height + 'px'
|
||||
}
|
||||
}
|
||||
|
||||
update (newProps) {
|
||||
@@ -3225,11 +3331,17 @@ class CustomGutterDecorationComponent {
|
||||
this.props = newProps
|
||||
|
||||
if (newProps.top !== oldProps.top) this.element.style.top = newProps.top + 'px'
|
||||
if (newProps.height !== oldProps.height) this.element.style.height = newProps.height + 'px'
|
||||
if (newProps.height !== oldProps.height) {
|
||||
this.element.style.height = newProps.height + 'px'
|
||||
if (newProps.element) newProps.element.style.height = newProps.height + 'px'
|
||||
}
|
||||
if (newProps.className !== oldProps.className) this.element.className = newProps.className || ''
|
||||
if (newProps.element !== oldProps.element) {
|
||||
if (this.element.firstChild) this.element.firstChild.remove()
|
||||
this.element.appendChild(newProps.element)
|
||||
if (newProps.element != null) {
|
||||
this.element.appendChild(newProps.element)
|
||||
newProps.element.style.height = newProps.height + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3301,8 +3413,8 @@ class CursorsAndInputComponent {
|
||||
renderHiddenInput () {
|
||||
const {
|
||||
lineHeight, hiddenInputPosition, didBlurHiddenInput, didFocusHiddenInput,
|
||||
didTextInput, didKeydown, didKeyup, didKeypress, didCompositionStart,
|
||||
didCompositionUpdate, didCompositionEnd
|
||||
didPaste, didTextInput, didKeydown, didKeyup, didKeypress,
|
||||
didCompositionStart, didCompositionUpdate, didCompositionEnd
|
||||
} = this.props
|
||||
|
||||
let top, left
|
||||
@@ -3321,6 +3433,7 @@ class CursorsAndInputComponent {
|
||||
on: {
|
||||
blur: didBlurHiddenInput,
|
||||
focus: didFocusHiddenInput,
|
||||
paste: didPaste,
|
||||
textInput: didTextInput,
|
||||
keydown: didKeydown,
|
||||
keyup: didKeyup,
|
||||
@@ -3346,8 +3459,10 @@ class CursorsAndInputComponent {
|
||||
|
||||
class LinesTileComponent {
|
||||
constructor (props) {
|
||||
this.highlightComponentsByKey = new Map()
|
||||
this.props = props
|
||||
etch.initialize(this)
|
||||
this.updateHighlights()
|
||||
this.createLines()
|
||||
this.updateBlockDecorations({}, props)
|
||||
}
|
||||
@@ -3361,13 +3476,22 @@ class LinesTileComponent {
|
||||
this.updateLines(oldProps, newProps)
|
||||
this.updateBlockDecorations(oldProps, newProps)
|
||||
}
|
||||
this.updateHighlights()
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.highlightComponentsByKey.forEach((highlightComponent) => {
|
||||
highlightComponent.destroy()
|
||||
})
|
||||
this.highlightComponentsByKey.clear()
|
||||
|
||||
for (let i = 0; i < this.lineComponents.length; i++) {
|
||||
this.lineComponents[i].destroy()
|
||||
}
|
||||
this.lineComponents.length = 0
|
||||
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -3385,34 +3509,12 @@ class LinesTileComponent {
|
||||
backgroundColor: 'inherit'
|
||||
}
|
||||
},
|
||||
this.renderHighlights()
|
||||
// Lines and block decorations will be manually inserted here for efficiency
|
||||
)
|
||||
}
|
||||
|
||||
renderHighlights () {
|
||||
const {top, lineHeight, highlightDecorations} = this.props
|
||||
|
||||
let children = null
|
||||
if (highlightDecorations) {
|
||||
const decorationCount = highlightDecorations.length
|
||||
children = new Array(decorationCount)
|
||||
for (let i = 0; i < decorationCount; i++) {
|
||||
const highlightProps = Object.assign(
|
||||
{parentTileTop: top, lineHeight},
|
||||
highlightDecorations[i]
|
||||
)
|
||||
children[i] = $(HighlightComponent, highlightProps)
|
||||
highlightDecorations[i].flashRequested = false
|
||||
}
|
||||
}
|
||||
|
||||
return $.div(
|
||||
{
|
||||
$.div({
|
||||
ref: 'highlights',
|
||||
className: 'highlights',
|
||||
style: {contain: 'layout'}
|
||||
},
|
||||
children
|
||||
style: {layout: 'contain'}
|
||||
})
|
||||
// Lines and block decorations will be manually inserted here for efficiency
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3605,6 +3707,40 @@ class LinesTileComponent {
|
||||
}
|
||||
}
|
||||
|
||||
updateHighlights () {
|
||||
const {top, lineHeight, highlightDecorations} = this.props
|
||||
|
||||
const visibleHighlightDecorations = new Set()
|
||||
if (highlightDecorations) {
|
||||
for (let i = 0; i < highlightDecorations.length; i++) {
|
||||
const highlightDecoration = highlightDecorations[i]
|
||||
|
||||
const highlightProps = Object.assign(
|
||||
{parentTileTop: top, lineHeight},
|
||||
highlightDecorations[i]
|
||||
)
|
||||
let highlightComponent = this.highlightComponentsByKey.get(highlightDecoration.key)
|
||||
if (highlightComponent) {
|
||||
highlightComponent.update(highlightProps)
|
||||
} else {
|
||||
highlightComponent = new HighlightComponent(highlightProps)
|
||||
this.refs.highlights.appendChild(highlightComponent.element)
|
||||
this.highlightComponentsByKey.set(highlightDecoration.key, highlightComponent)
|
||||
}
|
||||
|
||||
highlightDecorations[i].flashRequested = false
|
||||
visibleHighlightDecorations.add(highlightDecoration.key)
|
||||
}
|
||||
}
|
||||
|
||||
this.highlightComponentsByKey.forEach((highlightComponent, key) => {
|
||||
if (!visibleHighlightDecorations.has(key)) {
|
||||
highlightComponent.destroy()
|
||||
this.highlightComponentsByKey.delete(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
shouldUpdate (newProps) {
|
||||
const oldProps = this.props
|
||||
if (oldProps.top !== newProps.top) return true
|
||||
@@ -3805,6 +3941,17 @@ class HighlightComponent {
|
||||
if (this.props.flashRequested) this.performFlash()
|
||||
}
|
||||
|
||||
destroy () {
|
||||
if (this.timeoutsByClassName) {
|
||||
this.timeoutsByClassName.forEach((timeout) => {
|
||||
window.clearTimeout(timeout)
|
||||
})
|
||||
this.timeoutsByClassName.clear()
|
||||
}
|
||||
|
||||
return etch.destroy(this)
|
||||
}
|
||||
|
||||
update (newProps) {
|
||||
this.props = newProps
|
||||
etch.updateSync(this)
|
||||
@@ -4034,7 +4181,6 @@ class NodePool {
|
||||
constructor () {
|
||||
this.elementsByType = {}
|
||||
this.textNodes = []
|
||||
this.stylesByNode = new WeakMap()
|
||||
}
|
||||
|
||||
getElement (type, className, style) {
|
||||
@@ -4055,14 +4201,10 @@ class NodePool {
|
||||
|
||||
if (element) {
|
||||
element.className = className
|
||||
var existingStyle = this.stylesByNode.get(element)
|
||||
if (existingStyle) {
|
||||
for (var key in existingStyle) {
|
||||
if (!style || !style[key]) element.style[key] = ''
|
||||
}
|
||||
}
|
||||
element.styleMap.forEach((value, key) => {
|
||||
if (!style || style[key] == null) element.style[key] = ''
|
||||
})
|
||||
if (style) Object.assign(element.style, style)
|
||||
this.stylesByNode.set(element, style)
|
||||
|
||||
while (element.firstChild) element.firstChild.remove()
|
||||
return element
|
||||
@@ -4070,7 +4212,6 @@ class NodePool {
|
||||
var newElement = document.createElement(type)
|
||||
if (className) newElement.className = className
|
||||
if (style) Object.assign(newElement.style, style)
|
||||
this.stylesByNode.set(newElement, style)
|
||||
return newElement
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,7 +647,7 @@ class TextEditor extends Model
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
# Extended: Calls your `callback` when a {Cursor} is added to the editor.
|
||||
# Immediately calls your callback for each existing cursor.
|
||||
@@ -1582,6 +1582,8 @@ class TextEditor extends Model
|
||||
# undo history, no changes will be made to the buffer and this method will
|
||||
# return `false`.
|
||||
#
|
||||
# * `checkpoint` The checkpoint to revert to.
|
||||
#
|
||||
# Returns a {Boolean} indicating whether the operation succeeded.
|
||||
revertToCheckpoint: (checkpoint) -> @buffer.revertToCheckpoint(checkpoint)
|
||||
|
||||
@@ -1591,6 +1593,8 @@ class TextEditor extends Model
|
||||
# If the given checkpoint is no longer present in the undo history, no
|
||||
# grouping will be performed and this method will return `false`.
|
||||
#
|
||||
# * `checkpoint` The checkpoint from which to group changes.
|
||||
#
|
||||
# Returns a {Boolean} indicating whether the operation succeeded.
|
||||
groupChangesSinceCheckpoint: (checkpoint) -> @buffer.groupChangesSinceCheckpoint(checkpoint)
|
||||
|
||||
@@ -1787,8 +1791,13 @@ class TextEditor extends Model
|
||||
# spanned by the `DisplayMarker`.
|
||||
# * `line-number` Adds the given `class` to the line numbers overlapping
|
||||
# the rows spanned by the `DisplayMarker`.
|
||||
# * `highlight` Creates a `.highlight` div with the nested class with up
|
||||
# to 3 nested regions that fill the area spanned by the `DisplayMarker`.
|
||||
# * `text` Injects spans into all text overlapping the marked range,
|
||||
# then adds the given `class` or `style` properties to these spans.
|
||||
# Use this to manipulate the foreground color or styling of text in
|
||||
# a given range.
|
||||
# * `highlight` Creates an absolutely-positioned `.highlight` div
|
||||
# containing nested divs to cover the marked region. For example, this
|
||||
# is used to implement selections.
|
||||
# * `overlay` Positions the view associated with the given item at the
|
||||
# head or tail of the given `DisplayMarker`, depending on the `position`
|
||||
# property.
|
||||
@@ -1804,9 +1813,10 @@ class TextEditor extends Model
|
||||
# or render artificial cursors that don't actually exist in the model
|
||||
# by passing a marker that isn't actually associated with a cursor.
|
||||
# * `class` This CSS class will be applied to the decorated line number,
|
||||
# line, highlight, or overlay.
|
||||
# line, text spans, highlight regions, cursors, or overlay.
|
||||
# * `style` An {Object} containing CSS style properties to apply to the
|
||||
# relevant DOM node. Currently this only works with a `type` of `cursor`.
|
||||
# relevant DOM node. Currently this only works with a `type` of `cursor`
|
||||
# or `text`.
|
||||
# * `item` (optional) An {HTMLElement} or a model {Object} with a
|
||||
# corresponding view registered. Only applicable to the `gutter`,
|
||||
# `overlay` and `block` decoration types.
|
||||
@@ -3517,6 +3527,10 @@ class TextEditor extends Model
|
||||
else
|
||||
1
|
||||
|
||||
Object.defineProperty(@prototype, 'rowsPerPage', {
|
||||
get: -> @getRowsPerPage()
|
||||
})
|
||||
|
||||
###
|
||||
Section: Config
|
||||
###
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
module.exports =
|
||||
class TitleBar
|
||||
constructor: ({@workspace, @themes, @applicationDelegate}) ->
|
||||
@element = document.createElement('div')
|
||||
@element.classList.add('title-bar')
|
||||
|
||||
@titleElement = document.createElement('div')
|
||||
@titleElement.classList.add('title')
|
||||
@element.appendChild(@titleElement)
|
||||
|
||||
@element.addEventListener 'dblclick', @dblclickHandler
|
||||
|
||||
@workspace.onDidChangeActivePaneItem => @updateTitle()
|
||||
@themes.onDidChangeActiveThemes => @updateWindowSheetOffset()
|
||||
|
||||
@updateTitle()
|
||||
@updateWindowSheetOffset()
|
||||
|
||||
dblclickHandler: =>
|
||||
# User preference deciding which action to take on a title bar double-click
|
||||
switch @applicationDelegate.getUserDefault('AppleActionOnDoubleClick', 'string')
|
||||
when 'Minimize'
|
||||
@applicationDelegate.minimizeWindow()
|
||||
when 'Maximize'
|
||||
if @applicationDelegate.isWindowMaximized()
|
||||
@applicationDelegate.unmaximizeWindow()
|
||||
else
|
||||
@applicationDelegate.maximizeWindow()
|
||||
|
||||
updateTitle: ->
|
||||
@titleElement.textContent = document.title
|
||||
|
||||
updateWindowSheetOffset: ->
|
||||
@applicationDelegate.getCurrentWindow().setSheetOffset(@element.offsetHeight)
|
||||
47
src/title-bar.js
Normal file
47
src/title-bar.js
Normal file
@@ -0,0 +1,47 @@
|
||||
module.exports =
|
||||
class TitleBar {
|
||||
constructor ({workspace, themes, applicationDelegate}) {
|
||||
this.dblclickHandler = this.dblclickHandler.bind(this)
|
||||
this.workspace = workspace
|
||||
this.themes = themes
|
||||
this.applicationDelegate = applicationDelegate
|
||||
this.element = document.createElement('div')
|
||||
this.element.classList.add('title-bar')
|
||||
|
||||
this.titleElement = document.createElement('div')
|
||||
this.titleElement.classList.add('title')
|
||||
this.element.appendChild(this.titleElement)
|
||||
|
||||
this.element.addEventListener('dblclick', this.dblclickHandler)
|
||||
|
||||
this.workspace.onDidChangeWindowTitle(() => this.updateTitle())
|
||||
this.themes.onDidChangeActiveThemes(() => this.updateWindowSheetOffset())
|
||||
|
||||
this.updateTitle()
|
||||
this.updateWindowSheetOffset()
|
||||
}
|
||||
|
||||
dblclickHandler () {
|
||||
// User preference deciding which action to take on a title bar double-click
|
||||
switch (this.applicationDelegate.getUserDefault('AppleActionOnDoubleClick', 'string')) {
|
||||
case 'Minimize':
|
||||
this.applicationDelegate.minimizeWindow()
|
||||
break
|
||||
case 'Maximize':
|
||||
if (this.applicationDelegate.isWindowMaximized()) {
|
||||
this.applicationDelegate.unmaximizeWindow()
|
||||
} else {
|
||||
this.applicationDelegate.maximizeWindow()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
updateTitle () {
|
||||
this.titleElement.textContent = document.title
|
||||
}
|
||||
|
||||
updateWindowSheetOffset () {
|
||||
this.applicationDelegate.getCurrentWindow().setSheetOffset(this.element.offsetHeight)
|
||||
}
|
||||
}
|
||||
@@ -184,7 +184,6 @@ module.exports = class Workspace extends Model {
|
||||
this.didChangeActivePaneOnPaneContainer = this.didChangeActivePaneOnPaneContainer.bind(this)
|
||||
this.didChangeActivePaneItemOnPaneContainer = this.didChangeActivePaneItemOnPaneContainer.bind(this)
|
||||
this.didActivatePaneContainer = this.didActivatePaneContainer.bind(this)
|
||||
this.didHideDock = this.didHideDock.bind(this)
|
||||
|
||||
this.enablePersistence = params.enablePersistence
|
||||
this.packageManager = params.packageManager
|
||||
@@ -270,7 +269,6 @@ module.exports = class Workspace extends Model {
|
||||
deserializerManager: this.deserializerManager,
|
||||
notificationManager: this.notificationManager,
|
||||
viewRegistry: this.viewRegistry,
|
||||
didHide: this.didHideDock,
|
||||
didActivate: this.didActivatePaneContainer,
|
||||
didChangeActivePane: this.didChangeActivePaneOnPaneContainer,
|
||||
didChangeActivePaneItem: this.didChangeActivePaneItemOnPaneContainer,
|
||||
@@ -321,6 +319,7 @@ module.exports = class Workspace extends Model {
|
||||
this.subscribeToFontSize()
|
||||
this.subscribeToAddedItems()
|
||||
this.subscribeToMovedItems()
|
||||
this.subscribeToDockToggling()
|
||||
}
|
||||
|
||||
consumeServices ({serviceHub}) {
|
||||
@@ -484,14 +483,6 @@ module.exports = class Workspace extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
didHideDock (dock) {
|
||||
const {activeElement} = document
|
||||
const dockElement = dock.getElement()
|
||||
if (dockElement === activeElement || dockElement.contains(activeElement)) {
|
||||
this.getCenter().activate()
|
||||
}
|
||||
}
|
||||
|
||||
setDraggingItem (draggingItem) {
|
||||
_.values(this.paneContainers).forEach(dock => {
|
||||
dock.setDraggingItem(draggingItem)
|
||||
@@ -513,6 +504,20 @@ module.exports = class Workspace extends Model {
|
||||
})
|
||||
}
|
||||
|
||||
subscribeToDockToggling () {
|
||||
const docks = [this.getLeftDock(), this.getRightDock(), this.getBottomDock()]
|
||||
docks.forEach(dock => {
|
||||
dock.onDidChangeVisible(visible => {
|
||||
if (visible) return
|
||||
const {activeElement} = document
|
||||
const dockElement = dock.getElement()
|
||||
if (dockElement === activeElement || dockElement.contains(activeElement)) {
|
||||
this.getCenter().activate()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
subscribeToMovedItems () {
|
||||
for (const paneContainer of this.getPaneContainers()) {
|
||||
paneContainer.observePanes(pane => {
|
||||
@@ -582,6 +587,7 @@ module.exports = class Workspace extends Model {
|
||||
|
||||
document.title = titleParts.join(' \u2014 ')
|
||||
this.applicationDelegate.setRepresentedFilename(representedPath)
|
||||
this.emitter.emit('did-change-window-title')
|
||||
}
|
||||
|
||||
// On macOS, fades the application window's proxy icon when the current file
|
||||
@@ -606,7 +612,7 @@ module.exports = class Workspace extends Model {
|
||||
// editors in the workspace.
|
||||
//
|
||||
// * `callback` {Function} to be called with current and future text editors.
|
||||
// * `editor` An {TextEditor} that is present in {::getTextEditors} at the time
|
||||
// * `editor` A {TextEditor} that is present in {::getTextEditors} at the time
|
||||
// of subscription or that is added at some later time.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
@@ -859,6 +865,10 @@ module.exports = class Workspace extends Model {
|
||||
return this.emitter.on('did-add-text-editor', callback)
|
||||
}
|
||||
|
||||
onDidChangeWindowTitle (callback) {
|
||||
return this.emitter.on('did-change-window-title', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Opening
|
||||
*/
|
||||
@@ -1940,6 +1950,7 @@ module.exports = class Workspace extends Model {
|
||||
|
||||
if (!outOfProcessFinished.length) {
|
||||
let flags = 'g'
|
||||
if (regex.multiline) { flags += 'm' }
|
||||
if (regex.ignoreCase) { flags += 'i' }
|
||||
|
||||
const task = Task.once(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src * atom://*; img-src blob: data: * atom://*; script-src 'self'; style-src 'self' 'unsafe-inline'; media-src blob: data: mediastream: * atom://*;">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src * atom://*; img-src blob: data: * atom://*; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src blob: data: mediastream: * atom://*;">
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body tabindex="-1">
|
||||
|
||||
@@ -20,6 +20,7 @@ atom-text-editor {
|
||||
min-width: 1em;
|
||||
box-sizing: border-box;
|
||||
background-color: inherit;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gutter:hover {
|
||||
@@ -51,7 +52,6 @@ atom-text-editor {
|
||||
}
|
||||
|
||||
.line-number {
|
||||
width: min-content;
|
||||
padding-left: .5em;
|
||||
white-space: nowrap;
|
||||
opacity: 0.6;
|
||||
|
||||
Reference in New Issue
Block a user