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; + } +}