diff --git a/apm/package.json b/apm/package.json index 4ddb4dabe..d4fcc851a 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.9.3" + "atom-package-manager": "1.10.0" } } diff --git a/package.json b/package.json index 6d390354d..814ba0033 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "key-path-helpers": "^0.4.0", "less-cache": "0.23", "line-top-index": "0.2.0", - "marked": "^0.3.4", + "marked": "^0.3.5", "normalize-package-data": "^2.0.0", "nslog": "^3", "ohnogit": "0.0.11", @@ -77,7 +77,7 @@ "autocomplete-atom-api": "0.10.0", "autocomplete-css": "0.11.1", "autocomplete-html": "0.7.2", - "autocomplete-plus": "2.30.0", + "autocomplete-plus": "2.31.0", "autocomplete-snippets": "1.10.0", "autoflow": "0.27.0", "autosave": "0.23.1", @@ -87,7 +87,7 @@ "command-palette": "0.38.0", "deprecation-cop": "0.54.1", "dev-live-reload": "0.47.0", - "encoding-selector": "0.21.0", + "encoding-selector": "0.22.0", "exception-reporting": "0.38.1", "fuzzy-finder": "1.0.5", "git-diff": "1.0.1", @@ -97,20 +97,20 @@ "image-view": "0.57.0", "incompatible-packages": "0.26.1", "keybinding-resolver": "0.35.0", - "line-ending-selector": "0.4.1", + "line-ending-selector": "0.5.0", "link": "0.31.1", "markdown-preview": "0.158.0", "metrics": "0.53.1", "notifications": "0.63.2", "open-on-github": "1.1.0", "package-generator": "1.0.0", - "settings-view": "0.235.1", + "settings-view": "0.236.0", "snippets": "1.0.2", "spell-check": "0.67.1", "status-bar": "1.2.6", "styleguide": "0.45.2", - "symbols-view": "0.112.0", - "tabs": "0.93.1", + "symbols-view": "0.113.0", + "tabs": "0.93.2", "timecop": "0.33.1", "tree-view": "0.206.2", "update-package-dependencies": "0.10.0", diff --git a/spec/command-registry-spec.coffee b/spec/command-registry-spec.coffee index ecdd42fd6..aaf044b1d 100644 --- a/spec/command-registry-spec.coffee +++ b/spec/command-registry-spec.coffee @@ -74,6 +74,13 @@ describe "CommandRegistry", -> grandchild.dispatchEvent(new CustomEvent('command', bubbles: true)) expect(calls).toEqual ['.foo.bar', '.bar', '.foo'] + it "orders inline listeners by reverse registration order", -> + calls = [] + registry.add child, 'command', -> calls.push('child1') + registry.add child, 'command', -> calls.push('child2') + child.dispatchEvent(new CustomEvent('command', bubbles: true)) + expect(calls).toEqual ['child2', 'child1'] + it "stops bubbling through ancestors when .stopPropagation() is called on the event", -> calls = [] diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 9969b1dcc..8abbb0ece 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -917,6 +917,82 @@ describe "Pane", -> expect(item1.save).not.toHaveBeenCalled() expect(pane.isDestroyed()).toBe false + describe "when item fails to save", -> + [pane, item1, item2] = [] + + beforeEach -> + pane = new Pane({items: [new Item("A"), new Item("B")], applicationDelegate: atom.applicationDelegate, config: atom.config}) + [item1, item2] = pane.getItems() + + item1.shouldPromptToSave = -> true + item1.getURI = -> "/test/path" + + item1.save = jasmine.createSpy("save").andCallFake -> + error = new Error("EACCES, permission denied '/test/path'") + error.path = '/test/path' + error.code = 'EACCES' + throw error + + it "does not destroy the pane if save fails and user clicks cancel", -> + confirmations = 0 + confirm.andCallFake -> + confirmations++ + if confirmations is 1 + return 0 # click save + else + return 1 # click cancel + + pane.close() + + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() + expect(confirmations).toBe(2) + expect(item1.save).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe false + + it "does destroy the pane if the user saves the file under a new name", -> + item1.saveAs = jasmine.createSpy("saveAs").andReturn(true) + + confirmations = 0 + confirm.andCallFake -> + confirmations++ + return 0 # save and then save as + + showSaveDialog.andReturn("new/path") + + pane.close() + + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() + expect(confirmations).toBe(2) + expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled() + expect(item1.save).toHaveBeenCalled() + expect(item1.saveAs).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe true + + it "asks again if the saveAs also fails", -> + item1.saveAs = jasmine.createSpy("saveAs").andCallFake -> + error = new Error("EACCES, permission denied '/test/path'") + error.path = '/test/path' + error.code = 'EACCES' + throw error + + confirmations = 0 + confirm.andCallFake -> + confirmations++ + if confirmations < 3 + return 0 # save, save as, save as + return 2 # don't save + + showSaveDialog.andReturn("new/path") + + pane.close() + + expect(atom.applicationDelegate.confirm).toHaveBeenCalled() + expect(confirmations).toBe(3) + expect(atom.applicationDelegate.showSaveDialog).toHaveBeenCalled() + expect(item1.save).toHaveBeenCalled() + expect(item1.saveAs).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe true + describe "::destroy()", -> [container, pane1, pane2] = [] diff --git a/spec/text-editor-registry-spec.coffee b/spec/text-editor-registry-spec.coffee index 04665bef2..80f29f897 100644 --- a/spec/text-editor-registry-spec.coffee +++ b/spec/text-editor-registry-spec.coffee @@ -10,6 +10,7 @@ describe "TextEditorRegistry", -> it "gets added to the list of registered editors", -> editor = {} registry.add(editor) + expect(editor.registered).toBe true expect(registry.editors.size).toBe 1 expect(registry.editors.has(editor)).toBe(true) @@ -19,6 +20,16 @@ describe "TextEditorRegistry", -> expect(registry.editors.size).toBe 1 disposable.dispose() expect(registry.editors.size).toBe 0 + expect(editor.registered).toBe false + + it "can be removed", -> + editor = {} + registry.add(editor) + expect(registry.editors.size).toBe 1 + success = registry.remove(editor) + expect(success).toBe true + expect(registry.editors.size).toBe 0 + expect(editor.registered).toBe false describe "when the registry is observed", -> it "calls the callback for current and future editors until unsubscribed", -> diff --git a/src/command-registry.coffee b/src/command-registry.coffee index db2cf498d..955a1b540 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -244,11 +244,14 @@ class CommandRegistry (@selectorBasedListenersByCommandName[event.type] ? []) .filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector) .sort (a, b) -> a.compare(b) - listeners = listeners.concat(selectorBasedListeners) + listeners = selectorBasedListeners.concat(listeners) matched = true if listeners.length > 0 - for listener in listeners + # Call inline listeners first in reverse registration order, + # and selector-based listeners by specificity and reverse + # registration order. + for listener in listeners by -1 break if immediatePropagationStopped listener.callback.call(currentTarget, dispatchedEvent) @@ -271,8 +274,8 @@ class SelectorBasedListener @sequenceNumber = SequenceCount++ compare: (other) -> - other.specificity - @specificity or - other.sequenceNumber - @sequenceNumber + @specificity - other.specificity or + @sequenceNumber - other.sequenceNumber class InlineListener constructor: (@callback) -> diff --git a/src/pane.coffee b/src/pane.coffee index e8858a2b9..add6a365b 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -577,15 +577,23 @@ class Pane extends Model else return true - chosen = @applicationDelegate.confirm - message: "'#{item.getTitle?() ? uri}' has changes, do you want to save them?" - detailedMessage: "Your changes will be lost if you close this item without saving." - buttons: ["Save", "Cancel", "Don't Save"] + saveDialog = (saveButtonText, saveFn, message) => + chosen = @applicationDelegate.confirm + message: message + detailedMessage: "Your changes will be lost if you close this item without saving." + buttons: [saveButtonText, "Cancel", "Don't save"] + switch chosen + when 0 then saveFn(item, saveError) + when 1 then false + when 2 then true - switch chosen - when 0 then @saveItem(item, -> true) - when 1 then false - when 2 then true + saveError = (error) => + if error + saveDialog("Save as", @saveItemAs, "'#{item.getTitle?() ? uri}' could not be saved.\nError: #{@getMessageForErrorCode(error.code)}") + else + true + + saveDialog("Save", @saveItem, "'#{item.getTitle?() ? uri}' has changes, do you want to save them?") # Public: Save the active item. saveActiveItem: (nextAction) -> @@ -602,9 +610,11 @@ class Pane extends Model # Public: Save the given item. # # * `item` The item to save. - # * `nextAction` (optional) {Function} which will be called after the item is - # successfully saved. - saveItem: (item, nextAction) -> + # * `nextAction` (optional) {Function} which will be called with no argument + # after the item is successfully saved, or with the error if it failed. + # The return value will be that of `nextAction` or `undefined` if it was not + # provided + saveItem: (item, nextAction) => if typeof item?.getURI is 'function' itemURI = item.getURI() else if typeof item?.getUri is 'function' @@ -613,9 +623,12 @@ class Pane extends Model if itemURI? try item.save?() + nextAction?() catch error - @handleSaveError(error, item) - nextAction?() + if nextAction + nextAction(error) + else + @handleSaveError(error, item) else @saveItemAs(item, nextAction) @@ -623,9 +636,11 @@ class Pane extends Model # path they select. # # * `item` The item to save. - # * `nextAction` (optional) {Function} which will be called after the item is - # successfully saved. - saveItemAs: (item, nextAction) -> + # * `nextAction` (optional) {Function} which will be called with no argument + # after the item is successfully saved, or with the error if it failed. + # The return value will be that of `nextAction` or `undefined` if it was not + # provided + saveItemAs: (item, nextAction) => return unless item?.saveAs? saveOptions = item.getSaveDialogOptions?() ? {} @@ -634,9 +649,12 @@ class Pane extends Model if newItemPath try item.saveAs(newItemPath) + nextAction?() catch error - @handleSaveError(error, item) - nextAction?() + if nextAction + nextAction(error) + else + @handleSaveError(error, item) # Public: Save all items. saveItems: -> diff --git a/src/text-editor-registry.coffee b/src/text-editor-registry.coffee index 8a17335d4..e31630fee 100644 --- a/src/text-editor-registry.coffee +++ b/src/text-editor-registry.coffee @@ -26,8 +26,20 @@ class TextEditorRegistry # editor is destroyed. add: (editor) -> @editors.add(editor) + editor.registered = true + @emitter.emit 'did-add-editor', editor - new Disposable => @editors.delete(editor) + new Disposable => @remove(editor) + + # Remove a `TextEditor`. + # + # * `editor` The editor to remove. + # + # Returns a {Boolean} indicating whether the editor was successfully removed. + remove: (editor) -> + removed = @editors.delete(editor) + editor.registered = false + removed # Invoke the given callback with all the current and future registered # `TextEditors`. diff --git a/src/text-editor.coffee b/src/text-editor.coffee index b778491d1..9607f3437 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -76,6 +76,7 @@ class TextEditor extends Model defaultCharWidth: null height: null width: null + registered: false Object.defineProperty @prototype, "element", get: -> @getElement() @@ -202,7 +203,7 @@ class TextEditor extends Model tokenizedBuffer: tokenizedBufferState largeFileMode: @largeFileMode displayLayerId: @displayLayer.id - registered: atom.textEditors.editors.has this + registered: @registered subscribeToBuffer: -> @buffer.retain() diff --git a/src/workspace.coffee b/src/workspace.coffee index f75f00bc6..5e9de93dd 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -1092,7 +1092,7 @@ class Workspace extends Model if editor.getPath() checkoutHead = => @project.repositoryForDirectory(new Directory(editor.getDirectoryPath())) - .then (repository) => + .then (repository) -> repository?.async.checkoutHeadForEditor(editor) if @config.get('editor.confirmCheckoutHeadRevision')