Merge remote-tracking branch 'origin/master' into b3-failing-seed

This commit is contained in:
Linus Eriksson
2018-02-15 19:49:58 +01:00
56 changed files with 4648 additions and 3963 deletions

55
docs/focus/2018-02-12.md Normal file
View 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.

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,3 @@
/** @babel */
const fs = require('fs-plus')
const path = require('path')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

1446
src/config.js Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -429,7 +429,7 @@ class GrammarRegistry {
this.readGrammar(grammarPath, (error, grammar) => {
if (error) return callback(error)
this.addGrammar(grammar)
callback(grammar)
callback(null, grammar)
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}\"`

View File

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

View File

@@ -1,5 +1,3 @@
/** @babel */
const path = require('path')
// Private: re-join the segments split from an absolute path to form another absolute path.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,3 @@
'use babel'
const _ = require('underscore-plus')
const url = require('url')
const path = require('path')

View File

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