From c061f3604007b5210c37549aa7b736e5621a01d0 Mon Sep 17 00:00:00 2001 From: Bryant Ung Date: Fri, 17 Feb 2017 10:39:53 -0800 Subject: [PATCH 01/55] Updating requires for linux to support 32bit build --- resources/linux/redhat/atom.spec.in | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/linux/redhat/atom.spec.in b/resources/linux/redhat/atom.spec.in index 82a5fbf9a..306f1029e 100644 --- a/resources/linux/redhat/atom.spec.in +++ b/resources/linux/redhat/atom.spec.in @@ -7,7 +7,11 @@ URL: https://atom.io/ AutoReqProv: no # Avoid libchromiumcontent.so missing dependency Prefix: <%= installDir %> +%ifarch i386 i486 i586 i686 +Requires: lsb-core-noarch, libXss.so.1 +%else Requires: lsb-core-noarch, libXss.so.1()(64bit) +%endif %description <%= description %> From 09c36c0b3752aea3eb275804dd5b8c92755aba7c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 28 Feb 2017 15:15:08 +0100 Subject: [PATCH 02/55] :arrow_up: git-diff --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 01eedc2eb..671875642 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "exception-reporting": "0.41.1", "find-and-replace": "0.206.3", "fuzzy-finder": "1.4.1", - "git-diff": "1.3.2", + "git-diff": "1.3.3", "go-to-line": "0.32.0", "grammar-selector": "0.49.3", "image-view": "0.61.1", From 5582766563fb293469f6e63d14da888ea2c51b7c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 28 Feb 2017 10:48:25 -0800 Subject: [PATCH 03/55] Use new maxLineLength parameter to GrammarRegistry --- package.json | 2 +- src/grammar-registry.coffee | 2 +- src/tokenized-buffer.coffee | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 671875642..de3587aba 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "devtron": "1.3.0", "event-kit": "^2.1.0", "find-parent-dir": "^0.3.0", - "first-mate": "6.1.0", + "first-mate": "6.2.2-0", "fs-plus": "2.9.2", "fstream": "0.1.24", "fuzzaldrin": "^2.1", diff --git a/src/grammar-registry.coffee b/src/grammar-registry.coffee index 899bb4cff..a2341c967 100644 --- a/src/grammar-registry.coffee +++ b/src/grammar-registry.coffee @@ -15,7 +15,7 @@ PathSplitRegex = new RegExp("[/.]") module.exports = class GrammarRegistry extends FirstMate.GrammarRegistry constructor: ({@config}={}) -> - super(maxTokensPerLine: 100) + super(maxTokensPerLine: 100, maxLineLength: 1000) createToken: (value, scopes) -> new Token({value, scopes}) diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 77221f52e..234f82be9 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -8,8 +8,6 @@ ScopeDescriptor = require './scope-descriptor' TokenizedBufferIterator = require './tokenized-buffer-iterator' NullGrammar = require './null-grammar' -MAX_LINE_LENGTH_TO_TOKENIZE = 500 - module.exports = class TokenizedBuffer extends Model grammar: null @@ -253,8 +251,6 @@ class TokenizedBuffer extends Model buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) -> lineEnding = @buffer.lineEndingForRow(row) - if text.length > MAX_LINE_LENGTH_TO_TOKENIZE - text = text.slice(0, MAX_LINE_LENGTH_TO_TOKENIZE) {tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false) new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator}) From 507535e0702294c54a7f61bf9859b646aedf9a86 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 28 Feb 2017 12:41:53 -0800 Subject: [PATCH 04/55] :arrow_up: first-mate (prerelease) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de3587aba..9465d321d 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "devtron": "1.3.0", "event-kit": "^2.1.0", "find-parent-dir": "^0.3.0", - "first-mate": "6.2.2-0", + "first-mate": "6.2.2-1", "fs-plus": "2.9.2", "fstream": "0.1.24", "fuzzaldrin": "^2.1", From fabc38162b7e6b4b4ff003810853d523edc7a349 Mon Sep 17 00:00:00 2001 From: Wliu Date: Wed, 1 Mar 2017 17:32:31 -0500 Subject: [PATCH 05/55] Remove macOS emoji workaround No longer needed now that we are on Chrome 53 --- spec/workspace-element-spec.coffee | 2 -- src/workspace-element.coffee | 7 +------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/spec/workspace-element-spec.coffee b/spec/workspace-element-spec.coffee index a741dbbd4..ec24242ac 100644 --- a/spec/workspace-element-spec.coffee +++ b/spec/workspace-element-spec.coffee @@ -54,12 +54,10 @@ describe "WorkspaceElement", -> it "updates the font-family based on the 'editor.fontFamily' config value", -> initialCharWidth = editor.getDefaultCharWidth() fontFamily = atom.config.get('editor.fontFamily') - fontFamily += ', "Apple Color Emoji"' if process.platform is 'darwin' expect(getComputedStyle(editorElement).fontFamily).toBe fontFamily atom.config.set('editor.fontFamily', 'sans-serif') fontFamily = atom.config.get('editor.fontFamily') - fontFamily += ', "Apple Color Emoji"' if process.platform is 'darwin' expect(getComputedStyle(editorElement).fontFamily).toBe fontFamily expect(editor.getDefaultCharWidth()).not.toBe initialCharWidth diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee index f598bef0b..6defe33da 100644 --- a/src/workspace-element.coffee +++ b/src/workspace-element.coffee @@ -44,15 +44,10 @@ class WorkspaceElement extends HTMLElement @subscriptions.add @config.onDidChange 'editor.lineHeight', @updateGlobalTextEditorStyleSheet.bind(this) updateGlobalTextEditorStyleSheet: -> - fontFamily = @config.get('editor.fontFamily') - # TODO: There is a bug in how some emojis (e.g. ❤️) are rendered on macOS. - # This workaround should be removed once we update to Chromium 51, where the - # problem was fixed. - fontFamily += ', "Apple Color Emoji"' if process.platform is 'darwin' styleSheetSource = """ atom-text-editor { font-size: #{@config.get('editor.fontSize')}px; - font-family: #{fontFamily}; + font-family: #{@config.get('editor.fontFamily')}; line-height: #{@config.get('editor.lineHeight')}; } """ From c9358c6f433efe942be1526ffd0b88ad14bd8879 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 1 Mar 2017 16:05:59 -0800 Subject: [PATCH 06/55] :arrow_up: first-mate --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9465d321d..667506303 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "devtron": "1.3.0", "event-kit": "^2.1.0", "find-parent-dir": "^0.3.0", - "first-mate": "6.2.2-1", + "first-mate": "6.3.0", "fs-plus": "2.9.2", "fstream": "0.1.24", "fuzzaldrin": "^2.1", From 9e3999cab9de99688d8dcbce432ccdb7595238cf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 2 Mar 2017 10:04:07 -0700 Subject: [PATCH 07/55] Cause an assertion failure if defaultMarkerLayer is destroyed early This is to investigate a case where the default marker layer of the editor is destroyed without the editor itself or its buffer being destroyed, which is causing `Cannot decorate a destroyed marker` exceptions. --- src/text-editor.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 3cdd363a7..1f24fa252 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -192,6 +192,9 @@ class TextEditor extends Model @displayLayer.setTextDecorationLayer(@tokenizedBuffer) @defaultMarkerLayer = @displayLayer.addMarkerLayer() + @disposables.add(@defaultMarkerLayer.onDidDestroy => + @assert(false, "defaultMarkerLayer destroyed at an unexpected time") + ) @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true) @selectionsMarkerLayer.trackDestructionInOnDidCreateMarkerCallbacks = true From c26509cfab267f888bad325f153d2fcb6eb9390c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Mar 2017 10:14:59 -0800 Subject: [PATCH 08/55] Throw an error when assertions fail if built from source Signed-off-by: Nathan Sobo --- spec/atom-environment-spec.coffee | 6 ++++++ src/atom-environment.coffee | 2 ++ 2 files changed, 8 insertions(+) diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index d967fb97b..d1eabf2c8 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -126,6 +126,7 @@ describe "AtomEnvironment", -> beforeEach -> errors = [] + spyOn(atom, 'isReleasedVersion').andReturn(true) atom.onDidFailAssertion (error) -> errors.push(error) describe "if the condition is false", -> @@ -147,6 +148,11 @@ describe "AtomEnvironment", -> atom.assert(false, "a == b", {foo: 'bar'}) expect(errors[0].metadata).toEqual {foo: 'bar'} + describe "when Atom has been built from source", -> + it "throws an error", -> + atom.isReleasedVersion.andReturn(false) + expect(-> atom.assert(false, 'testing')).toThrow('Assertion failed: testing') + describe "if the condition is true", -> it "does nothing", -> result = atom.assert(true, "a == b") diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 3133b5af8..dc4318bec 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -840,6 +840,8 @@ class AtomEnvironment extends Model error.metadata = callbackOrMetadata @emitter.emit 'did-fail-assertion', error + unless @isReleasedVersion() + throw error false From d6981dfcab63129143b366efffd964eaa94d76f8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Mar 2017 10:19:48 -0800 Subject: [PATCH 09/55] Avoid throwing intentional errors in DOMElementPool test --- spec/dom-element-pool-spec.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spec/dom-element-pool-spec.js b/spec/dom-element-pool-spec.js index 9de932e27..91120ee48 100644 --- a/spec/dom-element-pool-spec.js +++ b/spec/dom-element-pool-spec.js @@ -3,7 +3,10 @@ const DOMElementPool = require ('../src/dom-element-pool') describe('DOMElementPool', function () { let domElementPool - beforeEach(() => { domElementPool = new DOMElementPool() }) + beforeEach(() => { + domElementPool = new DOMElementPool() + spyOn(atom, 'isReleasedVersion').andReturn(true) + }) it('builds DOM nodes, recycling them when they are freed', function () { let elements From c1d1bbcb18fe74fd3d29f1b859fcf30a67740b9c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Mar 2017 11:54:00 -0800 Subject: [PATCH 10/55] Provide a fresh environment when deserializing in specs --- spec/text-editor-spec.coffee | 8 +++- spec/workspace-spec.coffee | 72 +++++++++++++++++++++++------------- src/text-editor.coffee | 2 +- 3 files changed, 54 insertions(+), 28 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 911270d16..81c69f63f 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -28,7 +28,13 @@ describe "TextEditor", -> editor.foldBufferRow(4) expect(editor.isFoldedAtBufferRow(4)).toBeTruthy() - editor2 = TextEditor.deserialize(editor.serialize(), atom) + editor2 = TextEditor.deserialize(editor.serialize(), { + assert: atom.assert, + textEditors: atom.textEditors, + project: { + bufferForIdSync: (id) -> TextBuffer.deserialize(editor.buffer.serialize()) + } + }) expect(editor2.id).toBe editor.id expect(editor2.getBuffer().getPath()).toBe editor.getBuffer().getPath() diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index 153cc5dc3..8e4da1185 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -7,6 +7,7 @@ platform = require './spec-helper-platform' _ = require 'underscore-plus' fstream = require 'fstream' fs = require 'fs-plus' +AtomEnvironment = require '../src/atom-environment' describe "Workspace", -> [workspace, setDocumentEdited] = [] @@ -865,24 +866,35 @@ describe "Workspace", -> i = /test/; #FIXME """ - state = atom.workspace.serialize() - expect(state.packagesWithActiveGrammars).toEqual ['language-coffee-script', 'language-javascript', 'language-todo'] - - jsPackage = atom.packages.getLoadedPackage('language-javascript') - coffeePackage = atom.packages.getLoadedPackage('language-coffee-script') - spyOn(jsPackage, 'loadGrammarsSync') - spyOn(coffeePackage, 'loadGrammarsSync') - - workspace2 = new Workspace({ - config: atom.config, project: atom.project, packageManager: atom.packages, - notificationManager: atom.notifications, deserializerManager: atom.deserializers, - viewRegistry: atom.views, grammarRegistry: atom.grammars, - applicationDelegate: atom.applicationDelegate, assert: atom.assert.bind(atom), - textEditorRegistry: atom.textEditors + atom2 = new AtomEnvironment({ + applicationDelegate: atom.applicationDelegate, + window: document.createElement('div'), + document: Object.assign( + document.createElement('div'), + { + body: document.createElement('div'), + head: document.createElement('div'), + } + ) }) - workspace2.deserialize(state, atom.deserializers) - expect(jsPackage.loadGrammarsSync.callCount).toBe 1 - expect(coffeePackage.loadGrammarsSync.callCount).toBe 1 + + atom2.packages.loadPackage('language-javascript') + atom2.packages.loadPackage('language-coffee-script') + atom2.packages.loadPackage('language-todo') + atom2.project.deserialize(atom.project.serialize()) + atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers) + + expect(atom2.grammars.getGrammars().map((grammar) -> grammar.name).sort()).toEqual([ + 'CoffeeScript', + 'CoffeeScript (Literate)', + 'JavaScript', + 'Null Grammar', + 'Regular Expression Replacement (JavaScript)', + 'Regular Expressions (JavaScript)', + 'TODO' + ]) + + atom2.destroy() describe "document.title", -> describe "when there is no item open", -> @@ -971,18 +983,26 @@ describe "Workspace", -> it "updates the title to contain the project's path", -> document.title = null - workspace2 = new Workspace({ - config: atom.config, project: atom.project, packageManager: atom.packages, - notificationManager: atom.notifications, deserializerManager: atom.deserializers, - viewRegistry: atom.views, grammarRegistry: atom.grammars, - applicationDelegate: atom.applicationDelegate, assert: atom.assert.bind(atom), - textEditorRegistry: atom.textEditors + + atom2 = new AtomEnvironment({ + applicationDelegate: atom.applicationDelegate, + window: document.createElement('div'), + document: Object.assign( + document.createElement('div'), + { + body: document.createElement('div'), + head: document.createElement('div'), + } + ) }) - workspace2.deserialize(atom.workspace.serialize(), atom.deserializers) - item = workspace2.getActivePaneItem() + + atom2.project.deserialize(atom.project.serialize()) + atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers) + item = atom2.workspace.getActivePaneItem() pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{pathEscaped}/// - workspace2.destroy() + + atom2.destroy() describe "document edited status", -> [item1, item2] = [] diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 1f24fa252..8095632fd 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -387,7 +387,7 @@ class TextEditor extends Model softWrapHangingIndentLength: @displayLayer.softWrapHangingIndent @id, @softTabs, @softWrapped, @softWrapAtPreferredLineLength, - @preferredLineLength, @mini, @editorWidthInChars, @width, @largeFileMode, + @preferredLineLength, @mini, @editorWidthInChars, @width, @largeFileMode, @registered, @invisibles, @showInvisibles, @showIndentGuide, @autoHeight, @autoWidth } From 6262a0cb67bf64e4b9d29d5032be54a58c9d93f3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Mar 2017 12:01:20 -0800 Subject: [PATCH 11/55] :arrow_up: find-and-replace --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 667506303..79e8f5933 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,7 @@ "dev-live-reload": "0.47.0", "encoding-selector": "0.23.2", "exception-reporting": "0.41.1", - "find-and-replace": "0.206.3", + "find-and-replace": "0.207.0", "fuzzy-finder": "1.4.1", "git-diff": "1.3.3", "go-to-line": "0.32.0", From 920655419f20379b4510e3d0ef8ac4909700970a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 2 Mar 2017 16:25:42 -0800 Subject: [PATCH 12/55] :arrow_up: language-gfm --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79e8f5933..10a5f57f6 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "language-coffee-script": "0.48.4", "language-csharp": "0.14.2", "language-css": "0.42.0", - "language-gfm": "0.88.0", + "language-gfm": "0.88.1", "language-git": "0.19.0", "language-go": "0.43.1", "language-html": "0.47.2", From cb71ce64ae57fd40eeef6d522764203349d95230 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 2 Mar 2017 14:31:20 -0800 Subject: [PATCH 13/55] Convert workspace to JavaScript: decaffeinate --- spec/workspace-spec.coffee | 1798 -------------------------------- spec/workspace-spec.js | 1977 ++++++++++++++++++++++++++++++++++++ src/workspace.coffee | 1121 -------------------- src/workspace.js | 1314 ++++++++++++++++++++++++ 4 files changed, 3291 insertions(+), 2919 deletions(-) delete mode 100644 spec/workspace-spec.coffee create mode 100644 spec/workspace-spec.js delete mode 100644 src/workspace.coffee create mode 100644 src/workspace.js diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee deleted file mode 100644 index 8e4da1185..000000000 --- a/spec/workspace-spec.coffee +++ /dev/null @@ -1,1798 +0,0 @@ -path = require 'path' -temp = require('temp').track() -TextEditor = require '../src/text-editor' -Workspace = require '../src/workspace' -Project = require '../src/project' -platform = require './spec-helper-platform' -_ = require 'underscore-plus' -fstream = require 'fstream' -fs = require 'fs-plus' -AtomEnvironment = require '../src/atom-environment' - -describe "Workspace", -> - [workspace, setDocumentEdited] = [] - - beforeEach -> - workspace = atom.workspace - workspace.resetFontSize() - spyOn(atom.applicationDelegate, "confirm") - setDocumentEdited = spyOn(atom.applicationDelegate, 'setWindowDocumentEdited') - atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('dir')]) - waits(1) - - afterEach -> - temp.cleanupSync() - - describe "serialization", -> - simulateReload = -> - workspaceState = atom.workspace.serialize() - projectState = atom.project.serialize({isUnloading: true}) - atom.workspace.destroy() - atom.project.destroy() - atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom), applicationDelegate: atom.applicationDelegate}) - atom.project.deserialize(projectState) - atom.workspace = new Workspace({ - config: atom.config, project: atom.project, packageManager: atom.packages, - grammarRegistry: atom.grammars, deserializerManager: atom.deserializers, - notificationManager: atom.notifications, - applicationDelegate: atom.applicationDelegate, - viewRegistry: atom.views, assert: atom.assert.bind(atom), - textEditorRegistry: atom.textEditors - }) - atom.workspace.deserialize(workspaceState, atom.deserializers) - - describe "when the workspace contains text editors", -> - it "constructs the view with the same panes", -> - pane1 = atom.workspace.getActivePane() - pane2 = pane1.splitRight(copyActiveItem: true) - pane3 = pane2.splitRight(copyActiveItem: true) - pane4 = null - - waitsForPromise -> - atom.workspace.open(null).then (editor) -> editor.setText("An untitled editor.") - - waitsForPromise -> - atom.workspace.open('b').then (editor) -> - pane2.activateItem(editor.copy()) - - waitsForPromise -> - atom.workspace.open('../sample.js').then (editor) -> - pane3.activateItem(editor) - - runs -> - pane3.activeItem.setCursorScreenPosition([2, 4]) - pane4 = pane2.splitDown() - - waitsForPromise -> - atom.workspace.open('../sample.txt').then (editor) -> - pane4.activateItem(editor) - - runs -> - pane4.getActiveItem().setCursorScreenPosition([0, 2]) - pane2.activate() - - simulateReload() - - expect(atom.workspace.getTextEditors().length).toBe 5 - [editor1, editor2, untitledEditor, editor3, editor4] = atom.workspace.getTextEditors() - expect(editor1.getPath()).toBe atom.project.getDirectories()[0]?.resolve('b') - expect(editor2.getPath()).toBe atom.project.getDirectories()[0]?.resolve('../sample.txt') - expect(editor2.getCursorScreenPosition()).toEqual [0, 2] - expect(editor3.getPath()).toBe atom.project.getDirectories()[0]?.resolve('b') - expect(editor4.getPath()).toBe atom.project.getDirectories()[0]?.resolve('../sample.js') - expect(editor4.getCursorScreenPosition()).toEqual [2, 4] - expect(untitledEditor.getPath()).toBeUndefined() - expect(untitledEditor.getText()).toBe("An untitled editor.") - - expect(atom.workspace.getActiveTextEditor().getPath()).toBe editor3.getPath() - pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - expect(document.title).toMatch ///^#{path.basename(editor3.getLongTitle())}\ \u2014\ #{pathEscaped}/// - - describe "where there are no open panes or editors", -> - it "constructs the view with no open editors", -> - atom.workspace.getActivePane().destroy() - expect(atom.workspace.getTextEditors().length).toBe 0 - simulateReload() - expect(atom.workspace.getTextEditors().length).toBe 0 - - describe "::open(uri, options)", -> - openEvents = null - - beforeEach -> - openEvents = [] - workspace.onDidOpen (event) -> openEvents.push(event) - spyOn(workspace.getActivePane(), 'activate').andCallThrough() - - describe "when the 'searchAllPanes' option is false (default)", -> - describe "when called without a uri", -> - it "adds and activates an empty editor on the active pane", -> - [editor1, editor2] = [] - - waitsForPromise -> - workspace.open().then (editor) -> editor1 = editor - - runs -> - expect(editor1.getPath()).toBeUndefined() - expect(workspace.getActivePane().items).toEqual [editor1] - expect(workspace.getActivePaneItem()).toBe editor1 - expect(workspace.getActivePane().activate).toHaveBeenCalled() - expect(openEvents).toEqual [{uri: undefined, pane: workspace.getActivePane(), item: editor1, index: 0}] - openEvents = [] - - waitsForPromise -> - workspace.open().then (editor) -> editor2 = editor - - runs -> - expect(editor2.getPath()).toBeUndefined() - expect(workspace.getActivePane().items).toEqual [editor1, editor2] - expect(workspace.getActivePaneItem()).toBe editor2 - expect(workspace.getActivePane().activate).toHaveBeenCalled() - expect(openEvents).toEqual [{uri: undefined, pane: workspace.getActivePane(), item: editor2, index: 1}] - - describe "when called with a uri", -> - describe "when the active pane already has an editor for the given uri", -> - it "activates the existing editor on the active pane", -> - editor = null - editor1 = null - editor2 = null - - waitsForPromise -> - workspace.open('a').then (o) -> - editor1 = o - workspace.open('b').then (o) -> - editor2 = o - workspace.open('a').then (o) -> - editor = o - - runs -> - expect(editor).toBe editor1 - expect(workspace.getActivePaneItem()).toBe editor - expect(workspace.getActivePane().activate).toHaveBeenCalled() - - expect(openEvents).toEqual [ - { - uri: atom.project.getDirectories()[0]?.resolve('a') - item: editor1 - pane: atom.workspace.getActivePane() - index: 0 - } - { - uri: atom.project.getDirectories()[0]?.resolve('b') - item: editor2 - pane: atom.workspace.getActivePane() - index: 1 - } - { - uri: atom.project.getDirectories()[0]?.resolve('a') - item: editor1 - pane: atom.workspace.getActivePane() - index: 0 - } - ] - - describe "when the active pane does not have an editor for the given uri", -> - it "adds and activates a new editor for the given path on the active pane", -> - editor = null - waitsForPromise -> - workspace.open('a').then (o) -> editor = o - - runs -> - expect(editor.getURI()).toBe atom.project.getDirectories()[0]?.resolve('a') - expect(workspace.getActivePaneItem()).toBe editor - expect(workspace.getActivePane().items).toEqual [editor] - expect(workspace.getActivePane().activate).toHaveBeenCalled() - - describe "when the 'searchAllPanes' option is true", -> - describe "when an editor for the given uri is already open on an inactive pane", -> - it "activates the existing editor on the inactive pane, then activates that pane", -> - editor1 = null - editor2 = null - pane1 = workspace.getActivePane() - pane2 = workspace.getActivePane().splitRight() - - waitsForPromise -> - pane1.activate() - workspace.open('a').then (o) -> editor1 = o - - waitsForPromise -> - pane2.activate() - workspace.open('b').then (o) -> editor2 = o - - runs -> - expect(workspace.getActivePaneItem()).toBe editor2 - - waitsForPromise -> - workspace.open('a', searchAllPanes: true) - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(workspace.getActivePaneItem()).toBe editor1 - - describe "when no editor for the given uri is open in any pane", -> - it "opens an editor for the given uri in the active pane", -> - editor = null - waitsForPromise -> - workspace.open('a', searchAllPanes: true).then (o) -> editor = o - - runs -> - expect(workspace.getActivePaneItem()).toBe editor - - describe "when the 'split' option is set", -> - describe "when the 'split' option is 'left'", -> - it "opens the editor in the leftmost pane of the current pane axis", -> - pane1 = workspace.getActivePane() - pane2 = pane1.splitRight() - expect(workspace.getActivePane()).toBe pane2 - - editor = null - waitsForPromise -> - workspace.open('a', split: 'left').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(pane1.items).toEqual [editor] - expect(pane2.items).toEqual [] - - # Focus right pane and reopen the file on the left - waitsForPromise -> - pane2.focus() - workspace.open('a', split: 'left').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(pane1.items).toEqual [editor] - expect(pane2.items).toEqual [] - - describe "when a pane axis is the leftmost sibling of the current pane", -> - it "opens the new item in the current pane", -> - editor = null - pane1 = workspace.getActivePane() - pane2 = pane1.splitLeft() - pane3 = pane2.splitDown() - pane1.activate() - expect(workspace.getActivePane()).toBe pane1 - - waitsForPromise -> - workspace.open('a', split: 'left').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(pane1.items).toEqual [editor] - - describe "when the 'split' option is 'right'", -> - it "opens the editor in the rightmost pane of the current pane axis", -> - editor = null - pane1 = workspace.getActivePane() - pane2 = null - waitsForPromise -> - workspace.open('a', split: 'right').then (o) -> editor = o - - runs -> - pane2 = workspace.getPanes().filter((p) -> p isnt pane1)[0] - expect(workspace.getActivePane()).toBe pane2 - expect(pane1.items).toEqual [] - expect(pane2.items).toEqual [editor] - - # Focus right pane and reopen the file on the right - waitsForPromise -> - pane1.focus() - workspace.open('a', split: 'right').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane2 - expect(pane1.items).toEqual [] - expect(pane2.items).toEqual [editor] - - describe "when a pane axis is the rightmost sibling of the current pane", -> - it "opens the new item in a new pane split to the right of the current pane", -> - editor = null - pane1 = workspace.getActivePane() - pane2 = pane1.splitRight() - pane3 = pane2.splitDown() - pane1.activate() - expect(workspace.getActivePane()).toBe pane1 - pane4 = null - - waitsForPromise -> - workspace.open('a', split: 'right').then (o) -> editor = o - - runs -> - pane4 = workspace.getPanes().filter((p) -> p isnt pane1)[0] - expect(workspace.getActivePane()).toBe pane4 - expect(pane4.items).toEqual [editor] - expect(workspace.paneContainer.root.children[0]).toBe pane1 - expect(workspace.paneContainer.root.children[1]).toBe pane4 - - describe "when the 'split' option is 'up'", -> - it "opens the editor in the topmost pane of the current pane axis", -> - pane1 = workspace.getActivePane() - pane2 = pane1.splitDown() - expect(workspace.getActivePane()).toBe pane2 - - editor = null - waitsForPromise -> - workspace.open('a', split: 'up').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(pane1.items).toEqual [editor] - expect(pane2.items).toEqual [] - - # Focus bottom pane and reopen the file on the top - waitsForPromise -> - pane2.focus() - workspace.open('a', split: 'up').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(pane1.items).toEqual [editor] - expect(pane2.items).toEqual [] - - describe "when a pane axis is the topmost sibling of the current pane", -> - it "opens the new item in the current pane", -> - editor = null - pane1 = workspace.getActivePane() - pane2 = pane1.splitUp() - pane3 = pane2.splitRight() - pane1.activate() - expect(workspace.getActivePane()).toBe pane1 - - waitsForPromise -> - workspace.open('a', split: 'up').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane1 - expect(pane1.items).toEqual [editor] - - describe "when the 'split' option is 'down'", -> - it "opens the editor in the bottommost pane of the current pane axis", -> - editor = null - pane1 = workspace.getActivePane() - pane2 = null - waitsForPromise -> - workspace.open('a', split: 'down').then (o) -> editor = o - - runs -> - pane2 = workspace.getPanes().filter((p) -> p isnt pane1)[0] - expect(workspace.getActivePane()).toBe pane2 - expect(pane1.items).toEqual [] - expect(pane2.items).toEqual [editor] - - # Focus bottom pane and reopen the file on the right - waitsForPromise -> - pane1.focus() - workspace.open('a', split: 'down').then (o) -> editor = o - - runs -> - expect(workspace.getActivePane()).toBe pane2 - expect(pane1.items).toEqual [] - expect(pane2.items).toEqual [editor] - - describe "when a pane axis is the bottommost sibling of the current pane", -> - it "opens the new item in a new pane split to the bottom of the current pane", -> - editor = null - pane1 = workspace.getActivePane() - pane2 = pane1.splitDown() - pane1.activate() - expect(workspace.getActivePane()).toBe pane1 - pane4 = null - - waitsForPromise -> - workspace.open('a', split: 'down').then (o) -> editor = o - - runs -> - pane4 = workspace.getPanes().filter((p) -> p isnt pane1)[0] - expect(workspace.getActivePane()).toBe pane4 - expect(pane4.items).toEqual [editor] - expect(workspace.paneContainer.root.children[0]).toBe pane1 - expect(workspace.paneContainer.root.children[1]).toBe pane2 - - describe "when an initialLine and initialColumn are specified", -> - it "moves the cursor to the indicated location", -> - waitsForPromise -> - workspace.open('a', initialLine: 1, initialColumn: 5) - - runs -> - expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual [1, 5] - - waitsForPromise -> - workspace.open('a', initialLine: 2, initialColumn: 4) - - runs -> - expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual [2, 4] - - waitsForPromise -> - workspace.open('a', initialLine: 0, initialColumn: 0) - - runs -> - expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual [0, 0] - - waitsForPromise -> - workspace.open('a', initialLine: NaN, initialColumn: 4) - - runs -> - expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual [0, 4] - - waitsForPromise -> - workspace.open('a', initialLine: 2, initialColumn: NaN) - - runs -> - expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual [2, 0] - - waitsForPromise -> - workspace.open('a', initialLine: Infinity, initialColumn: Infinity) - - runs -> - expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual [2, 11] - - describe "when the file is over 2MB", -> - it "opens the editor with largeFileMode: true", -> - spyOn(fs, 'getSizeSync').andReturn 2 * 1048577 # 2MB - - editor = null - waitsForPromise -> - workspace.open('sample.js').then (e) -> editor = e - - runs -> - expect(editor.largeFileMode).toBe true - - describe "when the file is over user-defined limit", -> - shouldPromptForFileOfSize = (size, shouldPrompt) -> - spyOn(fs, 'getSizeSync').andReturn size * 1048577 - atom.applicationDelegate.confirm.andCallFake -> selectedButtonIndex - atom.applicationDelegate.confirm() - selectedButtonIndex = 1 # cancel - - editor = null - waitsForPromise -> - workspace.open('sample.js').then (e) -> editor = e - if shouldPrompt - runs -> - expect(editor).toBeUndefined() - expect(atom.applicationDelegate.confirm).toHaveBeenCalled() - - atom.applicationDelegate.confirm.reset() - selectedButtonIndex = 0 # open the file - - waitsForPromise -> - workspace.open('sample.js').then (e) -> editor = e - - runs -> - expect(atom.applicationDelegate.confirm).toHaveBeenCalled() - expect(editor.largeFileMode).toBe true - else - runs -> - expect(editor).not.toBeUndefined() - - it "prompts the user to make sure they want to open a file this big", -> - atom.config.set "core.warnOnLargeFileLimit", 20 - shouldPromptForFileOfSize 20, true - - it "doesn't prompt on files below the limit", -> - atom.config.set "core.warnOnLargeFileLimit", 30 - shouldPromptForFileOfSize 20, false - - it "prompts for smaller files with a lower limit", -> - atom.config.set "core.warnOnLargeFileLimit", 5 - shouldPromptForFileOfSize 10, true - - describe "when passed a path that matches a custom opener", -> - it "returns the resource returned by the custom opener", -> - fooOpener = (pathToOpen, options) -> {foo: pathToOpen, options} if pathToOpen?.match(/\.foo/) - barOpener = (pathToOpen) -> {bar: pathToOpen} if pathToOpen?.match(/^bar:\/\//) - workspace.addOpener(fooOpener) - workspace.addOpener(barOpener) - - waitsForPromise -> - pathToOpen = atom.project.getDirectories()[0]?.resolve('a.foo') - workspace.open(pathToOpen, hey: "there").then (item) -> - expect(item).toEqual {foo: pathToOpen, options: {hey: "there"}} - - waitsForPromise -> - workspace.open("bar://baz").then (item) -> - expect(item).toEqual {bar: "bar://baz"} - - it "adds the file to the application's recent documents list", -> - return unless process.platform is 'darwin' # Feature only supported on macOS - spyOn(atom.applicationDelegate, 'addRecentDocument') - - waitsForPromise -> - workspace.open() - - runs -> - expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled() - - waitsForPromise -> - workspace.open('something://a/url') - - runs -> - expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled() - - waitsForPromise -> - workspace.open(__filename) - - runs -> - expect(atom.applicationDelegate.addRecentDocument).toHaveBeenCalledWith(__filename) - - it "notifies ::onDidAddTextEditor observers", -> - absolutePath = require.resolve('./fixtures/dir/a') - newEditorHandler = jasmine.createSpy('newEditorHandler') - workspace.onDidAddTextEditor newEditorHandler - - editor = null - waitsForPromise -> - workspace.open(absolutePath).then (e) -> editor = e - - runs -> - expect(newEditorHandler.argsForCall[0][0].textEditor).toBe editor - - describe "when there is an error opening the file", -> - notificationSpy = null - beforeEach -> - atom.notifications.onDidAddNotification notificationSpy = jasmine.createSpy() - - describe "when a file does not exist", -> - it "creates an empty buffer for the specified path", -> - waitsForPromise -> - workspace.open('not-a-file.md') - - runs -> - editor = workspace.getActiveTextEditor() - expect(notificationSpy).not.toHaveBeenCalled() - expect(editor.getPath()).toContain 'not-a-file.md' - - describe "when the user does not have access to the file", -> - beforeEach -> - spyOn(fs, 'openSync').andCallFake (path) -> - error = new Error("EACCES, permission denied '#{path}'") - error.path = path - error.code = 'EACCES' - throw error - - it "creates a notification", -> - waitsForPromise -> - workspace.open('file1') - - runs -> - expect(notificationSpy).toHaveBeenCalled() - notification = notificationSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'warning' - expect(notification.getMessage()).toContain 'Permission denied' - expect(notification.getMessage()).toContain 'file1' - - describe "when the the operation is not permitted", -> - beforeEach -> - spyOn(fs, 'openSync').andCallFake (path) -> - error = new Error("EPERM, operation not permitted '#{path}'") - error.path = path - error.code = 'EPERM' - throw error - - it "creates a notification", -> - waitsForPromise -> - workspace.open('file1') - - runs -> - expect(notificationSpy).toHaveBeenCalled() - notification = notificationSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'warning' - expect(notification.getMessage()).toContain 'Unable to open' - expect(notification.getMessage()).toContain 'file1' - - describe "when the the file is already open in windows", -> - beforeEach -> - spyOn(fs, 'openSync').andCallFake (path) -> - error = new Error("EBUSY, resource busy or locked '#{path}'") - error.path = path - error.code = 'EBUSY' - throw error - - it "creates a notification", -> - waitsForPromise -> - workspace.open('file1') - - runs -> - expect(notificationSpy).toHaveBeenCalled() - notification = notificationSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'warning' - expect(notification.getMessage()).toContain 'Unable to open' - expect(notification.getMessage()).toContain 'file1' - - describe "when there is an unhandled error", -> - beforeEach -> - spyOn(fs, 'openSync').andCallFake (path) -> - throw new Error("I dont even know what is happening right now!!") - - it "creates a notification", -> - open = -> workspace.open('file1', workspace.getActivePane()) - expect(open).toThrow() - - describe "when the file is already open in pending state", -> - it "should terminate the pending state", -> - editor = null - pane = null - - waitsForPromise -> - atom.workspace.open('sample.js', pending: true).then (o) -> - editor = o - pane = atom.workspace.getActivePane() - - runs -> - expect(pane.getPendingItem()).toEqual editor - - waitsForPromise -> - atom.workspace.open('sample.js') - - runs -> - expect(pane.getPendingItem()).toBeNull() - - describe "when opening will switch from a pending tab to a permanent tab", -> - it "keeps the pending tab open", -> - editor1 = null - editor2 = null - - waitsForPromise -> - atom.workspace.open('sample.txt').then (o) -> - editor1 = o - - waitsForPromise -> - atom.workspace.open('sample2.txt', pending: true).then (o) -> - editor2 = o - - runs -> - pane = atom.workspace.getActivePane() - pane.activateItem(editor1) - expect(pane.getItems().length).toBe 2 - expect(pane.getItems()).toEqual [editor1, editor2] - - describe "when replacing a pending item which is the last item in a second pane", -> - it "does not destroy the pane even if core.destroyEmptyPanes is on", -> - atom.config.set('core.destroyEmptyPanes', true) - editor1 = null - editor2 = null - leftPane = atom.workspace.getActivePane() - rightPane = null - - waitsForPromise -> - atom.workspace.open('sample.js', pending: true, split: 'right').then (o) -> - editor1 = o - rightPane = atom.workspace.getActivePane() - spyOn rightPane, "destroyed" - - runs -> - expect(leftPane).not.toBe rightPane - expect(atom.workspace.getActivePane()).toBe rightPane - expect(atom.workspace.getActivePane().getItems().length).toBe 1 - expect(rightPane.getPendingItem()).toBe editor1 - - waitsForPromise -> - atom.workspace.open('sample.txt', pending: true).then (o) -> - editor2 = o - - runs -> - expect(rightPane.getPendingItem()).toBe editor2 - expect(rightPane.destroyed.callCount).toBe 0 - - describe 'the grammar-used hook', -> - it 'fires when opening a file or changing the grammar of an open file', -> - editor = null - javascriptGrammarUsed = false - coffeescriptGrammarUsed = false - - atom.packages.triggerDeferredActivationHooks() - - runs -> - atom.packages.onDidTriggerActivationHook 'language-javascript:grammar-used', -> javascriptGrammarUsed = true - atom.packages.onDidTriggerActivationHook 'language-coffee-script:grammar-used', -> coffeescriptGrammarUsed = true - - waitsForPromise -> - atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor = o - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - waitsFor -> javascriptGrammarUsed - - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - runs -> - editor.setGrammar(atom.grammars.selectGrammar('.coffee')) - - waitsFor -> coffeescriptGrammarUsed - - describe "::reopenItem()", -> - it "opens the uri associated with the last closed pane that isn't currently open", -> - pane = workspace.getActivePane() - waitsForPromise -> - workspace.open('a').then -> - workspace.open('b').then -> - workspace.open('file1').then -> - workspace.open() - - runs -> - # does not reopen items with no uri - expect(workspace.getActivePaneItem().getURI()).toBeUndefined() - pane.destroyActiveItem() - - waitsForPromise -> - workspace.reopenItem() - - runs -> - expect(workspace.getActivePaneItem().getURI()).not.toBeUndefined() - - # destroy all items - expect(workspace.getActivePaneItem().getURI()).toBe atom.project.getDirectories()[0]?.resolve('file1') - pane.destroyActiveItem() - expect(workspace.getActivePaneItem().getURI()).toBe atom.project.getDirectories()[0]?.resolve('b') - pane.destroyActiveItem() - expect(workspace.getActivePaneItem().getURI()).toBe atom.project.getDirectories()[0]?.resolve('a') - pane.destroyActiveItem() - - # reopens items with uris - expect(workspace.getActivePaneItem()).toBeUndefined() - - waitsForPromise -> - workspace.reopenItem() - - runs -> - expect(workspace.getActivePaneItem().getURI()).toBe atom.project.getDirectories()[0]?.resolve('a') - - # does not reopen items that are already open - waitsForPromise -> - workspace.open('b') - - runs -> - expect(workspace.getActivePaneItem().getURI()).toBe atom.project.getDirectories()[0]?.resolve('b') - - waitsForPromise -> - workspace.reopenItem() - - runs -> - expect(workspace.getActivePaneItem().getURI()).toBe atom.project.getDirectories()[0]?.resolve('file1') - - describe "::increase/decreaseFontSize()", -> - it "increases/decreases the font size without going below 1", -> - atom.config.set('editor.fontSize', 1) - workspace.increaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe 2 - workspace.increaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe 3 - workspace.decreaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe 2 - workspace.decreaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe 1 - workspace.decreaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe 1 - - describe "::resetFontSize()", -> - it "resets the font size to the window's starting font size", -> - originalFontSize = atom.config.get('editor.fontSize') - - workspace.increaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize + 1 - workspace.resetFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize - workspace.decreaseFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize - 1 - workspace.resetFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize - - it "does nothing if the font size has not been changed", -> - originalFontSize = atom.config.get('editor.fontSize') - - workspace.resetFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize - - it "resets the font size when the editor's font size changes", -> - originalFontSize = atom.config.get('editor.fontSize') - - atom.config.set('editor.fontSize', originalFontSize + 1) - workspace.resetFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize - atom.config.set('editor.fontSize', originalFontSize - 1) - workspace.resetFontSize() - expect(atom.config.get('editor.fontSize')).toBe originalFontSize - - describe "::openLicense()", -> - it "opens the license as plain-text in a buffer", -> - waitsForPromise -> workspace.openLicense() - runs -> expect(workspace.getActivePaneItem().getText()).toMatch /Copyright/ - - describe "::isTextEditor(obj)", -> - it "returns true when the passed object is an instance of `TextEditor`", -> - expect(workspace.isTextEditor(new TextEditor)).toBe(true) - expect(workspace.isTextEditor({getText: -> null})).toBe(false) - expect(workspace.isTextEditor(null)).toBe(false) - expect(workspace.isTextEditor(undefined)).toBe(false) - - describe "::observeTextEditors()", -> - it "invokes the observer with current and future text editors", -> - observed = [] - - waitsForPromise -> workspace.open() - waitsForPromise -> workspace.open() - waitsForPromise -> workspace.openLicense() - - runs -> - workspace.observeTextEditors (editor) -> observed.push(editor) - - waitsForPromise -> workspace.open() - - expect(observed).toEqual workspace.getTextEditors() - - describe "when an editor is destroyed", -> - it "removes the editor", -> - editor = null - - waitsForPromise -> - workspace.open("a").then (e) -> editor = e - - runs -> - expect(workspace.getTextEditors()).toHaveLength 1 - editor.destroy() - expect(workspace.getTextEditors()).toHaveLength 0 - - describe "when an editor is copied because its pane is split", -> - it "sets up the new editor to be configured by the text editor registry", -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - waitsForPromise -> - workspace.open('a').then (editor) -> - atom.textEditors.setGrammarOverride(editor, 'source.js') - expect(editor.getGrammar().name).toBe('JavaScript') - - workspace.getActivePane().splitRight(copyActiveItem: true) - newEditor = workspace.getActiveTextEditor() - expect(newEditor).not.toBe(editor) - expect(newEditor.getGrammar().name).toBe('JavaScript') - - it "stores the active grammars used by all the open editors", -> - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - waitsForPromise -> - atom.packages.activatePackage('language-todo') - - waitsForPromise -> - atom.workspace.open('sample.coffee') - - runs -> - atom.workspace.getActiveTextEditor().setText """ - i = /test/; #FIXME - """ - - atom2 = new AtomEnvironment({ - applicationDelegate: atom.applicationDelegate, - window: document.createElement('div'), - document: Object.assign( - document.createElement('div'), - { - body: document.createElement('div'), - head: document.createElement('div'), - } - ) - }) - - atom2.packages.loadPackage('language-javascript') - atom2.packages.loadPackage('language-coffee-script') - atom2.packages.loadPackage('language-todo') - atom2.project.deserialize(atom.project.serialize()) - atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers) - - expect(atom2.grammars.getGrammars().map((grammar) -> grammar.name).sort()).toEqual([ - 'CoffeeScript', - 'CoffeeScript (Literate)', - 'JavaScript', - 'Null Grammar', - 'Regular Expression Replacement (JavaScript)', - 'Regular Expressions (JavaScript)', - 'TODO' - ]) - - atom2.destroy() - - describe "document.title", -> - describe "when there is no item open", -> - it "sets the title to the project path", -> - expect(document.title).toMatch escapeStringRegex(fs.tildify(atom.project.getPaths()[0])) - - it "sets the title to 'untitled' if there is no project path", -> - atom.project.setPaths([]) - expect(document.title).toMatch /^untitled/ - - describe "when the active pane item's path is not inside a project path", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('b').then -> - atom.project.setPaths([]) - - it "sets the title to the pane item's title plus the item's path", -> - item = atom.workspace.getActivePaneItem() - pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) - expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// - - describe "when the title of the active pane item changes", -> - it "updates the window title based on the item's new title", -> - editor = atom.workspace.getActivePaneItem() - editor.buffer.setPath(path.join(temp.dir, 'hi')) - pathEscaped = fs.tildify(escapeStringRegex(path.dirname(editor.getPath()))) - expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{pathEscaped}/// - - describe "when the active pane's item changes", -> - it "updates the title to the new item's title plus the project path", -> - atom.workspace.getActivePane().activateNextItem() - item = atom.workspace.getActivePaneItem() - pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) - expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// - - describe "when an inactive pane's item changes", -> - it "does not update the title", -> - pane = atom.workspace.getActivePane() - pane.splitRight() - initialTitle = document.title - pane.activateNextItem() - expect(document.title).toBe initialTitle - - describe "when the active pane item is inside a project path", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('b') - - describe "when there is an active pane item", -> - it "sets the title to the pane item's title plus the project path", -> - item = atom.workspace.getActivePaneItem() - pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// - - describe "when the title of the active pane item changes", -> - it "updates the window title based on the item's new title", -> - editor = atom.workspace.getActivePaneItem() - editor.buffer.setPath(path.join(atom.project.getPaths()[0], 'hi')) - pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - expect(document.title).toMatch ///^#{editor.getTitle()}\ \u2014\ #{pathEscaped}/// - - describe "when the active pane's item changes", -> - it "updates the title to the new item's title plus the project path", -> - atom.workspace.getActivePane().activateNextItem() - item = atom.workspace.getActivePaneItem() - pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - expect(document.title).toMatch ///^#{item.getTitle()}\ \u2014\ #{pathEscaped}/// - - describe "when the last pane item is removed", -> - it "updates the title to the project's first path", -> - atom.workspace.getActivePane().destroy() - expect(atom.workspace.getActivePaneItem()).toBeUndefined() - expect(document.title).toMatch escapeStringRegex(fs.tildify(atom.project.getPaths()[0])) - - describe "when an inactive pane's item changes", -> - it "does not update the title", -> - pane = atom.workspace.getActivePane() - pane.splitRight() - initialTitle = document.title - pane.activateNextItem() - expect(document.title).toBe initialTitle - - describe "when the workspace is deserialized", -> - beforeEach -> - waitsForPromise -> atom.workspace.open('a') - - it "updates the title to contain the project's path", -> - document.title = null - - atom2 = new AtomEnvironment({ - applicationDelegate: atom.applicationDelegate, - window: document.createElement('div'), - document: Object.assign( - document.createElement('div'), - { - body: document.createElement('div'), - head: document.createElement('div'), - } - ) - }) - - atom2.project.deserialize(atom.project.serialize()) - atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers) - item = atom2.workspace.getActivePaneItem() - pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - expect(document.title).toMatch ///^#{item.getLongTitle()}\ \u2014\ #{pathEscaped}/// - - atom2.destroy() - - describe "document edited status", -> - [item1, item2] = [] - - beforeEach -> - waitsForPromise -> atom.workspace.open('a') - waitsForPromise -> atom.workspace.open('b') - runs -> - [item1, item2] = atom.workspace.getPaneItems() - - it "calls setDocumentEdited when the active item changes", -> - expect(atom.workspace.getActivePaneItem()).toBe item2 - item1.insertText('a') - expect(item1.isModified()).toBe true - atom.workspace.getActivePane().activateNextItem() - - expect(setDocumentEdited).toHaveBeenCalledWith(true) - - it "calls atom.setDocumentEdited when the active item's modified status changes", -> - expect(atom.workspace.getActivePaneItem()).toBe item2 - item2.insertText('a') - advanceClock(item2.getBuffer().getStoppedChangingDelay()) - - expect(item2.isModified()).toBe true - expect(setDocumentEdited).toHaveBeenCalledWith(true) - - item2.undo() - advanceClock(item2.getBuffer().getStoppedChangingDelay()) - - expect(item2.isModified()).toBe false - expect(setDocumentEdited).toHaveBeenCalledWith(false) - - describe "adding panels", -> - class TestItem - - class TestItemElement extends HTMLElement - constructor: -> - initialize: (@model) -> this - getModel: -> @model - - beforeEach -> - atom.views.addViewProvider TestItem, (model) -> - new TestItemElement().initialize(model) - - describe '::addLeftPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getLeftPanels().length).toBe(0) - atom.workspace.panelContainers.left.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addLeftPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getLeftPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe '::addRightPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getRightPanels().length).toBe(0) - atom.workspace.panelContainers.right.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addRightPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getRightPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe '::addTopPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getTopPanels().length).toBe(0) - atom.workspace.panelContainers.top.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addTopPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getTopPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe '::addBottomPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getBottomPanels().length).toBe(0) - atom.workspace.panelContainers.bottom.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addBottomPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getBottomPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe '::addHeaderPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getHeaderPanels().length).toBe(0) - atom.workspace.panelContainers.header.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addHeaderPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getHeaderPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe '::addFooterPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getFooterPanels().length).toBe(0) - atom.workspace.panelContainers.footer.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addFooterPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getFooterPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe '::addModalPanel(model)', -> - it 'adds a panel to the correct panel container', -> - expect(atom.workspace.getModalPanels().length).toBe(0) - atom.workspace.panelContainers.modal.onDidAddPanel addPanelSpy = jasmine.createSpy() - - model = new TestItem - panel = atom.workspace.addModalPanel(item: model) - - expect(panel).toBeDefined() - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - - itemView = atom.views.getView(atom.workspace.getModalPanels()[0].getItem()) - expect(itemView instanceof TestItemElement).toBe(true) - expect(itemView.getModel()).toBe(model) - - describe "::panelForItem(item)", -> - it "returns the panel associated with the item", -> - item = new TestItem - panel = atom.workspace.addLeftPanel(item: item) - - itemWithNoPanel = new TestItem - - expect(atom.workspace.panelForItem(item)).toBe panel - expect(atom.workspace.panelForItem(itemWithNoPanel)).toBe null - - describe "::scan(regex, options, callback)", -> - describe "when called with a regex", -> - it "calls the callback with all regex results in all files in the project", -> - results = [] - waitsForPromise -> - atom.workspace.scan /(a)+/, (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength(3) - expect(results[0].filePath).toBe atom.project.getDirectories()[0]?.resolve('a') - expect(results[0].matches).toHaveLength(3) - expect(results[0].matches[0]).toEqual - matchText: 'aaa' - lineText: 'aaa bbb' - lineTextOffset: 0 - range: [[0, 0], [0, 3]] - - it "works with with escaped literals (like $ and ^)", -> - results = [] - waitsForPromise -> - atom.workspace.scan /\$\w+/, (result) -> results.push(result) - - runs -> - expect(results.length).toBe 1 - - {filePath, matches} = results[0] - expect(filePath).toBe atom.project.getDirectories()[0]?.resolve('a') - expect(matches).toHaveLength 1 - expect(matches[0]).toEqual - matchText: '$bill' - lineText: 'dollar$bill' - lineTextOffset: 0 - range: [[2, 6], [2, 11]] - - it "works on evil filenames", -> - atom.config.set('core.excludeVcsIgnoredPaths', false) - platform.generateEvilFiles() - atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')]) - paths = [] - matches = [] - waitsForPromise -> - atom.workspace.scan /evil/, (result) -> - paths.push(result.filePath) - matches = matches.concat(result.matches) - - runs -> - _.each(matches, (m) -> expect(m.matchText).toEqual 'evil') - - if platform.isWindows() - expect(paths.length).toBe 3 - expect(paths[0]).toMatch /a_file_with_utf8.txt$/ - expect(paths[1]).toMatch /file with spaces.txt$/ - expect(path.basename(paths[2])).toBe "utfa\u0306.md" - else - expect(paths.length).toBe 5 - expect(paths[0]).toMatch /a_file_with_utf8.txt$/ - expect(paths[1]).toMatch /file with spaces.txt$/ - expect(paths[2]).toMatch /goddam\nnewlines$/m - expect(paths[3]).toMatch /quote".txt$/m - expect(path.basename(paths[4])).toBe "utfa\u0306.md" - - it "ignores case if the regex includes the `i` flag", -> - results = [] - waitsForPromise -> - atom.workspace.scan /DOLLAR/i, (result) -> results.push(result) - - runs -> - expect(results).toHaveLength 1 - - describe "when the core.excludeVcsIgnoredPaths config is truthy", -> - [projectPath, ignoredPath] = [] - - beforeEach -> - sourceProjectPath = path.join(__dirname, 'fixtures', 'git', 'working-dir') - projectPath = path.join(temp.mkdirSync("atom")) - - writerStream = fstream.Writer(projectPath) - fstream.Reader(sourceProjectPath).pipe(writerStream) - - waitsFor (done) -> - writerStream.on 'close', done - writerStream.on 'error', done - - runs -> - fs.rename(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) - ignoredPath = path.join(projectPath, 'ignored.txt') - fs.writeFileSync(ignoredPath, 'this match should not be included') - - afterEach -> - fs.removeSync(projectPath) if fs.existsSync(projectPath) - - it "excludes ignored files", -> - atom.project.setPaths([projectPath]) - atom.config.set('core.excludeVcsIgnoredPaths', true) - resultHandler = jasmine.createSpy("result found") - waitsForPromise -> - atom.workspace.scan /match/, (results) -> - resultHandler() - - runs -> - expect(resultHandler).not.toHaveBeenCalled() - - it "includes only files when a directory filter is specified", -> - projectPath = path.join(path.join(__dirname, 'fixtures', 'dir')) - atom.project.setPaths([projectPath]) - - filePath = path.join(projectPath, 'a-dir', 'oh-git') - - paths = [] - matches = [] - waitsForPromise -> - atom.workspace.scan /aaa/, paths: ["a-dir#{path.sep}"], (result) -> - paths.push(result.filePath) - matches = matches.concat(result.matches) - - runs -> - expect(paths.length).toBe 1 - expect(paths[0]).toBe filePath - expect(matches.length).toBe 1 - - it "includes files and folders that begin with a '.'", -> - projectPath = temp.mkdirSync('atom-spec-workspace') - filePath = path.join(projectPath, '.text') - fs.writeFileSync(filePath, 'match this') - atom.project.setPaths([projectPath]) - paths = [] - matches = [] - waitsForPromise -> - atom.workspace.scan /match this/, (result) -> - paths.push(result.filePath) - matches = matches.concat(result.matches) - - runs -> - expect(paths.length).toBe 1 - expect(paths[0]).toBe filePath - expect(matches.length).toBe 1 - - it "excludes values in core.ignoredNames", -> - ignoredNames = atom.config.get("core.ignoredNames") - ignoredNames.push("a") - atom.config.set("core.ignoredNames", ignoredNames) - - resultHandler = jasmine.createSpy("result found") - waitsForPromise -> - atom.workspace.scan /dollar/, (results) -> - resultHandler() - - runs -> - expect(resultHandler).not.toHaveBeenCalled() - - it "scans buffer contents if the buffer is modified", -> - editor = null - results = [] - - waitsForPromise -> - atom.workspace.open('a').then (o) -> - editor = o - editor.setText("Elephant") - - waitsForPromise -> - atom.workspace.scan /a|Elephant/, (result) -> results.push result - - runs -> - expect(results).toHaveLength 3 - resultForA = _.find results, ({filePath}) -> path.basename(filePath) is 'a' - expect(resultForA.matches).toHaveLength 1 - expect(resultForA.matches[0].matchText).toBe 'Elephant' - - it "ignores buffers outside the project", -> - editor = null - results = [] - - waitsForPromise -> - atom.workspace.open(temp.openSync().path).then (o) -> - editor = o - editor.setText("Elephant") - - waitsForPromise -> - atom.workspace.scan /Elephant/, (result) -> results.push result - - runs -> - expect(results).toHaveLength 0 - - describe "when the project has multiple root directories", -> - [dir1, dir2, file1, file2] = [] - - beforeEach -> - [dir1] = atom.project.getPaths() - file1 = path.join(dir1, "a-dir", "oh-git") - - dir2 = temp.mkdirSync("a-second-dir") - aDir2 = path.join(dir2, "a-dir") - file2 = path.join(aDir2, "a-file") - fs.mkdirSync(aDir2) - fs.writeFileSync(file2, "ccc aaaa") - - atom.project.addPath(dir2) - - it "searches matching files in all of the project's root directories", -> - resultPaths = [] - waitsForPromise -> - atom.workspace.scan /aaaa/, ({filePath}) -> - resultPaths.push(filePath) - - runs -> - expect(resultPaths.sort()).toEqual([file1, file2].sort()) - - describe "when an inclusion path starts with the basename of a root directory", -> - it "interprets the inclusion path as starting from that directory", -> - waitsForPromise -> - resultPaths = [] - atom.workspace - .scan /aaaa/, paths: ["dir"], ({filePath}) -> - resultPaths.push(filePath) unless filePath in resultPaths - .then -> - expect(resultPaths).toEqual([file1]) - - waitsForPromise -> - resultPaths = [] - atom.workspace - .scan /aaaa/, paths: [path.join("dir", "a-dir")], ({filePath}) -> - resultPaths.push(filePath) unless filePath in resultPaths - .then -> - expect(resultPaths).toEqual([file1]) - - waitsForPromise -> - resultPaths = [] - atom.workspace - .scan /aaaa/, paths: [path.basename(dir2)], ({filePath}) -> - resultPaths.push(filePath) unless filePath in resultPaths - .then -> - expect(resultPaths).toEqual([file2]) - - waitsForPromise -> - resultPaths = [] - atom.workspace - .scan /aaaa/, paths: [path.join(path.basename(dir2), "a-dir")], ({filePath}) -> - resultPaths.push(filePath) unless filePath in resultPaths - .then -> - expect(resultPaths).toEqual([file2]) - - describe "when a custom directory searcher is registered", -> - fakeSearch = null - # Function that is invoked once all of the fields on fakeSearch are set. - onFakeSearchCreated = null - - class FakeSearch - constructor: (@options) -> - # Note that hoisting resolve and reject in this way is generally frowned upon. - @promise = new Promise (resolve, reject) => - @hoistedResolve = resolve - @hoistedReject = reject - onFakeSearchCreated?(this) - then: (args...) -> - @promise.then.apply(@promise, args) - cancel: -> - @cancelled = true - # According to the spec for a DirectorySearcher, invoking `cancel()` should - # resolve the thenable rather than reject it. - @hoistedResolve() - - beforeEach -> - fakeSearch = null - onFakeSearchCreated = null - atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { - canSearchDirectory: (directory) -> directory.getPath() is dir1 - search: (directory, regex, options) -> fakeSearch = new FakeSearch(options) - }) - - waitsFor -> - atom.workspace.directorySearchers.length > 0 - - it "can override the DefaultDirectorySearcher on a per-directory basis", -> - foreignFilePath = 'ssh://foreign-directory:8080/hello.txt' - numPathsSearchedInDir2 = 1 - numPathsToPretendToSearchInCustomDirectorySearcher = 10 - searchResult = - filePath: foreignFilePath, - matches: [ - { - lineText: 'Hello world', - lineTextOffset: 0, - matchText: 'Hello', - range: [[0, 0], [0, 5]], - }, - ] - onFakeSearchCreated = (fakeSearch) -> - fakeSearch.options.didMatch(searchResult) - fakeSearch.options.didSearchPaths(numPathsToPretendToSearchInCustomDirectorySearcher) - fakeSearch.hoistedResolve() - - resultPaths = [] - onPathsSearched = jasmine.createSpy('onPathsSearched') - waitsForPromise -> - atom.workspace.scan /aaaa/, {onPathsSearched}, ({filePath}) -> - resultPaths.push(filePath) - - runs -> - expect(resultPaths.sort()).toEqual([foreignFilePath, file2].sort()) - # onPathsSearched should be called once by each DirectorySearcher. The order is not - # guaranteed, so we can only verify the total number of paths searched is correct - # after the second call. - expect(onPathsSearched.callCount).toBe(2) - expect(onPathsSearched.mostRecentCall.args[0]).toBe( - numPathsToPretendToSearchInCustomDirectorySearcher + numPathsSearchedInDir2) - - it "can be cancelled when the object returned by scan() has its cancel() method invoked", -> - thenable = atom.workspace.scan /aaaa/, -> - resultOfPromiseSearch = null - - waitsFor 'fakeSearch to be defined', -> fakeSearch? - - runs -> - expect(fakeSearch.cancelled).toBe(undefined) - thenable.cancel() - expect(fakeSearch.cancelled).toBe(true) - - - waitsForPromise -> - thenable.then (promiseResult) -> resultOfPromiseSearch = promiseResult - - runs -> - expect(resultOfPromiseSearch).toBe('cancelled') - - it "will have the side-effect of failing the overall search if it fails", -> - # This provider's search should be cancelled when the first provider fails - fakeSearch2 = null - atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { - canSearchDirectory: (directory) -> directory.getPath() is dir2 - search: (directory, regex, options) -> fakeSearch2 = new FakeSearch(options) - }) - - didReject = false - promise = cancelableSearch = atom.workspace.scan /aaaa/, -> - waitsFor 'fakeSearch to be defined', -> fakeSearch? - - runs -> - fakeSearch.hoistedReject() - - waitsForPromise -> - cancelableSearch.catch -> didReject = true - - waitsFor (done) -> promise.then(null, done) - - runs -> - expect(didReject).toBe(true) - expect(fakeSearch2.cancelled).toBe true # Cancels other ongoing searches - - describe "::replace(regex, replacementText, paths, iterator)", -> - [filePath, commentFilePath, sampleContent, sampleCommentContent] = [] - - beforeEach -> - atom.project.setPaths([atom.project.getDirectories()[0]?.resolve('../')]) - - filePath = atom.project.getDirectories()[0]?.resolve('sample.js') - commentFilePath = atom.project.getDirectories()[0]?.resolve('sample-with-comments.js') - sampleContent = fs.readFileSync(filePath).toString() - sampleCommentContent = fs.readFileSync(commentFilePath).toString() - - afterEach -> - fs.writeFileSync(filePath, sampleContent) - fs.writeFileSync(commentFilePath, sampleCommentContent) - - describe "when a file doesn't exist", -> - it "calls back with an error", -> - errors = [] - missingPath = path.resolve('/not-a-file.js') - expect(fs.existsSync(missingPath)).toBeFalsy() - - waitsForPromise -> - atom.workspace.replace /items/gi, 'items', [missingPath], (result, error) -> - errors.push(error) - - runs -> - expect(errors).toHaveLength 1 - expect(errors[0].path).toBe missingPath - - describe "when called with unopened files", -> - it "replaces properly", -> - results = [] - waitsForPromise -> - atom.workspace.replace /items/gi, 'items', [filePath], (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength 1 - expect(results[0].filePath).toBe filePath - expect(results[0].replacements).toBe 6 - - describe "when a buffer is already open", -> - it "replaces properly and saves when not modified", -> - editor = null - results = [] - - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - expect(editor.isModified()).toBeFalsy() - - waitsForPromise -> - atom.workspace.replace /items/gi, 'items', [filePath], (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength 1 - expect(results[0].filePath).toBe filePath - expect(results[0].replacements).toBe 6 - - expect(editor.isModified()).toBeFalsy() - - it "does not replace when the path is not specified", -> - editor = null - results = [] - - waitsForPromise -> - atom.workspace.open('sample-with-comments.js').then (o) -> editor = o - - waitsForPromise -> - atom.workspace.replace /items/gi, 'items', [commentFilePath], (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength 1 - expect(results[0].filePath).toBe commentFilePath - - it "does NOT save when modified", -> - editor = null - results = [] - - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - editor.buffer.setTextInRange([[0, 0], [0, 0]], 'omg') - expect(editor.isModified()).toBeTruthy() - - waitsForPromise -> - atom.workspace.replace /items/gi, 'okthen', [filePath], (result) -> - results.push(result) - - runs -> - expect(results).toHaveLength 1 - expect(results[0].filePath).toBe filePath - expect(results[0].replacements).toBe 6 - - expect(editor.isModified()).toBeTruthy() - - describe "::saveActivePaneItem()", -> - editor = null - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - describe "when there is an error", -> - it "emits a warning notification when the file cannot be saved", -> - spyOn(editor, 'save').andCallFake -> - throw new Error("'/some/file' is a directory") - - atom.notifications.onDidAddNotification addedSpy = jasmine.createSpy() - atom.workspace.saveActivePaneItem() - expect(addedSpy).toHaveBeenCalled() - expect(addedSpy.mostRecentCall.args[0].getType()).toBe 'warning' - - it "emits a warning notification when the directory cannot be written to", -> - spyOn(editor, 'save').andCallFake -> - throw new Error("ENOTDIR, not a directory '/Some/dir/and-a-file.js'") - - atom.notifications.onDidAddNotification addedSpy = jasmine.createSpy() - atom.workspace.saveActivePaneItem() - expect(addedSpy).toHaveBeenCalled() - expect(addedSpy.mostRecentCall.args[0].getType()).toBe 'warning' - - it "emits a warning notification when the user does not have permission", -> - spyOn(editor, 'save').andCallFake -> - error = new Error("EACCES, permission denied '/Some/dir/and-a-file.js'") - error.code = 'EACCES' - error.path = '/Some/dir/and-a-file.js' - throw error - - atom.notifications.onDidAddNotification addedSpy = jasmine.createSpy() - atom.workspace.saveActivePaneItem() - expect(addedSpy).toHaveBeenCalled() - expect(addedSpy.mostRecentCall.args[0].getType()).toBe 'warning' - - it "emits a warning notification when the operation is not permitted", -> - spyOn(editor, 'save').andCallFake -> - error = new Error("EPERM, operation not permitted '/Some/dir/and-a-file.js'") - error.code = 'EPERM' - error.path = '/Some/dir/and-a-file.js' - throw error - - it "emits a warning notification when the file is already open by another app", -> - spyOn(editor, 'save').andCallFake -> - error = new Error("EBUSY, resource busy or locked '/Some/dir/and-a-file.js'") - error.code = 'EBUSY' - error.path = '/Some/dir/and-a-file.js' - throw error - - atom.notifications.onDidAddNotification addedSpy = jasmine.createSpy() - atom.workspace.saveActivePaneItem() - expect(addedSpy).toHaveBeenCalled() - - notificaiton = addedSpy.mostRecentCall.args[0] - expect(notificaiton.getType()).toBe 'warning' - expect(notificaiton.getMessage()).toContain 'Unable to save' - - it "emits a warning notification when the file system is read-only", -> - spyOn(editor, 'save').andCallFake -> - error = new Error("EROFS, read-only file system '/Some/dir/and-a-file.js'") - error.code = 'EROFS' - error.path = '/Some/dir/and-a-file.js' - throw error - - atom.notifications.onDidAddNotification addedSpy = jasmine.createSpy() - atom.workspace.saveActivePaneItem() - expect(addedSpy).toHaveBeenCalled() - - notification = addedSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'warning' - expect(notification.getMessage()).toContain 'Unable to save' - - it "emits a warning notification when the file cannot be saved", -> - spyOn(editor, 'save').andCallFake -> - throw new Error("no one knows") - - save = -> atom.workspace.saveActivePaneItem() - expect(save).toThrow() - - describe "::closeActivePaneItemOrEmptyPaneOrWindow", -> - beforeEach -> - spyOn(atom, 'close') - waitsForPromise -> atom.workspace.open() - - it "closes the active pane item, or the active pane if it is empty, or the current window if there is only the empty root pane", -> - atom.config.set('core.destroyEmptyPanes', false) - - pane1 = atom.workspace.getActivePane() - pane2 = pane1.splitRight(copyActiveItem: true) - - expect(atom.workspace.getPanes().length).toBe 2 - expect(pane2.getItems().length).toBe 1 - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - - expect(atom.workspace.getPanes().length).toBe 2 - expect(pane2.getItems().length).toBe 0 - - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - - expect(atom.workspace.getPanes().length).toBe 1 - expect(pane1.getItems().length).toBe 1 - - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.workspace.getPanes().length).toBe 1 - expect(pane1.getItems().length).toBe 0 - - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.workspace.getPanes().length).toBe 1 - - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - expect(atom.close).toHaveBeenCalled() - - describe "when the core.allowPendingPaneItems option is falsey", -> - it "does not open item with `pending: true` option as pending", -> - pane = null - atom.config.set('core.allowPendingPaneItems', false) - - waitsForPromise -> - atom.workspace.open('sample.js', pending: true).then -> - pane = atom.workspace.getActivePane() - - runs -> - expect(pane.getPendingItem()).toBeFalsy() - - describe "grammar activation", -> - it "notifies the workspace of which grammar is used", -> - editor = null - atom.packages.triggerDeferredActivationHooks() - - javascriptGrammarUsed = jasmine.createSpy('js grammar used') - rubyGrammarUsed = jasmine.createSpy('ruby grammar used') - cGrammarUsed = jasmine.createSpy('c grammar used') - - atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', javascriptGrammarUsed) - atom.packages.onDidTriggerActivationHook('language-ruby:grammar-used', rubyGrammarUsed) - atom.packages.onDidTriggerActivationHook('language-c:grammar-used', cGrammarUsed) - - waitsForPromise -> atom.packages.activatePackage('language-ruby') - waitsForPromise -> atom.packages.activatePackage('language-javascript') - waitsForPromise -> atom.packages.activatePackage('language-c') - waitsForPromise -> atom.workspace.open('sample-with-comments.js') - - runs -> - # Hooks are triggered when opening new editors - expect(javascriptGrammarUsed).toHaveBeenCalled() - - # Hooks are triggered when changing existing editors grammars - atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.c')) - expect(cGrammarUsed).toHaveBeenCalled() - - # Hooks are triggered when editors are added in other ways. - atom.workspace.getActivePane().splitRight(copyActiveItem: true) - atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.ruby')) - expect(rubyGrammarUsed).toHaveBeenCalled() - - describe ".checkoutHeadRevision()", -> - editor = null - beforeEach -> - atom.config.set("editor.confirmCheckoutHeadRevision", false) - - waitsForPromise -> atom.workspace.open('sample-with-comments.js').then (o) -> editor = o - - it "reverts to the version of its file checked into the project repository", -> - editor.setCursorBufferPosition([0, 0]) - editor.insertText("---\n") - expect(editor.lineTextForBufferRow(0)).toBe "---" - - waitsForPromise -> - atom.workspace.checkoutHeadRevision(editor) - - runs -> - expect(editor.lineTextForBufferRow(0)).toBe "" - - describe "when there's no repository for the editor's file", -> - it "doesn't do anything", -> - editor = new TextEditor - editor.setText("stuff") - atom.workspace.checkoutHeadRevision(editor) - - waitsForPromise -> atom.workspace.checkoutHeadRevision(editor) - - escapeStringRegex = (str) -> - str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js new file mode 100644 index 000000000..d5ddad7d2 --- /dev/null +++ b/spec/workspace-spec.js @@ -0,0 +1,1977 @@ +const path = require('path'); +const temp = require('temp').track(); +const TextEditor = require('../src/text-editor'); +const Workspace = require('../src/workspace'); +const Project = require('../src/project'); +const platform = require('./spec-helper-platform'); +const _ = require('underscore-plus'); +const fstream = require('fstream'); +const fs = require('fs-plus'); +const AtomEnvironment = require('../src/atom-environment'); + +describe("Workspace", function() { + let escapeStringRegex; + let [workspace, setDocumentEdited] = Array.from([]); + + beforeEach(function() { + ({ workspace } = atom); + workspace.resetFontSize(); + spyOn(atom.applicationDelegate, "confirm"); + setDocumentEdited = spyOn(atom.applicationDelegate, 'setWindowDocumentEdited'); + atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('dir'))]); + return waits(1); + }); + + afterEach(() => temp.cleanupSync()); + + describe("serialization", function() { + const simulateReload = function() { + const workspaceState = atom.workspace.serialize(); + const projectState = atom.project.serialize({isUnloading: true}); + atom.workspace.destroy(); + atom.project.destroy(); + atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom), applicationDelegate: atom.applicationDelegate}); + atom.project.deserialize(projectState); + atom.workspace = new Workspace({ + config: atom.config, project: atom.project, packageManager: atom.packages, + grammarRegistry: atom.grammars, deserializerManager: atom.deserializers, + notificationManager: atom.notifications, + applicationDelegate: atom.applicationDelegate, + viewRegistry: atom.views, assert: atom.assert.bind(atom), + textEditorRegistry: atom.textEditors + }); + return atom.workspace.deserialize(workspaceState, atom.deserializers); + }; + + describe("when the workspace contains text editors", () => + it("constructs the view with the same panes", function() { + const pane1 = atom.workspace.getActivePane(); + const pane2 = pane1.splitRight({copyActiveItem: true}); + const pane3 = pane2.splitRight({copyActiveItem: true}); + let pane4 = null; + + waitsForPromise(() => atom.workspace.open(null).then(editor => editor.setText("An untitled editor."))); + + waitsForPromise(() => + atom.workspace.open('b').then(editor => pane2.activateItem(editor.copy())) + ); + + waitsForPromise(() => + atom.workspace.open('../sample.js').then(editor => pane3.activateItem(editor)) + ); + + runs(function() { + pane3.activeItem.setCursorScreenPosition([2, 4]); + return pane4 = pane2.splitDown(); + }); + + waitsForPromise(() => + atom.workspace.open('../sample.txt').then(editor => pane4.activateItem(editor)) + ); + + return runs(function() { + pane4.getActiveItem().setCursorScreenPosition([0, 2]); + pane2.activate(); + + simulateReload(); + + expect(atom.workspace.getTextEditors().length).toBe(5); + const [editor1, editor2, untitledEditor, editor3, editor4] = Array.from(atom.workspace.getTextEditors()); + expect(editor1.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('b'))); + expect(editor2.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x1 => x1.resolve('../sample.txt'))); + expect(editor2.getCursorScreenPosition()).toEqual([0, 2]); + expect(editor3.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x2 => x2.resolve('b'))); + expect(editor4.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x3 => x3.resolve('../sample.js'))); + expect(editor4.getCursorScreenPosition()).toEqual([2, 4]); + expect(untitledEditor.getPath()).toBeUndefined(); + expect(untitledEditor.getText()).toBe("An untitled editor."); + + expect(atom.workspace.getActiveTextEditor().getPath()).toBe(editor3.getPath()); + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])); + return expect(document.title).toMatch(new RegExp(`^${path.basename(editor3.getLongTitle())} \\u2014 ${pathEscaped}`)); + }); + }) + ); + + return describe("where there are no open panes or editors", () => + it("constructs the view with no open editors", function() { + atom.workspace.getActivePane().destroy(); + expect(atom.workspace.getTextEditors().length).toBe(0); + simulateReload(); + return expect(atom.workspace.getTextEditors().length).toBe(0); + }) + ); + }); + + describe("::open(uri, options)", function() { + let openEvents = null; + + beforeEach(function() { + openEvents = []; + workspace.onDidOpen(event => openEvents.push(event)); + return spyOn(workspace.getActivePane(), 'activate').andCallThrough(); + }); + + describe("when the 'searchAllPanes' option is false (default)", function() { + describe("when called without a uri", () => + it("adds and activates an empty editor on the active pane", function() { + let [editor1, editor2] = Array.from([]); + + waitsForPromise(() => workspace.open().then(editor => editor1 = editor)); + + runs(function() { + expect(editor1.getPath()).toBeUndefined(); + expect(workspace.getActivePane().items).toEqual([editor1]); + expect(workspace.getActivePaneItem()).toBe(editor1); + expect(workspace.getActivePane().activate).toHaveBeenCalled(); + expect(openEvents).toEqual([{uri: undefined, pane: workspace.getActivePane(), item: editor1, index: 0}]); + return openEvents = [];}); + + waitsForPromise(() => workspace.open().then(editor => editor2 = editor)); + + return runs(function() { + expect(editor2.getPath()).toBeUndefined(); + expect(workspace.getActivePane().items).toEqual([editor1, editor2]); + expect(workspace.getActivePaneItem()).toBe(editor2); + expect(workspace.getActivePane().activate).toHaveBeenCalled(); + return expect(openEvents).toEqual([{uri: undefined, pane: workspace.getActivePane(), item: editor2, index: 1}]);});})); + + return describe("when called with a uri", function() { + describe("when the active pane already has an editor for the given uri", () => + it("activates the existing editor on the active pane", function() { + let editor = null; + let editor1 = null; + let editor2 = null; + + waitsForPromise(() => + workspace.open('a').then(function(o) { + editor1 = o; + return workspace.open('b').then(function(o) { + editor2 = o; + return workspace.open('a').then(o => editor = o); + }); + }) + ); + + return runs(function() { + expect(editor).toBe(editor1); + expect(workspace.getActivePaneItem()).toBe(editor); + expect(workspace.getActivePane().activate).toHaveBeenCalled(); + + return expect(openEvents).toEqual([ + { + uri: __guard__(atom.project.getDirectories()[0], x => x.resolve('a')), + item: editor1, + pane: atom.workspace.getActivePane(), + index: 0 + }, + { + uri: __guard__(atom.project.getDirectories()[0], x1 => x1.resolve('b')), + item: editor2, + pane: atom.workspace.getActivePane(), + index: 1 + }, + { + uri: __guard__(atom.project.getDirectories()[0], x2 => x2.resolve('a')), + item: editor1, + pane: atom.workspace.getActivePane(), + index: 0 + } + ]);});})); + + return describe("when the active pane does not have an editor for the given uri", () => + it("adds and activates a new editor for the given path on the active pane", function() { + let editor = null; + waitsForPromise(() => workspace.open('a').then(o => editor = o)); + + return runs(function() { + expect(editor.getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a'))); + expect(workspace.getActivePaneItem()).toBe(editor); + expect(workspace.getActivePane().items).toEqual([editor]); + return expect(workspace.getActivePane().activate).toHaveBeenCalled(); + }); + }) + ); + }); + }); + + describe("when the 'searchAllPanes' option is true", function() { + describe("when an editor for the given uri is already open on an inactive pane", () => + it("activates the existing editor on the inactive pane, then activates that pane", function() { + let editor1 = null; + let editor2 = null; + const pane1 = workspace.getActivePane(); + const pane2 = workspace.getActivePane().splitRight(); + + waitsForPromise(function() { + pane1.activate(); + return workspace.open('a').then(o => editor1 = o); + }); + + waitsForPromise(function() { + pane2.activate(); + return workspace.open('b').then(o => editor2 = o); + }); + + runs(() => expect(workspace.getActivePaneItem()).toBe(editor2)); + + waitsForPromise(() => workspace.open('a', {searchAllPanes: true})); + + return runs(function() { + expect(workspace.getActivePane()).toBe(pane1); + return expect(workspace.getActivePaneItem()).toBe(editor1); + }); + }) + ); + + return describe("when no editor for the given uri is open in any pane", () => + it("opens an editor for the given uri in the active pane", function() { + let editor = null; + waitsForPromise(() => workspace.open('a', {searchAllPanes: true}).then(o => editor = o)); + + return runs(() => expect(workspace.getActivePaneItem()).toBe(editor)); + }) + ); + }); + + describe("when the 'split' option is set", function() { + describe("when the 'split' option is 'left'", () => + it("opens the editor in the leftmost pane of the current pane axis", function() { + const pane1 = workspace.getActivePane(); + const pane2 = pane1.splitRight(); + expect(workspace.getActivePane()).toBe(pane2); + + let editor = null; + waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => editor = o)); + + runs(function() { + expect(workspace.getActivePane()).toBe(pane1); + expect(pane1.items).toEqual([editor]); + return expect(pane2.items).toEqual([]);}); + + // Focus right pane and reopen the file on the left + waitsForPromise(function() { + pane2.focus(); + return workspace.open('a', {split: 'left'}).then(o => editor = o); + }); + + return runs(function() { + expect(workspace.getActivePane()).toBe(pane1); + expect(pane1.items).toEqual([editor]); + return expect(pane2.items).toEqual([]);});})); + + describe("when a pane axis is the leftmost sibling of the current pane", () => + it("opens the new item in the current pane", function() { + let editor = null; + const pane1 = workspace.getActivePane(); + const pane2 = pane1.splitLeft(); + const pane3 = pane2.splitDown(); + pane1.activate(); + expect(workspace.getActivePane()).toBe(pane1); + + waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => editor = o)); + + return runs(function() { + expect(workspace.getActivePane()).toBe(pane1); + return expect(pane1.items).toEqual([editor]);});})); + + describe("when the 'split' option is 'right'", function() { + it("opens the editor in the rightmost pane of the current pane axis", function() { + let editor = null; + const pane1 = workspace.getActivePane(); + let pane2 = null; + waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => editor = o)); + + runs(function() { + pane2 = workspace.getPanes().filter(p => p !== pane1)[0]; + expect(workspace.getActivePane()).toBe(pane2); + expect(pane1.items).toEqual([]); + return expect(pane2.items).toEqual([editor]);}); + + // Focus right pane and reopen the file on the right + waitsForPromise(function() { + pane1.focus(); + return workspace.open('a', {split: 'right'}).then(o => editor = o); + }); + + return runs(function() { + expect(workspace.getActivePane()).toBe(pane2); + expect(pane1.items).toEqual([]); + return expect(pane2.items).toEqual([editor]);});}); + + return describe("when a pane axis is the rightmost sibling of the current pane", () => + it("opens the new item in a new pane split to the right of the current pane", function() { + let editor = null; + const pane1 = workspace.getActivePane(); + const pane2 = pane1.splitRight(); + const pane3 = pane2.splitDown(); + pane1.activate(); + expect(workspace.getActivePane()).toBe(pane1); + let pane4 = null; + + waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => editor = o)); + + return runs(function() { + pane4 = workspace.getPanes().filter(p => p !== pane1)[0]; + expect(workspace.getActivePane()).toBe(pane4); + expect(pane4.items).toEqual([editor]); + expect(workspace.paneContainer.root.children[0]).toBe(pane1); + return expect(workspace.paneContainer.root.children[1]).toBe(pane4); + }); + }) + ); + }); + + describe("when the 'split' option is 'up'", () => + it("opens the editor in the topmost pane of the current pane axis", function() { + const pane1 = workspace.getActivePane(); + const pane2 = pane1.splitDown(); + expect(workspace.getActivePane()).toBe(pane2); + + let editor = null; + waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => editor = o)); + + runs(function() { + expect(workspace.getActivePane()).toBe(pane1); + expect(pane1.items).toEqual([editor]); + return expect(pane2.items).toEqual([]);}); + + // Focus bottom pane and reopen the file on the top + waitsForPromise(function() { + pane2.focus(); + return workspace.open('a', {split: 'up'}).then(o => editor = o); + }); + + return runs(function() { + expect(workspace.getActivePane()).toBe(pane1); + expect(pane1.items).toEqual([editor]); + return expect(pane2.items).toEqual([]);});})); + + describe("when a pane axis is the topmost sibling of the current pane", () => + it("opens the new item in the current pane", function() { + let editor = null; + const pane1 = workspace.getActivePane(); + const pane2 = pane1.splitUp(); + const pane3 = pane2.splitRight(); + pane1.activate(); + expect(workspace.getActivePane()).toBe(pane1); + + waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => editor = o)); + + return runs(function() { + expect(workspace.getActivePane()).toBe(pane1); + return expect(pane1.items).toEqual([editor]);});})); + + return describe("when the 'split' option is 'down'", function() { + it("opens the editor in the bottommost pane of the current pane axis", function() { + let editor = null; + const pane1 = workspace.getActivePane(); + let pane2 = null; + waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => editor = o)); + + runs(function() { + pane2 = workspace.getPanes().filter(p => p !== pane1)[0]; + expect(workspace.getActivePane()).toBe(pane2); + expect(pane1.items).toEqual([]); + return expect(pane2.items).toEqual([editor]);}); + + // Focus bottom pane and reopen the file on the right + waitsForPromise(function() { + pane1.focus(); + return workspace.open('a', {split: 'down'}).then(o => editor = o); + }); + + return runs(function() { + expect(workspace.getActivePane()).toBe(pane2); + expect(pane1.items).toEqual([]); + return expect(pane2.items).toEqual([editor]);});}); + + return describe("when a pane axis is the bottommost sibling of the current pane", () => + it("opens the new item in a new pane split to the bottom of the current pane", function() { + let editor = null; + const pane1 = workspace.getActivePane(); + const pane2 = pane1.splitDown(); + pane1.activate(); + expect(workspace.getActivePane()).toBe(pane1); + let pane4 = null; + + waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => editor = o)); + + return runs(function() { + pane4 = workspace.getPanes().filter(p => p !== pane1)[0]; + expect(workspace.getActivePane()).toBe(pane4); + expect(pane4.items).toEqual([editor]); + expect(workspace.paneContainer.root.children[0]).toBe(pane1); + return expect(workspace.paneContainer.root.children[1]).toBe(pane2); + }); + }) + ); + }); + }); + + describe("when an initialLine and initialColumn are specified", () => + it("moves the cursor to the indicated location", function() { + waitsForPromise(() => workspace.open('a', {initialLine: 1, initialColumn: 5})); + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([1, 5])); + + waitsForPromise(() => workspace.open('a', {initialLine: 2, initialColumn: 4})); + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 4])); + + waitsForPromise(() => workspace.open('a', {initialLine: 0, initialColumn: 0})); + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([0, 0])); + + waitsForPromise(() => workspace.open('a', {initialLine: NaN, initialColumn: 4})); + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([0, 4])); + + waitsForPromise(() => workspace.open('a', {initialLine: 2, initialColumn: NaN})); + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 0])); + + waitsForPromise(() => workspace.open('a', {initialLine: Infinity, initialColumn: Infinity})); + + return runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 11]));})); + + describe("when the file is over 2MB", () => + it("opens the editor with largeFileMode: true", function() { + spyOn(fs, 'getSizeSync').andReturn(2 * 1048577); // 2MB + + let editor = null; + waitsForPromise(() => workspace.open('sample.js').then(e => editor = e)); + + return runs(() => expect(editor.largeFileMode).toBe(true)); + }) + ); + + describe("when the file is over user-defined limit", function() { + const shouldPromptForFileOfSize = function(size, shouldPrompt) { + spyOn(fs, 'getSizeSync').andReturn(size * 1048577); + atom.applicationDelegate.confirm.andCallFake(() => selectedButtonIndex); + atom.applicationDelegate.confirm(); + var selectedButtonIndex = 1; // cancel + + let editor = null; + waitsForPromise(() => workspace.open('sample.js').then(e => editor = e)); + if (shouldPrompt) { + runs(function() { + expect(editor).toBeUndefined(); + expect(atom.applicationDelegate.confirm).toHaveBeenCalled(); + + atom.applicationDelegate.confirm.reset(); + return selectedButtonIndex = 0; + }); // open the file + + waitsForPromise(() => workspace.open('sample.js').then(e => editor = e)); + + return runs(function() { + expect(atom.applicationDelegate.confirm).toHaveBeenCalled(); + return expect(editor.largeFileMode).toBe(true); + }); + } else { + return runs(() => expect(editor).not.toBeUndefined()); + } + }; + + it("prompts the user to make sure they want to open a file this big", function() { + atom.config.set("core.warnOnLargeFileLimit", 20); + return shouldPromptForFileOfSize(20, true); + }); + + it("doesn't prompt on files below the limit", function() { + atom.config.set("core.warnOnLargeFileLimit", 30); + return shouldPromptForFileOfSize(20, false); + }); + + return it("prompts for smaller files with a lower limit", function() { + atom.config.set("core.warnOnLargeFileLimit", 5); + return shouldPromptForFileOfSize(10, true); + }); + }); + + describe("when passed a path that matches a custom opener", () => + it("returns the resource returned by the custom opener", function() { + const fooOpener = function(pathToOpen, options) { if (pathToOpen != null ? pathToOpen.match(/\.foo/) : undefined) { return {foo: pathToOpen, options}; } }; + const barOpener = function(pathToOpen) { if (pathToOpen != null ? pathToOpen.match(/^bar:\/\//) : undefined) { return {bar: pathToOpen}; } }; + workspace.addOpener(fooOpener); + workspace.addOpener(barOpener); + + waitsForPromise(function() { + const pathToOpen = __guard__(atom.project.getDirectories()[0], x => x.resolve('a.foo')); + return workspace.open(pathToOpen, {hey: "there"}).then(item => expect(item).toEqual({foo: pathToOpen, options: {hey: "there"}}));}); + + return waitsForPromise(() => + workspace.open("bar://baz").then(item => expect(item).toEqual({bar: "bar://baz"})));})); + + it("adds the file to the application's recent documents list", function() { + if (process.platform !== 'darwin') { return; } // Feature only supported on macOS + spyOn(atom.applicationDelegate, 'addRecentDocument'); + + waitsForPromise(() => workspace.open()); + + runs(() => expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled()); + + waitsForPromise(() => workspace.open('something://a/url')); + + runs(() => expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled()); + + waitsForPromise(() => workspace.open(__filename)); + + return runs(() => expect(atom.applicationDelegate.addRecentDocument).toHaveBeenCalledWith(__filename)); + }); + + it("notifies ::onDidAddTextEditor observers", function() { + const absolutePath = require.resolve('./fixtures/dir/a'); + const newEditorHandler = jasmine.createSpy('newEditorHandler'); + workspace.onDidAddTextEditor(newEditorHandler); + + let editor = null; + waitsForPromise(() => workspace.open(absolutePath).then(e => editor = e)); + + return runs(() => expect(newEditorHandler.argsForCall[0][0].textEditor).toBe(editor)); + }); + + describe("when there is an error opening the file", function() { + let notificationSpy = null; + beforeEach(() => atom.notifications.onDidAddNotification(notificationSpy = jasmine.createSpy())); + + describe("when a file does not exist", () => + it("creates an empty buffer for the specified path", function() { + waitsForPromise(() => workspace.open('not-a-file.md')); + + return runs(function() { + const editor = workspace.getActiveTextEditor(); + expect(notificationSpy).not.toHaveBeenCalled(); + return expect(editor.getPath()).toContain('not-a-file.md'); + }); + }) + ); + + describe("when the user does not have access to the file", function() { + beforeEach(() => + spyOn(fs, 'openSync').andCallFake(function(path) { + const error = new Error(`EACCES, permission denied '${path}'`); + error.path = path; + error.code = 'EACCES'; + throw error; + }) + ); + + return it("creates a notification", function() { + waitsForPromise(() => workspace.open('file1')); + + return runs(function() { + expect(notificationSpy).toHaveBeenCalled(); + const notification = notificationSpy.mostRecentCall.args[0]; + expect(notification.getType()).toBe('warning'); + expect(notification.getMessage()).toContain('Permission denied'); + return expect(notification.getMessage()).toContain('file1'); + }); + }); + }); + + describe("when the the operation is not permitted", function() { + beforeEach(() => + spyOn(fs, 'openSync').andCallFake(function(path) { + const error = new Error(`EPERM, operation not permitted '${path}'`); + error.path = path; + error.code = 'EPERM'; + throw error; + }) + ); + + return it("creates a notification", function() { + waitsForPromise(() => workspace.open('file1')); + + return runs(function() { + expect(notificationSpy).toHaveBeenCalled(); + const notification = notificationSpy.mostRecentCall.args[0]; + expect(notification.getType()).toBe('warning'); + expect(notification.getMessage()).toContain('Unable to open'); + return expect(notification.getMessage()).toContain('file1'); + }); + }); + }); + + describe("when the the file is already open in windows", function() { + beforeEach(() => + spyOn(fs, 'openSync').andCallFake(function(path) { + const error = new Error(`EBUSY, resource busy or locked '${path}'`); + error.path = path; + error.code = 'EBUSY'; + throw error; + }) + ); + + return it("creates a notification", function() { + waitsForPromise(() => workspace.open('file1')); + + return runs(function() { + expect(notificationSpy).toHaveBeenCalled(); + const notification = notificationSpy.mostRecentCall.args[0]; + expect(notification.getType()).toBe('warning'); + expect(notification.getMessage()).toContain('Unable to open'); + return expect(notification.getMessage()).toContain('file1'); + }); + }); + }); + + return describe("when there is an unhandled error", function() { + beforeEach(() => + spyOn(fs, 'openSync').andCallFake(function(path) { + throw new Error("I dont even know what is happening right now!!"); + }) + ); + + return it("creates a notification", function() { + const open = () => workspace.open('file1', workspace.getActivePane()); + return expect(open).toThrow(); + }); + }); + }); + + describe("when the file is already open in pending state", () => + it("should terminate the pending state", function() { + let editor = null; + let pane = null; + + waitsForPromise(() => + atom.workspace.open('sample.js', {pending: true}).then(function(o) { + editor = o; + return pane = atom.workspace.getActivePane(); + }) + ); + + runs(() => expect(pane.getPendingItem()).toEqual(editor)); + + waitsForPromise(() => atom.workspace.open('sample.js')); + + return runs(() => expect(pane.getPendingItem()).toBeNull()); + }) + ); + + describe("when opening will switch from a pending tab to a permanent tab", () => + it("keeps the pending tab open", function() { + let editor1 = null; + let editor2 = null; + + waitsForPromise(() => + atom.workspace.open('sample.txt').then(o => editor1 = o) + ); + + waitsForPromise(() => + atom.workspace.open('sample2.txt', {pending: true}).then(o => editor2 = o) + ); + + return runs(function() { + const pane = atom.workspace.getActivePane(); + pane.activateItem(editor1); + expect(pane.getItems().length).toBe(2); + return expect(pane.getItems()).toEqual([editor1, editor2]);});})); + + return describe("when replacing a pending item which is the last item in a second pane", () => + it("does not destroy the pane even if core.destroyEmptyPanes is on", function() { + atom.config.set('core.destroyEmptyPanes', true); + let editor1 = null; + let editor2 = null; + const leftPane = atom.workspace.getActivePane(); + let rightPane = null; + + waitsForPromise(() => + atom.workspace.open('sample.js', {pending: true, split: 'right'}).then(function(o) { + editor1 = o; + rightPane = atom.workspace.getActivePane(); + return spyOn(rightPane, "destroyed"); + }) + ); + + runs(function() { + expect(leftPane).not.toBe(rightPane); + expect(atom.workspace.getActivePane()).toBe(rightPane); + expect(atom.workspace.getActivePane().getItems().length).toBe(1); + return expect(rightPane.getPendingItem()).toBe(editor1); + }); + + waitsForPromise(() => + atom.workspace.open('sample.txt', {pending: true}).then(o => editor2 = o) + ); + + return runs(function() { + expect(rightPane.getPendingItem()).toBe(editor2); + return expect(rightPane.destroyed.callCount).toBe(0); + }); + }) + ); + }); + + describe('the grammar-used hook', () => + it('fires when opening a file or changing the grammar of an open file', function() { + let editor = null; + let javascriptGrammarUsed = false; + let coffeescriptGrammarUsed = false; + + atom.packages.triggerDeferredActivationHooks(); + + runs(function() { + atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', () => javascriptGrammarUsed = true); + return atom.packages.onDidTriggerActivationHook('language-coffee-script:grammar-used', () => coffeescriptGrammarUsed = true); + }); + + waitsForPromise(() => atom.workspace.open('sample.js', {autoIndent: false}).then(o => editor = o)); + + waitsForPromise(() => atom.packages.activatePackage('language-javascript')); + + waitsFor(() => javascriptGrammarUsed); + + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')); + + runs(() => editor.setGrammar(atom.grammars.selectGrammar('.coffee'))); + + return waitsFor(() => coffeescriptGrammarUsed); + }) + ); + + describe("::reopenItem()", () => + it("opens the uri associated with the last closed pane that isn't currently open", function() { + const pane = workspace.getActivePane(); + waitsForPromise(() => + workspace.open('a').then(() => + workspace.open('b').then(() => + workspace.open('file1').then(() => workspace.open()) + ) + ) + ); + + runs(function() { + // does not reopen items with no uri + expect(workspace.getActivePaneItem().getURI()).toBeUndefined(); + return pane.destroyActiveItem(); + }); + + waitsForPromise(() => workspace.reopenItem()); + + runs(function() { + expect(workspace.getActivePaneItem().getURI()).not.toBeUndefined(); + + // destroy all items + expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('file1'))); + pane.destroyActiveItem(); + expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x1 => x1.resolve('b'))); + pane.destroyActiveItem(); + expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x2 => x2.resolve('a'))); + pane.destroyActiveItem(); + + // reopens items with uris + return expect(workspace.getActivePaneItem()).toBeUndefined(); + }); + + waitsForPromise(() => workspace.reopenItem()); + + runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a')))); + + // does not reopen items that are already open + waitsForPromise(() => workspace.open('b')); + + runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('b')))); + + waitsForPromise(() => workspace.reopenItem()); + + return runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('file1')))); + }) + ); + + describe("::increase/decreaseFontSize()", () => + it("increases/decreases the font size without going below 1", function() { + atom.config.set('editor.fontSize', 1); + workspace.increaseFontSize(); + expect(atom.config.get('editor.fontSize')).toBe(2); + workspace.increaseFontSize(); + expect(atom.config.get('editor.fontSize')).toBe(3); + workspace.decreaseFontSize(); + expect(atom.config.get('editor.fontSize')).toBe(2); + workspace.decreaseFontSize(); + expect(atom.config.get('editor.fontSize')).toBe(1); + workspace.decreaseFontSize(); + return expect(atom.config.get('editor.fontSize')).toBe(1); + }) + ); + + describe("::resetFontSize()", function() { + it("resets the font size to the window's starting font size", function() { + const originalFontSize = atom.config.get('editor.fontSize'); + + workspace.increaseFontSize(); + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize + 1); + workspace.resetFontSize(); + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize); + workspace.decreaseFontSize(); + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize - 1); + workspace.resetFontSize(); + return expect(atom.config.get('editor.fontSize')).toBe(originalFontSize); + }); + + it("does nothing if the font size has not been changed", function() { + const originalFontSize = atom.config.get('editor.fontSize'); + + workspace.resetFontSize(); + return expect(atom.config.get('editor.fontSize')).toBe(originalFontSize); + }); + + return it("resets the font size when the editor's font size changes", function() { + const originalFontSize = atom.config.get('editor.fontSize'); + + atom.config.set('editor.fontSize', originalFontSize + 1); + workspace.resetFontSize(); + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize); + atom.config.set('editor.fontSize', originalFontSize - 1); + workspace.resetFontSize(); + return expect(atom.config.get('editor.fontSize')).toBe(originalFontSize); + }); + }); + + describe("::openLicense()", () => + it("opens the license as plain-text in a buffer", function() { + waitsForPromise(() => workspace.openLicense()); + return runs(() => expect(workspace.getActivePaneItem().getText()).toMatch(/Copyright/)); + }) + ); + + describe("::isTextEditor(obj)", () => + it("returns true when the passed object is an instance of `TextEditor`", function() { + expect(workspace.isTextEditor(new TextEditor)).toBe(true); + expect(workspace.isTextEditor({getText() { return null; }})).toBe(false); + expect(workspace.isTextEditor(null)).toBe(false); + return expect(workspace.isTextEditor(undefined)).toBe(false); + }) + ); + + describe("::observeTextEditors()", () => + it("invokes the observer with current and future text editors", function() { + const observed = []; + + waitsForPromise(() => workspace.open()); + waitsForPromise(() => workspace.open()); + waitsForPromise(() => workspace.openLicense()); + + runs(() => workspace.observeTextEditors(editor => observed.push(editor))); + + waitsForPromise(() => workspace.open()); + + return expect(observed).toEqual(workspace.getTextEditors()); + }) + ); + + describe("when an editor is destroyed", () => + it("removes the editor", function() { + let editor = null; + + waitsForPromise(() => workspace.open("a").then(e => editor = e)); + + return runs(function() { + expect(workspace.getTextEditors()).toHaveLength(1); + editor.destroy(); + return expect(workspace.getTextEditors()).toHaveLength(0); + }); + }) + ); + + describe("when an editor is copied because its pane is split", () => + it("sets up the new editor to be configured by the text editor registry", function() { + waitsForPromise(() => atom.packages.activatePackage('language-javascript')); + + return waitsForPromise(() => + workspace.open('a').then(function(editor) { + atom.textEditors.setGrammarOverride(editor, 'source.js'); + expect(editor.getGrammar().name).toBe('JavaScript'); + + workspace.getActivePane().splitRight({copyActiveItem: true}); + const newEditor = workspace.getActiveTextEditor(); + expect(newEditor).not.toBe(editor); + return expect(newEditor.getGrammar().name).toBe('JavaScript'); + }) + ); + }) + ); + + it("stores the active grammars used by all the open editors", function() { + waitsForPromise(() => atom.packages.activatePackage('language-javascript')); + + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')); + + waitsForPromise(() => atom.packages.activatePackage('language-todo')); + + waitsForPromise(() => atom.workspace.open('sample.coffee')); + + return runs(function() { + atom.workspace.getActiveTextEditor().setText(`\ +i = /test/; #FIXME\ +` + ); + + const atom2 = new AtomEnvironment({ + applicationDelegate: atom.applicationDelegate, + window: document.createElement('div'), + document: Object.assign( + document.createElement('div'), + { + body: document.createElement('div'), + head: document.createElement('div'), + } + ) + }); + + atom2.packages.loadPackage('language-javascript'); + atom2.packages.loadPackage('language-coffee-script'); + atom2.packages.loadPackage('language-todo'); + atom2.project.deserialize(atom.project.serialize()); + atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers); + + expect(atom2.grammars.getGrammars().map(grammar => grammar.name).sort()).toEqual([ + 'CoffeeScript', + 'CoffeeScript (Literate)', + 'JavaScript', + 'Null Grammar', + 'Regular Expression Replacement (JavaScript)', + 'Regular Expressions (JavaScript)', + 'TODO' + ]); + + return atom2.destroy(); + }); + }); + + describe("document.title", function() { + describe("when there is no item open", function() { + it("sets the title to the project path", () => expect(document.title).toMatch(escapeStringRegex(fs.tildify(atom.project.getPaths()[0])))); + + return it("sets the title to 'untitled' if there is no project path", function() { + atom.project.setPaths([]); + return expect(document.title).toMatch(/^untitled/); + }); + }); + + describe("when the active pane item's path is not inside a project path", function() { + beforeEach(() => + waitsForPromise(() => + atom.workspace.open('b').then(() => atom.project.setPaths([])) + ) + ); + + it("sets the title to the pane item's title plus the item's path", function() { + const item = atom.workspace.getActivePaneItem(); + const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))); + return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)); + }); + + describe("when the title of the active pane item changes", () => + it("updates the window title based on the item's new title", function() { + const editor = atom.workspace.getActivePaneItem(); + editor.buffer.setPath(path.join(temp.dir, 'hi')); + const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(editor.getPath()))); + return expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)); + }) + ); + + describe("when the active pane's item changes", () => + it("updates the title to the new item's title plus the project path", function() { + atom.workspace.getActivePane().activateNextItem(); + const item = atom.workspace.getActivePaneItem(); + const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))); + return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)); + }) + ); + + return describe("when an inactive pane's item changes", () => + it("does not update the title", function() { + const pane = atom.workspace.getActivePane(); + pane.splitRight(); + const initialTitle = document.title; + pane.activateNextItem(); + return expect(document.title).toBe(initialTitle); + }) + ); + }); + + describe("when the active pane item is inside a project path", function() { + beforeEach(() => + waitsForPromise(() => atom.workspace.open('b')) + ); + + describe("when there is an active pane item", () => + it("sets the title to the pane item's title plus the project path", function() { + const item = atom.workspace.getActivePaneItem(); + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])); + return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)); + }) + ); + + describe("when the title of the active pane item changes", () => + it("updates the window title based on the item's new title", function() { + const editor = atom.workspace.getActivePaneItem(); + editor.buffer.setPath(path.join(atom.project.getPaths()[0], 'hi')); + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])); + return expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)); + }) + ); + + describe("when the active pane's item changes", () => + it("updates the title to the new item's title plus the project path", function() { + atom.workspace.getActivePane().activateNextItem(); + const item = atom.workspace.getActivePaneItem(); + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])); + return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)); + }) + ); + + describe("when the last pane item is removed", () => + it("updates the title to the project's first path", function() { + atom.workspace.getActivePane().destroy(); + expect(atom.workspace.getActivePaneItem()).toBeUndefined(); + return expect(document.title).toMatch(escapeStringRegex(fs.tildify(atom.project.getPaths()[0]))); + }) + ); + + return describe("when an inactive pane's item changes", () => + it("does not update the title", function() { + const pane = atom.workspace.getActivePane(); + pane.splitRight(); + const initialTitle = document.title; + pane.activateNextItem(); + return expect(document.title).toBe(initialTitle); + }) + ); + }); + + return describe("when the workspace is deserialized", function() { + beforeEach(() => waitsForPromise(() => atom.workspace.open('a'))); + + return it("updates the title to contain the project's path", function() { + document.title = null; + + const atom2 = new AtomEnvironment({ + applicationDelegate: atom.applicationDelegate, + window: document.createElement('div'), + document: Object.assign( + document.createElement('div'), + { + body: document.createElement('div'), + head: document.createElement('div'), + } + ) + }); + + atom2.project.deserialize(atom.project.serialize()); + atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers); + const item = atom2.workspace.getActivePaneItem(); + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])); + expect(document.title).toMatch(new RegExp(`^${item.getLongTitle()} \\u2014 ${pathEscaped}`)); + + return atom2.destroy(); + }); + }); + }); + + describe("document edited status", function() { + let [item1, item2] = Array.from([]); + + beforeEach(function() { + waitsForPromise(() => atom.workspace.open('a')); + waitsForPromise(() => atom.workspace.open('b')); + return runs(() => [item1, item2] = Array.from(atom.workspace.getPaneItems())); + }); + + it("calls setDocumentEdited when the active item changes", function() { + expect(atom.workspace.getActivePaneItem()).toBe(item2); + item1.insertText('a'); + expect(item1.isModified()).toBe(true); + atom.workspace.getActivePane().activateNextItem(); + + return expect(setDocumentEdited).toHaveBeenCalledWith(true); + }); + + return it("calls atom.setDocumentEdited when the active item's modified status changes", function() { + expect(atom.workspace.getActivePaneItem()).toBe(item2); + item2.insertText('a'); + advanceClock(item2.getBuffer().getStoppedChangingDelay()); + + expect(item2.isModified()).toBe(true); + expect(setDocumentEdited).toHaveBeenCalledWith(true); + + item2.undo(); + advanceClock(item2.getBuffer().getStoppedChangingDelay()); + + expect(item2.isModified()).toBe(false); + return expect(setDocumentEdited).toHaveBeenCalledWith(false); + }); + }); + + describe("adding panels", function() { + class TestItem {} + + class TestItemElement extends HTMLElement { + constructor() {} + initialize(model) { this.model = model; return this; } + getModel() { return this.model; } + } + + beforeEach(() => + atom.views.addViewProvider(TestItem, model => new TestItemElement().initialize(model)) + ); + + describe('::addLeftPanel(model)', () => + it('adds a panel to the correct panel container', function() { + let addPanelSpy; + expect(atom.workspace.getLeftPanels().length).toBe(0); + atom.workspace.panelContainers.left.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + + const model = new TestItem; + const panel = atom.workspace.addLeftPanel({item: model}); + + expect(panel).toBeDefined(); + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + + const itemView = atom.views.getView(atom.workspace.getLeftPanels()[0].getItem()); + expect(itemView instanceof TestItemElement).toBe(true); + return expect(itemView.getModel()).toBe(model); + }) + ); + + describe('::addRightPanel(model)', () => + it('adds a panel to the correct panel container', function() { + let addPanelSpy; + expect(atom.workspace.getRightPanels().length).toBe(0); + atom.workspace.panelContainers.right.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + + const model = new TestItem; + const panel = atom.workspace.addRightPanel({item: model}); + + expect(panel).toBeDefined(); + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + + const itemView = atom.views.getView(atom.workspace.getRightPanels()[0].getItem()); + expect(itemView instanceof TestItemElement).toBe(true); + return expect(itemView.getModel()).toBe(model); + }) + ); + + describe('::addTopPanel(model)', () => + it('adds a panel to the correct panel container', function() { + let addPanelSpy; + expect(atom.workspace.getTopPanels().length).toBe(0); + atom.workspace.panelContainers.top.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + + const model = new TestItem; + const panel = atom.workspace.addTopPanel({item: model}); + + expect(panel).toBeDefined(); + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + + const itemView = atom.views.getView(atom.workspace.getTopPanels()[0].getItem()); + expect(itemView instanceof TestItemElement).toBe(true); + return expect(itemView.getModel()).toBe(model); + }) + ); + + describe('::addBottomPanel(model)', () => + it('adds a panel to the correct panel container', function() { + let addPanelSpy; + expect(atom.workspace.getBottomPanels().length).toBe(0); + atom.workspace.panelContainers.bottom.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + + const model = new TestItem; + const panel = atom.workspace.addBottomPanel({item: model}); + + expect(panel).toBeDefined(); + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + + const itemView = atom.views.getView(atom.workspace.getBottomPanels()[0].getItem()); + expect(itemView instanceof TestItemElement).toBe(true); + return expect(itemView.getModel()).toBe(model); + }) + ); + + describe('::addHeaderPanel(model)', () => + it('adds a panel to the correct panel container', function() { + let addPanelSpy; + expect(atom.workspace.getHeaderPanels().length).toBe(0); + atom.workspace.panelContainers.header.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + + const model = new TestItem; + const panel = atom.workspace.addHeaderPanel({item: model}); + + expect(panel).toBeDefined(); + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + + const itemView = atom.views.getView(atom.workspace.getHeaderPanels()[0].getItem()); + expect(itemView instanceof TestItemElement).toBe(true); + return expect(itemView.getModel()).toBe(model); + }) + ); + + describe('::addFooterPanel(model)', () => + it('adds a panel to the correct panel container', function() { + let addPanelSpy; + expect(atom.workspace.getFooterPanels().length).toBe(0); + atom.workspace.panelContainers.footer.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + + const model = new TestItem; + const panel = atom.workspace.addFooterPanel({item: model}); + + expect(panel).toBeDefined(); + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + + const itemView = atom.views.getView(atom.workspace.getFooterPanels()[0].getItem()); + expect(itemView instanceof TestItemElement).toBe(true); + return expect(itemView.getModel()).toBe(model); + }) + ); + + describe('::addModalPanel(model)', () => + it('adds a panel to the correct panel container', function() { + let addPanelSpy; + expect(atom.workspace.getModalPanels().length).toBe(0); + atom.workspace.panelContainers.modal.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + + const model = new TestItem; + const panel = atom.workspace.addModalPanel({item: model}); + + expect(panel).toBeDefined(); + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + + const itemView = atom.views.getView(atom.workspace.getModalPanels()[0].getItem()); + expect(itemView instanceof TestItemElement).toBe(true); + return expect(itemView.getModel()).toBe(model); + }) + ); + + return describe("::panelForItem(item)", () => + it("returns the panel associated with the item", function() { + const item = new TestItem; + const panel = atom.workspace.addLeftPanel({item}); + + const itemWithNoPanel = new TestItem; + + expect(atom.workspace.panelForItem(item)).toBe(panel); + return expect(atom.workspace.panelForItem(itemWithNoPanel)).toBe(null); + }) + ); + }); + + describe("::scan(regex, options, callback)", () => + describe("when called with a regex", function() { + it("calls the callback with all regex results in all files in the project", function() { + const results = []; + waitsForPromise(() => + atom.workspace.scan(/(a)+/, result => results.push(result)) + ); + + return runs(function() { + expect(results).toHaveLength(3); + expect(results[0].filePath).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a'))); + expect(results[0].matches).toHaveLength(3); + return expect(results[0].matches[0]).toEqual({ + matchText: 'aaa', + lineText: 'aaa bbb', + lineTextOffset: 0, + range: [[0, 0], [0, 3]]});});}); + + it("works with with escaped literals (like $ and ^)", function() { + const results = []; + waitsForPromise(() => atom.workspace.scan(/\$\w+/, result => results.push(result))); + + return runs(function() { + expect(results.length).toBe(1); + + const {filePath, matches} = results[0]; + expect(filePath).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a'))); + expect(matches).toHaveLength(1); + return expect(matches[0]).toEqual({ + matchText: '$bill', + lineText: 'dollar$bill', + lineTextOffset: 0, + range: [[2, 6], [2, 11]]});});}); + + it("works on evil filenames", function() { + atom.config.set('core.excludeVcsIgnoredPaths', false); + platform.generateEvilFiles(); + atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')]); + const paths = []; + let matches = []; + waitsForPromise(() => + atom.workspace.scan(/evil/, function(result) { + paths.push(result.filePath); + return matches = matches.concat(result.matches); + }) + ); + + return runs(function() { + _.each(matches, m => expect(m.matchText).toEqual('evil')); + + if (platform.isWindows()) { + expect(paths.length).toBe(3); + expect(paths[0]).toMatch(/a_file_with_utf8.txt$/); + expect(paths[1]).toMatch(/file with spaces.txt$/); + return expect(path.basename(paths[2])).toBe("utfa\u0306.md"); + } else { + expect(paths.length).toBe(5); + expect(paths[0]).toMatch(/a_file_with_utf8.txt$/); + expect(paths[1]).toMatch(/file with spaces.txt$/); + expect(paths[2]).toMatch(/goddam\nnewlines$/m); + expect(paths[3]).toMatch(/quote".txt$/m); + return expect(path.basename(paths[4])).toBe("utfa\u0306.md"); + } + }); + }); + + it("ignores case if the regex includes the `i` flag", function() { + const results = []; + waitsForPromise(() => atom.workspace.scan(/DOLLAR/i, result => results.push(result))); + + return runs(() => expect(results).toHaveLength(1)); + }); + + describe("when the core.excludeVcsIgnoredPaths config is truthy", function() { + let [projectPath, ignoredPath] = Array.from([]); + + beforeEach(function() { + const sourceProjectPath = path.join(__dirname, 'fixtures', 'git', 'working-dir'); + projectPath = path.join(temp.mkdirSync("atom")); + + const writerStream = fstream.Writer(projectPath); + fstream.Reader(sourceProjectPath).pipe(writerStream); + + waitsFor(function(done) { + writerStream.on('close', done); + return writerStream.on('error', done); + }); + + return runs(function() { + fs.rename(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')); + ignoredPath = path.join(projectPath, 'ignored.txt'); + return fs.writeFileSync(ignoredPath, 'this match should not be included'); + }); + }); + + afterEach(function() { + if (fs.existsSync(projectPath)) { return fs.removeSync(projectPath); } + }); + + return it("excludes ignored files", function() { + atom.project.setPaths([projectPath]); + atom.config.set('core.excludeVcsIgnoredPaths', true); + const resultHandler = jasmine.createSpy("result found"); + waitsForPromise(() => + atom.workspace.scan(/match/, results => resultHandler()) + ); + + return runs(() => expect(resultHandler).not.toHaveBeenCalled()); + }); + }); + + it("includes only files when a directory filter is specified", function() { + const projectPath = path.join(path.join(__dirname, 'fixtures', 'dir')); + atom.project.setPaths([projectPath]); + + const filePath = path.join(projectPath, 'a-dir', 'oh-git'); + + const paths = []; + let matches = []; + waitsForPromise(() => + atom.workspace.scan(/aaa/, {paths: [`a-dir${path.sep}`]}, function(result) { + paths.push(result.filePath); + return matches = matches.concat(result.matches); + }) + ); + + return runs(function() { + expect(paths.length).toBe(1); + expect(paths[0]).toBe(filePath); + return expect(matches.length).toBe(1); + }); + }); + + it("includes files and folders that begin with a '.'", function() { + const projectPath = temp.mkdirSync('atom-spec-workspace'); + const filePath = path.join(projectPath, '.text'); + fs.writeFileSync(filePath, 'match this'); + atom.project.setPaths([projectPath]); + const paths = []; + let matches = []; + waitsForPromise(() => + atom.workspace.scan(/match this/, function(result) { + paths.push(result.filePath); + return matches = matches.concat(result.matches); + }) + ); + + return runs(function() { + expect(paths.length).toBe(1); + expect(paths[0]).toBe(filePath); + return expect(matches.length).toBe(1); + }); + }); + + it("excludes values in core.ignoredNames", function() { + const ignoredNames = atom.config.get("core.ignoredNames"); + ignoredNames.push("a"); + atom.config.set("core.ignoredNames", ignoredNames); + + const resultHandler = jasmine.createSpy("result found"); + waitsForPromise(() => + atom.workspace.scan(/dollar/, results => resultHandler()) + ); + + return runs(() => expect(resultHandler).not.toHaveBeenCalled()); + }); + + it("scans buffer contents if the buffer is modified", function() { + let editor = null; + const results = []; + + waitsForPromise(() => + atom.workspace.open('a').then(function(o) { + editor = o; + return editor.setText("Elephant"); + }) + ); + + waitsForPromise(() => atom.workspace.scan(/a|Elephant/, result => results.push(result))); + + return runs(function() { + expect(results).toHaveLength(3); + const resultForA = _.find(results, ({filePath}) => path.basename(filePath) === 'a'); + expect(resultForA.matches).toHaveLength(1); + return expect(resultForA.matches[0].matchText).toBe('Elephant'); + }); + }); + + it("ignores buffers outside the project", function() { + let editor = null; + const results = []; + + waitsForPromise(() => + atom.workspace.open(temp.openSync().path).then(function(o) { + editor = o; + return editor.setText("Elephant"); + }) + ); + + waitsForPromise(() => atom.workspace.scan(/Elephant/, result => results.push(result))); + + return runs(() => expect(results).toHaveLength(0)); + }); + + return describe("when the project has multiple root directories", function() { + let [dir1, dir2, file1, file2] = Array.from([]); + + beforeEach(function() { + [dir1] = Array.from(atom.project.getPaths()); + file1 = path.join(dir1, "a-dir", "oh-git"); + + dir2 = temp.mkdirSync("a-second-dir"); + const aDir2 = path.join(dir2, "a-dir"); + file2 = path.join(aDir2, "a-file"); + fs.mkdirSync(aDir2); + fs.writeFileSync(file2, "ccc aaaa"); + + return atom.project.addPath(dir2); + }); + + it("searches matching files in all of the project's root directories", function() { + const resultPaths = []; + waitsForPromise(() => + atom.workspace.scan(/aaaa/, ({filePath}) => resultPaths.push(filePath)) + ); + + return runs(() => expect(resultPaths.sort()).toEqual([file1, file2].sort())); + }); + + describe("when an inclusion path starts with the basename of a root directory", () => + it("interprets the inclusion path as starting from that directory", function() { + waitsForPromise(function() { + const resultPaths = []; + return atom.workspace + .scan(/aaaa/, {paths: ["dir"]}, function({filePath}) { + if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath); }}).then(() => expect(resultPaths).toEqual([file1])); + }); + + waitsForPromise(function() { + const resultPaths = []; + return atom.workspace + .scan(/aaaa/, {paths: [path.join("dir", "a-dir")]}, function({filePath}) { + if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath); }}).then(() => expect(resultPaths).toEqual([file1])); + }); + + waitsForPromise(function() { + const resultPaths = []; + return atom.workspace + .scan(/aaaa/, {paths: [path.basename(dir2)]}, function({filePath}) { + if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath); }}).then(() => expect(resultPaths).toEqual([file2])); + }); + + return waitsForPromise(function() { + const resultPaths = []; + return atom.workspace + .scan(/aaaa/, {paths: [path.join(path.basename(dir2), "a-dir")]}, function({filePath}) { + if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath); }}).then(() => expect(resultPaths).toEqual([file2])); + }); + }) + ); + + return describe("when a custom directory searcher is registered", function() { + let fakeSearch = null; + // Function that is invoked once all of the fields on fakeSearch are set. + let onFakeSearchCreated = null; + + class FakeSearch { + constructor(options) { + // Note that hoisting resolve and reject in this way is generally frowned upon. + this.options = options; + this.promise = new Promise((function(resolve, reject) { + this.hoistedResolve = resolve; + this.hoistedReject = reject; + return (typeof onFakeSearchCreated === 'function' ? onFakeSearchCreated(this) : undefined); + }.bind(this))); + } + then(...args) { + return this.promise.then.apply(this.promise, args); + } + cancel() { + this.cancelled = true; + // According to the spec for a DirectorySearcher, invoking `cancel()` should + // resolve the thenable rather than reject it. + return this.hoistedResolve(); + } + } + + beforeEach(function() { + fakeSearch = null; + onFakeSearchCreated = null; + atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { + canSearchDirectory(directory) { return directory.getPath() === dir1; }, + search(directory, regex, options) { return fakeSearch = new FakeSearch(options); } + }); + + return waitsFor(() => atom.workspace.directorySearchers.length > 0); + }); + + it("can override the DefaultDirectorySearcher on a per-directory basis", function() { + const foreignFilePath = 'ssh://foreign-directory:8080/hello.txt'; + const numPathsSearchedInDir2 = 1; + const numPathsToPretendToSearchInCustomDirectorySearcher = 10; + const searchResult = { + filePath: foreignFilePath, + matches: [ + { + lineText: 'Hello world', + lineTextOffset: 0, + matchText: 'Hello', + range: [[0, 0], [0, 5]], + }, + ] + }; + onFakeSearchCreated = function(fakeSearch) { + fakeSearch.options.didMatch(searchResult); + fakeSearch.options.didSearchPaths(numPathsToPretendToSearchInCustomDirectorySearcher); + return fakeSearch.hoistedResolve(); + }; + + const resultPaths = []; + const onPathsSearched = jasmine.createSpy('onPathsSearched'); + waitsForPromise(() => + atom.workspace.scan(/aaaa/, {onPathsSearched}, ({filePath}) => resultPaths.push(filePath)) + ); + + return runs(function() { + expect(resultPaths.sort()).toEqual([foreignFilePath, file2].sort()); + // onPathsSearched should be called once by each DirectorySearcher. The order is not + // guaranteed, so we can only verify the total number of paths searched is correct + // after the second call. + expect(onPathsSearched.callCount).toBe(2); + return expect(onPathsSearched.mostRecentCall.args[0]).toBe( + numPathsToPretendToSearchInCustomDirectorySearcher + numPathsSearchedInDir2); + }); + }); + + it("can be cancelled when the object returned by scan() has its cancel() method invoked", function() { + const thenable = atom.workspace.scan(/aaaa/, function() {}); + let resultOfPromiseSearch = null; + + waitsFor('fakeSearch to be defined', () => fakeSearch != null); + + runs(function() { + expect(fakeSearch.cancelled).toBe(undefined); + thenable.cancel(); + return expect(fakeSearch.cancelled).toBe(true); + }); + + + waitsForPromise(() => thenable.then(promiseResult => resultOfPromiseSearch = promiseResult)); + + return runs(() => expect(resultOfPromiseSearch).toBe('cancelled')); + }); + + return it("will have the side-effect of failing the overall search if it fails", function() { + // This provider's search should be cancelled when the first provider fails + let cancelableSearch; + let fakeSearch2 = null; + atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { + canSearchDirectory(directory) { return directory.getPath() === dir2; }, + search(directory, regex, options) { return fakeSearch2 = new FakeSearch(options); } + }); + + let didReject = false; + const promise = cancelableSearch = atom.workspace.scan(/aaaa/, function() {}); + waitsFor('fakeSearch to be defined', () => fakeSearch != null); + + runs(() => fakeSearch.hoistedReject()); + + waitsForPromise(() => cancelableSearch.catch(() => didReject = true)); + + waitsFor(done => promise.then(null, done)); + + return runs(function() { + expect(didReject).toBe(true); + return expect(fakeSearch2.cancelled).toBe(true); + }); + }); + }); + }); + }) + ); // Cancels other ongoing searches + + describe("::replace(regex, replacementText, paths, iterator)", function() { + let [filePath, commentFilePath, sampleContent, sampleCommentContent] = Array.from([]); + + beforeEach(function() { + atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('../'))]); + + filePath = __guard__(atom.project.getDirectories()[0], x1 => x1.resolve('sample.js')); + commentFilePath = __guard__(atom.project.getDirectories()[0], x2 => x2.resolve('sample-with-comments.js')); + sampleContent = fs.readFileSync(filePath).toString(); + return sampleCommentContent = fs.readFileSync(commentFilePath).toString(); + }); + + afterEach(function() { + fs.writeFileSync(filePath, sampleContent); + return fs.writeFileSync(commentFilePath, sampleCommentContent); + }); + + describe("when a file doesn't exist", () => + it("calls back with an error", function() { + const errors = []; + const missingPath = path.resolve('/not-a-file.js'); + expect(fs.existsSync(missingPath)).toBeFalsy(); + + waitsForPromise(() => + atom.workspace.replace(/items/gi, 'items', [missingPath], (result, error) => errors.push(error)) + ); + + return runs(function() { + expect(errors).toHaveLength(1); + return expect(errors[0].path).toBe(missingPath); + }); + }) + ); + + describe("when called with unopened files", () => + it("replaces properly", function() { + const results = []; + waitsForPromise(() => + atom.workspace.replace(/items/gi, 'items', [filePath], result => results.push(result)) + ); + + return runs(function() { + expect(results).toHaveLength(1); + expect(results[0].filePath).toBe(filePath); + return expect(results[0].replacements).toBe(6); + }); + }) + ); + + return describe("when a buffer is already open", function() { + it("replaces properly and saves when not modified", function() { + let editor = null; + const results = []; + + waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)); + + runs(() => expect(editor.isModified()).toBeFalsy()); + + waitsForPromise(() => + atom.workspace.replace(/items/gi, 'items', [filePath], result => results.push(result)) + ); + + return runs(function() { + expect(results).toHaveLength(1); + expect(results[0].filePath).toBe(filePath); + expect(results[0].replacements).toBe(6); + + return expect(editor.isModified()).toBeFalsy(); + }); + }); + + it("does not replace when the path is not specified", function() { + let editor = null; + const results = []; + + waitsForPromise(() => atom.workspace.open('sample-with-comments.js').then(o => editor = o)); + + waitsForPromise(() => + atom.workspace.replace(/items/gi, 'items', [commentFilePath], result => results.push(result)) + ); + + return runs(function() { + expect(results).toHaveLength(1); + return expect(results[0].filePath).toBe(commentFilePath); + }); + }); + + return it("does NOT save when modified", function() { + let editor = null; + const results = []; + + waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)); + + runs(function() { + editor.buffer.setTextInRange([[0, 0], [0, 0]], 'omg'); + return expect(editor.isModified()).toBeTruthy(); + }); + + waitsForPromise(() => + atom.workspace.replace(/items/gi, 'okthen', [filePath], result => results.push(result)) + ); + + return runs(function() { + expect(results).toHaveLength(1); + expect(results[0].filePath).toBe(filePath); + expect(results[0].replacements).toBe(6); + + return expect(editor.isModified()).toBeTruthy(); + }); + }); + }); + }); + + describe("::saveActivePaneItem()", function() { + let editor = null; + beforeEach(() => + waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)) + ); + + return describe("when there is an error", function() { + it("emits a warning notification when the file cannot be saved", function() { + let addedSpy; + spyOn(editor, 'save').andCallFake(function() { + throw new Error("'/some/file' is a directory"); + }); + + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()); + atom.workspace.saveActivePaneItem(); + expect(addedSpy).toHaveBeenCalled(); + return expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning'); + }); + + it("emits a warning notification when the directory cannot be written to", function() { + let addedSpy; + spyOn(editor, 'save').andCallFake(function() { + throw new Error("ENOTDIR, not a directory '/Some/dir/and-a-file.js'"); + }); + + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()); + atom.workspace.saveActivePaneItem(); + expect(addedSpy).toHaveBeenCalled(); + return expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning'); + }); + + it("emits a warning notification when the user does not have permission", function() { + let addedSpy; + spyOn(editor, 'save').andCallFake(function() { + const error = new Error("EACCES, permission denied '/Some/dir/and-a-file.js'"); + error.code = 'EACCES'; + error.path = '/Some/dir/and-a-file.js'; + throw error; + }); + + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()); + atom.workspace.saveActivePaneItem(); + expect(addedSpy).toHaveBeenCalled(); + return expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning'); + }); + + it("emits a warning notification when the operation is not permitted", () => + spyOn(editor, 'save').andCallFake(function() { + const error = new Error("EPERM, operation not permitted '/Some/dir/and-a-file.js'"); + error.code = 'EPERM'; + error.path = '/Some/dir/and-a-file.js'; + throw error; + }) + ); + + it("emits a warning notification when the file is already open by another app", function() { + let addedSpy; + spyOn(editor, 'save').andCallFake(function() { + const error = new Error("EBUSY, resource busy or locked '/Some/dir/and-a-file.js'"); + error.code = 'EBUSY'; + error.path = '/Some/dir/and-a-file.js'; + throw error; + }); + + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()); + atom.workspace.saveActivePaneItem(); + expect(addedSpy).toHaveBeenCalled(); + + const notificaiton = addedSpy.mostRecentCall.args[0]; + expect(notificaiton.getType()).toBe('warning'); + return expect(notificaiton.getMessage()).toContain('Unable to save'); + }); + + it("emits a warning notification when the file system is read-only", function() { + let addedSpy; + spyOn(editor, 'save').andCallFake(function() { + const error = new Error("EROFS, read-only file system '/Some/dir/and-a-file.js'"); + error.code = 'EROFS'; + error.path = '/Some/dir/and-a-file.js'; + throw error; + }); + + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()); + atom.workspace.saveActivePaneItem(); + expect(addedSpy).toHaveBeenCalled(); + + const notification = addedSpy.mostRecentCall.args[0]; + expect(notification.getType()).toBe('warning'); + return expect(notification.getMessage()).toContain('Unable to save'); + }); + + return it("emits a warning notification when the file cannot be saved", function() { + spyOn(editor, 'save').andCallFake(function() { + throw new Error("no one knows"); + }); + + const save = () => atom.workspace.saveActivePaneItem(); + return expect(save).toThrow(); + }); + }); + }); + + describe("::closeActivePaneItemOrEmptyPaneOrWindow", function() { + beforeEach(function() { + spyOn(atom, 'close'); + return waitsForPromise(() => atom.workspace.open()); + }); + + return it("closes the active pane item, or the active pane if it is empty, or the current window if there is only the empty root pane", function() { + atom.config.set('core.destroyEmptyPanes', false); + + const pane1 = atom.workspace.getActivePane(); + const pane2 = pane1.splitRight({copyActiveItem: true}); + + expect(atom.workspace.getPanes().length).toBe(2); + expect(pane2.getItems().length).toBe(1); + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); + + expect(atom.workspace.getPanes().length).toBe(2); + expect(pane2.getItems().length).toBe(0); + + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); + + expect(atom.workspace.getPanes().length).toBe(1); + expect(pane1.getItems().length).toBe(1); + + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); + expect(atom.workspace.getPanes().length).toBe(1); + expect(pane1.getItems().length).toBe(0); + + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); + expect(atom.workspace.getPanes().length).toBe(1); + + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); + return expect(atom.close).toHaveBeenCalled(); + }); + }); + + describe("when the core.allowPendingPaneItems option is falsey", () => + it("does not open item with `pending: true` option as pending", function() { + let pane = null; + atom.config.set('core.allowPendingPaneItems', false); + + waitsForPromise(() => + atom.workspace.open('sample.js', {pending: true}).then(() => pane = atom.workspace.getActivePane()) + ); + + return runs(() => expect(pane.getPendingItem()).toBeFalsy()); + }) + ); + + describe("grammar activation", () => + it("notifies the workspace of which grammar is used", function() { + const editor = null; + atom.packages.triggerDeferredActivationHooks(); + + const javascriptGrammarUsed = jasmine.createSpy('js grammar used'); + const rubyGrammarUsed = jasmine.createSpy('ruby grammar used'); + const cGrammarUsed = jasmine.createSpy('c grammar used'); + + atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', javascriptGrammarUsed); + atom.packages.onDidTriggerActivationHook('language-ruby:grammar-used', rubyGrammarUsed); + atom.packages.onDidTriggerActivationHook('language-c:grammar-used', cGrammarUsed); + + waitsForPromise(() => atom.packages.activatePackage('language-ruby')); + waitsForPromise(() => atom.packages.activatePackage('language-javascript')); + waitsForPromise(() => atom.packages.activatePackage('language-c')); + waitsForPromise(() => atom.workspace.open('sample-with-comments.js')); + + return runs(function() { + // Hooks are triggered when opening new editors + expect(javascriptGrammarUsed).toHaveBeenCalled(); + + // Hooks are triggered when changing existing editors grammars + atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.c')); + expect(cGrammarUsed).toHaveBeenCalled(); + + // Hooks are triggered when editors are added in other ways. + atom.workspace.getActivePane().splitRight({copyActiveItem: true}); + atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.ruby')); + return expect(rubyGrammarUsed).toHaveBeenCalled(); + }); + }) + ); + + describe(".checkoutHeadRevision()", function() { + let editor = null; + beforeEach(function() { + atom.config.set("editor.confirmCheckoutHeadRevision", false); + + return waitsForPromise(() => atom.workspace.open('sample-with-comments.js').then(o => editor = o)); + }); + + it("reverts to the version of its file checked into the project repository", function() { + editor.setCursorBufferPosition([0, 0]); + editor.insertText("---\n"); + expect(editor.lineTextForBufferRow(0)).toBe("---"); + + waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor)); + + return runs(() => expect(editor.lineTextForBufferRow(0)).toBe("")); + }); + + return describe("when there's no repository for the editor's file", () => + it("doesn't do anything", function() { + editor = new TextEditor; + editor.setText("stuff"); + atom.workspace.checkoutHeadRevision(editor); + + return waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor)); + }) + ); + }); + + return escapeStringRegex = str => str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); +}); + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} diff --git a/src/workspace.coffee b/src/workspace.coffee deleted file mode 100644 index 2a46ce57a..000000000 --- a/src/workspace.coffee +++ /dev/null @@ -1,1121 +0,0 @@ -_ = require 'underscore-plus' -url = require 'url' -path = require 'path' -{Emitter, Disposable, CompositeDisposable} = require 'event-kit' -fs = require 'fs-plus' -{Directory} = require 'pathwatcher' -DefaultDirectorySearcher = require './default-directory-searcher' -Model = require './model' -TextEditor = require './text-editor' -PaneContainer = require './pane-container' -Panel = require './panel' -PanelContainer = require './panel-container' -Task = require './task' - -# Essential: Represents the state of the user interface for the entire window. -# An instance of this class is available via the `atom.workspace` global. -# -# Interact with this object to open files, be notified of current and future -# editors, and manipulate panes. To add panels, use {Workspace::addTopPanel} -# and friends. -# -# * `editor` {TextEditor} the new editor -# -module.exports = -class Workspace extends Model - constructor: (params) -> - super - - { - @packageManager, @config, @project, @grammarRegistry, @notificationManager, - @viewRegistry, @grammarRegistry, @applicationDelegate, @assert, - @deserializerManager, @textEditorRegistry - } = params - - @emitter = new Emitter - @openers = [] - @destroyedItemURIs = [] - - @paneContainer = new PaneContainer({@config, @applicationDelegate, @notificationManager, @deserializerManager}) - @paneContainer.onDidDestroyPaneItem(@didDestroyPaneItem) - - @defaultDirectorySearcher = new DefaultDirectorySearcher() - @consumeServices(@packageManager) - - # One cannot simply .bind here since it could be used as a component with - # Etch, in which case it'd be `new`d. And when it's `new`d, `this` is always - # the newly created object. - realThis = this - @buildTextEditor = -> Workspace.prototype.buildTextEditor.apply(realThis, arguments) - - @panelContainers = - top: new PanelContainer({location: 'top'}) - left: new PanelContainer({location: 'left'}) - right: new PanelContainer({location: 'right'}) - bottom: new PanelContainer({location: 'bottom'}) - header: new PanelContainer({location: 'header'}) - footer: new PanelContainer({location: 'footer'}) - modal: new PanelContainer({location: 'modal'}) - - @subscribeToEvents() - - reset: (@packageManager) -> - @emitter.dispose() - @emitter = new Emitter - - @paneContainer.destroy() - panelContainer.destroy() for panelContainer in @panelContainers - - @paneContainer = new PaneContainer({@config, @applicationDelegate, @notificationManager, @deserializerManager}) - @paneContainer.onDidDestroyPaneItem(@didDestroyPaneItem) - - @panelContainers = - top: new PanelContainer({location: 'top'}) - left: new PanelContainer({location: 'left'}) - right: new PanelContainer({location: 'right'}) - bottom: new PanelContainer({location: 'bottom'}) - header: new PanelContainer({location: 'header'}) - footer: new PanelContainer({location: 'footer'}) - modal: new PanelContainer({location: 'modal'}) - - @originalFontSize = null - @openers = [] - @destroyedItemURIs = [] - @consumeServices(@packageManager) - - subscribeToEvents: -> - @subscribeToActiveItem() - @subscribeToFontSize() - @subscribeToAddedItems() - - consumeServices: ({serviceHub}) -> - @directorySearchers = [] - serviceHub.consume( - 'atom.directory-searcher', - '^0.1.0', - (provider) => @directorySearchers.unshift(provider)) - - # Called by the Serializable mixin during serialization. - serialize: -> - deserializer: 'Workspace' - paneContainer: @paneContainer.serialize() - packagesWithActiveGrammars: @getPackageNamesWithActiveGrammars() - destroyedItemURIs: @destroyedItemURIs.slice() - - deserialize: (state, deserializerManager) -> - for packageName in state.packagesWithActiveGrammars ? [] - @packageManager.getLoadedPackage(packageName)?.loadGrammarsSync() - if state.destroyedItemURIs? - @destroyedItemURIs = state.destroyedItemURIs - @paneContainer.deserialize(state.paneContainer, deserializerManager) - - getPackageNamesWithActiveGrammars: -> - packageNames = [] - addGrammar = ({includedGrammarScopes, packageName}={}) => - return unless packageName - # Prevent cycles - return if packageNames.indexOf(packageName) isnt -1 - - packageNames.push(packageName) - for scopeName in includedGrammarScopes ? [] - addGrammar(@grammarRegistry.grammarForScopeName(scopeName)) - return - - editors = @getTextEditors() - addGrammar(editor.getGrammar()) for editor in editors - - if editors.length > 0 - for grammar in @grammarRegistry.getGrammars() when grammar.injectionSelector - addGrammar(grammar) - - _.uniq(packageNames) - - subscribeToActiveItem: -> - @updateWindowTitle() - @updateDocumentEdited() - @project.onDidChangePaths @updateWindowTitle - - @observeActivePaneItem (item) => - @updateWindowTitle() - @updateDocumentEdited() - - @activeItemSubscriptions?.dispose() - @activeItemSubscriptions = new CompositeDisposable - - if typeof item?.onDidChangeTitle is 'function' - titleSubscription = item.onDidChangeTitle(@updateWindowTitle) - else if typeof item?.on is 'function' - titleSubscription = item.on('title-changed', @updateWindowTitle) - unless typeof titleSubscription?.dispose is 'function' - titleSubscription = new Disposable => item.off('title-changed', @updateWindowTitle) - - if typeof item?.onDidChangeModified is 'function' - modifiedSubscription = item.onDidChangeModified(@updateDocumentEdited) - else if typeof item?.on? is 'function' - modifiedSubscription = item.on('modified-status-changed', @updateDocumentEdited) - unless typeof modifiedSubscription?.dispose is 'function' - modifiedSubscription = new Disposable => item.off('modified-status-changed', @updateDocumentEdited) - - @activeItemSubscriptions.add(titleSubscription) if titleSubscription? - @activeItemSubscriptions.add(modifiedSubscription) if modifiedSubscription? - - subscribeToAddedItems: -> - @onDidAddPaneItem ({item, pane, index}) => - if item instanceof TextEditor - subscriptions = new CompositeDisposable( - @textEditorRegistry.add(item) - @textEditorRegistry.maintainGrammar(item) - @textEditorRegistry.maintainConfig(item) - item.observeGrammar(@handleGrammarUsed.bind(this)) - ) - item.onDidDestroy -> subscriptions.dispose() - @emitter.emit 'did-add-text-editor', {textEditor: item, pane, index} - - # Updates the application's title and proxy icon based on whichever file is - # open. - updateWindowTitle: => - appName = 'Atom' - projectPaths = @project.getPaths() ? [] - if item = @getActivePaneItem() - itemPath = item.getPath?() - itemTitle = item.getLongTitle?() ? item.getTitle?() - projectPath = _.find projectPaths, (projectPath) -> - itemPath is projectPath or itemPath?.startsWith(projectPath + path.sep) - itemTitle ?= "untitled" - projectPath ?= if itemPath then path.dirname(itemPath) else projectPaths[0] - if projectPath? - projectPath = fs.tildify(projectPath) - - titleParts = [] - if item? and projectPath? - titleParts.push itemTitle, projectPath - representedPath = itemPath ? projectPath - else if projectPath? - titleParts.push projectPath - representedPath = projectPath - else - titleParts.push itemTitle - representedPath = "" - - unless process.platform is 'darwin' - titleParts.push appName - - document.title = titleParts.join(" \u2014 ") - @applicationDelegate.setRepresentedFilename(representedPath) - - # On macOS, fades the application window's proxy icon when the current file - # has been modified. - updateDocumentEdited: => - modified = @getActivePaneItem()?.isModified?() ? false - @applicationDelegate.setWindowDocumentEdited(modified) - - ### - Section: Event Subscription - ### - - # Essential: Invoke the given callback with all current and future text - # editors in the workspace. - # - # * `callback` {Function} to be called with current and future text editors. - # * `editor` An {TextEditor} that is present in {::getTextEditors} at the time - # of subscription or that is added at some later time. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeTextEditors: (callback) -> - callback(textEditor) for textEditor in @getTextEditors() - @onDidAddTextEditor ({textEditor}) -> callback(textEditor) - - # Essential: Invoke the given callback with all current and future panes items - # in the workspace. - # - # * `callback` {Function} to be called with current and future pane items. - # * `item` An item that is present in {::getPaneItems} at the time of - # subscription or that is added at some later time. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observePaneItems: (callback) -> @paneContainer.observePaneItems(callback) - - # Essential: Invoke the given callback when the active pane item changes. - # - # Because observers are invoked synchronously, it's important not to perform - # any expensive operations via this method. Consider - # {::onDidStopChangingActivePaneItem} to delay operations until after changes - # stop occurring. - # - # * `callback` {Function} to be called when the active pane item changes. - # * `item` The active pane item. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeActivePaneItem: (callback) -> - @paneContainer.onDidChangeActivePaneItem(callback) - - # Essential: Invoke the given callback when the active pane item stops - # changing. - # - # Observers are called asynchronously 100ms after the last active pane item - # change. Handling changes here rather than in the synchronous - # {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly - # changing or closing tabs and ensures critical UI feedback, like changing the - # highlighted tab, gets priority over work that can be done asynchronously. - # - # * `callback` {Function} to be called when the active pane item stopts - # changing. - # * `item` The active pane item. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidStopChangingActivePaneItem: (callback) -> - @paneContainer.onDidStopChangingActivePaneItem(callback) - - # Essential: Invoke the given callback with the current active pane item and - # with all future active pane items in the workspace. - # - # * `callback` {Function} to be called when the active pane item changes. - # * `item` The current active pane item. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeActivePaneItem: (callback) -> @paneContainer.observeActivePaneItem(callback) - - # Essential: Invoke the given callback whenever an item is opened. Unlike - # {::onDidAddPaneItem}, observers will be notified for items that are already - # present in the workspace when they are reopened. - # - # * `callback` {Function} to be called whenever an item is opened. - # * `event` {Object} with the following keys: - # * `uri` {String} representing the opened URI. Could be `undefined`. - # * `item` The opened item. - # * `pane` The pane in which the item was opened. - # * `index` The index of the opened item on its pane. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidOpen: (callback) -> - @emitter.on 'did-open', callback - - # Extended: Invoke the given callback when a pane is added to the workspace. - # - # * `callback` {Function} to be called panes are added. - # * `event` {Object} with the following keys: - # * `pane` The added pane. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddPane: (callback) -> @paneContainer.onDidAddPane(callback) - - # Extended: Invoke the given callback before a pane is destroyed in the - # workspace. - # - # * `callback` {Function} to be called before panes are destroyed. - # * `event` {Object} with the following keys: - # * `pane` The pane to be destroyed. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onWillDestroyPane: (callback) -> @paneContainer.onWillDestroyPane(callback) - - # Extended: Invoke the given callback when a pane is destroyed in the - # workspace. - # - # * `callback` {Function} to be called panes are destroyed. - # * `event` {Object} with the following keys: - # * `pane` The destroyed pane. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroyPane: (callback) -> @paneContainer.onDidDestroyPane(callback) - - # Extended: Invoke the given callback with all current and future panes in the - # workspace. - # - # * `callback` {Function} to be called with current and future panes. - # * `pane` A {Pane} that is present in {::getPanes} at the time of - # subscription or that is added at some later time. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observePanes: (callback) -> @paneContainer.observePanes(callback) - - # Extended: Invoke the given callback when the active pane changes. - # - # * `callback` {Function} to be called when the active pane changes. - # * `pane` A {Pane} that is the current return value of {::getActivePane}. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeActivePane: (callback) -> @paneContainer.onDidChangeActivePane(callback) - - # Extended: Invoke the given callback with the current active pane and when - # the active pane changes. - # - # * `callback` {Function} to be called with the current and future active# - # panes. - # * `pane` A {Pane} that is the current return value of {::getActivePane}. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeActivePane: (callback) -> @paneContainer.observeActivePane(callback) - - # Extended: Invoke the given callback when a pane item is added to the - # workspace. - # - # * `callback` {Function} to be called when pane items are added. - # * `event` {Object} with the following keys: - # * `item` The added pane item. - # * `pane` {Pane} containing the added item. - # * `index` {Number} indicating the index of the added item in its pane. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddPaneItem: (callback) -> @paneContainer.onDidAddPaneItem(callback) - - # Extended: Invoke the given callback when a pane item is about to be - # destroyed, before the user is prompted to save it. - # - # * `callback` {Function} to be called before pane items are destroyed. - # * `event` {Object} with the following keys: - # * `item` The item to be destroyed. - # * `pane` {Pane} containing the item to be destroyed. - # * `index` {Number} indicating the index of the item to be destroyed in - # its pane. - # - # Returns a {Disposable} on which `.dispose` can be called to unsubscribe. - onWillDestroyPaneItem: (callback) -> @paneContainer.onWillDestroyPaneItem(callback) - - # Extended: Invoke the given callback when a pane item is destroyed. - # - # * `callback` {Function} to be called when pane items are destroyed. - # * `event` {Object} with the following keys: - # * `item` The destroyed item. - # * `pane` {Pane} containing the destroyed item. - # * `index` {Number} indicating the index of the destroyed item in its - # pane. - # - # Returns a {Disposable} on which `.dispose` can be called to unsubscribe. - onDidDestroyPaneItem: (callback) -> @paneContainer.onDidDestroyPaneItem(callback) - - # Extended: Invoke the given callback when a text editor is added to the - # workspace. - # - # * `callback` {Function} to be called panes are added. - # * `event` {Object} with the following keys: - # * `textEditor` {TextEditor} that was added. - # * `pane` {Pane} containing the added text editor. - # * `index` {Number} indicating the index of the added text editor in its - # pane. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddTextEditor: (callback) -> - @emitter.on 'did-add-text-editor', callback - - ### - Section: Opening - ### - - # Essential: Opens the given URI in Atom asynchronously. - # If the URI is already open, the existing item for that URI will be - # activated. If no URI is given, or no registered opener can open - # the URI, a new empty {TextEditor} will be created. - # - # * `uri` (optional) A {String} containing a URI. - # * `options` (optional) {Object} - # * `initialLine` A {Number} indicating which row to move the cursor to - # initially. Defaults to `0`. - # * `initialColumn` A {Number} indicating which column to move the cursor to - # initially. Defaults to `0`. - # * `split` Either 'left', 'right', 'up' or 'down'. - # If 'left', the item will be opened in leftmost pane of the current active pane's row. - # If 'right', the item will be opened in the rightmost pane of the current active pane's row. If only one pane exists in the row, a new pane will be created. - # If 'up', the item will be opened in topmost pane of the current active pane's column. - # If 'down', the item will be opened in the bottommost pane of the current active pane's column. If only one pane exists in the column, a new pane will be created. - # * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on - # containing pane. Defaults to `true`. - # * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} - # on containing pane. Defaults to `true`. - # * `pending` A {Boolean} indicating whether or not the item should be opened - # in a pending state. Existing pending items in a pane are replaced with - # new pending items when they are opened. - # * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to - # activate an existing item for the given URI on any pane. - # If `false`, only the active pane will be searched for - # an existing item for the same URI. Defaults to `false`. - # - # Returns a {Promise} that resolves to the {TextEditor} for the file URI. - open: (uri, options={}) -> - searchAllPanes = options.searchAllPanes - split = options.split - uri = @project.resolvePath(uri) - - if not atom.config.get('core.allowPendingPaneItems') - options.pending = false - - # Avoid adding URLs as recent documents to work-around this Spotlight crash: - # https://github.com/atom/atom/issues/10071 - if uri? and (not url.parse(uri).protocol? or process.platform is 'win32') - @applicationDelegate.addRecentDocument(uri) - - pane = @paneContainer.paneForURI(uri) if searchAllPanes - pane ?= switch split - when 'left' - @getActivePane().findLeftmostSibling() - when 'right' - @getActivePane().findOrCreateRightmostSibling() - when 'up' - @getActivePane().findTopmostSibling() - when 'down' - @getActivePane().findOrCreateBottommostSibling() - else - @getActivePane() - - @openURIInPane(uri, pane, options) - - # Open Atom's license in the active pane. - openLicense: -> - @open(path.join(process.resourcesPath, 'LICENSE.md')) - - # Synchronously open the given URI in the active pane. **Only use this method - # in specs. Calling this in production code will block the UI thread and - # everyone will be mad at you.** - # - # * `uri` A {String} containing a URI. - # * `options` An optional options {Object} - # * `initialLine` A {Number} indicating which row to move the cursor to - # initially. Defaults to `0`. - # * `initialColumn` A {Number} indicating which column to move the cursor to - # initially. Defaults to `0`. - # * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on - # the containing pane. Defaults to `true`. - # * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} - # on containing pane. Defaults to `true`. - openSync: (uri='', options={}) -> - {initialLine, initialColumn} = options - activatePane = options.activatePane ? true - activateItem = options.activateItem ? true - - uri = @project.resolvePath(uri) - item = @getActivePane().itemForURI(uri) - if uri - item ?= opener(uri, options) for opener in @getOpeners() when not item - item ?= @project.openSync(uri, {initialLine, initialColumn}) - - @getActivePane().activateItem(item) if activateItem - @itemOpened(item) - @getActivePane().activate() if activatePane - item - - openURIInPane: (uri, pane, options={}) -> - activatePane = options.activatePane ? true - activateItem = options.activateItem ? true - - if uri? - if item = pane.itemForURI(uri) - pane.clearPendingItem() if not options.pending and pane.getPendingItem() is item - item ?= opener(uri, options) for opener in @getOpeners() when not item - - try - item ?= @openTextFile(uri, options) - catch error - switch error.code - when 'CANCELLED' - return Promise.resolve() - when 'EACCES' - @notificationManager.addWarning("Permission denied '#{error.path}'") - return Promise.resolve() - when 'EPERM', 'EBUSY', 'ENXIO', 'EIO', 'ENOTCONN', 'UNKNOWN', 'ECONNRESET', 'EINVAL', 'EMFILE', 'ENOTDIR', 'EAGAIN' - @notificationManager.addWarning("Unable to open '#{error.path ? uri}'", detail: error.message) - return Promise.resolve() - else - throw error - - Promise.resolve(item) - .then (item) => - return item if pane.isDestroyed() - - @itemOpened(item) - pane.activateItem(item, {pending: options.pending}) if activateItem - pane.activate() if activatePane - - initialLine = initialColumn = 0 - unless Number.isNaN(options.initialLine) - initialLine = options.initialLine - unless Number.isNaN(options.initialColumn) - initialColumn = options.initialColumn - if initialLine >= 0 or initialColumn >= 0 - item.setCursorBufferPosition?([initialLine, initialColumn]) - - index = pane.getActiveItemIndex() - @emitter.emit 'did-open', {uri, pane, item, index} - item - - openTextFile: (uri, options) -> - filePath = @project.resolvePath(uri) - - if filePath? - try - fs.closeSync(fs.openSync(filePath, 'r')) - catch error - # allow ENOENT errors to create an editor for paths that dont exist - throw error unless error.code is 'ENOENT' - - fileSize = fs.getSizeSync(filePath) - - largeFileMode = fileSize >= 2 * 1048576 # 2MB - if fileSize >= @config.get('core.warnOnLargeFileLimit') * 1048576 # 20MB by default - choice = @applicationDelegate.confirm - message: 'Atom will be unresponsive during the loading of very large files.' - detailedMessage: "Do you still want to load this file?" - buttons: ["Proceed", "Cancel"] - if choice is 1 - error = new Error - error.code = 'CANCELLED' - throw error - - @project.bufferForPath(filePath, options).then (buffer) => - @textEditorRegistry.build(Object.assign({buffer, largeFileMode, autoHeight: false}, options)) - - handleGrammarUsed: (grammar) -> - return unless grammar? - - @packageManager.triggerActivationHook("#{grammar.packageName}:grammar-used") - - # Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`. - # - # * `object` An {Object} you want to perform the check against. - isTextEditor: (object) -> - object instanceof TextEditor - - # Extended: Create a new text editor. - # - # Returns a {TextEditor}. - buildTextEditor: (params) -> - editor = @textEditorRegistry.build(params) - subscriptions = new CompositeDisposable( - @textEditorRegistry.maintainGrammar(editor) - @textEditorRegistry.maintainConfig(editor), - ) - editor.onDidDestroy -> subscriptions.dispose() - editor - - # Public: Asynchronously reopens the last-closed item's URI if it hasn't already been - # reopened. - # - # Returns a {Promise} that is resolved when the item is opened - reopenItem: -> - if uri = @destroyedItemURIs.pop() - @open(uri) - else - Promise.resolve() - - # Public: Register an opener for a uri. - # - # When a URI is opened via {Workspace::open}, Atom loops through its registered - # opener functions until one returns a value for the given uri. - # Openers are expected to return an object that inherits from HTMLElement or - # a model which has an associated view in the {ViewRegistry}. - # A {TextEditor} will be used if no opener returns a value. - # - # ## Examples - # - # ```coffee - # atom.workspace.addOpener (uri) -> - # if path.extname(uri) is '.toml' - # return new TomlEditor(uri) - # ``` - # - # * `opener` A {Function} to be called when a path is being opened. - # - # Returns a {Disposable} on which `.dispose()` can be called to remove the - # opener. - # - # Note that the opener will be called if and only if the URI is not already open - # in the current pane. The searchAllPanes flag expands the search from the - # current pane to all panes. If you wish to open a view of a different type for - # a file that is already open, consider changing the protocol of the URI. For - # example, perhaps you wish to preview a rendered version of the file `/foo/bar/baz.quux` - # that is already open in a text editor view. You could signal this by calling - # {Workspace::open} on the URI `quux-preview://foo/bar/baz.quux`. Then your opener - # can check the protocol for quux-preview and only handle those URIs that match. - addOpener: (opener) -> - @openers.push(opener) - new Disposable => _.remove(@openers, opener) - - getOpeners: -> - @openers - - ### - Section: Pane Items - ### - - # Essential: Get all pane items in the workspace. - # - # Returns an {Array} of items. - getPaneItems: -> - @paneContainer.getPaneItems() - - # Essential: Get the active {Pane}'s active item. - # - # Returns an pane item {Object}. - getActivePaneItem: -> - @paneContainer.getActivePaneItem() - - # Essential: Get all text editors in the workspace. - # - # Returns an {Array} of {TextEditor}s. - getTextEditors: -> - @getPaneItems().filter (item) -> item instanceof TextEditor - - # Essential: Get the active item if it is an {TextEditor}. - # - # Returns an {TextEditor} or `undefined` if the current active item is not an - # {TextEditor}. - getActiveTextEditor: -> - activeItem = @getActivePaneItem() - activeItem if activeItem instanceof TextEditor - - # Save all pane items. - saveAll: -> - @paneContainer.saveAll() - - confirmClose: (options) -> - @paneContainer.confirmClose(options) - - # Save the active pane item. - # - # If the active pane item currently has a URI according to the item's - # `.getURI` method, calls `.save` on the item. Otherwise - # {::saveActivePaneItemAs} # will be called instead. This method does nothing - # if the active item does not implement a `.save` method. - saveActivePaneItem: -> - @getActivePane().saveActiveItem() - - # Prompt the user for a path and save the active pane item to it. - # - # Opens a native dialog where the user selects a path on disk, then calls - # `.saveAs` on the item with the selected path. This method does nothing if - # the active item does not implement a `.saveAs` method. - saveActivePaneItemAs: -> - @getActivePane().saveActiveItemAs() - - # Destroy (close) the active pane item. - # - # Removes the active pane item and calls the `.destroy` method on it if one is - # defined. - destroyActivePaneItem: -> - @getActivePane().destroyActiveItem() - - ### - Section: Panes - ### - - # Extended: Get all panes in the workspace. - # - # Returns an {Array} of {Pane}s. - getPanes: -> - @paneContainer.getPanes() - - # Extended: Get the active {Pane}. - # - # Returns a {Pane}. - getActivePane: -> - @paneContainer.getActivePane() - - # Extended: Make the next pane active. - activateNextPane: -> - @paneContainer.activateNextPane() - - # Extended: Make the previous pane active. - activatePreviousPane: -> - @paneContainer.activatePreviousPane() - - # Extended: Get the first {Pane} with an item for the given URI. - # - # * `uri` {String} uri - # - # Returns a {Pane} or `undefined` if no pane exists for the given URI. - paneForURI: (uri) -> - @paneContainer.paneForURI(uri) - - # Extended: Get the {Pane} containing the given item. - # - # * `item` Item the returned pane contains. - # - # Returns a {Pane} or `undefined` if no pane exists for the given item. - paneForItem: (item) -> - @paneContainer.paneForItem(item) - - # Destroy (close) the active pane. - destroyActivePane: -> - @getActivePane()?.destroy() - - # Close the active pane item, or the active pane if it is empty, - # or the current window if there is only the empty root pane. - closeActivePaneItemOrEmptyPaneOrWindow: -> - if @getActivePaneItem()? - @destroyActivePaneItem() - else if @getPanes().length > 1 - @destroyActivePane() - else if @config.get('core.closeEmptyWindows') - atom.close() - - # Increase the editor font size by 1px. - increaseFontSize: -> - @config.set("editor.fontSize", @config.get("editor.fontSize") + 1) - - # Decrease the editor font size by 1px. - decreaseFontSize: -> - fontSize = @config.get("editor.fontSize") - @config.set("editor.fontSize", fontSize - 1) if fontSize > 1 - - # Restore to the window's original editor font size. - resetFontSize: -> - if @originalFontSize - @config.set("editor.fontSize", @originalFontSize) - - subscribeToFontSize: -> - @config.onDidChange 'editor.fontSize', ({oldValue}) => - @originalFontSize ?= oldValue - - # Removes the item's uri from the list of potential items to reopen. - itemOpened: (item) -> - if typeof item.getURI is 'function' - uri = item.getURI() - else if typeof item.getUri is 'function' - uri = item.getUri() - - if uri? - _.remove(@destroyedItemURIs, uri) - - # Adds the destroyed item's uri to the list of items to reopen. - didDestroyPaneItem: ({item}) => - if typeof item.getURI is 'function' - uri = item.getURI() - else if typeof item.getUri is 'function' - uri = item.getUri() - - if uri? - @destroyedItemURIs.push(uri) - - # Called by Model superclass when destroyed - destroyed: -> - @paneContainer.destroy() - @activeItemSubscriptions?.dispose() - - - ### - Section: Panels - - Panels are used to display UI related to an editor window. They are placed at one of the four - edges of the window: left, right, top or bottom. If there are multiple panels on the same window - edge they are stacked in order of priority: higher priority is closer to the center, lower - priority towards the edge. - - *Note:* If your panel changes its size throughout its lifetime, consider giving it a higher - priority, allowing fixed size panels to be closer to the edge. This allows control targets to - remain more static for easier targeting by users that employ mice or trackpads. (See - [atom/atom#4834](https://github.com/atom/atom/issues/4834) for discussion.) - ### - - # Essential: Get an {Array} of all the panel items at the bottom of the editor window. - getBottomPanels: -> - @getPanels('bottom') - - # Essential: Adds a panel item to the bottom of the editor window. - # - # * `options` {Object} - # * `item` Your panel content. It can be DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # latter. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addBottomPanel: (options) -> - @addPanel('bottom', options) - - # Essential: Get an {Array} of all the panel items to the left of the editor window. - getLeftPanels: -> - @getPanels('left') - - # Essential: Adds a panel item to the left of the editor window. - # - # * `options` {Object} - # * `item` Your panel content. It can be DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # latter. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addLeftPanel: (options) -> - @addPanel('left', options) - - # Essential: Get an {Array} of all the panel items to the right of the editor window. - getRightPanels: -> - @getPanels('right') - - # Essential: Adds a panel item to the right of the editor window. - # - # * `options` {Object} - # * `item` Your panel content. It can be DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # latter. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addRightPanel: (options) -> - @addPanel('right', options) - - # Essential: Get an {Array} of all the panel items at the top of the editor window. - getTopPanels: -> - @getPanels('top') - - # Essential: Adds a panel item to the top of the editor window above the tabs. - # - # * `options` {Object} - # * `item` Your panel content. It can be DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # latter. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addTopPanel: (options) -> - @addPanel('top', options) - - # Essential: Get an {Array} of all the panel items in the header. - getHeaderPanels: -> - @getPanels('header') - - # Essential: Adds a panel item to the header. - # - # * `options` {Object} - # * `item` Your panel content. It can be DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # latter. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addHeaderPanel: (options) -> - @addPanel('header', options) - - # Essential: Get an {Array} of all the panel items in the footer. - getFooterPanels: -> - @getPanels('footer') - - # Essential: Adds a panel item to the footer. - # - # * `options` {Object} - # * `item` Your panel content. It can be DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # latter. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addFooterPanel: (options) -> - @addPanel('footer', options) - - # Essential: Get an {Array} of all the modal panel items - getModalPanels: -> - @getPanels('modal') - - # Essential: Adds a panel item as a modal dialog. - # - # * `options` {Object} - # * `item` Your panel content. It can be a DOM element, a jQuery element, or - # a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the - # model option. See {ViewRegistry::addViewProvider} for more information. - # * `visible` (optional) {Boolean} false if you want the panel to initially be hidden - # (default: true) - # * `priority` (optional) {Number} Determines stacking order. Lower priority items are - # forced closer to the edges of the window. (default: 100) - # - # Returns a {Panel} - addModalPanel: (options={}) -> - @addPanel('modal', options) - - # Essential: Returns the {Panel} associated with the given item. Returns - # `null` when the item has no panel. - # - # * `item` Item the panel contains - panelForItem: (item) -> - for location, container of @panelContainers - panel = container.panelForItem(item) - return panel if panel? - null - - getPanels: (location) -> - @panelContainers[location].getPanels() - - addPanel: (location, options) -> - options ?= {} - @panelContainers[location].addPanel(new Panel(options)) - - ### - Section: Searching and Replacing - ### - - # Public: Performs a search across all files in the workspace. - # - # * `regex` {RegExp} to search with. - # * `options` (optional) {Object} - # * `paths` An {Array} of glob patterns to search within. - # * `onPathsSearched` (optional) {Function} to be periodically called - # with number of paths searched. - # * `iterator` {Function} callback on each file found. - # - # Returns a {Promise} with a `cancel()` method that will cancel all - # of the underlying searches that were started as part of this scan. - scan: (regex, options={}, iterator) -> - if _.isFunction(options) - iterator = options - options = {} - - # Find a searcher for every Directory in the project. Each searcher that is matched - # will be associated with an Array of Directory objects in the Map. - directoriesForSearcher = new Map() - for directory in @project.getDirectories() - searcher = @defaultDirectorySearcher - for directorySearcher in @directorySearchers - if directorySearcher.canSearchDirectory(directory) - searcher = directorySearcher - break - directories = directoriesForSearcher.get(searcher) - unless directories - directories = [] - directoriesForSearcher.set(searcher, directories) - directories.push(directory) - - # Define the onPathsSearched callback. - if _.isFunction(options.onPathsSearched) - # Maintain a map of directories to the number of search results. When notified of a new count, - # replace the entry in the map and update the total. - onPathsSearchedOption = options.onPathsSearched - totalNumberOfPathsSearched = 0 - numberOfPathsSearchedForSearcher = new Map() - onPathsSearched = (searcher, numberOfPathsSearched) -> - oldValue = numberOfPathsSearchedForSearcher.get(searcher) - if oldValue - totalNumberOfPathsSearched -= oldValue - numberOfPathsSearchedForSearcher.set(searcher, numberOfPathsSearched) - totalNumberOfPathsSearched += numberOfPathsSearched - onPathsSearchedOption(totalNumberOfPathsSearched) - else - onPathsSearched = -> - - # Kick off all of the searches and unify them into one Promise. - allSearches = [] - directoriesForSearcher.forEach (directories, searcher) => - searchOptions = - inclusions: options.paths or [] - includeHidden: true - excludeVcsIgnores: @config.get('core.excludeVcsIgnoredPaths') - exclusions: @config.get('core.ignoredNames') - follow: @config.get('core.followSymlinks') - didMatch: (result) => - iterator(result) unless @project.isPathModified(result.filePath) - didError: (error) -> - iterator(null, error) - didSearchPaths: (count) -> onPathsSearched(searcher, count) - directorySearcher = searcher.search(directories, regex, searchOptions) - allSearches.push(directorySearcher) - searchPromise = Promise.all(allSearches) - - for buffer in @project.getBuffers() when buffer.isModified() - filePath = buffer.getPath() - continue unless @project.contains(filePath) - matches = [] - buffer.scan regex, (match) -> matches.push match - iterator {filePath, matches} if matches.length > 0 - - # Make sure the Promise that is returned to the client is cancelable. To be consistent - # with the existing behavior, instead of cancel() rejecting the promise, it should - # resolve it with the special value 'cancelled'. At least the built-in find-and-replace - # package relies on this behavior. - isCancelled = false - cancellablePromise = new Promise (resolve, reject) -> - onSuccess = -> - if isCancelled - resolve('cancelled') - else - resolve(null) - - onFailure = -> - promise.cancel() for promise in allSearches - reject() - - searchPromise.then(onSuccess, onFailure) - cancellablePromise.cancel = -> - isCancelled = true - # Note that cancelling all of the members of allSearches will cause all of the searches - # to resolve, which causes searchPromise to resolve, which is ultimately what causes - # cancellablePromise to resolve. - promise.cancel() for promise in allSearches - - # Although this method claims to return a `Promise`, the `ResultsPaneView.onSearch()` - # method in the find-and-replace package expects the object returned by this method to have a - # `done()` method. Include a done() method until find-and-replace can be updated. - cancellablePromise.done = (onSuccessOrFailure) -> - cancellablePromise.then(onSuccessOrFailure, onSuccessOrFailure) - cancellablePromise - - # Public: Performs a replace across all the specified files in the project. - # - # * `regex` A {RegExp} to search with. - # * `replacementText` {String} to replace all matches of regex with. - # * `filePaths` An {Array} of file path strings to run the replace on. - # * `iterator` A {Function} callback on each file with replacements: - # * `options` {Object} with keys `filePath` and `replacements`. - # - # Returns a {Promise}. - replace: (regex, replacementText, filePaths, iterator) -> - new Promise (resolve, reject) => - openPaths = (buffer.getPath() for buffer in @project.getBuffers()) - outOfProcessPaths = _.difference(filePaths, openPaths) - - inProcessFinished = not openPaths.length - outOfProcessFinished = not outOfProcessPaths.length - checkFinished = -> - resolve() if outOfProcessFinished and inProcessFinished - - unless outOfProcessFinished.length - flags = 'g' - flags += 'i' if regex.ignoreCase - - task = Task.once require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, -> - outOfProcessFinished = true - checkFinished() - - task.on 'replace:path-replaced', iterator - task.on 'replace:file-error', (error) -> iterator(null, error) - - for buffer in @project.getBuffers() - continue unless buffer.getPath() in filePaths - replacements = buffer.replace(regex, replacementText, iterator) - iterator({filePath: buffer.getPath(), replacements}) if replacements - - inProcessFinished = true - checkFinished() - - checkoutHeadRevision: (editor) -> - if editor.getPath() - checkoutHead = => - @project.repositoryForDirectory(new Directory(editor.getDirectoryPath())) - .then (repository) -> - repository?.checkoutHeadForEditor(editor) - - if @config.get('editor.confirmCheckoutHeadRevision') - @applicationDelegate.confirm - message: 'Confirm Checkout HEAD Revision' - detailedMessage: "Are you sure you want to discard all changes to \"#{editor.getFileName()}\" since the last Git commit?" - buttons: - OK: checkoutHead - Cancel: null - else - checkoutHead() - else - Promise.resolve(false) diff --git a/src/workspace.js b/src/workspace.js new file mode 100644 index 000000000..612224da3 --- /dev/null +++ b/src/workspace.js @@ -0,0 +1,1314 @@ +let Workspace; +const _ = require('underscore-plus'); +const url = require('url'); +const path = require('path'); +const {Emitter, Disposable, CompositeDisposable} = require('event-kit'); +const fs = require('fs-plus'); +const {Directory} = require('pathwatcher'); +const DefaultDirectorySearcher = require('./default-directory-searcher'); +const Model = require('./model'); +const TextEditor = require('./text-editor'); +const PaneContainer = require('./pane-container'); +const Panel = require('./panel'); +const PanelContainer = require('./panel-container'); +const Task = require('./task'); + +// Essential: Represents the state of the user interface for the entire window. +// An instance of this class is available via the `atom.workspace` global. +// +// Interact with this object to open files, be notified of current and future +// editors, and manipulate panes. To add panels, use {Workspace::addTopPanel} +// and friends. +// +// * `editor` {TextEditor} the new editor +// +module.exports = +Workspace = class Workspace extends Model { + constructor(params) { + this.updateWindowTitle = this.updateWindowTitle.bind(this); + this.updateDocumentEdited = this.updateDocumentEdited.bind(this); + this.didDestroyPaneItem = this.didDestroyPaneItem.bind(this); + super(...arguments); + + ({ + packageManager: this.packageManager, config: this.config, project: this.project, grammarRegistry: this.grammarRegistry, notificationManager: this.notificationManager, + viewRegistry: this.viewRegistry, grammarRegistry: this.grammarRegistry, applicationDelegate: this.applicationDelegate, assert: this.assert, + deserializerManager: this.deserializerManager, textEditorRegistry: this.textEditorRegistry + } = params); + + this.emitter = new Emitter; + this.openers = []; + this.destroyedItemURIs = []; + + this.paneContainer = new PaneContainer({config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, deserializerManager: this.deserializerManager}); + this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem); + + this.defaultDirectorySearcher = new DefaultDirectorySearcher(); + this.consumeServices(this.packageManager); + + // One cannot simply .bind here since it could be used as a component with + // Etch, in which case it'd be `new`d. And when it's `new`d, `this` is always + // the newly created object. + const realThis = this; + this.buildTextEditor = function() { return Workspace.prototype.buildTextEditor.apply(realThis, arguments); }; + + this.panelContainers = { + top: new PanelContainer({location: 'top'}), + left: new PanelContainer({location: 'left'}), + right: new PanelContainer({location: 'right'}), + bottom: new PanelContainer({location: 'bottom'}), + header: new PanelContainer({location: 'header'}), + footer: new PanelContainer({location: 'footer'}), + modal: new PanelContainer({location: 'modal'}) + }; + + this.subscribeToEvents(); + } + + reset(packageManager) { + this.packageManager = packageManager; + this.emitter.dispose(); + this.emitter = new Emitter; + + this.paneContainer.destroy(); + for (let panelContainer of this.panelContainers) { panelContainer.destroy(); } + + this.paneContainer = new PaneContainer({config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, deserializerManager: this.deserializerManager}); + this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem); + + this.panelContainers = { + top: new PanelContainer({location: 'top'}), + left: new PanelContainer({location: 'left'}), + right: new PanelContainer({location: 'right'}), + bottom: new PanelContainer({location: 'bottom'}), + header: new PanelContainer({location: 'header'}), + footer: new PanelContainer({location: 'footer'}), + modal: new PanelContainer({location: 'modal'}) + }; + + this.originalFontSize = null; + this.openers = []; + this.destroyedItemURIs = []; + return this.consumeServices(this.packageManager); + } + + subscribeToEvents() { + this.subscribeToActiveItem(); + this.subscribeToFontSize(); + return this.subscribeToAddedItems(); + } + + consumeServices({serviceHub}) { + this.directorySearchers = []; + return serviceHub.consume( + 'atom.directory-searcher', + '^0.1.0', + provider => this.directorySearchers.unshift(provider)); + } + + // Called by the Serializable mixin during serialization. + serialize() { + return { + deserializer: 'Workspace', + paneContainer: this.paneContainer.serialize(), + packagesWithActiveGrammars: this.getPackageNamesWithActiveGrammars(), + destroyedItemURIs: this.destroyedItemURIs.slice() + }; + } + + deserialize(state, deserializerManager) { + for (let packageName of state.packagesWithActiveGrammars != null ? state.packagesWithActiveGrammars : []) { + __guard__(this.packageManager.getLoadedPackage(packageName), x => x.loadGrammarsSync()); + } + if (state.destroyedItemURIs != null) { + this.destroyedItemURIs = state.destroyedItemURIs; + } + return this.paneContainer.deserialize(state.paneContainer, deserializerManager); + } + + getPackageNamesWithActiveGrammars() { + const packageNames = []; + var addGrammar = ({includedGrammarScopes, packageName}={}) => { + if (!packageName) { return; } + // Prevent cycles + if (packageNames.indexOf(packageName) !== -1) { return; } + + packageNames.push(packageName); + for (let scopeName of includedGrammarScopes != null ? includedGrammarScopes : []) { + addGrammar(this.grammarRegistry.grammarForScopeName(scopeName)); + } + }; + + const editors = this.getTextEditors(); + for (let editor of editors) { addGrammar(editor.getGrammar()); } + + if (editors.length > 0) { + for (let grammar of this.grammarRegistry.getGrammars()) { + if (grammar.injectionSelector) { + addGrammar(grammar); + } + } + } + + return _.uniq(packageNames); + } + + subscribeToActiveItem() { + this.updateWindowTitle(); + this.updateDocumentEdited(); + this.project.onDidChangePaths(this.updateWindowTitle); + + return this.observeActivePaneItem(item => { + let modifiedSubscription, titleSubscription; + this.updateWindowTitle(); + this.updateDocumentEdited(); + + if (this.activeItemSubscriptions != null) { + this.activeItemSubscriptions.dispose(); + } + this.activeItemSubscriptions = new CompositeDisposable; + + if (typeof (item != null ? item.onDidChangeTitle : undefined) === 'function') { + titleSubscription = item.onDidChangeTitle(this.updateWindowTitle); + } else if (typeof (item != null ? item.on : undefined) === 'function') { + titleSubscription = item.on('title-changed', this.updateWindowTitle); + if (typeof (titleSubscription != null ? titleSubscription.dispose : undefined) !== 'function') { + titleSubscription = new Disposable((function() { return item.off('title-changed', this.updateWindowTitle); }.bind(this))); + } + } + + if (typeof (item != null ? item.onDidChangeModified : undefined) === 'function') { + modifiedSubscription = item.onDidChangeModified(this.updateDocumentEdited); + } else if (typeof ((item != null ? item.on : undefined) != null) === 'function') { + modifiedSubscription = item.on('modified-status-changed', this.updateDocumentEdited); + if (typeof (modifiedSubscription != null ? modifiedSubscription.dispose : undefined) !== 'function') { + modifiedSubscription = new Disposable((function() { return item.off('modified-status-changed', this.updateDocumentEdited); }.bind(this))); + } + } + + if (titleSubscription != null) { this.activeItemSubscriptions.add(titleSubscription); } + if (modifiedSubscription != null) { return this.activeItemSubscriptions.add(modifiedSubscription); } + } + ); + } + + subscribeToAddedItems() { + return this.onDidAddPaneItem(({item, pane, index}) => { + if (item instanceof TextEditor) { + const subscriptions = new CompositeDisposable( + this.textEditorRegistry.add(item), + this.textEditorRegistry.maintainGrammar(item), + this.textEditorRegistry.maintainConfig(item), + item.observeGrammar(this.handleGrammarUsed.bind(this)) + ); + item.onDidDestroy(() => subscriptions.dispose()); + return this.emitter.emit('did-add-text-editor', {textEditor: item, pane, index}); + } + }); + } + + // Updates the application's title and proxy icon based on whichever file is + // open. + updateWindowTitle() { + let item, itemPath, itemTitle, left, projectPath, representedPath; + const appName = 'Atom'; + const projectPaths = (left = this.project.getPaths()) != null ? left : []; + if (item = this.getActivePaneItem()) { + let left1; + itemPath = typeof item.getPath === 'function' ? item.getPath() : undefined; + itemTitle = (left1 = (typeof item.getLongTitle === 'function' ? item.getLongTitle() : undefined)) != null ? left1 : (typeof item.getTitle === 'function' ? item.getTitle() : undefined); + projectPath = _.find(projectPaths, projectPath => (itemPath === projectPath) || (itemPath != null ? itemPath.startsWith(projectPath + path.sep) : undefined)); + } + if (itemTitle == null) { itemTitle = "untitled"; } + if (projectPath == null) { projectPath = itemPath ? path.dirname(itemPath) : projectPaths[0]; } + if (projectPath != null) { + projectPath = fs.tildify(projectPath); + } + + const titleParts = []; + if ((item != null) && (projectPath != null)) { + titleParts.push(itemTitle, projectPath); + representedPath = itemPath != null ? itemPath : projectPath; + } else if (projectPath != null) { + titleParts.push(projectPath); + representedPath = projectPath; + } else { + titleParts.push(itemTitle); + representedPath = ""; + } + + if (process.platform !== 'darwin') { + titleParts.push(appName); + } + + document.title = titleParts.join(" \u2014 "); + return this.applicationDelegate.setRepresentedFilename(representedPath); + } + + // On macOS, fades the application window's proxy icon when the current file + // has been modified. + updateDocumentEdited() { + let left; + const modified = (left = __guardMethod__(this.getActivePaneItem(), 'isModified', o => o.isModified())) != null ? left : false; + return this.applicationDelegate.setWindowDocumentEdited(modified); + } + + /* + Section: Event Subscription + */ + + // Essential: Invoke the given callback with all current and future text + // editors in the workspace. + // + // * `callback` {Function} to be called with current and future text editors. + // * `editor` An {TextEditor} that is present in {::getTextEditors} at the time + // of subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeTextEditors(callback) { + for (let textEditor of this.getTextEditors()) { callback(textEditor); } + return this.onDidAddTextEditor(({textEditor}) => callback(textEditor)); + } + + // Essential: Invoke the given callback with all current and future panes items + // in the workspace. + // + // * `callback` {Function} to be called with current and future pane items. + // * `item` An item that is present in {::getPaneItems} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePaneItems(callback) { return this.paneContainer.observePaneItems(callback); } + + // Essential: Invoke the given callback when the active pane item changes. + // + // Because observers are invoked synchronously, it's important not to perform + // any expensive operations via this method. Consider + // {::onDidStopChangingActivePaneItem} to delay operations until after changes + // stop occurring. + // + // * `callback` {Function} to be called when the active pane item changes. + // * `item` The active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePaneItem(callback) { + return this.paneContainer.onDidChangeActivePaneItem(callback); + } + + // Essential: Invoke the given callback when the active pane item stops + // changing. + // + // Observers are called asynchronously 100ms after the last active pane item + // change. Handling changes here rather than in the synchronous + // {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly + // changing or closing tabs and ensures critical UI feedback, like changing the + // highlighted tab, gets priority over work that can be done asynchronously. + // + // * `callback` {Function} to be called when the active pane item stopts + // changing. + // * `item` The active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidStopChangingActivePaneItem(callback) { + return this.paneContainer.onDidStopChangingActivePaneItem(callback); + } + + // Essential: Invoke the given callback with the current active pane item and + // with all future active pane items in the workspace. + // + // * `callback` {Function} to be called when the active pane item changes. + // * `item` The current active pane item. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePaneItem(callback) { return this.paneContainer.observeActivePaneItem(callback); } + + // Essential: Invoke the given callback whenever an item is opened. Unlike + // {::onDidAddPaneItem}, observers will be notified for items that are already + // present in the workspace when they are reopened. + // + // * `callback` {Function} to be called whenever an item is opened. + // * `event` {Object} with the following keys: + // * `uri` {String} representing the opened URI. Could be `undefined`. + // * `item` The opened item. + // * `pane` The pane in which the item was opened. + // * `index` The index of the opened item on its pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidOpen(callback) { + return this.emitter.on('did-open', callback); + } + + // Extended: Invoke the given callback when a pane is added to the workspace. + // + // * `callback` {Function} to be called panes are added. + // * `event` {Object} with the following keys: + // * `pane` The added pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPane(callback) { return this.paneContainer.onDidAddPane(callback); } + + // Extended: Invoke the given callback before a pane is destroyed in the + // workspace. + // + // * `callback` {Function} to be called before panes are destroyed. + // * `event` {Object} with the following keys: + // * `pane` The pane to be destroyed. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onWillDestroyPane(callback) { return this.paneContainer.onWillDestroyPane(callback); } + + // Extended: Invoke the given callback when a pane is destroyed in the + // workspace. + // + // * `callback` {Function} to be called panes are destroyed. + // * `event` {Object} with the following keys: + // * `pane` The destroyed pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroyPane(callback) { return this.paneContainer.onDidDestroyPane(callback); } + + // Extended: Invoke the given callback with all current and future panes in the + // workspace. + // + // * `callback` {Function} to be called with current and future panes. + // * `pane` A {Pane} that is present in {::getPanes} at the time of + // subscription or that is added at some later time. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observePanes(callback) { return this.paneContainer.observePanes(callback); } + + // Extended: Invoke the given callback when the active pane changes. + // + // * `callback` {Function} to be called when the active pane changes. + // * `pane` A {Pane} that is the current return value of {::getActivePane}. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeActivePane(callback) { return this.paneContainer.onDidChangeActivePane(callback); } + + // Extended: Invoke the given callback with the current active pane and when + // the active pane changes. + // + // * `callback` {Function} to be called with the current and future active# + // panes. + // * `pane` A {Pane} that is the current return value of {::getActivePane}. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeActivePane(callback) { return this.paneContainer.observeActivePane(callback); } + + // Extended: Invoke the given callback when a pane item is added to the + // workspace. + // + // * `callback` {Function} to be called when pane items are added. + // * `event` {Object} with the following keys: + // * `item` The added pane item. + // * `pane` {Pane} containing the added item. + // * `index` {Number} indicating the index of the added item in its pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddPaneItem(callback) { return this.paneContainer.onDidAddPaneItem(callback); } + + // Extended: Invoke the given callback when a pane item is about to be + // destroyed, before the user is prompted to save it. + // + // * `callback` {Function} to be called before pane items are destroyed. + // * `event` {Object} with the following keys: + // * `item` The item to be destroyed. + // * `pane` {Pane} containing the item to be destroyed. + // * `index` {Number} indicating the index of the item to be destroyed in + // its pane. + // + // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. + onWillDestroyPaneItem(callback) { return this.paneContainer.onWillDestroyPaneItem(callback); } + + // Extended: Invoke the given callback when a pane item is destroyed. + // + // * `callback` {Function} to be called when pane items are destroyed. + // * `event` {Object} with the following keys: + // * `item` The destroyed item. + // * `pane` {Pane} containing the destroyed item. + // * `index` {Number} indicating the index of the destroyed item in its + // pane. + // + // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. + onDidDestroyPaneItem(callback) { return this.paneContainer.onDidDestroyPaneItem(callback); } + + // Extended: Invoke the given callback when a text editor is added to the + // workspace. + // + // * `callback` {Function} to be called panes are added. + // * `event` {Object} with the following keys: + // * `textEditor` {TextEditor} that was added. + // * `pane` {Pane} containing the added text editor. + // * `index` {Number} indicating the index of the added text editor in its + // pane. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidAddTextEditor(callback) { + return this.emitter.on('did-add-text-editor', callback); + } + + /* + Section: Opening + */ + + // Essential: Opens the given URI in Atom asynchronously. + // If the URI is already open, the existing item for that URI will be + // activated. If no URI is given, or no registered opener can open + // the URI, a new empty {TextEditor} will be created. + // + // * `uri` (optional) A {String} containing a URI. + // * `options` (optional) {Object} + // * `initialLine` A {Number} indicating which row to move the cursor to + // initially. Defaults to `0`. + // * `initialColumn` A {Number} indicating which column to move the cursor to + // initially. Defaults to `0`. + // * `split` Either 'left', 'right', 'up' or 'down'. + // If 'left', the item will be opened in leftmost pane of the current active pane's row. + // If 'right', the item will be opened in the rightmost pane of the current active pane's row. If only one pane exists in the row, a new pane will be created. + // If 'up', the item will be opened in topmost pane of the current active pane's column. + // If 'down', the item will be opened in the bottommost pane of the current active pane's column. If only one pane exists in the column, a new pane will be created. + // * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on + // containing pane. Defaults to `true`. + // * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} + // on containing pane. Defaults to `true`. + // * `pending` A {Boolean} indicating whether or not the item should be opened + // in a pending state. Existing pending items in a pane are replaced with + // new pending items when they are opened. + // * `searchAllPanes` A {Boolean}. If `true`, the workspace will attempt to + // activate an existing item for the given URI on any pane. + // If `false`, only the active pane will be searched for + // an existing item for the same URI. Defaults to `false`. + // + // Returns a {Promise} that resolves to the {TextEditor} for the file URI. + open(uri, options={}) { + let pane; + const { searchAllPanes } = options; + const { split } = options; + uri = this.project.resolvePath(uri); + + if (!atom.config.get('core.allowPendingPaneItems')) { + options.pending = false; + } + + // Avoid adding URLs as recent documents to work-around this Spotlight crash: + // https://github.com/atom/atom/issues/10071 + if ((uri != null) && ((url.parse(uri).protocol == null) || (process.platform === 'win32'))) { + this.applicationDelegate.addRecentDocument(uri); + } + + if (searchAllPanes) { pane = this.paneContainer.paneForURI(uri); } + if (pane == null) { pane = (() => { switch (split) { + case 'left': + return this.getActivePane().findLeftmostSibling(); + case 'right': + return this.getActivePane().findOrCreateRightmostSibling(); + case 'up': + return this.getActivePane().findTopmostSibling(); + case 'down': + return this.getActivePane().findOrCreateBottommostSibling(); + default: + return this.getActivePane(); + } })(); } + + return this.openURIInPane(uri, pane, options); + } + + // Open Atom's license in the active pane. + openLicense() { + return this.open(path.join(process.resourcesPath, 'LICENSE.md')); + } + + // Synchronously open the given URI in the active pane. **Only use this method + // in specs. Calling this in production code will block the UI thread and + // everyone will be mad at you.** + // + // * `uri` A {String} containing a URI. + // * `options` An optional options {Object} + // * `initialLine` A {Number} indicating which row to move the cursor to + // initially. Defaults to `0`. + // * `initialColumn` A {Number} indicating which column to move the cursor to + // initially. Defaults to `0`. + // * `activatePane` A {Boolean} indicating whether to call {Pane::activate} on + // the containing pane. Defaults to `true`. + // * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} + // on containing pane. Defaults to `true`. + openSync(uri='', options={}) { + const {initialLine, initialColumn} = options; + const activatePane = options.activatePane != null ? options.activatePane : true; + const activateItem = options.activateItem != null ? options.activateItem : true; + + uri = this.project.resolvePath(uri); + let item = this.getActivePane().itemForURI(uri); + if (uri) { + for (let opener of this.getOpeners()) { if (!item) { if (item == null) { item = opener(uri, options); } } } + } + if (item == null) { item = this.project.openSync(uri, {initialLine, initialColumn}); } + + if (activateItem) { this.getActivePane().activateItem(item); } + this.itemOpened(item); + if (activatePane) { this.getActivePane().activate(); } + return item; + } + + openURIInPane(uri, pane, options={}) { + let item; + const activatePane = options.activatePane != null ? options.activatePane : true; + const activateItem = options.activateItem != null ? options.activateItem : true; + + if (uri != null) { + if (item = pane.itemForURI(uri)) { + if (!options.pending && (pane.getPendingItem() === item)) { pane.clearPendingItem(); } + } + for (let opener of this.getOpeners()) { if (!item) { if (item == null) { item = opener(uri, options); } } } + } + + try { + if (item == null) { item = this.openTextFile(uri, options); } + } catch (error) { + switch (error.code) { + case 'CANCELLED': + return Promise.resolve(); + break; + case 'EACCES': + this.notificationManager.addWarning(`Permission denied '${error.path}'`); + return Promise.resolve(); + break; + case 'EPERM': case 'EBUSY': case 'ENXIO': case 'EIO': case 'ENOTCONN': case 'UNKNOWN': case 'ECONNRESET': case 'EINVAL': case 'EMFILE': case 'ENOTDIR': case 'EAGAIN': + this.notificationManager.addWarning(`Unable to open '${error.path != null ? error.path : uri}'`, {detail: error.message}); + return Promise.resolve(); + break; + default: + throw error; + } + } + + return Promise.resolve(item) + .then(item => { + let initialColumn; + if (pane.isDestroyed()) { return item; } + + this.itemOpened(item); + if (activateItem) { pane.activateItem(item, {pending: options.pending}); } + if (activatePane) { pane.activate(); } + + let initialLine = initialColumn = 0; + if (!Number.isNaN(options.initialLine)) { + ({ initialLine } = options); + } + if (!Number.isNaN(options.initialColumn)) { + ({ initialColumn } = options); + } + if ((initialLine >= 0) || (initialColumn >= 0)) { + if (typeof item.setCursorBufferPosition === 'function') { + item.setCursorBufferPosition([initialLine, initialColumn]); + } + } + + const index = pane.getActiveItemIndex(); + this.emitter.emit('did-open', {uri, pane, item, index}); + return item; + } + ); + } + + openTextFile(uri, options) { + const filePath = this.project.resolvePath(uri); + + if (filePath != null) { + try { + fs.closeSync(fs.openSync(filePath, 'r')); + } catch (error) { + // allow ENOENT errors to create an editor for paths that dont exist + if (error.code !== 'ENOENT') { throw error; } + } + } + + const fileSize = fs.getSizeSync(filePath); + + const largeFileMode = fileSize >= (2 * 1048576); // 2MB + if (fileSize >= (this.config.get('core.warnOnLargeFileLimit') * 1048576)) { // 20MB by default + const choice = this.applicationDelegate.confirm({ + message: 'Atom will be unresponsive during the loading of very large files.', + detailedMessage: "Do you still want to load this file?", + buttons: ["Proceed", "Cancel"]}); + if (choice === 1) { + const error = new Error; + error.code = 'CANCELLED'; + throw error; + } + } + + return this.project.bufferForPath(filePath, options).then(buffer => { + return this.textEditorRegistry.build(Object.assign({buffer, largeFileMode, autoHeight: false}, options)); + } + ); + } + + handleGrammarUsed(grammar) { + if (grammar == null) { return; } + + return this.packageManager.triggerActivationHook(`${grammar.packageName}:grammar-used`); + } + + // Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`. + // + // * `object` An {Object} you want to perform the check against. + isTextEditor(object) { + return object instanceof TextEditor; + } + + // Extended: Create a new text editor. + // + // Returns a {TextEditor}. + buildTextEditor(params) { + const editor = this.textEditorRegistry.build(params); + const subscriptions = new CompositeDisposable( + this.textEditorRegistry.maintainGrammar(editor), + this.textEditorRegistry.maintainConfig(editor) + ); + editor.onDidDestroy(() => subscriptions.dispose()); + return editor; + } + + // Public: Asynchronously reopens the last-closed item's URI if it hasn't already been + // reopened. + // + // Returns a {Promise} that is resolved when the item is opened + reopenItem() { + let uri; + if (uri = this.destroyedItemURIs.pop()) { + return this.open(uri); + } else { + return Promise.resolve(); + } + } + + // Public: Register an opener for a uri. + // + // When a URI is opened via {Workspace::open}, Atom loops through its registered + // opener functions until one returns a value for the given uri. + // Openers are expected to return an object that inherits from HTMLElement or + // a model which has an associated view in the {ViewRegistry}. + // A {TextEditor} will be used if no opener returns a value. + // + // ## Examples + // + // ```coffee + // atom.workspace.addOpener (uri) -> + // if path.extname(uri) is '.toml' + // return new TomlEditor(uri) + // ``` + // + // * `opener` A {Function} to be called when a path is being opened. + // + // Returns a {Disposable} on which `.dispose()` can be called to remove the + // opener. + // + // Note that the opener will be called if and only if the URI is not already open + // in the current pane. The searchAllPanes flag expands the search from the + // current pane to all panes. If you wish to open a view of a different type for + // a file that is already open, consider changing the protocol of the URI. For + // example, perhaps you wish to preview a rendered version of the file `/foo/bar/baz.quux` + // that is already open in a text editor view. You could signal this by calling + // {Workspace::open} on the URI `quux-preview://foo/bar/baz.quux`. Then your opener + // can check the protocol for quux-preview and only handle those URIs that match. + addOpener(opener) { + this.openers.push(opener); + return new Disposable((function() { return _.remove(this.openers, opener); }.bind(this))); + } + + getOpeners() { + return this.openers; + } + + /* + Section: Pane Items + */ + + // Essential: Get all pane items in the workspace. + // + // Returns an {Array} of items. + getPaneItems() { + return this.paneContainer.getPaneItems(); + } + + // Essential: Get the active {Pane}'s active item. + // + // Returns an pane item {Object}. + getActivePaneItem() { + return this.paneContainer.getActivePaneItem(); + } + + // Essential: Get all text editors in the workspace. + // + // Returns an {Array} of {TextEditor}s. + getTextEditors() { + return this.getPaneItems().filter(item => item instanceof TextEditor); + } + + // Essential: Get the active item if it is an {TextEditor}. + // + // Returns an {TextEditor} or `undefined` if the current active item is not an + // {TextEditor}. + getActiveTextEditor() { + const activeItem = this.getActivePaneItem(); + if (activeItem instanceof TextEditor) { return activeItem; } + } + + // Save all pane items. + saveAll() { + return this.paneContainer.saveAll(); + } + + confirmClose(options) { + return this.paneContainer.confirmClose(options); + } + + // Save the active pane item. + // + // If the active pane item currently has a URI according to the item's + // `.getURI` method, calls `.save` on the item. Otherwise + // {::saveActivePaneItemAs} # will be called instead. This method does nothing + // if the active item does not implement a `.save` method. + saveActivePaneItem() { + return this.getActivePane().saveActiveItem(); + } + + // Prompt the user for a path and save the active pane item to it. + // + // Opens a native dialog where the user selects a path on disk, then calls + // `.saveAs` on the item with the selected path. This method does nothing if + // the active item does not implement a `.saveAs` method. + saveActivePaneItemAs() { + return this.getActivePane().saveActiveItemAs(); + } + + // Destroy (close) the active pane item. + // + // Removes the active pane item and calls the `.destroy` method on it if one is + // defined. + destroyActivePaneItem() { + return this.getActivePane().destroyActiveItem(); + } + + /* + Section: Panes + */ + + // Extended: Get all panes in the workspace. + // + // Returns an {Array} of {Pane}s. + getPanes() { + return this.paneContainer.getPanes(); + } + + // Extended: Get the active {Pane}. + // + // Returns a {Pane}. + getActivePane() { + return this.paneContainer.getActivePane(); + } + + // Extended: Make the next pane active. + activateNextPane() { + return this.paneContainer.activateNextPane(); + } + + // Extended: Make the previous pane active. + activatePreviousPane() { + return this.paneContainer.activatePreviousPane(); + } + + // Extended: Get the first {Pane} with an item for the given URI. + // + // * `uri` {String} uri + // + // Returns a {Pane} or `undefined` if no pane exists for the given URI. + paneForURI(uri) { + return this.paneContainer.paneForURI(uri); + } + + // Extended: Get the {Pane} containing the given item. + // + // * `item` Item the returned pane contains. + // + // Returns a {Pane} or `undefined` if no pane exists for the given item. + paneForItem(item) { + return this.paneContainer.paneForItem(item); + } + + // Destroy (close) the active pane. + destroyActivePane() { + return __guard__(this.getActivePane(), x => x.destroy()); + } + + // Close the active pane item, or the active pane if it is empty, + // or the current window if there is only the empty root pane. + closeActivePaneItemOrEmptyPaneOrWindow() { + if (this.getActivePaneItem() != null) { + return this.destroyActivePaneItem(); + } else if (this.getPanes().length > 1) { + return this.destroyActivePane(); + } else if (this.config.get('core.closeEmptyWindows')) { + return atom.close(); + } + } + + // Increase the editor font size by 1px. + increaseFontSize() { + return this.config.set("editor.fontSize", this.config.get("editor.fontSize") + 1); + } + + // Decrease the editor font size by 1px. + decreaseFontSize() { + const fontSize = this.config.get("editor.fontSize"); + if (fontSize > 1) { return this.config.set("editor.fontSize", fontSize - 1); } + } + + // Restore to the window's original editor font size. + resetFontSize() { + if (this.originalFontSize) { + return this.config.set("editor.fontSize", this.originalFontSize); + } + } + + subscribeToFontSize() { + return this.config.onDidChange('editor.fontSize', ({oldValue}) => { + return this.originalFontSize != null ? this.originalFontSize : (this.originalFontSize = oldValue); + } + ); + } + + // Removes the item's uri from the list of potential items to reopen. + itemOpened(item) { + let uri; + if (typeof item.getURI === 'function') { + uri = item.getURI(); + } else if (typeof item.getUri === 'function') { + uri = item.getUri(); + } + + if (uri != null) { + return _.remove(this.destroyedItemURIs, uri); + } + } + + // Adds the destroyed item's uri to the list of items to reopen. + didDestroyPaneItem({item}) { + let uri; + if (typeof item.getURI === 'function') { + uri = item.getURI(); + } else if (typeof item.getUri === 'function') { + uri = item.getUri(); + } + + if (uri != null) { + return this.destroyedItemURIs.push(uri); + } + } + + // Called by Model superclass when destroyed + destroyed() { + this.paneContainer.destroy(); + return (this.activeItemSubscriptions != null ? this.activeItemSubscriptions.dispose() : undefined); + } + + + /* + Section: Panels + + Panels are used to display UI related to an editor window. They are placed at one of the four + edges of the window: left, right, top or bottom. If there are multiple panels on the same window + edge they are stacked in order of priority: higher priority is closer to the center, lower + priority towards the edge. + + *Note:* If your panel changes its size throughout its lifetime, consider giving it a higher + priority, allowing fixed size panels to be closer to the edge. This allows control targets to + remain more static for easier targeting by users that employ mice or trackpads. (See + [atom/atom#4834](https://github.com/atom/atom/issues/4834) for discussion.) + */ + + // Essential: Get an {Array} of all the panel items at the bottom of the editor window. + getBottomPanels() { + return this.getPanels('bottom'); + } + + // Essential: Adds a panel item to the bottom of the editor window. + // + // * `options` {Object} + // * `item` Your panel content. It can be DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // latter. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addBottomPanel(options) { + return this.addPanel('bottom', options); + } + + // Essential: Get an {Array} of all the panel items to the left of the editor window. + getLeftPanels() { + return this.getPanels('left'); + } + + // Essential: Adds a panel item to the left of the editor window. + // + // * `options` {Object} + // * `item` Your panel content. It can be DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // latter. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addLeftPanel(options) { + return this.addPanel('left', options); + } + + // Essential: Get an {Array} of all the panel items to the right of the editor window. + getRightPanels() { + return this.getPanels('right'); + } + + // Essential: Adds a panel item to the right of the editor window. + // + // * `options` {Object} + // * `item` Your panel content. It can be DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // latter. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addRightPanel(options) { + return this.addPanel('right', options); + } + + // Essential: Get an {Array} of all the panel items at the top of the editor window. + getTopPanels() { + return this.getPanels('top'); + } + + // Essential: Adds a panel item to the top of the editor window above the tabs. + // + // * `options` {Object} + // * `item` Your panel content. It can be DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // latter. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addTopPanel(options) { + return this.addPanel('top', options); + } + + // Essential: Get an {Array} of all the panel items in the header. + getHeaderPanels() { + return this.getPanels('header'); + } + + // Essential: Adds a panel item to the header. + // + // * `options` {Object} + // * `item` Your panel content. It can be DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // latter. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addHeaderPanel(options) { + return this.addPanel('header', options); + } + + // Essential: Get an {Array} of all the panel items in the footer. + getFooterPanels() { + return this.getPanels('footer'); + } + + // Essential: Adds a panel item to the footer. + // + // * `options` {Object} + // * `item` Your panel content. It can be DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // latter. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addFooterPanel(options) { + return this.addPanel('footer', options); + } + + // Essential: Get an {Array} of all the modal panel items + getModalPanels() { + return this.getPanels('modal'); + } + + // Essential: Adds a panel item as a modal dialog. + // + // * `options` {Object} + // * `item` Your panel content. It can be a DOM element, a jQuery element, or + // a model with a view registered via {ViewRegistry::addViewProvider}. We recommend the + // model option. See {ViewRegistry::addViewProvider} for more information. + // * `visible` (optional) {Boolean} false if you want the panel to initially be hidden + // (default: true) + // * `priority` (optional) {Number} Determines stacking order. Lower priority items are + // forced closer to the edges of the window. (default: 100) + // + // Returns a {Panel} + addModalPanel(options={}) { + return this.addPanel('modal', options); + } + + // Essential: Returns the {Panel} associated with the given item. Returns + // `null` when the item has no panel. + // + // * `item` Item the panel contains + panelForItem(item) { + for (let location in this.panelContainers) { + const container = this.panelContainers[location]; + const panel = container.panelForItem(item); + if (panel != null) { return panel; } + } + return null; + } + + getPanels(location) { + return this.panelContainers[location].getPanels(); + } + + addPanel(location, options) { + if (options == null) { options = {}; } + return this.panelContainers[location].addPanel(new Panel(options)); + } + + /* + Section: Searching and Replacing + */ + + // Public: Performs a search across all files in the workspace. + // + // * `regex` {RegExp} to search with. + // * `options` (optional) {Object} + // * `paths` An {Array} of glob patterns to search within. + // * `onPathsSearched` (optional) {Function} to be periodically called + // with number of paths searched. + // * `iterator` {Function} callback on each file found. + // + // Returns a {Promise} with a `cancel()` method that will cancel all + // of the underlying searches that were started as part of this scan. + scan(regex, options={}, iterator) { + let directorySearcher, onPathsSearched; + if (_.isFunction(options)) { + iterator = options; + options = {}; + } + + // Find a searcher for every Directory in the project. Each searcher that is matched + // will be associated with an Array of Directory objects in the Map. + const directoriesForSearcher = new Map(); + for (let directory of this.project.getDirectories()) { + let searcher = this.defaultDirectorySearcher; + for (directorySearcher of this.directorySearchers) { + if (directorySearcher.canSearchDirectory(directory)) { + searcher = directorySearcher; + break; + } + } + let directories = directoriesForSearcher.get(searcher); + if (!directories) { + directories = []; + directoriesForSearcher.set(searcher, directories); + } + directories.push(directory); + } + + // Define the onPathsSearched callback. + if (_.isFunction(options.onPathsSearched)) { + // Maintain a map of directories to the number of search results. When notified of a new count, + // replace the entry in the map and update the total. + const onPathsSearchedOption = options.onPathsSearched; + let totalNumberOfPathsSearched = 0; + const numberOfPathsSearchedForSearcher = new Map(); + onPathsSearched = function(searcher, numberOfPathsSearched) { + const oldValue = numberOfPathsSearchedForSearcher.get(searcher); + if (oldValue) { + totalNumberOfPathsSearched -= oldValue; + } + numberOfPathsSearchedForSearcher.set(searcher, numberOfPathsSearched); + totalNumberOfPathsSearched += numberOfPathsSearched; + return onPathsSearchedOption(totalNumberOfPathsSearched); + }; + } else { + onPathsSearched = function() {}; + } + + // Kick off all of the searches and unify them into one Promise. + const allSearches = []; + directoriesForSearcher.forEach((directories, searcher) => { + const searchOptions = { + inclusions: options.paths || [], + includeHidden: true, + excludeVcsIgnores: this.config.get('core.excludeVcsIgnoredPaths'), + exclusions: this.config.get('core.ignoredNames'), + follow: this.config.get('core.followSymlinks'), + didMatch: result => { + if (!this.project.isPathModified(result.filePath)) { return iterator(result); } + }, + didError(error) { + return iterator(null, error); + }, + didSearchPaths(count) { return onPathsSearched(searcher, count); } + }; + directorySearcher = searcher.search(directories, regex, searchOptions); + return allSearches.push(directorySearcher); + } + ); + const searchPromise = Promise.all(allSearches); + + for (let buffer of this.project.getBuffers()) { + if (buffer.isModified()) { + const filePath = buffer.getPath(); + if (!this.project.contains(filePath)) { continue; } + var matches = []; + buffer.scan(regex, match => matches.push(match)); + if (matches.length > 0) { iterator({filePath, matches}); } + } + } + + // Make sure the Promise that is returned to the client is cancelable. To be consistent + // with the existing behavior, instead of cancel() rejecting the promise, it should + // resolve it with the special value 'cancelled'. At least the built-in find-and-replace + // package relies on this behavior. + let isCancelled = false; + const cancellablePromise = new Promise(function(resolve, reject) { + const onSuccess = function() { + if (isCancelled) { + return resolve('cancelled'); + } else { + return resolve(null); + } + }; + + const onFailure = function() { + for (let promise of allSearches) { promise.cancel(); } + return reject(); + }; + + return searchPromise.then(onSuccess, onFailure); + }); + cancellablePromise.cancel = function() { + isCancelled = true; + // Note that cancelling all of the members of allSearches will cause all of the searches + // to resolve, which causes searchPromise to resolve, which is ultimately what causes + // cancellablePromise to resolve. + return allSearches.map((promise) => promise.cancel()); + }; + + // Although this method claims to return a `Promise`, the `ResultsPaneView.onSearch()` + // method in the find-and-replace package expects the object returned by this method to have a + // `done()` method. Include a done() method until find-and-replace can be updated. + cancellablePromise.done = onSuccessOrFailure => cancellablePromise.then(onSuccessOrFailure, onSuccessOrFailure); + return cancellablePromise; + } + + // Public: Performs a replace across all the specified files in the project. + // + // * `regex` A {RegExp} to search with. + // * `replacementText` {String} to replace all matches of regex with. + // * `filePaths` An {Array} of file path strings to run the replace on. + // * `iterator` A {Function} callback on each file with replacements: + // * `options` {Object} with keys `filePath` and `replacements`. + // + // Returns a {Promise}. + replace(regex, replacementText, filePaths, iterator) { + return new Promise((function(resolve, reject) { + let buffer; + const openPaths = ((() => { + const result = []; + for (buffer of this.project.getBuffers()) { result.push(buffer.getPath()); + } + return result; + })()); + const outOfProcessPaths = _.difference(filePaths, openPaths); + + let inProcessFinished = !openPaths.length; + let outOfProcessFinished = !outOfProcessPaths.length; + const checkFinished = function() { + if (outOfProcessFinished && inProcessFinished) { return resolve(); } + }; + + if (!outOfProcessFinished.length) { + let flags = 'g'; + if (regex.ignoreCase) { flags += 'i'; } + + const task = Task.once(require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, function() { + outOfProcessFinished = true; + return checkFinished(); + }); + + task.on('replace:path-replaced', iterator); + task.on('replace:file-error', function(error) { return iterator(null, error); }); + } + + for (buffer of this.project.getBuffers()) { + if (!Array.from(filePaths).includes(buffer.getPath())) { continue; } + const replacements = buffer.replace(regex, replacementText, iterator); + if (replacements) { iterator({filePath: buffer.getPath(), replacements}); } + } + + inProcessFinished = true; + return checkFinished(); + }.bind(this))); + } + + checkoutHeadRevision(editor) { + if (editor.getPath()) { + const checkoutHead = () => { + return this.project.repositoryForDirectory(new Directory(editor.getDirectoryPath())) + .then(repository => repository != null ? repository.checkoutHeadForEditor(editor) : undefined); + }; + + if (this.config.get('editor.confirmCheckoutHeadRevision')) { + return this.applicationDelegate.confirm({ + message: 'Confirm Checkout HEAD Revision', + detailedMessage: `Are you sure you want to discard all changes to \"${editor.getFileName()}\" since the last Git commit?`, + buttons: { + OK: checkoutHead, + Cancel: null + } + }); + } else { + return checkoutHead(); + } + } else { + return Promise.resolve(false); + } + } +}; + +function __guard__(value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +} +function __guardMethod__(obj, methodName, transform) { + if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { + return transform(obj, methodName); + } else { + return undefined; + } +} From 7a0d7f8b4c04ef1130e569916903291be9f0cc5a Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 2 Mar 2017 15:12:12 -0800 Subject: [PATCH 14/55] Convert workspace to JavaScript: lint --- spec/workspace-spec.js | 2928 ++++++++++++++++++++-------------------- src/workspace.js | 874 ++++++------ 2 files changed, 1929 insertions(+), 1873 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index d5ddad7d2..ba7da0fce 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -1,162 +1,172 @@ -const path = require('path'); -const temp = require('temp').track(); -const TextEditor = require('../src/text-editor'); -const Workspace = require('../src/workspace'); -const Project = require('../src/project'); -const platform = require('./spec-helper-platform'); -const _ = require('underscore-plus'); -const fstream = require('fstream'); -const fs = require('fs-plus'); -const AtomEnvironment = require('../src/atom-environment'); +/* global advanceClock, HTMLElement, waits */ -describe("Workspace", function() { - let escapeStringRegex; - let [workspace, setDocumentEdited] = Array.from([]); +const path = require('path') +const temp = require('temp').track() +const TextEditor = require('../src/text-editor') +const Workspace = require('../src/workspace') +const Project = require('../src/project') +const platform = require('./spec-helper-platform') +const _ = require('underscore-plus') +const fstream = require('fstream') +const fs = require('fs-plus') +const AtomEnvironment = require('../src/atom-environment') - beforeEach(function() { - ({ workspace } = atom); - workspace.resetFontSize(); - spyOn(atom.applicationDelegate, "confirm"); - setDocumentEdited = spyOn(atom.applicationDelegate, 'setWindowDocumentEdited'); - atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('dir'))]); - return waits(1); - }); +describe('Workspace', function () { + let escapeStringRegex + let [workspace, setDocumentEdited] = Array.from([]) - afterEach(() => temp.cleanupSync()); + beforeEach(function () { + ({ workspace } = atom) + workspace.resetFontSize() + spyOn(atom.applicationDelegate, 'confirm') + setDocumentEdited = spyOn(atom.applicationDelegate, 'setWindowDocumentEdited') + atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('dir'))]) + return waits(1) + }) - describe("serialization", function() { - const simulateReload = function() { - const workspaceState = atom.workspace.serialize(); - const projectState = atom.project.serialize({isUnloading: true}); - atom.workspace.destroy(); - atom.project.destroy(); - atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom), applicationDelegate: atom.applicationDelegate}); - atom.project.deserialize(projectState); + afterEach(() => temp.cleanupSync()) + + describe('serialization', function () { + const simulateReload = function () { + const workspaceState = atom.workspace.serialize() + const projectState = atom.project.serialize({isUnloading: true}) + atom.workspace.destroy() + atom.project.destroy() + atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom), applicationDelegate: atom.applicationDelegate}) + atom.project.deserialize(projectState) atom.workspace = new Workspace({ - config: atom.config, project: atom.project, packageManager: atom.packages, - grammarRegistry: atom.grammars, deserializerManager: atom.deserializers, + config: atom.config, + project: atom.project, + packageManager: atom.packages, + grammarRegistry: atom.grammars, + deserializerManager: atom.deserializers, notificationManager: atom.notifications, applicationDelegate: atom.applicationDelegate, - viewRegistry: atom.views, assert: atom.assert.bind(atom), + viewRegistry: atom.views, + assert: atom.assert.bind(atom), textEditorRegistry: atom.textEditors - }); - return atom.workspace.deserialize(workspaceState, atom.deserializers); - }; + }) + return atom.workspace.deserialize(workspaceState, atom.deserializers) + } - describe("when the workspace contains text editors", () => - it("constructs the view with the same panes", function() { - const pane1 = atom.workspace.getActivePane(); - const pane2 = pane1.splitRight({copyActiveItem: true}); - const pane3 = pane2.splitRight({copyActiveItem: true}); - let pane4 = null; + describe('when the workspace contains text editors', () => + it('constructs the view with the same panes', function () { + const pane1 = atom.workspace.getActivePane() + const pane2 = pane1.splitRight({copyActiveItem: true}) + const pane3 = pane2.splitRight({copyActiveItem: true}) + let pane4 = null - waitsForPromise(() => atom.workspace.open(null).then(editor => editor.setText("An untitled editor."))); + waitsForPromise(() => atom.workspace.open(null).then(editor => editor.setText('An untitled editor.'))) waitsForPromise(() => atom.workspace.open('b').then(editor => pane2.activateItem(editor.copy())) - ); + ) waitsForPromise(() => atom.workspace.open('../sample.js').then(editor => pane3.activateItem(editor)) - ); + ) - runs(function() { - pane3.activeItem.setCursorScreenPosition([2, 4]); - return pane4 = pane2.splitDown(); - }); + runs(function () { + pane3.activeItem.setCursorScreenPosition([2, 4]) + return (pane4 = pane2.splitDown()) + }) waitsForPromise(() => atom.workspace.open('../sample.txt').then(editor => pane4.activateItem(editor)) - ); + ) - return runs(function() { - pane4.getActiveItem().setCursorScreenPosition([0, 2]); - pane2.activate(); + return runs(function () { + pane4.getActiveItem().setCursorScreenPosition([0, 2]) + pane2.activate() - simulateReload(); + simulateReload() - expect(atom.workspace.getTextEditors().length).toBe(5); - const [editor1, editor2, untitledEditor, editor3, editor4] = Array.from(atom.workspace.getTextEditors()); - expect(editor1.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('b'))); - expect(editor2.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x1 => x1.resolve('../sample.txt'))); - expect(editor2.getCursorScreenPosition()).toEqual([0, 2]); - expect(editor3.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x2 => x2.resolve('b'))); - expect(editor4.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x3 => x3.resolve('../sample.js'))); - expect(editor4.getCursorScreenPosition()).toEqual([2, 4]); - expect(untitledEditor.getPath()).toBeUndefined(); - expect(untitledEditor.getText()).toBe("An untitled editor."); + expect(atom.workspace.getTextEditors().length).toBe(5) + const [editor1, editor2, untitledEditor, editor3, editor4] = Array.from(atom.workspace.getTextEditors()) + expect(editor1.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('b'))) + expect(editor2.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x1 => x1.resolve('../sample.txt'))) + expect(editor2.getCursorScreenPosition()).toEqual([0, 2]) + expect(editor3.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x2 => x2.resolve('b'))) + expect(editor4.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x3 => x3.resolve('../sample.js'))) + expect(editor4.getCursorScreenPosition()).toEqual([2, 4]) + expect(untitledEditor.getPath()).toBeUndefined() + expect(untitledEditor.getText()).toBe('An untitled editor.') - expect(atom.workspace.getActiveTextEditor().getPath()).toBe(editor3.getPath()); - const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])); - return expect(document.title).toMatch(new RegExp(`^${path.basename(editor3.getLongTitle())} \\u2014 ${pathEscaped}`)); - }); + expect(atom.workspace.getActiveTextEditor().getPath()).toBe(editor3.getPath()) + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) + return expect(document.title).toMatch(new RegExp(`^${path.basename(editor3.getLongTitle())} \\u2014 ${pathEscaped}`)) + }) }) - ); + ) - return describe("where there are no open panes or editors", () => - it("constructs the view with no open editors", function() { - atom.workspace.getActivePane().destroy(); - expect(atom.workspace.getTextEditors().length).toBe(0); - simulateReload(); - return expect(atom.workspace.getTextEditors().length).toBe(0); + return describe('where there are no open panes or editors', () => + it('constructs the view with no open editors', function () { + atom.workspace.getActivePane().destroy() + expect(atom.workspace.getTextEditors().length).toBe(0) + simulateReload() + return expect(atom.workspace.getTextEditors().length).toBe(0) }) - ); - }); + ) + }) - describe("::open(uri, options)", function() { - let openEvents = null; + describe('::open(uri, options)', function () { + let openEvents = null - beforeEach(function() { - openEvents = []; - workspace.onDidOpen(event => openEvents.push(event)); - return spyOn(workspace.getActivePane(), 'activate').andCallThrough(); - }); + beforeEach(function () { + openEvents = [] + workspace.onDidOpen(event => openEvents.push(event)) + return spyOn(workspace.getActivePane(), 'activate').andCallThrough() + }) - describe("when the 'searchAllPanes' option is false (default)", function() { - describe("when called without a uri", () => - it("adds and activates an empty editor on the active pane", function() { - let [editor1, editor2] = Array.from([]); + describe("when the 'searchAllPanes' option is false (default)", function () { + describe('when called without a uri', () => + it('adds and activates an empty editor on the active pane', function () { + let [editor1, editor2] = Array.from([]) - waitsForPromise(() => workspace.open().then(editor => editor1 = editor)); + waitsForPromise(() => workspace.open().then(editor => (editor1 = editor))) - runs(function() { - expect(editor1.getPath()).toBeUndefined(); - expect(workspace.getActivePane().items).toEqual([editor1]); - expect(workspace.getActivePaneItem()).toBe(editor1); - expect(workspace.getActivePane().activate).toHaveBeenCalled(); - expect(openEvents).toEqual([{uri: undefined, pane: workspace.getActivePane(), item: editor1, index: 0}]); - return openEvents = [];}); + runs(function () { + expect(editor1.getPath()).toBeUndefined() + expect(workspace.getActivePane().items).toEqual([editor1]) + expect(workspace.getActivePaneItem()).toBe(editor1) + expect(workspace.getActivePane().activate).toHaveBeenCalled() + expect(openEvents).toEqual([{uri: undefined, pane: workspace.getActivePane(), item: editor1, index: 0}]) + return (openEvents = []) + }) - waitsForPromise(() => workspace.open().then(editor => editor2 = editor)); + waitsForPromise(() => workspace.open().then(editor => (editor2 = editor))) - return runs(function() { - expect(editor2.getPath()).toBeUndefined(); - expect(workspace.getActivePane().items).toEqual([editor1, editor2]); - expect(workspace.getActivePaneItem()).toBe(editor2); - expect(workspace.getActivePane().activate).toHaveBeenCalled(); - return expect(openEvents).toEqual([{uri: undefined, pane: workspace.getActivePane(), item: editor2, index: 1}]);});})); + return runs(function () { + expect(editor2.getPath()).toBeUndefined() + expect(workspace.getActivePane().items).toEqual([editor1, editor2]) + expect(workspace.getActivePaneItem()).toBe(editor2) + expect(workspace.getActivePane().activate).toHaveBeenCalled() + return expect(openEvents).toEqual([{uri: undefined, pane: workspace.getActivePane(), item: editor2, index: 1}]) + }) + }) + ) - return describe("when called with a uri", function() { - describe("when the active pane already has an editor for the given uri", () => - it("activates the existing editor on the active pane", function() { - let editor = null; - let editor1 = null; - let editor2 = null; + return describe('when called with a uri', function () { + describe('when the active pane already has an editor for the given uri', () => + it('activates the existing editor on the active pane', function () { + let editor = null + let editor1 = null + let editor2 = null waitsForPromise(() => - workspace.open('a').then(function(o) { - editor1 = o; - return workspace.open('b').then(function(o) { - editor2 = o; - return workspace.open('a').then(o => editor = o); - }); + workspace.open('a').then(function (o) { + editor1 = o + return workspace.open('b').then(function (o) { + editor2 = o + return workspace.open('a').then(o => (editor = o)) + }) }) - ); + ) - return runs(function() { - expect(editor).toBe(editor1); - expect(workspace.getActivePaneItem()).toBe(editor); - expect(workspace.getActivePane().activate).toHaveBeenCalled(); + return runs(function () { + expect(editor).toBe(editor1) + expect(workspace.getActivePaneItem()).toBe(editor) + expect(workspace.getActivePane().activate).toHaveBeenCalled() return expect(openEvents).toEqual([ { @@ -177,738 +187,769 @@ describe("Workspace", function() { pane: atom.workspace.getActivePane(), index: 0 } - ]);});})); - - return describe("when the active pane does not have an editor for the given uri", () => - it("adds and activates a new editor for the given path on the active pane", function() { - let editor = null; - waitsForPromise(() => workspace.open('a').then(o => editor = o)); - - return runs(function() { - expect(editor.getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a'))); - expect(workspace.getActivePaneItem()).toBe(editor); - expect(workspace.getActivePane().items).toEqual([editor]); - return expect(workspace.getActivePane().activate).toHaveBeenCalled(); - }); + ]) + }) }) - ); - }); - }); + ) - describe("when the 'searchAllPanes' option is true", function() { - describe("when an editor for the given uri is already open on an inactive pane", () => - it("activates the existing editor on the inactive pane, then activates that pane", function() { - let editor1 = null; - let editor2 = null; - const pane1 = workspace.getActivePane(); - const pane2 = workspace.getActivePane().splitRight(); + return describe('when the active pane does not have an editor for the given uri', () => + it('adds and activates a new editor for the given path on the active pane', function () { + let editor = null + waitsForPromise(() => workspace.open('a').then(o => (editor = o))) - waitsForPromise(function() { - pane1.activate(); - return workspace.open('a').then(o => editor1 = o); - }); + return runs(function () { + expect(editor.getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a'))) + expect(workspace.getActivePaneItem()).toBe(editor) + expect(workspace.getActivePane().items).toEqual([editor]) + return expect(workspace.getActivePane().activate).toHaveBeenCalled() + }) + }) + ) + }) + }) - waitsForPromise(function() { - pane2.activate(); - return workspace.open('b').then(o => editor2 = o); - }); + describe("when the 'searchAllPanes' option is true", function () { + describe('when an editor for the given uri is already open on an inactive pane', () => + it('activates the existing editor on the inactive pane, then activates that pane', function () { + let editor1 = null + let editor2 = null + const pane1 = workspace.getActivePane() + const pane2 = workspace.getActivePane().splitRight() - runs(() => expect(workspace.getActivePaneItem()).toBe(editor2)); + waitsForPromise(function () { + pane1.activate() + return workspace.open('a').then(o => (editor1 = o)) + }) - waitsForPromise(() => workspace.open('a', {searchAllPanes: true})); + waitsForPromise(function () { + pane2.activate() + return workspace.open('b').then(o => (editor2 = o)) + }) - return runs(function() { - expect(workspace.getActivePane()).toBe(pane1); - return expect(workspace.getActivePaneItem()).toBe(editor1); - }); + runs(() => expect(workspace.getActivePaneItem()).toBe(editor2)) + + waitsForPromise(() => workspace.open('a', {searchAllPanes: true})) + + return runs(function () { + expect(workspace.getActivePane()).toBe(pane1) + return expect(workspace.getActivePaneItem()).toBe(editor1) + }) }) - ); + ) - return describe("when no editor for the given uri is open in any pane", () => - it("opens an editor for the given uri in the active pane", function() { - let editor = null; - waitsForPromise(() => workspace.open('a', {searchAllPanes: true}).then(o => editor = o)); + return describe('when no editor for the given uri is open in any pane', () => + it('opens an editor for the given uri in the active pane', function () { + let editor = null + waitsForPromise(() => workspace.open('a', {searchAllPanes: true}).then(o => (editor = o))) - return runs(() => expect(workspace.getActivePaneItem()).toBe(editor)); + return runs(() => expect(workspace.getActivePaneItem()).toBe(editor)) }) - ); - }); + ) + }) - describe("when the 'split' option is set", function() { + describe("when the 'split' option is set", function () { describe("when the 'split' option is 'left'", () => - it("opens the editor in the leftmost pane of the current pane axis", function() { - const pane1 = workspace.getActivePane(); - const pane2 = pane1.splitRight(); - expect(workspace.getActivePane()).toBe(pane2); + it('opens the editor in the leftmost pane of the current pane axis', function () { + const pane1 = workspace.getActivePane() + const pane2 = pane1.splitRight() + expect(workspace.getActivePane()).toBe(pane2) - let editor = null; - waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => editor = o)); + let editor = null + waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => (editor = o))) - runs(function() { - expect(workspace.getActivePane()).toBe(pane1); - expect(pane1.items).toEqual([editor]); - return expect(pane2.items).toEqual([]);}); + runs(function () { + expect(workspace.getActivePane()).toBe(pane1) + expect(pane1.items).toEqual([editor]) + return expect(pane2.items).toEqual([]) + }) // Focus right pane and reopen the file on the left - waitsForPromise(function() { - pane2.focus(); - return workspace.open('a', {split: 'left'}).then(o => editor = o); - }); + waitsForPromise(function () { + pane2.focus() + return workspace.open('a', {split: 'left'}).then(o => (editor = o)) + }) - return runs(function() { - expect(workspace.getActivePane()).toBe(pane1); - expect(pane1.items).toEqual([editor]); - return expect(pane2.items).toEqual([]);});})); + return runs(function () { + expect(workspace.getActivePane()).toBe(pane1) + expect(pane1.items).toEqual([editor]) + return expect(pane2.items).toEqual([]) + }) + }) + ) - describe("when a pane axis is the leftmost sibling of the current pane", () => - it("opens the new item in the current pane", function() { - let editor = null; - const pane1 = workspace.getActivePane(); - const pane2 = pane1.splitLeft(); - const pane3 = pane2.splitDown(); - pane1.activate(); - expect(workspace.getActivePane()).toBe(pane1); + describe('when a pane axis is the leftmost sibling of the current pane', () => + it('opens the new item in the current pane', function () { + let editor = null + const pane1 = workspace.getActivePane() + const pane2 = pane1.splitLeft() + pane2.splitDown() + pane1.activate() + expect(workspace.getActivePane()).toBe(pane1) - waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => editor = o)); + waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => (editor = o))) - return runs(function() { - expect(workspace.getActivePane()).toBe(pane1); - return expect(pane1.items).toEqual([editor]);});})); + return runs(function () { + expect(workspace.getActivePane()).toBe(pane1) + return expect(pane1.items).toEqual([editor]) + }) + }) + ) - describe("when the 'split' option is 'right'", function() { - it("opens the editor in the rightmost pane of the current pane axis", function() { - let editor = null; - const pane1 = workspace.getActivePane(); - let pane2 = null; - waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => editor = o)); + describe("when the 'split' option is 'right'", function () { + it('opens the editor in the rightmost pane of the current pane axis', function () { + let editor = null + const pane1 = workspace.getActivePane() + let pane2 = null + waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => (editor = o))) - runs(function() { - pane2 = workspace.getPanes().filter(p => p !== pane1)[0]; - expect(workspace.getActivePane()).toBe(pane2); - expect(pane1.items).toEqual([]); - return expect(pane2.items).toEqual([editor]);}); + runs(function () { + pane2 = workspace.getPanes().filter(p => p !== pane1)[0] + expect(workspace.getActivePane()).toBe(pane2) + expect(pane1.items).toEqual([]) + return expect(pane2.items).toEqual([editor]) + }) // Focus right pane and reopen the file on the right - waitsForPromise(function() { - pane1.focus(); - return workspace.open('a', {split: 'right'}).then(o => editor = o); - }); - - return runs(function() { - expect(workspace.getActivePane()).toBe(pane2); - expect(pane1.items).toEqual([]); - return expect(pane2.items).toEqual([editor]);});}); - - return describe("when a pane axis is the rightmost sibling of the current pane", () => - it("opens the new item in a new pane split to the right of the current pane", function() { - let editor = null; - const pane1 = workspace.getActivePane(); - const pane2 = pane1.splitRight(); - const pane3 = pane2.splitDown(); - pane1.activate(); - expect(workspace.getActivePane()).toBe(pane1); - let pane4 = null; - - waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => editor = o)); - - return runs(function() { - pane4 = workspace.getPanes().filter(p => p !== pane1)[0]; - expect(workspace.getActivePane()).toBe(pane4); - expect(pane4.items).toEqual([editor]); - expect(workspace.paneContainer.root.children[0]).toBe(pane1); - return expect(workspace.paneContainer.root.children[1]).toBe(pane4); - }); + waitsForPromise(function () { + pane1.focus() + return workspace.open('a', {split: 'right'}).then(o => (editor = o)) }) - ); - }); + + return runs(function () { + expect(workspace.getActivePane()).toBe(pane2) + expect(pane1.items).toEqual([]) + return expect(pane2.items).toEqual([editor]) + }) + }) + + return describe('when a pane axis is the rightmost sibling of the current pane', () => + it('opens the new item in a new pane split to the right of the current pane', function () { + let editor = null + const pane1 = workspace.getActivePane() + const pane2 = pane1.splitRight() + pane2.splitDown() + pane1.activate() + expect(workspace.getActivePane()).toBe(pane1) + let pane4 = null + + waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => (editor = o))) + + return runs(function () { + pane4 = workspace.getPanes().filter(p => p !== pane1)[0] + expect(workspace.getActivePane()).toBe(pane4) + expect(pane4.items).toEqual([editor]) + expect(workspace.paneContainer.root.children[0]).toBe(pane1) + return expect(workspace.paneContainer.root.children[1]).toBe(pane4) + }) + }) + ) + }) describe("when the 'split' option is 'up'", () => - it("opens the editor in the topmost pane of the current pane axis", function() { - const pane1 = workspace.getActivePane(); - const pane2 = pane1.splitDown(); - expect(workspace.getActivePane()).toBe(pane2); + it('opens the editor in the topmost pane of the current pane axis', function () { + const pane1 = workspace.getActivePane() + const pane2 = pane1.splitDown() + expect(workspace.getActivePane()).toBe(pane2) - let editor = null; - waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => editor = o)); + let editor = null + waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => (editor = o))) - runs(function() { - expect(workspace.getActivePane()).toBe(pane1); - expect(pane1.items).toEqual([editor]); - return expect(pane2.items).toEqual([]);}); + runs(function () { + expect(workspace.getActivePane()).toBe(pane1) + expect(pane1.items).toEqual([editor]) + return expect(pane2.items).toEqual([]) + }) // Focus bottom pane and reopen the file on the top - waitsForPromise(function() { - pane2.focus(); - return workspace.open('a', {split: 'up'}).then(o => editor = o); - }); + waitsForPromise(function () { + pane2.focus() + return workspace.open('a', {split: 'up'}).then(o => (editor = o)) + }) - return runs(function() { - expect(workspace.getActivePane()).toBe(pane1); - expect(pane1.items).toEqual([editor]); - return expect(pane2.items).toEqual([]);});})); + return runs(function () { + expect(workspace.getActivePane()).toBe(pane1) + expect(pane1.items).toEqual([editor]) + return expect(pane2.items).toEqual([]) + }) + }) + ) - describe("when a pane axis is the topmost sibling of the current pane", () => - it("opens the new item in the current pane", function() { - let editor = null; - const pane1 = workspace.getActivePane(); - const pane2 = pane1.splitUp(); - const pane3 = pane2.splitRight(); - pane1.activate(); - expect(workspace.getActivePane()).toBe(pane1); + describe('when a pane axis is the topmost sibling of the current pane', () => + it('opens the new item in the current pane', function () { + let editor = null + const pane1 = workspace.getActivePane() + const pane2 = pane1.splitUp() + pane2.splitRight() + pane1.activate() + expect(workspace.getActivePane()).toBe(pane1) - waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => editor = o)); + waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => (editor = o))) - return runs(function() { - expect(workspace.getActivePane()).toBe(pane1); - return expect(pane1.items).toEqual([editor]);});})); + return runs(function () { + expect(workspace.getActivePane()).toBe(pane1) + return expect(pane1.items).toEqual([editor]) + }) + }) + ) - return describe("when the 'split' option is 'down'", function() { - it("opens the editor in the bottommost pane of the current pane axis", function() { - let editor = null; - const pane1 = workspace.getActivePane(); - let pane2 = null; - waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => editor = o)); + return describe("when the 'split' option is 'down'", function () { + it('opens the editor in the bottommost pane of the current pane axis', function () { + let editor = null + const pane1 = workspace.getActivePane() + let pane2 = null + waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => (editor = o))) - runs(function() { - pane2 = workspace.getPanes().filter(p => p !== pane1)[0]; - expect(workspace.getActivePane()).toBe(pane2); - expect(pane1.items).toEqual([]); - return expect(pane2.items).toEqual([editor]);}); + runs(function () { + pane2 = workspace.getPanes().filter(p => p !== pane1)[0] + expect(workspace.getActivePane()).toBe(pane2) + expect(pane1.items).toEqual([]) + return expect(pane2.items).toEqual([editor]) + }) // Focus bottom pane and reopen the file on the right - waitsForPromise(function() { - pane1.focus(); - return workspace.open('a', {split: 'down'}).then(o => editor = o); - }); - - return runs(function() { - expect(workspace.getActivePane()).toBe(pane2); - expect(pane1.items).toEqual([]); - return expect(pane2.items).toEqual([editor]);});}); - - return describe("when a pane axis is the bottommost sibling of the current pane", () => - it("opens the new item in a new pane split to the bottom of the current pane", function() { - let editor = null; - const pane1 = workspace.getActivePane(); - const pane2 = pane1.splitDown(); - pane1.activate(); - expect(workspace.getActivePane()).toBe(pane1); - let pane4 = null; - - waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => editor = o)); - - return runs(function() { - pane4 = workspace.getPanes().filter(p => p !== pane1)[0]; - expect(workspace.getActivePane()).toBe(pane4); - expect(pane4.items).toEqual([editor]); - expect(workspace.paneContainer.root.children[0]).toBe(pane1); - return expect(workspace.paneContainer.root.children[1]).toBe(pane2); - }); + waitsForPromise(function () { + pane1.focus() + return workspace.open('a', {split: 'down'}).then(o => (editor = o)) }) - ); - }); - }); - describe("when an initialLine and initialColumn are specified", () => - it("moves the cursor to the indicated location", function() { - waitsForPromise(() => workspace.open('a', {initialLine: 1, initialColumn: 5})); + return runs(function () { + expect(workspace.getActivePane()).toBe(pane2) + expect(pane1.items).toEqual([]) + return expect(pane2.items).toEqual([editor]) + }) + }) - runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([1, 5])); + return describe('when a pane axis is the bottommost sibling of the current pane', () => + it('opens the new item in a new pane split to the bottom of the current pane', function () { + let editor = null + const pane1 = workspace.getActivePane() + const pane2 = pane1.splitDown() + pane1.activate() + expect(workspace.getActivePane()).toBe(pane1) + let pane4 = null - waitsForPromise(() => workspace.open('a', {initialLine: 2, initialColumn: 4})); + waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => (editor = o))) - runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 4])); - - waitsForPromise(() => workspace.open('a', {initialLine: 0, initialColumn: 0})); - - runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([0, 0])); - - waitsForPromise(() => workspace.open('a', {initialLine: NaN, initialColumn: 4})); - - runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([0, 4])); - - waitsForPromise(() => workspace.open('a', {initialLine: 2, initialColumn: NaN})); - - runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 0])); - - waitsForPromise(() => workspace.open('a', {initialLine: Infinity, initialColumn: Infinity})); - - return runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 11]));})); - - describe("when the file is over 2MB", () => - it("opens the editor with largeFileMode: true", function() { - spyOn(fs, 'getSizeSync').andReturn(2 * 1048577); // 2MB - - let editor = null; - waitsForPromise(() => workspace.open('sample.js').then(e => editor = e)); - - return runs(() => expect(editor.largeFileMode).toBe(true)); + return runs(function () { + pane4 = workspace.getPanes().filter(p => p !== pane1)[0] + expect(workspace.getActivePane()).toBe(pane4) + expect(pane4.items).toEqual([editor]) + expect(workspace.paneContainer.root.children[0]).toBe(pane1) + return expect(workspace.paneContainer.root.children[1]).toBe(pane2) + }) + }) + ) }) - ); + }) - describe("when the file is over user-defined limit", function() { - const shouldPromptForFileOfSize = function(size, shouldPrompt) { - spyOn(fs, 'getSizeSync').andReturn(size * 1048577); - atom.applicationDelegate.confirm.andCallFake(() => selectedButtonIndex); - atom.applicationDelegate.confirm(); - var selectedButtonIndex = 1; // cancel + describe('when an initialLine and initialColumn are specified', () => + it('moves the cursor to the indicated location', function () { + waitsForPromise(() => workspace.open('a', {initialLine: 1, initialColumn: 5})) - let editor = null; - waitsForPromise(() => workspace.open('sample.js').then(e => editor = e)); + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([1, 5])) + + waitsForPromise(() => workspace.open('a', {initialLine: 2, initialColumn: 4})) + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 4])) + + waitsForPromise(() => workspace.open('a', {initialLine: 0, initialColumn: 0})) + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([0, 0])) + + waitsForPromise(() => workspace.open('a', {initialLine: NaN, initialColumn: 4})) + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([0, 4])) + + waitsForPromise(() => workspace.open('a', {initialLine: 2, initialColumn: NaN})) + + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 0])) + + waitsForPromise(() => workspace.open('a', {initialLine: Infinity, initialColumn: Infinity})) + + return runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 11])) + }) + ) + + describe('when the file is over 2MB', () => + it('opens the editor with largeFileMode: true', function () { + spyOn(fs, 'getSizeSync').andReturn(2 * 1048577) // 2MB + + let editor = null + waitsForPromise(() => workspace.open('sample.js').then(e => (editor = e))) + + return runs(() => expect(editor.largeFileMode).toBe(true)) + }) + ) + + describe('when the file is over user-defined limit', function () { + const shouldPromptForFileOfSize = function (size, shouldPrompt) { + spyOn(fs, 'getSizeSync').andReturn(size * 1048577) + atom.applicationDelegate.confirm.andCallFake(() => selectedButtonIndex) + atom.applicationDelegate.confirm() + var selectedButtonIndex = 1 // cancel + + let editor = null + waitsForPromise(() => workspace.open('sample.js').then(e => (editor = e))) if (shouldPrompt) { - runs(function() { - expect(editor).toBeUndefined(); - expect(atom.applicationDelegate.confirm).toHaveBeenCalled(); + runs(function () { + expect(editor).toBeUndefined() + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() - atom.applicationDelegate.confirm.reset(); - return selectedButtonIndex = 0; - }); // open the file + atom.applicationDelegate.confirm.reset() + return (selectedButtonIndex = 0) + }) // open the file - waitsForPromise(() => workspace.open('sample.js').then(e => editor = e)); + waitsForPromise(() => workspace.open('sample.js').then(e => (editor = e))) - return runs(function() { - expect(atom.applicationDelegate.confirm).toHaveBeenCalled(); - return expect(editor.largeFileMode).toBe(true); - }); + return runs(function () { + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() + return expect(editor.largeFileMode).toBe(true) + }) } else { - return runs(() => expect(editor).not.toBeUndefined()); + return runs(() => expect(editor).not.toBeUndefined()) } - }; + } - it("prompts the user to make sure they want to open a file this big", function() { - atom.config.set("core.warnOnLargeFileLimit", 20); - return shouldPromptForFileOfSize(20, true); - }); + it('prompts the user to make sure they want to open a file this big', function () { + atom.config.set('core.warnOnLargeFileLimit', 20) + return shouldPromptForFileOfSize(20, true) + }) - it("doesn't prompt on files below the limit", function() { - atom.config.set("core.warnOnLargeFileLimit", 30); - return shouldPromptForFileOfSize(20, false); - }); + it("doesn't prompt on files below the limit", function () { + atom.config.set('core.warnOnLargeFileLimit', 30) + return shouldPromptForFileOfSize(20, false) + }) - return it("prompts for smaller files with a lower limit", function() { - atom.config.set("core.warnOnLargeFileLimit", 5); - return shouldPromptForFileOfSize(10, true); - }); - }); + return it('prompts for smaller files with a lower limit', function () { + atom.config.set('core.warnOnLargeFileLimit', 5) + return shouldPromptForFileOfSize(10, true) + }) + }) - describe("when passed a path that matches a custom opener", () => - it("returns the resource returned by the custom opener", function() { - const fooOpener = function(pathToOpen, options) { if (pathToOpen != null ? pathToOpen.match(/\.foo/) : undefined) { return {foo: pathToOpen, options}; } }; - const barOpener = function(pathToOpen) { if (pathToOpen != null ? pathToOpen.match(/^bar:\/\//) : undefined) { return {bar: pathToOpen}; } }; - workspace.addOpener(fooOpener); - workspace.addOpener(barOpener); + describe('when passed a path that matches a custom opener', () => + it('returns the resource returned by the custom opener', function () { + const fooOpener = function (pathToOpen, options) { if (pathToOpen != null ? pathToOpen.match(/\.foo/) : undefined) { return {foo: pathToOpen, options} } } + const barOpener = function (pathToOpen) { if (pathToOpen != null ? pathToOpen.match(/^bar:\/\//) : undefined) { return {bar: pathToOpen} } } + workspace.addOpener(fooOpener) + workspace.addOpener(barOpener) - waitsForPromise(function() { - const pathToOpen = __guard__(atom.project.getDirectories()[0], x => x.resolve('a.foo')); - return workspace.open(pathToOpen, {hey: "there"}).then(item => expect(item).toEqual({foo: pathToOpen, options: {hey: "there"}}));}); + waitsForPromise(function () { + const pathToOpen = __guard__(atom.project.getDirectories()[0], x => x.resolve('a.foo')) + return workspace.open(pathToOpen, {hey: 'there'}).then(item => expect(item).toEqual({foo: pathToOpen, options: {hey: 'there'}})) + }) return waitsForPromise(() => - workspace.open("bar://baz").then(item => expect(item).toEqual({bar: "bar://baz"})));})); + workspace.open('bar://baz').then(item => expect(item).toEqual({bar: 'bar://baz'}))) + }) + ) - it("adds the file to the application's recent documents list", function() { - if (process.platform !== 'darwin') { return; } // Feature only supported on macOS - spyOn(atom.applicationDelegate, 'addRecentDocument'); + it("adds the file to the application's recent documents list", function () { + if (process.platform !== 'darwin') { return } // Feature only supported on macOS + spyOn(atom.applicationDelegate, 'addRecentDocument') - waitsForPromise(() => workspace.open()); + waitsForPromise(() => workspace.open()) - runs(() => expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled()); + runs(() => expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled()) - waitsForPromise(() => workspace.open('something://a/url')); + waitsForPromise(() => workspace.open('something://a/url')) - runs(() => expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled()); + runs(() => expect(atom.applicationDelegate.addRecentDocument).not.toHaveBeenCalled()) - waitsForPromise(() => workspace.open(__filename)); + waitsForPromise(() => workspace.open(__filename)) - return runs(() => expect(atom.applicationDelegate.addRecentDocument).toHaveBeenCalledWith(__filename)); - }); + return runs(() => expect(atom.applicationDelegate.addRecentDocument).toHaveBeenCalledWith(__filename)) + }) - it("notifies ::onDidAddTextEditor observers", function() { - const absolutePath = require.resolve('./fixtures/dir/a'); - const newEditorHandler = jasmine.createSpy('newEditorHandler'); - workspace.onDidAddTextEditor(newEditorHandler); + it('notifies ::onDidAddTextEditor observers', function () { + const absolutePath = require.resolve('./fixtures/dir/a') + const newEditorHandler = jasmine.createSpy('newEditorHandler') + workspace.onDidAddTextEditor(newEditorHandler) - let editor = null; - waitsForPromise(() => workspace.open(absolutePath).then(e => editor = e)); + let editor = null + waitsForPromise(() => workspace.open(absolutePath).then(e => (editor = e))) - return runs(() => expect(newEditorHandler.argsForCall[0][0].textEditor).toBe(editor)); - }); + return runs(() => expect(newEditorHandler.argsForCall[0][0].textEditor).toBe(editor)) + }) - describe("when there is an error opening the file", function() { - let notificationSpy = null; - beforeEach(() => atom.notifications.onDidAddNotification(notificationSpy = jasmine.createSpy())); + describe('when there is an error opening the file', function () { + let notificationSpy = null + beforeEach(() => atom.notifications.onDidAddNotification(notificationSpy = jasmine.createSpy())) - describe("when a file does not exist", () => - it("creates an empty buffer for the specified path", function() { - waitsForPromise(() => workspace.open('not-a-file.md')); + describe('when a file does not exist', () => + it('creates an empty buffer for the specified path', function () { + waitsForPromise(() => workspace.open('not-a-file.md')) - return runs(function() { - const editor = workspace.getActiveTextEditor(); - expect(notificationSpy).not.toHaveBeenCalled(); - return expect(editor.getPath()).toContain('not-a-file.md'); - }); + return runs(function () { + const editor = workspace.getActiveTextEditor() + expect(notificationSpy).not.toHaveBeenCalled() + return expect(editor.getPath()).toContain('not-a-file.md') + }) }) - ); + ) - describe("when the user does not have access to the file", function() { + describe('when the user does not have access to the file', function () { beforeEach(() => - spyOn(fs, 'openSync').andCallFake(function(path) { - const error = new Error(`EACCES, permission denied '${path}'`); - error.path = path; - error.code = 'EACCES'; - throw error; + spyOn(fs, 'openSync').andCallFake(function (path) { + const error = new Error(`EACCES, permission denied '${path}'`) + error.path = path + error.code = 'EACCES' + throw error }) - ); + ) - return it("creates a notification", function() { - waitsForPromise(() => workspace.open('file1')); + return it('creates a notification', function () { + waitsForPromise(() => workspace.open('file1')) - return runs(function() { - expect(notificationSpy).toHaveBeenCalled(); - const notification = notificationSpy.mostRecentCall.args[0]; - expect(notification.getType()).toBe('warning'); - expect(notification.getMessage()).toContain('Permission denied'); - return expect(notification.getMessage()).toContain('file1'); - }); - }); - }); - - describe("when the the operation is not permitted", function() { - beforeEach(() => - spyOn(fs, 'openSync').andCallFake(function(path) { - const error = new Error(`EPERM, operation not permitted '${path}'`); - error.path = path; - error.code = 'EPERM'; - throw error; + return runs(function () { + expect(notificationSpy).toHaveBeenCalled() + const notification = notificationSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('warning') + expect(notification.getMessage()).toContain('Permission denied') + return expect(notification.getMessage()).toContain('file1') }) - ); - - return it("creates a notification", function() { - waitsForPromise(() => workspace.open('file1')); - - return runs(function() { - expect(notificationSpy).toHaveBeenCalled(); - const notification = notificationSpy.mostRecentCall.args[0]; - expect(notification.getType()).toBe('warning'); - expect(notification.getMessage()).toContain('Unable to open'); - return expect(notification.getMessage()).toContain('file1'); - }); - }); - }); - - describe("when the the file is already open in windows", function() { - beforeEach(() => - spyOn(fs, 'openSync').andCallFake(function(path) { - const error = new Error(`EBUSY, resource busy or locked '${path}'`); - error.path = path; - error.code = 'EBUSY'; - throw error; - }) - ); - - return it("creates a notification", function() { - waitsForPromise(() => workspace.open('file1')); - - return runs(function() { - expect(notificationSpy).toHaveBeenCalled(); - const notification = notificationSpy.mostRecentCall.args[0]; - expect(notification.getType()).toBe('warning'); - expect(notification.getMessage()).toContain('Unable to open'); - return expect(notification.getMessage()).toContain('file1'); - }); - }); - }); - - return describe("when there is an unhandled error", function() { - beforeEach(() => - spyOn(fs, 'openSync').andCallFake(function(path) { - throw new Error("I dont even know what is happening right now!!"); - }) - ); - - return it("creates a notification", function() { - const open = () => workspace.open('file1', workspace.getActivePane()); - return expect(open).toThrow(); - }); - }); - }); - - describe("when the file is already open in pending state", () => - it("should terminate the pending state", function() { - let editor = null; - let pane = null; - - waitsForPromise(() => - atom.workspace.open('sample.js', {pending: true}).then(function(o) { - editor = o; - return pane = atom.workspace.getActivePane(); - }) - ); - - runs(() => expect(pane.getPendingItem()).toEqual(editor)); - - waitsForPromise(() => atom.workspace.open('sample.js')); - - return runs(() => expect(pane.getPendingItem()).toBeNull()); + }) }) - ); - describe("when opening will switch from a pending tab to a permanent tab", () => - it("keeps the pending tab open", function() { - let editor1 = null; - let editor2 = null; - - waitsForPromise(() => - atom.workspace.open('sample.txt').then(o => editor1 = o) - ); - - waitsForPromise(() => - atom.workspace.open('sample2.txt', {pending: true}).then(o => editor2 = o) - ); - - return runs(function() { - const pane = atom.workspace.getActivePane(); - pane.activateItem(editor1); - expect(pane.getItems().length).toBe(2); - return expect(pane.getItems()).toEqual([editor1, editor2]);});})); - - return describe("when replacing a pending item which is the last item in a second pane", () => - it("does not destroy the pane even if core.destroyEmptyPanes is on", function() { - atom.config.set('core.destroyEmptyPanes', true); - let editor1 = null; - let editor2 = null; - const leftPane = atom.workspace.getActivePane(); - let rightPane = null; - - waitsForPromise(() => - atom.workspace.open('sample.js', {pending: true, split: 'right'}).then(function(o) { - editor1 = o; - rightPane = atom.workspace.getActivePane(); - return spyOn(rightPane, "destroyed"); + describe('when the the operation is not permitted', function () { + beforeEach(() => + spyOn(fs, 'openSync').andCallFake(function (path) { + const error = new Error(`EPERM, operation not permitted '${path}'`) + error.path = path + error.code = 'EPERM' + throw error }) - ); + ) - runs(function() { - expect(leftPane).not.toBe(rightPane); - expect(atom.workspace.getActivePane()).toBe(rightPane); - expect(atom.workspace.getActivePane().getItems().length).toBe(1); - return expect(rightPane.getPendingItem()).toBe(editor1); - }); + return it('creates a notification', function () { + waitsForPromise(() => workspace.open('file1')) + + return runs(function () { + expect(notificationSpy).toHaveBeenCalled() + const notification = notificationSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('warning') + expect(notification.getMessage()).toContain('Unable to open') + return expect(notification.getMessage()).toContain('file1') + }) + }) + }) + + describe('when the the file is already open in windows', function () { + beforeEach(() => + spyOn(fs, 'openSync').andCallFake(function (path) { + const error = new Error(`EBUSY, resource busy or locked '${path}'`) + error.path = path + error.code = 'EBUSY' + throw error + }) + ) + + return it('creates a notification', function () { + waitsForPromise(() => workspace.open('file1')) + + return runs(function () { + expect(notificationSpy).toHaveBeenCalled() + const notification = notificationSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('warning') + expect(notification.getMessage()).toContain('Unable to open') + return expect(notification.getMessage()).toContain('file1') + }) + }) + }) + + return describe('when there is an unhandled error', function () { + beforeEach(() => + spyOn(fs, 'openSync').andCallFake(function (path) { + throw new Error('I dont even know what is happening right now!!') + }) + ) + + return it('creates a notification', function () { + const open = () => workspace.open('file1', workspace.getActivePane()) + return expect(open).toThrow() + }) + }) + }) + + describe('when the file is already open in pending state', () => + it('should terminate the pending state', function () { + let editor = null + let pane = null waitsForPromise(() => - atom.workspace.open('sample.txt', {pending: true}).then(o => editor2 = o) - ); + atom.workspace.open('sample.js', {pending: true}).then(function (o) { + editor = o + return (pane = atom.workspace.getActivePane()) + }) + ) - return runs(function() { - expect(rightPane.getPendingItem()).toBe(editor2); - return expect(rightPane.destroyed.callCount).toBe(0); - }); + runs(() => expect(pane.getPendingItem()).toEqual(editor)) + + waitsForPromise(() => atom.workspace.open('sample.js')) + + return runs(() => expect(pane.getPendingItem()).toBeNull()) }) - ); - }); + ) + + describe('when opening will switch from a pending tab to a permanent tab', () => + it('keeps the pending tab open', function () { + let editor1 = null + let editor2 = null + + waitsForPromise(() => + atom.workspace.open('sample.txt').then(o => (editor1 = o)) + ) + + waitsForPromise(() => + atom.workspace.open('sample2.txt', {pending: true}).then(o => (editor2 = o)) + ) + + return runs(function () { + const pane = atom.workspace.getActivePane() + pane.activateItem(editor1) + expect(pane.getItems().length).toBe(2) + return expect(pane.getItems()).toEqual([editor1, editor2]) + }) + }) + ) + + return describe('when replacing a pending item which is the last item in a second pane', () => + it('does not destroy the pane even if core.destroyEmptyPanes is on', function () { + atom.config.set('core.destroyEmptyPanes', true) + let editor1 = null + let editor2 = null + const leftPane = atom.workspace.getActivePane() + let rightPane = null + + waitsForPromise(() => + atom.workspace.open('sample.js', {pending: true, split: 'right'}).then(function (o) { + editor1 = o + rightPane = atom.workspace.getActivePane() + return spyOn(rightPane, 'destroyed') + }) + ) + + runs(function () { + expect(leftPane).not.toBe(rightPane) + expect(atom.workspace.getActivePane()).toBe(rightPane) + expect(atom.workspace.getActivePane().getItems().length).toBe(1) + return expect(rightPane.getPendingItem()).toBe(editor1) + }) + + waitsForPromise(() => + atom.workspace.open('sample.txt', {pending: true}).then(o => (editor2 = o)) + ) + + return runs(function () { + expect(rightPane.getPendingItem()).toBe(editor2) + return expect(rightPane.destroyed.callCount).toBe(0) + }) + }) + ) + }) describe('the grammar-used hook', () => - it('fires when opening a file or changing the grammar of an open file', function() { - let editor = null; - let javascriptGrammarUsed = false; - let coffeescriptGrammarUsed = false; + it('fires when opening a file or changing the grammar of an open file', function () { + let editor = null + let javascriptGrammarUsed = false + let coffeescriptGrammarUsed = false - atom.packages.triggerDeferredActivationHooks(); + atom.packages.triggerDeferredActivationHooks() - runs(function() { - atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', () => javascriptGrammarUsed = true); - return atom.packages.onDidTriggerActivationHook('language-coffee-script:grammar-used', () => coffeescriptGrammarUsed = true); - }); + runs(function () { + atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', () => (javascriptGrammarUsed = true)) + return atom.packages.onDidTriggerActivationHook('language-coffee-script:grammar-used', () => (coffeescriptGrammarUsed = true)) + }) - waitsForPromise(() => atom.workspace.open('sample.js', {autoIndent: false}).then(o => editor = o)); + waitsForPromise(() => atom.workspace.open('sample.js', {autoIndent: false}).then(o => (editor = o))) - waitsForPromise(() => atom.packages.activatePackage('language-javascript')); + waitsForPromise(() => atom.packages.activatePackage('language-javascript')) - waitsFor(() => javascriptGrammarUsed); + waitsFor(() => javascriptGrammarUsed) - waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')); + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) - runs(() => editor.setGrammar(atom.grammars.selectGrammar('.coffee'))); + runs(() => editor.setGrammar(atom.grammars.selectGrammar('.coffee'))) - return waitsFor(() => coffeescriptGrammarUsed); + return waitsFor(() => coffeescriptGrammarUsed) }) - ); + ) - describe("::reopenItem()", () => - it("opens the uri associated with the last closed pane that isn't currently open", function() { - const pane = workspace.getActivePane(); + describe('::reopenItem()', () => + it("opens the uri associated with the last closed pane that isn't currently open", function () { + const pane = workspace.getActivePane() waitsForPromise(() => workspace.open('a').then(() => workspace.open('b').then(() => workspace.open('file1').then(() => workspace.open()) ) ) - ); + ) - runs(function() { + runs(function () { // does not reopen items with no uri - expect(workspace.getActivePaneItem().getURI()).toBeUndefined(); - return pane.destroyActiveItem(); - }); + expect(workspace.getActivePaneItem().getURI()).toBeUndefined() + return pane.destroyActiveItem() + }) - waitsForPromise(() => workspace.reopenItem()); + waitsForPromise(() => workspace.reopenItem()) - runs(function() { - expect(workspace.getActivePaneItem().getURI()).not.toBeUndefined(); + runs(function () { + expect(workspace.getActivePaneItem().getURI()).not.toBeUndefined() // destroy all items - expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('file1'))); - pane.destroyActiveItem(); - expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x1 => x1.resolve('b'))); - pane.destroyActiveItem(); - expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x2 => x2.resolve('a'))); - pane.destroyActiveItem(); + expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('file1'))) + pane.destroyActiveItem() + expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x1 => x1.resolve('b'))) + pane.destroyActiveItem() + expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x2 => x2.resolve('a'))) + pane.destroyActiveItem() // reopens items with uris - return expect(workspace.getActivePaneItem()).toBeUndefined(); - }); + return expect(workspace.getActivePaneItem()).toBeUndefined() + }) - waitsForPromise(() => workspace.reopenItem()); + waitsForPromise(() => workspace.reopenItem()) - runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a')))); + runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a')))) // does not reopen items that are already open - waitsForPromise(() => workspace.open('b')); + waitsForPromise(() => workspace.open('b')) - runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('b')))); + runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('b')))) - waitsForPromise(() => workspace.reopenItem()); + waitsForPromise(() => workspace.reopenItem()) - return runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('file1')))); + return runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('file1')))) }) - ); + ) - describe("::increase/decreaseFontSize()", () => - it("increases/decreases the font size without going below 1", function() { - atom.config.set('editor.fontSize', 1); - workspace.increaseFontSize(); - expect(atom.config.get('editor.fontSize')).toBe(2); - workspace.increaseFontSize(); - expect(atom.config.get('editor.fontSize')).toBe(3); - workspace.decreaseFontSize(); - expect(atom.config.get('editor.fontSize')).toBe(2); - workspace.decreaseFontSize(); - expect(atom.config.get('editor.fontSize')).toBe(1); - workspace.decreaseFontSize(); - return expect(atom.config.get('editor.fontSize')).toBe(1); + describe('::increase/decreaseFontSize()', () => + it('increases/decreases the font size without going below 1', function () { + atom.config.set('editor.fontSize', 1) + workspace.increaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe(2) + workspace.increaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe(3) + workspace.decreaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe(2) + workspace.decreaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe(1) + workspace.decreaseFontSize() + return expect(atom.config.get('editor.fontSize')).toBe(1) }) - ); + ) - describe("::resetFontSize()", function() { - it("resets the font size to the window's starting font size", function() { - const originalFontSize = atom.config.get('editor.fontSize'); + describe('::resetFontSize()', function () { + it("resets the font size to the window's starting font size", function () { + const originalFontSize = atom.config.get('editor.fontSize') - workspace.increaseFontSize(); - expect(atom.config.get('editor.fontSize')).toBe(originalFontSize + 1); - workspace.resetFontSize(); - expect(atom.config.get('editor.fontSize')).toBe(originalFontSize); - workspace.decreaseFontSize(); - expect(atom.config.get('editor.fontSize')).toBe(originalFontSize - 1); - workspace.resetFontSize(); - return expect(atom.config.get('editor.fontSize')).toBe(originalFontSize); - }); - - it("does nothing if the font size has not been changed", function() { - const originalFontSize = atom.config.get('editor.fontSize'); - - workspace.resetFontSize(); - return expect(atom.config.get('editor.fontSize')).toBe(originalFontSize); - }); - - return it("resets the font size when the editor's font size changes", function() { - const originalFontSize = atom.config.get('editor.fontSize'); - - atom.config.set('editor.fontSize', originalFontSize + 1); - workspace.resetFontSize(); - expect(atom.config.get('editor.fontSize')).toBe(originalFontSize); - atom.config.set('editor.fontSize', originalFontSize - 1); - workspace.resetFontSize(); - return expect(atom.config.get('editor.fontSize')).toBe(originalFontSize); - }); - }); - - describe("::openLicense()", () => - it("opens the license as plain-text in a buffer", function() { - waitsForPromise(() => workspace.openLicense()); - return runs(() => expect(workspace.getActivePaneItem().getText()).toMatch(/Copyright/)); + workspace.increaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize + 1) + workspace.resetFontSize() + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) + workspace.decreaseFontSize() + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize - 1) + workspace.resetFontSize() + return expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) }) - ); - describe("::isTextEditor(obj)", () => - it("returns true when the passed object is an instance of `TextEditor`", function() { - expect(workspace.isTextEditor(new TextEditor)).toBe(true); - expect(workspace.isTextEditor({getText() { return null; }})).toBe(false); - expect(workspace.isTextEditor(null)).toBe(false); - return expect(workspace.isTextEditor(undefined)).toBe(false); + it('does nothing if the font size has not been changed', function () { + const originalFontSize = atom.config.get('editor.fontSize') + + workspace.resetFontSize() + return expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) }) - ); - describe("::observeTextEditors()", () => - it("invokes the observer with current and future text editors", function() { - const observed = []; + return it("resets the font size when the editor's font size changes", function () { + const originalFontSize = atom.config.get('editor.fontSize') - waitsForPromise(() => workspace.open()); - waitsForPromise(() => workspace.open()); - waitsForPromise(() => workspace.openLicense()); - - runs(() => workspace.observeTextEditors(editor => observed.push(editor))); - - waitsForPromise(() => workspace.open()); - - return expect(observed).toEqual(workspace.getTextEditors()); + atom.config.set('editor.fontSize', originalFontSize + 1) + workspace.resetFontSize() + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) + atom.config.set('editor.fontSize', originalFontSize - 1) + workspace.resetFontSize() + return expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) }) - ); + }) - describe("when an editor is destroyed", () => - it("removes the editor", function() { - let editor = null; - - waitsForPromise(() => workspace.open("a").then(e => editor = e)); - - return runs(function() { - expect(workspace.getTextEditors()).toHaveLength(1); - editor.destroy(); - return expect(workspace.getTextEditors()).toHaveLength(0); - }); + describe('::openLicense()', () => + it('opens the license as plain-text in a buffer', function () { + waitsForPromise(() => workspace.openLicense()) + return runs(() => expect(workspace.getActivePaneItem().getText()).toMatch(/Copyright/)) }) - ); + ) - describe("when an editor is copied because its pane is split", () => - it("sets up the new editor to be configured by the text editor registry", function() { - waitsForPromise(() => atom.packages.activatePackage('language-javascript')); + describe('::isTextEditor(obj)', () => + it('returns true when the passed object is an instance of `TextEditor`', function () { + expect(workspace.isTextEditor(new TextEditor())).toBe(true) + expect(workspace.isTextEditor({getText () { return null }})).toBe(false) + expect(workspace.isTextEditor(null)).toBe(false) + return expect(workspace.isTextEditor(undefined)).toBe(false) + }) + ) + + describe('::observeTextEditors()', () => + it('invokes the observer with current and future text editors', function () { + const observed = [] + + waitsForPromise(() => workspace.open()) + waitsForPromise(() => workspace.open()) + waitsForPromise(() => workspace.openLicense()) + + runs(() => workspace.observeTextEditors(editor => observed.push(editor))) + + waitsForPromise(() => workspace.open()) + + return expect(observed).toEqual(workspace.getTextEditors()) + }) + ) + + describe('when an editor is destroyed', () => + it('removes the editor', function () { + let editor = null + + waitsForPromise(() => workspace.open('a').then(e => (editor = e))) + + return runs(function () { + expect(workspace.getTextEditors()).toHaveLength(1) + editor.destroy() + return expect(workspace.getTextEditors()).toHaveLength(0) + }) + }) + ) + + describe('when an editor is copied because its pane is split', () => + it('sets up the new editor to be configured by the text editor registry', function () { + waitsForPromise(() => atom.packages.activatePackage('language-javascript')) return waitsForPromise(() => - workspace.open('a').then(function(editor) { - atom.textEditors.setGrammarOverride(editor, 'source.js'); - expect(editor.getGrammar().name).toBe('JavaScript'); + workspace.open('a').then(function (editor) { + atom.textEditors.setGrammarOverride(editor, 'source.js') + expect(editor.getGrammar().name).toBe('JavaScript') - workspace.getActivePane().splitRight({copyActiveItem: true}); - const newEditor = workspace.getActiveTextEditor(); - expect(newEditor).not.toBe(editor); - return expect(newEditor.getGrammar().name).toBe('JavaScript'); + workspace.getActivePane().splitRight({copyActiveItem: true}) + const newEditor = workspace.getActiveTextEditor() + expect(newEditor).not.toBe(editor) + return expect(newEditor.getGrammar().name).toBe('JavaScript') }) - ); + ) }) - ); + ) - it("stores the active grammars used by all the open editors", function() { - waitsForPromise(() => atom.packages.activatePackage('language-javascript')); + it('stores the active grammars used by all the open editors', function () { + waitsForPromise(() => atom.packages.activatePackage('language-javascript')) - waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')); + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) - waitsForPromise(() => atom.packages.activatePackage('language-todo')); + waitsForPromise(() => atom.packages.activatePackage('language-todo')) - waitsForPromise(() => atom.workspace.open('sample.coffee')); + waitsForPromise(() => atom.workspace.open('sample.coffee')) - return runs(function() { + return runs(function () { atom.workspace.getActiveTextEditor().setText(`\ i = /test/; #FIXME\ ` - ); + ) const atom2 = new AtomEnvironment({ applicationDelegate: atom.applicationDelegate, @@ -917,16 +958,16 @@ i = /test/; #FIXME\ document.createElement('div'), { body: document.createElement('div'), - head: document.createElement('div'), + head: document.createElement('div') } ) - }); + }) - atom2.packages.loadPackage('language-javascript'); - atom2.packages.loadPackage('language-coffee-script'); - atom2.packages.loadPackage('language-todo'); - atom2.project.deserialize(atom.project.serialize()); - atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers); + atom2.packages.loadPackage('language-javascript') + atom2.packages.loadPackage('language-coffee-script') + atom2.packages.loadPackage('language-todo') + atom2.project.deserialize(atom.project.serialize()) + atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers) expect(atom2.grammars.getGrammars().map(grammar => grammar.name).sort()).toEqual([ 'CoffeeScript', @@ -936,119 +977,119 @@ i = /test/; #FIXME\ 'Regular Expression Replacement (JavaScript)', 'Regular Expressions (JavaScript)', 'TODO' - ]); + ]) - return atom2.destroy(); - }); - }); + return atom2.destroy() + }) + }) - describe("document.title", function() { - describe("when there is no item open", function() { - it("sets the title to the project path", () => expect(document.title).toMatch(escapeStringRegex(fs.tildify(atom.project.getPaths()[0])))); + describe('document.title', function () { + describe('when there is no item open', function () { + it('sets the title to the project path', () => expect(document.title).toMatch(escapeStringRegex(fs.tildify(atom.project.getPaths()[0])))) - return it("sets the title to 'untitled' if there is no project path", function() { - atom.project.setPaths([]); - return expect(document.title).toMatch(/^untitled/); - }); - }); + return it("sets the title to 'untitled' if there is no project path", function () { + atom.project.setPaths([]) + return expect(document.title).toMatch(/^untitled/) + }) + }) - describe("when the active pane item's path is not inside a project path", function() { + describe("when the active pane item's path is not inside a project path", function () { beforeEach(() => waitsForPromise(() => atom.workspace.open('b').then(() => atom.project.setPaths([])) ) - ); + ) - it("sets the title to the pane item's title plus the item's path", function() { - const item = atom.workspace.getActivePaneItem(); - const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))); - return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)); - }); + it("sets the title to the pane item's title plus the item's path", function () { + const item = atom.workspace.getActivePaneItem() + const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) + return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) + }) - describe("when the title of the active pane item changes", () => - it("updates the window title based on the item's new title", function() { - const editor = atom.workspace.getActivePaneItem(); - editor.buffer.setPath(path.join(temp.dir, 'hi')); - const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(editor.getPath()))); - return expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)); + describe('when the title of the active pane item changes', () => + it("updates the window title based on the item's new title", function () { + const editor = atom.workspace.getActivePaneItem() + editor.buffer.setPath(path.join(temp.dir, 'hi')) + const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(editor.getPath()))) + return expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)) }) - ); + ) describe("when the active pane's item changes", () => - it("updates the title to the new item's title plus the project path", function() { - atom.workspace.getActivePane().activateNextItem(); - const item = atom.workspace.getActivePaneItem(); - const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))); - return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)); + it("updates the title to the new item's title plus the project path", function () { + atom.workspace.getActivePane().activateNextItem() + const item = atom.workspace.getActivePaneItem() + const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) + return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) }) - ); + ) return describe("when an inactive pane's item changes", () => - it("does not update the title", function() { - const pane = atom.workspace.getActivePane(); - pane.splitRight(); - const initialTitle = document.title; - pane.activateNextItem(); - return expect(document.title).toBe(initialTitle); + it('does not update the title', function () { + const pane = atom.workspace.getActivePane() + pane.splitRight() + const initialTitle = document.title + pane.activateNextItem() + return expect(document.title).toBe(initialTitle) }) - ); - }); + ) + }) - describe("when the active pane item is inside a project path", function() { + describe('when the active pane item is inside a project path', function () { beforeEach(() => waitsForPromise(() => atom.workspace.open('b')) - ); + ) - describe("when there is an active pane item", () => - it("sets the title to the pane item's title plus the project path", function() { - const item = atom.workspace.getActivePaneItem(); - const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])); - return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)); + describe('when there is an active pane item', () => + it("sets the title to the pane item's title plus the project path", function () { + const item = atom.workspace.getActivePaneItem() + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) + return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) }) - ); + ) - describe("when the title of the active pane item changes", () => - it("updates the window title based on the item's new title", function() { - const editor = atom.workspace.getActivePaneItem(); - editor.buffer.setPath(path.join(atom.project.getPaths()[0], 'hi')); - const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])); - return expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)); + describe('when the title of the active pane item changes', () => + it("updates the window title based on the item's new title", function () { + const editor = atom.workspace.getActivePaneItem() + editor.buffer.setPath(path.join(atom.project.getPaths()[0], 'hi')) + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) + return expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)) }) - ); + ) describe("when the active pane's item changes", () => - it("updates the title to the new item's title plus the project path", function() { - atom.workspace.getActivePane().activateNextItem(); - const item = atom.workspace.getActivePaneItem(); - const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])); - return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)); + it("updates the title to the new item's title plus the project path", function () { + atom.workspace.getActivePane().activateNextItem() + const item = atom.workspace.getActivePaneItem() + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) + return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) }) - ); + ) - describe("when the last pane item is removed", () => - it("updates the title to the project's first path", function() { - atom.workspace.getActivePane().destroy(); - expect(atom.workspace.getActivePaneItem()).toBeUndefined(); - return expect(document.title).toMatch(escapeStringRegex(fs.tildify(atom.project.getPaths()[0]))); + describe('when the last pane item is removed', () => + it("updates the title to the project's first path", function () { + atom.workspace.getActivePane().destroy() + expect(atom.workspace.getActivePaneItem()).toBeUndefined() + return expect(document.title).toMatch(escapeStringRegex(fs.tildify(atom.project.getPaths()[0]))) }) - ); + ) return describe("when an inactive pane's item changes", () => - it("does not update the title", function() { - const pane = atom.workspace.getActivePane(); - pane.splitRight(); - const initialTitle = document.title; - pane.activateNextItem(); - return expect(document.title).toBe(initialTitle); + it('does not update the title', function () { + const pane = atom.workspace.getActivePane() + pane.splitRight() + const initialTitle = document.title + pane.activateNextItem() + return expect(document.title).toBe(initialTitle) }) - ); - }); + ) + }) - return describe("when the workspace is deserialized", function() { - beforeEach(() => waitsForPromise(() => atom.workspace.open('a'))); + return describe('when the workspace is deserialized', function () { + beforeEach(() => waitsForPromise(() => atom.workspace.open('a'))) - return it("updates the title to contain the project's path", function() { - document.title = null; + return it("updates the title to contain the project's path", function () { + document.title = null const atom2 = new AtomEnvironment({ applicationDelegate: atom.applicationDelegate, @@ -1057,510 +1098,528 @@ i = /test/; #FIXME\ document.createElement('div'), { body: document.createElement('div'), - head: document.createElement('div'), + head: document.createElement('div') } ) - }); + }) - atom2.project.deserialize(atom.project.serialize()); - atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers); - const item = atom2.workspace.getActivePaneItem(); - const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])); - expect(document.title).toMatch(new RegExp(`^${item.getLongTitle()} \\u2014 ${pathEscaped}`)); + atom2.project.deserialize(atom.project.serialize()) + atom2.workspace.deserialize(atom.workspace.serialize(), atom2.deserializers) + const item = atom2.workspace.getActivePaneItem() + const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) + expect(document.title).toMatch(new RegExp(`^${item.getLongTitle()} \\u2014 ${pathEscaped}`)) - return atom2.destroy(); - }); - }); - }); + return atom2.destroy() + }) + }) + }) - describe("document edited status", function() { - let [item1, item2] = Array.from([]); + describe('document edited status', function () { + let [item1, item2] = Array.from([]) - beforeEach(function() { - waitsForPromise(() => atom.workspace.open('a')); - waitsForPromise(() => atom.workspace.open('b')); - return runs(() => [item1, item2] = Array.from(atom.workspace.getPaneItems())); - }); + beforeEach(function () { + waitsForPromise(() => atom.workspace.open('a')) + waitsForPromise(() => atom.workspace.open('b')) + return runs(() => ([item1, item2] = Array.from(atom.workspace.getPaneItems()))) + }) - it("calls setDocumentEdited when the active item changes", function() { - expect(atom.workspace.getActivePaneItem()).toBe(item2); - item1.insertText('a'); - expect(item1.isModified()).toBe(true); - atom.workspace.getActivePane().activateNextItem(); + it('calls setDocumentEdited when the active item changes', function () { + expect(atom.workspace.getActivePaneItem()).toBe(item2) + item1.insertText('a') + expect(item1.isModified()).toBe(true) + atom.workspace.getActivePane().activateNextItem() - return expect(setDocumentEdited).toHaveBeenCalledWith(true); - }); + return expect(setDocumentEdited).toHaveBeenCalledWith(true) + }) - return it("calls atom.setDocumentEdited when the active item's modified status changes", function() { - expect(atom.workspace.getActivePaneItem()).toBe(item2); - item2.insertText('a'); - advanceClock(item2.getBuffer().getStoppedChangingDelay()); + return it("calls atom.setDocumentEdited when the active item's modified status changes", function () { + expect(atom.workspace.getActivePaneItem()).toBe(item2) + item2.insertText('a') + advanceClock(item2.getBuffer().getStoppedChangingDelay()) - expect(item2.isModified()).toBe(true); - expect(setDocumentEdited).toHaveBeenCalledWith(true); + expect(item2.isModified()).toBe(true) + expect(setDocumentEdited).toHaveBeenCalledWith(true) - item2.undo(); - advanceClock(item2.getBuffer().getStoppedChangingDelay()); + item2.undo() + advanceClock(item2.getBuffer().getStoppedChangingDelay()) - expect(item2.isModified()).toBe(false); - return expect(setDocumentEdited).toHaveBeenCalledWith(false); - }); - }); + expect(item2.isModified()).toBe(false) + return expect(setDocumentEdited).toHaveBeenCalledWith(false) + }) + }) - describe("adding panels", function() { + describe('adding panels', function () { class TestItem {} - class TestItemElement extends HTMLElement { - constructor() {} - initialize(model) { this.model = model; return this; } - getModel() { return this.model; } - } + // Don't use ES6 classes because then we'll have to call `super()` which we can't do with + // HTMLElement + function TestItemElement () { this.constructor = TestItemElement } + function Ctor () { this.constructor = TestItemElement } + Ctor.prototype = HTMLElement.prototype + TestItemElement.prototype = new Ctor() + TestItemElement.__super__ = HTMLElement.prototype + TestItemElement.prototype.initialize = function (model) { this.model = model; return this } + TestItemElement.prototype.getModel = function () { return this.model } beforeEach(() => atom.views.addViewProvider(TestItem, model => new TestItemElement().initialize(model)) - ); + ) describe('::addLeftPanel(model)', () => - it('adds a panel to the correct panel container', function() { - let addPanelSpy; - expect(atom.workspace.getLeftPanels().length).toBe(0); - atom.workspace.panelContainers.left.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + it('adds a panel to the correct panel container', function () { + let addPanelSpy + expect(atom.workspace.getLeftPanels().length).toBe(0) + atom.workspace.panelContainers.left.onDidAddPanel(addPanelSpy = jasmine.createSpy()) - const model = new TestItem; - const panel = atom.workspace.addLeftPanel({item: model}); + const model = new TestItem() + const panel = atom.workspace.addLeftPanel({item: model}) - expect(panel).toBeDefined(); - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - const itemView = atom.views.getView(atom.workspace.getLeftPanels()[0].getItem()); - expect(itemView instanceof TestItemElement).toBe(true); - return expect(itemView.getModel()).toBe(model); + const itemView = atom.views.getView(atom.workspace.getLeftPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + return expect(itemView.getModel()).toBe(model) }) - ); + ) describe('::addRightPanel(model)', () => - it('adds a panel to the correct panel container', function() { - let addPanelSpy; - expect(atom.workspace.getRightPanels().length).toBe(0); - atom.workspace.panelContainers.right.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + it('adds a panel to the correct panel container', function () { + let addPanelSpy + expect(atom.workspace.getRightPanels().length).toBe(0) + atom.workspace.panelContainers.right.onDidAddPanel(addPanelSpy = jasmine.createSpy()) - const model = new TestItem; - const panel = atom.workspace.addRightPanel({item: model}); + const model = new TestItem() + const panel = atom.workspace.addRightPanel({item: model}) - expect(panel).toBeDefined(); - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - const itemView = atom.views.getView(atom.workspace.getRightPanels()[0].getItem()); - expect(itemView instanceof TestItemElement).toBe(true); - return expect(itemView.getModel()).toBe(model); + const itemView = atom.views.getView(atom.workspace.getRightPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + return expect(itemView.getModel()).toBe(model) }) - ); + ) describe('::addTopPanel(model)', () => - it('adds a panel to the correct panel container', function() { - let addPanelSpy; - expect(atom.workspace.getTopPanels().length).toBe(0); - atom.workspace.panelContainers.top.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + it('adds a panel to the correct panel container', function () { + let addPanelSpy + expect(atom.workspace.getTopPanels().length).toBe(0) + atom.workspace.panelContainers.top.onDidAddPanel(addPanelSpy = jasmine.createSpy()) - const model = new TestItem; - const panel = atom.workspace.addTopPanel({item: model}); + const model = new TestItem() + const panel = atom.workspace.addTopPanel({item: model}) - expect(panel).toBeDefined(); - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - const itemView = atom.views.getView(atom.workspace.getTopPanels()[0].getItem()); - expect(itemView instanceof TestItemElement).toBe(true); - return expect(itemView.getModel()).toBe(model); + const itemView = atom.views.getView(atom.workspace.getTopPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + return expect(itemView.getModel()).toBe(model) }) - ); + ) describe('::addBottomPanel(model)', () => - it('adds a panel to the correct panel container', function() { - let addPanelSpy; - expect(atom.workspace.getBottomPanels().length).toBe(0); - atom.workspace.panelContainers.bottom.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + it('adds a panel to the correct panel container', function () { + let addPanelSpy + expect(atom.workspace.getBottomPanels().length).toBe(0) + atom.workspace.panelContainers.bottom.onDidAddPanel(addPanelSpy = jasmine.createSpy()) - const model = new TestItem; - const panel = atom.workspace.addBottomPanel({item: model}); + const model = new TestItem() + const panel = atom.workspace.addBottomPanel({item: model}) - expect(panel).toBeDefined(); - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - const itemView = atom.views.getView(atom.workspace.getBottomPanels()[0].getItem()); - expect(itemView instanceof TestItemElement).toBe(true); - return expect(itemView.getModel()).toBe(model); + const itemView = atom.views.getView(atom.workspace.getBottomPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + return expect(itemView.getModel()).toBe(model) }) - ); + ) describe('::addHeaderPanel(model)', () => - it('adds a panel to the correct panel container', function() { - let addPanelSpy; - expect(atom.workspace.getHeaderPanels().length).toBe(0); - atom.workspace.panelContainers.header.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + it('adds a panel to the correct panel container', function () { + let addPanelSpy + expect(atom.workspace.getHeaderPanels().length).toBe(0) + atom.workspace.panelContainers.header.onDidAddPanel(addPanelSpy = jasmine.createSpy()) - const model = new TestItem; - const panel = atom.workspace.addHeaderPanel({item: model}); + const model = new TestItem() + const panel = atom.workspace.addHeaderPanel({item: model}) - expect(panel).toBeDefined(); - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - const itemView = atom.views.getView(atom.workspace.getHeaderPanels()[0].getItem()); - expect(itemView instanceof TestItemElement).toBe(true); - return expect(itemView.getModel()).toBe(model); + const itemView = atom.views.getView(atom.workspace.getHeaderPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + return expect(itemView.getModel()).toBe(model) }) - ); + ) describe('::addFooterPanel(model)', () => - it('adds a panel to the correct panel container', function() { - let addPanelSpy; - expect(atom.workspace.getFooterPanels().length).toBe(0); - atom.workspace.panelContainers.footer.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + it('adds a panel to the correct panel container', function () { + let addPanelSpy + expect(atom.workspace.getFooterPanels().length).toBe(0) + atom.workspace.panelContainers.footer.onDidAddPanel(addPanelSpy = jasmine.createSpy()) - const model = new TestItem; - const panel = atom.workspace.addFooterPanel({item: model}); + const model = new TestItem() + const panel = atom.workspace.addFooterPanel({item: model}) - expect(panel).toBeDefined(); - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - const itemView = atom.views.getView(atom.workspace.getFooterPanels()[0].getItem()); - expect(itemView instanceof TestItemElement).toBe(true); - return expect(itemView.getModel()).toBe(model); + const itemView = atom.views.getView(atom.workspace.getFooterPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + return expect(itemView.getModel()).toBe(model) }) - ); + ) describe('::addModalPanel(model)', () => - it('adds a panel to the correct panel container', function() { - let addPanelSpy; - expect(atom.workspace.getModalPanels().length).toBe(0); - atom.workspace.panelContainers.modal.onDidAddPanel(addPanelSpy = jasmine.createSpy()); + it('adds a panel to the correct panel container', function () { + let addPanelSpy + expect(atom.workspace.getModalPanels().length).toBe(0) + atom.workspace.panelContainers.modal.onDidAddPanel(addPanelSpy = jasmine.createSpy()) - const model = new TestItem; - const panel = atom.workspace.addModalPanel({item: model}); + const model = new TestItem() + const panel = atom.workspace.addModalPanel({item: model}) - expect(panel).toBeDefined(); - expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}); + expect(panel).toBeDefined() + expect(addPanelSpy).toHaveBeenCalledWith({panel, index: 0}) - const itemView = atom.views.getView(atom.workspace.getModalPanels()[0].getItem()); - expect(itemView instanceof TestItemElement).toBe(true); - return expect(itemView.getModel()).toBe(model); + const itemView = atom.views.getView(atom.workspace.getModalPanels()[0].getItem()) + expect(itemView instanceof TestItemElement).toBe(true) + return expect(itemView.getModel()).toBe(model) }) - ); + ) - return describe("::panelForItem(item)", () => - it("returns the panel associated with the item", function() { - const item = new TestItem; - const panel = atom.workspace.addLeftPanel({item}); + return describe('::panelForItem(item)', () => + it('returns the panel associated with the item', function () { + const item = new TestItem() + const panel = atom.workspace.addLeftPanel({item}) - const itemWithNoPanel = new TestItem; + const itemWithNoPanel = new TestItem() - expect(atom.workspace.panelForItem(item)).toBe(panel); - return expect(atom.workspace.panelForItem(itemWithNoPanel)).toBe(null); + expect(atom.workspace.panelForItem(item)).toBe(panel) + return expect(atom.workspace.panelForItem(itemWithNoPanel)).toBe(null) }) - ); - }); + ) + }) - describe("::scan(regex, options, callback)", () => - describe("when called with a regex", function() { - it("calls the callback with all regex results in all files in the project", function() { - const results = []; + describe('::scan(regex, options, callback)', () => + describe('when called with a regex', function () { + it('calls the callback with all regex results in all files in the project', function () { + const results = [] waitsForPromise(() => atom.workspace.scan(/(a)+/, result => results.push(result)) - ); + ) - return runs(function() { - expect(results).toHaveLength(3); - expect(results[0].filePath).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a'))); - expect(results[0].matches).toHaveLength(3); + return runs(function () { + expect(results).toHaveLength(3) + expect(results[0].filePath).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a'))) + expect(results[0].matches).toHaveLength(3) return expect(results[0].matches[0]).toEqual({ matchText: 'aaa', lineText: 'aaa bbb', lineTextOffset: 0, - range: [[0, 0], [0, 3]]});});}); + range: [[0, 0], [0, 3]] + }) + }) + }) - it("works with with escaped literals (like $ and ^)", function() { - const results = []; - waitsForPromise(() => atom.workspace.scan(/\$\w+/, result => results.push(result))); + it('works with with escaped literals (like $ and ^)', function () { + const results = [] + waitsForPromise(() => atom.workspace.scan(/\$\w+/, result => results.push(result))) - return runs(function() { - expect(results.length).toBe(1); + return runs(function () { + expect(results.length).toBe(1) - const {filePath, matches} = results[0]; - expect(filePath).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a'))); - expect(matches).toHaveLength(1); + const {filePath, matches} = results[0] + expect(filePath).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a'))) + expect(matches).toHaveLength(1) return expect(matches[0]).toEqual({ matchText: '$bill', lineText: 'dollar$bill', lineTextOffset: 0, - range: [[2, 6], [2, 11]]});});}); - - it("works on evil filenames", function() { - atom.config.set('core.excludeVcsIgnoredPaths', false); - platform.generateEvilFiles(); - atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')]); - const paths = []; - let matches = []; - waitsForPromise(() => - atom.workspace.scan(/evil/, function(result) { - paths.push(result.filePath); - return matches = matches.concat(result.matches); + range: [[2, 6], [2, 11]] }) - ); + }) + }) - return runs(function() { - _.each(matches, m => expect(m.matchText).toEqual('evil')); + it('works on evil filenames', function () { + atom.config.set('core.excludeVcsIgnoredPaths', false) + platform.generateEvilFiles() + atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')]) + const paths = [] + let matches = [] + waitsForPromise(() => + atom.workspace.scan(/evil/, function (result) { + paths.push(result.filePath) + return (matches = matches.concat(result.matches)) + }) + ) + + return runs(function () { + _.each(matches, m => expect(m.matchText).toEqual('evil')) if (platform.isWindows()) { - expect(paths.length).toBe(3); - expect(paths[0]).toMatch(/a_file_with_utf8.txt$/); - expect(paths[1]).toMatch(/file with spaces.txt$/); - return expect(path.basename(paths[2])).toBe("utfa\u0306.md"); + expect(paths.length).toBe(3) + expect(paths[0]).toMatch(/a_file_with_utf8.txt$/) + expect(paths[1]).toMatch(/file with spaces.txt$/) + return expect(path.basename(paths[2])).toBe('utfa\u0306.md') } else { - expect(paths.length).toBe(5); - expect(paths[0]).toMatch(/a_file_with_utf8.txt$/); - expect(paths[1]).toMatch(/file with spaces.txt$/); - expect(paths[2]).toMatch(/goddam\nnewlines$/m); - expect(paths[3]).toMatch(/quote".txt$/m); - return expect(path.basename(paths[4])).toBe("utfa\u0306.md"); + expect(paths.length).toBe(5) + expect(paths[0]).toMatch(/a_file_with_utf8.txt$/) + expect(paths[1]).toMatch(/file with spaces.txt$/) + expect(paths[2]).toMatch(/goddam\nnewlines$/m) + expect(paths[3]).toMatch(/quote".txt$/m) + return expect(path.basename(paths[4])).toBe('utfa\u0306.md') } - }); - }); + }) + }) - it("ignores case if the regex includes the `i` flag", function() { - const results = []; - waitsForPromise(() => atom.workspace.scan(/DOLLAR/i, result => results.push(result))); + it('ignores case if the regex includes the `i` flag', function () { + const results = [] + waitsForPromise(() => atom.workspace.scan(/DOLLAR/i, result => results.push(result))) - return runs(() => expect(results).toHaveLength(1)); - }); + return runs(() => expect(results).toHaveLength(1)) + }) - describe("when the core.excludeVcsIgnoredPaths config is truthy", function() { - let [projectPath, ignoredPath] = Array.from([]); + describe('when the core.excludeVcsIgnoredPaths config is truthy', function () { + let [projectPath, ignoredPath] = Array.from([]) - beforeEach(function() { - const sourceProjectPath = path.join(__dirname, 'fixtures', 'git', 'working-dir'); - projectPath = path.join(temp.mkdirSync("atom")); + beforeEach(function () { + const sourceProjectPath = path.join(__dirname, 'fixtures', 'git', 'working-dir') + projectPath = path.join(temp.mkdirSync('atom')) - const writerStream = fstream.Writer(projectPath); - fstream.Reader(sourceProjectPath).pipe(writerStream); + const writerStream = fstream.Writer(projectPath) + fstream.Reader(sourceProjectPath).pipe(writerStream) - waitsFor(function(done) { - writerStream.on('close', done); - return writerStream.on('error', done); - }); + waitsFor(function (done) { + writerStream.on('close', done) + return writerStream.on('error', done) + }) - return runs(function() { - fs.rename(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')); - ignoredPath = path.join(projectPath, 'ignored.txt'); - return fs.writeFileSync(ignoredPath, 'this match should not be included'); - }); - }); + return runs(function () { + fs.rename(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) + ignoredPath = path.join(projectPath, 'ignored.txt') + return fs.writeFileSync(ignoredPath, 'this match should not be included') + }) + }) - afterEach(function() { - if (fs.existsSync(projectPath)) { return fs.removeSync(projectPath); } - }); + afterEach(function () { + if (fs.existsSync(projectPath)) { return fs.removeSync(projectPath) } + }) - return it("excludes ignored files", function() { - atom.project.setPaths([projectPath]); - atom.config.set('core.excludeVcsIgnoredPaths', true); - const resultHandler = jasmine.createSpy("result found"); + return it('excludes ignored files', function () { + atom.project.setPaths([projectPath]) + atom.config.set('core.excludeVcsIgnoredPaths', true) + const resultHandler = jasmine.createSpy('result found') waitsForPromise(() => atom.workspace.scan(/match/, results => resultHandler()) - ); + ) - return runs(() => expect(resultHandler).not.toHaveBeenCalled()); - }); - }); + return runs(() => expect(resultHandler).not.toHaveBeenCalled()) + }) + }) - it("includes only files when a directory filter is specified", function() { - const projectPath = path.join(path.join(__dirname, 'fixtures', 'dir')); - atom.project.setPaths([projectPath]); + it('includes only files when a directory filter is specified', function () { + const projectPath = path.join(path.join(__dirname, 'fixtures', 'dir')) + atom.project.setPaths([projectPath]) - const filePath = path.join(projectPath, 'a-dir', 'oh-git'); + const filePath = path.join(projectPath, 'a-dir', 'oh-git') - const paths = []; - let matches = []; + const paths = [] + let matches = [] waitsForPromise(() => - atom.workspace.scan(/aaa/, {paths: [`a-dir${path.sep}`]}, function(result) { - paths.push(result.filePath); - return matches = matches.concat(result.matches); + atom.workspace.scan(/aaa/, {paths: [`a-dir${path.sep}`]}, function (result) { + paths.push(result.filePath) + return (matches = matches.concat(result.matches)) }) - ); + ) - return runs(function() { - expect(paths.length).toBe(1); - expect(paths[0]).toBe(filePath); - return expect(matches.length).toBe(1); - }); - }); + return runs(function () { + expect(paths.length).toBe(1) + expect(paths[0]).toBe(filePath) + return expect(matches.length).toBe(1) + }) + }) - it("includes files and folders that begin with a '.'", function() { - const projectPath = temp.mkdirSync('atom-spec-workspace'); - const filePath = path.join(projectPath, '.text'); - fs.writeFileSync(filePath, 'match this'); - atom.project.setPaths([projectPath]); - const paths = []; - let matches = []; + it("includes files and folders that begin with a '.'", function () { + const projectPath = temp.mkdirSync('atom-spec-workspace') + const filePath = path.join(projectPath, '.text') + fs.writeFileSync(filePath, 'match this') + atom.project.setPaths([projectPath]) + const paths = [] + let matches = [] waitsForPromise(() => - atom.workspace.scan(/match this/, function(result) { - paths.push(result.filePath); - return matches = matches.concat(result.matches); + atom.workspace.scan(/match this/, function (result) { + paths.push(result.filePath) + return (matches = matches.concat(result.matches)) }) - ); + ) - return runs(function() { - expect(paths.length).toBe(1); - expect(paths[0]).toBe(filePath); - return expect(matches.length).toBe(1); - }); - }); + return runs(function () { + expect(paths.length).toBe(1) + expect(paths[0]).toBe(filePath) + return expect(matches.length).toBe(1) + }) + }) - it("excludes values in core.ignoredNames", function() { - const ignoredNames = atom.config.get("core.ignoredNames"); - ignoredNames.push("a"); - atom.config.set("core.ignoredNames", ignoredNames); + it('excludes values in core.ignoredNames', function () { + const ignoredNames = atom.config.get('core.ignoredNames') + ignoredNames.push('a') + atom.config.set('core.ignoredNames', ignoredNames) - const resultHandler = jasmine.createSpy("result found"); + const resultHandler = jasmine.createSpy('result found') waitsForPromise(() => atom.workspace.scan(/dollar/, results => resultHandler()) - ); + ) - return runs(() => expect(resultHandler).not.toHaveBeenCalled()); - }); + return runs(() => expect(resultHandler).not.toHaveBeenCalled()) + }) - it("scans buffer contents if the buffer is modified", function() { - let editor = null; - const results = []; + it('scans buffer contents if the buffer is modified', function () { + let editor = null + const results = [] waitsForPromise(() => - atom.workspace.open('a').then(function(o) { - editor = o; - return editor.setText("Elephant"); + atom.workspace.open('a').then(function (o) { + editor = o + return editor.setText('Elephant') }) - ); + ) - waitsForPromise(() => atom.workspace.scan(/a|Elephant/, result => results.push(result))); + waitsForPromise(() => atom.workspace.scan(/a|Elephant/, result => results.push(result))) - return runs(function() { - expect(results).toHaveLength(3); - const resultForA = _.find(results, ({filePath}) => path.basename(filePath) === 'a'); - expect(resultForA.matches).toHaveLength(1); - return expect(resultForA.matches[0].matchText).toBe('Elephant'); - }); - }); + return runs(function () { + expect(results).toHaveLength(3) + const resultForA = _.find(results, ({filePath}) => path.basename(filePath) === 'a') + expect(resultForA.matches).toHaveLength(1) + return expect(resultForA.matches[0].matchText).toBe('Elephant') + }) + }) - it("ignores buffers outside the project", function() { - let editor = null; - const results = []; + it('ignores buffers outside the project', function () { + let editor = null + const results = [] waitsForPromise(() => - atom.workspace.open(temp.openSync().path).then(function(o) { - editor = o; - return editor.setText("Elephant"); + atom.workspace.open(temp.openSync().path).then(function (o) { + editor = o + return editor.setText('Elephant') }) - ); + ) - waitsForPromise(() => atom.workspace.scan(/Elephant/, result => results.push(result))); + waitsForPromise(() => atom.workspace.scan(/Elephant/, result => results.push(result))) - return runs(() => expect(results).toHaveLength(0)); - }); + return runs(() => expect(results).toHaveLength(0)) + }) - return describe("when the project has multiple root directories", function() { - let [dir1, dir2, file1, file2] = Array.from([]); + return describe('when the project has multiple root directories', function () { + let [dir1, dir2, file1, file2] = Array.from([]) - beforeEach(function() { - [dir1] = Array.from(atom.project.getPaths()); - file1 = path.join(dir1, "a-dir", "oh-git"); + beforeEach(function () { + [dir1] = Array.from(atom.project.getPaths()) + file1 = path.join(dir1, 'a-dir', 'oh-git') - dir2 = temp.mkdirSync("a-second-dir"); - const aDir2 = path.join(dir2, "a-dir"); - file2 = path.join(aDir2, "a-file"); - fs.mkdirSync(aDir2); - fs.writeFileSync(file2, "ccc aaaa"); + dir2 = temp.mkdirSync('a-second-dir') + const aDir2 = path.join(dir2, 'a-dir') + file2 = path.join(aDir2, 'a-file') + fs.mkdirSync(aDir2) + fs.writeFileSync(file2, 'ccc aaaa') - return atom.project.addPath(dir2); - }); + return atom.project.addPath(dir2) + }) - it("searches matching files in all of the project's root directories", function() { - const resultPaths = []; + it("searches matching files in all of the project's root directories", function () { + const resultPaths = [] waitsForPromise(() => atom.workspace.scan(/aaaa/, ({filePath}) => resultPaths.push(filePath)) - ); + ) - return runs(() => expect(resultPaths.sort()).toEqual([file1, file2].sort())); - }); + return runs(() => expect(resultPaths.sort()).toEqual([file1, file2].sort())) + }) - describe("when an inclusion path starts with the basename of a root directory", () => - it("interprets the inclusion path as starting from that directory", function() { - waitsForPromise(function() { - const resultPaths = []; + describe('when an inclusion path starts with the basename of a root directory', () => + it('interprets the inclusion path as starting from that directory', function () { + waitsForPromise(function () { + const resultPaths = [] return atom.workspace - .scan(/aaaa/, {paths: ["dir"]}, function({filePath}) { - if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath); }}).then(() => expect(resultPaths).toEqual([file1])); - }); + .scan(/aaaa/, {paths: ['dir']}, function ({filePath}) { + if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath) } + }) + .then(() => expect(resultPaths).toEqual([file1])) + }) - waitsForPromise(function() { - const resultPaths = []; + waitsForPromise(function () { + const resultPaths = [] return atom.workspace - .scan(/aaaa/, {paths: [path.join("dir", "a-dir")]}, function({filePath}) { - if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath); }}).then(() => expect(resultPaths).toEqual([file1])); - }); + .scan(/aaaa/, {paths: [path.join('dir', 'a-dir')]}, function ({filePath}) { + if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath) } + }) + .then(() => expect(resultPaths).toEqual([file1])) + }) - waitsForPromise(function() { - const resultPaths = []; + waitsForPromise(function () { + const resultPaths = [] return atom.workspace - .scan(/aaaa/, {paths: [path.basename(dir2)]}, function({filePath}) { - if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath); }}).then(() => expect(resultPaths).toEqual([file2])); - }); + .scan(/aaaa/, {paths: [path.basename(dir2)]}, function ({filePath}) { + if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath) } + }) + .then(() => expect(resultPaths).toEqual([file2])) + }) - return waitsForPromise(function() { - const resultPaths = []; + return waitsForPromise(function () { + const resultPaths = [] return atom.workspace - .scan(/aaaa/, {paths: [path.join(path.basename(dir2), "a-dir")]}, function({filePath}) { - if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath); }}).then(() => expect(resultPaths).toEqual([file2])); - }); + .scan(/aaaa/, {paths: [path.join(path.basename(dir2), 'a-dir')]}, function ({filePath}) { + if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath) } + }) + .then(() => expect(resultPaths).toEqual([file2])) + }) }) - ); + ) - return describe("when a custom directory searcher is registered", function() { - let fakeSearch = null; + return describe('when a custom directory searcher is registered', function () { + let fakeSearch = null // Function that is invoked once all of the fields on fakeSearch are set. - let onFakeSearchCreated = null; + let onFakeSearchCreated = null class FakeSearch { - constructor(options) { + constructor (options) { // Note that hoisting resolve and reject in this way is generally frowned upon. - this.options = options; - this.promise = new Promise((function(resolve, reject) { - this.hoistedResolve = resolve; - this.hoistedReject = reject; - return (typeof onFakeSearchCreated === 'function' ? onFakeSearchCreated(this) : undefined); - }.bind(this))); + this.options = options + this.promise = new Promise((function (resolve, reject) { + this.hoistedResolve = resolve + this.hoistedReject = reject + return (typeof onFakeSearchCreated === 'function' ? onFakeSearchCreated(this) : undefined) + }.bind(this))) } - then(...args) { - return this.promise.then.apply(this.promise, args); + then (...args) { + return this.promise.then.apply(this.promise, args) } - cancel() { - this.cancelled = true; + cancel () { + this.cancelled = true // According to the spec for a DirectorySearcher, invoking `cancel()` should // resolve the thenable rather than reject it. - return this.hoistedResolve(); + return this.hoistedResolve() } } - beforeEach(function() { - fakeSearch = null; - onFakeSearchCreated = null; + beforeEach(function () { + fakeSearch = null + onFakeSearchCreated = null atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { - canSearchDirectory(directory) { return directory.getPath() === dir1; }, - search(directory, regex, options) { return fakeSearch = new FakeSearch(options); } - }); + canSearchDirectory (directory) { return directory.getPath() === dir1 }, + search (directory, regex, options) { return (fakeSearch = new FakeSearch(options)) } + }) - return waitsFor(() => atom.workspace.directorySearchers.length > 0); - }); + return waitsFor(() => atom.workspace.directorySearchers.length > 0) + }) - it("can override the DefaultDirectorySearcher on a per-directory basis", function() { - const foreignFilePath = 'ssh://foreign-directory:8080/hello.txt'; - const numPathsSearchedInDir2 = 1; - const numPathsToPretendToSearchInCustomDirectorySearcher = 10; + it('can override the DefaultDirectorySearcher on a per-directory basis', function () { + const foreignFilePath = 'ssh://foreign-directory:8080/hello.txt' + const numPathsSearchedInDir2 = 1 + const numPathsToPretendToSearchInCustomDirectorySearcher = 10 const searchResult = { filePath: foreignFilePath, matches: [ @@ -1568,410 +1627,407 @@ i = /test/; #FIXME\ lineText: 'Hello world', lineTextOffset: 0, matchText: 'Hello', - range: [[0, 0], [0, 5]], - }, + range: [[0, 0], [0, 5]] + } ] - }; - onFakeSearchCreated = function(fakeSearch) { - fakeSearch.options.didMatch(searchResult); - fakeSearch.options.didSearchPaths(numPathsToPretendToSearchInCustomDirectorySearcher); - return fakeSearch.hoistedResolve(); - }; + } + onFakeSearchCreated = function (fakeSearch) { + fakeSearch.options.didMatch(searchResult) + fakeSearch.options.didSearchPaths(numPathsToPretendToSearchInCustomDirectorySearcher) + return fakeSearch.hoistedResolve() + } - const resultPaths = []; - const onPathsSearched = jasmine.createSpy('onPathsSearched'); + const resultPaths = [] + const onPathsSearched = jasmine.createSpy('onPathsSearched') waitsForPromise(() => atom.workspace.scan(/aaaa/, {onPathsSearched}, ({filePath}) => resultPaths.push(filePath)) - ); + ) - return runs(function() { - expect(resultPaths.sort()).toEqual([foreignFilePath, file2].sort()); + return runs(function () { + expect(resultPaths.sort()).toEqual([foreignFilePath, file2].sort()) // onPathsSearched should be called once by each DirectorySearcher. The order is not // guaranteed, so we can only verify the total number of paths searched is correct // after the second call. - expect(onPathsSearched.callCount).toBe(2); + expect(onPathsSearched.callCount).toBe(2) return expect(onPathsSearched.mostRecentCall.args[0]).toBe( - numPathsToPretendToSearchInCustomDirectorySearcher + numPathsSearchedInDir2); - }); - }); + numPathsToPretendToSearchInCustomDirectorySearcher + numPathsSearchedInDir2) + }) + }) - it("can be cancelled when the object returned by scan() has its cancel() method invoked", function() { - const thenable = atom.workspace.scan(/aaaa/, function() {}); - let resultOfPromiseSearch = null; + it('can be cancelled when the object returned by scan() has its cancel() method invoked', function () { + const thenable = atom.workspace.scan(/aaaa/, function () {}) + let resultOfPromiseSearch = null - waitsFor('fakeSearch to be defined', () => fakeSearch != null); + waitsFor('fakeSearch to be defined', () => fakeSearch != null) - runs(function() { - expect(fakeSearch.cancelled).toBe(undefined); - thenable.cancel(); - return expect(fakeSearch.cancelled).toBe(true); - }); + runs(function () { + expect(fakeSearch.cancelled).toBe(undefined) + thenable.cancel() + return expect(fakeSearch.cancelled).toBe(true) + }) + waitsForPromise(() => thenable.then(promiseResult => (resultOfPromiseSearch = promiseResult))) - waitsForPromise(() => thenable.then(promiseResult => resultOfPromiseSearch = promiseResult)); + return runs(() => expect(resultOfPromiseSearch).toBe('cancelled')) + }) - return runs(() => expect(resultOfPromiseSearch).toBe('cancelled')); - }); - - return it("will have the side-effect of failing the overall search if it fails", function() { + return it('will have the side-effect of failing the overall search if it fails', function () { // This provider's search should be cancelled when the first provider fails - let cancelableSearch; - let fakeSearch2 = null; + let cancelableSearch + let fakeSearch2 = null atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { - canSearchDirectory(directory) { return directory.getPath() === dir2; }, - search(directory, regex, options) { return fakeSearch2 = new FakeSearch(options); } - }); + canSearchDirectory (directory) { return directory.getPath() === dir2 }, + search (directory, regex, options) { return (fakeSearch2 = new FakeSearch(options)) } + }) - let didReject = false; - const promise = cancelableSearch = atom.workspace.scan(/aaaa/, function() {}); - waitsFor('fakeSearch to be defined', () => fakeSearch != null); + let didReject = false + const promise = cancelableSearch = atom.workspace.scan(/aaaa/, function () {}) + waitsFor('fakeSearch to be defined', () => fakeSearch != null) - runs(() => fakeSearch.hoistedReject()); + runs(() => fakeSearch.hoistedReject()) - waitsForPromise(() => cancelableSearch.catch(() => didReject = true)); + waitsForPromise(() => cancelableSearch.catch(() => (didReject = true))) - waitsFor(done => promise.then(null, done)); + waitsFor(done => promise.then(null, done)) - return runs(function() { - expect(didReject).toBe(true); - return expect(fakeSearch2.cancelled).toBe(true); - }); - }); - }); - }); + return runs(function () { + expect(didReject).toBe(true) + return expect(fakeSearch2.cancelled).toBe(true) + }) + }) + }) + }) }) - ); // Cancels other ongoing searches + ) // Cancels other ongoing searches - describe("::replace(regex, replacementText, paths, iterator)", function() { - let [filePath, commentFilePath, sampleContent, sampleCommentContent] = Array.from([]); + describe('::replace(regex, replacementText, paths, iterator)', function () { + let [filePath, commentFilePath, sampleContent, sampleCommentContent] = Array.from([]) - beforeEach(function() { - atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('../'))]); + beforeEach(function () { + atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('../'))]) - filePath = __guard__(atom.project.getDirectories()[0], x1 => x1.resolve('sample.js')); - commentFilePath = __guard__(atom.project.getDirectories()[0], x2 => x2.resolve('sample-with-comments.js')); - sampleContent = fs.readFileSync(filePath).toString(); - return sampleCommentContent = fs.readFileSync(commentFilePath).toString(); - }); + filePath = __guard__(atom.project.getDirectories()[0], x1 => x1.resolve('sample.js')) + commentFilePath = __guard__(atom.project.getDirectories()[0], x2 => x2.resolve('sample-with-comments.js')) + sampleContent = fs.readFileSync(filePath).toString() + return (sampleCommentContent = fs.readFileSync(commentFilePath).toString()) + }) - afterEach(function() { - fs.writeFileSync(filePath, sampleContent); - return fs.writeFileSync(commentFilePath, sampleCommentContent); - }); + afterEach(function () { + fs.writeFileSync(filePath, sampleContent) + return fs.writeFileSync(commentFilePath, sampleCommentContent) + }) describe("when a file doesn't exist", () => - it("calls back with an error", function() { - const errors = []; - const missingPath = path.resolve('/not-a-file.js'); - expect(fs.existsSync(missingPath)).toBeFalsy(); + it('calls back with an error', function () { + const errors = [] + const missingPath = path.resolve('/not-a-file.js') + expect(fs.existsSync(missingPath)).toBeFalsy() waitsForPromise(() => atom.workspace.replace(/items/gi, 'items', [missingPath], (result, error) => errors.push(error)) - ); + ) - return runs(function() { - expect(errors).toHaveLength(1); - return expect(errors[0].path).toBe(missingPath); - }); + return runs(function () { + expect(errors).toHaveLength(1) + return expect(errors[0].path).toBe(missingPath) + }) }) - ); + ) - describe("when called with unopened files", () => - it("replaces properly", function() { - const results = []; + describe('when called with unopened files', () => + it('replaces properly', function () { + const results = [] waitsForPromise(() => atom.workspace.replace(/items/gi, 'items', [filePath], result => results.push(result)) - ); + ) - return runs(function() { - expect(results).toHaveLength(1); - expect(results[0].filePath).toBe(filePath); - return expect(results[0].replacements).toBe(6); - }); + return runs(function () { + expect(results).toHaveLength(1) + expect(results[0].filePath).toBe(filePath) + return expect(results[0].replacements).toBe(6) + }) }) - ); + ) - return describe("when a buffer is already open", function() { - it("replaces properly and saves when not modified", function() { - let editor = null; - const results = []; + return describe('when a buffer is already open', function () { + it('replaces properly and saves when not modified', function () { + let editor = null + const results = [] - waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)); + waitsForPromise(() => atom.workspace.open('sample.js').then(o => (editor = o))) - runs(() => expect(editor.isModified()).toBeFalsy()); + runs(() => expect(editor.isModified()).toBeFalsy()) waitsForPromise(() => atom.workspace.replace(/items/gi, 'items', [filePath], result => results.push(result)) - ); + ) - return runs(function() { - expect(results).toHaveLength(1); - expect(results[0].filePath).toBe(filePath); - expect(results[0].replacements).toBe(6); + return runs(function () { + expect(results).toHaveLength(1) + expect(results[0].filePath).toBe(filePath) + expect(results[0].replacements).toBe(6) - return expect(editor.isModified()).toBeFalsy(); - }); - }); + return expect(editor.isModified()).toBeFalsy() + }) + }) - it("does not replace when the path is not specified", function() { - let editor = null; - const results = []; + it('does not replace when the path is not specified', function () { + const results = [] - waitsForPromise(() => atom.workspace.open('sample-with-comments.js').then(o => editor = o)); + waitsForPromise(() => atom.workspace.open('sample-with-comments.js')) waitsForPromise(() => atom.workspace.replace(/items/gi, 'items', [commentFilePath], result => results.push(result)) - ); + ) - return runs(function() { - expect(results).toHaveLength(1); - return expect(results[0].filePath).toBe(commentFilePath); - }); - }); + return runs(function () { + expect(results).toHaveLength(1) + return expect(results[0].filePath).toBe(commentFilePath) + }) + }) - return it("does NOT save when modified", function() { - let editor = null; - const results = []; + return it('does NOT save when modified', function () { + let editor = null + const results = [] - waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)); + waitsForPromise(() => atom.workspace.open('sample.js').then(o => (editor = o))) - runs(function() { - editor.buffer.setTextInRange([[0, 0], [0, 0]], 'omg'); - return expect(editor.isModified()).toBeTruthy(); - }); + runs(function () { + editor.buffer.setTextInRange([[0, 0], [0, 0]], 'omg') + return expect(editor.isModified()).toBeTruthy() + }) waitsForPromise(() => atom.workspace.replace(/items/gi, 'okthen', [filePath], result => results.push(result)) - ); + ) - return runs(function() { - expect(results).toHaveLength(1); - expect(results[0].filePath).toBe(filePath); - expect(results[0].replacements).toBe(6); + return runs(function () { + expect(results).toHaveLength(1) + expect(results[0].filePath).toBe(filePath) + expect(results[0].replacements).toBe(6) - return expect(editor.isModified()).toBeTruthy(); - }); - }); - }); - }); - - describe("::saveActivePaneItem()", function() { - let editor = null; - beforeEach(() => - waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)) - ); - - return describe("when there is an error", function() { - it("emits a warning notification when the file cannot be saved", function() { - let addedSpy; - spyOn(editor, 'save').andCallFake(function() { - throw new Error("'/some/file' is a directory"); - }); - - atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()); - atom.workspace.saveActivePaneItem(); - expect(addedSpy).toHaveBeenCalled(); - return expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning'); - }); - - it("emits a warning notification when the directory cannot be written to", function() { - let addedSpy; - spyOn(editor, 'save').andCallFake(function() { - throw new Error("ENOTDIR, not a directory '/Some/dir/and-a-file.js'"); - }); - - atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()); - atom.workspace.saveActivePaneItem(); - expect(addedSpy).toHaveBeenCalled(); - return expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning'); - }); - - it("emits a warning notification when the user does not have permission", function() { - let addedSpy; - spyOn(editor, 'save').andCallFake(function() { - const error = new Error("EACCES, permission denied '/Some/dir/and-a-file.js'"); - error.code = 'EACCES'; - error.path = '/Some/dir/and-a-file.js'; - throw error; - }); - - atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()); - atom.workspace.saveActivePaneItem(); - expect(addedSpy).toHaveBeenCalled(); - return expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning'); - }); - - it("emits a warning notification when the operation is not permitted", () => - spyOn(editor, 'save').andCallFake(function() { - const error = new Error("EPERM, operation not permitted '/Some/dir/and-a-file.js'"); - error.code = 'EPERM'; - error.path = '/Some/dir/and-a-file.js'; - throw error; + return expect(editor.isModified()).toBeTruthy() }) - ); + }) + }) + }) - it("emits a warning notification when the file is already open by another app", function() { - let addedSpy; - spyOn(editor, 'save').andCallFake(function() { - const error = new Error("EBUSY, resource busy or locked '/Some/dir/and-a-file.js'"); - error.code = 'EBUSY'; - error.path = '/Some/dir/and-a-file.js'; - throw error; - }); + describe('::saveActivePaneItem()', function () { + let editor = null + beforeEach(() => + waitsForPromise(() => atom.workspace.open('sample.js').then(o => (editor = o))) + ) - atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()); - atom.workspace.saveActivePaneItem(); - expect(addedSpy).toHaveBeenCalled(); + return describe('when there is an error', function () { + it('emits a warning notification when the file cannot be saved', function () { + let addedSpy + spyOn(editor, 'save').andCallFake(function () { + throw new Error("'/some/file' is a directory") + }) - const notificaiton = addedSpy.mostRecentCall.args[0]; - expect(notificaiton.getType()).toBe('warning'); - return expect(notificaiton.getMessage()).toContain('Unable to save'); - }); + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) + atom.workspace.saveActivePaneItem() + expect(addedSpy).toHaveBeenCalled() + return expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') + }) - it("emits a warning notification when the file system is read-only", function() { - let addedSpy; - spyOn(editor, 'save').andCallFake(function() { - const error = new Error("EROFS, read-only file system '/Some/dir/and-a-file.js'"); - error.code = 'EROFS'; - error.path = '/Some/dir/and-a-file.js'; - throw error; - }); + it('emits a warning notification when the directory cannot be written to', function () { + let addedSpy + spyOn(editor, 'save').andCallFake(function () { + throw new Error("ENOTDIR, not a directory '/Some/dir/and-a-file.js'") + }) - atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()); - atom.workspace.saveActivePaneItem(); - expect(addedSpy).toHaveBeenCalled(); + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) + atom.workspace.saveActivePaneItem() + expect(addedSpy).toHaveBeenCalled() + return expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') + }) - const notification = addedSpy.mostRecentCall.args[0]; - expect(notification.getType()).toBe('warning'); - return expect(notification.getMessage()).toContain('Unable to save'); - }); + it('emits a warning notification when the user does not have permission', function () { + let addedSpy + spyOn(editor, 'save').andCallFake(function () { + const error = new Error("EACCES, permission denied '/Some/dir/and-a-file.js'") + error.code = 'EACCES' + error.path = '/Some/dir/and-a-file.js' + throw error + }) - return it("emits a warning notification when the file cannot be saved", function() { - spyOn(editor, 'save').andCallFake(function() { - throw new Error("no one knows"); - }); + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) + atom.workspace.saveActivePaneItem() + expect(addedSpy).toHaveBeenCalled() + return expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') + }) - const save = () => atom.workspace.saveActivePaneItem(); - return expect(save).toThrow(); - }); - }); - }); + it('emits a warning notification when the operation is not permitted', () => + spyOn(editor, 'save').andCallFake(function () { + const error = new Error("EPERM, operation not permitted '/Some/dir/and-a-file.js'") + error.code = 'EPERM' + error.path = '/Some/dir/and-a-file.js' + throw error + }) + ) - describe("::closeActivePaneItemOrEmptyPaneOrWindow", function() { - beforeEach(function() { - spyOn(atom, 'close'); - return waitsForPromise(() => atom.workspace.open()); - }); + it('emits a warning notification when the file is already open by another app', function () { + let addedSpy + spyOn(editor, 'save').andCallFake(function () { + const error = new Error("EBUSY, resource busy or locked '/Some/dir/and-a-file.js'") + error.code = 'EBUSY' + error.path = '/Some/dir/and-a-file.js' + throw error + }) - return it("closes the active pane item, or the active pane if it is empty, or the current window if there is only the empty root pane", function() { - atom.config.set('core.destroyEmptyPanes', false); + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) + atom.workspace.saveActivePaneItem() + expect(addedSpy).toHaveBeenCalled() - const pane1 = atom.workspace.getActivePane(); - const pane2 = pane1.splitRight({copyActiveItem: true}); + const notificaiton = addedSpy.mostRecentCall.args[0] + expect(notificaiton.getType()).toBe('warning') + return expect(notificaiton.getMessage()).toContain('Unable to save') + }) - expect(atom.workspace.getPanes().length).toBe(2); - expect(pane2.getItems().length).toBe(1); - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); + it('emits a warning notification when the file system is read-only', function () { + let addedSpy + spyOn(editor, 'save').andCallFake(function () { + const error = new Error("EROFS, read-only file system '/Some/dir/and-a-file.js'") + error.code = 'EROFS' + error.path = '/Some/dir/and-a-file.js' + throw error + }) - expect(atom.workspace.getPanes().length).toBe(2); - expect(pane2.getItems().length).toBe(0); + atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) + atom.workspace.saveActivePaneItem() + expect(addedSpy).toHaveBeenCalled() - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); + const notification = addedSpy.mostRecentCall.args[0] + expect(notification.getType()).toBe('warning') + return expect(notification.getMessage()).toContain('Unable to save') + }) - expect(atom.workspace.getPanes().length).toBe(1); - expect(pane1.getItems().length).toBe(1); + return it('emits a warning notification when the file cannot be saved', function () { + spyOn(editor, 'save').andCallFake(function () { + throw new Error('no one knows') + }) - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); - expect(atom.workspace.getPanes().length).toBe(1); - expect(pane1.getItems().length).toBe(0); + const save = () => atom.workspace.saveActivePaneItem() + return expect(save).toThrow() + }) + }) + }) - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); - expect(atom.workspace.getPanes().length).toBe(1); + describe('::closeActivePaneItemOrEmptyPaneOrWindow', function () { + beforeEach(function () { + spyOn(atom, 'close') + return waitsForPromise(() => atom.workspace.open()) + }) - atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow(); - return expect(atom.close).toHaveBeenCalled(); - }); - }); + return it('closes the active pane item, or the active pane if it is empty, or the current window if there is only the empty root pane', function () { + atom.config.set('core.destroyEmptyPanes', false) - describe("when the core.allowPendingPaneItems option is falsey", () => - it("does not open item with `pending: true` option as pending", function() { - let pane = null; - atom.config.set('core.allowPendingPaneItems', false); + const pane1 = atom.workspace.getActivePane() + const pane2 = pane1.splitRight({copyActiveItem: true}) + + expect(atom.workspace.getPanes().length).toBe(2) + expect(pane2.getItems().length).toBe(1) + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + + expect(atom.workspace.getPanes().length).toBe(2) + expect(pane2.getItems().length).toBe(0) + + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + + expect(atom.workspace.getPanes().length).toBe(1) + expect(pane1.getItems().length).toBe(1) + + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + expect(atom.workspace.getPanes().length).toBe(1) + expect(pane1.getItems().length).toBe(0) + + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + expect(atom.workspace.getPanes().length).toBe(1) + + atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() + return expect(atom.close).toHaveBeenCalled() + }) + }) + + describe('when the core.allowPendingPaneItems option is falsey', () => + it('does not open item with `pending: true` option as pending', function () { + let pane = null + atom.config.set('core.allowPendingPaneItems', false) waitsForPromise(() => - atom.workspace.open('sample.js', {pending: true}).then(() => pane = atom.workspace.getActivePane()) - ); + atom.workspace.open('sample.js', {pending: true}).then(() => (pane = atom.workspace.getActivePane())) + ) - return runs(() => expect(pane.getPendingItem()).toBeFalsy()); + return runs(() => expect(pane.getPendingItem()).toBeFalsy()) }) - ); + ) - describe("grammar activation", () => - it("notifies the workspace of which grammar is used", function() { - const editor = null; - atom.packages.triggerDeferredActivationHooks(); + describe('grammar activation', () => + it('notifies the workspace of which grammar is used', function () { + atom.packages.triggerDeferredActivationHooks() - const javascriptGrammarUsed = jasmine.createSpy('js grammar used'); - const rubyGrammarUsed = jasmine.createSpy('ruby grammar used'); - const cGrammarUsed = jasmine.createSpy('c grammar used'); + const javascriptGrammarUsed = jasmine.createSpy('js grammar used') + const rubyGrammarUsed = jasmine.createSpy('ruby grammar used') + const cGrammarUsed = jasmine.createSpy('c grammar used') - atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', javascriptGrammarUsed); - atom.packages.onDidTriggerActivationHook('language-ruby:grammar-used', rubyGrammarUsed); - atom.packages.onDidTriggerActivationHook('language-c:grammar-used', cGrammarUsed); + atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', javascriptGrammarUsed) + atom.packages.onDidTriggerActivationHook('language-ruby:grammar-used', rubyGrammarUsed) + atom.packages.onDidTriggerActivationHook('language-c:grammar-used', cGrammarUsed) - waitsForPromise(() => atom.packages.activatePackage('language-ruby')); - waitsForPromise(() => atom.packages.activatePackage('language-javascript')); - waitsForPromise(() => atom.packages.activatePackage('language-c')); - waitsForPromise(() => atom.workspace.open('sample-with-comments.js')); + waitsForPromise(() => atom.packages.activatePackage('language-ruby')) + waitsForPromise(() => atom.packages.activatePackage('language-javascript')) + waitsForPromise(() => atom.packages.activatePackage('language-c')) + waitsForPromise(() => atom.workspace.open('sample-with-comments.js')) - return runs(function() { + return runs(function () { // Hooks are triggered when opening new editors - expect(javascriptGrammarUsed).toHaveBeenCalled(); + expect(javascriptGrammarUsed).toHaveBeenCalled() // Hooks are triggered when changing existing editors grammars - atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.c')); - expect(cGrammarUsed).toHaveBeenCalled(); + atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.c')) + expect(cGrammarUsed).toHaveBeenCalled() // Hooks are triggered when editors are added in other ways. - atom.workspace.getActivePane().splitRight({copyActiveItem: true}); - atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.ruby')); - return expect(rubyGrammarUsed).toHaveBeenCalled(); - }); + atom.workspace.getActivePane().splitRight({copyActiveItem: true}) + atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.ruby')) + return expect(rubyGrammarUsed).toHaveBeenCalled() + }) }) - ); + ) - describe(".checkoutHeadRevision()", function() { - let editor = null; - beforeEach(function() { - atom.config.set("editor.confirmCheckoutHeadRevision", false); + describe('.checkoutHeadRevision()', function () { + let editor = null + beforeEach(function () { + atom.config.set('editor.confirmCheckoutHeadRevision', false) - return waitsForPromise(() => atom.workspace.open('sample-with-comments.js').then(o => editor = o)); - }); + return waitsForPromise(() => atom.workspace.open('sample-with-comments.js').then(o => (editor = o))) + }) - it("reverts to the version of its file checked into the project repository", function() { - editor.setCursorBufferPosition([0, 0]); - editor.insertText("---\n"); - expect(editor.lineTextForBufferRow(0)).toBe("---"); + it('reverts to the version of its file checked into the project repository', function () { + editor.setCursorBufferPosition([0, 0]) + editor.insertText('---\n') + expect(editor.lineTextForBufferRow(0)).toBe('---') - waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor)); + waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor)) - return runs(() => expect(editor.lineTextForBufferRow(0)).toBe("")); - }); + return runs(() => expect(editor.lineTextForBufferRow(0)).toBe('')) + }) return describe("when there's no repository for the editor's file", () => - it("doesn't do anything", function() { - editor = new TextEditor; - editor.setText("stuff"); - atom.workspace.checkoutHeadRevision(editor); + it("doesn't do anything", function () { + editor = new TextEditor() + editor.setText('stuff') + atom.workspace.checkoutHeadRevision(editor) - return waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor)); + return waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor)) }) - ); - }); + ) + }) - return escapeStringRegex = str => str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); -}); + return (escapeStringRegex = str => str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')) +}) -function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; +function __guard__ (value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined } diff --git a/src/workspace.js b/src/workspace.js index 612224da3..7ffe8f387 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -1,17 +1,16 @@ -let Workspace; -const _ = require('underscore-plus'); -const url = require('url'); -const path = require('path'); -const {Emitter, Disposable, CompositeDisposable} = require('event-kit'); -const fs = require('fs-plus'); -const {Directory} = require('pathwatcher'); -const DefaultDirectorySearcher = require('./default-directory-searcher'); -const Model = require('./model'); -const TextEditor = require('./text-editor'); -const PaneContainer = require('./pane-container'); -const Panel = require('./panel'); -const PanelContainer = require('./panel-container'); -const Task = require('./task'); +const _ = require('underscore-plus') +const url = require('url') +const path = require('path') +const {Emitter, Disposable, CompositeDisposable} = require('event-kit') +const fs = require('fs-plus') +const {Directory} = require('pathwatcher') +const DefaultDirectorySearcher = require('./default-directory-searcher') +const Model = require('./model') +const TextEditor = require('./text-editor') +const PaneContainer = require('./pane-container') +const Panel = require('./panel') +const PanelContainer = require('./panel-container') +const Task = require('./task') // Essential: Represents the state of the user interface for the entire window. // An instance of this class is available via the `atom.workspace` global. @@ -22,35 +21,35 @@ const Task = require('./task'); // // * `editor` {TextEditor} the new editor // -module.exports = -Workspace = class Workspace extends Model { - constructor(params) { - this.updateWindowTitle = this.updateWindowTitle.bind(this); - this.updateDocumentEdited = this.updateDocumentEdited.bind(this); - this.didDestroyPaneItem = this.didDestroyPaneItem.bind(this); - super(...arguments); +module.exports = class Workspace extends Model { + constructor (params) { + super(...arguments) - ({ + this.updateWindowTitle = this.updateWindowTitle.bind(this) + this.updateDocumentEdited = this.updateDocumentEdited.bind(this) + this.didDestroyPaneItem = this.didDestroyPaneItem.bind(this) + + ;({ packageManager: this.packageManager, config: this.config, project: this.project, grammarRegistry: this.grammarRegistry, notificationManager: this.notificationManager, viewRegistry: this.viewRegistry, grammarRegistry: this.grammarRegistry, applicationDelegate: this.applicationDelegate, assert: this.assert, deserializerManager: this.deserializerManager, textEditorRegistry: this.textEditorRegistry - } = params); + } = params) - this.emitter = new Emitter; - this.openers = []; - this.destroyedItemURIs = []; + this.emitter = new Emitter() + this.openers = [] + this.destroyedItemURIs = [] - this.paneContainer = new PaneContainer({config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, deserializerManager: this.deserializerManager}); - this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem); + this.paneContainer = new PaneContainer({config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, deserializerManager: this.deserializerManager}) + this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem) - this.defaultDirectorySearcher = new DefaultDirectorySearcher(); - this.consumeServices(this.packageManager); + this.defaultDirectorySearcher = new DefaultDirectorySearcher() + this.consumeServices(this.packageManager) // One cannot simply .bind here since it could be used as a component with // Etch, in which case it'd be `new`d. And when it's `new`d, `this` is always // the newly created object. - const realThis = this; - this.buildTextEditor = function() { return Workspace.prototype.buildTextEditor.apply(realThis, arguments); }; + const realThis = this + this.buildTextEditor = function () { return Workspace.prototype.buildTextEditor.apply(realThis, arguments) } this.panelContainers = { top: new PanelContainer({location: 'top'}), @@ -60,21 +59,21 @@ Workspace = class Workspace extends Model { header: new PanelContainer({location: 'header'}), footer: new PanelContainer({location: 'footer'}), modal: new PanelContainer({location: 'modal'}) - }; + } - this.subscribeToEvents(); + this.subscribeToEvents() } - reset(packageManager) { - this.packageManager = packageManager; - this.emitter.dispose(); - this.emitter = new Emitter; + reset (packageManager) { + this.packageManager = packageManager + this.emitter.dispose() + this.emitter = new Emitter() - this.paneContainer.destroy(); - for (let panelContainer of this.panelContainers) { panelContainer.destroy(); } + this.paneContainer.destroy() + for (let panelContainer of this.panelContainers) { panelContainer.destroy() } - this.paneContainer = new PaneContainer({config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, deserializerManager: this.deserializerManager}); - this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem); + this.paneContainer = new PaneContainer({config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, deserializerManager: this.deserializerManager}) + this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem) this.panelContainers = { top: new PanelContainer({location: 'top'}), @@ -84,115 +83,115 @@ Workspace = class Workspace extends Model { header: new PanelContainer({location: 'header'}), footer: new PanelContainer({location: 'footer'}), modal: new PanelContainer({location: 'modal'}) - }; + } - this.originalFontSize = null; - this.openers = []; - this.destroyedItemURIs = []; - return this.consumeServices(this.packageManager); + this.originalFontSize = null + this.openers = [] + this.destroyedItemURIs = [] + return this.consumeServices(this.packageManager) } - subscribeToEvents() { - this.subscribeToActiveItem(); - this.subscribeToFontSize(); - return this.subscribeToAddedItems(); + subscribeToEvents () { + this.subscribeToActiveItem() + this.subscribeToFontSize() + return this.subscribeToAddedItems() } - consumeServices({serviceHub}) { - this.directorySearchers = []; + consumeServices ({serviceHub}) { + this.directorySearchers = [] return serviceHub.consume( 'atom.directory-searcher', '^0.1.0', - provider => this.directorySearchers.unshift(provider)); + provider => this.directorySearchers.unshift(provider)) } // Called by the Serializable mixin during serialization. - serialize() { + serialize () { return { deserializer: 'Workspace', paneContainer: this.paneContainer.serialize(), packagesWithActiveGrammars: this.getPackageNamesWithActiveGrammars(), destroyedItemURIs: this.destroyedItemURIs.slice() - }; + } } - deserialize(state, deserializerManager) { + deserialize (state, deserializerManager) { for (let packageName of state.packagesWithActiveGrammars != null ? state.packagesWithActiveGrammars : []) { - __guard__(this.packageManager.getLoadedPackage(packageName), x => x.loadGrammarsSync()); + __guard__(this.packageManager.getLoadedPackage(packageName), x => x.loadGrammarsSync()) } if (state.destroyedItemURIs != null) { - this.destroyedItemURIs = state.destroyedItemURIs; + this.destroyedItemURIs = state.destroyedItemURIs } - return this.paneContainer.deserialize(state.paneContainer, deserializerManager); + return this.paneContainer.deserialize(state.paneContainer, deserializerManager) } - getPackageNamesWithActiveGrammars() { - const packageNames = []; - var addGrammar = ({includedGrammarScopes, packageName}={}) => { - if (!packageName) { return; } + getPackageNamesWithActiveGrammars () { + const packageNames = [] + var addGrammar = ({includedGrammarScopes, packageName} = {}) => { + if (!packageName) { return } // Prevent cycles - if (packageNames.indexOf(packageName) !== -1) { return; } + if (packageNames.indexOf(packageName) !== -1) { return } - packageNames.push(packageName); + packageNames.push(packageName) for (let scopeName of includedGrammarScopes != null ? includedGrammarScopes : []) { - addGrammar(this.grammarRegistry.grammarForScopeName(scopeName)); + addGrammar(this.grammarRegistry.grammarForScopeName(scopeName)) } - }; + } - const editors = this.getTextEditors(); - for (let editor of editors) { addGrammar(editor.getGrammar()); } + const editors = this.getTextEditors() + for (let editor of editors) { addGrammar(editor.getGrammar()) } if (editors.length > 0) { for (let grammar of this.grammarRegistry.getGrammars()) { if (grammar.injectionSelector) { - addGrammar(grammar); + addGrammar(grammar) } } } - return _.uniq(packageNames); + return _.uniq(packageNames) } - subscribeToActiveItem() { - this.updateWindowTitle(); - this.updateDocumentEdited(); - this.project.onDidChangePaths(this.updateWindowTitle); + subscribeToActiveItem () { + this.updateWindowTitle() + this.updateDocumentEdited() + this.project.onDidChangePaths(this.updateWindowTitle) return this.observeActivePaneItem(item => { - let modifiedSubscription, titleSubscription; - this.updateWindowTitle(); - this.updateDocumentEdited(); + let modifiedSubscription, titleSubscription + this.updateWindowTitle() + this.updateDocumentEdited() if (this.activeItemSubscriptions != null) { - this.activeItemSubscriptions.dispose(); + this.activeItemSubscriptions.dispose() } - this.activeItemSubscriptions = new CompositeDisposable; + this.activeItemSubscriptions = new CompositeDisposable() if (typeof (item != null ? item.onDidChangeTitle : undefined) === 'function') { - titleSubscription = item.onDidChangeTitle(this.updateWindowTitle); + titleSubscription = item.onDidChangeTitle(this.updateWindowTitle) } else if (typeof (item != null ? item.on : undefined) === 'function') { - titleSubscription = item.on('title-changed', this.updateWindowTitle); + titleSubscription = item.on('title-changed', this.updateWindowTitle) if (typeof (titleSubscription != null ? titleSubscription.dispose : undefined) !== 'function') { - titleSubscription = new Disposable((function() { return item.off('title-changed', this.updateWindowTitle); }.bind(this))); + titleSubscription = new Disposable((function () { return item.off('title-changed', this.updateWindowTitle) }.bind(this))) } } if (typeof (item != null ? item.onDidChangeModified : undefined) === 'function') { - modifiedSubscription = item.onDidChangeModified(this.updateDocumentEdited); + modifiedSubscription = item.onDidChangeModified(this.updateDocumentEdited) } else if (typeof ((item != null ? item.on : undefined) != null) === 'function') { - modifiedSubscription = item.on('modified-status-changed', this.updateDocumentEdited); + modifiedSubscription = item.on('modified-status-changed', this.updateDocumentEdited) if (typeof (modifiedSubscription != null ? modifiedSubscription.dispose : undefined) !== 'function') { - modifiedSubscription = new Disposable((function() { return item.off('modified-status-changed', this.updateDocumentEdited); }.bind(this))); + modifiedSubscription = new Disposable((function () { return item.off('modified-status-changed', this.updateDocumentEdited) }.bind(this))) } } - if (titleSubscription != null) { this.activeItemSubscriptions.add(titleSubscription); } - if (modifiedSubscription != null) { return this.activeItemSubscriptions.add(modifiedSubscription); } + if (titleSubscription != null) { this.activeItemSubscriptions.add(titleSubscription) } + if (modifiedSubscription != null) { return this.activeItemSubscriptions.add(modifiedSubscription) } } - ); + ) } - subscribeToAddedItems() { + subscribeToAddedItems () { return this.onDidAddPaneItem(({item, pane, index}) => { if (item instanceof TextEditor) { const subscriptions = new CompositeDisposable( @@ -200,57 +199,57 @@ Workspace = class Workspace extends Model { this.textEditorRegistry.maintainGrammar(item), this.textEditorRegistry.maintainConfig(item), item.observeGrammar(this.handleGrammarUsed.bind(this)) - ); - item.onDidDestroy(() => subscriptions.dispose()); - return this.emitter.emit('did-add-text-editor', {textEditor: item, pane, index}); + ) + item.onDidDestroy(() => subscriptions.dispose()) + return this.emitter.emit('did-add-text-editor', {textEditor: item, pane, index}) } - }); + }) } // Updates the application's title and proxy icon based on whichever file is // open. - updateWindowTitle() { - let item, itemPath, itemTitle, left, projectPath, representedPath; - const appName = 'Atom'; - const projectPaths = (left = this.project.getPaths()) != null ? left : []; - if (item = this.getActivePaneItem()) { - let left1; - itemPath = typeof item.getPath === 'function' ? item.getPath() : undefined; - itemTitle = (left1 = (typeof item.getLongTitle === 'function' ? item.getLongTitle() : undefined)) != null ? left1 : (typeof item.getTitle === 'function' ? item.getTitle() : undefined); - projectPath = _.find(projectPaths, projectPath => (itemPath === projectPath) || (itemPath != null ? itemPath.startsWith(projectPath + path.sep) : undefined)); + updateWindowTitle () { + let item, itemPath, itemTitle, left, projectPath, representedPath + const appName = 'Atom' + const projectPaths = (left = this.project.getPaths()) != null ? left : [] + if ((item = this.getActivePaneItem())) { + let left1 + itemPath = typeof item.getPath === 'function' ? item.getPath() : undefined + itemTitle = (left1 = (typeof item.getLongTitle === 'function' ? item.getLongTitle() : undefined)) != null ? left1 : (typeof item.getTitle === 'function' ? item.getTitle() : undefined) + projectPath = _.find(projectPaths, projectPath => (itemPath === projectPath) || (itemPath != null ? itemPath.startsWith(projectPath + path.sep) : undefined)) } - if (itemTitle == null) { itemTitle = "untitled"; } - if (projectPath == null) { projectPath = itemPath ? path.dirname(itemPath) : projectPaths[0]; } + if (itemTitle == null) { itemTitle = 'untitled' } + if (projectPath == null) { projectPath = itemPath ? path.dirname(itemPath) : projectPaths[0] } if (projectPath != null) { - projectPath = fs.tildify(projectPath); + projectPath = fs.tildify(projectPath) } - const titleParts = []; + const titleParts = [] if ((item != null) && (projectPath != null)) { - titleParts.push(itemTitle, projectPath); - representedPath = itemPath != null ? itemPath : projectPath; + titleParts.push(itemTitle, projectPath) + representedPath = itemPath != null ? itemPath : projectPath } else if (projectPath != null) { - titleParts.push(projectPath); - representedPath = projectPath; + titleParts.push(projectPath) + representedPath = projectPath } else { - titleParts.push(itemTitle); - representedPath = ""; + titleParts.push(itemTitle) + representedPath = '' } if (process.platform !== 'darwin') { - titleParts.push(appName); + titleParts.push(appName) } - document.title = titleParts.join(" \u2014 "); - return this.applicationDelegate.setRepresentedFilename(representedPath); + document.title = titleParts.join(' \u2014 ') + return this.applicationDelegate.setRepresentedFilename(representedPath) } // On macOS, fades the application window's proxy icon when the current file // has been modified. - updateDocumentEdited() { - let left; - const modified = (left = __guardMethod__(this.getActivePaneItem(), 'isModified', o => o.isModified())) != null ? left : false; - return this.applicationDelegate.setWindowDocumentEdited(modified); + updateDocumentEdited () { + let left + const modified = (left = __guardMethod__(this.getActivePaneItem(), 'isModified', o => o.isModified())) != null ? left : false + return this.applicationDelegate.setWindowDocumentEdited(modified) } /* @@ -265,9 +264,9 @@ Workspace = class Workspace extends Model { // of subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeTextEditors(callback) { - for (let textEditor of this.getTextEditors()) { callback(textEditor); } - return this.onDidAddTextEditor(({textEditor}) => callback(textEditor)); + observeTextEditors (callback) { + for (let textEditor of this.getTextEditors()) { callback(textEditor) } + return this.onDidAddTextEditor(({textEditor}) => callback(textEditor)) } // Essential: Invoke the given callback with all current and future panes items @@ -278,7 +277,7 @@ Workspace = class Workspace extends Model { // subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observePaneItems(callback) { return this.paneContainer.observePaneItems(callback); } + observePaneItems (callback) { return this.paneContainer.observePaneItems(callback) } // Essential: Invoke the given callback when the active pane item changes. // @@ -291,8 +290,8 @@ Workspace = class Workspace extends Model { // * `item` The active pane item. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeActivePaneItem(callback) { - return this.paneContainer.onDidChangeActivePaneItem(callback); + onDidChangeActivePaneItem (callback) { + return this.paneContainer.onDidChangeActivePaneItem(callback) } // Essential: Invoke the given callback when the active pane item stops @@ -309,8 +308,8 @@ Workspace = class Workspace extends Model { // * `item` The active pane item. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidStopChangingActivePaneItem(callback) { - return this.paneContainer.onDidStopChangingActivePaneItem(callback); + onDidStopChangingActivePaneItem (callback) { + return this.paneContainer.onDidStopChangingActivePaneItem(callback) } // Essential: Invoke the given callback with the current active pane item and @@ -320,7 +319,7 @@ Workspace = class Workspace extends Model { // * `item` The current active pane item. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeActivePaneItem(callback) { return this.paneContainer.observeActivePaneItem(callback); } + observeActivePaneItem (callback) { return this.paneContainer.observeActivePaneItem(callback) } // Essential: Invoke the given callback whenever an item is opened. Unlike // {::onDidAddPaneItem}, observers will be notified for items that are already @@ -334,8 +333,8 @@ Workspace = class Workspace extends Model { // * `index` The index of the opened item on its pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidOpen(callback) { - return this.emitter.on('did-open', callback); + onDidOpen (callback) { + return this.emitter.on('did-open', callback) } // Extended: Invoke the given callback when a pane is added to the workspace. @@ -345,7 +344,7 @@ Workspace = class Workspace extends Model { // * `pane` The added pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddPane(callback) { return this.paneContainer.onDidAddPane(callback); } + onDidAddPane (callback) { return this.paneContainer.onDidAddPane(callback) } // Extended: Invoke the given callback before a pane is destroyed in the // workspace. @@ -355,7 +354,7 @@ Workspace = class Workspace extends Model { // * `pane` The pane to be destroyed. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onWillDestroyPane(callback) { return this.paneContainer.onWillDestroyPane(callback); } + onWillDestroyPane (callback) { return this.paneContainer.onWillDestroyPane(callback) } // Extended: Invoke the given callback when a pane is destroyed in the // workspace. @@ -365,7 +364,7 @@ Workspace = class Workspace extends Model { // * `pane` The destroyed pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroyPane(callback) { return this.paneContainer.onDidDestroyPane(callback); } + onDidDestroyPane (callback) { return this.paneContainer.onDidDestroyPane(callback) } // Extended: Invoke the given callback with all current and future panes in the // workspace. @@ -375,7 +374,7 @@ Workspace = class Workspace extends Model { // subscription or that is added at some later time. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observePanes(callback) { return this.paneContainer.observePanes(callback); } + observePanes (callback) { return this.paneContainer.observePanes(callback) } // Extended: Invoke the given callback when the active pane changes. // @@ -383,7 +382,7 @@ Workspace = class Workspace extends Model { // * `pane` A {Pane} that is the current return value of {::getActivePane}. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeActivePane(callback) { return this.paneContainer.onDidChangeActivePane(callback); } + onDidChangeActivePane (callback) { return this.paneContainer.onDidChangeActivePane(callback) } // Extended: Invoke the given callback with the current active pane and when // the active pane changes. @@ -393,7 +392,7 @@ Workspace = class Workspace extends Model { // * `pane` A {Pane} that is the current return value of {::getActivePane}. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeActivePane(callback) { return this.paneContainer.observeActivePane(callback); } + observeActivePane (callback) { return this.paneContainer.observeActivePane(callback) } // Extended: Invoke the given callback when a pane item is added to the // workspace. @@ -405,7 +404,7 @@ Workspace = class Workspace extends Model { // * `index` {Number} indicating the index of the added item in its pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddPaneItem(callback) { return this.paneContainer.onDidAddPaneItem(callback); } + onDidAddPaneItem (callback) { return this.paneContainer.onDidAddPaneItem(callback) } // Extended: Invoke the given callback when a pane item is about to be // destroyed, before the user is prompted to save it. @@ -418,7 +417,7 @@ Workspace = class Workspace extends Model { // its pane. // // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. - onWillDestroyPaneItem(callback) { return this.paneContainer.onWillDestroyPaneItem(callback); } + onWillDestroyPaneItem (callback) { return this.paneContainer.onWillDestroyPaneItem(callback) } // Extended: Invoke the given callback when a pane item is destroyed. // @@ -430,7 +429,7 @@ Workspace = class Workspace extends Model { // pane. // // Returns a {Disposable} on which `.dispose` can be called to unsubscribe. - onDidDestroyPaneItem(callback) { return this.paneContainer.onDidDestroyPaneItem(callback); } + onDidDestroyPaneItem (callback) { return this.paneContainer.onDidDestroyPaneItem(callback) } // Extended: Invoke the given callback when a text editor is added to the // workspace. @@ -443,8 +442,8 @@ Workspace = class Workspace extends Model { // pane. // // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidAddTextEditor(callback) { - return this.emitter.on('did-add-text-editor', callback); + onDidAddTextEditor (callback) { + return this.emitter.on('did-add-text-editor', callback) } /* @@ -480,42 +479,46 @@ Workspace = class Workspace extends Model { // an existing item for the same URI. Defaults to `false`. // // Returns a {Promise} that resolves to the {TextEditor} for the file URI. - open(uri, options={}) { - let pane; - const { searchAllPanes } = options; - const { split } = options; - uri = this.project.resolvePath(uri); + open (uri, options = {}) { + let pane + const { searchAllPanes } = options + const { split } = options + uri = this.project.resolvePath(uri) if (!atom.config.get('core.allowPendingPaneItems')) { - options.pending = false; + options.pending = false } // Avoid adding URLs as recent documents to work-around this Spotlight crash: // https://github.com/atom/atom/issues/10071 if ((uri != null) && ((url.parse(uri).protocol == null) || (process.platform === 'win32'))) { - this.applicationDelegate.addRecentDocument(uri); + this.applicationDelegate.addRecentDocument(uri) } - if (searchAllPanes) { pane = this.paneContainer.paneForURI(uri); } - if (pane == null) { pane = (() => { switch (split) { - case 'left': - return this.getActivePane().findLeftmostSibling(); - case 'right': - return this.getActivePane().findOrCreateRightmostSibling(); - case 'up': - return this.getActivePane().findTopmostSibling(); - case 'down': - return this.getActivePane().findOrCreateBottommostSibling(); - default: - return this.getActivePane(); - } })(); } + if (searchAllPanes) { pane = this.paneContainer.paneForURI(uri) } + if (pane == null) { + pane = (() => { + switch (split) { + case 'left': + return this.getActivePane().findLeftmostSibling() + case 'right': + return this.getActivePane().findOrCreateRightmostSibling() + case 'up': + return this.getActivePane().findTopmostSibling() + case 'down': + return this.getActivePane().findOrCreateBottommostSibling() + default: + return this.getActivePane() + } + })() + } - return this.openURIInPane(uri, pane, options); + return this.openURIInPane(uri, pane, options) } // Open Atom's license in the active pane. - openLicense() { - return this.open(path.join(process.resourcesPath, 'LICENSE.md')); + openLicense () { + return this.open(path.join(process.resourcesPath, 'LICENSE.md')) } // Synchronously open the given URI in the active pane. **Only use this method @@ -532,154 +535,151 @@ Workspace = class Workspace extends Model { // the containing pane. Defaults to `true`. // * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} // on containing pane. Defaults to `true`. - openSync(uri='', options={}) { - const {initialLine, initialColumn} = options; - const activatePane = options.activatePane != null ? options.activatePane : true; - const activateItem = options.activateItem != null ? options.activateItem : true; + openSync (uri = '', options = {}) { + const {initialLine, initialColumn} = options + const activatePane = options.activatePane != null ? options.activatePane : true + const activateItem = options.activateItem != null ? options.activateItem : true - uri = this.project.resolvePath(uri); - let item = this.getActivePane().itemForURI(uri); + uri = this.project.resolvePath(uri) + let item = this.getActivePane().itemForURI(uri) if (uri) { - for (let opener of this.getOpeners()) { if (!item) { if (item == null) { item = opener(uri, options); } } } + for (let opener of this.getOpeners()) { if (!item) { if (item == null) { item = opener(uri, options) } } } } - if (item == null) { item = this.project.openSync(uri, {initialLine, initialColumn}); } + if (item == null) { item = this.project.openSync(uri, {initialLine, initialColumn}) } - if (activateItem) { this.getActivePane().activateItem(item); } - this.itemOpened(item); - if (activatePane) { this.getActivePane().activate(); } - return item; + if (activateItem) { this.getActivePane().activateItem(item) } + this.itemOpened(item) + if (activatePane) { this.getActivePane().activate() } + return item } - openURIInPane(uri, pane, options={}) { - let item; - const activatePane = options.activatePane != null ? options.activatePane : true; - const activateItem = options.activateItem != null ? options.activateItem : true; + openURIInPane (uri, pane, options = {}) { + let item + const activatePane = options.activatePane != null ? options.activatePane : true + const activateItem = options.activateItem != null ? options.activateItem : true if (uri != null) { - if (item = pane.itemForURI(uri)) { - if (!options.pending && (pane.getPendingItem() === item)) { pane.clearPendingItem(); } + if ((item = pane.itemForURI(uri))) { + if (!options.pending && (pane.getPendingItem() === item)) { pane.clearPendingItem() } } - for (let opener of this.getOpeners()) { if (!item) { if (item == null) { item = opener(uri, options); } } } + for (let opener of this.getOpeners()) { if (!item) { if (item == null) { item = opener(uri, options) } } } } try { - if (item == null) { item = this.openTextFile(uri, options); } + if (item == null) { item = this.openTextFile(uri, options) } } catch (error) { switch (error.code) { case 'CANCELLED': - return Promise.resolve(); - break; + return Promise.resolve() case 'EACCES': - this.notificationManager.addWarning(`Permission denied '${error.path}'`); - return Promise.resolve(); - break; + this.notificationManager.addWarning(`Permission denied '${error.path}'`) + return Promise.resolve() case 'EPERM': case 'EBUSY': case 'ENXIO': case 'EIO': case 'ENOTCONN': case 'UNKNOWN': case 'ECONNRESET': case 'EINVAL': case 'EMFILE': case 'ENOTDIR': case 'EAGAIN': - this.notificationManager.addWarning(`Unable to open '${error.path != null ? error.path : uri}'`, {detail: error.message}); - return Promise.resolve(); - break; + this.notificationManager.addWarning(`Unable to open '${error.path != null ? error.path : uri}'`, {detail: error.message}) + return Promise.resolve() default: - throw error; + throw error } } return Promise.resolve(item) .then(item => { - let initialColumn; - if (pane.isDestroyed()) { return item; } + let initialColumn + if (pane.isDestroyed()) { return item } - this.itemOpened(item); - if (activateItem) { pane.activateItem(item, {pending: options.pending}); } - if (activatePane) { pane.activate(); } + this.itemOpened(item) + if (activateItem) { pane.activateItem(item, {pending: options.pending}) } + if (activatePane) { pane.activate() } - let initialLine = initialColumn = 0; + let initialLine = initialColumn = 0 if (!Number.isNaN(options.initialLine)) { - ({ initialLine } = options); + ({ initialLine } = options) } if (!Number.isNaN(options.initialColumn)) { - ({ initialColumn } = options); + ({ initialColumn } = options) } if ((initialLine >= 0) || (initialColumn >= 0)) { if (typeof item.setCursorBufferPosition === 'function') { - item.setCursorBufferPosition([initialLine, initialColumn]); + item.setCursorBufferPosition([initialLine, initialColumn]) } } - const index = pane.getActiveItemIndex(); - this.emitter.emit('did-open', {uri, pane, item, index}); - return item; + const index = pane.getActiveItemIndex() + this.emitter.emit('did-open', {uri, pane, item, index}) + return item } - ); + ) } - openTextFile(uri, options) { - const filePath = this.project.resolvePath(uri); + openTextFile (uri, options) { + const filePath = this.project.resolvePath(uri) if (filePath != null) { try { - fs.closeSync(fs.openSync(filePath, 'r')); + fs.closeSync(fs.openSync(filePath, 'r')) } catch (error) { // allow ENOENT errors to create an editor for paths that dont exist - if (error.code !== 'ENOENT') { throw error; } + if (error.code !== 'ENOENT') { throw error } } } - const fileSize = fs.getSizeSync(filePath); + const fileSize = fs.getSizeSync(filePath) - const largeFileMode = fileSize >= (2 * 1048576); // 2MB + const largeFileMode = fileSize >= (2 * 1048576) // 2MB if (fileSize >= (this.config.get('core.warnOnLargeFileLimit') * 1048576)) { // 20MB by default const choice = this.applicationDelegate.confirm({ message: 'Atom will be unresponsive during the loading of very large files.', - detailedMessage: "Do you still want to load this file?", - buttons: ["Proceed", "Cancel"]}); + detailedMessage: 'Do you still want to load this file?', + buttons: ['Proceed', 'Cancel']}) if (choice === 1) { - const error = new Error; - error.code = 'CANCELLED'; - throw error; + const error = new Error() + error.code = 'CANCELLED' + throw error } } return this.project.bufferForPath(filePath, options).then(buffer => { - return this.textEditorRegistry.build(Object.assign({buffer, largeFileMode, autoHeight: false}, options)); + return this.textEditorRegistry.build(Object.assign({buffer, largeFileMode, autoHeight: false}, options)) } - ); + ) } - handleGrammarUsed(grammar) { - if (grammar == null) { return; } + handleGrammarUsed (grammar) { + if (grammar == null) { return } - return this.packageManager.triggerActivationHook(`${grammar.packageName}:grammar-used`); + return this.packageManager.triggerActivationHook(`${grammar.packageName}:grammar-used`) } // Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`. // // * `object` An {Object} you want to perform the check against. - isTextEditor(object) { - return object instanceof TextEditor; + isTextEditor (object) { + return object instanceof TextEditor } // Extended: Create a new text editor. // // Returns a {TextEditor}. - buildTextEditor(params) { - const editor = this.textEditorRegistry.build(params); + buildTextEditor (params) { + const editor = this.textEditorRegistry.build(params) const subscriptions = new CompositeDisposable( this.textEditorRegistry.maintainGrammar(editor), this.textEditorRegistry.maintainConfig(editor) - ); - editor.onDidDestroy(() => subscriptions.dispose()); - return editor; + ) + editor.onDidDestroy(() => subscriptions.dispose()) + return editor } // Public: Asynchronously reopens the last-closed item's URI if it hasn't already been // reopened. // // Returns a {Promise} that is resolved when the item is opened - reopenItem() { - let uri; - if (uri = this.destroyedItemURIs.pop()) { - return this.open(uri); + reopenItem () { + let uri + if ((uri = this.destroyedItemURIs.pop())) { + return this.open(uri) } else { - return Promise.resolve(); + return Promise.resolve() } } @@ -712,13 +712,13 @@ Workspace = class Workspace extends Model { // that is already open in a text editor view. You could signal this by calling // {Workspace::open} on the URI `quux-preview://foo/bar/baz.quux`. Then your opener // can check the protocol for quux-preview and only handle those URIs that match. - addOpener(opener) { - this.openers.push(opener); - return new Disposable((function() { return _.remove(this.openers, opener); }.bind(this))); + addOpener (opener) { + this.openers.push(opener) + return new Disposable((function () { return _.remove(this.openers, opener) }.bind(this))) } - getOpeners() { - return this.openers; + getOpeners () { + return this.openers } /* @@ -728,40 +728,40 @@ Workspace = class Workspace extends Model { // Essential: Get all pane items in the workspace. // // Returns an {Array} of items. - getPaneItems() { - return this.paneContainer.getPaneItems(); + getPaneItems () { + return this.paneContainer.getPaneItems() } // Essential: Get the active {Pane}'s active item. // // Returns an pane item {Object}. - getActivePaneItem() { - return this.paneContainer.getActivePaneItem(); + getActivePaneItem () { + return this.paneContainer.getActivePaneItem() } // Essential: Get all text editors in the workspace. // // Returns an {Array} of {TextEditor}s. - getTextEditors() { - return this.getPaneItems().filter(item => item instanceof TextEditor); + getTextEditors () { + return this.getPaneItems().filter(item => item instanceof TextEditor) } // Essential: Get the active item if it is an {TextEditor}. // // Returns an {TextEditor} or `undefined` if the current active item is not an // {TextEditor}. - getActiveTextEditor() { - const activeItem = this.getActivePaneItem(); - if (activeItem instanceof TextEditor) { return activeItem; } + getActiveTextEditor () { + const activeItem = this.getActivePaneItem() + if (activeItem instanceof TextEditor) { return activeItem } } // Save all pane items. - saveAll() { - return this.paneContainer.saveAll(); + saveAll () { + return this.paneContainer.saveAll() } - confirmClose(options) { - return this.paneContainer.confirmClose(options); + confirmClose (options) { + return this.paneContainer.confirmClose(options) } // Save the active pane item. @@ -770,8 +770,8 @@ Workspace = class Workspace extends Model { // `.getURI` method, calls `.save` on the item. Otherwise // {::saveActivePaneItemAs} # will be called instead. This method does nothing // if the active item does not implement a `.save` method. - saveActivePaneItem() { - return this.getActivePane().saveActiveItem(); + saveActivePaneItem () { + return this.getActivePane().saveActiveItem() } // Prompt the user for a path and save the active pane item to it. @@ -779,16 +779,16 @@ Workspace = class Workspace extends Model { // Opens a native dialog where the user selects a path on disk, then calls // `.saveAs` on the item with the selected path. This method does nothing if // the active item does not implement a `.saveAs` method. - saveActivePaneItemAs() { - return this.getActivePane().saveActiveItemAs(); + saveActivePaneItemAs () { + return this.getActivePane().saveActiveItemAs() } // Destroy (close) the active pane item. // // Removes the active pane item and calls the `.destroy` method on it if one is // defined. - destroyActivePaneItem() { - return this.getActivePane().destroyActiveItem(); + destroyActivePaneItem () { + return this.getActivePane().destroyActiveItem() } /* @@ -798,25 +798,25 @@ Workspace = class Workspace extends Model { // Extended: Get all panes in the workspace. // // Returns an {Array} of {Pane}s. - getPanes() { - return this.paneContainer.getPanes(); + getPanes () { + return this.paneContainer.getPanes() } // Extended: Get the active {Pane}. // // Returns a {Pane}. - getActivePane() { - return this.paneContainer.getActivePane(); + getActivePane () { + return this.paneContainer.getActivePane() } // Extended: Make the next pane active. - activateNextPane() { - return this.paneContainer.activateNextPane(); + activateNextPane () { + return this.paneContainer.activateNextPane() } // Extended: Make the previous pane active. - activatePreviousPane() { - return this.paneContainer.activatePreviousPane(); + activatePreviousPane () { + return this.paneContainer.activatePreviousPane() } // Extended: Get the first {Pane} with an item for the given URI. @@ -824,8 +824,8 @@ Workspace = class Workspace extends Model { // * `uri` {String} uri // // Returns a {Pane} or `undefined` if no pane exists for the given URI. - paneForURI(uri) { - return this.paneContainer.paneForURI(uri); + paneForURI (uri) { + return this.paneContainer.paneForURI(uri) } // Extended: Get the {Pane} containing the given item. @@ -833,87 +833,86 @@ Workspace = class Workspace extends Model { // * `item` Item the returned pane contains. // // Returns a {Pane} or `undefined` if no pane exists for the given item. - paneForItem(item) { - return this.paneContainer.paneForItem(item); + paneForItem (item) { + return this.paneContainer.paneForItem(item) } // Destroy (close) the active pane. - destroyActivePane() { - return __guard__(this.getActivePane(), x => x.destroy()); + destroyActivePane () { + return __guard__(this.getActivePane(), x => x.destroy()) } // Close the active pane item, or the active pane if it is empty, // or the current window if there is only the empty root pane. - closeActivePaneItemOrEmptyPaneOrWindow() { + closeActivePaneItemOrEmptyPaneOrWindow () { if (this.getActivePaneItem() != null) { - return this.destroyActivePaneItem(); + return this.destroyActivePaneItem() } else if (this.getPanes().length > 1) { - return this.destroyActivePane(); + return this.destroyActivePane() } else if (this.config.get('core.closeEmptyWindows')) { - return atom.close(); + return atom.close() } } // Increase the editor font size by 1px. - increaseFontSize() { - return this.config.set("editor.fontSize", this.config.get("editor.fontSize") + 1); + increaseFontSize () { + return this.config.set('editor.fontSize', this.config.get('editor.fontSize') + 1) } // Decrease the editor font size by 1px. - decreaseFontSize() { - const fontSize = this.config.get("editor.fontSize"); - if (fontSize > 1) { return this.config.set("editor.fontSize", fontSize - 1); } + decreaseFontSize () { + const fontSize = this.config.get('editor.fontSize') + if (fontSize > 1) { return this.config.set('editor.fontSize', fontSize - 1) } } // Restore to the window's original editor font size. - resetFontSize() { + resetFontSize () { if (this.originalFontSize) { - return this.config.set("editor.fontSize", this.originalFontSize); + return this.config.set('editor.fontSize', this.originalFontSize) } } - subscribeToFontSize() { + subscribeToFontSize () { return this.config.onDidChange('editor.fontSize', ({oldValue}) => { - return this.originalFontSize != null ? this.originalFontSize : (this.originalFontSize = oldValue); + return this.originalFontSize != null ? this.originalFontSize : (this.originalFontSize = oldValue) } - ); + ) } // Removes the item's uri from the list of potential items to reopen. - itemOpened(item) { - let uri; + itemOpened (item) { + let uri if (typeof item.getURI === 'function') { - uri = item.getURI(); + uri = item.getURI() } else if (typeof item.getUri === 'function') { - uri = item.getUri(); + uri = item.getUri() } if (uri != null) { - return _.remove(this.destroyedItemURIs, uri); + return _.remove(this.destroyedItemURIs, uri) } } // Adds the destroyed item's uri to the list of items to reopen. - didDestroyPaneItem({item}) { - let uri; + didDestroyPaneItem ({item}) { + let uri if (typeof item.getURI === 'function') { - uri = item.getURI(); + uri = item.getURI() } else if (typeof item.getUri === 'function') { - uri = item.getUri(); + uri = item.getUri() } if (uri != null) { - return this.destroyedItemURIs.push(uri); + return this.destroyedItemURIs.push(uri) } } // Called by Model superclass when destroyed - destroyed() { - this.paneContainer.destroy(); - return (this.activeItemSubscriptions != null ? this.activeItemSubscriptions.dispose() : undefined); + destroyed () { + this.paneContainer.destroy() + return (this.activeItemSubscriptions != null ? this.activeItemSubscriptions.dispose() : undefined) } - /* Section: Panels @@ -929,8 +928,8 @@ Workspace = class Workspace extends Model { */ // Essential: Get an {Array} of all the panel items at the bottom of the editor window. - getBottomPanels() { - return this.getPanels('bottom'); + getBottomPanels () { + return this.getPanels('bottom') } // Essential: Adds a panel item to the bottom of the editor window. @@ -945,13 +944,13 @@ Workspace = class Workspace extends Model { // forced closer to the edges of the window. (default: 100) // // Returns a {Panel} - addBottomPanel(options) { - return this.addPanel('bottom', options); + addBottomPanel (options) { + return this.addPanel('bottom', options) } // Essential: Get an {Array} of all the panel items to the left of the editor window. - getLeftPanels() { - return this.getPanels('left'); + getLeftPanels () { + return this.getPanels('left') } // Essential: Adds a panel item to the left of the editor window. @@ -966,13 +965,13 @@ Workspace = class Workspace extends Model { // forced closer to the edges of the window. (default: 100) // // Returns a {Panel} - addLeftPanel(options) { - return this.addPanel('left', options); + addLeftPanel (options) { + return this.addPanel('left', options) } // Essential: Get an {Array} of all the panel items to the right of the editor window. - getRightPanels() { - return this.getPanels('right'); + getRightPanels () { + return this.getPanels('right') } // Essential: Adds a panel item to the right of the editor window. @@ -987,13 +986,13 @@ Workspace = class Workspace extends Model { // forced closer to the edges of the window. (default: 100) // // Returns a {Panel} - addRightPanel(options) { - return this.addPanel('right', options); + addRightPanel (options) { + return this.addPanel('right', options) } // Essential: Get an {Array} of all the panel items at the top of the editor window. - getTopPanels() { - return this.getPanels('top'); + getTopPanels () { + return this.getPanels('top') } // Essential: Adds a panel item to the top of the editor window above the tabs. @@ -1008,13 +1007,13 @@ Workspace = class Workspace extends Model { // forced closer to the edges of the window. (default: 100) // // Returns a {Panel} - addTopPanel(options) { - return this.addPanel('top', options); + addTopPanel (options) { + return this.addPanel('top', options) } // Essential: Get an {Array} of all the panel items in the header. - getHeaderPanels() { - return this.getPanels('header'); + getHeaderPanels () { + return this.getPanels('header') } // Essential: Adds a panel item to the header. @@ -1029,13 +1028,13 @@ Workspace = class Workspace extends Model { // forced closer to the edges of the window. (default: 100) // // Returns a {Panel} - addHeaderPanel(options) { - return this.addPanel('header', options); + addHeaderPanel (options) { + return this.addPanel('header', options) } // Essential: Get an {Array} of all the panel items in the footer. - getFooterPanels() { - return this.getPanels('footer'); + getFooterPanels () { + return this.getPanels('footer') } // Essential: Adds a panel item to the footer. @@ -1050,13 +1049,13 @@ Workspace = class Workspace extends Model { // forced closer to the edges of the window. (default: 100) // // Returns a {Panel} - addFooterPanel(options) { - return this.addPanel('footer', options); + addFooterPanel (options) { + return this.addPanel('footer', options) } // Essential: Get an {Array} of all the modal panel items - getModalPanels() { - return this.getPanels('modal'); + getModalPanels () { + return this.getPanels('modal') } // Essential: Adds a panel item as a modal dialog. @@ -1071,30 +1070,30 @@ Workspace = class Workspace extends Model { // forced closer to the edges of the window. (default: 100) // // Returns a {Panel} - addModalPanel(options={}) { - return this.addPanel('modal', options); + addModalPanel (options = {}) { + return this.addPanel('modal', options) } // Essential: Returns the {Panel} associated with the given item. Returns // `null` when the item has no panel. // // * `item` Item the panel contains - panelForItem(item) { + panelForItem (item) { for (let location in this.panelContainers) { - const container = this.panelContainers[location]; - const panel = container.panelForItem(item); - if (panel != null) { return panel; } + const container = this.panelContainers[location] + const panel = container.panelForItem(item) + if (panel != null) { return panel } } - return null; + return null } - getPanels(location) { - return this.panelContainers[location].getPanels(); + getPanels (location) { + return this.panelContainers[location].getPanels() } - addPanel(location, options) { - if (options == null) { options = {}; } - return this.panelContainers[location].addPanel(new Panel(options)); + addPanel (location, options) { + if (options == null) { options = {} } + return this.panelContainers[location].addPanel(new Panel(options)) } /* @@ -1112,54 +1111,54 @@ Workspace = class Workspace extends Model { // // Returns a {Promise} with a `cancel()` method that will cancel all // of the underlying searches that were started as part of this scan. - scan(regex, options={}, iterator) { - let directorySearcher, onPathsSearched; + scan (regex, options = {}, iterator) { + let directorySearcher, onPathsSearched if (_.isFunction(options)) { - iterator = options; - options = {}; + iterator = options + options = {} } // Find a searcher for every Directory in the project. Each searcher that is matched // will be associated with an Array of Directory objects in the Map. - const directoriesForSearcher = new Map(); + const directoriesForSearcher = new Map() for (let directory of this.project.getDirectories()) { - let searcher = this.defaultDirectorySearcher; + let searcher = this.defaultDirectorySearcher for (directorySearcher of this.directorySearchers) { if (directorySearcher.canSearchDirectory(directory)) { - searcher = directorySearcher; - break; + searcher = directorySearcher + break } } - let directories = directoriesForSearcher.get(searcher); + let directories = directoriesForSearcher.get(searcher) if (!directories) { - directories = []; - directoriesForSearcher.set(searcher, directories); + directories = [] + directoriesForSearcher.set(searcher, directories) } - directories.push(directory); + directories.push(directory) } // Define the onPathsSearched callback. if (_.isFunction(options.onPathsSearched)) { // Maintain a map of directories to the number of search results. When notified of a new count, // replace the entry in the map and update the total. - const onPathsSearchedOption = options.onPathsSearched; - let totalNumberOfPathsSearched = 0; - const numberOfPathsSearchedForSearcher = new Map(); - onPathsSearched = function(searcher, numberOfPathsSearched) { - const oldValue = numberOfPathsSearchedForSearcher.get(searcher); + const onPathsSearchedOption = options.onPathsSearched + let totalNumberOfPathsSearched = 0 + const numberOfPathsSearchedForSearcher = new Map() + onPathsSearched = function (searcher, numberOfPathsSearched) { + const oldValue = numberOfPathsSearchedForSearcher.get(searcher) if (oldValue) { - totalNumberOfPathsSearched -= oldValue; + totalNumberOfPathsSearched -= oldValue } - numberOfPathsSearchedForSearcher.set(searcher, numberOfPathsSearched); - totalNumberOfPathsSearched += numberOfPathsSearched; - return onPathsSearchedOption(totalNumberOfPathsSearched); - }; + numberOfPathsSearchedForSearcher.set(searcher, numberOfPathsSearched) + totalNumberOfPathsSearched += numberOfPathsSearched + return onPathsSearchedOption(totalNumberOfPathsSearched) + } } else { - onPathsSearched = function() {}; + onPathsSearched = function () {} } // Kick off all of the searches and unify them into one Promise. - const allSearches = []; + const allSearches = [] directoriesForSearcher.forEach((directories, searcher) => { const searchOptions = { inclusions: options.paths || [], @@ -1168,26 +1167,26 @@ Workspace = class Workspace extends Model { exclusions: this.config.get('core.ignoredNames'), follow: this.config.get('core.followSymlinks'), didMatch: result => { - if (!this.project.isPathModified(result.filePath)) { return iterator(result); } + if (!this.project.isPathModified(result.filePath)) { return iterator(result) } }, - didError(error) { - return iterator(null, error); + didError (error) { + return iterator(null, error) }, - didSearchPaths(count) { return onPathsSearched(searcher, count); } - }; - directorySearcher = searcher.search(directories, regex, searchOptions); - return allSearches.push(directorySearcher); + didSearchPaths (count) { return onPathsSearched(searcher, count) } + } + directorySearcher = searcher.search(directories, regex, searchOptions) + return allSearches.push(directorySearcher) } - ); - const searchPromise = Promise.all(allSearches); + ) + const searchPromise = Promise.all(allSearches) for (let buffer of this.project.getBuffers()) { if (buffer.isModified()) { - const filePath = buffer.getPath(); - if (!this.project.contains(filePath)) { continue; } - var matches = []; - buffer.scan(regex, match => matches.push(match)); - if (matches.length > 0) { iterator({filePath, matches}); } + const filePath = buffer.getPath() + if (!this.project.contains(filePath)) { continue } + var matches = [] + buffer.scan(regex, match => matches.push(match)) + if (matches.length > 0) { iterator({filePath, matches}) } } } @@ -1195,36 +1194,36 @@ Workspace = class Workspace extends Model { // with the existing behavior, instead of cancel() rejecting the promise, it should // resolve it with the special value 'cancelled'. At least the built-in find-and-replace // package relies on this behavior. - let isCancelled = false; - const cancellablePromise = new Promise(function(resolve, reject) { - const onSuccess = function() { + let isCancelled = false + const cancellablePromise = new Promise(function (resolve, reject) { + const onSuccess = function () { if (isCancelled) { - return resolve('cancelled'); + return resolve('cancelled') } else { - return resolve(null); + return resolve(null) } - }; + } - const onFailure = function() { - for (let promise of allSearches) { promise.cancel(); } - return reject(); - }; + const onFailure = function () { + for (let promise of allSearches) { promise.cancel() } + return reject() + } - return searchPromise.then(onSuccess, onFailure); - }); - cancellablePromise.cancel = function() { - isCancelled = true; + return searchPromise.then(onSuccess, onFailure) + }) + cancellablePromise.cancel = function () { + isCancelled = true // Note that cancelling all of the members of allSearches will cause all of the searches // to resolve, which causes searchPromise to resolve, which is ultimately what causes // cancellablePromise to resolve. - return allSearches.map((promise) => promise.cancel()); - }; + return allSearches.map((promise) => promise.cancel()) + } // Although this method claims to return a `Promise`, the `ResultsPaneView.onSearch()` // method in the find-and-replace package expects the object returned by this method to have a // `done()` method. Include a done() method until find-and-replace can be updated. - cancellablePromise.done = onSuccessOrFailure => cancellablePromise.then(onSuccessOrFailure, onSuccessOrFailure); - return cancellablePromise; + cancellablePromise.done = onSuccessOrFailure => cancellablePromise.then(onSuccessOrFailure, onSuccessOrFailure) + return cancellablePromise } // Public: Performs a replace across all the specified files in the project. @@ -1236,79 +1235,80 @@ Workspace = class Workspace extends Model { // * `options` {Object} with keys `filePath` and `replacements`. // // Returns a {Promise}. - replace(regex, replacementText, filePaths, iterator) { - return new Promise((function(resolve, reject) { - let buffer; + replace (regex, replacementText, filePaths, iterator) { + return new Promise((function (resolve, reject) { + let buffer const openPaths = ((() => { - const result = []; - for (buffer of this.project.getBuffers()) { result.push(buffer.getPath()); + const result = [] + for (buffer of this.project.getBuffers()) { + result.push(buffer.getPath()) } - return result; - })()); - const outOfProcessPaths = _.difference(filePaths, openPaths); + return result + })()) + const outOfProcessPaths = _.difference(filePaths, openPaths) - let inProcessFinished = !openPaths.length; - let outOfProcessFinished = !outOfProcessPaths.length; - const checkFinished = function() { - if (outOfProcessFinished && inProcessFinished) { return resolve(); } - }; + let inProcessFinished = !openPaths.length + let outOfProcessFinished = !outOfProcessPaths.length + const checkFinished = function () { + if (outOfProcessFinished && inProcessFinished) { return resolve() } + } if (!outOfProcessFinished.length) { - let flags = 'g'; - if (regex.ignoreCase) { flags += 'i'; } + let flags = 'g' + if (regex.ignoreCase) { flags += 'i' } - const task = Task.once(require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, function() { - outOfProcessFinished = true; - return checkFinished(); - }); + const task = Task.once(require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, function () { + outOfProcessFinished = true + return checkFinished() + }) - task.on('replace:path-replaced', iterator); - task.on('replace:file-error', function(error) { return iterator(null, error); }); + task.on('replace:path-replaced', iterator) + task.on('replace:file-error', function (error) { return iterator(null, error) }) } for (buffer of this.project.getBuffers()) { - if (!Array.from(filePaths).includes(buffer.getPath())) { continue; } - const replacements = buffer.replace(regex, replacementText, iterator); - if (replacements) { iterator({filePath: buffer.getPath(), replacements}); } + if (!Array.from(filePaths).includes(buffer.getPath())) { continue } + const replacements = buffer.replace(regex, replacementText, iterator) + if (replacements) { iterator({filePath: buffer.getPath(), replacements}) } } - inProcessFinished = true; - return checkFinished(); - }.bind(this))); + inProcessFinished = true + return checkFinished() + }.bind(this))) } - checkoutHeadRevision(editor) { + checkoutHeadRevision (editor) { if (editor.getPath()) { const checkoutHead = () => { return this.project.repositoryForDirectory(new Directory(editor.getDirectoryPath())) - .then(repository => repository != null ? repository.checkoutHeadForEditor(editor) : undefined); - }; + .then(repository => repository != null ? repository.checkoutHeadForEditor(editor) : undefined) + } if (this.config.get('editor.confirmCheckoutHeadRevision')) { return this.applicationDelegate.confirm({ message: 'Confirm Checkout HEAD Revision', - detailedMessage: `Are you sure you want to discard all changes to \"${editor.getFileName()}\" since the last Git commit?`, + detailedMessage: `Are you sure you want to discard all changes to "${editor.getFileName()}" since the last Git commit?`, buttons: { OK: checkoutHead, Cancel: null } - }); + }) } else { - return checkoutHead(); + return checkoutHead() } } else { - return Promise.resolve(false); + return Promise.resolve(false) } } -}; - -function __guard__(value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; } -function __guardMethod__(obj, methodName, transform) { + +function __guard__ (value, transform) { + return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined +} +function __guardMethod__ (obj, methodName, transform) { if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { - return transform(obj, methodName); + return transform(obj, methodName) } else { - return undefined; + return undefined } } From 53ec839ae51f7491f3ee4539939fefa3ac9bc8fe Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Thu, 2 Mar 2017 15:40:57 -0800 Subject: [PATCH 15/55] Convert workspace to JavaScript: cleanup --- spec/workspace-spec.js | 1153 +++++++++++++++++++++------------------- src/workspace.js | 392 ++++++++------ 2 files changed, 841 insertions(+), 704 deletions(-) diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index ba7da0fce..270f81526 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -1,3 +1,5 @@ +'use strict' + /* global advanceClock, HTMLElement, waits */ const path = require('path') @@ -11,28 +13,33 @@ const fstream = require('fstream') const fs = require('fs-plus') const AtomEnvironment = require('../src/atom-environment') -describe('Workspace', function () { - let escapeStringRegex - let [workspace, setDocumentEdited] = Array.from([]) +describe('Workspace', () => { + let workspace + let setDocumentEdited - beforeEach(function () { - ({ workspace } = atom) + beforeEach(() => { + workspace = atom.workspace workspace.resetFontSize() spyOn(atom.applicationDelegate, 'confirm') setDocumentEdited = spyOn(atom.applicationDelegate, 'setWindowDocumentEdited') - atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('dir'))]) - return waits(1) + atom.project.setPaths([atom.project.getDirectories()[0].resolve('dir')]) + waits(1) }) afterEach(() => temp.cleanupSync()) - describe('serialization', function () { - const simulateReload = function () { + describe('serialization', () => { + const simulateReload = () => { const workspaceState = atom.workspace.serialize() const projectState = atom.project.serialize({isUnloading: true}) atom.workspace.destroy() atom.project.destroy() - atom.project = new Project({notificationManager: atom.notifications, packageManager: atom.packages, confirm: atom.confirm.bind(atom), applicationDelegate: atom.applicationDelegate}) + atom.project = new Project({ + notificationManager: atom.notifications, + packageManager: atom.packages, + confirm: atom.confirm.bind(atom), + applicationDelegate: atom.applicationDelegate + }) atom.project.deserialize(projectState) atom.workspace = new Workspace({ config: atom.config, @@ -49,8 +56,8 @@ describe('Workspace', function () { return atom.workspace.deserialize(workspaceState, atom.deserializers) } - describe('when the workspace contains text editors', () => - it('constructs the view with the same panes', function () { + describe('when the workspace contains text editors', () => { + it('constructs the view with the same panes', () => { const pane1 = atom.workspace.getActivePane() const pane2 = pane1.splitRight({copyActiveItem: true}) const pane3 = pane2.splitRight({copyActiveItem: true}) @@ -66,123 +73,127 @@ describe('Workspace', function () { atom.workspace.open('../sample.js').then(editor => pane3.activateItem(editor)) ) - runs(function () { + runs(() => { pane3.activeItem.setCursorScreenPosition([2, 4]) - return (pane4 = pane2.splitDown()) + pane4 = pane2.splitDown() }) waitsForPromise(() => atom.workspace.open('../sample.txt').then(editor => pane4.activateItem(editor)) ) - return runs(function () { + runs(() => { pane4.getActiveItem().setCursorScreenPosition([0, 2]) pane2.activate() simulateReload() expect(atom.workspace.getTextEditors().length).toBe(5) - const [editor1, editor2, untitledEditor, editor3, editor4] = Array.from(atom.workspace.getTextEditors()) - expect(editor1.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('b'))) - expect(editor2.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x1 => x1.resolve('../sample.txt'))) + const [editor1, editor2, untitledEditor, editor3, editor4] = atom.workspace.getTextEditors() + const firstDirectory = atom.project.getDirectories()[0] + expect(firstDirectory).toBeDefined() + expect(editor1.getPath()).toBe(firstDirectory.resolve('b')) + expect(editor2.getPath()).toBe(firstDirectory.resolve('../sample.txt')) expect(editor2.getCursorScreenPosition()).toEqual([0, 2]) - expect(editor3.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x2 => x2.resolve('b'))) - expect(editor4.getPath()).toBe(__guard__(atom.project.getDirectories()[0], x3 => x3.resolve('../sample.js'))) + expect(editor3.getPath()).toBe(firstDirectory.resolve('b')) + expect(editor4.getPath()).toBe(firstDirectory.resolve('../sample.js')) expect(editor4.getCursorScreenPosition()).toEqual([2, 4]) expect(untitledEditor.getPath()).toBeUndefined() expect(untitledEditor.getText()).toBe('An untitled editor.') expect(atom.workspace.getActiveTextEditor().getPath()).toBe(editor3.getPath()) const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - return expect(document.title).toMatch(new RegExp(`^${path.basename(editor3.getLongTitle())} \\u2014 ${pathEscaped}`)) + expect(document.title).toMatch(new RegExp(`^${path.basename(editor3.getLongTitle())} \\u2014 ${pathEscaped}`)) }) }) - ) + }) - return describe('where there are no open panes or editors', () => - it('constructs the view with no open editors', function () { + describe('where there are no open panes or editors', () => { + it('constructs the view with no open editors', () => { atom.workspace.getActivePane().destroy() expect(atom.workspace.getTextEditors().length).toBe(0) simulateReload() - return expect(atom.workspace.getTextEditors().length).toBe(0) + expect(atom.workspace.getTextEditors().length).toBe(0) }) - ) + }) }) - describe('::open(uri, options)', function () { + describe('::open(uri, options)', () => { let openEvents = null - beforeEach(function () { + beforeEach(() => { openEvents = [] workspace.onDidOpen(event => openEvents.push(event)) - return spyOn(workspace.getActivePane(), 'activate').andCallThrough() + spyOn(workspace.getActivePane(), 'activate').andCallThrough() }) - describe("when the 'searchAllPanes' option is false (default)", function () { - describe('when called without a uri', () => - it('adds and activates an empty editor on the active pane', function () { - let [editor1, editor2] = Array.from([]) + describe("when the 'searchAllPanes' option is false (default)", () => { + describe('when called without a uri', () => { + it('adds and activates an empty editor on the active pane', () => { + let editor1 + let editor2 - waitsForPromise(() => workspace.open().then(editor => (editor1 = editor))) + waitsForPromise(() => workspace.open().then(editor => { editor1 = editor })) - runs(function () { + runs(() => { expect(editor1.getPath()).toBeUndefined() expect(workspace.getActivePane().items).toEqual([editor1]) expect(workspace.getActivePaneItem()).toBe(editor1) expect(workspace.getActivePane().activate).toHaveBeenCalled() expect(openEvents).toEqual([{uri: undefined, pane: workspace.getActivePane(), item: editor1, index: 0}]) - return (openEvents = []) + openEvents = [] }) - waitsForPromise(() => workspace.open().then(editor => (editor2 = editor))) + waitsForPromise(() => workspace.open().then(editor => { editor2 = editor })) - return runs(function () { + runs(() => { expect(editor2.getPath()).toBeUndefined() expect(workspace.getActivePane().items).toEqual([editor1, editor2]) expect(workspace.getActivePaneItem()).toBe(editor2) expect(workspace.getActivePane().activate).toHaveBeenCalled() - return expect(openEvents).toEqual([{uri: undefined, pane: workspace.getActivePane(), item: editor2, index: 1}]) + expect(openEvents).toEqual([{uri: undefined, pane: workspace.getActivePane(), item: editor2, index: 1}]) }) }) - ) + }) - return describe('when called with a uri', function () { - describe('when the active pane already has an editor for the given uri', () => - it('activates the existing editor on the active pane', function () { + describe('when called with a uri', () => { + describe('when the active pane already has an editor for the given uri', () => { + it('activates the existing editor on the active pane', () => { let editor = null let editor1 = null let editor2 = null waitsForPromise(() => - workspace.open('a').then(function (o) { + workspace.open('a').then(o => { editor1 = o - return workspace.open('b').then(function (o) { + return workspace.open('b').then(o => { editor2 = o - return workspace.open('a').then(o => (editor = o)) + return workspace.open('a').then(o => { editor = o }) }) }) ) - return runs(function () { + runs(() => { expect(editor).toBe(editor1) expect(workspace.getActivePaneItem()).toBe(editor) expect(workspace.getActivePane().activate).toHaveBeenCalled() - - return expect(openEvents).toEqual([ + const firstDirectory = atom.project.getDirectories()[0] + expect(firstDirectory).toBeDefined() + expect(openEvents).toEqual([ { - uri: __guard__(atom.project.getDirectories()[0], x => x.resolve('a')), + uri: firstDirectory.resolve('a'), item: editor1, pane: atom.workspace.getActivePane(), index: 0 }, { - uri: __guard__(atom.project.getDirectories()[0], x1 => x1.resolve('b')), + uri: firstDirectory.resolve('b'), item: editor2, pane: atom.workspace.getActivePane(), index: 1 }, { - uri: __guard__(atom.project.getDirectories()[0], x2 => x2.resolve('a')), + uri: firstDirectory.resolve('a'), item: editor1, pane: atom.workspace.getActivePane(), index: 0 @@ -190,95 +201,97 @@ describe('Workspace', function () { ]) }) }) - ) + }) - return describe('when the active pane does not have an editor for the given uri', () => - it('adds and activates a new editor for the given path on the active pane', function () { + describe('when the active pane does not have an editor for the given uri', () => { + it('adds and activates a new editor for the given path on the active pane', () => { let editor = null - waitsForPromise(() => workspace.open('a').then(o => (editor = o))) + waitsForPromise(() => workspace.open('a').then(o => { editor = o })) - return runs(function () { - expect(editor.getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a'))) + runs(() => { + const firstDirectory = atom.project.getDirectories()[0] + expect(firstDirectory).toBeDefined() + expect(editor.getURI()).toBe(firstDirectory.resolve('a')) expect(workspace.getActivePaneItem()).toBe(editor) expect(workspace.getActivePane().items).toEqual([editor]) - return expect(workspace.getActivePane().activate).toHaveBeenCalled() + expect(workspace.getActivePane().activate).toHaveBeenCalled() }) }) - ) + }) }) }) - describe("when the 'searchAllPanes' option is true", function () { - describe('when an editor for the given uri is already open on an inactive pane', () => - it('activates the existing editor on the inactive pane, then activates that pane', function () { + describe("when the 'searchAllPanes' option is true", () => { + describe('when an editor for the given uri is already open on an inactive pane', () => { + it('activates the existing editor on the inactive pane, then activates that pane', () => { let editor1 = null let editor2 = null const pane1 = workspace.getActivePane() const pane2 = workspace.getActivePane().splitRight() - waitsForPromise(function () { + waitsForPromise(() => { pane1.activate() - return workspace.open('a').then(o => (editor1 = o)) + return workspace.open('a').then(o => { editor1 = o }) }) - waitsForPromise(function () { + waitsForPromise(() => { pane2.activate() - return workspace.open('b').then(o => (editor2 = o)) + return workspace.open('b').then(o => { editor2 = o }) }) runs(() => expect(workspace.getActivePaneItem()).toBe(editor2)) waitsForPromise(() => workspace.open('a', {searchAllPanes: true})) - return runs(function () { + runs(() => { expect(workspace.getActivePane()).toBe(pane1) - return expect(workspace.getActivePaneItem()).toBe(editor1) + expect(workspace.getActivePaneItem()).toBe(editor1) }) }) - ) + }) - return describe('when no editor for the given uri is open in any pane', () => - it('opens an editor for the given uri in the active pane', function () { + describe('when no editor for the given uri is open in any pane', () => { + it('opens an editor for the given uri in the active pane', () => { let editor = null - waitsForPromise(() => workspace.open('a', {searchAllPanes: true}).then(o => (editor = o))) + waitsForPromise(() => workspace.open('a', {searchAllPanes: true}).then(o => { editor = o })) - return runs(() => expect(workspace.getActivePaneItem()).toBe(editor)) + runs(() => expect(workspace.getActivePaneItem()).toBe(editor)) }) - ) + }) }) - describe("when the 'split' option is set", function () { - describe("when the 'split' option is 'left'", () => - it('opens the editor in the leftmost pane of the current pane axis', function () { + describe("when the 'split' option is set", () => { + describe("when the 'split' option is 'left'", () => { + it('opens the editor in the leftmost pane of the current pane axis', () => { const pane1 = workspace.getActivePane() const pane2 = pane1.splitRight() expect(workspace.getActivePane()).toBe(pane2) let editor = null - waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => (editor = o))) + waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => { editor = o })) - runs(function () { + runs(() => { expect(workspace.getActivePane()).toBe(pane1) expect(pane1.items).toEqual([editor]) - return expect(pane2.items).toEqual([]) + expect(pane2.items).toEqual([]) }) // Focus right pane and reopen the file on the left - waitsForPromise(function () { + waitsForPromise(() => { pane2.focus() - return workspace.open('a', {split: 'left'}).then(o => (editor = o)) + return workspace.open('a', {split: 'left'}).then(o => { editor = o }) }) - return runs(function () { + runs(() => { expect(workspace.getActivePane()).toBe(pane1) expect(pane1.items).toEqual([editor]) - return expect(pane2.items).toEqual([]) + expect(pane2.items).toEqual([]) }) }) - ) + }) - describe('when a pane axis is the leftmost sibling of the current pane', () => - it('opens the new item in the current pane', function () { + describe('when a pane axis is the leftmost sibling of the current pane', () => { + it('opens the new item in the current pane', () => { let editor = null const pane1 = workspace.getActivePane() const pane2 = pane1.splitLeft() @@ -286,44 +299,44 @@ describe('Workspace', function () { pane1.activate() expect(workspace.getActivePane()).toBe(pane1) - waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => (editor = o))) + waitsForPromise(() => workspace.open('a', {split: 'left'}).then(o => { editor = o })) - return runs(function () { + runs(() => { expect(workspace.getActivePane()).toBe(pane1) - return expect(pane1.items).toEqual([editor]) + expect(pane1.items).toEqual([editor]) }) }) - ) + }) - describe("when the 'split' option is 'right'", function () { - it('opens the editor in the rightmost pane of the current pane axis', function () { + describe("when the 'split' option is 'right'", () => { + it('opens the editor in the rightmost pane of the current pane axis', () => { let editor = null const pane1 = workspace.getActivePane() let pane2 = null - waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => (editor = o))) + waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => { editor = o })) - runs(function () { + runs(() => { pane2 = workspace.getPanes().filter(p => p !== pane1)[0] expect(workspace.getActivePane()).toBe(pane2) expect(pane1.items).toEqual([]) - return expect(pane2.items).toEqual([editor]) + expect(pane2.items).toEqual([editor]) }) // Focus right pane and reopen the file on the right - waitsForPromise(function () { + waitsForPromise(() => { pane1.focus() - return workspace.open('a', {split: 'right'}).then(o => (editor = o)) + return workspace.open('a', {split: 'right'}).then(o => { editor = o }) }) - return runs(function () { + runs(() => { expect(workspace.getActivePane()).toBe(pane2) expect(pane1.items).toEqual([]) - return expect(pane2.items).toEqual([editor]) + expect(pane2.items).toEqual([editor]) }) }) - return describe('when a pane axis is the rightmost sibling of the current pane', () => - it('opens the new item in a new pane split to the right of the current pane', function () { + describe('when a pane axis is the rightmost sibling of the current pane', () => { + it('opens the new item in a new pane split to the right of the current pane', () => { let editor = null const pane1 = workspace.getActivePane() const pane2 = pane1.splitRight() @@ -332,50 +345,50 @@ describe('Workspace', function () { expect(workspace.getActivePane()).toBe(pane1) let pane4 = null - waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => (editor = o))) + waitsForPromise(() => workspace.open('a', {split: 'right'}).then(o => { editor = o })) - return runs(function () { + runs(() => { pane4 = workspace.getPanes().filter(p => p !== pane1)[0] expect(workspace.getActivePane()).toBe(pane4) expect(pane4.items).toEqual([editor]) expect(workspace.paneContainer.root.children[0]).toBe(pane1) - return expect(workspace.paneContainer.root.children[1]).toBe(pane4) + expect(workspace.paneContainer.root.children[1]).toBe(pane4) }) }) - ) + }) }) - describe("when the 'split' option is 'up'", () => - it('opens the editor in the topmost pane of the current pane axis', function () { + describe("when the 'split' option is 'up'", () => { + it('opens the editor in the topmost pane of the current pane axis', () => { const pane1 = workspace.getActivePane() const pane2 = pane1.splitDown() expect(workspace.getActivePane()).toBe(pane2) let editor = null - waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => (editor = o))) + waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => { editor = o })) - runs(function () { + runs(() => { expect(workspace.getActivePane()).toBe(pane1) expect(pane1.items).toEqual([editor]) - return expect(pane2.items).toEqual([]) + expect(pane2.items).toEqual([]) }) // Focus bottom pane and reopen the file on the top - waitsForPromise(function () { + waitsForPromise(() => { pane2.focus() - return workspace.open('a', {split: 'up'}).then(o => (editor = o)) + return workspace.open('a', {split: 'up'}).then(o => { editor = o }) }) - return runs(function () { + runs(() => { expect(workspace.getActivePane()).toBe(pane1) expect(pane1.items).toEqual([editor]) - return expect(pane2.items).toEqual([]) + expect(pane2.items).toEqual([]) }) }) - ) + }) - describe('when a pane axis is the topmost sibling of the current pane', () => - it('opens the new item in the current pane', function () { + describe('when a pane axis is the topmost sibling of the current pane', () => { + it('opens the new item in the current pane', () => { let editor = null const pane1 = workspace.getActivePane() const pane2 = pane1.splitUp() @@ -383,44 +396,44 @@ describe('Workspace', function () { pane1.activate() expect(workspace.getActivePane()).toBe(pane1) - waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => (editor = o))) + waitsForPromise(() => workspace.open('a', {split: 'up'}).then(o => { editor = o })) - return runs(function () { + runs(() => { expect(workspace.getActivePane()).toBe(pane1) - return expect(pane1.items).toEqual([editor]) + expect(pane1.items).toEqual([editor]) }) }) - ) + }) - return describe("when the 'split' option is 'down'", function () { - it('opens the editor in the bottommost pane of the current pane axis', function () { + describe("when the 'split' option is 'down'", () => { + it('opens the editor in the bottommost pane of the current pane axis', () => { let editor = null const pane1 = workspace.getActivePane() let pane2 = null - waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => (editor = o))) + waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => { editor = o })) - runs(function () { + runs(() => { pane2 = workspace.getPanes().filter(p => p !== pane1)[0] expect(workspace.getActivePane()).toBe(pane2) expect(pane1.items).toEqual([]) - return expect(pane2.items).toEqual([editor]) + expect(pane2.items).toEqual([editor]) }) // Focus bottom pane and reopen the file on the right - waitsForPromise(function () { + waitsForPromise(() => { pane1.focus() - return workspace.open('a', {split: 'down'}).then(o => (editor = o)) + return workspace.open('a', {split: 'down'}).then(o => { editor = o }) }) - return runs(function () { + runs(() => { expect(workspace.getActivePane()).toBe(pane2) expect(pane1.items).toEqual([]) - return expect(pane2.items).toEqual([editor]) + expect(pane2.items).toEqual([editor]) }) }) - return describe('when a pane axis is the bottommost sibling of the current pane', () => - it('opens the new item in a new pane split to the bottom of the current pane', function () { + describe('when a pane axis is the bottommost sibling of the current pane', () => { + it('opens the new item in a new pane split to the bottom of the current pane', () => { let editor = null const pane1 = workspace.getActivePane() const pane2 = pane1.splitDown() @@ -428,22 +441,22 @@ describe('Workspace', function () { expect(workspace.getActivePane()).toBe(pane1) let pane4 = null - waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => (editor = o))) + waitsForPromise(() => workspace.open('a', {split: 'down'}).then(o => { editor = o })) - return runs(function () { + runs(() => { pane4 = workspace.getPanes().filter(p => p !== pane1)[0] expect(workspace.getActivePane()).toBe(pane4) expect(pane4.items).toEqual([editor]) expect(workspace.paneContainer.root.children[0]).toBe(pane1) - return expect(workspace.paneContainer.root.children[1]).toBe(pane2) + expect(workspace.paneContainer.root.children[1]).toBe(pane2) }) }) - ) + }) }) }) - describe('when an initialLine and initialColumn are specified', () => - it('moves the cursor to the indicated location', function () { + describe('when an initialLine and initialColumn are specified', () => { + it('moves the cursor to the indicated location', () => { waitsForPromise(() => workspace.open('a', {initialLine: 1, initialColumn: 5})) runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([1, 5])) @@ -466,84 +479,92 @@ describe('Workspace', function () { waitsForPromise(() => workspace.open('a', {initialLine: Infinity, initialColumn: Infinity})) - return runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 11])) + runs(() => expect(workspace.getActiveTextEditor().getCursorBufferPosition()).toEqual([2, 11])) }) - ) + }) - describe('when the file is over 2MB', () => - it('opens the editor with largeFileMode: true', function () { + describe('when the file is over 2MB', () => { + it('opens the editor with largeFileMode: true', () => { spyOn(fs, 'getSizeSync').andReturn(2 * 1048577) // 2MB let editor = null - waitsForPromise(() => workspace.open('sample.js').then(e => (editor = e))) + waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e })) - return runs(() => expect(editor.largeFileMode).toBe(true)) + runs(() => expect(editor.largeFileMode).toBe(true)) }) - ) + }) - describe('when the file is over user-defined limit', function () { - const shouldPromptForFileOfSize = function (size, shouldPrompt) { + describe('when the file is over user-defined limit', () => { + const shouldPromptForFileOfSize = (size, shouldPrompt) => { spyOn(fs, 'getSizeSync').andReturn(size * 1048577) atom.applicationDelegate.confirm.andCallFake(() => selectedButtonIndex) atom.applicationDelegate.confirm() var selectedButtonIndex = 1 // cancel let editor = null - waitsForPromise(() => workspace.open('sample.js').then(e => (editor = e))) + waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e })) if (shouldPrompt) { - runs(function () { + runs(() => { expect(editor).toBeUndefined() expect(atom.applicationDelegate.confirm).toHaveBeenCalled() atom.applicationDelegate.confirm.reset() - return (selectedButtonIndex = 0) + selectedButtonIndex = 0 }) // open the file - waitsForPromise(() => workspace.open('sample.js').then(e => (editor = e))) + waitsForPromise(() => workspace.open('sample.js').then(e => { editor = e })) - return runs(function () { + runs(() => { expect(atom.applicationDelegate.confirm).toHaveBeenCalled() - return expect(editor.largeFileMode).toBe(true) + expect(editor.largeFileMode).toBe(true) }) } else { - return runs(() => expect(editor).not.toBeUndefined()) + runs(() => expect(editor).not.toBeUndefined()) } } - it('prompts the user to make sure they want to open a file this big', function () { + it('prompts the user to make sure they want to open a file this big', () => { atom.config.set('core.warnOnLargeFileLimit', 20) - return shouldPromptForFileOfSize(20, true) + shouldPromptForFileOfSize(20, true) }) - it("doesn't prompt on files below the limit", function () { + it("doesn't prompt on files below the limit", () => { atom.config.set('core.warnOnLargeFileLimit', 30) - return shouldPromptForFileOfSize(20, false) + shouldPromptForFileOfSize(20, false) }) - return it('prompts for smaller files with a lower limit', function () { + it('prompts for smaller files with a lower limit', () => { atom.config.set('core.warnOnLargeFileLimit', 5) - return shouldPromptForFileOfSize(10, true) + shouldPromptForFileOfSize(10, true) }) }) - describe('when passed a path that matches a custom opener', () => - it('returns the resource returned by the custom opener', function () { - const fooOpener = function (pathToOpen, options) { if (pathToOpen != null ? pathToOpen.match(/\.foo/) : undefined) { return {foo: pathToOpen, options} } } - const barOpener = function (pathToOpen) { if (pathToOpen != null ? pathToOpen.match(/^bar:\/\//) : undefined) { return {bar: pathToOpen} } } + describe('when passed a path that matches a custom opener', () => { + it('returns the resource returned by the custom opener', () => { + const fooOpener = (pathToOpen, options) => { + if (pathToOpen != null ? pathToOpen.match(/\.foo/) : undefined) { + return {foo: pathToOpen, options} + } + } + const barOpener = (pathToOpen) => { + if (pathToOpen != null ? pathToOpen.match(/^bar:\/\//) : undefined) { + return {bar: pathToOpen} + } + } workspace.addOpener(fooOpener) workspace.addOpener(barOpener) - waitsForPromise(function () { - const pathToOpen = __guard__(atom.project.getDirectories()[0], x => x.resolve('a.foo')) + waitsForPromise(() => { + const pathToOpen = atom.project.getDirectories()[0].resolve('a.foo') return workspace.open(pathToOpen, {hey: 'there'}).then(item => expect(item).toEqual({foo: pathToOpen, options: {hey: 'there'}})) }) - return waitsForPromise(() => + waitsForPromise(() => workspace.open('bar://baz').then(item => expect(item).toEqual({bar: 'bar://baz'}))) }) - ) + }) - it("adds the file to the application's recent documents list", function () { + it("adds the file to the application's recent documents list", () => { if (process.platform !== 'darwin') { return } // Feature only supported on macOS spyOn(atom.applicationDelegate, 'addRecentDocument') @@ -557,39 +578,39 @@ describe('Workspace', function () { waitsForPromise(() => workspace.open(__filename)) - return runs(() => expect(atom.applicationDelegate.addRecentDocument).toHaveBeenCalledWith(__filename)) + runs(() => expect(atom.applicationDelegate.addRecentDocument).toHaveBeenCalledWith(__filename)) }) - it('notifies ::onDidAddTextEditor observers', function () { + it('notifies ::onDidAddTextEditor observers', () => { const absolutePath = require.resolve('./fixtures/dir/a') const newEditorHandler = jasmine.createSpy('newEditorHandler') workspace.onDidAddTextEditor(newEditorHandler) let editor = null - waitsForPromise(() => workspace.open(absolutePath).then(e => (editor = e))) + waitsForPromise(() => workspace.open(absolutePath).then(e => { editor = e })) - return runs(() => expect(newEditorHandler.argsForCall[0][0].textEditor).toBe(editor)) + runs(() => expect(newEditorHandler.argsForCall[0][0].textEditor).toBe(editor)) }) - describe('when there is an error opening the file', function () { + describe('when there is an error opening the file', () => { let notificationSpy = null beforeEach(() => atom.notifications.onDidAddNotification(notificationSpy = jasmine.createSpy())) - describe('when a file does not exist', () => - it('creates an empty buffer for the specified path', function () { + describe('when a file does not exist', () => { + it('creates an empty buffer for the specified path', () => { waitsForPromise(() => workspace.open('not-a-file.md')) - return runs(function () { + runs(() => { const editor = workspace.getActiveTextEditor() expect(notificationSpy).not.toHaveBeenCalled() - return expect(editor.getPath()).toContain('not-a-file.md') + expect(editor.getPath()).toContain('not-a-file.md') }) }) - ) + }) - describe('when the user does not have access to the file', function () { + describe('when the user does not have access to the file', () => { beforeEach(() => - spyOn(fs, 'openSync').andCallFake(function (path) { + spyOn(fs, 'openSync').andCallFake(path => { const error = new Error(`EACCES, permission denied '${path}'`) error.path = path error.code = 'EACCES' @@ -597,22 +618,22 @@ describe('Workspace', function () { }) ) - return it('creates a notification', function () { + it('creates a notification', () => { waitsForPromise(() => workspace.open('file1')) - return runs(function () { + runs(() => { expect(notificationSpy).toHaveBeenCalled() const notification = notificationSpy.mostRecentCall.args[0] expect(notification.getType()).toBe('warning') expect(notification.getMessage()).toContain('Permission denied') - return expect(notification.getMessage()).toContain('file1') + expect(notification.getMessage()).toContain('file1') }) }) }) - describe('when the the operation is not permitted', function () { + describe('when the the operation is not permitted', () => { beforeEach(() => - spyOn(fs, 'openSync').andCallFake(function (path) { + spyOn(fs, 'openSync').andCallFake(path => { const error = new Error(`EPERM, operation not permitted '${path}'`) error.path = path error.code = 'EPERM' @@ -620,22 +641,22 @@ describe('Workspace', function () { }) ) - return it('creates a notification', function () { + it('creates a notification', () => { waitsForPromise(() => workspace.open('file1')) - return runs(function () { + runs(() => { expect(notificationSpy).toHaveBeenCalled() const notification = notificationSpy.mostRecentCall.args[0] expect(notification.getType()).toBe('warning') expect(notification.getMessage()).toContain('Unable to open') - return expect(notification.getMessage()).toContain('file1') + expect(notification.getMessage()).toContain('file1') }) }) }) - describe('when the the file is already open in windows', function () { + describe('when the the file is already open in windows', () => { beforeEach(() => - spyOn(fs, 'openSync').andCallFake(function (path) { + spyOn(fs, 'openSync').andCallFake(path => { const error = new Error(`EBUSY, resource busy or locked '${path}'`) error.path = path error.code = 'EBUSY' @@ -643,42 +664,42 @@ describe('Workspace', function () { }) ) - return it('creates a notification', function () { + it('creates a notification', () => { waitsForPromise(() => workspace.open('file1')) - return runs(function () { + runs(() => { expect(notificationSpy).toHaveBeenCalled() const notification = notificationSpy.mostRecentCall.args[0] expect(notification.getType()).toBe('warning') expect(notification.getMessage()).toContain('Unable to open') - return expect(notification.getMessage()).toContain('file1') + expect(notification.getMessage()).toContain('file1') }) }) }) - return describe('when there is an unhandled error', function () { + describe('when there is an unhandled error', () => { beforeEach(() => - spyOn(fs, 'openSync').andCallFake(function (path) { + spyOn(fs, 'openSync').andCallFake(path => { throw new Error('I dont even know what is happening right now!!') }) ) - return it('creates a notification', function () { + it('creates a notification', () => { const open = () => workspace.open('file1', workspace.getActivePane()) - return expect(open).toThrow() + expect(open).toThrow() }) }) }) - describe('when the file is already open in pending state', () => - it('should terminate the pending state', function () { + describe('when the file is already open in pending state', () => { + it('should terminate the pending state', () => { let editor = null let pane = null waitsForPromise(() => - atom.workspace.open('sample.js', {pending: true}).then(function (o) { + atom.workspace.open('sample.js', {pending: true}).then(o => { editor = o - return (pane = atom.workspace.getActivePane()) + pane = atom.workspace.getActivePane() }) ) @@ -686,34 +707,34 @@ describe('Workspace', function () { waitsForPromise(() => atom.workspace.open('sample.js')) - return runs(() => expect(pane.getPendingItem()).toBeNull()) + runs(() => expect(pane.getPendingItem()).toBeNull()) }) - ) + }) - describe('when opening will switch from a pending tab to a permanent tab', () => - it('keeps the pending tab open', function () { + describe('when opening will switch from a pending tab to a permanent tab', () => { + it('keeps the pending tab open', () => { let editor1 = null let editor2 = null waitsForPromise(() => - atom.workspace.open('sample.txt').then(o => (editor1 = o)) + atom.workspace.open('sample.txt').then(o => { editor1 = o }) ) waitsForPromise(() => - atom.workspace.open('sample2.txt', {pending: true}).then(o => (editor2 = o)) + atom.workspace.open('sample2.txt', {pending: true}).then(o => { editor2 = o }) ) - return runs(function () { + runs(() => { const pane = atom.workspace.getActivePane() pane.activateItem(editor1) expect(pane.getItems().length).toBe(2) - return expect(pane.getItems()).toEqual([editor1, editor2]) + expect(pane.getItems()).toEqual([editor1, editor2]) }) }) - ) + }) - return describe('when replacing a pending item which is the last item in a second pane', () => - it('does not destroy the pane even if core.destroyEmptyPanes is on', function () { + describe('when replacing a pending item which is the last item in a second pane', () => { + it('does not destroy the pane even if core.destroyEmptyPanes is on', () => { atom.config.set('core.destroyEmptyPanes', true) let editor1 = null let editor2 = null @@ -721,46 +742,46 @@ describe('Workspace', function () { let rightPane = null waitsForPromise(() => - atom.workspace.open('sample.js', {pending: true, split: 'right'}).then(function (o) { + atom.workspace.open('sample.js', {pending: true, split: 'right'}).then(o => { editor1 = o rightPane = atom.workspace.getActivePane() - return spyOn(rightPane, 'destroyed') + spyOn(rightPane, 'destroyed') }) ) - runs(function () { + runs(() => { expect(leftPane).not.toBe(rightPane) expect(atom.workspace.getActivePane()).toBe(rightPane) expect(atom.workspace.getActivePane().getItems().length).toBe(1) - return expect(rightPane.getPendingItem()).toBe(editor1) + expect(rightPane.getPendingItem()).toBe(editor1) }) waitsForPromise(() => - atom.workspace.open('sample.txt', {pending: true}).then(o => (editor2 = o)) + atom.workspace.open('sample.txt', {pending: true}).then(o => { editor2 = o }) ) - return runs(function () { + runs(() => { expect(rightPane.getPendingItem()).toBe(editor2) - return expect(rightPane.destroyed.callCount).toBe(0) + expect(rightPane.destroyed.callCount).toBe(0) }) }) - ) + }) }) - describe('the grammar-used hook', () => - it('fires when opening a file or changing the grammar of an open file', function () { + describe('the grammar-used hook', () => { + it('fires when opening a file or changing the grammar of an open file', () => { let editor = null let javascriptGrammarUsed = false let coffeescriptGrammarUsed = false atom.packages.triggerDeferredActivationHooks() - runs(function () { - atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', () => (javascriptGrammarUsed = true)) - return atom.packages.onDidTriggerActivationHook('language-coffee-script:grammar-used', () => (coffeescriptGrammarUsed = true)) + runs(() => { + atom.packages.onDidTriggerActivationHook('language-javascript:grammar-used', () => { javascriptGrammarUsed = true }) + atom.packages.onDidTriggerActivationHook('language-coffee-script:grammar-used', () => { coffeescriptGrammarUsed = true }) }) - waitsForPromise(() => atom.workspace.open('sample.js', {autoIndent: false}).then(o => (editor = o))) + waitsForPromise(() => atom.workspace.open('sample.js', {autoIndent: false}).then(o => { editor = o })) waitsForPromise(() => atom.packages.activatePackage('language-javascript')) @@ -770,12 +791,12 @@ describe('Workspace', function () { runs(() => editor.setGrammar(atom.grammars.selectGrammar('.coffee'))) - return waitsFor(() => coffeescriptGrammarUsed) + waitsFor(() => coffeescriptGrammarUsed) }) - ) + }) - describe('::reopenItem()', () => - it("opens the uri associated with the last closed pane that isn't currently open", function () { + describe('::reopenItem()', () => { + it("opens the uri associated with the last closed pane that isn't currently open", () => { const pane = workspace.getActivePane() waitsForPromise(() => workspace.open('a').then(() => @@ -785,46 +806,49 @@ describe('Workspace', function () { ) ) - runs(function () { + runs(() => { // does not reopen items with no uri expect(workspace.getActivePaneItem().getURI()).toBeUndefined() - return pane.destroyActiveItem() + pane.destroyActiveItem() }) waitsForPromise(() => workspace.reopenItem()) - runs(function () { + const firstDirectory = atom.project.getDirectories()[0] + expect(firstDirectory).toBeDefined() + + runs(() => { expect(workspace.getActivePaneItem().getURI()).not.toBeUndefined() // destroy all items - expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('file1'))) + expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('file1')) pane.destroyActiveItem() - expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x1 => x1.resolve('b'))) + expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('b')) pane.destroyActiveItem() - expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x2 => x2.resolve('a'))) + expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('a')) pane.destroyActiveItem() // reopens items with uris - return expect(workspace.getActivePaneItem()).toBeUndefined() + expect(workspace.getActivePaneItem()).toBeUndefined() }) waitsForPromise(() => workspace.reopenItem()) - runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a')))) + runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('a'))) // does not reopen items that are already open waitsForPromise(() => workspace.open('b')) - runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('b')))) + runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('b'))) waitsForPromise(() => workspace.reopenItem()) - return runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('file1')))) + runs(() => expect(workspace.getActivePaneItem().getURI()).toBe(firstDirectory.resolve('file1'))) }) - ) + }) - describe('::increase/decreaseFontSize()', () => - it('increases/decreases the font size without going below 1', function () { + describe('::increase/decreaseFontSize()', () => { + it('increases/decreases the font size without going below 1', () => { atom.config.set('editor.fontSize', 1) workspace.increaseFontSize() expect(atom.config.get('editor.fontSize')).toBe(2) @@ -835,12 +859,12 @@ describe('Workspace', function () { workspace.decreaseFontSize() expect(atom.config.get('editor.fontSize')).toBe(1) workspace.decreaseFontSize() - return expect(atom.config.get('editor.fontSize')).toBe(1) + expect(atom.config.get('editor.fontSize')).toBe(1) }) - ) + }) - describe('::resetFontSize()', function () { - it("resets the font size to the window's starting font size", function () { + describe('::resetFontSize()', () => { + it("resets the font size to the window's starting font size", () => { const originalFontSize = atom.config.get('editor.fontSize') workspace.increaseFontSize() @@ -850,17 +874,17 @@ describe('Workspace', function () { workspace.decreaseFontSize() expect(atom.config.get('editor.fontSize')).toBe(originalFontSize - 1) workspace.resetFontSize() - return expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) }) - it('does nothing if the font size has not been changed', function () { + it('does nothing if the font size has not been changed', () => { const originalFontSize = atom.config.get('editor.fontSize') workspace.resetFontSize() - return expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) }) - return it("resets the font size when the editor's font size changes", function () { + it("resets the font size when the editor's font size changes", () => { const originalFontSize = atom.config.get('editor.fontSize') atom.config.set('editor.fontSize', originalFontSize + 1) @@ -868,28 +892,28 @@ describe('Workspace', function () { expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) atom.config.set('editor.fontSize', originalFontSize - 1) workspace.resetFontSize() - return expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) + expect(atom.config.get('editor.fontSize')).toBe(originalFontSize) }) }) - describe('::openLicense()', () => - it('opens the license as plain-text in a buffer', function () { + describe('::openLicense()', () => { + it('opens the license as plain-text in a buffer', () => { waitsForPromise(() => workspace.openLicense()) - return runs(() => expect(workspace.getActivePaneItem().getText()).toMatch(/Copyright/)) + runs(() => expect(workspace.getActivePaneItem().getText()).toMatch(/Copyright/)) }) - ) + }) - describe('::isTextEditor(obj)', () => - it('returns true when the passed object is an instance of `TextEditor`', function () { + describe('::isTextEditor(obj)', () => { + it('returns true when the passed object is an instance of `TextEditor`', () => { expect(workspace.isTextEditor(new TextEditor())).toBe(true) - expect(workspace.isTextEditor({getText () { return null }})).toBe(false) + expect(workspace.isTextEditor({getText: () => null})).toBe(false) expect(workspace.isTextEditor(null)).toBe(false) - return expect(workspace.isTextEditor(undefined)).toBe(false) + expect(workspace.isTextEditor(undefined)).toBe(false) }) - ) + }) - describe('::observeTextEditors()', () => - it('invokes the observer with current and future text editors', function () { + describe('::observeTextEditors()', () => { + it('invokes the observer with current and future text editors', () => { const observed = [] waitsForPromise(() => workspace.open()) @@ -900,43 +924,43 @@ describe('Workspace', function () { waitsForPromise(() => workspace.open()) - return expect(observed).toEqual(workspace.getTextEditors()) + expect(observed).toEqual(workspace.getTextEditors()) }) - ) + }) - describe('when an editor is destroyed', () => - it('removes the editor', function () { + describe('when an editor is destroyed', () => { + it('removes the editor', () => { let editor = null - waitsForPromise(() => workspace.open('a').then(e => (editor = e))) + waitsForPromise(() => workspace.open('a').then(e => { editor = e })) - return runs(function () { + runs(() => { expect(workspace.getTextEditors()).toHaveLength(1) editor.destroy() - return expect(workspace.getTextEditors()).toHaveLength(0) + expect(workspace.getTextEditors()).toHaveLength(0) }) }) - ) + }) - describe('when an editor is copied because its pane is split', () => - it('sets up the new editor to be configured by the text editor registry', function () { + describe('when an editor is copied because its pane is split', () => { + it('sets up the new editor to be configured by the text editor registry', () => { waitsForPromise(() => atom.packages.activatePackage('language-javascript')) - return waitsForPromise(() => - workspace.open('a').then(function (editor) { + waitsForPromise(() => + workspace.open('a').then(editor => { atom.textEditors.setGrammarOverride(editor, 'source.js') expect(editor.getGrammar().name).toBe('JavaScript') workspace.getActivePane().splitRight({copyActiveItem: true}) const newEditor = workspace.getActiveTextEditor() expect(newEditor).not.toBe(editor) - return expect(newEditor.getGrammar().name).toBe('JavaScript') + expect(newEditor.getGrammar().name).toBe('JavaScript') }) ) }) - ) + }) - it('stores the active grammars used by all the open editors', function () { + it('stores the active grammars used by all the open editors', () => { waitsForPromise(() => atom.packages.activatePackage('language-javascript')) waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) @@ -945,7 +969,7 @@ describe('Workspace', function () { waitsForPromise(() => atom.workspace.open('sample.coffee')) - return runs(function () { + runs(function () { atom.workspace.getActiveTextEditor().setText(`\ i = /test/; #FIXME\ ` @@ -979,116 +1003,116 @@ i = /test/; #FIXME\ 'TODO' ]) - return atom2.destroy() + atom2.destroy() }) }) - describe('document.title', function () { - describe('when there is no item open', function () { + describe('document.title', () => { + describe('when there is no item open', () => { it('sets the title to the project path', () => expect(document.title).toMatch(escapeStringRegex(fs.tildify(atom.project.getPaths()[0])))) - return it("sets the title to 'untitled' if there is no project path", function () { + it("sets the title to 'untitled' if there is no project path", () => { atom.project.setPaths([]) - return expect(document.title).toMatch(/^untitled/) + expect(document.title).toMatch(/^untitled/) }) }) - describe("when the active pane item's path is not inside a project path", function () { + describe("when the active pane item's path is not inside a project path", () => { beforeEach(() => waitsForPromise(() => atom.workspace.open('b').then(() => atom.project.setPaths([])) ) ) - it("sets the title to the pane item's title plus the item's path", function () { + it("sets the title to the pane item's title plus the item's path", () => { const item = atom.workspace.getActivePaneItem() const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) - return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) + expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) }) - describe('when the title of the active pane item changes', () => - it("updates the window title based on the item's new title", function () { + describe('when the title of the active pane item changes', () => { + it("updates the window title based on the item's new title", () => { const editor = atom.workspace.getActivePaneItem() editor.buffer.setPath(path.join(temp.dir, 'hi')) const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(editor.getPath()))) - return expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)) + expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)) }) - ) + }) - describe("when the active pane's item changes", () => - it("updates the title to the new item's title plus the project path", function () { + describe("when the active pane's item changes", () => { + it("updates the title to the new item's title plus the project path", () => { atom.workspace.getActivePane().activateNextItem() const item = atom.workspace.getActivePaneItem() const pathEscaped = fs.tildify(escapeStringRegex(path.dirname(item.getPath()))) - return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) + expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) }) - ) + }) - return describe("when an inactive pane's item changes", () => - it('does not update the title', function () { + describe("when an inactive pane's item changes", () => { + it('does not update the title', () => { const pane = atom.workspace.getActivePane() pane.splitRight() const initialTitle = document.title pane.activateNextItem() - return expect(document.title).toBe(initialTitle) + expect(document.title).toBe(initialTitle) }) - ) + }) }) - describe('when the active pane item is inside a project path', function () { + describe('when the active pane item is inside a project path', () => { beforeEach(() => waitsForPromise(() => atom.workspace.open('b')) ) - describe('when there is an active pane item', () => - it("sets the title to the pane item's title plus the project path", function () { + describe('when there is an active pane item', () => { + it("sets the title to the pane item's title plus the project path", () => { const item = atom.workspace.getActivePaneItem() const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) + expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) }) - ) + }) - describe('when the title of the active pane item changes', () => - it("updates the window title based on the item's new title", function () { + describe('when the title of the active pane item changes', () => { + it("updates the window title based on the item's new title", () => { const editor = atom.workspace.getActivePaneItem() editor.buffer.setPath(path.join(atom.project.getPaths()[0], 'hi')) const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - return expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)) + expect(document.title).toMatch(new RegExp(`^${editor.getTitle()} \\u2014 ${pathEscaped}`)) }) - ) + }) - describe("when the active pane's item changes", () => - it("updates the title to the new item's title plus the project path", function () { + describe("when the active pane's item changes", () => { + it("updates the title to the new item's title plus the project path", () => { atom.workspace.getActivePane().activateNextItem() const item = atom.workspace.getActivePaneItem() const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) - return expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) + expect(document.title).toMatch(new RegExp(`^${item.getTitle()} \\u2014 ${pathEscaped}`)) }) - ) + }) - describe('when the last pane item is removed', () => - it("updates the title to the project's first path", function () { + describe('when the last pane item is removed', () => { + it("updates the title to the project's first path", () => { atom.workspace.getActivePane().destroy() expect(atom.workspace.getActivePaneItem()).toBeUndefined() - return expect(document.title).toMatch(escapeStringRegex(fs.tildify(atom.project.getPaths()[0]))) + expect(document.title).toMatch(escapeStringRegex(fs.tildify(atom.project.getPaths()[0]))) }) - ) + }) - return describe("when an inactive pane's item changes", () => - it('does not update the title', function () { + describe("when an inactive pane's item changes", () => { + it('does not update the title', () => { const pane = atom.workspace.getActivePane() pane.splitRight() const initialTitle = document.title pane.activateNextItem() - return expect(document.title).toBe(initialTitle) + expect(document.title).toBe(initialTitle) }) - ) + }) }) - return describe('when the workspace is deserialized', function () { + describe('when the workspace is deserialized', () => { beforeEach(() => waitsForPromise(() => atom.workspace.open('a'))) - return it("updates the title to contain the project's path", function () { + it("updates the title to contain the project's path", () => { document.title = null const atom2 = new AtomEnvironment({ @@ -1109,30 +1133,33 @@ i = /test/; #FIXME\ const pathEscaped = fs.tildify(escapeStringRegex(atom.project.getPaths()[0])) expect(document.title).toMatch(new RegExp(`^${item.getLongTitle()} \\u2014 ${pathEscaped}`)) - return atom2.destroy() + atom2.destroy() }) }) }) - describe('document edited status', function () { - let [item1, item2] = Array.from([]) + describe('document edited status', () => { + let item1 + let item2 - beforeEach(function () { + beforeEach(() => { waitsForPromise(() => atom.workspace.open('a')) waitsForPromise(() => atom.workspace.open('b')) - return runs(() => ([item1, item2] = Array.from(atom.workspace.getPaneItems()))) + runs(() => { + [item1, item2] = atom.workspace.getPaneItems() + }) }) - it('calls setDocumentEdited when the active item changes', function () { + it('calls setDocumentEdited when the active item changes', () => { expect(atom.workspace.getActivePaneItem()).toBe(item2) item1.insertText('a') expect(item1.isModified()).toBe(true) atom.workspace.getActivePane().activateNextItem() - return expect(setDocumentEdited).toHaveBeenCalledWith(true) + expect(setDocumentEdited).toHaveBeenCalledWith(true) }) - return it("calls atom.setDocumentEdited when the active item's modified status changes", function () { + it("calls atom.setDocumentEdited when the active item's modified status changes", () => { expect(atom.workspace.getActivePaneItem()).toBe(item2) item2.insertText('a') advanceClock(item2.getBuffer().getStoppedChangingDelay()) @@ -1144,11 +1171,11 @@ i = /test/; #FIXME\ advanceClock(item2.getBuffer().getStoppedChangingDelay()) expect(item2.isModified()).toBe(false) - return expect(setDocumentEdited).toHaveBeenCalledWith(false) + expect(setDocumentEdited).toHaveBeenCalledWith(false) }) }) - describe('adding panels', function () { + describe('adding panels', () => { class TestItem {} // Don't use ES6 classes because then we'll have to call `super()` which we can't do with @@ -1165,8 +1192,8 @@ i = /test/; #FIXME\ atom.views.addViewProvider(TestItem, model => new TestItemElement().initialize(model)) ) - describe('::addLeftPanel(model)', () => - it('adds a panel to the correct panel container', function () { + describe('::addLeftPanel(model)', () => { + it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getLeftPanels().length).toBe(0) atom.workspace.panelContainers.left.onDidAddPanel(addPanelSpy = jasmine.createSpy()) @@ -1179,12 +1206,12 @@ i = /test/; #FIXME\ const itemView = atom.views.getView(atom.workspace.getLeftPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) - return expect(itemView.getModel()).toBe(model) + expect(itemView.getModel()).toBe(model) }) - ) + }) - describe('::addRightPanel(model)', () => - it('adds a panel to the correct panel container', function () { + describe('::addRightPanel(model)', () => { + it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getRightPanels().length).toBe(0) atom.workspace.panelContainers.right.onDidAddPanel(addPanelSpy = jasmine.createSpy()) @@ -1197,12 +1224,12 @@ i = /test/; #FIXME\ const itemView = atom.views.getView(atom.workspace.getRightPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) - return expect(itemView.getModel()).toBe(model) + expect(itemView.getModel()).toBe(model) }) - ) + }) - describe('::addTopPanel(model)', () => - it('adds a panel to the correct panel container', function () { + describe('::addTopPanel(model)', () => { + it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getTopPanels().length).toBe(0) atom.workspace.panelContainers.top.onDidAddPanel(addPanelSpy = jasmine.createSpy()) @@ -1215,12 +1242,12 @@ i = /test/; #FIXME\ const itemView = atom.views.getView(atom.workspace.getTopPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) - return expect(itemView.getModel()).toBe(model) + expect(itemView.getModel()).toBe(model) }) - ) + }) - describe('::addBottomPanel(model)', () => - it('adds a panel to the correct panel container', function () { + describe('::addBottomPanel(model)', () => { + it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getBottomPanels().length).toBe(0) atom.workspace.panelContainers.bottom.onDidAddPanel(addPanelSpy = jasmine.createSpy()) @@ -1233,12 +1260,12 @@ i = /test/; #FIXME\ const itemView = atom.views.getView(atom.workspace.getBottomPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) - return expect(itemView.getModel()).toBe(model) + expect(itemView.getModel()).toBe(model) }) - ) + }) - describe('::addHeaderPanel(model)', () => - it('adds a panel to the correct panel container', function () { + describe('::addHeaderPanel(model)', () => { + it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getHeaderPanels().length).toBe(0) atom.workspace.panelContainers.header.onDidAddPanel(addPanelSpy = jasmine.createSpy()) @@ -1251,12 +1278,12 @@ i = /test/; #FIXME\ const itemView = atom.views.getView(atom.workspace.getHeaderPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) - return expect(itemView.getModel()).toBe(model) + expect(itemView.getModel()).toBe(model) }) - ) + }) - describe('::addFooterPanel(model)', () => - it('adds a panel to the correct panel container', function () { + describe('::addFooterPanel(model)', () => { + it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getFooterPanels().length).toBe(0) atom.workspace.panelContainers.footer.onDidAddPanel(addPanelSpy = jasmine.createSpy()) @@ -1269,12 +1296,12 @@ i = /test/; #FIXME\ const itemView = atom.views.getView(atom.workspace.getFooterPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) - return expect(itemView.getModel()).toBe(model) + expect(itemView.getModel()).toBe(model) }) - ) + }) - describe('::addModalPanel(model)', () => - it('adds a panel to the correct panel container', function () { + describe('::addModalPanel(model)', () => { + it('adds a panel to the correct panel container', () => { let addPanelSpy expect(atom.workspace.getModalPanels().length).toBe(0) atom.workspace.panelContainers.modal.onDidAddPanel(addPanelSpy = jasmine.createSpy()) @@ -1287,36 +1314,36 @@ i = /test/; #FIXME\ const itemView = atom.views.getView(atom.workspace.getModalPanels()[0].getItem()) expect(itemView instanceof TestItemElement).toBe(true) - return expect(itemView.getModel()).toBe(model) + expect(itemView.getModel()).toBe(model) }) - ) + }) - return describe('::panelForItem(item)', () => - it('returns the panel associated with the item', function () { + describe('::panelForItem(item)', () => { + it('returns the panel associated with the item', () => { const item = new TestItem() const panel = atom.workspace.addLeftPanel({item}) const itemWithNoPanel = new TestItem() expect(atom.workspace.panelForItem(item)).toBe(panel) - return expect(atom.workspace.panelForItem(itemWithNoPanel)).toBe(null) + expect(atom.workspace.panelForItem(itemWithNoPanel)).toBe(null) }) - ) + }) }) - describe('::scan(regex, options, callback)', () => - describe('when called with a regex', function () { - it('calls the callback with all regex results in all files in the project', function () { + describe('::scan(regex, options, callback)', () => { + describe('when called with a regex', () => { + it('calls the callback with all regex results in all files in the project', () => { const results = [] waitsForPromise(() => atom.workspace.scan(/(a)+/, result => results.push(result)) ) - return runs(function () { + runs(() => { expect(results).toHaveLength(3) - expect(results[0].filePath).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a'))) + expect(results[0].filePath).toBe(atom.project.getDirectories()[0].resolve('a')) expect(results[0].matches).toHaveLength(3) - return expect(results[0].matches[0]).toEqual({ + expect(results[0].matches[0]).toEqual({ matchText: 'aaa', lineText: 'aaa bbb', lineTextOffset: 0, @@ -1325,17 +1352,16 @@ i = /test/; #FIXME\ }) }) - it('works with with escaped literals (like $ and ^)', function () { + it('works with with escaped literals (like $ and ^)', () => { const results = [] waitsForPromise(() => atom.workspace.scan(/\$\w+/, result => results.push(result))) - return runs(function () { + runs(() => { expect(results.length).toBe(1) - const {filePath, matches} = results[0] - expect(filePath).toBe(__guard__(atom.project.getDirectories()[0], x => x.resolve('a'))) + expect(filePath).toBe(atom.project.getDirectories()[0].resolve('a')) expect(matches).toHaveLength(1) - return expect(matches[0]).toEqual({ + expect(matches[0]).toEqual({ matchText: '$bill', lineText: 'dollar$bill', lineTextOffset: 0, @@ -1344,72 +1370,75 @@ i = /test/; #FIXME\ }) }) - it('works on evil filenames', function () { + it('works on evil filenames', () => { atom.config.set('core.excludeVcsIgnoredPaths', false) platform.generateEvilFiles() atom.project.setPaths([path.join(__dirname, 'fixtures', 'evil-files')]) const paths = [] let matches = [] waitsForPromise(() => - atom.workspace.scan(/evil/, function (result) { + atom.workspace.scan(/evil/, result => { paths.push(result.filePath) - return (matches = matches.concat(result.matches)) + matches = matches.concat(result.matches) }) ) - return runs(function () { + runs(() => { _.each(matches, m => expect(m.matchText).toEqual('evil')) if (platform.isWindows()) { expect(paths.length).toBe(3) expect(paths[0]).toMatch(/a_file_with_utf8.txt$/) expect(paths[1]).toMatch(/file with spaces.txt$/) - return expect(path.basename(paths[2])).toBe('utfa\u0306.md') + expect(path.basename(paths[2])).toBe('utfa\u0306.md') } else { expect(paths.length).toBe(5) expect(paths[0]).toMatch(/a_file_with_utf8.txt$/) expect(paths[1]).toMatch(/file with spaces.txt$/) expect(paths[2]).toMatch(/goddam\nnewlines$/m) expect(paths[3]).toMatch(/quote".txt$/m) - return expect(path.basename(paths[4])).toBe('utfa\u0306.md') + expect(path.basename(paths[4])).toBe('utfa\u0306.md') } }) }) - it('ignores case if the regex includes the `i` flag', function () { + it('ignores case if the regex includes the `i` flag', () => { const results = [] waitsForPromise(() => atom.workspace.scan(/DOLLAR/i, result => results.push(result))) - return runs(() => expect(results).toHaveLength(1)) + runs(() => expect(results).toHaveLength(1)) }) - describe('when the core.excludeVcsIgnoredPaths config is truthy', function () { - let [projectPath, ignoredPath] = Array.from([]) + describe('when the core.excludeVcsIgnoredPaths config is truthy', () => { + let projectPath + let ignoredPath - beforeEach(function () { + beforeEach(() => { const sourceProjectPath = path.join(__dirname, 'fixtures', 'git', 'working-dir') projectPath = path.join(temp.mkdirSync('atom')) const writerStream = fstream.Writer(projectPath) fstream.Reader(sourceProjectPath).pipe(writerStream) - waitsFor(function (done) { + waitsFor(done => { writerStream.on('close', done) - return writerStream.on('error', done) + writerStream.on('error', done) }) - return runs(function () { + runs(() => { fs.rename(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) ignoredPath = path.join(projectPath, 'ignored.txt') - return fs.writeFileSync(ignoredPath, 'this match should not be included') + fs.writeFileSync(ignoredPath, 'this match should not be included') }) }) - afterEach(function () { - if (fs.existsSync(projectPath)) { return fs.removeSync(projectPath) } + afterEach(() => { + if (fs.existsSync(projectPath)) { + fs.removeSync(projectPath) + } }) - return it('excludes ignored files', function () { + it('excludes ignored files', () => { atom.project.setPaths([projectPath]) atom.config.set('core.excludeVcsIgnoredPaths', true) const resultHandler = jasmine.createSpy('result found') @@ -1417,11 +1446,11 @@ i = /test/; #FIXME\ atom.workspace.scan(/match/, results => resultHandler()) ) - return runs(() => expect(resultHandler).not.toHaveBeenCalled()) + runs(() => expect(resultHandler).not.toHaveBeenCalled()) }) }) - it('includes only files when a directory filter is specified', function () { + it('includes only files when a directory filter is specified', () => { const projectPath = path.join(path.join(__dirname, 'fixtures', 'dir')) atom.project.setPaths([projectPath]) @@ -1430,20 +1459,20 @@ i = /test/; #FIXME\ const paths = [] let matches = [] waitsForPromise(() => - atom.workspace.scan(/aaa/, {paths: [`a-dir${path.sep}`]}, function (result) { + atom.workspace.scan(/aaa/, {paths: [`a-dir${path.sep}`]}, result => { paths.push(result.filePath) - return (matches = matches.concat(result.matches)) + matches = matches.concat(result.matches) }) ) - return runs(function () { + runs(() => { expect(paths.length).toBe(1) expect(paths[0]).toBe(filePath) - return expect(matches.length).toBe(1) + expect(matches.length).toBe(1) }) }) - it("includes files and folders that begin with a '.'", function () { + it("includes files and folders that begin with a '.'", () => { const projectPath = temp.mkdirSync('atom-spec-workspace') const filePath = path.join(projectPath, '.text') fs.writeFileSync(filePath, 'match this') @@ -1451,20 +1480,20 @@ i = /test/; #FIXME\ const paths = [] let matches = [] waitsForPromise(() => - atom.workspace.scan(/match this/, function (result) { + atom.workspace.scan(/match this/, result => { paths.push(result.filePath) - return (matches = matches.concat(result.matches)) + matches = matches.concat(result.matches) }) ) - return runs(function () { + runs(() => { expect(paths.length).toBe(1) expect(paths[0]).toBe(filePath) - return expect(matches.length).toBe(1) + expect(matches.length).toBe(1) }) }) - it('excludes values in core.ignoredNames', function () { + it('excludes values in core.ignoredNames', () => { const ignoredNames = atom.config.get('core.ignoredNames') ignoredNames.push('a') atom.config.set('core.ignoredNames', ignoredNames) @@ -1474,51 +1503,54 @@ i = /test/; #FIXME\ atom.workspace.scan(/dollar/, results => resultHandler()) ) - return runs(() => expect(resultHandler).not.toHaveBeenCalled()) + runs(() => expect(resultHandler).not.toHaveBeenCalled()) }) - it('scans buffer contents if the buffer is modified', function () { + it('scans buffer contents if the buffer is modified', () => { let editor = null const results = [] waitsForPromise(() => - atom.workspace.open('a').then(function (o) { + atom.workspace.open('a').then(o => { editor = o - return editor.setText('Elephant') + editor.setText('Elephant') }) ) waitsForPromise(() => atom.workspace.scan(/a|Elephant/, result => results.push(result))) - return runs(function () { + runs(() => { expect(results).toHaveLength(3) const resultForA = _.find(results, ({filePath}) => path.basename(filePath) === 'a') expect(resultForA.matches).toHaveLength(1) - return expect(resultForA.matches[0].matchText).toBe('Elephant') + expect(resultForA.matches[0].matchText).toBe('Elephant') }) }) - it('ignores buffers outside the project', function () { + it('ignores buffers outside the project', () => { let editor = null const results = [] waitsForPromise(() => - atom.workspace.open(temp.openSync().path).then(function (o) { + atom.workspace.open(temp.openSync().path).then(o => { editor = o - return editor.setText('Elephant') + editor.setText('Elephant') }) ) waitsForPromise(() => atom.workspace.scan(/Elephant/, result => results.push(result))) - return runs(() => expect(results).toHaveLength(0)) + runs(() => expect(results).toHaveLength(0)) }) - return describe('when the project has multiple root directories', function () { - let [dir1, dir2, file1, file2] = Array.from([]) + describe('when the project has multiple root directories', () => { + let dir1 + let dir2 + let file1 + let file2 - beforeEach(function () { - [dir1] = Array.from(atom.project.getPaths()) + beforeEach(() => { + dir1 = atom.project.getPaths()[0] file1 = path.join(dir1, 'a-dir', 'oh-git') dir2 = temp.mkdirSync('a-second-dir') @@ -1527,59 +1559,67 @@ i = /test/; #FIXME\ fs.mkdirSync(aDir2) fs.writeFileSync(file2, 'ccc aaaa') - return atom.project.addPath(dir2) + atom.project.addPath(dir2) }) - it("searches matching files in all of the project's root directories", function () { + it("searches matching files in all of the project's root directories", () => { const resultPaths = [] waitsForPromise(() => atom.workspace.scan(/aaaa/, ({filePath}) => resultPaths.push(filePath)) ) - return runs(() => expect(resultPaths.sort()).toEqual([file1, file2].sort())) + runs(() => expect(resultPaths.sort()).toEqual([file1, file2].sort())) }) - describe('when an inclusion path starts with the basename of a root directory', () => - it('interprets the inclusion path as starting from that directory', function () { - waitsForPromise(function () { + describe('when an inclusion path starts with the basename of a root directory', () => { + it('interprets the inclusion path as starting from that directory', () => { + waitsForPromise(() => { const resultPaths = [] return atom.workspace - .scan(/aaaa/, {paths: ['dir']}, function ({filePath}) { - if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath) } + .scan(/aaaa/, {paths: ['dir']}, ({filePath}) => { + if (!resultPaths.includes(filePath)) { + resultPaths.push(filePath) + } }) .then(() => expect(resultPaths).toEqual([file1])) }) - waitsForPromise(function () { + waitsForPromise(() => { const resultPaths = [] return atom.workspace - .scan(/aaaa/, {paths: [path.join('dir', 'a-dir')]}, function ({filePath}) { - if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath) } + .scan(/aaaa/, {paths: [path.join('dir', 'a-dir')]}, ({filePath}) => { + if (!resultPaths.includes(filePath)) { + resultPaths.push(filePath) + } }) .then(() => expect(resultPaths).toEqual([file1])) }) - waitsForPromise(function () { + waitsForPromise(() => { const resultPaths = [] return atom.workspace - .scan(/aaaa/, {paths: [path.basename(dir2)]}, function ({filePath}) { - if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath) } + .scan(/aaaa/, {paths: [path.basename(dir2)]}, ({filePath}) => { + if (!resultPaths.includes(filePath)) { + resultPaths.push(filePath) + } }) .then(() => expect(resultPaths).toEqual([file2])) }) - return waitsForPromise(function () { + waitsForPromise(() => { const resultPaths = [] return atom.workspace - .scan(/aaaa/, {paths: [path.join(path.basename(dir2), 'a-dir')]}, function ({filePath}) { - if (!Array.from(resultPaths).includes(filePath)) { return resultPaths.push(filePath) } + .scan(/aaaa/, {paths: [path.join(path.basename(dir2), 'a-dir')]}, ({filePath}) => { + if (!resultPaths.includes(filePath)) { + resultPaths.push(filePath) + } }) .then(() => expect(resultPaths).toEqual([file2])) }) }) - ) + }) - return describe('when a custom directory searcher is registered', function () { + describe('when a custom directory searcher is registered', () => { let fakeSearch = null // Function that is invoked once all of the fields on fakeSearch are set. let onFakeSearchCreated = null @@ -1588,11 +1628,13 @@ i = /test/; #FIXME\ constructor (options) { // Note that hoisting resolve and reject in this way is generally frowned upon. this.options = options - this.promise = new Promise((function (resolve, reject) { + this.promise = new Promise((resolve, reject) => { this.hoistedResolve = resolve this.hoistedReject = reject - return (typeof onFakeSearchCreated === 'function' ? onFakeSearchCreated(this) : undefined) - }.bind(this))) + if (typeof onFakeSearchCreated === 'function') { + onFakeSearchCreated(this) + } + }) } then (...args) { return this.promise.then.apply(this.promise, args) @@ -1601,22 +1643,25 @@ i = /test/; #FIXME\ this.cancelled = true // According to the spec for a DirectorySearcher, invoking `cancel()` should // resolve the thenable rather than reject it. - return this.hoistedResolve() + this.hoistedResolve() } } - beforeEach(function () { + beforeEach(() => { fakeSearch = null onFakeSearchCreated = null atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { canSearchDirectory (directory) { return directory.getPath() === dir1 }, - search (directory, regex, options) { return (fakeSearch = new FakeSearch(options)) } + search (directory, regex, options) { + fakeSearch = new FakeSearch(options) + return fakeSearch + } }) - return waitsFor(() => atom.workspace.directorySearchers.length > 0) + waitsFor(() => atom.workspace.directorySearchers.length > 0) }) - it('can override the DefaultDirectorySearcher on a per-directory basis', function () { + it('can override the DefaultDirectorySearcher on a per-directory basis', () => { const foreignFilePath = 'ssh://foreign-directory:8080/hello.txt' const numPathsSearchedInDir2 = 1 const numPathsToPretendToSearchInCustomDirectorySearcher = 10 @@ -1631,10 +1676,10 @@ i = /test/; #FIXME\ } ] } - onFakeSearchCreated = function (fakeSearch) { + onFakeSearchCreated = fakeSearch => { fakeSearch.options.didMatch(searchResult) fakeSearch.options.didSearchPaths(numPathsToPretendToSearchInCustomDirectorySearcher) - return fakeSearch.hoistedResolve() + fakeSearch.hoistedResolve() } const resultPaths = [] @@ -1643,82 +1688,88 @@ i = /test/; #FIXME\ atom.workspace.scan(/aaaa/, {onPathsSearched}, ({filePath}) => resultPaths.push(filePath)) ) - return runs(function () { + runs(() => { expect(resultPaths.sort()).toEqual([foreignFilePath, file2].sort()) // onPathsSearched should be called once by each DirectorySearcher. The order is not // guaranteed, so we can only verify the total number of paths searched is correct // after the second call. expect(onPathsSearched.callCount).toBe(2) - return expect(onPathsSearched.mostRecentCall.args[0]).toBe( + expect(onPathsSearched.mostRecentCall.args[0]).toBe( numPathsToPretendToSearchInCustomDirectorySearcher + numPathsSearchedInDir2) }) }) - it('can be cancelled when the object returned by scan() has its cancel() method invoked', function () { - const thenable = atom.workspace.scan(/aaaa/, function () {}) + it('can be cancelled when the object returned by scan() has its cancel() method invoked', () => { + const thenable = atom.workspace.scan(/aaaa/, () => {}) let resultOfPromiseSearch = null waitsFor('fakeSearch to be defined', () => fakeSearch != null) - runs(function () { + runs(() => { expect(fakeSearch.cancelled).toBe(undefined) thenable.cancel() - return expect(fakeSearch.cancelled).toBe(true) + expect(fakeSearch.cancelled).toBe(true) }) - waitsForPromise(() => thenable.then(promiseResult => (resultOfPromiseSearch = promiseResult))) + waitsForPromise(() => thenable.then(promiseResult => { resultOfPromiseSearch = promiseResult })) - return runs(() => expect(resultOfPromiseSearch).toBe('cancelled')) + runs(() => expect(resultOfPromiseSearch).toBe('cancelled')) }) - return it('will have the side-effect of failing the overall search if it fails', function () { + it('will have the side-effect of failing the overall search if it fails', () => { // This provider's search should be cancelled when the first provider fails let cancelableSearch let fakeSearch2 = null atom.packages.serviceHub.provide('atom.directory-searcher', '0.1.0', { canSearchDirectory (directory) { return directory.getPath() === dir2 }, - search (directory, regex, options) { return (fakeSearch2 = new FakeSearch(options)) } + search (directory, regex, options) { + fakeSearch2 = new FakeSearch(options) + return fakeSearch2 + } }) let didReject = false - const promise = cancelableSearch = atom.workspace.scan(/aaaa/, function () {}) + const promise = cancelableSearch = atom.workspace.scan(/aaaa/, () => {}) waitsFor('fakeSearch to be defined', () => fakeSearch != null) runs(() => fakeSearch.hoistedReject()) - waitsForPromise(() => cancelableSearch.catch(() => (didReject = true))) + waitsForPromise(() => cancelableSearch.catch(() => { didReject = true })) waitsFor(done => promise.then(null, done)) - return runs(function () { + runs(() => { expect(didReject).toBe(true) - return expect(fakeSearch2.cancelled).toBe(true) + expect(fakeSearch2.cancelled).toBe(true) }) }) }) }) }) - ) // Cancels other ongoing searches + }) // Cancels other ongoing searches - describe('::replace(regex, replacementText, paths, iterator)', function () { - let [filePath, commentFilePath, sampleContent, sampleCommentContent] = Array.from([]) + describe('::replace(regex, replacementText, paths, iterator)', () => { + let filePath + let commentFilePath + let sampleContent + let sampleCommentContent - beforeEach(function () { - atom.project.setPaths([__guard__(atom.project.getDirectories()[0], x => x.resolve('../'))]) + beforeEach(() => { + atom.project.setPaths([atom.project.getDirectories()[0].resolve('../')]) - filePath = __guard__(atom.project.getDirectories()[0], x1 => x1.resolve('sample.js')) - commentFilePath = __guard__(atom.project.getDirectories()[0], x2 => x2.resolve('sample-with-comments.js')) + filePath = atom.project.getDirectories()[0].resolve('sample.js') + commentFilePath = atom.project.getDirectories()[0].resolve('sample-with-comments.js') sampleContent = fs.readFileSync(filePath).toString() - return (sampleCommentContent = fs.readFileSync(commentFilePath).toString()) + sampleCommentContent = fs.readFileSync(commentFilePath).toString() }) - afterEach(function () { + afterEach(() => { fs.writeFileSync(filePath, sampleContent) - return fs.writeFileSync(commentFilePath, sampleCommentContent) + fs.writeFileSync(commentFilePath, sampleCommentContent) }) - describe("when a file doesn't exist", () => - it('calls back with an error', function () { + describe("when a file doesn't exist", () => { + it('calls back with an error', () => { const errors = [] const missingPath = path.resolve('/not-a-file.js') expect(fs.existsSync(missingPath)).toBeFalsy() @@ -1727,34 +1778,34 @@ i = /test/; #FIXME\ atom.workspace.replace(/items/gi, 'items', [missingPath], (result, error) => errors.push(error)) ) - return runs(function () { + runs(() => { expect(errors).toHaveLength(1) - return expect(errors[0].path).toBe(missingPath) + expect(errors[0].path).toBe(missingPath) }) }) - ) + }) - describe('when called with unopened files', () => - it('replaces properly', function () { + describe('when called with unopened files', () => { + it('replaces properly', () => { const results = [] waitsForPromise(() => atom.workspace.replace(/items/gi, 'items', [filePath], result => results.push(result)) ) - return runs(function () { + runs(() => { expect(results).toHaveLength(1) expect(results[0].filePath).toBe(filePath) - return expect(results[0].replacements).toBe(6) + expect(results[0].replacements).toBe(6) }) }) - ) + }) - return describe('when a buffer is already open', function () { - it('replaces properly and saves when not modified', function () { + describe('when a buffer is already open', () => { + it('replaces properly and saves when not modified', () => { let editor = null const results = [] - waitsForPromise(() => atom.workspace.open('sample.js').then(o => (editor = o))) + waitsForPromise(() => atom.workspace.open('sample.js').then(o => { editor = o })) runs(() => expect(editor.isModified()).toBeFalsy()) @@ -1762,16 +1813,16 @@ i = /test/; #FIXME\ atom.workspace.replace(/items/gi, 'items', [filePath], result => results.push(result)) ) - return runs(function () { + runs(() => { expect(results).toHaveLength(1) expect(results[0].filePath).toBe(filePath) expect(results[0].replacements).toBe(6) - return expect(editor.isModified()).toBeFalsy() + expect(editor.isModified()).toBeFalsy() }) }) - it('does not replace when the path is not specified', function () { + it('does not replace when the path is not specified', () => { const results = [] waitsForPromise(() => atom.workspace.open('sample-with-comments.js')) @@ -1780,72 +1831,72 @@ i = /test/; #FIXME\ atom.workspace.replace(/items/gi, 'items', [commentFilePath], result => results.push(result)) ) - return runs(function () { + runs(() => { expect(results).toHaveLength(1) - return expect(results[0].filePath).toBe(commentFilePath) + expect(results[0].filePath).toBe(commentFilePath) }) }) - return it('does NOT save when modified', function () { + it('does NOT save when modified', () => { let editor = null const results = [] - waitsForPromise(() => atom.workspace.open('sample.js').then(o => (editor = o))) + waitsForPromise(() => atom.workspace.open('sample.js').then(o => { editor = o })) - runs(function () { + runs(() => { editor.buffer.setTextInRange([[0, 0], [0, 0]], 'omg') - return expect(editor.isModified()).toBeTruthy() + expect(editor.isModified()).toBeTruthy() }) waitsForPromise(() => atom.workspace.replace(/items/gi, 'okthen', [filePath], result => results.push(result)) ) - return runs(function () { + runs(() => { expect(results).toHaveLength(1) expect(results[0].filePath).toBe(filePath) expect(results[0].replacements).toBe(6) - return expect(editor.isModified()).toBeTruthy() + expect(editor.isModified()).toBeTruthy() }) }) }) }) - describe('::saveActivePaneItem()', function () { + describe('::saveActivePaneItem()', () => { let editor = null beforeEach(() => - waitsForPromise(() => atom.workspace.open('sample.js').then(o => (editor = o))) + waitsForPromise(() => atom.workspace.open('sample.js').then(o => { editor = o })) ) - return describe('when there is an error', function () { - it('emits a warning notification when the file cannot be saved', function () { + describe('when there is an error', () => { + it('emits a warning notification when the file cannot be saved', () => { let addedSpy - spyOn(editor, 'save').andCallFake(function () { + spyOn(editor, 'save').andCallFake(() => { throw new Error("'/some/file' is a directory") }) atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) atom.workspace.saveActivePaneItem() expect(addedSpy).toHaveBeenCalled() - return expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') + expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') }) - it('emits a warning notification when the directory cannot be written to', function () { + it('emits a warning notification when the directory cannot be written to', () => { let addedSpy - spyOn(editor, 'save').andCallFake(function () { + spyOn(editor, 'save').andCallFake(() => { throw new Error("ENOTDIR, not a directory '/Some/dir/and-a-file.js'") }) atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) atom.workspace.saveActivePaneItem() expect(addedSpy).toHaveBeenCalled() - return expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') + expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') }) - it('emits a warning notification when the user does not have permission', function () { + it('emits a warning notification when the user does not have permission', () => { let addedSpy - spyOn(editor, 'save').andCallFake(function () { + spyOn(editor, 'save').andCallFake(() => { const error = new Error("EACCES, permission denied '/Some/dir/and-a-file.js'") error.code = 'EACCES' error.path = '/Some/dir/and-a-file.js' @@ -1855,21 +1906,21 @@ i = /test/; #FIXME\ atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) atom.workspace.saveActivePaneItem() expect(addedSpy).toHaveBeenCalled() - return expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') + expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') }) - it('emits a warning notification when the operation is not permitted', () => - spyOn(editor, 'save').andCallFake(function () { + it('emits a warning notification when the operation is not permitted', () => { + spyOn(editor, 'save').andCallFake(() => { const error = new Error("EPERM, operation not permitted '/Some/dir/and-a-file.js'") error.code = 'EPERM' error.path = '/Some/dir/and-a-file.js' throw error }) - ) + }) - it('emits a warning notification when the file is already open by another app', function () { + it('emits a warning notification when the file is already open by another app', () => { let addedSpy - spyOn(editor, 'save').andCallFake(function () { + spyOn(editor, 'save').andCallFake(() => { const error = new Error("EBUSY, resource busy or locked '/Some/dir/and-a-file.js'") error.code = 'EBUSY' error.path = '/Some/dir/and-a-file.js' @@ -1882,12 +1933,12 @@ i = /test/; #FIXME\ const notificaiton = addedSpy.mostRecentCall.args[0] expect(notificaiton.getType()).toBe('warning') - return expect(notificaiton.getMessage()).toContain('Unable to save') + expect(notificaiton.getMessage()).toContain('Unable to save') }) - it('emits a warning notification when the file system is read-only', function () { + it('emits a warning notification when the file system is read-only', () => { let addedSpy - spyOn(editor, 'save').andCallFake(function () { + spyOn(editor, 'save').andCallFake(() => { const error = new Error("EROFS, read-only file system '/Some/dir/and-a-file.js'") error.code = 'EROFS' error.path = '/Some/dir/and-a-file.js' @@ -1900,27 +1951,27 @@ i = /test/; #FIXME\ const notification = addedSpy.mostRecentCall.args[0] expect(notification.getType()).toBe('warning') - return expect(notification.getMessage()).toContain('Unable to save') + expect(notification.getMessage()).toContain('Unable to save') }) - return it('emits a warning notification when the file cannot be saved', function () { - spyOn(editor, 'save').andCallFake(function () { + it('emits a warning notification when the file cannot be saved', () => { + spyOn(editor, 'save').andCallFake(() => { throw new Error('no one knows') }) const save = () => atom.workspace.saveActivePaneItem() - return expect(save).toThrow() + expect(save).toThrow() }) }) }) - describe('::closeActivePaneItemOrEmptyPaneOrWindow', function () { - beforeEach(function () { + describe('::closeActivePaneItemOrEmptyPaneOrWindow', () => { + beforeEach(() => { spyOn(atom, 'close') - return waitsForPromise(() => atom.workspace.open()) + waitsForPromise(() => atom.workspace.open()) }) - return it('closes the active pane item, or the active pane if it is empty, or the current window if there is only the empty root pane', function () { + it('closes the active pane item, or the active pane if it is empty, or the current window if there is only the empty root pane', () => { atom.config.set('core.destroyEmptyPanes', false) const pane1 = atom.workspace.getActivePane() @@ -1946,25 +1997,27 @@ i = /test/; #FIXME\ expect(atom.workspace.getPanes().length).toBe(1) atom.workspace.closeActivePaneItemOrEmptyPaneOrWindow() - return expect(atom.close).toHaveBeenCalled() + expect(atom.close).toHaveBeenCalled() }) }) - describe('when the core.allowPendingPaneItems option is falsey', () => - it('does not open item with `pending: true` option as pending', function () { + describe('when the core.allowPendingPaneItems option is falsey', () => { + it('does not open item with `pending: true` option as pending', () => { let pane = null atom.config.set('core.allowPendingPaneItems', false) waitsForPromise(() => - atom.workspace.open('sample.js', {pending: true}).then(() => (pane = atom.workspace.getActivePane())) + atom.workspace.open('sample.js', {pending: true}).then(() => { + pane = atom.workspace.getActivePane() + }) ) - return runs(() => expect(pane.getPendingItem()).toBeFalsy()) + runs(() => expect(pane.getPendingItem()).toBeFalsy()) }) - ) + }) - describe('grammar activation', () => - it('notifies the workspace of which grammar is used', function () { + describe('grammar activation', () => { + it('notifies the workspace of which grammar is used', () => { atom.packages.triggerDeferredActivationHooks() const javascriptGrammarUsed = jasmine.createSpy('js grammar used') @@ -1980,7 +2033,7 @@ i = /test/; #FIXME\ waitsForPromise(() => atom.packages.activatePackage('language-c')) waitsForPromise(() => atom.workspace.open('sample-with-comments.js')) - return runs(function () { + runs(() => { // Hooks are triggered when opening new editors expect(javascriptGrammarUsed).toHaveBeenCalled() @@ -1991,43 +2044,39 @@ i = /test/; #FIXME\ // Hooks are triggered when editors are added in other ways. atom.workspace.getActivePane().splitRight({copyActiveItem: true}) atom.workspace.getActiveTextEditor().setGrammar(atom.grammars.grammarForScopeName('source.ruby')) - return expect(rubyGrammarUsed).toHaveBeenCalled() + expect(rubyGrammarUsed).toHaveBeenCalled() }) }) - ) + }) - describe('.checkoutHeadRevision()', function () { + describe('.checkoutHeadRevision()', () => { let editor = null - beforeEach(function () { + beforeEach(() => { atom.config.set('editor.confirmCheckoutHeadRevision', false) - return waitsForPromise(() => atom.workspace.open('sample-with-comments.js').then(o => (editor = o))) + waitsForPromise(() => atom.workspace.open('sample-with-comments.js').then(o => { editor = o })) }) - it('reverts to the version of its file checked into the project repository', function () { + it('reverts to the version of its file checked into the project repository', () => { editor.setCursorBufferPosition([0, 0]) editor.insertText('---\n') expect(editor.lineTextForBufferRow(0)).toBe('---') waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor)) - return runs(() => expect(editor.lineTextForBufferRow(0)).toBe('')) + runs(() => expect(editor.lineTextForBufferRow(0)).toBe('')) }) - return describe("when there's no repository for the editor's file", () => - it("doesn't do anything", function () { + describe("when there's no repository for the editor's file", () => { + it("doesn't do anything", () => { editor = new TextEditor() editor.setText('stuff') atom.workspace.checkoutHeadRevision(editor) - return waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor)) + waitsForPromise(() => atom.workspace.checkoutHeadRevision(editor)) }) - ) + }) }) - - return (escapeStringRegex = str => str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')) }) -function __guard__ (value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined -} +const escapeStringRegex = str => str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') diff --git a/src/workspace.js b/src/workspace.js index 7ffe8f387..7282ed419 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -1,3 +1,5 @@ +'use strict' + const _ = require('underscore-plus') const url = require('url') const path = require('path') @@ -29,17 +31,27 @@ module.exports = class Workspace extends Model { this.updateDocumentEdited = this.updateDocumentEdited.bind(this) this.didDestroyPaneItem = this.didDestroyPaneItem.bind(this) - ;({ - packageManager: this.packageManager, config: this.config, project: this.project, grammarRegistry: this.grammarRegistry, notificationManager: this.notificationManager, - viewRegistry: this.viewRegistry, grammarRegistry: this.grammarRegistry, applicationDelegate: this.applicationDelegate, assert: this.assert, - deserializerManager: this.deserializerManager, textEditorRegistry: this.textEditorRegistry - } = params) + this.packageManager = params.packageManager + this.config = params.config + this.project = params.project + this.notificationManager = params.notificationManager + this.viewRegistry = params.viewRegistry + this.grammarRegistry = params.grammarRegistry + this.applicationDelegate = params.applicationDelegate + this.assert = params.assert + this.deserializerManager = params.deserializerManager + this.textEditorRegistry = params.textEditorRegistry this.emitter = new Emitter() this.openers = [] this.destroyedItemURIs = [] - this.paneContainer = new PaneContainer({config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, deserializerManager: this.deserializerManager}) + this.paneContainer = new PaneContainer({ + config: this.config, + applicationDelegate: this.applicationDelegate, + notificationManager: this.notificationManager, + deserializerManager: this.deserializerManager + }) this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem) this.defaultDirectorySearcher = new DefaultDirectorySearcher() @@ -49,7 +61,7 @@ module.exports = class Workspace extends Model { // Etch, in which case it'd be `new`d. And when it's `new`d, `this` is always // the newly created object. const realThis = this - this.buildTextEditor = function () { return Workspace.prototype.buildTextEditor.apply(realThis, arguments) } + this.buildTextEditor = () => Workspace.prototype.buildTextEditor.apply(realThis, arguments) this.panelContainers = { top: new PanelContainer({location: 'top'}), @@ -70,9 +82,14 @@ module.exports = class Workspace extends Model { this.emitter = new Emitter() this.paneContainer.destroy() - for (let panelContainer of this.panelContainers) { panelContainer.destroy() } + _.values(this.panelContainers).forEach(panelContainer => { panelContainer.destroy() }) - this.paneContainer = new PaneContainer({config: this.config, applicationDelegate: this.applicationDelegate, notificationManager: this.notificationManager, deserializerManager: this.deserializerManager}) + this.paneContainer = new PaneContainer({ + config: this.config, + applicationDelegate: this.applicationDelegate, + notificationManager: this.notificationManager, + deserializerManager: this.deserializerManager + }) this.paneContainer.onDidDestroyPaneItem(this.didDestroyPaneItem) this.panelContainers = { @@ -88,21 +105,22 @@ module.exports = class Workspace extends Model { this.originalFontSize = null this.openers = [] this.destroyedItemURIs = [] - return this.consumeServices(this.packageManager) + this.consumeServices(this.packageManager) } subscribeToEvents () { this.subscribeToActiveItem() this.subscribeToFontSize() - return this.subscribeToAddedItems() + this.subscribeToAddedItems() } consumeServices ({serviceHub}) { this.directorySearchers = [] - return serviceHub.consume( + serviceHub.consume( 'atom.directory-searcher', '^0.1.0', - provider => this.directorySearchers.unshift(provider)) + provider => this.directorySearchers.unshift(provider) + ) } // Called by the Serializable mixin during serialization. @@ -116,8 +134,13 @@ module.exports = class Workspace extends Model { } deserialize (state, deserializerManager) { - for (let packageName of state.packagesWithActiveGrammars != null ? state.packagesWithActiveGrammars : []) { - __guard__(this.packageManager.getLoadedPackage(packageName), x => x.loadGrammarsSync()) + const packagesWithActiveGrammars = + state.packagesWithActiveGrammars != null ? state.packagesWithActiveGrammars : [] + for (let packageName of packagesWithActiveGrammars) { + const pkg = this.packageManager.getLoadedPackage(packageName) + if (pkg != null) { + pkg.loadGrammarsSync() + } } if (state.destroyedItemURIs != null) { this.destroyedItemURIs = state.destroyedItemURIs @@ -127,7 +150,7 @@ module.exports = class Workspace extends Model { getPackageNamesWithActiveGrammars () { const packageNames = [] - var addGrammar = ({includedGrammarScopes, packageName} = {}) => { + const addGrammar = ({includedGrammarScopes, packageName} = {}) => { if (!packageName) { return } // Prevent cycles if (packageNames.indexOf(packageName) !== -1) { return } @@ -157,8 +180,7 @@ module.exports = class Workspace extends Model { this.updateDocumentEdited() this.project.onDidChangePaths(this.updateWindowTitle) - return this.observeActivePaneItem(item => { - let modifiedSubscription, titleSubscription + this.observeActivePaneItem(item => { this.updateWindowTitle() this.updateDocumentEdited() @@ -167,32 +189,37 @@ module.exports = class Workspace extends Model { } this.activeItemSubscriptions = new CompositeDisposable() - if (typeof (item != null ? item.onDidChangeTitle : undefined) === 'function') { + let modifiedSubscription, titleSubscription + + if (item != null && typeof item.onDidChangeTitle === 'function') { titleSubscription = item.onDidChangeTitle(this.updateWindowTitle) - } else if (typeof (item != null ? item.on : undefined) === 'function') { + } else if (item != null && typeof item.on === 'function') { titleSubscription = item.on('title-changed', this.updateWindowTitle) - if (typeof (titleSubscription != null ? titleSubscription.dispose : undefined) !== 'function') { - titleSubscription = new Disposable((function () { return item.off('title-changed', this.updateWindowTitle) }.bind(this))) + if (titleSubscription == null || typeof titleSubscription.dispose !== 'function') { + titleSubscription = new Disposable(() => { + item.off('title-changed', this.updateWindowTitle) + }) } } - if (typeof (item != null ? item.onDidChangeModified : undefined) === 'function') { + if (item != null && typeof item.onDidChangeModified === 'function') { modifiedSubscription = item.onDidChangeModified(this.updateDocumentEdited) - } else if (typeof ((item != null ? item.on : undefined) != null) === 'function') { + } else if (item != null && typeof item.on === 'function') { modifiedSubscription = item.on('modified-status-changed', this.updateDocumentEdited) - if (typeof (modifiedSubscription != null ? modifiedSubscription.dispose : undefined) !== 'function') { - modifiedSubscription = new Disposable((function () { return item.off('modified-status-changed', this.updateDocumentEdited) }.bind(this))) + if (modifiedSubscription == null || typeof modifiedSubscription.dispose !== 'function') { + modifiedSubscription = new Disposable(() => { + item.off('modified-status-changed', this.updateDocumentEdited) + }) } } if (titleSubscription != null) { this.activeItemSubscriptions.add(titleSubscription) } - if (modifiedSubscription != null) { return this.activeItemSubscriptions.add(modifiedSubscription) } - } - ) + if (modifiedSubscription != null) { this.activeItemSubscriptions.add(modifiedSubscription) } + }) } subscribeToAddedItems () { - return this.onDidAddPaneItem(({item, pane, index}) => { + this.onDidAddPaneItem(({item, pane, index}) => { if (item instanceof TextEditor) { const subscriptions = new CompositeDisposable( this.textEditorRegistry.add(item), @@ -200,8 +227,8 @@ module.exports = class Workspace extends Model { this.textEditorRegistry.maintainConfig(item), item.observeGrammar(this.handleGrammarUsed.bind(this)) ) - item.onDidDestroy(() => subscriptions.dispose()) - return this.emitter.emit('did-add-text-editor', {textEditor: item, pane, index}) + item.onDidDestroy(() => { subscriptions.dispose() }) + this.emitter.emit('did-add-text-editor', {textEditor: item, pane, index}) } }) } @@ -209,14 +236,22 @@ module.exports = class Workspace extends Model { // Updates the application's title and proxy icon based on whichever file is // open. updateWindowTitle () { - let item, itemPath, itemTitle, left, projectPath, representedPath + let itemPath, itemTitle, projectPath, representedPath const appName = 'Atom' - const projectPaths = (left = this.project.getPaths()) != null ? left : [] - if ((item = this.getActivePaneItem())) { - let left1 + const left = this.project.getPaths() + const projectPaths = left != null ? left : [] + const item = this.getActivePaneItem() + if (item) { itemPath = typeof item.getPath === 'function' ? item.getPath() : undefined - itemTitle = (left1 = (typeof item.getLongTitle === 'function' ? item.getLongTitle() : undefined)) != null ? left1 : (typeof item.getTitle === 'function' ? item.getTitle() : undefined) - projectPath = _.find(projectPaths, projectPath => (itemPath === projectPath) || (itemPath != null ? itemPath.startsWith(projectPath + path.sep) : undefined)) + const longTitle = typeof item.getLongTitle === 'function' ? item.getLongTitle() : undefined + itemTitle = longTitle == null + ? (typeof item.getTitle === 'function' ? item.getTitle() : undefined) + : longTitle + projectPath = _.find( + projectPaths, + projectPath => + (itemPath === projectPath) || (itemPath != null ? itemPath.startsWith(projectPath + path.sep) : undefined) + ) } if (itemTitle == null) { itemTitle = 'untitled' } if (projectPath == null) { projectPath = itemPath ? path.dirname(itemPath) : projectPaths[0] } @@ -241,15 +276,17 @@ module.exports = class Workspace extends Model { } document.title = titleParts.join(' \u2014 ') - return this.applicationDelegate.setRepresentedFilename(representedPath) + this.applicationDelegate.setRepresentedFilename(representedPath) } // On macOS, fades the application window's proxy icon when the current file // has been modified. updateDocumentEdited () { - let left - const modified = (left = __guardMethod__(this.getActivePaneItem(), 'isModified', o => o.isModified())) != null ? left : false - return this.applicationDelegate.setWindowDocumentEdited(modified) + const activePaneItem = this.getActivePaneItem() + const modified = activePaneItem != null && typeof activePaneItem.isModified === 'function' + ? activePaneItem.isModified() || false + : false + this.applicationDelegate.setWindowDocumentEdited(modified) } /* @@ -479,11 +516,10 @@ module.exports = class Workspace extends Model { // an existing item for the same URI. Defaults to `false`. // // Returns a {Promise} that resolves to the {TextEditor} for the file URI. - open (uri, options = {}) { - let pane + open (uri_, options = {}) { const { searchAllPanes } = options const { split } = options - uri = this.project.resolvePath(uri) + const uri = this.project.resolvePath(uri_) if (!atom.config.get('core.allowPendingPaneItems')) { options.pending = false @@ -495,22 +531,26 @@ module.exports = class Workspace extends Model { this.applicationDelegate.addRecentDocument(uri) } + let pane if (searchAllPanes) { pane = this.paneContainer.paneForURI(uri) } if (pane == null) { - pane = (() => { - switch (split) { - case 'left': - return this.getActivePane().findLeftmostSibling() - case 'right': - return this.getActivePane().findOrCreateRightmostSibling() - case 'up': - return this.getActivePane().findTopmostSibling() - case 'down': - return this.getActivePane().findOrCreateBottommostSibling() - default: - return this.getActivePane() - } - })() + switch (split) { + case 'left': + pane = this.getActivePane().findLeftmostSibling() + break + case 'right': + pane = this.getActivePane().findOrCreateRightmostSibling() + break + case 'up': + pane = this.getActivePane().findTopmostSibling() + break + case 'down': + pane = this.getActivePane().findOrCreateBottommostSibling() + break + default: + pane = this.getActivePane() + break + } } return this.openURIInPane(uri, pane, options) @@ -535,38 +575,54 @@ module.exports = class Workspace extends Model { // the containing pane. Defaults to `true`. // * `activateItem` A {Boolean} indicating whether to call {Pane::activateItem} // on containing pane. Defaults to `true`. - openSync (uri = '', options = {}) { + openSync (uri_ = '', options = {}) { const {initialLine, initialColumn} = options const activatePane = options.activatePane != null ? options.activatePane : true const activateItem = options.activateItem != null ? options.activateItem : true - uri = this.project.resolvePath(uri) + const uri = this.project.resolvePath(uri) let item = this.getActivePane().itemForURI(uri) - if (uri) { - for (let opener of this.getOpeners()) { if (!item) { if (item == null) { item = opener(uri, options) } } } + if (uri && (item == null)) { + for (const opener of this.getOpeners()) { + item = opener(uri, options) + if (item) break + } + } + if (item == null) { + item = this.project.openSync(uri, {initialLine, initialColumn}) } - if (item == null) { item = this.project.openSync(uri, {initialLine, initialColumn}) } - if (activateItem) { this.getActivePane().activateItem(item) } + if (activateItem) { + this.getActivePane().activateItem(item) + } this.itemOpened(item) - if (activatePane) { this.getActivePane().activate() } + if (activatePane) { + this.getActivePane().activate() + } return item } openURIInPane (uri, pane, options = {}) { - let item const activatePane = options.activatePane != null ? options.activatePane : true const activateItem = options.activateItem != null ? options.activateItem : true + let item if (uri != null) { - if ((item = pane.itemForURI(uri))) { - if (!options.pending && (pane.getPendingItem() === item)) { pane.clearPendingItem() } + item = pane.itemForURI(uri) + if (item == null) { + for (let opener of this.getOpeners()) { + item = opener(uri, options) + if (item != null) break + } + } else if (!options.pending && (pane.getPendingItem() === item)) { + pane.clearPendingItem() } - for (let opener of this.getOpeners()) { if (!item) { if (item == null) { item = opener(uri, options) } } } } try { - if (item == null) { item = this.openTextFile(uri, options) } + if (item == null) { + item = this.openTextFile(uri, options) + } } catch (error) { switch (error.code) { case 'CANCELLED': @@ -574,8 +630,21 @@ module.exports = class Workspace extends Model { case 'EACCES': this.notificationManager.addWarning(`Permission denied '${error.path}'`) return Promise.resolve() - case 'EPERM': case 'EBUSY': case 'ENXIO': case 'EIO': case 'ENOTCONN': case 'UNKNOWN': case 'ECONNRESET': case 'EINVAL': case 'EMFILE': case 'ENOTDIR': case 'EAGAIN': - this.notificationManager.addWarning(`Unable to open '${error.path != null ? error.path : uri}'`, {detail: error.message}) + case 'EPERM': + case 'EBUSY': + case 'ENXIO': + case 'EIO': + case 'ENOTCONN': + case 'UNKNOWN': + case 'ECONNRESET': + case 'EINVAL': + case 'EMFILE': + case 'ENOTDIR': + case 'EAGAIN': + this.notificationManager.addWarning( + `Unable to open '${error.path != null ? error.path : uri}'`, + {detail: error.message} + ) return Promise.resolve() default: throw error @@ -585,18 +654,24 @@ module.exports = class Workspace extends Model { return Promise.resolve(item) .then(item => { let initialColumn - if (pane.isDestroyed()) { return item } + if (pane.isDestroyed()) { + return item + } this.itemOpened(item) - if (activateItem) { pane.activateItem(item, {pending: options.pending}) } - if (activatePane) { pane.activate() } + if (activateItem) { + pane.activateItem(item, {pending: options.pending}) + } + if (activatePane) { + pane.activate() + } let initialLine = initialColumn = 0 if (!Number.isNaN(options.initialLine)) { - ({ initialLine } = options) + initialLine = options.initialLine } if (!Number.isNaN(options.initialColumn)) { - ({ initialColumn } = options) + initialColumn = options.initialColumn } if ((initialLine >= 0) || (initialColumn >= 0)) { if (typeof item.setCursorBufferPosition === 'function') { @@ -619,7 +694,9 @@ module.exports = class Workspace extends Model { fs.closeSync(fs.openSync(filePath, 'r')) } catch (error) { // allow ENOENT errors to create an editor for paths that dont exist - if (error.code !== 'ENOENT') { throw error } + if (error.code !== 'ENOENT') { + throw error + } } } @@ -630,7 +707,8 @@ module.exports = class Workspace extends Model { const choice = this.applicationDelegate.confirm({ message: 'Atom will be unresponsive during the loading of very large files.', detailedMessage: 'Do you still want to load this file?', - buttons: ['Proceed', 'Cancel']}) + buttons: ['Proceed', 'Cancel'] + }) if (choice === 1) { const error = new Error() error.code = 'CANCELLED' @@ -638,15 +716,14 @@ module.exports = class Workspace extends Model { } } - return this.project.bufferForPath(filePath, options).then(buffer => { - return this.textEditorRegistry.build(Object.assign({buffer, largeFileMode, autoHeight: false}, options)) - } - ) + return this.project.bufferForPath(filePath, options) + .then(buffer => { + return this.textEditorRegistry.build(Object.assign({buffer, largeFileMode, autoHeight: false}, options)) + }) } handleGrammarUsed (grammar) { if (grammar == null) { return } - return this.packageManager.triggerActivationHook(`${grammar.packageName}:grammar-used`) } @@ -666,7 +743,7 @@ module.exports = class Workspace extends Model { this.textEditorRegistry.maintainGrammar(editor), this.textEditorRegistry.maintainConfig(editor) ) - editor.onDidDestroy(() => subscriptions.dispose()) + editor.onDidDestroy(() => { subscriptions.dispose() }) return editor } @@ -675,8 +752,8 @@ module.exports = class Workspace extends Model { // // Returns a {Promise} that is resolved when the item is opened reopenItem () { - let uri - if ((uri = this.destroyedItemURIs.pop())) { + const uri = this.destroyedItemURIs.pop() + if (uri) { return this.open(uri) } else { return Promise.resolve() @@ -714,7 +791,7 @@ module.exports = class Workspace extends Model { // can check the protocol for quux-preview and only handle those URIs that match. addOpener (opener) { this.openers.push(opener) - return new Disposable((function () { return _.remove(this.openers, opener) }.bind(this))) + return new Disposable(() => { _.remove(this.openers, opener) }) } getOpeners () { @@ -839,44 +916,50 @@ module.exports = class Workspace extends Model { // Destroy (close) the active pane. destroyActivePane () { - return __guard__(this.getActivePane(), x => x.destroy()) + const activePane = this.getActivePane() + if (activePane != null) { + activePane.destroy() + } } // Close the active pane item, or the active pane if it is empty, // or the current window if there is only the empty root pane. closeActivePaneItemOrEmptyPaneOrWindow () { if (this.getActivePaneItem() != null) { - return this.destroyActivePaneItem() + this.destroyActivePaneItem() } else if (this.getPanes().length > 1) { - return this.destroyActivePane() + this.destroyActivePane() } else if (this.config.get('core.closeEmptyWindows')) { - return atom.close() + atom.close() } } // Increase the editor font size by 1px. increaseFontSize () { - return this.config.set('editor.fontSize', this.config.get('editor.fontSize') + 1) + this.config.set('editor.fontSize', this.config.get('editor.fontSize') + 1) } // Decrease the editor font size by 1px. decreaseFontSize () { const fontSize = this.config.get('editor.fontSize') - if (fontSize > 1) { return this.config.set('editor.fontSize', fontSize - 1) } + if (fontSize > 1) { + this.config.set('editor.fontSize', fontSize - 1) + } } // Restore to the window's original editor font size. resetFontSize () { if (this.originalFontSize) { - return this.config.set('editor.fontSize', this.originalFontSize) + this.config.set('editor.fontSize', this.originalFontSize) } } subscribeToFontSize () { return this.config.onDidChange('editor.fontSize', ({oldValue}) => { - return this.originalFontSize != null ? this.originalFontSize : (this.originalFontSize = oldValue) - } - ) + if (this.originalFontSize == null) { + this.originalFontSize = oldValue + } + }) } // Removes the item's uri from the list of potential items to reopen. @@ -889,7 +972,7 @@ module.exports = class Workspace extends Model { } if (uri != null) { - return _.remove(this.destroyedItemURIs, uri) + _.remove(this.destroyedItemURIs, uri) } } @@ -903,14 +986,16 @@ module.exports = class Workspace extends Model { } if (uri != null) { - return this.destroyedItemURIs.push(uri) + this.destroyedItemURIs.push(uri) } } // Called by Model superclass when destroyed destroyed () { this.paneContainer.destroy() - return (this.activeItemSubscriptions != null ? this.activeItemSubscriptions.dispose() : undefined) + if (this.activeItemSubscriptions != null) { + this.activeItemSubscriptions.dispose() + } } /* @@ -1112,7 +1197,6 @@ module.exports = class Workspace extends Model { // Returns a {Promise} with a `cancel()` method that will cancel all // of the underlying searches that were started as part of this scan. scan (regex, options = {}, iterator) { - let directorySearcher, onPathsSearched if (_.isFunction(options)) { iterator = options options = {} @@ -1121,9 +1205,9 @@ module.exports = class Workspace extends Model { // Find a searcher for every Directory in the project. Each searcher that is matched // will be associated with an Array of Directory objects in the Map. const directoriesForSearcher = new Map() - for (let directory of this.project.getDirectories()) { + for (const directory of this.project.getDirectories()) { let searcher = this.defaultDirectorySearcher - for (directorySearcher of this.directorySearchers) { + for (const directorySearcher of this.directorySearchers) { if (directorySearcher.canSearchDirectory(directory)) { searcher = directorySearcher break @@ -1138,6 +1222,7 @@ module.exports = class Workspace extends Model { } // Define the onPathsSearched callback. + let onPathsSearched if (_.isFunction(options.onPathsSearched)) { // Maintain a map of directories to the number of search results. When notified of a new count, // replace the entry in the map and update the total. @@ -1167,26 +1252,33 @@ module.exports = class Workspace extends Model { exclusions: this.config.get('core.ignoredNames'), follow: this.config.get('core.followSymlinks'), didMatch: result => { - if (!this.project.isPathModified(result.filePath)) { return iterator(result) } + if (!this.project.isPathModified(result.filePath)) { + return iterator(result) + } }, didError (error) { return iterator(null, error) }, - didSearchPaths (count) { return onPathsSearched(searcher, count) } + didSearchPaths (count) { + return onPathsSearched(searcher, count) + } } - directorySearcher = searcher.search(directories, regex, searchOptions) - return allSearches.push(directorySearcher) - } - ) + const directorySearcher = searcher.search(directories, regex, searchOptions) + allSearches.push(directorySearcher) + }) const searchPromise = Promise.all(allSearches) for (let buffer of this.project.getBuffers()) { if (buffer.isModified()) { const filePath = buffer.getPath() - if (!this.project.contains(filePath)) { continue } + if (!this.project.contains(filePath)) { + continue + } var matches = [] buffer.scan(regex, match => matches.push(match)) - if (matches.length > 0) { iterator({filePath, matches}) } + if (matches.length > 0) { + iterator({filePath, matches}) + } } } @@ -1195,34 +1287,36 @@ module.exports = class Workspace extends Model { // resolve it with the special value 'cancelled'. At least the built-in find-and-replace // package relies on this behavior. let isCancelled = false - const cancellablePromise = new Promise(function (resolve, reject) { + const cancellablePromise = new Promise((resolve, reject) => { const onSuccess = function () { if (isCancelled) { - return resolve('cancelled') + resolve('cancelled') } else { - return resolve(null) + resolve(null) } } const onFailure = function () { for (let promise of allSearches) { promise.cancel() } - return reject() + reject() } - return searchPromise.then(onSuccess, onFailure) + searchPromise.then(onSuccess, onFailure) }) - cancellablePromise.cancel = function () { + cancellablePromise.cancel = () => { isCancelled = true // Note that cancelling all of the members of allSearches will cause all of the searches // to resolve, which causes searchPromise to resolve, which is ultimately what causes // cancellablePromise to resolve. - return allSearches.map((promise) => promise.cancel()) + allSearches.map((promise) => promise.cancel()) } // Although this method claims to return a `Promise`, the `ResultsPaneView.onSearch()` // method in the find-and-replace package expects the object returned by this method to have a // `done()` method. Include a done() method until find-and-replace can be updated. - cancellablePromise.done = onSuccessOrFailure => cancellablePromise.then(onSuccessOrFailure, onSuccessOrFailure) + cancellablePromise.done = onSuccessOrFailure => { + cancellablePromise.then(onSuccessOrFailure, onSuccessOrFailure) + } return cancellablePromise } @@ -1236,45 +1330,50 @@ module.exports = class Workspace extends Model { // // Returns a {Promise}. replace (regex, replacementText, filePaths, iterator) { - return new Promise((function (resolve, reject) { + return new Promise((resolve, reject) => { let buffer - const openPaths = ((() => { - const result = [] - for (buffer of this.project.getBuffers()) { - result.push(buffer.getPath()) - } - return result - })()) + const openPaths = this.project.getBuffers().map(buffer => buffer.getPath()) const outOfProcessPaths = _.difference(filePaths, openPaths) let inProcessFinished = !openPaths.length let outOfProcessFinished = !outOfProcessPaths.length - const checkFinished = function () { - if (outOfProcessFinished && inProcessFinished) { return resolve() } + const checkFinished = () => { + if (outOfProcessFinished && inProcessFinished) { + resolve() + } } if (!outOfProcessFinished.length) { let flags = 'g' if (regex.ignoreCase) { flags += 'i' } - const task = Task.once(require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, function () { - outOfProcessFinished = true - return checkFinished() - }) + const task = Task.once( + require.resolve('./replace-handler'), + outOfProcessPaths, + regex.source, + flags, + replacementText, + () => { + outOfProcessFinished = true + checkFinished() + } + ) task.on('replace:path-replaced', iterator) - task.on('replace:file-error', function (error) { return iterator(null, error) }) + task.on('replace:file-error', error => { iterator(null, error) }) } for (buffer of this.project.getBuffers()) { - if (!Array.from(filePaths).includes(buffer.getPath())) { continue } + if (!filePaths.includes(buffer.getPath())) { continue } const replacements = buffer.replace(regex, replacementText, iterator) - if (replacements) { iterator({filePath: buffer.getPath(), replacements}) } + if (replacements) { + iterator({filePath: buffer.getPath(), replacements}) + } } inProcessFinished = true - return checkFinished() - }.bind(this))) + checkFinished() + }) } checkoutHeadRevision (editor) { @@ -1285,7 +1384,7 @@ module.exports = class Workspace extends Model { } if (this.config.get('editor.confirmCheckoutHeadRevision')) { - return this.applicationDelegate.confirm({ + this.applicationDelegate.confirm({ message: 'Confirm Checkout HEAD Revision', detailedMessage: `Are you sure you want to discard all changes to "${editor.getFileName()}" since the last Git commit?`, buttons: { @@ -1301,14 +1400,3 @@ module.exports = class Workspace extends Model { } } } - -function __guard__ (value, transform) { - return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined -} -function __guardMethod__ (obj, methodName, transform) { - if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { - return transform(obj, methodName) - } else { - return undefined - } -} From b5012bc347a92070ed4a8f175e377fc7c25f76b8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Mar 2017 10:41:43 +0100 Subject: [PATCH 16/55] :arrow_up: tabs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 10a5f57f6..bb7d5caa8 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "status-bar": "1.8.1", "styleguide": "0.49.3", "symbols-view": "0.114.0", - "tabs": "0.104.1", + "tabs": "0.104.2", "timecop": "0.36.0", "tree-view": "0.214.1", "update-package-dependencies": "0.10.0", From 24f02d161a9331244524bdc2344652205dc1ef5a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Mar 2017 10:48:34 +0100 Subject: [PATCH 17/55] :arrow_up: fuzzy-finder --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bb7d5caa8..501ad0252 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "encoding-selector": "0.23.2", "exception-reporting": "0.41.1", "find-and-replace": "0.207.0", - "fuzzy-finder": "1.4.1", + "fuzzy-finder": "1.5.0", "git-diff": "1.3.3", "go-to-line": "0.32.0", "grammar-selector": "0.49.3", From 5c3357049b65877bd45398aa1e062ba6b1201f42 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Mar 2017 10:49:21 +0100 Subject: [PATCH 18/55] :arrow_up: snippets --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 501ad0252..97bae403a 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "open-on-github": "1.2.1", "package-generator": "1.1.0", "settings-view": "0.247.2", - "snippets": "1.0.5", + "snippets": "1.1.1", "spell-check": "0.71.1", "status-bar": "1.8.1", "styleguide": "0.49.3", From 16e03a54d02f1a25320dd1cc51ea1998a14bef92 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Mar 2017 11:22:39 +0100 Subject: [PATCH 19/55] :arrow_up: archive_view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97bae403a..facc60dfa 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", "about": "1.7.4", - "archive-view": "0.62.2", + "archive-view": "0.63.0", "autocomplete-atom-api": "0.10.0", "autocomplete-css": "0.15.0", "autocomplete-html": "0.7.2", From c1a773ed9ea4a75aba79a355ca2cdfccc047a448 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Mar 2017 11:23:35 +0100 Subject: [PATCH 20/55] :arrow_up: update-package-dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index facc60dfa..d7cd4961d 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "tabs": "0.104.2", "timecop": "0.36.0", "tree-view": "0.214.1", - "update-package-dependencies": "0.10.0", + "update-package-dependencies": "0.11.0", "welcome": "0.36.2", "whitespace": "0.36.2", "wrap-guide": "0.39.1", From b2f1286afbf3db1901f8ae738eb74d24b1562ae8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Mar 2017 11:24:32 +0100 Subject: [PATCH 21/55] :arrow_up: wrap-guide --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d7cd4961d..771a31868 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "update-package-dependencies": "0.11.0", "welcome": "0.36.2", "whitespace": "0.36.2", - "wrap-guide": "0.39.1", + "wrap-guide": "0.40.0", "language-c": "0.56.0", "language-clojure": "0.22.2", "language-coffee-script": "0.48.4", From b25317e7acab954db1c7b3201f642bd0550cc539 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Mar 2017 11:26:40 +0100 Subject: [PATCH 22/55] :arrow_up: symbols-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 771a31868..cb5b651e5 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "spell-check": "0.71.1", "status-bar": "1.8.1", "styleguide": "0.49.3", - "symbols-view": "0.114.0", + "symbols-view": "0.115.0", "tabs": "0.104.2", "timecop": "0.36.0", "tree-view": "0.214.1", From 22ffe5f34cd265809403e0b52edd152f04dd6582 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Mar 2017 12:04:44 +0100 Subject: [PATCH 23/55] :arrow_up: settings-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cb5b651e5..956c7f609 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "notifications": "0.66.2", "open-on-github": "1.2.1", "package-generator": "1.1.0", - "settings-view": "0.247.2", + "settings-view": "0.248.0", "snippets": "1.1.1", "spell-check": "0.71.1", "status-bar": "1.8.1", From 5d5e95562c0638bf2e3b84c38fc0c05bd0277987 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Mar 2017 12:07:32 +0100 Subject: [PATCH 24/55] :arrow_up: tree-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 956c7f609..4562a67e8 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "symbols-view": "0.115.0", "tabs": "0.104.2", "timecop": "0.36.0", - "tree-view": "0.214.1", + "tree-view": "0.215.0", "update-package-dependencies": "0.11.0", "welcome": "0.36.2", "whitespace": "0.36.2", From d80782355a73d83ad17c4c4ca981ca89c66cb380 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Mar 2017 12:09:51 +0100 Subject: [PATCH 25/55] :arrow_up: pathwatcher in all node modules and atom packages that use it * :arrow_up: text-buffer * :arrow_up: tree-view * :arrow_up: atom-keymap --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 4562a67e8..c805362e6 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "electronVersion": "1.3.13", "dependencies": { "async": "0.2.6", - "atom-keymap": "7.1.21", + "atom-keymap": "7.1.22", "atom-select-list": "0.0.15", "atom-ui": "0.4.1", "babel-core": "6.22.1", @@ -60,7 +60,7 @@ "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "6.1.0", - "pathwatcher": "6.8.0", + "pathwatcher": "6.9.0", "postcss": "5.2.4", "postcss-selector-parser": "2.2.1", "property-accessors": "^1.1.3", @@ -76,7 +76,7 @@ "sinon": "1.17.4", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "10.3.12", + "text-buffer": "10.4.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", @@ -136,7 +136,7 @@ "symbols-view": "0.115.0", "tabs": "0.104.2", "timecop": "0.36.0", - "tree-view": "0.215.0", + "tree-view": "0.215.1", "update-package-dependencies": "0.11.0", "welcome": "0.36.2", "whitespace": "0.36.2", From dd64470381fdd4252c6bbd0359770a1881334d36 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 3 Mar 2017 10:47:05 -0500 Subject: [PATCH 26/55] :arrow_up: about@1.7.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c805362e6..a96dc7ff9 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "one-light-syntax": "1.7.1", "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", - "about": "1.7.4", + "about": "1.7.5", "archive-view": "0.63.0", "autocomplete-atom-api": "0.10.0", "autocomplete-css": "0.15.0", From 679abe0d1e380451e295452ee86cf21fcee82e49 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 3 Mar 2017 10:48:51 -0500 Subject: [PATCH 27/55] :arrow_up: language-coffee-script@0.48.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a96dc7ff9..00bae5980 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,7 @@ "wrap-guide": "0.40.0", "language-c": "0.56.0", "language-clojure": "0.22.2", - "language-coffee-script": "0.48.4", + "language-coffee-script": "0.48.5", "language-csharp": "0.14.2", "language-css": "0.42.0", "language-gfm": "0.88.1", From 4334bd2fa41b05b4236965ba8b5df396f249faae Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Mar 2017 16:52:31 +0100 Subject: [PATCH 28/55] :arrow_up: fs-plus in all node modules and atom packages that use it * :arrow_up: exception-reporting * :arrow_up: status-bar * :arrow_up: symbols-view --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 00bae5980..ee7142f32 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "event-kit": "^2.1.0", "find-parent-dir": "^0.3.0", "first-mate": "6.3.0", - "fs-plus": "2.9.2", + "fs-plus": "2.10.0", "fstream": "0.1.24", "fuzzaldrin": "^2.1", "git-utils": "4.1.2", @@ -112,7 +112,7 @@ "deprecation-cop": "0.56.2", "dev-live-reload": "0.47.0", "encoding-selector": "0.23.2", - "exception-reporting": "0.41.1", + "exception-reporting": "0.41.2", "find-and-replace": "0.207.0", "fuzzy-finder": "1.5.0", "git-diff": "1.3.3", @@ -131,9 +131,9 @@ "settings-view": "0.248.0", "snippets": "1.1.1", "spell-check": "0.71.1", - "status-bar": "1.8.1", + "status-bar": "1.8.2", "styleguide": "0.49.3", - "symbols-view": "0.115.0", + "symbols-view": "0.115.1", "tabs": "0.104.2", "timecop": "0.36.0", "tree-view": "0.215.1", From 4c0ace0e1b10bcafd69df8087386a22c7c6f86ba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Mar 2017 17:07:19 +0100 Subject: [PATCH 29/55] :arrow_up: text-buffer to fix build errors Ref: https://github.com/atom/text-buffer/pull/218 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ee7142f32..a76f858b7 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "sinon": "1.17.4", "source-map-support": "^0.3.2", "temp": "0.8.1", - "text-buffer": "10.4.1", + "text-buffer": "10.4.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From c06fd3f40224cf3bb1142b23bf63449ddcdaa62a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 3 Mar 2017 18:50:52 +0100 Subject: [PATCH 30/55] :arrow_up: apm --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index ede2d1bd7..07931700d 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.15.3" + "atom-package-manager": "1.16.0" } } From 3c0638101825210575f3f15f44945c1d01b10f88 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 3 Mar 2017 12:00:45 -0800 Subject: [PATCH 31/55] Fix usage of dynamic arguments variable in an arrow function --- src/workspace.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workspace.js b/src/workspace.js index 7282ed419..f411cf823 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -61,7 +61,7 @@ module.exports = class Workspace extends Model { // Etch, in which case it'd be `new`d. And when it's `new`d, `this` is always // the newly created object. const realThis = this - this.buildTextEditor = () => Workspace.prototype.buildTextEditor.apply(realThis, arguments) + this.buildTextEditor = (params) => Workspace.prototype.buildTextEditor.call(realThis, params) this.panelContainers = { top: new PanelContainer({location: 'top'}), From c8bcb6038037c5f427e4f2a1c80982b1255674dd Mon Sep 17 00:00:00 2001 From: simurai Date: Sat, 4 Mar 2017 09:20:42 +0900 Subject: [PATCH 32/55] :arrow_up: one-dark/light-ui@v1.9.2 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a76f858b7..554a35422 100644 --- a/package.json +++ b/package.json @@ -89,8 +89,8 @@ "atom-light-ui": "0.46.0", "base16-tomorrow-dark-theme": "1.5.0", "base16-tomorrow-light-theme": "1.5.0", - "one-dark-ui": "1.9.1", - "one-light-ui": "1.9.1", + "one-dark-ui": "1.9.2", + "one-light-ui": "1.9.2", "one-dark-syntax": "1.7.1", "one-light-syntax": "1.7.1", "solarized-dark-syntax": "1.1.2", From 07b6db7de9dbd75a59cfc3813aa83236450b0329 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 4 Mar 2017 13:40:47 +0100 Subject: [PATCH 33/55] :arrow_up: fs-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 554a35422..8ba31e259 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "event-kit": "^2.1.0", "find-parent-dir": "^0.3.0", "first-mate": "6.3.0", - "fs-plus": "2.10.0", + "fs-plus": "2.10.1", "fstream": "0.1.24", "fuzzaldrin": "^2.1", "git-utils": "4.1.2", From 6cde3242900fbf16988e9e5ff5a403b56eb658cf Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 4 Mar 2017 14:33:57 +0100 Subject: [PATCH 34/55] Fix core renderer tests --- src/config.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.coffee b/src/config.coffee index e873a1348..e7d05da6d 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -839,7 +839,7 @@ class Config relativePath = sourcePath.substring(templateConfigDirPath.length + 1) destinationPath = path.join(@configDirPath, relativePath) queue.push({sourcePath, destinationPath}) - fs.traverseTree(templateConfigDirPath, onConfigDirFile, (path) -> true) + fs.traverseTree(templateConfigDirPath, onConfigDirFile, ((path) -> true), (->)) loadUserConfig: -> return if @shouldNotAccessFileSystem() From 78935dddd5297e9da8361fb847ccc86c1e8bbbe7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 4 Mar 2017 14:40:02 +0100 Subject: [PATCH 35/55] :arrow_up: status-bar --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8ba31e259..89499cf9a 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "settings-view": "0.248.0", "snippets": "1.1.1", "spell-check": "0.71.1", - "status-bar": "1.8.2", + "status-bar": "1.8.3", "styleguide": "0.49.3", "symbols-view": "0.115.1", "tabs": "0.104.2", From 8df511be5512bdc1fee7b315de1aab2768c7b720 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 4 Mar 2017 14:40:49 +0100 Subject: [PATCH 36/55] :arrow_up: symbols-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 89499cf9a..97d523fa4 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "spell-check": "0.71.1", "status-bar": "1.8.3", "styleguide": "0.49.3", - "symbols-view": "0.115.1", + "symbols-view": "0.115.2", "tabs": "0.104.2", "timecop": "0.36.0", "tree-view": "0.215.1", From ed9a101de2803bff00e988673cad7935da6029f5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 4 Mar 2017 15:39:38 +0100 Subject: [PATCH 37/55] Fix destroying a `PanelContainer` containing multiple panels Previously, when calling `destroy` on a `PanelContainer` containing multiple panels, Atom would throw a `Cannot read property 'destroy' of undefined` exception. This was due to iterating over the panels while at the same time destroying them, which caused the iterated array to be modified during the loop. With this commit we slice the array before iterating over it so that destroying a `PanelContainer` doesn't throw exceptions anymore. --- spec/panel-container-spec.coffee | 19 ++++++++++++++++++- src/panel-container.coffee | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/spec/panel-container-spec.coffee b/spec/panel-container-spec.coffee index 08eaea92b..fbf1c3446 100644 --- a/spec/panel-container-spec.coffee +++ b/spec/panel-container-spec.coffee @@ -5,7 +5,7 @@ describe "PanelContainer", -> [container] = [] class TestPanelItem - constructior: -> + constructor: -> beforeEach -> container = new PanelContainer @@ -39,6 +39,23 @@ describe "PanelContainer", -> panel1.destroy() expect(removePanelSpy).toHaveBeenCalledWith({panel: panel1, index: 0}) + describe "::destroy()", -> + it "destroys the container and all of its panels", -> + destroyedPanels = [] + + panel1 = new Panel(item: new TestPanelItem()) + panel1.onDidDestroy -> destroyedPanels.push(panel1) + container.addPanel(panel1) + + panel2 = new Panel(item: new TestPanelItem()) + panel2.onDidDestroy -> destroyedPanels.push(panel2) + container.addPanel(panel2) + + container.destroy() + + expect(container.getPanels().length).toBe(0) + expect(destroyedPanels).toEqual([panel1, panel2]) + describe "panel priority", -> describe 'left / top panel container', -> [initialPanel] = [] diff --git a/src/panel-container.coffee b/src/panel-container.coffee index 322773f69..d109210a7 100644 --- a/src/panel-container.coffee +++ b/src/panel-container.coffee @@ -34,7 +34,7 @@ class PanelContainer isModal: -> @location is 'modal' - getPanels: -> @panels + getPanels: -> @panels.slice() addPanel: (panel) -> @subscriptions.add panel.onDidDestroy(@panelDestroyed.bind(this)) From ab1fb2f217aa94cac98105316c7f164bfb6251bd Mon Sep 17 00:00:00 2001 From: Mikhail Fesenko Date: Sun, 5 Mar 2017 21:59:07 +0300 Subject: [PATCH 38/55] Fix path on macOS link --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index e9b6ff120..c555306b5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,7 +10,7 @@ In this directory you can only find very specific build and API level documentat Instructions for building Atom on various platforms from source. -* [macOS](./build-instructions/macos.md) +* [macOS](./build-instructions/macOS.md) * [Windows](./build-instructions/windows.md) * [Linux](./build-instructions/linux.md) * [FreeBSD](./build-instructions/freebsd.md) From 52492c1386b87d97d0d4d605f980b3bc2eab3121 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Sun, 5 Mar 2017 17:21:05 -0800 Subject: [PATCH 39/55] Ensure recent project list populated when no project loaded, fixes #13758 --- src/reopen-project-menu-manager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/reopen-project-menu-manager.js b/src/reopen-project-menu-manager.js index 79acbba66..6f9e2884f 100644 --- a/src/reopen-project-menu-manager.js +++ b/src/reopen-project-menu-manager.js @@ -20,6 +20,7 @@ export default class ReopenProjectMenuManager { commands.add('atom-workspace', { 'application:reopen-project': this.reopenProjectCommand.bind(this) }) ) + this.update() this.applyWindowsJumpListRemovals() } From ef0619048e320fcc539f998b5202a42eacb50114 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Sun, 5 Mar 2017 17:49:01 -0800 Subject: [PATCH 40/55] Revert "Ensure recent project list populated when no project loaded, fixes #13758" This reverts commit 52492c1386b87d97d0d4d605f980b3bc2eab3121. --- src/reopen-project-menu-manager.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/reopen-project-menu-manager.js b/src/reopen-project-menu-manager.js index 6f9e2884f..79acbba66 100644 --- a/src/reopen-project-menu-manager.js +++ b/src/reopen-project-menu-manager.js @@ -20,7 +20,6 @@ export default class ReopenProjectMenuManager { commands.add('atom-workspace', { 'application:reopen-project': this.reopenProjectCommand.bind(this) }) ) - this.update() this.applyWindowsJumpListRemovals() } From d64d99d2f74ecfd4db0c059055ba05ac40dab8ac Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 6 Mar 2017 11:47:09 -0500 Subject: [PATCH 41/55] :arrow_up: autocomplete-css@0.15.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97d523fa4..f3fd9d9cd 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "about": "1.7.5", "archive-view": "0.63.0", "autocomplete-atom-api": "0.10.0", - "autocomplete-css": "0.15.0", + "autocomplete-css": "0.15.1", "autocomplete-html": "0.7.2", "autocomplete-plus": "2.34.2", "autocomplete-snippets": "1.11.0", From 273dd5405ee61380a19c756e805812d522c2a032 Mon Sep 17 00:00:00 2001 From: Wliu Date: Mon, 6 Mar 2017 11:49:51 -0500 Subject: [PATCH 42/55] :arrow_up: language-xml@0.35.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f3fd9d9cd..0cceb8760 100644 --- a/package.json +++ b/package.json @@ -171,7 +171,7 @@ "language-text": "0.7.1", "language-todo": "0.29.1", "language-toml": "0.18.1", - "language-xml": "0.34.16", + "language-xml": "0.35.0", "language-yaml": "0.28.0" }, "private": true, From 5d6cd1e54291ea10fd62e837e2d401374ccb01ad Mon Sep 17 00:00:00 2001 From: Wliu Date: Mon, 6 Mar 2017 11:51:00 -0500 Subject: [PATCH 43/55] :arrow_up: language-property-list@0.9.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0cceb8760..f1a429063 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,7 @@ "language-objective-c": "0.15.1", "language-perl": "0.37.0", "language-php": "0.37.4", - "language-property-list": "0.9.0", + "language-property-list": "0.9.1", "language-python": "0.45.2", "language-ruby": "0.70.5", "language-ruby-on-rails": "0.25.2", From e97c8f8e6b673f6ae070cafaec142997a89b656d Mon Sep 17 00:00:00 2001 From: Wliu Date: Mon, 6 Mar 2017 11:51:54 -0500 Subject: [PATCH 44/55] :arrow_up: language-sass@0.58.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f1a429063..e2bd39671 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,7 @@ "language-python": "0.45.2", "language-ruby": "0.70.5", "language-ruby-on-rails": "0.25.2", - "language-sass": "0.57.1", + "language-sass": "0.58.0", "language-shellscript": "0.25.0", "language-source": "0.9.0", "language-sql": "0.25.3", From eb9cf0330a04db8062a74c775d0406c458402c80 Mon Sep 17 00:00:00 2001 From: Wliu Date: Mon, 6 Mar 2017 11:53:02 -0500 Subject: [PATCH 45/55] :arrow_up: language-java@0.27.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e2bd39671..c6be559f9 100644 --- a/package.json +++ b/package.json @@ -151,10 +151,10 @@ "language-go": "0.43.1", "language-html": "0.47.2", "language-hyperlink": "0.16.1", - "language-java": "0.26.0", + "language-java": "0.27.0", "language-javascript": "0.126.1", "language-json": "0.18.3", - "language-less": "0.30.1", + "language-less": "0.31.0", "language-make": "0.22.3", "language-mustache": "0.13.1", "language-objective-c": "0.15.1", From 4e8ac61e54f426808378a09da76e7ed65192308e Mon Sep 17 00:00:00 2001 From: Wliu Date: Mon, 6 Mar 2017 11:53:42 -0500 Subject: [PATCH 46/55] :arrow_up: language-php@0.37.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c6be559f9..9ecbfac5b 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "language-mustache": "0.13.1", "language-objective-c": "0.15.1", "language-perl": "0.37.0", - "language-php": "0.37.4", + "language-php": "0.37.5", "language-property-list": "0.9.1", "language-python": "0.45.2", "language-ruby": "0.70.5", From a58f732b0ad1f0c92b0354a7f74d5b06f87ac786 Mon Sep 17 00:00:00 2001 From: Wliu Date: Mon, 6 Mar 2017 11:54:30 -0500 Subject: [PATCH 47/55] :arrow_up: language-json@0.19.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ecbfac5b..a6ac8f639 100644 --- a/package.json +++ b/package.json @@ -153,7 +153,7 @@ "language-hyperlink": "0.16.1", "language-java": "0.27.0", "language-javascript": "0.126.1", - "language-json": "0.18.3", + "language-json": "0.19.0", "language-less": "0.31.0", "language-make": "0.22.3", "language-mustache": "0.13.1", From 6c70984b29ecd85bc2719624f7063df496d58a03 Mon Sep 17 00:00:00 2001 From: Wliu Date: Mon, 6 Mar 2017 11:55:14 -0500 Subject: [PATCH 48/55] :arrow_up: language-yaml@0.29.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a6ac8f639..d1afaac99 100644 --- a/package.json +++ b/package.json @@ -172,7 +172,7 @@ "language-todo": "0.29.1", "language-toml": "0.18.1", "language-xml": "0.35.0", - "language-yaml": "0.28.0" + "language-yaml": "0.29.0" }, "private": true, "scripts": { From d298a4e7c6f8d456460895b235784c2b6ee7b4d3 Mon Sep 17 00:00:00 2001 From: Wliu Date: Mon, 6 Mar 2017 11:56:19 -0500 Subject: [PATCH 49/55] :arrow_up: language-text@0.7.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d1afaac99..fa571aa88 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,7 @@ "language-shellscript": "0.25.0", "language-source": "0.9.0", "language-sql": "0.25.3", - "language-text": "0.7.1", + "language-text": "0.7.2", "language-todo": "0.29.1", "language-toml": "0.18.1", "language-xml": "0.35.0", From 59c20484aa222af436040f022e0bb44cdee8d7e9 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 6 Mar 2017 09:41:14 -0800 Subject: [PATCH 50/55] Always restore Reopen Project menu, fixes #13758 --- src/atom-environment.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index dc4318bec..d7fab1924 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -237,7 +237,7 @@ class AtomEnvironment extends Model @applicationDelegate.didChangeHistoryManager() unless e.reloaded @disposables.add @applicationDelegate.onDidChangeHistoryManager(=> @history.loadState()) - new ReopenProjectMenuManager({@menu, @commands, @history, @config, open: (paths) => @open(pathsToOpen: paths)}) + (new ReopenProjectMenuManager({@menu, @commands, @history, @config, open: (paths) => @open(pathsToOpen: paths)})).update() attachSaveStateListeners: -> saveState = _.debounce((=> From 24503806d7eb77c62c1da45398b2ad00a8178d27 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 6 Mar 2017 17:30:43 -0500 Subject: [PATCH 51/55] :arrow_up: language-c@0.57.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fa571aa88..ed3756a38 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "welcome": "0.36.2", "whitespace": "0.36.2", "wrap-guide": "0.40.0", - "language-c": "0.56.0", + "language-c": "0.57.0", "language-clojure": "0.22.2", "language-coffee-script": "0.48.5", "language-csharp": "0.14.2", From 051e27dbcb733b66349873a96b2458fd88d6fed5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 6 Mar 2017 20:03:40 +0100 Subject: [PATCH 52/55] Expose load settings to `BrowserWindow`s as JSON When accessing objects in the main process via the `remote` module, Electron returns proxy objects that are references to the original ones. This means that trying to access a remote object's property or function results in a synchronous message exchange with the main process. In Atom core we frequently access the load settings coming from the main process, especially during startup. This caused a lot of synchronous I/O which was blocking the renderer process for several milliseconds. With this commit, instead of exposing load settings as a JavaScript object, we serialize them to JSON in the main process and parse them back to a JavaScript object in the renderer processes. This allows us to get a full copy of the object locally and pay for I/O just once when retrieving load settings from the main process for the first time. --- src/get-window-load-settings.js | 2 +- src/main-process/atom-window.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/get-window-load-settings.js b/src/get-window-load-settings.js index 7ee465141..d35b24213 100644 --- a/src/get-window-load-settings.js +++ b/src/get-window-load-settings.js @@ -4,7 +4,7 @@ let windowLoadSettings = null module.exports = () => { if (!windowLoadSettings) { - windowLoadSettings = remote.getCurrentWindow().loadSettings + windowLoadSettings = JSON.parse(remote.getCurrentWindow().loadSettingsJSON) } return windowLoadSettings } diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index 03386d31a..da903d71e 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -83,7 +83,7 @@ class AtomWindow @representedDirectoryPaths = loadSettings.initialPaths @env = loadSettings.env if loadSettings.env? - @browserWindow.loadSettings = loadSettings + @browserWindow.loadSettingsJSON = JSON.stringify(loadSettings) @browserWindow.on 'window:loaded', => @emit 'window:loaded' From 727472af58246f21bde7da2d3e2a2c65d8ee1659 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Mar 2017 09:35:11 +0100 Subject: [PATCH 53/55] Disable zoom in the main process This commit will register the `display-added` and `display-removed` events only once in the main process in order to disable zoom (see https://github.com/atom/atom/pull/11345) directly instead of unnecessarily paying for I/O in the renderer process during startup. --- src/application-delegate.coffee | 16 +--------------- src/atom-environment.coffee | 2 -- src/main-process/atom-application.coffee | 22 +++++++++++++++++++--- src/main-process/atom-window.coffee | 4 ++++ 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 766ba7aa8..cd7a4d128 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -1,5 +1,5 @@ _ = require 'underscore-plus' -{screen, ipcRenderer, remote, shell, webFrame} = require 'electron' +{ipcRenderer, remote, shell} = require 'electron' ipcHelpers = require './ipc-helpers' {Disposable} = require 'event-kit' getWindowLoadSettings = require './get-window-load-settings' @@ -254,20 +254,6 @@ class ApplicationDelegate openExternal: (url) -> shell.openExternal(url) - disableZoom: -> - outerCallback = -> - webFrame.setZoomLevelLimits(1, 1) - - outerCallback() - # Set the limits every time a display is added or removed, otherwise the - # configuration gets reset to the default, which allows zooming the - # webframe. - screen.on('display-added', outerCallback) - screen.on('display-removed', outerCallback) - new Disposable -> - screen.removeListener('display-added', outerCallback) - screen.removeListener('display-removed', outerCallback) - checkForUpdate: -> ipcRenderer.send('command', 'application:check-for-update') diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index d7fab1924..d834a355f 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -215,8 +215,6 @@ class AtomEnvironment extends Model @stylesElement = @styles.buildStylesElement() @document.head.appendChild(@stylesElement) - @disposables.add(@applicationDelegate.disableZoom()) - @keymaps.subscribeToFileReadFailure() @keymaps.loadBundledKeymaps() diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 93e9e3395..295343100 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -6,8 +6,8 @@ StorageFolder = require '../storage-folder' Config = require '../config' FileRecoveryService = require './file-recovery-service' ipcHelpers = require '../ipc-helpers' -{BrowserWindow, Menu, app, dialog, ipcMain, shell} = require 'electron' -{CompositeDisposable} = require 'event-kit' +{BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron' +{CompositeDisposable, Disposable} = require 'event-kit' fs = require 'fs-plus' path = require 'path' os = require 'os' @@ -89,7 +89,7 @@ class AtomApplication if process.platform is 'darwin' and @config.get('core.useCustomTitleBar') @config.unset('core.useCustomTitleBar') @config.set('core.titleBar', 'custom') - + @config.onDidChange 'core.titleBar', @promptForRestart.bind(this) @autoUpdateManager = new AutoUpdateManager( @@ -394,6 +394,8 @@ class AtomApplication @disposable.add ipcHelpers.on ipcMain, 'did-change-paths', => @saveState(false) + @disposable.add(@disableZoomOnDisplayChange()) + setupDockMenu: -> if process.platform is 'darwin' dockMenu = Menu.buildFromTemplate [ @@ -812,3 +814,17 @@ class AtomApplication args.push("--resource-path=#{@resourcePath}") app.relaunch({args}) app.quit() + + disableZoomOnDisplayChange: -> + outerCallback = => + for window in @windows + window.disableZoom() + + # Set the limits every time a display is added or removed, otherwise the + # configuration gets reset to the default, which allows zooming the + # webframe. + screen.on('display-added', outerCallback) + screen.on('display-removed', outerCallback) + new Disposable -> + screen.removeListener('display-added', outerCallback) + screen.removeListener('display-removed', outerCallback) diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index da903d71e..6013819e3 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -101,6 +101,7 @@ class AtomWindow hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?) @openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow() + @disableZoom() @atomApplication.addWindow(this) @@ -303,3 +304,6 @@ class AtomWindow @atomApplication.saveState() copy: -> @browserWindow.copy() + + disableZoom: -> + @browserWindow.webContents.setZoomLevelLimits(1, 1) From 45d41ca69f00426f911c721882a6500bb83f1154 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Mar 2017 09:40:20 +0100 Subject: [PATCH 54/55] Register enter/leave fullscreen events in the main process This will still notify render processes when such events are triggered without, however, incurring the additional cost of synchronously retrieving a `BrowserWindow` (and its properties) via `remote` during startup. --- src/application-delegate.coffee | 6 ++++++ src/main-process/atom-window.coffee | 6 ++++++ src/window-event-handler.coffee | 10 ++-------- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index cd7a4d128..b247fe7c2 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -80,6 +80,12 @@ class ApplicationDelegate setWindowFullScreen: (fullScreen=false) -> ipcHelpers.call('window-method', 'setFullScreen', fullScreen) + onDidEnterFullScreen: (callback) -> + ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback) + + onDidLeaveFullScreen: (callback) -> + ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback) + openWindowDevTools: -> # Defer DevTools interaction to the next tick, because using them during # event handling causes some wrong input events to be triggered on diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index 6013819e3..bbc235bc5 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -89,6 +89,12 @@ class AtomWindow @emit 'window:loaded' @resolveLoadedPromise() + @browserWindow.on 'enter-full-screen', => + @browserWindow.webContents.send('did-enter-full-screen') + + @browserWindow.on 'leave-full-screen', => + @browserWindow.webContents.send('did-leave-full-screen') + @browserWindow.loadURL url.format protocol: 'file' pathname: "#{@resourcePath}/static/index.html" diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 62ce4527a..95cd45de9 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -20,14 +20,8 @@ class WindowEventHandler @subscriptions.add listen(@document, 'click', 'a', @handleLinkClick) @subscriptions.add listen(@document, 'submit', 'form', @handleFormSubmit) - browserWindow = @applicationDelegate.getCurrentWindow() - browserWindow.on 'enter-full-screen', @handleEnterFullScreen - @subscriptions.add new Disposable => - browserWindow.removeListener('enter-full-screen', @handleEnterFullScreen) - - browserWindow.on 'leave-full-screen', @handleLeaveFullScreen - @subscriptions.add new Disposable => - browserWindow.removeListener('leave-full-screen', @handleLeaveFullScreen) + @subscriptions.add(@applicationDelegate.onDidEnterFullScreen(@handleEnterFullScreen)) + @subscriptions.add(@applicationDelegate.onDidLeaveFullScreen(@handleLeaveFullScreen)) @subscriptions.add @atomEnvironment.commands.add @window, 'window:toggle-full-screen': @handleWindowToggleFullScreen From cf9a5b13e3d875b87de5bf0e3446c4edf80565f1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Mar 2017 10:37:41 +0100 Subject: [PATCH 55/55] Replace `localStorage` with `StateStore` in `HistoryManager` Instead of using `localStorage` to store and retrieve the project history, with this commit we will use `StateStore` so that we can retrieve state asynchronously without blocking Atom during startup. --- spec/history-manager-spec.js | 103 ++++++++++++++--------------- src/atom-environment.coffee | 15 +++-- src/history-manager.js | 56 ++++++++-------- src/reopen-project-menu-manager.js | 4 +- 4 files changed, 90 insertions(+), 88 deletions(-) diff --git a/spec/history-manager-spec.js b/spec/history-manager-spec.js index 425f1efe0..bc77cb9b8 100644 --- a/spec/history-manager-spec.js +++ b/spec/history-manager-spec.js @@ -4,27 +4,24 @@ import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' import {Emitter, Disposable, CompositeDisposable} from 'event-kit' import {HistoryManager, HistoryProject} from '../src/history-manager' +import StateStore from '../src/state-store' describe("HistoryManager", () => { - let historyManager, commandRegistry, project, localStorage, stateStore + let historyManager, commandRegistry, project, stateStore let commandDisposable, projectDisposable - beforeEach(() => { + beforeEach(async () => { commandDisposable = jasmine.createSpyObj('Disposable', ['dispose']) commandRegistry = jasmine.createSpyObj('CommandRegistry', ['add']) commandRegistry.add.andReturn(commandDisposable) - localStorage = jasmine.createSpyObj('LocalStorage', ['getItem', 'setItem']) - localStorage.items = { - history: JSON.stringify({ - projects: [ - { paths: ['/1', 'c:\\2'], lastOpened: new Date(2016, 9, 17, 17, 16, 23) }, - { paths: ['/test'], lastOpened: new Date(2016, 9, 17, 11, 12, 13) } - ] - }) - } - localStorage.getItem.andCallFake((key) => localStorage.items[key]) - localStorage.setItem.andCallFake((key, value) => localStorage.items[key] = value) + stateStore = new StateStore('history-manager-test', 1) + await stateStore.save('history-manager', { + projects: [ + {paths: ['/1', 'c:\\2'], lastOpened: new Date(2016, 9, 17, 17, 16, 23)}, + {paths: ['/test'], lastOpened: new Date(2016, 9, 17, 11, 12, 13)} + ] + }) projectDisposable = jasmine.createSpyObj('Disposable', ['dispose']) project = jasmine.createSpyObj('Project', ['onDidChangePaths']) @@ -33,7 +30,12 @@ describe("HistoryManager", () => { return projectDisposable }) - historyManager = new HistoryManager({project, commands:commandRegistry, localStorage}) + historyManager = new HistoryManager({stateStore, project, commands: commandRegistry}) + await historyManager.loadState() + }) + + afterEach(async () => { + await stateStore.clear() }) describe("constructor", () => { @@ -65,33 +67,28 @@ describe("HistoryManager", () => { }) describe("clearProjects", () => { - it("clears the list of projects", () => { + it("clears the list of projects", async () => { expect(historyManager.getProjects().length).not.toBe(0) - historyManager.clearProjects() + await historyManager.clearProjects() expect(historyManager.getProjects().length).toBe(0) }) - it("saves the state", () => { - expect(localStorage.setItem).not.toHaveBeenCalled() - historyManager.clearProjects() - expect(localStorage.setItem).toHaveBeenCalled() - expect(localStorage.setItem.calls[0].args[0]).toBe('history') + it("saves the state", async () => { + await historyManager.clearProjects() + const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry}) + await historyManager2.loadState() expect(historyManager.getProjects().length).toBe(0) }) - it("fires the onDidChangeProjects event", () => { - expect(localStorage.setItem).not.toHaveBeenCalled() - historyManager.clearProjects() - expect(localStorage.setItem).toHaveBeenCalled() - expect(localStorage.setItem.calls[0].args[0]).toBe('history') + it("fires the onDidChangeProjects event", async () => { + const didChangeSpy = jasmine.createSpy() + historyManager.onDidChangeProjects(didChangeSpy) + await historyManager.clearProjects() expect(historyManager.getProjects().length).toBe(0) + expect(didChangeSpy).toHaveBeenCalled() }) }) - it("loads state", () => { - expect(localStorage.getItem).toHaveBeenCalledWith('history') - }) - it("listens to project.onDidChangePaths adding a new project", () => { const start = new Date() project.didChangePathsListener(['/a/new', '/path/or/two']) @@ -112,61 +109,61 @@ describe("HistoryManager", () => { }) describe("loadState", () => { - it("defaults to an empty array if no state", () => { - localStorage.items.history = null - historyManager.loadState() + it("defaults to an empty array if no state", async () => { + await stateStore.clear() + await historyManager.loadState() expect(historyManager.getProjects()).toEqual([]) }) - it("defaults to an empty array if no projects", () => { - localStorage.items.history = JSON.stringify('') - historyManager.loadState() + it("defaults to an empty array if no projects", async () => { + await stateStore.save('history-manager', {}) + await historyManager.loadState() expect(historyManager.getProjects()).toEqual([]) }) }) describe("addProject", () => { - it("adds a new project to the end", () => { + it("adds a new project to the end", async () => { const date = new Date(2010, 10, 9, 8, 7, 6) - historyManager.addProject(['/a/b'], date) + await historyManager.addProject(['/a/b'], date) const projects = historyManager.getProjects() expect(projects.length).toBe(3) expect(projects[2].paths).toEqual(['/a/b']) expect(projects[2].lastOpened).toBe(date) }) - it("adds a new project to the start", () => { + it("adds a new project to the start", async () => { const date = new Date() - historyManager.addProject(['/so/new'], date) + await historyManager.addProject(['/so/new'], date) const projects = historyManager.getProjects() expect(projects.length).toBe(3) expect(projects[0].paths).toEqual(['/so/new']) expect(projects[0].lastOpened).toBe(date) }) - it("updates an existing project and moves it to the start", () => { + it("updates an existing project and moves it to the start", async () => { const date = new Date() - historyManager.addProject(['/test'], date) + await historyManager.addProject(['/test'], date) const projects = historyManager.getProjects() expect(projects.length).toBe(2) expect(projects[0].paths).toEqual(['/test']) expect(projects[0].lastOpened).toBe(date) }) - it("fires the onDidChangeProjects event when adding a project", () => { + it("fires the onDidChangeProjects event when adding a project", async () => { const didChangeSpy = jasmine.createSpy() const beforeCount = historyManager.getProjects().length historyManager.onDidChangeProjects(didChangeSpy) - historyManager.addProject(['/test-new'], new Date()) + await historyManager.addProject(['/test-new'], new Date()) expect(didChangeSpy).toHaveBeenCalled() expect(historyManager.getProjects().length).toBe(beforeCount + 1) }) - it("fires the onDidChangeProjects event when updating a project", () => { + it("fires the onDidChangeProjects event when updating a project", async () => { const didChangeSpy = jasmine.createSpy() const beforeCount = historyManager.getProjects().length historyManager.onDidChangeProjects(didChangeSpy) - historyManager.addProject(['/test'], new Date()) + await historyManager.addProject(['/test'], new Date()) expect(didChangeSpy).toHaveBeenCalled() expect(historyManager.getProjects().length).toBe(beforeCount) }) @@ -186,14 +183,12 @@ describe("HistoryManager", () => { }) describe("saveState" ,() => { - it("saves the state", () => { - historyManager.addProject(["/save/state"]) - historyManager.saveState() - expect(localStorage.setItem).toHaveBeenCalled() - expect(localStorage.setItem.calls[0].args[0]).toBe('history') - expect(localStorage.items['history']).toContain('/save/state') - historyManager.loadState() - expect(historyManager.getProjects()[0].paths).toEqual(['/save/state']) + it("saves the state", async () => { + await historyManager.addProject(["/save/state"]) + await historyManager.saveState() + const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry}) + await historyManager2.loadState() + expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state']) }) }) }) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index d834a355f..cf5df00d2 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -229,14 +229,12 @@ class AtomEnvironment extends Model @observeAutoHideMenuBar() - @history = new HistoryManager({@project, @commands, localStorage}) + @history = new HistoryManager({@project, @commands, @stateStore}) # Keep instances of HistoryManager in sync - @history.onDidChangeProjects (e) => + @disposables.add @history.onDidChangeProjects (e) => @applicationDelegate.didChangeHistoryManager() unless e.reloaded @disposables.add @applicationDelegate.onDidChangeHistoryManager(=> @history.loadState()) - (new ReopenProjectMenuManager({@menu, @commands, @history, @config, open: (paths) => @open(pathsToOpen: paths)})).update() - attachSaveStateListeners: -> saveState = _.debounce((=> window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded @@ -714,7 +712,14 @@ class AtomEnvironment extends Model @openInitialEmptyEditorIfNecessary() - Promise.all([loadStatePromise, updateProcessEnvPromise]) + loadHistoryPromise = @history.loadState().then => + @reopenProjectMenuManager = new ReopenProjectMenuManager({ + @menu, @commands, @history, @config, + open: (paths) => @open(pathsToOpen: paths) + }) + @reopenProjectMenuManager.update() + + Promise.all([loadStatePromise, loadHistoryPromise, updateProcessEnvPromise]) serialize: (options) -> version: @constructor.version diff --git a/src/history-manager.js b/src/history-manager.js index f013957b9..5087c3bf9 100644 --- a/src/history-manager.js +++ b/src/history-manager.js @@ -1,6 +1,6 @@ /** @babel */ -import {Emitter} from 'event-kit' +import {Emitter, CompositeDisposable} from 'event-kit' // Extended: History manager for remembering which projects have been opened. // @@ -8,12 +8,17 @@ import {Emitter} from 'event-kit' // // The project history is used to enable the 'Reopen Project' menu. export class HistoryManager { - constructor ({project, commands, localStorage}) { - this.localStorage = localStorage - commands.add('atom-workspace', {'application:clear-project-history': this.clearProjects.bind(this)}) + constructor ({stateStore, project, commands}) { + this.stateStore = stateStore this.emitter = new Emitter() - this.loadState() - project.onDidChangePaths((projectPaths) => this.addProject(projectPaths)) + this.projects = [] + this.disposables = new CompositeDisposable() + this.disposables.add(commands.add('atom-workspace', {'application:clear-project-history': this.clearProjects.bind(this)})) + this.disposables.add(project.onDidChangePaths((projectPaths) => this.addProject(projectPaths))) + } + + destroy () { + this.disposables.dispose() } // Public: Obtain a list of previously opened projects. @@ -27,9 +32,12 @@ export class HistoryManager { // // Note: This is not a privacy function - other traces will still exist, // e.g. window state. - clearProjects () { + // + // Return a {Promise} that resolves when the history has been successfully + // cleared. + async clearProjects () { this.projects = [] - this.saveState() + await this.saveState() this.didChangeProjects() } @@ -46,7 +54,7 @@ export class HistoryManager { this.emitter.emit('did-change-projects', args || { reloaded: false }) } - addProject (paths, lastOpened) { + async addProject (paths, lastOpened) { if (paths.length === 0) return let project = this.getProject(paths) @@ -57,11 +65,11 @@ export class HistoryManager { project.lastOpened = lastOpened || new Date() this.projects.sort((a, b) => b.lastOpened - a.lastOpened) - this.saveState() + await this.saveState() this.didChangeProjects() } - removeProject (paths) { + async removeProject (paths) { if (paths.length === 0) return let project = this.getProject(paths) @@ -70,7 +78,7 @@ export class HistoryManager { let index = this.projects.indexOf(project) this.projects.splice(index, 1) - this.saveState() + await this.saveState() this.didChangeProjects() } @@ -84,31 +92,25 @@ export class HistoryManager { return null } - loadState () { - const state = JSON.parse(this.localStorage.getItem('history')) - if (state && state.projects) { - this.projects = state.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened))) - this.didChangeProjects({ reloaded: true }) + async loadState () { + const history = await this.stateStore.load('history-manager') + if (history && history.projects) { + this.projects = history.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened))) + this.didChangeProjects({reloaded: true}) } else { this.projects = [] } } - saveState () { - const state = JSON.stringify({ - projects: this.projects.map(p => ({ - paths: p.paths, lastOpened: p.lastOpened - })) - }) - this.localStorage.setItem('history', state) + async saveState () { + const projects = this.projects.map(p => ({paths: p.paths, lastOpened: p.lastOpened})) + await this.stateStore.save('history-manager', {projects}) } async importProjectHistory () { for (let project of await HistoryImporter.getAllProjects()) { - this.addProject(project.paths, project.lastOpened) + await this.addProject(project.paths, project.lastOpened) } - this.saveState() - this.didChangeProjects() } } diff --git a/src/reopen-project-menu-manager.js b/src/reopen-project-menu-manager.js index 79acbba66..3f88e41f0 100644 --- a/src/reopen-project-menu-manager.js +++ b/src/reopen-project-menu-manager.js @@ -58,7 +58,7 @@ export default class ReopenProjectMenuManager { // Windows users can right-click Atom taskbar and remove project from the jump list. // We have to honor that or the group stops working. As we only get a partial list // each time we remove them from history entirely. - applyWindowsJumpListRemovals () { + async applyWindowsJumpListRemovals () { if (process.platform !== 'win32') return if (this.app === undefined) { this.app = require('remote').app @@ -68,7 +68,7 @@ export default class ReopenProjectMenuManager { if (removed.length === 0) return for (let project of this.historyManager.getProjects()) { if (removed.includes(ReopenProjectMenuManager.taskDescription(project.paths))) { - this.historyManager.removeProject(project.paths) + await this.historyManager.removeProject(project.paths) } } }