diff --git a/circle.yml b/circle.yml new file mode 100644 index 000000000..a55900cca --- /dev/null +++ b/circle.yml @@ -0,0 +1,4 @@ +general: + branches: + only: + - io-circle-ci diff --git a/keymaps/darwin.cson b/keymaps/darwin.cson index 4859f9b67..819e0079e 100644 --- a/keymaps/darwin.cson +++ b/keymaps/darwin.cson @@ -73,8 +73,10 @@ 'cmd-alt-right': 'pane:show-next-item' 'ctrl-pageup': 'pane:show-previous-item' 'ctrl-pagedown': 'pane:show-next-item' - 'ctrl-tab': 'pane:show-next-item' - 'ctrl-shift-tab': 'pane:show-previous-item' + 'ctrl-tab': 'pane:show-next-recently-used-item' + 'ctrl-tab ^ctrl': 'pane:move-active-item-to-top-of-stack' + 'ctrl-shift-tab': 'pane:show-previous-recently-used-item' + 'ctrl-shift-tab ^ctrl': 'pane:move-active-item-to-top-of-stack' 'cmd-=': 'window:increase-font-size' 'cmd-+': 'window:increase-font-size' 'cmd--': 'window:decrease-font-size' diff --git a/keymaps/linux.cson b/keymaps/linux.cson index 9ddb760e2..e676ed5ab 100644 --- a/keymaps/linux.cson +++ b/keymaps/linux.cson @@ -46,8 +46,10 @@ 'pagedown': 'core:page-down' 'backspace': 'core:backspace' 'shift-backspace': 'core:backspace' - 'ctrl-tab': 'pane:show-next-item' - 'ctrl-shift-tab': 'pane:show-previous-item' + 'ctrl-tab': 'pane:show-next-recently-used-item' + 'ctrl-tab ^ctrl': 'pane:move-active-item-to-top-of-stack' + 'ctrl-shift-tab': 'pane:show-previous-recently-used-item' + 'ctrl-shift-tab ^ctrl': 'pane:move-active-item-to-top-of-stack' 'ctrl-pageup': 'pane:show-previous-item' 'ctrl-pagedown': 'pane:show-next-item' 'ctrl-up': 'core:move-up' diff --git a/keymaps/win32.cson b/keymaps/win32.cson index e4703bac8..5869a3ed8 100644 --- a/keymaps/win32.cson +++ b/keymaps/win32.cson @@ -52,8 +52,10 @@ 'pagedown': 'core:page-down' 'backspace': 'core:backspace' 'shift-backspace': 'core:backspace' - 'ctrl-tab': 'pane:show-next-item' - 'ctrl-shift-tab': 'pane:show-previous-item' + 'ctrl-tab': 'pane:show-next-recently-used-item' + 'ctrl-tab ^ctrl': 'pane:move-active-item-to-top-of-stack' + 'ctrl-shift-tab': 'pane:show-previous-recently-used-item' + 'ctrl-shift-tab ^ctrl': 'pane:move-active-item-to-top-of-stack' 'ctrl-pageup': 'pane:show-previous-item' 'ctrl-pagedown': 'pane:show-next-item' 'ctrl-shift-up': 'core:move-up' diff --git a/package.json b/package.json index a9eab6b3b..8830a3f98 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "less-cache": "0.23", "line-top-index": "0.2.0", "marked": "^0.3.4", - "nodegit": "0.9.0", + "nodegit": "0.11.5", "normalize-package-data": "^2.0.0", "nslog": "^3", "oniguruma": "^5", @@ -83,7 +83,7 @@ "autosave": "0.23.1", "background-tips": "0.26.0", "bookmarks": "0.38.2", - "bracket-matcher": "0.80.0", + "bracket-matcher": "0.80.1", "command-palette": "0.38.0", "deprecation-cop": "0.54.1", "dev-live-reload": "0.47.0", @@ -112,7 +112,7 @@ "symbols-view": "0.111.1", "tabs": "0.90.2", "timecop": "0.33.1", - "tree-view": "0.201.4", + "tree-view": "0.201.5", "update-package-dependencies": "0.10.0", "welcome": "0.34.0", "whitespace": "0.32.2", diff --git a/resources/win/atom.cmd b/resources/win/atom.cmd index c9bfdd5ba..8d5e6f97a 100644 --- a/resources/win/atom.cmd +++ b/resources/win/atom.cmd @@ -35,8 +35,7 @@ IF "%EXPECT_OUTPUT%"=="YES" ( "%~dp0\..\..\atom.exe" --pid=%PID% %* rem If the wait flag is set, don't exit this process until Atom tells it to. goto waitLoop - ) - ELSE ( + ) ELSE ( "%~dp0\..\..\atom.exe" %* ) ) ELSE ( diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 8c228e2a8..888ccf6e2 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -18,8 +18,8 @@ describe "Pane", -> onDidDestroy: (fn) -> @emitter.on('did-destroy', fn) destroy: -> @destroyed = true; @emitter.emit('did-destroy') isDestroyed: -> @destroyed - isPending: -> @pending - pending: false + onDidTerminatePendingState: (callback) -> @emitter.on 'terminate-pending-state', callback + terminatePendingState: -> @emitter.emit 'terminate-pending-state' beforeEach -> confirm = spyOn(atom.applicationDelegate, 'confirm') @@ -136,10 +136,8 @@ describe "Pane", -> pane = new Pane(paneParams(items: [])) itemA = new Item("A") itemB = new Item("B") - itemA.pending = true - itemB.pending = true - pane.addItem(itemA) - pane.addItem(itemB) + pane.addItem(itemA, undefined, false, true) + pane.addItem(itemB, undefined, false, true) expect(itemA.isDestroyed()).toBe true describe "::activateItem(item)", -> @@ -172,21 +170,83 @@ describe "Pane", -> beforeEach -> itemC = new Item("C") itemD = new Item("D") - itemC.pending = true - itemD.pending = true it "replaces the active item if it is pending", -> - pane.activateItem(itemC) + pane.activateItem(itemC, true) expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'C', 'B'] - pane.activateItem(itemD) + pane.activateItem(itemD, true) expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'D', 'B'] it "adds the item after the active item if it is not pending", -> - pane.activateItem(itemC) + pane.activateItem(itemC, true) pane.activateItemAtIndex(2) - pane.activateItem(itemD) + pane.activateItem(itemD, true) expect(pane.getItems().map (item) -> item.name).toEqual ['A', 'B', 'D'] + describe "::setPendingItem", -> + pane = null + + beforeEach -> + pane = atom.workspace.getActivePane() + + it "changes the pending item", -> + expect(pane.getPendingItem()).toBeNull() + pane.setPendingItem("fake item") + expect(pane.getPendingItem()).toEqual "fake item" + + describe "::onItemDidTerminatePendingState callback", -> + pane = null + callbackCalled = false + + beforeEach -> + pane = atom.workspace.getActivePane() + callbackCalled = false + + it "is called when the pending item changes", -> + pane.setPendingItem("fake item one") + pane.onItemDidTerminatePendingState (item) -> + callbackCalled = true + expect(item).toEqual "fake item one" + pane.setPendingItem("fake item two") + expect(callbackCalled).toBeTruthy() + + it "has access to the new pending item via ::getPendingItem", -> + pane.setPendingItem("fake item one") + pane.onItemDidTerminatePendingState (item) -> + callbackCalled = true + expect(pane.getPendingItem()).toEqual "fake item two" + pane.setPendingItem("fake item two") + expect(callbackCalled).toBeTruthy() + + describe "::activateNextRecentlyUsedItem() and ::activatePreviousRecentlyUsedItem()", -> + it "sets the active item to the next/previous item in the itemStack, looping around at either end", -> + pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C"), new Item("D"), new Item("E")])) + [item1, item2, item3, item4, item5] = pane.getItems() + pane.itemStack = [item3, item1, item2, item5, item4] + + pane.activateItem(item4) + expect(pane.getActiveItem()).toBe item4 + pane.activateNextRecentlyUsedItem() + expect(pane.getActiveItem()).toBe item5 + pane.activateNextRecentlyUsedItem() + expect(pane.getActiveItem()).toBe item2 + pane.activatePreviousRecentlyUsedItem() + expect(pane.getActiveItem()).toBe item5 + pane.activatePreviousRecentlyUsedItem() + expect(pane.getActiveItem()).toBe item4 + pane.activatePreviousRecentlyUsedItem() + expect(pane.getActiveItem()).toBe item3 + pane.activatePreviousRecentlyUsedItem() + expect(pane.getActiveItem()).toBe item1 + pane.activateNextRecentlyUsedItem() + expect(pane.getActiveItem()).toBe item3 + pane.activateNextRecentlyUsedItem() + expect(pane.getActiveItem()).toBe item4 + pane.activateNextRecentlyUsedItem() + pane.moveActiveItemToTopOfStack() + expect(pane.getActiveItem()).toBe item5 + expect(pane.itemStack[4]).toBe item5 + describe "::activateNextItem() and ::activatePreviousItem()", -> it "sets the active item to the next/previous item, looping around at either end", -> pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")])) @@ -253,7 +313,7 @@ describe "Pane", -> pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")])) [item1, item2, item3] = pane.getItems() - it "removes the item from the items list and destroyes it", -> + it "removes the item from the items list and destroys it", -> expect(pane.getActiveItem()).toBe item1 pane.destroyItem(item2) expect(item2 in pane.getItems()).toBe false @@ -264,6 +324,23 @@ describe "Pane", -> expect(item1 in pane.getItems()).toBe false expect(item1.isDestroyed()).toBe true + it "removes the item from the itemStack", -> + pane.itemStack = [item2, item3, item1] + + pane.activateItem(item1) + expect(pane.getActiveItem()).toBe item1 + pane.destroyItem(item3) + expect(pane.itemStack).toEqual [item2, item1] + expect(pane.getActiveItem()).toBe item1 + + pane.destroyItem(item1) + expect(pane.itemStack).toEqual [item2] + expect(pane.getActiveItem()).toBe item2 + + pane.destroyItem(item2) + expect(pane.itemStack).toEqual [] + expect(pane.getActiveItem()).toBeUndefined() + it "invokes ::onWillDestroyItem() observers before destroying the item", -> events = [] pane.onWillDestroyItem (event) -> @@ -806,6 +883,67 @@ describe "Pane", -> pane2.destroy() expect(container.root).toBe pane1 + describe "pending state", -> + editor1 = null + pane = null + eventCount = null + + beforeEach -> + waitsForPromise -> + atom.workspace.open('sample.txt', pending: true).then (o) -> + editor1 = o + pane = atom.workspace.getActivePane() + + runs -> + eventCount = 0 + editor1.onDidTerminatePendingState -> eventCount++ + + it "does not open file in pending state by default", -> + waitsForPromise -> + atom.workspace.open('sample.js').then (o) -> + editor1 = o + pane = atom.workspace.getActivePane() + + runs -> + expect(pane.getPendingItem()).toBeNull() + + it "opens file in pending state if 'pending' option is true", -> + expect(pane.getPendingItem()).toEqual editor1 + + it "terminates pending state if ::terminatePendingState is invoked", -> + editor1.terminatePendingState() + + expect(pane.getPendingItem()).toBeNull() + expect(eventCount).toBe 1 + + it "terminates pending state when buffer is changed", -> + editor1.insertText('I\'ll be back!') + advanceClock(editor1.getBuffer().stoppedChangingDelay) + + expect(pane.getPendingItem()).toBeNull() + expect(eventCount).toBe 1 + + it "only calls terminate handler once when text is modified twice", -> + editor1.insertText('Some text') + advanceClock(editor1.getBuffer().stoppedChangingDelay) + + editor1.save() + + editor1.insertText('More text') + advanceClock(editor1.getBuffer().stoppedChangingDelay) + + expect(pane.getPendingItem()).toBeNull() + expect(eventCount).toBe 1 + + it "only calls clearPendingItem if there is a pending item to clear", -> + spyOn(pane, "clearPendingItem").andCallThrough() + + editor1.terminatePendingState() + editor1.terminatePendingState() + + expect(pane.getPendingItem()).toBeNull() + expect(pane.clearPendingItem.callCount).toBe 1 + describe "serialization", -> pane = null @@ -837,3 +975,30 @@ describe "Pane", -> pane.focus() newPane = Pane.deserialize(pane.serialize(), atom) expect(newPane.focused).toBe true + + it "can serialize and deserialize the order of the items in the itemStack", -> + [item1, item2, item3] = pane.getItems() + pane.itemStack = [item3, item1, item2] + newPane = Pane.deserialize(pane.serialize(), atom) + expect(newPane.itemStack).toEqual pane.itemStack + expect(newPane.itemStack[2]).toEqual item2 + + it "builds the itemStack if the itemStack is not serialized", -> + [item1, item2, item3] = pane.getItems() + newPane = Pane.deserialize(pane.serialize(), atom) + expect(newPane.getItems()).toEqual newPane.itemStack + + it "rebuilds the itemStack if items.length does not match itemStack.length", -> + [item1, item2, item3] = pane.getItems() + pane.itemStack = [item2, item3] + newPane = Pane.deserialize(pane.serialize(), atom) + expect(newPane.getItems()).toEqual newPane.itemStack + + it "does not serialize the reference to the items in the itemStack for pane items that will not be serialized", -> + [item1, item2, item3] = pane.getItems() + pane.itemStack = [item2, item1, item3] + unserializable = {} + pane.activateItem(unserializable) + + newPane = Pane.deserialize(pane.serialize(), atom) + expect(newPane.itemStack).toEqual [item2, item1, item3] diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 6959d4da5..6f16f0cf7 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -55,16 +55,6 @@ describe "TextEditor", -> expect(editor.tokenizedLineForScreenRow(0).invisibles.eol).toBe '?' - it "restores pending tabs in pending state", -> - expect(editor.isPending()).toBe false - editor2 = TextEditor.deserialize(editor.serialize(), atom) - expect(editor2.isPending()).toBe false - - pendingEditor = atom.workspace.buildTextEditor(pending: true) - expect(pendingEditor.isPending()).toBe true - editor3 = TextEditor.deserialize(pendingEditor.serialize(), atom) - expect(editor3.isPending()).toBe true - describe "when the editor is constructed with the largeFileMode option set to true", -> it "loads the editor but doesn't tokenize", -> editor = null @@ -5827,53 +5817,3 @@ describe "TextEditor", -> screenRange: marker1.getRange(), rangeIsReversed: false } - - describe "pending state", -> - editor1 = null - eventCount = null - - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.txt', pending: true).then (o) -> editor1 = o - - runs -> - eventCount = 0 - editor1.onDidTerminatePendingState -> eventCount++ - - it "does not open file in pending state by default", -> - expect(editor.isPending()).toBe false - - it "opens file in pending state if 'pending' option is true", -> - expect(editor1.isPending()).toBe true - - it "terminates pending state if ::terminatePendingState is invoked", -> - editor1.terminatePendingState() - - expect(editor1.isPending()).toBe false - expect(eventCount).toBe 1 - - it "terminates pending state when buffer is changed", -> - editor1.insertText('I\'ll be back!') - advanceClock(editor1.getBuffer().stoppedChangingDelay) - - expect(editor1.isPending()).toBe false - expect(eventCount).toBe 1 - - it "only calls terminate handler once when text is modified twice", -> - editor1.insertText('Some text') - advanceClock(editor1.getBuffer().stoppedChangingDelay) - - editor1.save() - - editor1.insertText('More text') - advanceClock(editor1.getBuffer().stoppedChangingDelay) - - expect(editor1.isPending()).toBe false - expect(eventCount).toBe 1 - - it "only calls terminate handler once when terminatePendingState is called twice", -> - editor1.terminatePendingState() - editor1.terminatePendingState() - - expect(editor1.isPending()).toBe false - expect(eventCount).toBe 1 diff --git a/spec/workspace-spec.coffee b/spec/workspace-spec.coffee index ef89636a8..78bbf2fdb 100644 --- a/spec/workspace-spec.coffee +++ b/spec/workspace-spec.coffee @@ -588,19 +588,22 @@ describe "Workspace", -> 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 - + atom.workspace.open('sample.js', pending: true).then (o) -> + editor = o + pane = atom.workspace.getActivePane() + runs -> - expect(editor.isPending()).toBe true - + expect(pane.getPendingItem()).toEqual editor + waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - + atom.workspace.open('sample.js') + runs -> - expect(editor.isPending()).toBe false - + expect(pane.getPendingItem()).toBeNull() + describe "::reopenItem()", -> it "opens the uri associated with the last closed pane that isn't currently open", -> pane = workspace.getActivePane() @@ -1551,11 +1554,12 @@ describe "Workspace", -> describe "when the core.allowPendingPaneItems option is falsey", -> it "does not open item with `pending: true` option as pending", -> - editor = null + pane = null atom.config.set('core.allowPendingPaneItems', false) waitsForPromise -> - atom.workspace.open('sample.js', pending: true).then (o) -> editor = o + atom.workspace.open('sample.js', pending: true).then -> + pane = atom.workspace.getActivePane() runs -> - expect(editor.isPending()).toBeFalsy() + expect(pane.getPendingItem()).toBeFalsy() diff --git a/src/browser/squirrel-update.coffee b/src/browser/squirrel-update.coffee index f9684ce49..9d6f772db 100644 --- a/src/browser/squirrel-update.coffee +++ b/src/browser/squirrel-update.coffee @@ -11,9 +11,11 @@ exeName = path.basename(process.execPath) if process.env.SystemRoot system32Path = path.join(process.env.SystemRoot, 'System32') regPath = path.join(system32Path, 'reg.exe') + powershellPath = path.join(system32Path, 'WindowsPowerShell', 'v1.0', 'powershell.exe') setxPath = path.join(system32Path, 'setx.exe') else regPath = 'reg.exe' + powershellPath = 'powershell.exe' setxPath = 'setx.exe' # Registry keys used for context menu @@ -44,11 +46,31 @@ spawn = (command, args, callback) -> error?.code ?= code error?.stdout ?= stdout callback?(error, stdout) + # This is necessary if using Powershell 2 on Windows 7 to get the events to raise + # http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs + spawnedProcess.stdin.end() + # Spawn reg.exe and callback when it completes spawnReg = (args, callback) -> spawn(regPath, args, callback) +# Spawn powershell.exe and callback when it completes +spawnPowershell = (args, callback) -> + # set encoding and execute the command, capture the output, and return it via .NET's console in order to have consistent UTF-8 encoding + # http://stackoverflow.com/questions/22349139/utf-8-output-from-powershell + # to address https://github.com/atom/atom/issues/5063 + args[0] = """ + [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 + $output=#{args[0]} + [Console]::WriteLine($output) + """ + args.unshift('-command') + args.unshift('RemoteSigned') + args.unshift('-ExecutionPolicy') + args.unshift('-noprofile') + spawn(powershellPath, args, callback) + # Spawn setx.exe and callback when it completes spawnSetx = (args, callback) -> spawn(setxPath, args, callback) @@ -82,46 +104,14 @@ installContextMenu = (callback) -> installMenu backgroundKeyPath, '%V', -> installFileHandler(callback) -isAscii = (text) -> - index = 0 - while index < text.length - return false if text.charCodeAt(index) > 127 - index++ - true - # Get the user's PATH environment variable registry value. getPath = (callback) -> - spawnReg ['query', environmentKeyPath, '/v', 'Path'], (error, stdout) -> + spawnPowershell ['[environment]::GetEnvironmentVariable(\'Path\',\'User\')'], (error, stdout) -> if error? - if error.code is 1 - # FIXME Don't overwrite path when reading value is disabled - # https://github.com/atom/atom/issues/5092 - if stdout.indexOf('ERROR: Registry editing has been disabled by your administrator.') isnt -1 - return callback(error) + return callback(error) - # The query failed so the Path does not exist yet in the registry - return callback(null, '') - else - return callback(error) - - # Registry query output is in the form: - # - # HKEY_CURRENT_USER\Environment - # Path REG_SZ C:\a\folder\on\the\path;C\another\folder - # - - lines = stdout.split(/[\r\n]+/).filter (line) -> line - segments = lines[lines.length - 1]?.split(' ') - if segments[1] is 'Path' and segments.length >= 3 - pathEnv = segments?[3..].join(' ') - if isAscii(pathEnv) - callback(null, pathEnv) - else - # FIXME Don't corrupt non-ASCII PATH values - # https://github.com/atom/atom/issues/5063 - callback(new Error('PATH contains non-ASCII values')) - else - callback(new Error('Registry query for PATH failed')) + pathOutput = stdout.replace(/^\s+|\s+$/g, '') + callback(null, pathOutput) # Uninstall the Open with Atom explorer context menu items via the registry. uninstallContextMenu = (callback) -> diff --git a/src/pane.coffee b/src/pane.coffee index 9dff81d5c..25f421934 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -1,5 +1,5 @@ {find, compact, extend, last} = require 'underscore-plus' -{Emitter} = require 'event-kit' +{CompositeDisposable, Emitter} = require 'event-kit' Model = require './model' PaneAxis = require './pane-axis' TextEditor = require './text-editor' @@ -8,6 +8,11 @@ TextEditor = require './text-editor' # Panes can contain multiple items, one of which is *active* at a given time. # The view corresponding to the active item is displayed in the interface. In # the default configuration, tabs are also displayed for each item. +# +# Each pane may also contain one *pending* item. When a pending item is added +# to a pane, it will replace the currently pending item, if any, instead of +# simply being added. In the default configuration, the text in the tab for +# pending items is shown in italics. module.exports = class Pane extends Model container: undefined @@ -15,7 +20,7 @@ class Pane extends Model focused: false @deserialize: (state, {deserializers, applicationDelegate, config, notifications}) -> - {items, activeItemURI, activeItemUri} = state + {items, itemStackIndices, activeItemURI, activeItemUri} = state activeItemURI ?= activeItemUri state.items = compact(items.map (itemState) -> deserializers.deserialize(itemState)) state.activeItem = find state.items, (item) -> @@ -37,20 +42,25 @@ class Pane extends Model } = params @emitter = new Emitter - @itemSubscriptions = new WeakMap + @subscriptionsPerItem = new WeakMap @items = [] + @itemStack = [] @addItems(compact(params?.items ? [])) @setActiveItem(@items[0]) unless @getActiveItem()? + @addItemsToStack(params?.itemStackIndices ? []) @setFlexScale(params?.flexScale ? 1) serialize: -> if typeof @activeItem?.getURI is 'function' activeItemURI = @activeItem.getURI() + itemsToBeSerialized = compact(@items.map((item) -> item if typeof item.serialize is 'function')) + itemStackIndices = (itemsToBeSerialized.indexOf(item) for item in @itemStack when typeof item.serialize is 'function') deserializer: 'Pane' id: @id - items: compact(@items.map((item) -> item.serialize?())) + items: itemsToBeSerialized.map((item) -> item.serialize()) + itemStackIndices: itemStackIndices activeItemURI: activeItemURI focused: @focused flexScale: @flexScale @@ -260,8 +270,8 @@ class Pane extends Model getPanes: -> [this] unsubscribeFromItem: (item) -> - @itemSubscriptions.get(item)?.dispose() - @itemSubscriptions.delete(item) + @subscriptionsPerItem.get(item)?.dispose() + @subscriptionsPerItem.delete(item) ### Section: Items @@ -278,12 +288,30 @@ class Pane extends Model # Returns a pane item. getActiveItem: -> @activeItem - setActiveItem: (activeItem) -> + setActiveItem: (activeItem, options) -> + {modifyStack} = options if options? unless activeItem is @activeItem + @addItemToStack(activeItem) unless modifyStack is false @activeItem = activeItem @emitter.emit 'did-change-active-item', @activeItem @activeItem + # Build the itemStack after deserializing + addItemsToStack: (itemStackIndices) -> + if @items.length > 0 + if itemStackIndices.length is 0 or itemStackIndices.length isnt @items.length or itemStackIndices.indexOf(-1) >= 0 + itemStackIndices = (i for i in [0..@items.length-1]) + for itemIndex in itemStackIndices + @addItemToStack(@items[itemIndex]) + return + + # Add item (or move item) to the end of the itemStack + addItemToStack: (newItem) -> + return unless newItem? + index = @itemStack.indexOf(newItem) + @itemStack.splice(index, 1) unless index is -1 + @itemStack.push(newItem) + # Return an {TextEditor} if the pane item is an {TextEditor}, or null otherwise. getActiveEditor: -> @activeItem if @activeItem instanceof TextEditor @@ -296,6 +324,29 @@ class Pane extends Model itemAtIndex: (index) -> @items[index] + # Makes the next item in the itemStack active. + activateNextRecentlyUsedItem: -> + if @items.length > 1 + @itemStackIndex = @itemStack.length - 1 unless @itemStackIndex? + @itemStackIndex = @itemStack.length if @itemStackIndex is 0 + @itemStackIndex = @itemStackIndex - 1 + nextRecentlyUsedItem = @itemStack[@itemStackIndex] + @setActiveItem(nextRecentlyUsedItem, modifyStack: false) + + # Makes the previous item in the itemStack active. + activatePreviousRecentlyUsedItem: -> + if @items.length > 1 + if @itemStackIndex + 1 is @itemStack.length or not @itemStackIndex? + @itemStackIndex = -1 + @itemStackIndex = @itemStackIndex + 1 + previousRecentlyUsedItem = @itemStack[@itemStackIndex] + @setActiveItem(previousRecentlyUsedItem, modifyStack: false) + + # Moves the active item to the end of the itemStack once the ctrl key is lifted + moveActiveItemToTopOfStack: -> + delete @itemStackIndex + @addItemToStack(@activeItem) + # Public: Makes the next item active. activateNextItem: -> index = @getActiveItemIndex() @@ -342,13 +393,17 @@ class Pane extends Model # Public: Make the given item *active*, causing it to be displayed by # the pane's view. - activateItem: (item) -> + # + # * `pending` (optional) {Boolean} indicating that the item should be added + # in a pending state if it does not yet exist in the pane. Existing pending + # items in a pane are replaced with new pending items when they are opened. + activateItem: (item, pending=false) -> if item? - if @activeItem?.isPending?() + if @getPendingItem() is @activeItem index = @getActiveItemIndex() else index = @getActiveItemIndex() + 1 - @addItem(item, index, false) + @addItem(item, index, false, pending) @setActiveItem(item) # Public: Add the given item to the pane. @@ -357,28 +412,50 @@ class Pane extends Model # view. # * `index` (optional) {Number} indicating the index at which to add the item. # If omitted, the item is added after the current active item. + # * `pending` (optional) {Boolean} indicating that the item should be + # added in a pending state. Existing pending items in a pane are replaced with + # new pending items when they are opened. # # Returns the added item. - addItem: (item, index=@getActiveItemIndex() + 1, moved=false) -> + addItem: (item, index=@getActiveItemIndex() + 1, moved=false, pending=false) -> throw new Error("Pane items must be objects. Attempted to add item #{item}.") unless item? and typeof item is 'object' throw new Error("Adding a pane item with URI '#{item.getURI?()}' that has already been destroyed") if item.isDestroyed?() return if item in @items - if item.isPending?() - for existingItem, i in @items - if existingItem.isPending?() - @destroyItem(existingItem) - break + pendingItem = @getPendingItem() + @destroyItem(pendingItem) if pendingItem? + @setPendingItem(item) if pending if typeof item.onDidDestroy is 'function' - @itemSubscriptions.set item, item.onDidDestroy => @removeItem(item, false) + itemSubscriptions = new CompositeDisposable + itemSubscriptions.add item.onDidDestroy => @removeItem(item, false) + if typeof item.onDidTerminatePendingState is "function" + itemSubscriptions.add item.onDidTerminatePendingState => + @clearPendingItem() if @getPendingItem() is item + itemSubscriptions.add item.onDidDestroy => @removeItem(item, false) + @subscriptionsPerItem.set item, itemSubscriptions @items.splice(index, 0, item) @emitter.emit 'did-add-item', {item, index, moved} @setActiveItem(item) unless @getActiveItem()? item + setPendingItem: (item) => + if @pendingItem isnt item + mostRecentPendingItem = @pendingItem + @pendingItem = item + @emitter.emit 'item-did-terminate-pending-state', mostRecentPendingItem + + getPendingItem: => + @pendingItem or null + + clearPendingItem: => + @setPendingItem(null) + + onItemDidTerminatePendingState: (callback) => + @emitter.on 'item-did-terminate-pending-state', callback + # Public: Add the given items to the pane. # # * `items` An {Array} of items to add. Items can be views or models with @@ -396,7 +473,8 @@ class Pane extends Model removeItem: (item, moved) -> index = @items.indexOf(item) return if index is -1 - + @pendingItem = null if @getPendingItem() is item + @removeItemFromStack(item) @emitter.emit 'will-remove-item', {item, index, destroyed: not moved, moved} @unsubscribeFromItem(item) @@ -412,6 +490,14 @@ class Pane extends Model @container?.didDestroyPaneItem({item, index, pane: this}) unless moved @destroy() if @items.length is 0 and @config.get('core.destroyEmptyPanes') + # Remove the given item from the itemStack. + # + # * `item` The item to remove. + # * `index` {Number} indicating the index to which to remove the item from the itemStack. + removeItemFromStack: (item) -> + index = @itemStack.indexOf(item) + @itemStack.splice(index, 1) unless index is -1 + # Public: Move the given item to the given index. # # * `item` The item to move. diff --git a/src/register-default-commands.coffee b/src/register-default-commands.coffee index 1b1aad2cf..dacb1d228 100644 --- a/src/register-default-commands.coffee +++ b/src/register-default-commands.coffee @@ -2,6 +2,9 @@ module.exports = ({commandRegistry, commandInstaller, config}) -> commandRegistry.add 'atom-workspace', + 'pane:show-next-recently-used-item': -> @getModel().getActivePane().activateNextRecentlyUsedItem() + 'pane:show-previous-recently-used-item': -> @getModel().getActivePane().activatePreviousRecentlyUsedItem() + 'pane:move-active-item-to-top-of-stack': -> @getModel().getActivePane().moveActiveItemToTopOfStack() 'pane:show-next-item': -> @getModel().getActivePane().activateNextItem() 'pane:show-previous-item': -> @getModel().getActivePane().activatePreviousItem() 'pane:show-item-1': -> @getModel().getActivePane().activateItemAtIndex(0) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 2823b993e..fd16db603 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -97,7 +97,7 @@ class TextEditor extends Model softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, @notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry, - @project, @assert, @applicationDelegate, @pending, grammarName, ignoreInvisibles, @autoHeight, @ignoreScrollPastEnd + @project, @assert, @applicationDelegate, grammarName, ignoreInvisibles, @autoHeight, @ignoreScrollPastEnd } = params throw new Error("Must pass a config parameter when constructing TextEditors") unless @config? @@ -118,6 +118,7 @@ class TextEditor extends Model @selections = [] @autoHeight ?= true @ignoreScrollPastEnd ?= false + @hasTerminatedPendingState = false buffer ?= new TextBuffer @displayBuffer ?= new DisplayBuffer({ @@ -161,7 +162,6 @@ class TextEditor extends Model firstVisibleScreenColumn: @getFirstVisibleScreenColumn() displayBuffer: @displayBuffer.serialize() selectionsMarkerLayerId: @selectionsMarkerLayer.id - pending: @isPending() subscribeToBuffer: -> @buffer.retain() @@ -173,12 +173,18 @@ class TextEditor extends Model @disposables.add @buffer.onDidChangeEncoding => @emitter.emit 'did-change-encoding', @getEncoding() @disposables.add @buffer.onDidDestroy => @destroy() - if @pending - @disposables.add @buffer.onDidChangeModified => - @terminatePendingState() if @buffer.isModified() + @disposables.add @buffer.onDidChangeModified => + @terminatePendingState() if not @hasTerminatedPendingState and @buffer.isModified() @preserveCursorPositionOnBufferReload() + terminatePendingState: -> + @emitter.emit 'did-terminate-pending-state' if not @hasTerminatedPendingState + @hasTerminatedPendingState = true + + onDidTerminatePendingState: (callback) -> + @emitter.on 'did-terminate-pending-state', callback + subscribeToDisplayBuffer: -> @disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this) @disposables.add @displayBuffer.onDidChangeGrammar @handleGrammarChange.bind(this) @@ -585,13 +591,6 @@ class TextEditor extends Model getEditorWidthInChars: -> @displayBuffer.getEditorWidthInChars() - onDidTerminatePendingState: (callback) -> - @emitter.on 'did-terminate-pending-state', callback - - terminatePendingState: -> - return if not @pending - @pending = false - @emitter.emit 'did-terminate-pending-state' ### Section: File Details @@ -676,9 +675,6 @@ class TextEditor extends Model # Essential: Returns {Boolean} `true` if this editor has no content. isEmpty: -> @buffer.isEmpty() - # Returns {Boolean} `true` if this editor is pending and `false` if it is permanent. - isPending: -> Boolean(@pending) - # Copies the current file path to the native clipboard. copyPathToClipboard: (relative = false) -> if filePath = @getPath() diff --git a/src/workspace.coffee b/src/workspace.coffee index 31c381428..afc778c4b 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -409,6 +409,9 @@ class Workspace extends Model # 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 @@ -483,7 +486,7 @@ class Workspace extends Model if uri? if item = pane.itemForURI(uri) - item.terminatePendingState?() if item.isPending?() and not options.pending + pane.clearPendingItem() if not options.pending and pane.getPendingItem() is item item ?= opener(uri, options) for opener in @getOpeners() when not item try @@ -506,7 +509,7 @@ class Workspace extends Model return item if pane.isDestroyed() @itemOpened(item) - pane.activateItem(item) if activateItem + pane.activateItem(item, options.pending) if activateItem pane.activate() if activatePane initialLine = initialColumn = 0