Merge branch 'master' into bring-back-asar

This commit is contained in:
Antonio Scandurra
2017-08-15 10:03:27 +02:00
committed by GitHub
82 changed files with 3372 additions and 717 deletions

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
[![macOS Build Status](https://circleci.com/gh/atom/atom/tree/master.svg?style=shield)](https://circleci.com/gh/atom/atom) [![Linux Build Status](https://travis-ci.org/atom/atom.svg?branch=master)](https://travis-ci.org/atom/atom) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/1tkktwh654w07eim?svg=true)](https://ci.appveyor.com/project/Atom/atom)
[![Dependency Status](https://david-dm.org/atom/atom.svg)](https://david-dm.org/atom/atom)
[![Join the Atom Community on Slack](http://atom-slack.herokuapp.com/badge.svg)](http://atom-slack.herokuapp.com/)
[![Join the Atom Community on Slack](https://atom-slack.herokuapp.com/badge.svg)](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
View 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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": {

View File

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

View File

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

View File

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

View 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

View File

@@ -875,10 +875,6 @@
"hasDeprecations": true,
"latestHasDeprecations": false
},
"language-typescript": {
"hasAlternative": true,
"alternative": "atom-typescript"
},
"laravel-facades": {
"version": "<=1.0.0",
"hasDeprecations": true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,11 @@ describe("FileRecoveryService", () => {
})
afterEach(() => {
temp.cleanupSync()
try {
temp.cleanupSync()
} catch (e) {
// Ignore
}
})
describe("when no crash happens during a save", () => {

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,8 @@ describe "atom.themes", ->
afterEach ->
atom.themes.deactivateThemes()
temp.cleanupSync()
try
temp.cleanupSync()
describe "theme getters and setters", ->
beforeEach ->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -143,6 +143,7 @@ class ApplicationDelegate
message: message
detail: detailedMessage
buttons: buttonLabels
normalizeAccessKeys: true
})
if _.isArray(buttons)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -40,7 +40,7 @@ module.exports = class PanelContainer {
}
onDidDestroy (callback) {
return this.emitter.on('did-destroy', callback)
return this.emitter.once('did-destroy', callback)
}
/*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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