mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Merge remote-tracking branch 'origin/master' into b3-failing-seed
This commit is contained in:
55
docs/focus/2018-02-12.md
Normal file
55
docs/focus/2018-02-12.md
Normal file
@@ -0,0 +1,55 @@
|
||||
## Highlights from the past week
|
||||
|
||||
- Atom IDE
|
||||
- Started conversion of atom-languageclient to TypeScript [atom/atom-languageclient#175](https://github.com/atom/atom-languageclient/pull/175)
|
||||
- @atom/watcher
|
||||
- Report events related to [symlinks](https://github.com/atom/watcher/pull/111) and [test for symlink-related edge cases.](https://github.com/atom/watcher/pull/114)
|
||||
- Produce filesystem events with a [consistent parent path](https://github.com/atom/watcher/pull/113) to the one used to create a watcher, even if the watcher was created with a a path containing symlinks.
|
||||
- Verified correct behavior with regard to [filesystem case sensitivity.](https://github.com/atom/watcher/pull/116)
|
||||
- Corrected buggy [utf8 to utf16 conversion](https://github.com/atom/watcher/pull/115) on Windows.
|
||||
- Ran through the MacOS cases in the [testing matrix.](https://github.com/atom/atom/pull/16124)
|
||||
- Set up a Samba share on @ungb's testing server to exercise Samba network drives.
|
||||
- Published version 1.0.0 on [npm.](https://www.npmjs.com/package/@atom/watcher)
|
||||
- GitHub Package
|
||||
- Introduce a package configuration option to [disable the in-editor merge conflict resolution.](https://github.com/atom/github/pull/1305)
|
||||
- Published a new release v0.10.0
|
||||
- Investigated and spiked on a fix for amending bug in single-commit repos, which was surfaced by failing cache invalidation tests that were blocking release
|
||||
- Deferred fixing underlying bug - [atom/github#1303](https://github.com/atom/github/issues/1303)
|
||||
- Fixed failing tests - [atom/github#1302](https://github.com/atom/github/pull/1302)
|
||||
- Teletype
|
||||
- Released [Teletype 0.7.0](https://github.com/atom/teletype/releases/tag/v0.7.0) with improved diagnostics for errors that occur during package initialization ([atom/teletype#266](https://github.com/atom/teletype/issues/266), [atom/teletype#297](https://github.com/atom/teletype/issues/297))
|
||||
- Opened [atom/teletype#323](https://github.com/atom/teletype/pull/323), [atom/teletype-client#52](https://github.com/atom/teletype-client/pull/52), and [atom/fuzzy-finder#335](https://github.com/atom/fuzzy-finder/pull/335) to pave the way for guests to use the fuzzy-finder to open any remote editor shared by the host ([atom/teletype#268](https://github.com/atom/teletype/issues/268))
|
||||
|
||||
## Focus for week ahead
|
||||
|
||||
- Atom IDE
|
||||
- Finish conversion of atom-languageclient to TypeScript [atom/atom-languageclient#175](https://github.com/atom/atom-languageclient/pull/175)
|
||||
- Contribute TypeScript type definitions for Atom IDE to [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped)
|
||||
- Contribute missing TypeScript type defintions for Atom to [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/atom)
|
||||
- @atom/watcher
|
||||
- Complete [the testing matrix](https://github.com/atom/atom/pull/16124) on Linux and Windows.
|
||||
- :shipit: Merge [@atom/watcher support]((https://github.com/atom/atom/pull/16124)) into Atom _(as a non-default `PathWatcher` backend)_. :shipit:
|
||||
- GitHub Package
|
||||
- Quarterly planning. Which might change all of these :wink:
|
||||
- Finish tracking down our [freezing CI builds.](https://github.com/atom/github/pull/1289)
|
||||
- Resurrect the [gargantuan credential helper and GPG pinentry refactoring PR](https://github.com/atom/github/pull/846) and see how much work is needed to get it over the finish line.
|
||||
- Fix issue with diff view popping up unexpectedly - [atom/github#1287](https://github.com/atom/github/issues/1287)
|
||||
- Teletype
|
||||
- Complete initial implementation and merge pull requests ([atom/teletype#323](https://github.com/atom/teletype/pull/323), [atom/teletype-client#52](https://github.com/atom/teletype-client/pull/52), and [atom/fuzzy-finder#335](https://github.com/atom/fuzzy-finder/pull/335)) allowing guests to use the fuzzy-finder to open any remote editor shared by the host ([atom/teletype#268](https://github.com/atom/teletype/issues/268))
|
||||
- Use fuzzy-finder support internally in our day-to-day workflows to assess usability
|
||||
- Tree-sitter
|
||||
- Finish and merge [tree-sitter/tree-sitter#128](https://github.com/tree-sitter/tree-sitter/pull/128), which fixes a fundamental performance problem when editing large files.
|
||||
- Fix syntax highlighting bugs [#16643](https://github.com/atom/atom/issues/16643) and [#16642](https://github.com/atom/atom/issues/16642).
|
||||
- Fix [#16621](https://github.com/atom/atom/issues/16621) - snippets not working when using Tree-sitter.
|
||||
- Xray
|
||||
* @nathansobo (and @as-cii part time) will be focusing the next 12 weeks on a prototype for [a new Electron-based text editor](https://github.com/atom/xray). The goal is to explore the viability of radical performance improvements that could be possible if we make breaking changes to Atom's APIs. At the end of the 12 weeks, we will reassess our plans based on what we have managed to learn and accomplish.
|
||||
* Week 1 of 12
|
||||
* Clarify and document goals for the next 12 weeks.
|
||||
* Ensure that the guide matches our current plans.
|
||||
* Refine WebGL based text rendering.
|
||||
* Make sure ASCII text renders correctly without being clipped
|
||||
* Render text correctly on high DPI displays
|
||||
* Use correct API for texture atlas updates
|
||||
* Add mouse-wheel scrolling support
|
||||
* Non-ASCII rendering, using the HarfBuzz text shaping library to detect combining characters
|
||||
* Stretch goal: Switch document encoding to UTF-8 for memory compactness and support multi-byte-aware character indexing.
|
||||
@@ -1,13 +1,12 @@
|
||||
/** @babel */
|
||||
|
||||
import TextBuffer, {Point, Range} from 'text-buffer'
|
||||
import {File, Directory} from 'pathwatcher'
|
||||
import {Emitter, Disposable, CompositeDisposable} from 'event-kit'
|
||||
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 TextBuffer = require('text-buffer')
|
||||
const {Point, Range} = TextBuffer
|
||||
const {File, Directory} = require('pathwatcher')
|
||||
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
|
||||
const BufferedNodeProcess = require('../src/buffered-node-process')
|
||||
const BufferedProcess = require('../src/buffered-process')
|
||||
const GitRepository = require('../src/git-repository')
|
||||
const Notification = require('../src/notification')
|
||||
const {watchPath} = require('../src/path-watcher')
|
||||
|
||||
const atomExport = {
|
||||
BufferedNodeProcess,
|
||||
@@ -42,4 +41,4 @@ if (process.type === 'renderer') {
|
||||
atomExport.TextEditor = require('../src/text-editor')
|
||||
}
|
||||
|
||||
export default atomExport
|
||||
module.exports = atomExport
|
||||
|
||||
71
package.json
71
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "atom",
|
||||
"productName": "Atom",
|
||||
"version": "1.25.0-dev",
|
||||
"version": "1.26.0-dev",
|
||||
"description": "A hackable text editor for the 21st Century.",
|
||||
"main": "./src/main-process/main.js",
|
||||
"repository": {
|
||||
@@ -12,12 +12,13 @@
|
||||
"url": "https://github.com/atom/atom/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"electronVersion": "1.7.10",
|
||||
"electronVersion": "1.7.11",
|
||||
"dependencies": {
|
||||
"@atom/nsfw": "^1.0.18",
|
||||
"@atom/watcher": "1.0.1",
|
||||
"@atom/source-map-support": "^0.3.4",
|
||||
"async": "0.2.6",
|
||||
"atom-keymap": "8.2.8",
|
||||
"atom-keymap": "8.2.9",
|
||||
"atom-select-list": "^0.7.0",
|
||||
"atom-ui": "0.4.1",
|
||||
"babel-core": "5.8.38",
|
||||
@@ -47,7 +48,7 @@
|
||||
"key-path-helpers": "^0.4.0",
|
||||
"less-cache": "1.1.0",
|
||||
"line-top-index": "0.3.1",
|
||||
"marked": "^0.3.6",
|
||||
"marked": "^0.3.12",
|
||||
"minimatch": "^3.0.3",
|
||||
"mocha": "2.5.1",
|
||||
"mocha-junit-reporter": "^1.13.0",
|
||||
@@ -70,8 +71,8 @@
|
||||
"service-hub": "^0.7.4",
|
||||
"sinon": "1.17.4",
|
||||
"temp": "^0.8.3",
|
||||
"text-buffer": "13.11.5",
|
||||
"tree-sitter": "^0.8.6",
|
||||
"text-buffer": "13.11.8",
|
||||
"tree-sitter": "^0.9.1",
|
||||
"typescript-simple": "1.0.0",
|
||||
"underscore-plus": "^1.6.6",
|
||||
"winreg": "^1.2.1",
|
||||
@@ -79,20 +80,20 @@
|
||||
},
|
||||
"packageDependencies": {
|
||||
"atom-dark-syntax": "0.29.0",
|
||||
"atom-dark-ui": "0.53.1",
|
||||
"atom-dark-ui": "0.53.2",
|
||||
"atom-light-syntax": "0.29.0",
|
||||
"atom-light-ui": "0.46.1",
|
||||
"atom-light-ui": "0.46.2",
|
||||
"base16-tomorrow-dark-theme": "1.5.0",
|
||||
"base16-tomorrow-light-theme": "1.5.0",
|
||||
"one-dark-ui": "1.10.10",
|
||||
"one-light-ui": "1.10.10",
|
||||
"one-dark-ui": "1.10.11",
|
||||
"one-light-ui": "1.10.11",
|
||||
"one-dark-syntax": "1.8.2",
|
||||
"one-light-syntax": "1.8.2",
|
||||
"solarized-dark-syntax": "1.1.4",
|
||||
"solarized-light-syntax": "1.1.4",
|
||||
"about": "1.8.0",
|
||||
"archive-view": "0.64.2",
|
||||
"autocomplete-atom-api": "0.10.6",
|
||||
"archive-view": "0.64.3",
|
||||
"autocomplete-atom-api": "0.10.7",
|
||||
"autocomplete-css": "0.17.5",
|
||||
"autocomplete-html": "0.8.4",
|
||||
"autocomplete-plus": "2.40.2",
|
||||
@@ -101,19 +102,19 @@
|
||||
"autosave": "0.24.6",
|
||||
"background-tips": "0.27.1",
|
||||
"bookmarks": "0.45.1",
|
||||
"bracket-matcher": "0.89.0",
|
||||
"command-palette": "0.43.0",
|
||||
"bracket-matcher": "0.89.1",
|
||||
"command-palette": "0.43.3",
|
||||
"dalek": "0.2.1",
|
||||
"deprecation-cop": "0.56.9",
|
||||
"dev-live-reload": "0.48.1",
|
||||
"encoding-selector": "0.23.8",
|
||||
"exception-reporting": "0.42.0",
|
||||
"exception-reporting": "0.43.1",
|
||||
"find-and-replace": "0.215.5",
|
||||
"fuzzy-finder": "1.7.5",
|
||||
"github": "0.9.1",
|
||||
"github": "0.10.1",
|
||||
"git-diff": "1.3.9",
|
||||
"go-to-line": "0.32.1",
|
||||
"grammar-selector": "0.49.9",
|
||||
"go-to-line": "0.33.0",
|
||||
"grammar-selector": "0.50.0",
|
||||
"image-view": "0.62.4",
|
||||
"incompatible-packages": "0.27.3",
|
||||
"keybinding-resolver": "0.38.1",
|
||||
@@ -137,39 +138,39 @@
|
||||
"welcome": "0.36.6",
|
||||
"whitespace": "0.37.5",
|
||||
"wrap-guide": "0.40.3",
|
||||
"language-c": "0.59.1",
|
||||
"language-clojure": "0.22.6",
|
||||
"language-c": "0.59.2",
|
||||
"language-clojure": "0.22.7",
|
||||
"language-coffee-script": "0.49.3",
|
||||
"language-csharp": "0.14.4",
|
||||
"language-css": "0.42.9",
|
||||
"language-csharp": "1.0.1",
|
||||
"language-css": "0.42.10",
|
||||
"language-gfm": "0.90.3",
|
||||
"language-git": "0.19.1",
|
||||
"language-go": "0.45.0",
|
||||
"language-html": "0.48.6",
|
||||
"language-go": "0.45.1",
|
||||
"language-html": "0.49.0",
|
||||
"language-hyperlink": "0.16.3",
|
||||
"language-java": "0.27.6",
|
||||
"language-javascript": "0.128.1",
|
||||
"language-java": "0.28.0",
|
||||
"language-javascript": "0.128.2",
|
||||
"language-json": "0.19.1",
|
||||
"language-less": "0.34.2",
|
||||
"language-make": "0.22.3",
|
||||
"language-mustache": "0.14.4",
|
||||
"language-mustache": "0.14.5",
|
||||
"language-objective-c": "0.15.1",
|
||||
"language-perl": "0.38.1",
|
||||
"language-php": "0.43.0",
|
||||
"language-php": "0.43.1",
|
||||
"language-property-list": "0.9.1",
|
||||
"language-python": "0.47.0",
|
||||
"language-python": "0.49.1",
|
||||
"language-ruby": "0.71.4",
|
||||
"language-ruby-on-rails": "0.25.3",
|
||||
"language-sass": "0.61.4",
|
||||
"language-shellscript": "0.26.0",
|
||||
"language-shellscript": "0.26.1",
|
||||
"language-source": "0.9.0",
|
||||
"language-sql": "0.25.9",
|
||||
"language-sql": "0.25.10",
|
||||
"language-text": "0.7.3",
|
||||
"language-todo": "0.29.3",
|
||||
"language-toml": "0.18.1",
|
||||
"language-typescript": "0.3.0",
|
||||
"language-todo": "0.29.4",
|
||||
"language-toml": "0.18.2",
|
||||
"language-typescript": "0.3.1",
|
||||
"language-xml": "0.35.2",
|
||||
"language-yaml": "0.31.1"
|
||||
"language-yaml": "0.31.2"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -16,9 +16,6 @@ module.exports = function () {
|
||||
|
||||
function getPathsToTranspile () {
|
||||
let paths = []
|
||||
paths = paths.concat(glob.sync(path.join(CONFIG.intermediateAppPath, 'benchmarks', '**', '*.js'), {nodir: true}))
|
||||
paths = paths.concat(glob.sync(path.join(CONFIG.intermediateAppPath, 'exports', '**', '*.js'), {nodir: true}))
|
||||
paths = paths.concat(glob.sync(path.join(CONFIG.intermediateAppPath, 'src', '**', '*.js'), {nodir: true}))
|
||||
for (let packageName of Object.keys(CONFIG.appMetadata.packageDependencies)) {
|
||||
paths = paths.concat(glob.sync(
|
||||
path.join(CONFIG.intermediateAppPath, 'node_modules', packageName, '**', '*.js'),
|
||||
|
||||
@@ -4,7 +4,6 @@ const fs = require('fs')
|
||||
const path = require('path')
|
||||
const temp = require('temp').track()
|
||||
const AtomEnvironment = require('../src/atom-environment')
|
||||
const StorageFolder = require('../src/storage-folder')
|
||||
|
||||
describe('AtomEnvironment', () => {
|
||||
afterEach(() => {
|
||||
|
||||
100
spec/config-file-spec.js
Normal file
100
spec/config-file-spec.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const {it, fit, ffit, beforeEach, afterEach, conditionPromise} = require('./async-spec-helpers')
|
||||
const fs = require('fs-plus')
|
||||
const path = require('path')
|
||||
const temp = require('temp').track()
|
||||
const dedent = require('dedent')
|
||||
const ConfigFile = require('../src/config-file')
|
||||
|
||||
describe('ConfigFile', () => {
|
||||
let filePath, configFile, subscription
|
||||
|
||||
beforeEach(async () => {
|
||||
jasmine.useRealClock()
|
||||
const tempDir = fs.realpathSync(temp.mkdirSync())
|
||||
filePath = path.join(tempDir, 'the-config.cson')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
subscription.dispose()
|
||||
})
|
||||
|
||||
describe('when the file does not exist', () => {
|
||||
it('returns an empty object from .get()', async () => {
|
||||
configFile = new ConfigFile(filePath)
|
||||
subscription = await configFile.watch()
|
||||
expect(configFile.get()).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the file is empty', () => {
|
||||
it('returns an empty object from .get()', async () => {
|
||||
writeFileSync(filePath, '')
|
||||
configFile = new ConfigFile(filePath)
|
||||
subscription = await configFile.watch()
|
||||
expect(configFile.get()).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the file is updated with valid CSON', () => {
|
||||
it('notifies onDidChange observers with the data', async () => {
|
||||
configFile = new ConfigFile(filePath)
|
||||
subscription = await configFile.watch()
|
||||
|
||||
const event = new Promise(resolve => configFile.onDidChange(resolve))
|
||||
|
||||
writeFileSync(filePath, dedent `
|
||||
'*':
|
||||
foo: 'bar'
|
||||
|
||||
'javascript':
|
||||
foo: 'baz'
|
||||
`)
|
||||
|
||||
expect(await event).toEqual({
|
||||
'*': {foo: 'bar'},
|
||||
'javascript': {foo: 'baz'}
|
||||
})
|
||||
|
||||
expect(configFile.get()).toEqual({
|
||||
'*': {foo: 'bar'},
|
||||
'javascript': {foo: 'baz'}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the file is updated with invalid CSON', () => {
|
||||
it('notifies onDidError observers', async () => {
|
||||
configFile = new ConfigFile(filePath)
|
||||
subscription = await configFile.watch()
|
||||
|
||||
const message = new Promise(resolve => configFile.onDidError(resolve))
|
||||
|
||||
writeFileSync(filePath, dedent `
|
||||
um what?
|
||||
`, 2)
|
||||
|
||||
expect(await message).toContain('Failed to load `the-config.cson`')
|
||||
|
||||
const event = new Promise(resolve => configFile.onDidChange(resolve))
|
||||
|
||||
writeFileSync(filePath, dedent `
|
||||
'*':
|
||||
foo: 'bar'
|
||||
|
||||
'javascript':
|
||||
foo: 'baz'
|
||||
`, 4)
|
||||
|
||||
expect(await event).toEqual({
|
||||
'*': {foo: 'bar'},
|
||||
'javascript': {foo: 'baz'}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function writeFileSync (filePath, content, seconds = 2) {
|
||||
const utime = (Date.now() / 1000) + seconds
|
||||
fs.writeFileSync(filePath, content)
|
||||
fs.utimesSync(filePath, utime, utime)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
1843
spec/config-spec.js
Normal file
1843
spec/config-spec.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,11 +15,6 @@ describe('GitRepository', () => {
|
||||
|
||||
afterEach(() => {
|
||||
if (repo && !repo.isDestroyed()) repo.destroy()
|
||||
|
||||
// These tests sometimes lag at shutting down resources
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (error) {}
|
||||
})
|
||||
|
||||
describe('@open(path)', () => {
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('AtomApplication', function () {
|
||||
fs.writeFileSync(filePath, '1\n2\n3\n4\n')
|
||||
const atomApplication = buildAtomApplication()
|
||||
|
||||
const window = atomApplication.launch(parseCommandLine([filePath + ':3']))
|
||||
const [window] = await atomApplication.launch(parseCommandLine([filePath + ':3']))
|
||||
await focusWindow(window)
|
||||
|
||||
const cursorRow = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
|
||||
@@ -66,7 +66,7 @@ describe('AtomApplication', function () {
|
||||
fs.writeFileSync(filePath, '1\n2\n3\n4\n')
|
||||
const atomApplication = buildAtomApplication()
|
||||
|
||||
const window = atomApplication.launch(parseCommandLine([filePath + ':2:2']))
|
||||
const [window] = await atomApplication.launch(parseCommandLine([filePath + ':2:2']))
|
||||
await focusWindow(window)
|
||||
|
||||
const cursorPosition = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
|
||||
@@ -83,7 +83,7 @@ describe('AtomApplication', function () {
|
||||
fs.writeFileSync(filePath, '1\n2\n3\n4\n')
|
||||
const atomApplication = buildAtomApplication()
|
||||
|
||||
const window = atomApplication.launch(parseCommandLine([filePath + ':: ']))
|
||||
const [window] = await atomApplication.launch(parseCommandLine([filePath + ':: ']))
|
||||
await focusWindow(window)
|
||||
|
||||
const openedPath = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
|
||||
@@ -99,11 +99,11 @@ describe('AtomApplication', function () {
|
||||
it('positions new windows at an offset distance from the previous window', async () => {
|
||||
const atomApplication = buildAtomApplication()
|
||||
|
||||
const window1 = atomApplication.launch(parseCommandLine([makeTempDir()]))
|
||||
const [window1] = await atomApplication.launch(parseCommandLine([makeTempDir()]))
|
||||
await focusWindow(window1)
|
||||
window1.browserWindow.setBounds({width: 400, height: 400, x: 0, y: 0})
|
||||
|
||||
const window2 = atomApplication.launch(parseCommandLine([makeTempDir()]))
|
||||
const [window2] = await atomApplication.launch(parseCommandLine([makeTempDir()]))
|
||||
await focusWindow(window2)
|
||||
|
||||
assert.notEqual(window1, window2)
|
||||
@@ -122,7 +122,7 @@ describe('AtomApplication', function () {
|
||||
fs.writeFileSync(existingDirCFilePath, 'this is an existing file')
|
||||
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'new-file')]))
|
||||
const [window1] = await atomApplication.launch(parseCommandLine([path.join(dirAPath, 'new-file')]))
|
||||
await emitterEventPromise(window1, 'window:locations-opened')
|
||||
await focusWindow(window1)
|
||||
|
||||
@@ -135,7 +135,7 @@ describe('AtomApplication', function () {
|
||||
|
||||
// Reuses the window when opening *files*, even if they're in a different directory
|
||||
// Does not change the project paths when doing so.
|
||||
const reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath]))
|
||||
const [reusedWindow] = await atomApplication.launch(parseCommandLine([existingDirCFilePath]))
|
||||
assert.equal(reusedWindow, window1)
|
||||
assert.deepEqual(atomApplication.getAllWindows(), [window1])
|
||||
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
|
||||
@@ -148,7 +148,7 @@ describe('AtomApplication', function () {
|
||||
assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath])
|
||||
|
||||
// Opens new windows when opening directories
|
||||
const window2 = atomApplication.launch(parseCommandLine([dirCPath]))
|
||||
const [window2] = await atomApplication.launch(parseCommandLine([dirCPath]))
|
||||
await emitterEventPromise(window2, 'window:locations-opened')
|
||||
assert.notEqual(window2, window1)
|
||||
await focusWindow(window2)
|
||||
@@ -163,7 +163,7 @@ describe('AtomApplication', function () {
|
||||
fs.writeFileSync(existingDirCFilePath, 'this is an existing file')
|
||||
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'new-file')]))
|
||||
const [window1] = await atomApplication.launch(parseCommandLine([path.join(dirAPath, 'new-file')]))
|
||||
await focusWindow(window1)
|
||||
|
||||
let activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
|
||||
@@ -175,7 +175,7 @@ describe('AtomApplication', function () {
|
||||
|
||||
// When opening *files* with --add, reuses an existing window and adds
|
||||
// parent directory to the project
|
||||
let reusedWindow = atomApplication.launch(parseCommandLine([existingDirCFilePath, '--add']))
|
||||
let [reusedWindow] = await atomApplication.launch(parseCommandLine([existingDirCFilePath, '--add']))
|
||||
assert.equal(reusedWindow, window1)
|
||||
assert.deepEqual(atomApplication.getAllWindows(), [window1])
|
||||
activeEditorPath = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
|
||||
@@ -189,7 +189,7 @@ describe('AtomApplication', function () {
|
||||
|
||||
// When opening *directories* with add reuses an existing window and adds
|
||||
// the directory to the project
|
||||
reusedWindow = atomApplication.launch(parseCommandLine([dirBPath, '-a']))
|
||||
reusedWindow = (await atomApplication.launch(parseCommandLine([dirBPath, '-a'])))[0]
|
||||
assert.equal(reusedWindow, window1)
|
||||
assert.deepEqual(atomApplication.getAllWindows(), [window1])
|
||||
|
||||
@@ -202,7 +202,7 @@ describe('AtomApplication', function () {
|
||||
const atomApplication = buildAtomApplication()
|
||||
const nonExistentFilePath = path.join(tempDirPath, 'new-file')
|
||||
|
||||
const window1 = atomApplication.launch(parseCommandLine([nonExistentFilePath]))
|
||||
const [window1] = await atomApplication.launch(parseCommandLine([nonExistentFilePath]))
|
||||
await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
|
||||
atom.workspace.observeTextEditors(textEditor => {
|
||||
textEditor.insertText('Hello World!')
|
||||
@@ -214,7 +214,7 @@ describe('AtomApplication', function () {
|
||||
await window1.closedPromise
|
||||
|
||||
// Restore unsaved state when opening the directory itself
|
||||
const window2 = atomApplication.launch(parseCommandLine([tempDirPath]))
|
||||
const [window2] = await atomApplication.launch(parseCommandLine([tempDirPath]))
|
||||
await window2.loadedPromise
|
||||
const window2Text = await evalInWebContents(window2.browserWindow.webContents, sendBackToMainProcess => {
|
||||
const textEditor = atom.workspace.getActiveTextEditor()
|
||||
@@ -228,7 +228,7 @@ describe('AtomApplication', function () {
|
||||
await window2.closedPromise
|
||||
|
||||
// Restore unsaved state when opening a path to a non-existent file in the directory
|
||||
const window3 = atomApplication.launch(parseCommandLine([path.join(tempDirPath, 'another-non-existent-file')]))
|
||||
const [window3] = await atomApplication.launch(parseCommandLine([path.join(tempDirPath, 'another-non-existent-file')]))
|
||||
await window3.loadedPromise
|
||||
const window3Texts = await evalInWebContents(window3.browserWindow.webContents, (sendBackToMainProcess, nonExistentFilePath) => {
|
||||
sendBackToMainProcess(atom.workspace.getTextEditors().map(editor => editor.getText()))
|
||||
@@ -243,7 +243,7 @@ describe('AtomApplication', function () {
|
||||
fs.mkdirSync(dirBSubdirPath)
|
||||
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window1 = atomApplication.launch(parseCommandLine([dirAPath, dirBPath]))
|
||||
const [window1] = await atomApplication.launch(parseCommandLine([dirAPath, dirBPath]))
|
||||
await focusWindow(window1)
|
||||
|
||||
assert.deepEqual(await getTreeViewRootDirectories(window1), [dirAPath, dirBPath])
|
||||
@@ -252,17 +252,17 @@ describe('AtomApplication', function () {
|
||||
it('reuses windows with no project paths to open directories', async () => {
|
||||
const tempDirPath = makeTempDir()
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window1 = atomApplication.launch(parseCommandLine([]))
|
||||
const [window1] = await atomApplication.launch(parseCommandLine([]))
|
||||
await focusWindow(window1)
|
||||
|
||||
const reusedWindow = atomApplication.launch(parseCommandLine([tempDirPath]))
|
||||
const [reusedWindow] = await atomApplication.launch(parseCommandLine([tempDirPath]))
|
||||
assert.equal(reusedWindow, window1)
|
||||
await conditionPromise(async () => (await getTreeViewRootDirectories(reusedWindow)).length > 0)
|
||||
})
|
||||
|
||||
it('opens a new window with a single untitled buffer when launched with no path, even if windows already exist', async () => {
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window1 = atomApplication.launch(parseCommandLine([]))
|
||||
const [window1] = await atomApplication.launch(parseCommandLine([]))
|
||||
await focusWindow(window1)
|
||||
const window1EditorTitle = await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
|
||||
sendBackToMainProcess(atom.workspace.getActiveTextEditor().getTitle())
|
||||
@@ -287,7 +287,7 @@ describe('AtomApplication', function () {
|
||||
season.writeFileSync(configPath, config)
|
||||
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window1 = atomApplication.launch(parseCommandLine([]))
|
||||
const [window1] = await atomApplication.launch(parseCommandLine([]))
|
||||
await focusWindow(window1)
|
||||
|
||||
// wait a bit just to make sure we don't pass due to querying the render process before it loads
|
||||
@@ -302,7 +302,7 @@ describe('AtomApplication', function () {
|
||||
it('opens an empty text editor and loads its parent directory in the tree-view when launched with a new file path', async () => {
|
||||
const atomApplication = buildAtomApplication()
|
||||
const newFilePath = path.join(makeTempDir(), 'new-file')
|
||||
const window = atomApplication.launch(parseCommandLine([newFilePath]))
|
||||
const [window] = await atomApplication.launch(parseCommandLine([newFilePath]))
|
||||
await focusWindow(window)
|
||||
const {editorTitle, editorText} = await evalInWebContents(window.browserWindow.webContents, sendBackToMainProcess => {
|
||||
atom.workspace.observeTextEditors(editor => {
|
||||
@@ -324,7 +324,7 @@ describe('AtomApplication', function () {
|
||||
atomApplication.config.set('core.disabledPackages', ['fuzzy-finder'])
|
||||
|
||||
const remotePath = 'remote://server:3437/some/directory/path'
|
||||
let window = atomApplication.launch(parseCommandLine([remotePath]))
|
||||
let [window] = await atomApplication.launch(parseCommandLine([remotePath]))
|
||||
|
||||
await focusWindow(window)
|
||||
await conditionPromise(async () => (await getProjectDirectories()).length > 0)
|
||||
@@ -350,9 +350,9 @@ describe('AtomApplication', function () {
|
||||
const tempDirPath2 = makeTempDir()
|
||||
|
||||
const atomApplication1 = buildAtomApplication()
|
||||
const app1Window1 = atomApplication1.launch(parseCommandLine([tempDirPath1]))
|
||||
const [app1Window1] = await atomApplication1.launch(parseCommandLine([tempDirPath1]))
|
||||
await emitterEventPromise(app1Window1, 'window:locations-opened')
|
||||
const app1Window2 = atomApplication1.launch(parseCommandLine([tempDirPath2]))
|
||||
const [app1Window2] = await atomApplication1.launch(parseCommandLine([tempDirPath2]))
|
||||
await emitterEventPromise(app1Window2, 'window:locations-opened')
|
||||
|
||||
await Promise.all([
|
||||
@@ -361,7 +361,7 @@ describe('AtomApplication', function () {
|
||||
])
|
||||
|
||||
const atomApplication2 = buildAtomApplication()
|
||||
const [app2Window1, app2Window2] = atomApplication2.launch(parseCommandLine([]))
|
||||
const [app2Window1, app2Window2] = await atomApplication2.launch(parseCommandLine([]))
|
||||
await Promise.all([
|
||||
emitterEventPromise(app2Window1, 'window:locations-opened'),
|
||||
emitterEventPromise(app2Window2, 'window:locations-opened')
|
||||
@@ -373,9 +373,9 @@ describe('AtomApplication', function () {
|
||||
|
||||
it('does not reopen any previously opened windows when launched with no path and `core.restorePreviousWindowsOnStart` is no', async () => {
|
||||
const atomApplication1 = buildAtomApplication()
|
||||
const app1Window1 = atomApplication1.launch(parseCommandLine([makeTempDir()]))
|
||||
const [app1Window1] = await atomApplication1.launch(parseCommandLine([makeTempDir()]))
|
||||
await focusWindow(app1Window1)
|
||||
const app1Window2 = atomApplication1.launch(parseCommandLine([makeTempDir()]))
|
||||
const [app1Window2] = await atomApplication1.launch(parseCommandLine([makeTempDir()]))
|
||||
await focusWindow(app1Window2)
|
||||
|
||||
const configPath = path.join(process.env.ATOM_HOME, 'config.cson')
|
||||
@@ -385,7 +385,7 @@ describe('AtomApplication', function () {
|
||||
season.writeFileSync(configPath, config)
|
||||
|
||||
const atomApplication2 = buildAtomApplication()
|
||||
const app2Window = atomApplication2.launch(parseCommandLine([]))
|
||||
const [app2Window] = await atomApplication2.launch(parseCommandLine([]))
|
||||
await focusWindow(app2Window)
|
||||
assert.deepEqual(app2Window.representedDirectoryPaths, [])
|
||||
})
|
||||
@@ -405,10 +405,10 @@ describe('AtomApplication', function () {
|
||||
})
|
||||
|
||||
it('kills the specified pid after a newly-opened window is closed', async () => {
|
||||
const window1 = atomApplication.launch(parseCommandLine(['--wait', '--pid', '101']))
|
||||
const [window1] = await atomApplication.launch(parseCommandLine(['--wait', '--pid', '101']))
|
||||
await focusWindow(window1)
|
||||
|
||||
const [window2] = atomApplication.launch(parseCommandLine(['--new-window', '--wait', '--pid', '102']))
|
||||
const [window2] = await atomApplication.launch(parseCommandLine(['--new-window', '--wait', '--pid', '102']))
|
||||
await focusWindow(window2)
|
||||
assert.deepEqual(killedPids, [])
|
||||
|
||||
@@ -424,7 +424,7 @@ describe('AtomApplication', function () {
|
||||
})
|
||||
|
||||
it('kills the specified pid after a newly-opened file in an existing window is closed', async () => {
|
||||
const window = atomApplication.launch(parseCommandLine(['--wait', '--pid', '101']))
|
||||
const [window] = await atomApplication.launch(parseCommandLine(['--wait', '--pid', '101']))
|
||||
await focusWindow(window)
|
||||
|
||||
const filePath1 = temp.openSync('test').path
|
||||
@@ -432,7 +432,7 @@ describe('AtomApplication', function () {
|
||||
fs.writeFileSync(filePath1, 'File 1')
|
||||
fs.writeFileSync(filePath2, 'File 2')
|
||||
|
||||
const reusedWindow = atomApplication.launch(parseCommandLine(['--wait', '--pid', '102', filePath1, filePath2]))
|
||||
const [reusedWindow] = await atomApplication.launch(parseCommandLine(['--wait', '--pid', '102', filePath1, filePath2]))
|
||||
assert.equal(reusedWindow, window)
|
||||
|
||||
const activeEditorPath = await evalInWebContents(window.browserWindow.webContents, send => {
|
||||
@@ -467,11 +467,11 @@ describe('AtomApplication', function () {
|
||||
})
|
||||
|
||||
it('kills the specified pid after a newly-opened directory in an existing window is closed', async () => {
|
||||
const window = atomApplication.launch(parseCommandLine([]))
|
||||
const [window] = await atomApplication.launch(parseCommandLine([]))
|
||||
await focusWindow(window)
|
||||
|
||||
const dirPath1 = makeTempDir()
|
||||
const reusedWindow = atomApplication.launch(parseCommandLine(['--wait', '--pid', '101', dirPath1]))
|
||||
const [reusedWindow] = await atomApplication.launch(parseCommandLine(['--wait', '--pid', '101', dirPath1]))
|
||||
assert.equal(reusedWindow, window)
|
||||
assert.deepEqual(await getTreeViewRootDirectories(window), [dirPath1])
|
||||
assert.deepEqual(killedPids, [])
|
||||
@@ -498,7 +498,7 @@ describe('AtomApplication', function () {
|
||||
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||
it('quits the application', async () => {
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window = atomApplication.launch(parseCommandLine([path.join(makeTempDir("a"), 'file-a')]))
|
||||
const [window] = await atomApplication.launch(parseCommandLine([path.join(makeTempDir("a"), 'file-a')]))
|
||||
await focusWindow(window)
|
||||
window.close()
|
||||
await window.closedPromise
|
||||
@@ -508,7 +508,7 @@ describe('AtomApplication', function () {
|
||||
} else if (process.platform === 'darwin') {
|
||||
it('leaves the application open', async () => {
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window = atomApplication.launch(parseCommandLine([path.join(makeTempDir("a"), 'file-a')]))
|
||||
const [window] = await atomApplication.launch(parseCommandLine([path.join(makeTempDir("a"), 'file-a')]))
|
||||
await focusWindow(window)
|
||||
window.close()
|
||||
await window.closedPromise
|
||||
@@ -524,7 +524,7 @@ describe('AtomApplication', function () {
|
||||
const dirB = makeTempDir()
|
||||
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window = atomApplication.launch(parseCommandLine([dirA, dirB]))
|
||||
const [window] = await atomApplication.launch(parseCommandLine([dirA, dirB]))
|
||||
await emitterEventPromise(window, 'window:locations-opened')
|
||||
await focusWindow(window)
|
||||
assert.deepEqual(await getTreeViewRootDirectories(window), [dirA, dirB])
|
||||
@@ -539,7 +539,7 @@ describe('AtomApplication', function () {
|
||||
|
||||
// Window state should be saved when the project folder is removed
|
||||
const atomApplication2 = buildAtomApplication()
|
||||
const [window2] = atomApplication2.launch(parseCommandLine([]))
|
||||
const [window2] = await atomApplication2.launch(parseCommandLine([]))
|
||||
await emitterEventPromise(window2, 'window:locations-opened')
|
||||
await focusWindow(window2)
|
||||
assert.deepEqual(await getTreeViewRootDirectories(window2), [dirB])
|
||||
@@ -556,7 +556,7 @@ describe('AtomApplication', function () {
|
||||
const atomApplication = buildAtomApplication()
|
||||
const launchOptions = parseCommandLine([])
|
||||
launchOptions.urlsToOpen = ['atom://package-with-url-main/test']
|
||||
let windows = atomApplication.launch(launchOptions)
|
||||
let [windows] = await atomApplication.launch(launchOptions)
|
||||
await windows[0].loadedPromise
|
||||
|
||||
let reached = await evalInWebContents(windows[0].browserWindow.webContents, sendBackToMainProcess => {
|
||||
@@ -571,9 +571,9 @@ describe('AtomApplication', function () {
|
||||
const dirBPath = makeTempDir('b')
|
||||
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath)]))
|
||||
const [window1] = await atomApplication.launch(parseCommandLine([path.join(dirAPath)]))
|
||||
await focusWindow(window1)
|
||||
const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath)]))
|
||||
const [window2] = await atomApplication.launch(parseCommandLine([path.join(dirBPath)]))
|
||||
await focusWindow(window2)
|
||||
|
||||
const fileA = path.join(dirAPath, 'file-a')
|
||||
@@ -597,9 +597,9 @@ describe('AtomApplication', function () {
|
||||
const dirAPath = makeTempDir("a")
|
||||
const dirBPath = makeTempDir("b")
|
||||
const atomApplication = buildAtomApplication()
|
||||
const window1 = atomApplication.launch(parseCommandLine([path.join(dirAPath, 'file-a')]))
|
||||
const [window1] = await atomApplication.launch(parseCommandLine([path.join(dirAPath, 'file-a')]))
|
||||
await focusWindow(window1)
|
||||
const window2 = atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')]))
|
||||
const [window2] = await atomApplication.launch(parseCommandLine([path.join(dirBPath, 'file-b')]))
|
||||
await focusWindow(window2)
|
||||
electron.app.quit()
|
||||
await new Promise(process.nextTick)
|
||||
@@ -612,8 +612,8 @@ describe('AtomApplication', function () {
|
||||
|
||||
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([]))
|
||||
const [window1] = await atomApplication.launch(parseCommandLine([]))
|
||||
const [window2] = await atomApplication.launch(parseCommandLine([]))
|
||||
await Promise.all([window1.loadedPromise, window2.loadedPromise])
|
||||
await evalInWebContents(window1.browserWindow.webContents, sendBackToMainProcess => {
|
||||
atom.workspace.getActiveTextEditor().insertText('unsaved text')
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/** @babel */
|
||||
|
||||
import {dialog} from 'electron'
|
||||
import FileRecoveryService from '../../src/main-process/file-recovery-service'
|
||||
import fs from 'fs-plus'
|
||||
import sinon from 'sinon'
|
||||
import {escapeRegExp} from 'underscore-plus'
|
||||
const {dialog} = require('electron')
|
||||
const FileRecoveryService = require('../../src/main-process/file-recovery-service')
|
||||
const fs = require('fs-plus')
|
||||
const sinon = require('sinon')
|
||||
const {escapeRegExp} = require('underscore-plus')
|
||||
const temp = require('temp').track()
|
||||
|
||||
describe("FileRecoveryService", () => {
|
||||
let recoveryService, recoveryDirectory
|
||||
let recoveryService, recoveryDirectory, spies
|
||||
|
||||
beforeEach(() => {
|
||||
recoveryDirectory = temp.mkdirSync('atom-spec-file-recovery')
|
||||
recoveryService = new FileRecoveryService(recoveryDirectory)
|
||||
spies = sinon.sandbox.create()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
spies.restore()
|
||||
try {
|
||||
temp.cleanupSync()
|
||||
} catch (e) {
|
||||
@@ -24,38 +24,38 @@ describe("FileRecoveryService", () => {
|
||||
})
|
||||
|
||||
describe("when no crash happens during a save", () => {
|
||||
it("creates a recovery file and deletes it after saving", () => {
|
||||
it("creates a recovery file and deletes it after saving", async () => {
|
||||
const mockWindow = {}
|
||||
const filePath = temp.path()
|
||||
|
||||
fs.writeFileSync(filePath, "some content")
|
||||
recoveryService.willSavePath(mockWindow, filePath)
|
||||
await recoveryService.willSavePath(mockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)
|
||||
|
||||
fs.writeFileSync(filePath, "changed")
|
||||
recoveryService.didSavePath(mockWindow, filePath)
|
||||
await recoveryService.didSavePath(mockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8'), "changed")
|
||||
|
||||
fs.removeSync(filePath)
|
||||
})
|
||||
|
||||
it("creates only one recovery file when many windows attempt to save the same file, deleting it when the last one finishes saving it", () => {
|
||||
it("creates only one recovery file when many windows attempt to save the same file, deleting it when the last one finishes saving it", async () => {
|
||||
const mockWindow = {}
|
||||
const anotherMockWindow = {}
|
||||
const filePath = temp.path()
|
||||
|
||||
fs.writeFileSync(filePath, "some content")
|
||||
recoveryService.willSavePath(mockWindow, filePath)
|
||||
recoveryService.willSavePath(anotherMockWindow, filePath)
|
||||
await recoveryService.willSavePath(mockWindow, filePath)
|
||||
await recoveryService.willSavePath(anotherMockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)
|
||||
|
||||
fs.writeFileSync(filePath, "changed")
|
||||
recoveryService.didSavePath(mockWindow, filePath)
|
||||
await recoveryService.didSavePath(mockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8'), "changed")
|
||||
|
||||
recoveryService.didSavePath(anotherMockWindow, filePath)
|
||||
await recoveryService.didSavePath(anotherMockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8'), "changed")
|
||||
|
||||
@@ -64,66 +64,66 @@ describe("FileRecoveryService", () => {
|
||||
})
|
||||
|
||||
describe("when a crash happens during a save", () => {
|
||||
it("restores the created recovery file and deletes it", () => {
|
||||
it("restores the created recovery file and deletes it", async () => {
|
||||
const mockWindow = {}
|
||||
const filePath = temp.path()
|
||||
|
||||
fs.writeFileSync(filePath, "some content")
|
||||
recoveryService.willSavePath(mockWindow, filePath)
|
||||
await recoveryService.willSavePath(mockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)
|
||||
|
||||
fs.writeFileSync(filePath, "changed")
|
||||
recoveryService.didCrashWindow(mockWindow)
|
||||
await recoveryService.didCrashWindow(mockWindow)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8'), "some content")
|
||||
|
||||
fs.removeSync(filePath)
|
||||
})
|
||||
|
||||
it("restores the created recovery file when many windows attempt to save the same file and one of them crashes", () => {
|
||||
it("restores the created recovery file when many windows attempt to save the same file and one of them crashes", async () => {
|
||||
const mockWindow = {}
|
||||
const anotherMockWindow = {}
|
||||
const filePath = temp.path()
|
||||
|
||||
fs.writeFileSync(filePath, "A")
|
||||
recoveryService.willSavePath(mockWindow, filePath)
|
||||
await recoveryService.willSavePath(mockWindow, filePath)
|
||||
fs.writeFileSync(filePath, "B")
|
||||
recoveryService.willSavePath(anotherMockWindow, filePath)
|
||||
await recoveryService.willSavePath(anotherMockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)
|
||||
|
||||
fs.writeFileSync(filePath, "C")
|
||||
|
||||
recoveryService.didCrashWindow(mockWindow)
|
||||
await recoveryService.didCrashWindow(mockWindow)
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8'), "A")
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
|
||||
fs.writeFileSync(filePath, "D")
|
||||
recoveryService.willSavePath(mockWindow, filePath)
|
||||
await recoveryService.willSavePath(mockWindow, filePath)
|
||||
fs.writeFileSync(filePath, "E")
|
||||
recoveryService.willSavePath(anotherMockWindow, filePath)
|
||||
await recoveryService.willSavePath(anotherMockWindow, filePath)
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 1)
|
||||
|
||||
fs.writeFileSync(filePath, "F")
|
||||
|
||||
recoveryService.didCrashWindow(anotherMockWindow)
|
||||
await recoveryService.didCrashWindow(anotherMockWindow)
|
||||
assert.equal(fs.readFileSync(filePath, 'utf8'), "D")
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
|
||||
fs.removeSync(filePath)
|
||||
})
|
||||
|
||||
it("emits a warning when a file can't be recovered", sinon.test(function () {
|
||||
it("emits a warning when a file can't be recovered", async () => {
|
||||
const mockWindow = {}
|
||||
const filePath = temp.path()
|
||||
fs.writeFileSync(filePath, "content")
|
||||
fs.chmodSync(filePath, 0444)
|
||||
|
||||
let logs = []
|
||||
this.stub(console, 'log', (message) => logs.push(message))
|
||||
this.stub(dialog, 'showMessageBox')
|
||||
spies.stub(console, 'log', (message) => logs.push(message))
|
||||
spies.stub(dialog, 'showMessageBox')
|
||||
|
||||
recoveryService.willSavePath(mockWindow, filePath)
|
||||
recoveryService.didCrashWindow(mockWindow)
|
||||
await recoveryService.willSavePath(mockWindow, filePath)
|
||||
await recoveryService.didCrashWindow(mockWindow)
|
||||
let recoveryFiles = fs.listTreeSync(recoveryDirectory)
|
||||
assert.equal(recoveryFiles.length, 1)
|
||||
assert.equal(logs.length, 1)
|
||||
@@ -131,16 +131,16 @@ describe("FileRecoveryService", () => {
|
||||
assert.match(logs[0], new RegExp(escapeRegExp(recoveryFiles[0])))
|
||||
|
||||
fs.removeSync(filePath)
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it("doesn't create a recovery file when the file that's being saved doesn't exist yet", () => {
|
||||
it("doesn't create a recovery file when the file that's being saved doesn't exist yet", async () => {
|
||||
const mockWindow = {}
|
||||
|
||||
recoveryService.willSavePath(mockWindow, "a-file-that-doesnt-exist")
|
||||
await recoveryService.willSavePath(mockWindow, "a-file-that-doesnt-exist")
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
|
||||
recoveryService.didSavePath(mockWindow, "a-file-that-doesnt-exist")
|
||||
await recoveryService.didSavePath(mockWindow, "a-file-that-doesnt-exist")
|
||||
assert.equal(fs.listTreeSync(recoveryDirectory).length, 0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
{
|
||||
expect(editor.getApproximateLongestScreenRow()).toBe(3)
|
||||
const expectedWidth = Math.round(
|
||||
const expectedWidth = Math.ceil(
|
||||
component.pixelPositionForScreenPosition(Point(3, Infinity)).left +
|
||||
component.getBaseCharacterWidth()
|
||||
)
|
||||
@@ -121,7 +121,7 @@ describe('TextEditorComponent', () => {
|
||||
// Capture the width of the lines before requesting the width of
|
||||
// longest line, because making that request forces a DOM update
|
||||
const actualWidth = element.querySelector('.lines').style.width
|
||||
const expectedWidth = Math.round(
|
||||
const expectedWidth = Math.ceil(
|
||||
component.pixelPositionForScreenPosition(Point(6, Infinity)).left +
|
||||
component.getBaseCharacterWidth()
|
||||
)
|
||||
@@ -2874,9 +2874,9 @@ describe('TextEditorComponent', () => {
|
||||
|
||||
describe('mouse input', () => {
|
||||
describe('on the lines', () => {
|
||||
describe('when there is only one cursor and no selection', () => {
|
||||
it('positions the cursor on single-click or when middle/right-clicking', async () => {
|
||||
for (const button of [0, 1, 2]) {
|
||||
describe('when there is only one cursor', () => {
|
||||
it('positions the cursor on single-click or when middle-clicking', async () => {
|
||||
for (const button of [0, 1]) {
|
||||
const {component, element, editor} = buildComponent()
|
||||
const {lineHeight} = component.measurements
|
||||
|
||||
@@ -2955,70 +2955,6 @@ describe('TextEditorComponent', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there is more than one cursor', () => {
|
||||
it('does not move the cursor when right-clicking', async () => {
|
||||
const {component, element, editor} = buildComponent()
|
||||
const {lineHeight} = component.measurements
|
||||
|
||||
editor.setCursorScreenPosition([5, 17], {autoscroll: false})
|
||||
editor.addCursorAtScreenPosition([2, 4])
|
||||
component.didMouseDownOnContent({
|
||||
detail: 1,
|
||||
button: 2,
|
||||
clientX: clientLeftForCharacter(component, 0, 0) - 1,
|
||||
clientY: clientTopForLine(component, 0) - 1
|
||||
})
|
||||
expect(editor.getCursorScreenPositions()).toEqual([Point.fromObject([5, 17]), Point.fromObject([2, 4])])
|
||||
})
|
||||
|
||||
it('does move the cursor when middle-clicking', async () => {
|
||||
const {component, element, editor} = buildComponent()
|
||||
const {lineHeight} = component.measurements
|
||||
|
||||
editor.setCursorScreenPosition([5, 17], {autoscroll: false})
|
||||
editor.addCursorAtScreenPosition([2, 4])
|
||||
component.didMouseDownOnContent({
|
||||
detail: 1,
|
||||
button: 1,
|
||||
clientX: clientLeftForCharacter(component, 0, 0) - 1,
|
||||
clientY: clientTopForLine(component, 0) - 1
|
||||
})
|
||||
expect(editor.getCursorScreenPositions()).toEqual([Point.fromObject([0, 0])])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there are non-empty selections', () => {
|
||||
it('does not move the cursor when right-clicking', async () => {
|
||||
const {component, element, editor} = buildComponent()
|
||||
const {lineHeight} = component.measurements
|
||||
|
||||
editor.setCursorScreenPosition([5, 17], {autoscroll: false})
|
||||
editor.selectRight(3)
|
||||
component.didMouseDownOnContent({
|
||||
detail: 1,
|
||||
button: 2,
|
||||
clientX: clientLeftForCharacter(component, 0, 0) - 1,
|
||||
clientY: clientTopForLine(component, 0) - 1
|
||||
})
|
||||
expect(editor.getSelectedScreenRange()).toEqual([[5, 17], [5, 20]])
|
||||
})
|
||||
|
||||
it('does move the cursor when middle-clicking', async () => {
|
||||
const {component, element, editor} = buildComponent()
|
||||
const {lineHeight} = component.measurements
|
||||
|
||||
editor.setCursorScreenPosition([5, 17], {autoscroll: false})
|
||||
editor.selectRight(3)
|
||||
component.didMouseDownOnContent({
|
||||
detail: 1,
|
||||
button: 1,
|
||||
clientX: clientLeftForCharacter(component, 0, 0) - 1,
|
||||
clientY: clientTopForLine(component, 0) - 1
|
||||
})
|
||||
expect(editor.getSelectedScreenRange()).toEqual([[0, 0], [0, 0]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the input is for the primary mouse button', () => {
|
||||
it('selects words on double-click', () => {
|
||||
const {component, editor} = buildComponent()
|
||||
@@ -3090,7 +3026,7 @@ describe('TextEditorComponent', () => {
|
||||
[[1, 16], [1, 16]]
|
||||
])
|
||||
|
||||
// ctrl-click does not add cursors on macOS, but it *does* move the cursor
|
||||
// ctrl-click does not add cursors on macOS, nor does it move the cursor
|
||||
component.didMouseDownOnContent(
|
||||
Object.assign(clientPositionForCharacter(component, 1, 4), {
|
||||
detail: 1,
|
||||
@@ -3099,7 +3035,7 @@ describe('TextEditorComponent', () => {
|
||||
})
|
||||
)
|
||||
expect(editor.getSelectedScreenRanges()).toEqual([
|
||||
[[1, 4], [1, 4]]
|
||||
[[1, 16], [1, 16]]
|
||||
])
|
||||
|
||||
// ctrl-click adds cursors on platforms *other* than macOS
|
||||
@@ -3408,6 +3344,31 @@ describe('TextEditorComponent', () => {
|
||||
})
|
||||
expect(editor.lineTextForBufferRow(10)).toBe('var')
|
||||
})
|
||||
|
||||
it('does not paste into a read only editor when clicking the middle mouse button on Linux', async () => {
|
||||
spyOn(electron.ipcRenderer, 'send').andCallFake(function (eventName, selectedText) {
|
||||
if (eventName === 'write-text-to-selection-clipboard') {
|
||||
clipboard.writeText(selectedText, 'selection')
|
||||
}
|
||||
})
|
||||
|
||||
const {component, editor} = buildComponent({platform: 'linux', readOnly: true})
|
||||
|
||||
// Select the word 'sort' on line 2 and copy to clipboard
|
||||
editor.setSelectedBufferRange([[1, 6], [1, 10]])
|
||||
await conditionPromise(() => TextEditor.clipboard.read() === 'sort')
|
||||
|
||||
// Middle-click in the buffer at line 11, column 1
|
||||
component.didMouseDownOnContent({
|
||||
button: 1,
|
||||
clientX: clientLeftForCharacter(component, 10, 0),
|
||||
clientY: clientTopForLine(component, 10)
|
||||
})
|
||||
|
||||
// Ensure that the correct text was copied but not pasted
|
||||
expect(TextEditor.clipboard.read()).toBe('sort')
|
||||
expect(editor.lineTextForBufferRow(10)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('on the line number gutter', () => {
|
||||
@@ -4014,7 +3975,7 @@ describe('TextEditorComponent', () => {
|
||||
// Capture the width of the lines before requesting the width of
|
||||
// longest line, because making that request forces a DOM update
|
||||
const actualWidth = element.querySelector('.lines').style.width
|
||||
const expectedWidth = Math.round(
|
||||
const expectedWidth = Math.ceil(
|
||||
component.pixelPositionForScreenPosition(Point(3, Infinity)).left +
|
||||
component.getBaseCharacterWidth()
|
||||
)
|
||||
@@ -4363,7 +4324,7 @@ describe('TextEditorComponent', () => {
|
||||
function buildEditor (params = {}) {
|
||||
const text = params.text != null ? params.text : SAMPLE_TEXT
|
||||
const buffer = new TextBuffer({text})
|
||||
const editorParams = {buffer}
|
||||
const editorParams = {buffer, readOnly: params.readOnly}
|
||||
if (params.height != null) params.autoHeight = false
|
||||
for (const paramName of ['mini', 'autoHeight', 'autoWidth', 'lineNumberGutterVisible', 'showLineNumbers', 'placeholderText', 'softWrapped', 'scrollSensitivity']) {
|
||||
if (params[paramName] != null) editorParams[paramName] = params[paramName]
|
||||
|
||||
@@ -138,6 +138,81 @@ describe('TreeSitterLanguageMode', () => {
|
||||
]
|
||||
])
|
||||
})
|
||||
|
||||
it('updates lines\' highlighting when they are affected by distant changes', () => {
|
||||
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
|
||||
parser: 'tree-sitter-javascript',
|
||||
scopes: {
|
||||
'call_expression > identifier': 'function',
|
||||
'property_identifier': 'member'
|
||||
}
|
||||
})
|
||||
|
||||
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
|
||||
|
||||
// missing closing paren
|
||||
buffer.setText('a(\nb,\nc\n')
|
||||
expectTokensToEqual(editor, [
|
||||
[{text: 'a(', scopes: []}],
|
||||
[{text: 'b,', scopes: []}],
|
||||
[{text: 'c', scopes: []}],
|
||||
[{text: '', scopes: []}]
|
||||
])
|
||||
|
||||
buffer.append(')')
|
||||
expectTokensToEqual(editor, [
|
||||
[
|
||||
{text: 'a', scopes: ['function']},
|
||||
{text: '(', scopes: []}
|
||||
],
|
||||
[{text: 'b,', scopes: []}],
|
||||
[{text: 'c', scopes: []}],
|
||||
[{text: ')', scopes: []}]
|
||||
])
|
||||
})
|
||||
|
||||
it('handles edits after tokens that end between CR and LF characters (regression)', () => {
|
||||
const grammar = new TreeSitterGrammar(atom.grammars, jsGrammarPath, {
|
||||
parser: 'tree-sitter-javascript',
|
||||
scopes: {
|
||||
'comment': 'comment',
|
||||
'string': 'string',
|
||||
'property_identifier': 'property',
|
||||
}
|
||||
})
|
||||
|
||||
buffer.setLanguageMode(new TreeSitterLanguageMode({buffer, grammar}))
|
||||
|
||||
buffer.setText([
|
||||
'// abc',
|
||||
'',
|
||||
'a("b").c'
|
||||
].join('\r\n'))
|
||||
|
||||
expectTokensToEqual(editor, [
|
||||
[{text: '// abc', scopes: ['comment']}],
|
||||
[{text: '', scopes: []}],
|
||||
[
|
||||
{text: 'a(', scopes: []},
|
||||
{text: '"b"', scopes: ['string']},
|
||||
{text: ').', scopes: []},
|
||||
{text: 'c', scopes: ['property']}
|
||||
]
|
||||
])
|
||||
|
||||
buffer.insert([2, 0], ' ')
|
||||
expectTokensToEqual(editor, [
|
||||
[{text: '// abc', scopes: ['comment']}],
|
||||
[{text: '', scopes: []}],
|
||||
[
|
||||
{text: ' ', scopes: ['whitespace']},
|
||||
{text: 'a(', scopes: []},
|
||||
{text: '"b"', scopes: ['string']},
|
||||
{text: ').', scopes: []},
|
||||
{text: 'c', scopes: ['property']}
|
||||
]
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('folding', () => {
|
||||
@@ -533,7 +608,14 @@ function expectTokensToEqual (editor, expectedTokenLines) {
|
||||
// Assert that the correct tokens are returned regardless of which row
|
||||
// the highlighting iterator starts on.
|
||||
for (let startRow = 0; startRow <= lastRow; startRow++) {
|
||||
editor.displayLayer.clearSpatialIndex()
|
||||
|
||||
// Clear the screen line cache between iterations, but not on the first
|
||||
// iteration, so that the first iteration tests that the cache has been
|
||||
// correctly invalidated by any changes.
|
||||
if (startRow > 0) {
|
||||
editor.displayLayer.clearSpatialIndex()
|
||||
}
|
||||
|
||||
editor.displayLayer.getScreenLines(startRow, Infinity)
|
||||
|
||||
const tokenLines = []
|
||||
@@ -557,4 +639,8 @@ function expectTokensToEqual (editor, expectedTokenLines) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fully populate the screen line cache again so that cache invalidation
|
||||
// due to subsequent edits can be tested.
|
||||
editor.displayLayer.getScreenLines(0, Infinity)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ const getWindowLoadSettings = require('./get-window-load-settings')
|
||||
|
||||
module.exports =
|
||||
class ApplicationDelegate {
|
||||
constructor () {
|
||||
this.pendingSettingsUpdateCount = 0
|
||||
}
|
||||
|
||||
getWindowLoadSettings () { return getWindowLoadSettings() }
|
||||
|
||||
open (params) {
|
||||
@@ -175,6 +179,33 @@ class ApplicationDelegate {
|
||||
return remote.systemPreferences.getUserDefault(key, type)
|
||||
}
|
||||
|
||||
async setUserSettings (config) {
|
||||
this.pendingSettingsUpdateCount++
|
||||
try {
|
||||
await ipcHelpers.call('set-user-settings', config)
|
||||
} finally {
|
||||
this.pendingSettingsUpdateCount--
|
||||
}
|
||||
}
|
||||
|
||||
onDidChangeUserSettings (callback) {
|
||||
const outerCallback = (event, message, detail) => {
|
||||
if (message === 'did-change-user-settings') {
|
||||
if (this.pendingSettingsUpdateCount === 0) callback(detail)
|
||||
}
|
||||
}
|
||||
ipcRenderer.on('message', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
|
||||
}
|
||||
|
||||
onDidFailToReadUserSettings (callback) {
|
||||
const outerCallback = (event, message, detail) => {
|
||||
if (message === 'did-fail-to-read-user-settings') callback(detail)
|
||||
}
|
||||
ipcRenderer.on('message', outerCallback)
|
||||
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
|
||||
}
|
||||
|
||||
confirm (options, callback) {
|
||||
if (typeof callback === 'function') {
|
||||
// Async version: pass options directly to Electron but set sane defaults
|
||||
@@ -354,11 +385,11 @@ class ApplicationDelegate {
|
||||
}
|
||||
|
||||
emitWillSavePath (path) {
|
||||
return ipcRenderer.sendSync('will-save-path', path)
|
||||
return ipcHelpers.call('will-save-path', path)
|
||||
}
|
||||
|
||||
emitDidSavePath (path) {
|
||||
return ipcRenderer.sendSync('did-save-path', path)
|
||||
return ipcHelpers.call('did-save-path', path)
|
||||
}
|
||||
|
||||
resolveProxy (requestId, url) {
|
||||
|
||||
@@ -9,7 +9,6 @@ const fs = require('fs-plus')
|
||||
const {mapSourcePosition} = require('@atom/source-map-support')
|
||||
const WindowEventHandler = require('./window-event-handler')
|
||||
const StateStore = require('./state-store')
|
||||
const StorageFolder = require('./storage-folder')
|
||||
const registerDefaultCommands = require('./register-default-commands')
|
||||
const {updateProcessEnv} = require('./update-process-env')
|
||||
const ConfigSchema = require('./config-schema')
|
||||
@@ -86,8 +85,11 @@ class AtomEnvironment {
|
||||
|
||||
// Public: A {Config} instance
|
||||
this.config = new Config({
|
||||
notificationManager: this.notifications,
|
||||
enablePersistence: this.enablePersistence
|
||||
saveCallback: settings => {
|
||||
if (this.enablePersistence) {
|
||||
this.applicationDelegate.setUserSettings(settings)
|
||||
}
|
||||
}
|
||||
})
|
||||
this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)})
|
||||
|
||||
@@ -208,19 +210,19 @@ class AtomEnvironment {
|
||||
this.blobStore = params.blobStore
|
||||
this.configDirPath = params.configDirPath
|
||||
|
||||
const {devMode, safeMode, resourcePath, clearWindowState} = this.getLoadSettings()
|
||||
|
||||
if (clearWindowState) {
|
||||
this.getStorageFolder().clear()
|
||||
this.stateStore.clear()
|
||||
}
|
||||
const {devMode, safeMode, resourcePath, userSettings} = this.getLoadSettings()
|
||||
|
||||
ConfigSchema.projectHome = {
|
||||
type: 'string',
|
||||
default: path.join(fs.getHomeDirectory(), 'github'),
|
||||
description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.'
|
||||
}
|
||||
this.config.initialize({configDirPath: this.configDirPath, resourcePath, projectHomeSchema: ConfigSchema.projectHome})
|
||||
|
||||
this.config.initialize({
|
||||
mainSource: this.enablePersistence && path.join(this.configDirPath, 'config.cson'),
|
||||
projectHomeSchema: ConfigSchema.projectHome
|
||||
})
|
||||
this.config.resetUserSettings(userSettings)
|
||||
|
||||
this.menu.initialize({resourcePath})
|
||||
this.contextMenu.initialize({resourcePath, devMode})
|
||||
@@ -242,8 +244,6 @@ class AtomEnvironment {
|
||||
this.uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this))
|
||||
this.autoUpdater.initialize()
|
||||
|
||||
this.config.load()
|
||||
|
||||
this.protocolHandlerInstaller.initialize(this.config, this.notifications)
|
||||
|
||||
this.themes.loadBaseStylesheets()
|
||||
@@ -374,7 +374,6 @@ class AtomEnvironment {
|
||||
this.project = null
|
||||
this.commands.clear()
|
||||
if (this.stylesElement) this.stylesElement.remove()
|
||||
this.config.unobserveUserConfig()
|
||||
this.autoUpdater.destroy()
|
||||
this.uriHandlerRegistry.destroy()
|
||||
|
||||
@@ -764,7 +763,11 @@ class AtomEnvironment {
|
||||
}
|
||||
|
||||
// Call this method when establishing a real application window.
|
||||
startEditorWindow () {
|
||||
async startEditorWindow () {
|
||||
if (this.getLoadSettings().clearWindowState) {
|
||||
await this.stateStore.clear()
|
||||
}
|
||||
|
||||
this.unloaded = false
|
||||
|
||||
const updateProcessEnvPromise = this.updateProcessEnvAndTriggerHooks()
|
||||
@@ -779,6 +782,12 @@ class AtomEnvironment {
|
||||
if (error) console.warn(error.message)
|
||||
})
|
||||
|
||||
this.disposables.add(this.applicationDelegate.onDidChangeUserSettings(settings =>
|
||||
this.config.resetUserSettings(settings)
|
||||
))
|
||||
this.disposables.add(this.applicationDelegate.onDidFailToReadUserSettings(message =>
|
||||
this.notifications.addError(message)
|
||||
))
|
||||
this.disposables.add(this.applicationDelegate.onDidOpenLocations(this.openLocations.bind(this)))
|
||||
this.disposables.add(this.applicationDelegate.onApplicationMenuCommand(this.dispatchApplicationMenuCommand.bind(this)))
|
||||
this.disposables.add(this.applicationDelegate.onContextMenuCommand(this.dispatchContextMenuCommand.bind(this)))
|
||||
@@ -1264,11 +1273,6 @@ or use Pane::saveItemAs for programmatic saving.`)
|
||||
}
|
||||
}
|
||||
|
||||
getStorageFolder () {
|
||||
if (!this.storageFolder) this.storageFolder = new StorageFolder(this.getConfigDirPath())
|
||||
return this.storageFolder
|
||||
}
|
||||
|
||||
getConfigDirPath () {
|
||||
if (!this.configDirPath) this.configDirPath = process.env.ATOM_HOME
|
||||
return this.configDirPath
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/** @babel */
|
||||
|
||||
const fs = require('fs-plus')
|
||||
const path = require('path')
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use babel'
|
||||
const {Emitter, CompositeDisposable} = require('event-kit')
|
||||
|
||||
import {Emitter, CompositeDisposable} from 'event-kit'
|
||||
|
||||
export default class AutoUpdateManager {
|
||||
module.exports =
|
||||
class AutoUpdateManager {
|
||||
constructor ({applicationDelegate}) {
|
||||
this.applicationDelegate = applicationDelegate
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/** @babel */
|
||||
|
||||
import BufferedProcess from './buffered-process'
|
||||
const BufferedProcess = require('./buffered-process')
|
||||
|
||||
// Extended: Like {BufferedProcess}, but accepts a Node script as the command
|
||||
// to run.
|
||||
@@ -12,7 +10,8 @@ import BufferedProcess from './buffered-process'
|
||||
// ```js
|
||||
// const {BufferedNodeProcess} = require('atom')
|
||||
// ```
|
||||
export default class BufferedNodeProcess extends BufferedProcess {
|
||||
module.exports =
|
||||
class BufferedNodeProcess extends BufferedProcess {
|
||||
|
||||
// Public: Runs the given Node script by spawning a new child process.
|
||||
//
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/** @babel */
|
||||
|
||||
import _ from 'underscore-plus'
|
||||
import ChildProcess from 'child_process'
|
||||
import {Emitter} from 'event-kit'
|
||||
import path from 'path'
|
||||
const _ = require('underscore-plus')
|
||||
const ChildProcess = require('child_process')
|
||||
const {Emitter} = require('event-kit')
|
||||
const path = require('path')
|
||||
|
||||
// Extended: A wrapper which provides standard error/output line buffering for
|
||||
// Node's ChildProcess.
|
||||
@@ -19,7 +17,8 @@ import path from 'path'
|
||||
// const exit = (code) => console.log("ps -ef exited with #{code}")
|
||||
// const process = new BufferedProcess({command, args, stdout, exit})
|
||||
// ```
|
||||
export default class BufferedProcess {
|
||||
module.exports =
|
||||
class BufferedProcess {
|
||||
/*
|
||||
Section: Construction
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/** @babel */
|
||||
|
||||
import crypto from 'crypto'
|
||||
import clipboard from './safe-clipboard'
|
||||
const crypto = require('crypto')
|
||||
const clipboard = require('./safe-clipboard')
|
||||
|
||||
// Extended: Represents the clipboard used for copying and pasting in Atom.
|
||||
//
|
||||
@@ -14,7 +12,8 @@ import clipboard from './safe-clipboard'
|
||||
//
|
||||
// console.log(atom.clipboard.read()) # 'hello'
|
||||
// ```
|
||||
export default class Clipboard {
|
||||
module.exports =
|
||||
class Clipboard {
|
||||
constructor () {
|
||||
this.reset()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/** @babel */
|
||||
|
||||
let ParsedColor = null
|
||||
|
||||
// Essential: A simple color class returned from {Config::get} when the value
|
||||
// at the key path is of type 'color'.
|
||||
export default class Color {
|
||||
module.exports =
|
||||
class Color {
|
||||
// Essential: Parse a {String} or {Object} into a {Color}.
|
||||
//
|
||||
// * `value` A {String} such as `'white'`, `#ff00ff`, or
|
||||
|
||||
103
src/config-file.js
Normal file
103
src/config-file.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const _ = require('underscore-plus')
|
||||
const fs = require('fs-plus')
|
||||
const dedent = require('dedent')
|
||||
const {Emitter} = require('event-kit')
|
||||
const {watchPath} = require('./path-watcher')
|
||||
const CSON = require('season')
|
||||
const Path = require('path')
|
||||
const async = require('async')
|
||||
|
||||
const EVENT_TYPES = new Set([
|
||||
'created',
|
||||
'modified',
|
||||
'renamed'
|
||||
])
|
||||
|
||||
module.exports =
|
||||
class ConfigFile {
|
||||
constructor (path) {
|
||||
this.path = path
|
||||
this.emitter = new Emitter()
|
||||
this.value = {}
|
||||
this.reloadCallbacks = []
|
||||
|
||||
// Use a queue to prevent multiple concurrent write to the same file.
|
||||
const writeQueue = async.queue((data, callback) =>
|
||||
CSON.writeFile(this.path, data, error => {
|
||||
if (error) {
|
||||
this.emitter.emit('did-error', dedent `
|
||||
Failed to write \`${Path.basename(this.path)}\`.
|
||||
|
||||
${error.message}
|
||||
`)
|
||||
}
|
||||
callback()
|
||||
})
|
||||
)
|
||||
|
||||
this.requestLoad = _.debounce(() => this.reload(), 200)
|
||||
this.requestSave = _.debounce((data) => writeQueue.push(data), 200)
|
||||
}
|
||||
|
||||
get () {
|
||||
return this.value
|
||||
}
|
||||
|
||||
update (value) {
|
||||
return new Promise(resolve => {
|
||||
this.requestSave(value)
|
||||
this.reloadCallbacks.push(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
async watch (callback) {
|
||||
if (!fs.existsSync(this.path)) {
|
||||
fs.makeTreeSync(Path.dirname(this.path))
|
||||
CSON.writeFileSync(this.path, {}, {flag: 'wx'})
|
||||
}
|
||||
|
||||
await this.reload()
|
||||
|
||||
try {
|
||||
const watcher = await watchPath(this.path, {}, events => {
|
||||
if (events.some(event => EVENT_TYPES.has(event.action))) this.requestLoad()
|
||||
})
|
||||
return watcher
|
||||
} catch (error) {
|
||||
this.emitter.emit('did-error', dedent `
|
||||
Unable to watch path: \`${Path.basename(this.path)}\`.
|
||||
|
||||
Make sure you have permissions to \`${this.path}\`.
|
||||
On linux there are currently problems with watch sizes.
|
||||
See [this document][watches] for more info.
|
||||
|
||||
[watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path\
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
onDidChange (callback) {
|
||||
return this.emitter.on('did-change', callback)
|
||||
}
|
||||
|
||||
onDidError (callback) {
|
||||
return this.emitter.on('did-error', callback)
|
||||
}
|
||||
|
||||
reload () {
|
||||
return new Promise(resolve => {
|
||||
CSON.readFile(this.path, (error, data) => {
|
||||
if (error) {
|
||||
this.emitter.emit('did-error', `Failed to load \`${Path.basename(this.path)}\` - ${error.message}`)
|
||||
} else {
|
||||
this.value = data || {}
|
||||
this.emitter.emit('did-change', this.value)
|
||||
|
||||
for (const callback of this.reloadCallbacks) callback()
|
||||
this.reloadCallbacks.length = 0
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -337,6 +337,14 @@ const configSchema = {
|
||||
value: 'native',
|
||||
description: 'Native operating system APIs'
|
||||
},
|
||||
{
|
||||
value: 'experimental',
|
||||
description: 'Experimental filesystem watching library'
|
||||
},
|
||||
{
|
||||
value: 'poll',
|
||||
description: 'Polling'
|
||||
},
|
||||
{
|
||||
value: 'atom',
|
||||
description: 'Emulated with Atom events'
|
||||
@@ -346,7 +354,7 @@ const configSchema = {
|
||||
useTreeSitterParsers: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Use the new Tree-sitter parsing system for supported languages'
|
||||
description: 'Experimental: Use the new Tree-sitter parsing system for supported languages.'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
1353
src/config.coffee
1353
src/config.coffee
File diff suppressed because it is too large
Load Diff
1446
src/config.js
Normal file
1446
src/config.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -524,7 +524,7 @@ class Cursor extends Model {
|
||||
: new Range(new Point(position.row, 0), position)
|
||||
|
||||
const ranges = this.editor.buffer.findAllInRangeSync(
|
||||
options.wordRegex || this.wordRegExp(),
|
||||
options.wordRegex || this.wordRegExp(options),
|
||||
scanRange
|
||||
)
|
||||
|
||||
@@ -556,7 +556,7 @@ class Cursor extends Model {
|
||||
: new Range(position, new Point(position.row, Infinity))
|
||||
|
||||
const ranges = this.editor.buffer.findAllInRangeSync(
|
||||
options.wordRegex || this.wordRegExp(),
|
||||
options.wordRegex || this.wordRegExp(options),
|
||||
scanRange
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/** @babel */
|
||||
|
||||
import {Disposable} from 'event-kit'
|
||||
const {Disposable} = require('event-kit')
|
||||
|
||||
// Extended: Manages the deserializers used for serialized state
|
||||
//
|
||||
@@ -21,7 +19,8 @@ import {Disposable} from 'event-kit'
|
||||
// serialize: ->
|
||||
// @state
|
||||
// ```
|
||||
export default class DeserializerManager {
|
||||
module.exports =
|
||||
class DeserializerManager {
|
||||
constructor (atomEnvironment) {
|
||||
this.atomEnvironment = atomEnvironment
|
||||
this.deserializers = {}
|
||||
|
||||
@@ -429,7 +429,7 @@ class GrammarRegistry {
|
||||
this.readGrammar(grammarPath, (error, grammar) => {
|
||||
if (error) return callback(error)
|
||||
this.addGrammar(grammar)
|
||||
callback(grammar)
|
||||
callback(null, grammar)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
/** @babel */
|
||||
|
||||
import {Emitter, CompositeDisposable} from 'event-kit'
|
||||
const {Emitter, CompositeDisposable} = require('event-kit')
|
||||
|
||||
// Extended: History manager for remembering which projects have been opened.
|
||||
//
|
||||
// An instance of this class is always available as the `atom.history` global.
|
||||
//
|
||||
// The project history is used to enable the 'Reopen Project' menu.
|
||||
export class HistoryManager {
|
||||
class HistoryManager {
|
||||
constructor ({project, commands, stateStore}) {
|
||||
this.stateStore = stateStore
|
||||
this.emitter = new Emitter()
|
||||
@@ -116,7 +114,7 @@ function arrayEquivalent (a, b) {
|
||||
return true
|
||||
}
|
||||
|
||||
export class HistoryProject {
|
||||
class HistoryProject {
|
||||
constructor (paths, lastOpened) {
|
||||
this.paths = paths
|
||||
this.lastOpened = lastOpened || new Date()
|
||||
@@ -128,3 +126,5 @@ export class HistoryProject {
|
||||
set lastOpened (lastOpened) { this._lastOpened = lastOpened }
|
||||
get lastOpened () { return this._lastOpened }
|
||||
}
|
||||
|
||||
module.exports = {HistoryManager, HistoryProject}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
/** @babel */
|
||||
const {remote} = require('electron')
|
||||
const path = require('path')
|
||||
const ipcHelpers = require('./ipc-helpers')
|
||||
const util = require('util')
|
||||
|
||||
import {remote} from 'electron'
|
||||
import path from 'path'
|
||||
import ipcHelpers from './ipc-helpers'
|
||||
import util from 'util'
|
||||
|
||||
export default async function () {
|
||||
module.exports = async function () {
|
||||
const getWindowLoadSettings = require('./get-window-load-settings')
|
||||
const {test, headless, resourcePath, benchmarkPaths} = getWindowLoadSettings()
|
||||
try {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
'use strict'
|
||||
|
||||
const Disposable = require('event-kit').Disposable
|
||||
let ipcRenderer = null
|
||||
let ipcMain = null
|
||||
let BrowserWindow = null
|
||||
|
||||
let nextResponseChannelId = 0
|
||||
|
||||
exports.on = function (emitter, eventName, callback) {
|
||||
emitter.on(eventName, callback)
|
||||
return new Disposable(function () {
|
||||
emitter.removeListener(eventName, callback)
|
||||
})
|
||||
return new Disposable(() => emitter.removeListener(eventName, callback))
|
||||
}
|
||||
|
||||
exports.call = function (channel, ...args) {
|
||||
@@ -18,34 +16,28 @@ exports.call = function (channel, ...args) {
|
||||
ipcRenderer.setMaxListeners(20)
|
||||
}
|
||||
|
||||
var responseChannel = getResponseChannel(channel)
|
||||
const responseChannel = `ipc-helpers-response-${nextResponseChannelId++}`
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
ipcRenderer.on(responseChannel, function (event, result) {
|
||||
return new Promise(resolve => {
|
||||
ipcRenderer.on(responseChannel, (event, result) => {
|
||||
ipcRenderer.removeAllListeners(responseChannel)
|
||||
resolve(result)
|
||||
})
|
||||
|
||||
ipcRenderer.send(channel, ...args)
|
||||
ipcRenderer.send(channel, responseChannel, ...args)
|
||||
})
|
||||
}
|
||||
|
||||
exports.respondTo = function (channel, callback) {
|
||||
if (!ipcMain) {
|
||||
var electron = require('electron')
|
||||
const electron = require('electron')
|
||||
ipcMain = electron.ipcMain
|
||||
BrowserWindow = electron.BrowserWindow
|
||||
}
|
||||
|
||||
var responseChannel = getResponseChannel(channel)
|
||||
|
||||
return exports.on(ipcMain, channel, function (event, ...args) {
|
||||
var browserWindow = BrowserWindow.fromWebContents(event.sender)
|
||||
var result = callback(browserWindow, ...args)
|
||||
return exports.on(ipcMain, channel, async (event, responseChannel, ...args) => {
|
||||
const browserWindow = BrowserWindow.fromWebContents(event.sender)
|
||||
const result = await callback(browserWindow, ...args)
|
||||
event.sender.send(responseChannel, result)
|
||||
})
|
||||
}
|
||||
|
||||
function getResponseChannel (channel) {
|
||||
return 'ipc-helpers-' + channel + '-response'
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
module.exports =
|
||||
class ItemRegistry
|
||||
constructor: ->
|
||||
@items = new WeakSet
|
||||
|
||||
addItem: (item) ->
|
||||
if @hasItem(item)
|
||||
throw new Error("The workspace can only contain one instance of item #{item}")
|
||||
@items.add(item)
|
||||
|
||||
removeItem: (item) ->
|
||||
@items.delete(item)
|
||||
|
||||
hasItem: (item) ->
|
||||
@items.has(item)
|
||||
21
src/item-registry.js
Normal file
21
src/item-registry.js
Normal file
@@ -0,0 +1,21 @@
|
||||
module.exports =
|
||||
class ItemRegistry {
|
||||
constructor () {
|
||||
this.items = new WeakSet()
|
||||
}
|
||||
|
||||
addItem (item) {
|
||||
if (this.hasItem(item)) {
|
||||
throw new Error(`The workspace can only contain one instance of item ${item}`)
|
||||
}
|
||||
return this.items.add(item)
|
||||
}
|
||||
|
||||
removeItem (item) {
|
||||
return this.items.delete(item)
|
||||
}
|
||||
|
||||
hasItem (item) {
|
||||
return this.items.has(item)
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ const AtomProtocolHandler = require('./atom-protocol-handler')
|
||||
const AutoUpdateManager = require('./auto-update-manager')
|
||||
const StorageFolder = require('../storage-folder')
|
||||
const Config = require('../config')
|
||||
const ConfigFile = require('../config-file')
|
||||
const FileRecoveryService = require('./file-recovery-service')
|
||||
const ipcHelpers = require('../ipc-helpers')
|
||||
const {BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require('electron')
|
||||
@@ -107,20 +108,17 @@ class AtomApplication extends EventEmitter {
|
||||
this.waitSessionsByWindow = new Map()
|
||||
this.windowStack = new WindowStack()
|
||||
|
||||
this.config = new Config({enablePersistence: true})
|
||||
this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)})
|
||||
ConfigSchema.projectHome = {
|
||||
type: 'string',
|
||||
default: path.join(fs.getHomeDirectory(), 'github'),
|
||||
description:
|
||||
'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.'
|
||||
}
|
||||
this.config.initialize({
|
||||
configDirPath: process.env.ATOM_HOME,
|
||||
resourcePath: this.resourcePath,
|
||||
projectHomeSchema: ConfigSchema.projectHome
|
||||
this.initializeAtomHome(process.env.ATOM_HOME)
|
||||
|
||||
const configFilePath = fs.existsSync(path.join(process.env.ATOM_HOME, 'config.json'))
|
||||
? path.join(process.env.ATOM_HOME, 'config.json')
|
||||
: path.join(process.env.ATOM_HOME, 'config.cson')
|
||||
|
||||
this.configFile = new ConfigFile(configFilePath)
|
||||
this.config = new Config({
|
||||
saveCallback: settings => this.configFile.update(settings)
|
||||
})
|
||||
this.config.load()
|
||||
this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)})
|
||||
|
||||
this.fileRecoveryService = new FileRecoveryService(path.join(process.env.ATOM_HOME, 'recovery'))
|
||||
this.storageFolder = new StorageFolder(process.env.ATOM_HOME)
|
||||
@@ -148,8 +146,6 @@ class AtomApplication extends EventEmitter {
|
||||
this.config.set('core.titleBar', 'custom')
|
||||
}
|
||||
|
||||
this.config.onDidChange('core.titleBar', this.promptForRestart.bind(this))
|
||||
|
||||
process.nextTick(() => this.autoUpdateManager.initialize())
|
||||
this.applicationMenu = new ApplicationMenu(this.version, this.autoUpdateManager)
|
||||
this.atomProtocolHandler = new AtomProtocolHandler(this.resourcePath, this.safeMode)
|
||||
@@ -169,18 +165,38 @@ class AtomApplication extends EventEmitter {
|
||||
this.disposable.dispose()
|
||||
}
|
||||
|
||||
launch (options) {
|
||||
async launch (options) {
|
||||
if (!this.configFilePromise) {
|
||||
this.configFilePromise = this.configFile.watch()
|
||||
this.disposable.add(await this.configFilePromise)
|
||||
this.config.onDidChange('core.titleBar', this.promptForRestart.bind(this))
|
||||
}
|
||||
|
||||
const optionsForWindowsToOpen = []
|
||||
|
||||
let shouldReopenPreviousWindows = false
|
||||
|
||||
if (options.test || options.benchmark || options.benchmarkTest) {
|
||||
return this.openWithOptions(options)
|
||||
optionsForWindowsToOpen.push(options)
|
||||
} else if ((options.pathsToOpen && options.pathsToOpen.length > 0) ||
|
||||
(options.urlsToOpen && options.urlsToOpen.length > 0)) {
|
||||
if (this.config.get('core.restorePreviousWindowsOnStart') === 'always') {
|
||||
this.loadState(_.deepClone(options))
|
||||
}
|
||||
return this.openWithOptions(options)
|
||||
optionsForWindowsToOpen.push(options)
|
||||
shouldReopenPreviousWindows = this.config.get('core.restorePreviousWindowsOnStart') === 'always'
|
||||
} else {
|
||||
return this.loadState(options) || this.openPath(options)
|
||||
shouldReopenPreviousWindows = this.config.get('core.restorePreviousWindowsOnStart') !== 'no'
|
||||
}
|
||||
|
||||
if (shouldReopenPreviousWindows) {
|
||||
for (const previousOptions of await this.loadPreviousWindowOptions()) {
|
||||
optionsForWindowsToOpen.push(Object.assign({}, options, previousOptions))
|
||||
}
|
||||
}
|
||||
|
||||
if (optionsForWindowsToOpen.length === 0) {
|
||||
optionsForWindowsToOpen.push(options)
|
||||
}
|
||||
|
||||
return optionsForWindowsToOpen.map(options => this.openWithOptions(options))
|
||||
}
|
||||
|
||||
openWithOptions (options) {
|
||||
@@ -271,7 +287,7 @@ class AtomApplication extends EventEmitter {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!window.isSpec) this.saveState(true)
|
||||
if (!window.isSpec) this.saveCurrentWindowOptions(true)
|
||||
}
|
||||
|
||||
// Public: Adds the {AtomWindow} to the global window list.
|
||||
@@ -285,7 +301,7 @@ class AtomApplication extends EventEmitter {
|
||||
|
||||
if (!window.isSpec) {
|
||||
const focusHandler = () => this.windowStack.touch(window)
|
||||
const blurHandler = () => this.saveState(false)
|
||||
const blurHandler = () => this.saveCurrentWindowOptions(false)
|
||||
window.browserWindow.on('focus', focusHandler)
|
||||
window.browserWindow.on('blur', blurHandler)
|
||||
window.browserWindow.once('closed', () => {
|
||||
@@ -397,6 +413,18 @@ class AtomApplication extends EventEmitter {
|
||||
this.openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet')
|
||||
this.openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
|
||||
|
||||
this.configFile.onDidChange(settings => {
|
||||
for (let window of this.getAllWindows()) {
|
||||
window.didChangeUserSettings(settings)
|
||||
}
|
||||
this.config.resetUserSettings(settings)
|
||||
})
|
||||
|
||||
this.configFile.onDidError(message => {
|
||||
const window = this.focusedWindow() || this.getLastFocusedWindow()
|
||||
if (window) window.didFailToReadUserSettings(message)
|
||||
})
|
||||
|
||||
this.disposable.add(ipcHelpers.on(app, 'before-quit', async event => {
|
||||
let resolveBeforeQuitPromise
|
||||
this.lastBeforeQuitPromise = new Promise(resolve => { resolveBeforeQuitPromise = resolve })
|
||||
@@ -530,6 +558,10 @@ class AtomApplication extends EventEmitter {
|
||||
window.setPosition(x, y)
|
||||
}))
|
||||
|
||||
this.disposable.add(ipcHelpers.respondTo('set-user-settings', (window, settings) =>
|
||||
this.configFile.update(settings)
|
||||
))
|
||||
|
||||
this.disposable.add(ipcHelpers.respondTo('center-window', window => window.center()))
|
||||
this.disposable.add(ipcHelpers.respondTo('focus-window', window => window.focus()))
|
||||
this.disposable.add(ipcHelpers.respondTo('show-window', window => window.show()))
|
||||
@@ -569,18 +601,16 @@ class AtomApplication extends EventEmitter {
|
||||
event.returnValue = this.autoUpdateManager.getErrorMessage()
|
||||
}))
|
||||
|
||||
this.disposable.add(ipcHelpers.on(ipcMain, 'will-save-path', (event, path) => {
|
||||
this.fileRecoveryService.willSavePath(this.atomWindowForEvent(event), path)
|
||||
event.returnValue = true
|
||||
}))
|
||||
this.disposable.add(ipcHelpers.respondTo('will-save-path', (window, path) =>
|
||||
this.fileRecoveryService.willSavePath(window, path)
|
||||
))
|
||||
|
||||
this.disposable.add(ipcHelpers.on(ipcMain, 'did-save-path', (event, path) => {
|
||||
this.fileRecoveryService.didSavePath(this.atomWindowForEvent(event), path)
|
||||
event.returnValue = true
|
||||
}))
|
||||
this.disposable.add(ipcHelpers.respondTo('did-save-path', (window, path) =>
|
||||
this.fileRecoveryService.didSavePath(window, path)
|
||||
))
|
||||
|
||||
this.disposable.add(ipcHelpers.on(ipcMain, 'did-change-paths', () =>
|
||||
this.saveState(false)
|
||||
this.saveCurrentWindowOptions(false)
|
||||
))
|
||||
|
||||
this.disposable.add(this.disableZoomOnDisplayChange())
|
||||
@@ -594,6 +624,13 @@ class AtomApplication extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
initializeAtomHome (configDirPath) {
|
||||
if (!fs.existsSync(configDirPath)) {
|
||||
const templateConfigDirPath = fs.resolve(this.resourcePath, 'dot-atom')
|
||||
fs.copySync(templateConfigDirPath, configDirPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Executes the given command.
|
||||
//
|
||||
// If it isn't handled globally, delegate to the currently focused window.
|
||||
@@ -801,13 +838,12 @@ class AtomApplication extends EventEmitter {
|
||||
let existingWindow
|
||||
if (!newWindow) {
|
||||
existingWindow = this.windowForPaths(pathsToOpen, devMode)
|
||||
const stats = pathsToOpen.map(pathToOpen => fs.statSyncNoException(pathToOpen))
|
||||
if (!existingWindow) {
|
||||
let lastWindow = window || this.getLastFocusedWindow()
|
||||
if (lastWindow && lastWindow.devMode === devMode) {
|
||||
if (addToLastWindow || (
|
||||
stats.every(s => s.isFile && s.isFile()) ||
|
||||
(stats.some(s => s.isDirectory && s.isDirectory()) && !lastWindow.hasProjectPath()))) {
|
||||
locationsToOpen.every(({stat}) => stat && stat.isFile()) ||
|
||||
(locationsToOpen.some(({stat}) => stat && stat.isDirectory()) && !lastWindow.hasProjectPath()))) {
|
||||
existingWindow = lastWindow
|
||||
}
|
||||
}
|
||||
@@ -911,7 +947,7 @@ class AtomApplication extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
saveState (allowEmpty = false) {
|
||||
async saveCurrentWindowOptions (allowEmpty = false) {
|
||||
if (this.quitting) return
|
||||
|
||||
const states = []
|
||||
@@ -921,28 +957,23 @@ class AtomApplication extends EventEmitter {
|
||||
states.reverse()
|
||||
|
||||
if (states.length > 0 || allowEmpty) {
|
||||
this.storageFolder.storeSync('application.json', states)
|
||||
await this.storageFolder.store('application.json', states)
|
||||
this.emit('application:did-save-state')
|
||||
}
|
||||
}
|
||||
|
||||
loadState (options) {
|
||||
const states = this.storageFolder.load('application.json')
|
||||
if (
|
||||
['yes', 'always'].includes(this.config.get('core.restorePreviousWindowsOnStart')) &&
|
||||
states && states.length > 0
|
||||
) {
|
||||
return states.map(state =>
|
||||
this.openWithOptions(Object.assign(options, {
|
||||
initialPaths: state.initialPaths,
|
||||
pathsToOpen: state.initialPaths.filter(p => fs.isDirectorySync(p)),
|
||||
urlsToOpen: [],
|
||||
devMode: this.devMode,
|
||||
safeMode: this.safeMode
|
||||
}))
|
||||
)
|
||||
async loadPreviousWindowOptions () {
|
||||
const states = await this.storageFolder.load('application.json')
|
||||
if (states) {
|
||||
return states.map(state => ({
|
||||
initialPaths: state.initialPaths,
|
||||
pathsToOpen: state.initialPaths.filter(p => fs.isDirectorySync(p)),
|
||||
urlsToOpen: [],
|
||||
devMode: this.devMode,
|
||||
safeMode: this.safeMode
|
||||
}))
|
||||
} else {
|
||||
return null
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1235,11 +1266,11 @@ class AtomApplication extends EventEmitter {
|
||||
initialLine = initialColumn = null
|
||||
}
|
||||
|
||||
if (url.parse(pathToOpen).protocol == null) {
|
||||
pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen))
|
||||
}
|
||||
const normalizedPath = path.normalize(path.resolve(executedFrom, fs.normalize(pathToOpen)))
|
||||
const stat = fs.statSyncNoException(normalizedPath)
|
||||
if (stat) pathToOpen = normalizedPath
|
||||
|
||||
return {pathToOpen, initialLine, initialColumn}
|
||||
return {pathToOpen, stat, initialLine, initialColumn}
|
||||
}
|
||||
|
||||
// Opens a native dialog to prompt the user for a path.
|
||||
@@ -1293,17 +1324,16 @@ class AtomApplication extends EventEmitter {
|
||||
|
||||
// File dialog defaults to project directory of currently active editor
|
||||
if (path) openOptions.defaultPath = path
|
||||
return dialog.showOpenDialog(parentWindow, openOptions, callback)
|
||||
dialog.showOpenDialog(parentWindow, openOptions, callback)
|
||||
}
|
||||
|
||||
promptForRestart () {
|
||||
const chosen = dialog.showMessageBox(BrowserWindow.getFocusedWindow(), {
|
||||
dialog.showMessageBox(BrowserWindow.getFocusedWindow(), {
|
||||
type: 'warning',
|
||||
title: 'Restart required',
|
||||
message: 'You will need to restart Atom for this change to take effect.',
|
||||
buttons: ['Restart Atom', 'Cancel']
|
||||
})
|
||||
if (chosen === 0) return this.restart()
|
||||
}, response => { if (response === 0) this.restart() })
|
||||
}
|
||||
|
||||
restart () {
|
||||
|
||||
@@ -55,6 +55,13 @@ class AtomWindow extends EventEmitter {
|
||||
if (this.shouldHideTitleBar()) options.frame = false
|
||||
this.browserWindow = new BrowserWindow(options)
|
||||
|
||||
Object.defineProperty(this.browserWindow, 'loadSettingsJSON', {
|
||||
get: () => JSON.stringify(Object.assign({
|
||||
userSettings: this.atomApplication.configFile.get()
|
||||
}, this.loadSettings)),
|
||||
configurable: true
|
||||
})
|
||||
|
||||
this.handleEvents()
|
||||
|
||||
this.loadSettings = Object.assign({}, settings)
|
||||
@@ -96,8 +103,6 @@ class AtomWindow extends EventEmitter {
|
||||
this.representedDirectoryPaths = this.loadSettings.initialPaths
|
||||
if (!this.loadSettings.env) this.env = this.loadSettings.env
|
||||
|
||||
this.browserWindow.loadSettingsJSON = JSON.stringify(this.loadSettings)
|
||||
|
||||
this.browserWindow.on('window:loaded', () => {
|
||||
this.disableZoom()
|
||||
this.emit('window:loaded')
|
||||
@@ -163,7 +168,7 @@ class AtomWindow extends EventEmitter {
|
||||
if (!this.atomApplication.quitting && !this.unloading) {
|
||||
event.preventDefault()
|
||||
this.unloading = true
|
||||
this.atomApplication.saveState(false)
|
||||
this.atomApplication.saveCurrentWindowOptions(false)
|
||||
if (await this.prepareToUnload()) this.close()
|
||||
}
|
||||
})
|
||||
@@ -176,34 +181,34 @@ class AtomWindow extends EventEmitter {
|
||||
|
||||
this.browserWindow.on('unresponsive', () => {
|
||||
if (this.isSpec) return
|
||||
const chosen = dialog.showMessageBox(this.browserWindow, {
|
||||
dialog.showMessageBox(this.browserWindow, {
|
||||
type: 'warning',
|
||||
buttons: ['Force Close', 'Keep Waiting'],
|
||||
message: 'Editor is not responding',
|
||||
detail:
|
||||
'The editor is not responding. Would you like to force close it or just keep waiting?'
|
||||
})
|
||||
if (chosen === 0) this.browserWindow.destroy()
|
||||
}, response => { if (response === 0) this.browserWindow.destroy() })
|
||||
})
|
||||
|
||||
this.browserWindow.webContents.on('crashed', () => {
|
||||
this.browserWindow.webContents.on('crashed', async () => {
|
||||
if (this.headless) {
|
||||
console.log('Renderer process crashed, exiting')
|
||||
this.atomApplication.exit(100)
|
||||
return
|
||||
}
|
||||
|
||||
this.fileRecoveryService.didCrashWindow(this)
|
||||
const chosen = dialog.showMessageBox(this.browserWindow, {
|
||||
await this.fileRecoveryService.didCrashWindow(this)
|
||||
dialog.showMessageBox(this.browserWindow, {
|
||||
type: 'warning',
|
||||
buttons: ['Close Window', 'Reload', 'Keep It Open'],
|
||||
message: 'The editor has crashed',
|
||||
detail: 'Please report this issue to https://github.com/atom/atom'
|
||||
}, response => {
|
||||
switch (response) {
|
||||
case 0: return this.browserWindow.destroy()
|
||||
case 1: return this.browserWindow.reload()
|
||||
}
|
||||
})
|
||||
switch (chosen) {
|
||||
case 0: return this.browserWindow.destroy()
|
||||
case 1: return this.browserWindow.reload()
|
||||
}
|
||||
})
|
||||
|
||||
this.browserWindow.webContents.on('will-navigate', (event, url) => {
|
||||
@@ -246,6 +251,14 @@ class AtomWindow extends EventEmitter {
|
||||
this.sendMessage('open-locations', locationsToOpen)
|
||||
}
|
||||
|
||||
didChangeUserSettings (settings) {
|
||||
this.sendMessage('did-change-user-settings', settings)
|
||||
}
|
||||
|
||||
didFailToReadUserSettings (message) {
|
||||
this.sendMessage('did-fail-to-read-user-settings', message)
|
||||
}
|
||||
|
||||
replaceEnvironment (env) {
|
||||
this.browserWindow.webContents.send('environment', env)
|
||||
}
|
||||
@@ -414,8 +427,7 @@ class AtomWindow extends EventEmitter {
|
||||
this.representedDirectoryPaths = representedDirectoryPaths
|
||||
this.representedDirectoryPaths.sort()
|
||||
this.loadSettings.initialPaths = this.representedDirectoryPaths
|
||||
this.browserWindow.loadSettingsJSON = JSON.stringify(this.loadSettings)
|
||||
return this.atomApplication.saveState()
|
||||
return this.atomApplication.saveCurrentWindowOptions()
|
||||
}
|
||||
|
||||
didClosePathWithWaitSession (path) {
|
||||
|
||||
@@ -118,24 +118,26 @@ class AutoUpdateManager
|
||||
onUpdateNotAvailable: =>
|
||||
autoUpdater.removeListener 'error', @onUpdateError
|
||||
{dialog} = require 'electron'
|
||||
dialog.showMessageBox
|
||||
dialog.showMessageBox {
|
||||
type: 'info'
|
||||
buttons: ['OK']
|
||||
icon: @iconPath
|
||||
message: 'No update available.'
|
||||
title: 'No Update Available'
|
||||
detail: "Version #{@version} is the latest version."
|
||||
}, -> # noop callback to get async behavior
|
||||
|
||||
onUpdateError: (event, message) =>
|
||||
autoUpdater.removeListener 'update-not-available', @onUpdateNotAvailable
|
||||
{dialog} = require 'electron'
|
||||
dialog.showMessageBox
|
||||
dialog.showMessageBox {
|
||||
type: 'warning'
|
||||
buttons: ['OK']
|
||||
icon: @iconPath
|
||||
message: 'There was an error checking for updates.'
|
||||
title: 'Update Error'
|
||||
detail: message
|
||||
}, -> # noop callback to get async behavior
|
||||
|
||||
getWindows: ->
|
||||
global.atomApplication.getAllWindows()
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
'use babel'
|
||||
const {dialog} = require('electron')
|
||||
const crypto = require('crypto')
|
||||
const Path = require('path')
|
||||
const fs = require('fs-plus')
|
||||
|
||||
import {dialog} from 'electron'
|
||||
import crypto from 'crypto'
|
||||
import Path from 'path'
|
||||
import fs from 'fs-plus'
|
||||
|
||||
export default class FileRecoveryService {
|
||||
module.exports =
|
||||
class FileRecoveryService {
|
||||
constructor (recoveryDirectory) {
|
||||
this.recoveryDirectory = recoveryDirectory
|
||||
this.recoveryFilesByFilePath = new Map()
|
||||
@@ -13,15 +12,16 @@ export default class FileRecoveryService {
|
||||
this.windowsByRecoveryFile = new Map()
|
||||
}
|
||||
|
||||
willSavePath (window, path) {
|
||||
if (!fs.existsSync(path)) return
|
||||
async willSavePath (window, path) {
|
||||
const stats = await tryStatFile(path)
|
||||
if (!stats) return
|
||||
|
||||
const recoveryPath = Path.join(this.recoveryDirectory, RecoveryFile.fileNameForPath(path))
|
||||
const recoveryFile =
|
||||
this.recoveryFilesByFilePath.get(path) || new RecoveryFile(path, recoveryPath)
|
||||
this.recoveryFilesByFilePath.get(path) || new RecoveryFile(path, stats.mode, recoveryPath)
|
||||
|
||||
try {
|
||||
recoveryFile.retain()
|
||||
await recoveryFile.retain()
|
||||
} catch (err) {
|
||||
console.log(`Couldn't retain ${recoveryFile.recoveryPath}. Code: ${err.code}. Message: ${err.message}`)
|
||||
return
|
||||
@@ -39,11 +39,11 @@ export default class FileRecoveryService {
|
||||
this.recoveryFilesByFilePath.set(path, recoveryFile)
|
||||
}
|
||||
|
||||
didSavePath (window, path) {
|
||||
async didSavePath (window, path) {
|
||||
const recoveryFile = this.recoveryFilesByFilePath.get(path)
|
||||
if (recoveryFile != null) {
|
||||
try {
|
||||
recoveryFile.release()
|
||||
await recoveryFile.release()
|
||||
} catch (err) {
|
||||
console.log(`Couldn't release ${recoveryFile.recoveryPath}. Code: ${err.code}. Message: ${err.message}`)
|
||||
}
|
||||
@@ -53,27 +53,31 @@ export default class FileRecoveryService {
|
||||
}
|
||||
}
|
||||
|
||||
didCrashWindow (window) {
|
||||
async didCrashWindow (window) {
|
||||
if (!this.recoveryFilesByWindow.has(window)) return
|
||||
|
||||
const promises = []
|
||||
for (const recoveryFile of this.recoveryFilesByWindow.get(window)) {
|
||||
try {
|
||||
recoveryFile.recoverSync()
|
||||
} catch (error) {
|
||||
const message = 'A file that Atom was saving could be corrupted'
|
||||
const detail =
|
||||
`Error ${error.code}. There was a crash while saving "${recoveryFile.originalPath}", so this file might be blank or corrupted.\n` +
|
||||
`Atom couldn't recover it automatically, but a recovery file has been saved at: "${recoveryFile.recoveryPath}".`
|
||||
console.log(detail)
|
||||
dialog.showMessageBox(window.browserWindow, {type: 'info', buttons: ['OK'], message, detail})
|
||||
} finally {
|
||||
for (let window of this.windowsByRecoveryFile.get(recoveryFile)) {
|
||||
this.recoveryFilesByWindow.get(window).delete(recoveryFile)
|
||||
}
|
||||
this.windowsByRecoveryFile.delete(recoveryFile)
|
||||
this.recoveryFilesByFilePath.delete(recoveryFile.originalPath)
|
||||
}
|
||||
promises.push(recoveryFile.recover()
|
||||
.catch(error => {
|
||||
const message = 'A file that Atom was saving could be corrupted'
|
||||
const detail =
|
||||
`Error ${error.code}. There was a crash while saving "${recoveryFile.originalPath}", so this file might be blank or corrupted.\n` +
|
||||
`Atom couldn't recover it automatically, but a recovery file has been saved at: "${recoveryFile.recoveryPath}".`
|
||||
console.log(detail)
|
||||
dialog.showMessageBox(window, {type: 'info', buttons: ['OK'], message, detail}, () => { /* noop callback to get async behavior */ })
|
||||
})
|
||||
.then(() => {
|
||||
for (let window of this.windowsByRecoveryFile.get(recoveryFile)) {
|
||||
this.recoveryFilesByWindow.get(window).delete(recoveryFile)
|
||||
}
|
||||
this.windowsByRecoveryFile.delete(recoveryFile)
|
||||
this.recoveryFilesByFilePath.delete(recoveryFile.originalPath)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
didCloseWindow (window) {
|
||||
@@ -94,36 +98,64 @@ class RecoveryFile {
|
||||
return `${basename}-${randomSuffix}${extension}`
|
||||
}
|
||||
|
||||
constructor (originalPath, recoveryPath) {
|
||||
constructor (originalPath, fileMode, recoveryPath) {
|
||||
this.originalPath = originalPath
|
||||
this.fileMode = fileMode
|
||||
this.recoveryPath = recoveryPath
|
||||
this.refCount = 0
|
||||
}
|
||||
|
||||
storeSync () {
|
||||
fs.copyFileSync(this.originalPath, this.recoveryPath)
|
||||
async store () {
|
||||
await copyFile(this.originalPath, this.recoveryPath, this.fileMode)
|
||||
}
|
||||
|
||||
recoverSync () {
|
||||
fs.copyFileSync(this.recoveryPath, this.originalPath)
|
||||
this.removeSync()
|
||||
async recover () {
|
||||
await copyFile(this.recoveryPath, this.originalPath, this.fileMode)
|
||||
await this.remove()
|
||||
}
|
||||
|
||||
removeSync () {
|
||||
fs.unlinkSync(this.recoveryPath)
|
||||
async remove () {
|
||||
return new Promise((resolve, reject) =>
|
||||
fs.unlink(this.recoveryPath, error =>
|
||||
error && error.code !== 'ENOENT' ? reject(error) : resolve()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
retain () {
|
||||
if (this.isReleased()) this.storeSync()
|
||||
async retain () {
|
||||
if (this.isReleased()) await this.store()
|
||||
this.refCount++
|
||||
}
|
||||
|
||||
release () {
|
||||
async release () {
|
||||
this.refCount--
|
||||
if (this.isReleased()) this.removeSync()
|
||||
if (this.isReleased()) await this.remove()
|
||||
}
|
||||
|
||||
isReleased () {
|
||||
return this.refCount === 0
|
||||
}
|
||||
}
|
||||
|
||||
async function tryStatFile (path) {
|
||||
return new Promise((resolve, reject) =>
|
||||
fs.stat(path, (error, result) =>
|
||||
resolve(error == null && result)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async function copyFile (source, destination, mode) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const readStream = fs.createReadStream(source)
|
||||
readStream
|
||||
.on('error', reject)
|
||||
.once('open', () => {
|
||||
const writeStream = fs.createWriteStream(destination, {mode})
|
||||
writeStream
|
||||
.on('error', reject)
|
||||
.on('open', () => readStream.pipe(writeStream))
|
||||
.once('close', () => resolve())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,16 @@ module.exports = function start (resourcePath, startTime) {
|
||||
}
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', function (error = {}) {
|
||||
if (error.message != null) {
|
||||
console.log(error.message)
|
||||
}
|
||||
|
||||
if (error.stack != null) {
|
||||
console.log(error.stack)
|
||||
}
|
||||
})
|
||||
|
||||
const previousConsoleLog = console.log
|
||||
console.log = nslog
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
'use babel'
|
||||
|
||||
import Registry from 'winreg'
|
||||
import Path from 'path'
|
||||
const Registry = require('winreg')
|
||||
const Path = require('path')
|
||||
|
||||
let exeName = Path.basename(process.execPath)
|
||||
let appPath = `\"${process.execPath}\"`
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
ItemSpecificities = new WeakMap
|
||||
|
||||
merge = (menu, item, itemSpecificity=Infinity) ->
|
||||
item = cloneMenuItem(item)
|
||||
ItemSpecificities.set(item, itemSpecificity) if itemSpecificity
|
||||
matchingItemIndex = findMatchingItemIndex(menu, item)
|
||||
matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1
|
||||
|
||||
if matchingItem?
|
||||
if item.submenu?
|
||||
merge(matchingItem.submenu, submenuItem, itemSpecificity) for submenuItem in item.submenu
|
||||
else if itemSpecificity
|
||||
unless itemSpecificity < ItemSpecificities.get(matchingItem)
|
||||
menu[matchingItemIndex] = item
|
||||
else unless item.type is 'separator' and _.last(menu)?.type is 'separator'
|
||||
menu.push(item)
|
||||
|
||||
return
|
||||
|
||||
unmerge = (menu, item) ->
|
||||
matchingItemIndex = findMatchingItemIndex(menu, item)
|
||||
matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1
|
||||
|
||||
if matchingItem?
|
||||
if item.submenu?
|
||||
unmerge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu
|
||||
|
||||
unless matchingItem.submenu?.length > 0
|
||||
menu.splice(matchingItemIndex, 1)
|
||||
|
||||
findMatchingItemIndex = (menu, {type, label, submenu}) ->
|
||||
return -1 if type is 'separator'
|
||||
for item, index in menu
|
||||
if normalizeLabel(item.label) is normalizeLabel(label) and item.submenu? is submenu?
|
||||
return index
|
||||
-1
|
||||
|
||||
normalizeLabel = (label) ->
|
||||
return undefined unless label?
|
||||
|
||||
if process.platform is 'darwin'
|
||||
label
|
||||
else
|
||||
label.replace(/\&/g, '')
|
||||
|
||||
cloneMenuItem = (item) ->
|
||||
item = _.pick(item, 'type', 'label', 'enabled', 'visible', 'command', 'submenu', 'commandDetail', 'role', 'accelerator')
|
||||
if item.submenu?
|
||||
item.submenu = item.submenu.map (submenuItem) -> cloneMenuItem(submenuItem)
|
||||
item
|
||||
|
||||
# Determine the Electron accelerator for a given Atom keystroke.
|
||||
#
|
||||
# keystroke - The keystroke.
|
||||
#
|
||||
# Returns a String containing the keystroke in a format that can be interpreted
|
||||
# by Electron to provide nice icons where available.
|
||||
acceleratorForKeystroke = (keystroke) ->
|
||||
return null unless keystroke
|
||||
modifiers = keystroke.split(/-(?=.)/)
|
||||
key = modifiers.pop().toUpperCase().replace('+', 'Plus')
|
||||
|
||||
modifiers = modifiers.map (modifier) ->
|
||||
modifier.replace(/shift/ig, "Shift")
|
||||
.replace(/cmd/ig, "Command")
|
||||
.replace(/ctrl/ig, "Ctrl")
|
||||
.replace(/alt/ig, "Alt")
|
||||
|
||||
keys = modifiers.concat([key])
|
||||
keys.join("+")
|
||||
|
||||
module.exports = {merge, unmerge, normalizeLabel, cloneMenuItem, acceleratorForKeystroke}
|
||||
128
src/menu-helpers.js
Normal file
128
src/menu-helpers.js
Normal file
@@ -0,0 +1,128 @@
|
||||
const _ = require('underscore-plus')
|
||||
|
||||
const ItemSpecificities = new WeakMap()
|
||||
|
||||
// Add an item to a menu, ensuring separators are not duplicated.
|
||||
function addItemToMenu (item, menu) {
|
||||
const lastMenuItem = _.last(menu)
|
||||
const lastMenuItemIsSpearator = lastMenuItem && lastMenuItem.type === 'separator'
|
||||
if (!(item.type === 'separator' && lastMenuItemIsSpearator)) {
|
||||
menu.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
function merge (menu, item, itemSpecificity = Infinity) {
|
||||
item = cloneMenuItem(item)
|
||||
ItemSpecificities.set(item, itemSpecificity)
|
||||
const matchingItemIndex = findMatchingItemIndex(menu, item)
|
||||
|
||||
if (matchingItemIndex === -1) {
|
||||
addItemToMenu(item, menu)
|
||||
return
|
||||
}
|
||||
|
||||
const matchingItem = menu[matchingItemIndex]
|
||||
if (item.submenu != null) {
|
||||
for (let submenuItem of item.submenu) {
|
||||
merge(matchingItem.submenu, submenuItem, itemSpecificity)
|
||||
}
|
||||
} else if (itemSpecificity && itemSpecificity >= ItemSpecificities.get(matchingItem)) {
|
||||
menu[matchingItemIndex] = item
|
||||
}
|
||||
}
|
||||
|
||||
function unmerge (menu, item) {
|
||||
const matchingItemIndex = findMatchingItemIndex(menu, item)
|
||||
if (matchingItemIndex === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const matchingItem = menu[matchingItemIndex]
|
||||
if (item.submenu != null) {
|
||||
for (let submenuItem of item.submenu) {
|
||||
unmerge(matchingItem.submenu, submenuItem)
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingItem.submenu == null || matchingItem.submenu.length === 0) {
|
||||
menu.splice(matchingItemIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function findMatchingItemIndex (menu, { type, label, submenu }) {
|
||||
if (type === 'separator') {
|
||||
return -1
|
||||
}
|
||||
for (let index = 0; index < menu.length; index++) {
|
||||
const item = menu[index]
|
||||
if (
|
||||
normalizeLabel(item.label) === normalizeLabel(label) &&
|
||||
(item.submenu != null) === (submenu != null)
|
||||
) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
function normalizeLabel (label) {
|
||||
if (label == null) {
|
||||
return
|
||||
}
|
||||
return process.platform === 'darwin' ? label : label.replace(/&/g, '')
|
||||
}
|
||||
|
||||
function cloneMenuItem (item) {
|
||||
item = _.pick(
|
||||
item,
|
||||
'type',
|
||||
'label',
|
||||
'enabled',
|
||||
'visible',
|
||||
'command',
|
||||
'submenu',
|
||||
'commandDetail',
|
||||
'role',
|
||||
'accelerator'
|
||||
)
|
||||
if (item.submenu != null) {
|
||||
item.submenu = item.submenu.map(submenuItem => cloneMenuItem(submenuItem))
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
// Determine the Electron accelerator for a given Atom keystroke.
|
||||
//
|
||||
// keystroke - The keystroke.
|
||||
//
|
||||
// Returns a String containing the keystroke in a format that can be interpreted
|
||||
// by Electron to provide nice icons where available.
|
||||
function acceleratorForKeystroke (keystroke) {
|
||||
if (!keystroke) {
|
||||
return null
|
||||
}
|
||||
let modifiers = keystroke.split(/-(?=.)/)
|
||||
const key = modifiers
|
||||
.pop()
|
||||
.toUpperCase()
|
||||
.replace('+', 'Plus')
|
||||
|
||||
modifiers = modifiers.map(modifier =>
|
||||
modifier
|
||||
.replace(/shift/gi, 'Shift')
|
||||
.replace(/cmd/gi, 'Command')
|
||||
.replace(/ctrl/gi, 'Ctrl')
|
||||
.replace(/alt/gi, 'Alt')
|
||||
)
|
||||
|
||||
const keys = [...modifiers, key]
|
||||
return keys.join('+')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
merge,
|
||||
unmerge,
|
||||
normalizeLabel,
|
||||
cloneMenuItem,
|
||||
acceleratorForKeystroke
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
/** @babel */
|
||||
|
||||
const path = require('path')
|
||||
|
||||
// Private: re-join the segments split from an absolute path to form another absolute path.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/** @babel */
|
||||
const {Disposable} = require('event-kit')
|
||||
|
||||
import {Disposable} from 'event-kit'
|
||||
|
||||
export default {
|
||||
module.exports = {
|
||||
name: 'Null Grammar',
|
||||
scopeName: 'text.plain.null-grammar',
|
||||
scopeForId (id) {
|
||||
|
||||
@@ -43,8 +43,8 @@ class Package {
|
||||
? params.bundledPackage
|
||||
: this.packageManager.isBundledPackagePath(this.path)
|
||||
this.name =
|
||||
params.name ||
|
||||
(this.metadata && this.metadata.name) ||
|
||||
params.name ||
|
||||
path.basename(this.path)
|
||||
this.reset()
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ class PaneContainer {
|
||||
|
||||
deserialize (state, deserializerManager) {
|
||||
if (state.version !== SERIALIZATION_VERSION) return
|
||||
this.itemRegistry = new ItemRegistry()
|
||||
this.setRoot(deserializerManager.deserialize(state.root))
|
||||
this.activePane = find(this.getRoot().getPanes(), pane => pane.id === state.activePaneId) || this.getPanes()[0]
|
||||
if (this.config.get('core.destroyEmptyPanes')) this.destroyEmptyPanes()
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/** @babel */
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
|
||||
const nsfw = require('@atom/nsfw')
|
||||
const watcher = require('@atom/watcher')
|
||||
const {NativeWatcherRegistry} = require('./native-watcher-registry')
|
||||
|
||||
// Private: Associate native watcher action flags with descriptive String equivalents.
|
||||
@@ -23,145 +22,7 @@ const WATCHER_STATE = {
|
||||
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.
|
||||
// Private: Interface with and normalize events from a filesystem watcher implementation.
|
||||
class NativeWatcher {
|
||||
|
||||
// Private: Initialize a native watcher on a path.
|
||||
@@ -172,37 +33,10 @@ class NativeWatcher {
|
||||
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.
|
||||
@@ -214,15 +48,16 @@ class NativeWatcher {
|
||||
}
|
||||
this.state = WATCHER_STATE.STARTING
|
||||
|
||||
const Backend = this.getCurrentBackend()
|
||||
|
||||
this.backend = new Backend()
|
||||
await this.backend.start(this.normalizedPath, this.onEvents, this.onError)
|
||||
await this.doStart()
|
||||
|
||||
this.state = WATCHER_STATE.RUNNING
|
||||
this.emitter.emit('did-start')
|
||||
}
|
||||
|
||||
doStart () {
|
||||
return Promise.reject('doStart() not overridden')
|
||||
}
|
||||
|
||||
// Private: Return true if the underlying watcher is actively listening for filesystem events.
|
||||
isRunning () {
|
||||
return this.state === WATCHER_STATE.RUNNING
|
||||
@@ -285,8 +120,8 @@ class NativeWatcher {
|
||||
//
|
||||
// * `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})
|
||||
reattachTo (replacement, watchedPath, options) {
|
||||
this.emitter.emit('should-detach', {replacement, watchedPath, options})
|
||||
}
|
||||
|
||||
// Private: Stop the native watcher and release any operating system resources associated with it.
|
||||
@@ -299,12 +134,17 @@ class NativeWatcher {
|
||||
this.state = WATCHER_STATE.STOPPING
|
||||
this.emitter.emit('will-stop')
|
||||
|
||||
await this.backend.stop()
|
||||
await this.doStop()
|
||||
|
||||
this.state = WATCHER_STATE.STOPPED
|
||||
|
||||
this.emitter.emit('did-stop')
|
||||
}
|
||||
|
||||
doStop () {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// Private: Detach any event subscribers.
|
||||
dispose () {
|
||||
this.emitter.dispose()
|
||||
@@ -326,6 +166,129 @@ class NativeWatcher {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 AtomNativeWatcher extends NativeWatcher {
|
||||
async doStart () {
|
||||
const getRealPath = givenPath => {
|
||||
return new Promise(resolve => {
|
||||
fs.realpath(givenPath, (err, resolvedPath) => {
|
||||
err ? resolve(null) : resolve(resolvedPath)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.subs.add(atom.workspace.observeTextEditors(async editor => {
|
||||
let realPath = await getRealPath(editor.getPath())
|
||||
if (!realPath || !realPath.startsWith(this.normalizedPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const announce = (action, oldPath) => {
|
||||
const payload = {action, path: realPath}
|
||||
if (oldPath) payload.oldPath = oldPath
|
||||
this.onEvents([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 !== this.normalizedPath) {
|
||||
const oldPath = this.normalizedPath
|
||||
this.normalizedPath = 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
|
||||
|
||||
this.onEvents([{action: 'added', path: realPath}])
|
||||
}))
|
||||
|
||||
this.subs.add(treeView.onEntryDeleted(async event => {
|
||||
const realPath = await getRealPath(event.path)
|
||||
if (!realPath || isOpenInEditor(realPath)) return
|
||||
|
||||
this.onEvents([{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
|
||||
|
||||
this.onEvents([{action: 'renamed', path: realNewPath, oldPath: realOldPath}])
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Implement a native watcher by translating events from an NSFW watcher.
|
||||
class NSFWNativeWatcher extends NativeWatcher {
|
||||
async doStart (rootPath, eventCallback, errorCallback) {
|
||||
const handler = events => {
|
||||
this.onEvents(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(
|
||||
this.normalizedPath,
|
||||
handler,
|
||||
{debounceMS: 100, errorCallback: this.onError}
|
||||
)
|
||||
|
||||
await this.watcher.start()
|
||||
}
|
||||
|
||||
doStop () {
|
||||
return this.watcher.stop()
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -386,6 +349,15 @@ class PathWatcher {
|
||||
this.native = null
|
||||
this.changeCallbacks = new Map()
|
||||
|
||||
this.attachedPromise = new Promise(resolve => {
|
||||
this.resolveAttachedPromise = resolve
|
||||
})
|
||||
|
||||
this.startPromise = new Promise((resolve, reject) => {
|
||||
this.resolveStartPromise = resolve
|
||||
this.rejectStartPromise = reject
|
||||
})
|
||||
|
||||
this.normalizedPathPromise = new Promise((resolve, reject) => {
|
||||
fs.realpath(watchedPath, (err, real) => {
|
||||
if (err) {
|
||||
@@ -397,13 +369,7 @@ class PathWatcher {
|
||||
resolve(real)
|
||||
})
|
||||
})
|
||||
|
||||
this.attachedPromise = new Promise(resolve => {
|
||||
this.resolveAttachedPromise = resolve
|
||||
})
|
||||
this.startPromise = new Promise(resolve => {
|
||||
this.resolveStartPromise = resolve
|
||||
})
|
||||
this.normalizedPathPromise.catch(err => this.rejectStartPromise(err))
|
||||
|
||||
this.emitter = new Emitter()
|
||||
this.subs = new CompositeDisposable()
|
||||
@@ -545,46 +511,139 @@ class PathWatcher {
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Globally tracked state used to de-duplicate related [PathWatchers]{PathWatcher}.
|
||||
// Private: Globally tracked state used to de-duplicate related [PathWatchers]{PathWatcher} backed by emulated Atom
|
||||
// events or NSFW.
|
||||
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()
|
||||
// Private: Access the currently active manager instance, creating one if necessary.
|
||||
static active () {
|
||||
if (!this.activeManager) {
|
||||
this.activeManager = new PathWatcherManager(atom.config.get('core.fileSystemWatcher'))
|
||||
this.sub = atom.config.onDidChange('core.fileSystemWatcher', ({newValue}) => { this.transitionTo(newValue) })
|
||||
}
|
||||
return PathWatcherManager.theManager
|
||||
return this.activeManager
|
||||
}
|
||||
|
||||
// Private: Replace the active {PathWatcherManager} with a new one that creates [NativeWatchers]{NativeWatcher}
|
||||
// based on the value of `setting`.
|
||||
static async transitionTo (setting) {
|
||||
const current = this.active()
|
||||
|
||||
if (this.transitionPromise) {
|
||||
await this.transitionPromise
|
||||
}
|
||||
|
||||
if (current.setting === setting) {
|
||||
return
|
||||
}
|
||||
current.isShuttingDown = true
|
||||
|
||||
let resolveTransitionPromise = () => {}
|
||||
this.transitionPromise = new Promise(resolve => {
|
||||
resolveTransitionPromise = resolve
|
||||
})
|
||||
|
||||
const replacement = new PathWatcherManager(setting)
|
||||
this.activeManager = replacement
|
||||
|
||||
await Promise.all(
|
||||
Array.from(current.live, async ([root, native]) => {
|
||||
const w = await replacement.createWatcher(root, {}, () => {})
|
||||
native.reattachTo(w.native, root, w.native.options || {})
|
||||
})
|
||||
)
|
||||
|
||||
current.stopAllWatchers()
|
||||
|
||||
resolveTransitionPromise()
|
||||
this.transitionPromise = null
|
||||
}
|
||||
|
||||
// Private: Initialize global {PathWatcher} state.
|
||||
constructor () {
|
||||
this.live = new Set()
|
||||
this.nativeRegistry = new NativeWatcherRegistry(
|
||||
normalizedPath => {
|
||||
const nativeWatcher = new NativeWatcher(normalizedPath)
|
||||
constructor (setting) {
|
||||
this.setting = setting
|
||||
this.live = new Map()
|
||||
|
||||
this.live.add(nativeWatcher)
|
||||
const sub = nativeWatcher.onWillStop(() => {
|
||||
this.live.delete(nativeWatcher)
|
||||
sub.dispose()
|
||||
})
|
||||
const initLocal = NativeConstructor => {
|
||||
this.nativeRegistry = new NativeWatcherRegistry(
|
||||
normalizedPath => {
|
||||
const nativeWatcher = new NativeConstructor(normalizedPath)
|
||||
|
||||
return nativeWatcher
|
||||
}
|
||||
)
|
||||
this.live.set(normalizedPath, nativeWatcher)
|
||||
const sub = nativeWatcher.onWillStop(() => {
|
||||
this.live.delete(normalizedPath)
|
||||
sub.dispose()
|
||||
})
|
||||
|
||||
return nativeWatcher
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (setting === 'atom') {
|
||||
initLocal(AtomNativeWatcher)
|
||||
} else if (setting === 'experimental') {
|
||||
//
|
||||
} else if (setting === 'poll') {
|
||||
//
|
||||
} else {
|
||||
initLocal(NSFWNativeWatcher)
|
||||
}
|
||||
|
||||
this.isShuttingDown = false
|
||||
}
|
||||
|
||||
useExperimentalWatcher () {
|
||||
return this.setting === 'experimental' || this.setting === 'poll'
|
||||
}
|
||||
|
||||
// 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
|
||||
async createWatcher (rootPath, options, eventCallback) {
|
||||
if (this.isShuttingDown) {
|
||||
await this.constructor.transitionPromise
|
||||
return PathWatcherManager.active().createWatcher(rootPath, options, eventCallback)
|
||||
}
|
||||
|
||||
if (this.useExperimentalWatcher()) {
|
||||
if (this.setting === 'poll') {
|
||||
options.poll = true
|
||||
}
|
||||
|
||||
const w = await watcher.watchPath(rootPath, options, eventCallback)
|
||||
this.live.set(rootPath, w.native)
|
||||
return w
|
||||
}
|
||||
|
||||
const w = new PathWatcher(this.nativeRegistry, rootPath, options)
|
||||
w.onDidChange(eventCallback)
|
||||
await w.getStartPromise()
|
||||
return w
|
||||
}
|
||||
|
||||
// Private: Directly access the {NativeWatcherRegistry}.
|
||||
getRegistry () {
|
||||
if (this.useExperimentalWatcher()) {
|
||||
return watcher.getRegistry()
|
||||
}
|
||||
|
||||
return this.nativeRegistry
|
||||
}
|
||||
|
||||
// Private: Sample watcher usage statistics. Only available for experimental watchers.
|
||||
status () {
|
||||
if (this.useExperimentalWatcher()) {
|
||||
return watcher.status()
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
// Private: Return a {String} depicting the currently active native watchers.
|
||||
print () {
|
||||
if (this.useExperimentalWatcher()) {
|
||||
return watcher.printWatchers()
|
||||
}
|
||||
|
||||
return this.nativeRegistry.print()
|
||||
}
|
||||
|
||||
@@ -592,8 +651,12 @@ class PathWatcherManager {
|
||||
//
|
||||
// Returns a {Promise} that resolves when all native watcher resources are disposed.
|
||||
stopAllWatchers () {
|
||||
if (this.useExperimentalWatcher()) {
|
||||
return watcher.stopAllWatchers()
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
Array.from(this.live, watcher => watcher.stop())
|
||||
Array.from(this.live, ([, w]) => w.stop())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -638,19 +701,33 @@ class PathWatcherManager {
|
||||
// ```
|
||||
//
|
||||
function watchPath (rootPath, options, eventCallback) {
|
||||
const watcher = PathWatcherManager.instance().createWatcher(rootPath, options, eventCallback)
|
||||
return watcher.getStartPromise().then(() => watcher)
|
||||
return PathWatcherManager.active().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()
|
||||
return PathWatcherManager.active().stopAllWatchers()
|
||||
}
|
||||
|
||||
// Private: Show the currently active native watchers.
|
||||
function printWatchers () {
|
||||
return PathWatcherManager.instance().print()
|
||||
// Private: Show the currently active native watchers in a formatted {String}.
|
||||
watchPath.printWatchers = function () {
|
||||
return PathWatcherManager.active().print()
|
||||
}
|
||||
|
||||
module.exports = {watchPath, stopAllWatchers, printWatchers}
|
||||
// Private: Access the active {NativeWatcherRegistry}.
|
||||
watchPath.getRegistry = function () {
|
||||
return PathWatcherManager.active().getRegistry()
|
||||
}
|
||||
|
||||
// Private: Sample usage statistics for the active watcher.
|
||||
watchPath.status = function () {
|
||||
return PathWatcherManager.active().status()
|
||||
}
|
||||
|
||||
// Private: Configure @atom/watcher ("experimental") directly.
|
||||
watchPath.configure = function (...args) {
|
||||
return watcher.configure(...args)
|
||||
}
|
||||
|
||||
module.exports = {watchPath, stopAllWatchers}
|
||||
|
||||
@@ -695,7 +695,7 @@ class Project extends Model {
|
||||
}
|
||||
|
||||
subscribeToBuffer (buffer) {
|
||||
buffer.onWillSave(({path}) => this.applicationDelegate.emitWillSavePath(path))
|
||||
buffer.onWillSave(async ({path}) => this.applicationDelegate.emitWillSavePath(path))
|
||||
buffer.onDidSave(({path}) => this.applicationDelegate.emitDidSavePath(path))
|
||||
buffer.onDidDestroy(() => this.removeBuffer(buffer))
|
||||
buffer.onDidChangePath(() => {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/** @babel */
|
||||
const SelectListView = require('atom-select-list')
|
||||
|
||||
import SelectListView from 'atom-select-list'
|
||||
|
||||
export default class ReopenProjectListView {
|
||||
module.exports =
|
||||
class ReopenProjectListView {
|
||||
constructor (callback) {
|
||||
this.callback = callback
|
||||
this.selectListView = new SelectListView({
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/** @babel */
|
||||
const {CompositeDisposable} = require('event-kit')
|
||||
const path = require('path')
|
||||
|
||||
import {CompositeDisposable} from 'event-kit'
|
||||
import path from 'path'
|
||||
|
||||
export default class ReopenProjectMenuManager {
|
||||
module.exports =
|
||||
class ReopenProjectMenuManager {
|
||||
constructor ({menu, commands, history, config, open}) {
|
||||
this.menuManager = menu
|
||||
this.historyManager = history
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
path = require "path"
|
||||
fs = require "fs-plus"
|
||||
|
||||
module.exports =
|
||||
class StorageFolder
|
||||
constructor: (containingPath) ->
|
||||
@path = path.join(containingPath, "storage") if containingPath?
|
||||
|
||||
clear: ->
|
||||
return unless @path?
|
||||
|
||||
try
|
||||
fs.removeSync(@path)
|
||||
catch error
|
||||
console.warn "Error deleting #{@path}", error.stack, error
|
||||
|
||||
storeSync: (name, object) ->
|
||||
return unless @path?
|
||||
|
||||
fs.writeFileSync(@pathForKey(name), JSON.stringify(object), 'utf8')
|
||||
|
||||
load: (name) ->
|
||||
return unless @path?
|
||||
|
||||
statePath = @pathForKey(name)
|
||||
try
|
||||
stateString = fs.readFileSync(statePath, 'utf8')
|
||||
catch error
|
||||
unless error.code is 'ENOENT'
|
||||
console.warn "Error reading state file: #{statePath}", error.stack, error
|
||||
return undefined
|
||||
|
||||
try
|
||||
JSON.parse(stateString)
|
||||
catch error
|
||||
console.warn "Error parsing state file: #{statePath}", error.stack, error
|
||||
|
||||
pathForKey: (name) -> path.join(@getPath(), name)
|
||||
getPath: -> @path
|
||||
49
src/storage-folder.js
Normal file
49
src/storage-folder.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs-plus')
|
||||
|
||||
module.exports =
|
||||
class StorageFolder {
|
||||
constructor (containingPath) {
|
||||
if (containingPath) {
|
||||
this.path = path.join(containingPath, 'storage')
|
||||
}
|
||||
}
|
||||
|
||||
store (name, object) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.path) return resolve()
|
||||
fs.writeFile(this.pathForKey(name), JSON.stringify(object), 'utf8', error =>
|
||||
error ? reject(error) : resolve()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
load (name) {
|
||||
return new Promise(resolve => {
|
||||
if (!this.path) return resolve(null)
|
||||
const statePath = this.pathForKey(name)
|
||||
fs.readFile(statePath, 'utf8', (error, stateString) => {
|
||||
if (error && error.code !== 'ENOENT') {
|
||||
console.warn(`Error reading state file: ${statePath}`, error.stack, error)
|
||||
}
|
||||
|
||||
if (!stateString) return resolve(null)
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(stateString))
|
||||
} catch (error) {
|
||||
console.warn(`Error parsing state file: ${statePath}`, error.stack, error)
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pathForKey (name) {
|
||||
return path.join(this.getPath(), name)
|
||||
}
|
||||
|
||||
getPath () {
|
||||
return this.path
|
||||
}
|
||||
}
|
||||
@@ -1763,28 +1763,28 @@ class TextEditorComponent {
|
||||
|
||||
const screenPosition = this.screenPositionForMouseEvent(event)
|
||||
|
||||
if (button !== 0 || (platform === 'darwin' && ctrlKey)) {
|
||||
// Always set cursor position on middle-click
|
||||
// Only set cursor position on right-click if there is one cursor with no selection
|
||||
const ranges = model.getSelectedBufferRanges()
|
||||
if (button === 1 || (ranges.length === 1 && ranges[0].isEmpty())) {
|
||||
model.setCursorScreenPosition(screenPosition, {autoscroll: false})
|
||||
}
|
||||
if (button === 1) {
|
||||
model.setCursorScreenPosition(screenPosition, {autoscroll: false})
|
||||
|
||||
// On Linux, pasting happens on middle click. A textInput event with the
|
||||
// contents of the selection clipboard will be dispatched by the browser
|
||||
// automatically on mouseup.
|
||||
if (platform === 'linux' && button === 1) model.insertText(clipboard.readText('selection'))
|
||||
if (platform === 'linux' && this.isInputEnabled()) model.insertText(clipboard.readText('selection'))
|
||||
return
|
||||
}
|
||||
|
||||
if (button !== 0) return
|
||||
|
||||
// Ctrl-click brings up the context menu on macOS
|
||||
if (platform === 'darwin' && ctrlKey) return
|
||||
|
||||
if (target && target.matches('.fold-marker')) {
|
||||
const bufferPosition = model.bufferPositionForScreenPosition(screenPosition)
|
||||
model.destroyFoldsContainingBufferPositions([bufferPosition], false)
|
||||
return
|
||||
}
|
||||
|
||||
const addOrRemoveSelection = metaKey || ctrlKey
|
||||
const addOrRemoveSelection = metaKey || (ctrlKey && platform !== 'darwin')
|
||||
|
||||
switch (detail) {
|
||||
case 1:
|
||||
@@ -2705,7 +2705,7 @@ class TextEditorComponent {
|
||||
}
|
||||
|
||||
getContentWidth () {
|
||||
return Math.round(this.getLongestLineWidth() + this.getBaseCharacterWidth())
|
||||
return Math.ceil(this.getLongestLineWidth() + this.getBaseCharacterWidth())
|
||||
}
|
||||
|
||||
getScrollContainerClientWidthInBaseCharacters () {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const {Document} = require('tree-sitter')
|
||||
const {Point, Range, Emitter} = require('atom')
|
||||
const {Point, Range} = require('text-buffer')
|
||||
const {Emitter, Disposable} = require('event-kit')
|
||||
const ScopeDescriptor = require('./scope-descriptor')
|
||||
const TokenizedLine = require('./tokenized-line')
|
||||
const TextMateLanguageMode = require('./text-mate-language-mode')
|
||||
@@ -64,7 +65,7 @@ class TreeSitterLanguageMode {
|
||||
}
|
||||
|
||||
onDidChangeHighlighting (callback) {
|
||||
return this.emitter.on('did-change-hightlighting', callback)
|
||||
return this.emitter.on('did-change-highlighting', callback)
|
||||
}
|
||||
|
||||
classNameForScopeId (scopeId) {
|
||||
@@ -279,10 +280,16 @@ class TreeSitterLanguageMode {
|
||||
if (node) return new Range(node.startPosition, node.endPosition)
|
||||
}
|
||||
|
||||
bufferRangeForScopeAtPosition (position) {
|
||||
return this.getRangeForSyntaxNodeContainingRange(new Range(position, position))
|
||||
}
|
||||
|
||||
/*
|
||||
Section - Backward compatibility shims
|
||||
*/
|
||||
|
||||
onDidTokenize (callback) { return new Disposable(() => {}) }
|
||||
|
||||
tokenizedLineForRow (row) {
|
||||
return new TokenizedLine({
|
||||
openScopes: [],
|
||||
@@ -495,16 +502,22 @@ class TreeSitterHighlightIterator {
|
||||
class TreeSitterTextBufferInput {
|
||||
constructor (buffer) {
|
||||
this.buffer = buffer
|
||||
this.seek(0)
|
||||
this.position = {row: 0, column: 0}
|
||||
this.isBetweenCRLF = false
|
||||
}
|
||||
|
||||
seek (characterIndex) {
|
||||
this.position = this.buffer.positionForCharacterIndex(characterIndex)
|
||||
seek (offset, position) {
|
||||
this.position = position
|
||||
this.isBetweenCRLF = this.position.column > this.buffer.lineLengthForRow(this.position.row)
|
||||
}
|
||||
|
||||
read () {
|
||||
const endPosition = this.buffer.clipPosition(this.position.traverse({row: 1000, column: 0}))
|
||||
const text = this.buffer.getTextInRange([this.position, endPosition])
|
||||
const endPosition = this.buffer.clipPosition(new Point(this.position.row + 1000, 0))
|
||||
let text = this.buffer.getTextInRange([this.position, endPosition])
|
||||
if (this.isBetweenCRLF) {
|
||||
text = text.slice(1)
|
||||
this.isBetweenCRLF = false
|
||||
}
|
||||
this.position = endPosition
|
||||
return text
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/** @babel */
|
||||
|
||||
import fs from 'fs'
|
||||
import childProcess from 'child_process'
|
||||
const fs = require('fs')
|
||||
const childProcess = require('child_process')
|
||||
|
||||
const ENVIRONMENT_VARIABLES_TO_PRESERVE = new Set([
|
||||
'NODE_ENV',
|
||||
@@ -120,4 +118,4 @@ async function getEnvFromShell (env) {
|
||||
return result
|
||||
}
|
||||
|
||||
export default { updateProcessEnv, shouldGetEnvFromShell }
|
||||
module.exports = {updateProcessEnv, shouldGetEnvFromShell}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use babel'
|
||||
|
||||
const _ = require('underscore-plus')
|
||||
const url = require('url')
|
||||
const path = require('path')
|
||||
|
||||
@@ -81,5 +81,5 @@
|
||||
|
||||
// Other
|
||||
|
||||
@font-family: 'BlinkMacSystemFont', 'Lucida Grande', 'Segoe UI', Ubuntu, Cantarell, sans-serif;
|
||||
@font-family: system-ui;
|
||||
@use-custom-controls: true; // false uses native controls
|
||||
|
||||
Reference in New Issue
Block a user