diff --git a/spec/atom-environment-spec.coffee b/spec/atom-environment-spec.coffee index 3365fc1b0..5d269a3bb 100644 --- a/spec/atom-environment-spec.coffee +++ b/spec/atom-environment-spec.coffee @@ -477,8 +477,6 @@ describe "AtomEnvironment", -> devMode: atom.inDevMode() safeMode: atom.inSafeMode() - - describe "::unloadEditorWindow()", -> it "saves the BlobStore so it can be loaded after reload", -> configDirPath = temp.mkdirSync('atom-spec-environment') diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 5929b6ac9..1fd1b6a23 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -207,7 +207,7 @@ describe('AtomApplication', function () { sendBackToMainProcess(null) }) }) - await window1.saveState() + await window1.prepareToUnload() window1.close() await window1.closedPromise @@ -221,7 +221,7 @@ describe('AtomApplication', function () { sendBackToMainProcess(textEditor.getText()) }) assert.equal(window2Text, 'Hello World! How are you?') - await window2.saveState() + await window2.prepareToUnload() window2.close() await window2.closedPromise @@ -354,8 +354,8 @@ describe('AtomApplication', function () { ]) await Promise.all([ - app1Window1.saveState(), - app1Window2.saveState() + app1Window1.prepareToUnload(), + app1Window2.prepareToUnload() ]) const atomApplication2 = buildAtomApplication() @@ -471,7 +471,7 @@ describe('AtomApplication', function () { await focusWindow(window2) electron.app.quit() assert(!electron.app.hasQuitted()) - await Promise.all([window1.lastSaveStatePromise, window2.lastSaveStatePromise]) + await Promise.all([window1.lastPrepareToUnloadPromise, window2.lastPrepareToUnloadPromise]) assert(electron.app.hasQuitted()) }) }) diff --git a/spec/pane-container-element-spec.coffee b/spec/pane-container-element-spec.coffee index 9f25e13eb..7ea1cc8a0 100644 --- a/spec/pane-container-element-spec.coffee +++ b/spec/pane-container-element-spec.coffee @@ -149,11 +149,11 @@ describe "PaneContainerElement", -> ) expectPaneScale [leftPane, 0.5], [middlePane, 0.75], [rightPane, 1.75] - middlePane.close() - expectPaneScale [leftPane, 0.44], [rightPane, 1.55] + waitsForPromise -> middlePane.close() + runs -> expectPaneScale [leftPane, 0.44], [rightPane, 1.55] - leftPane.close() - expectPaneScale [rightPane, 1] + waitsForPromise -> leftPane.close() + runs -> expectPaneScale [rightPane, 1] it "splits or closes panes in orthogonal direction that the pane is being dragged", -> leftPane = container.getActivePane() @@ -173,8 +173,8 @@ describe "PaneContainerElement", -> expectPaneScale [lowerPane, 1], [leftPane, 1], [leftPane.getParent(), 0.5] # dynamically close pane, the pane's flexscale will recorver to origin value - lowerPane.close() - expectPaneScale [leftPane, 0.5], [rightPane, 1.5] + waitsForPromise -> lowerPane.close() + runs -> expectPaneScale [leftPane, 0.5], [rightPane, 1.5] it "unsubscribes from mouse events when the pane is detached", -> container.getActivePane().splitRight() diff --git a/spec/pane-container-spec.coffee b/spec/pane-container-spec.coffee index a232eaecd..587fbae6f 100644 --- a/spec/pane-container-spec.coffee +++ b/spec/pane-container-spec.coffee @@ -200,15 +200,17 @@ describe "PaneContainer", -> it "returns true if the user saves all modified files when prompted", -> confirm.andReturn(0) - saved = container.confirmClose() - expect(saved).toBeTruthy() - expect(confirm).toHaveBeenCalled() + waitsForPromise -> + container.confirmClose().then (saved) -> + expect(confirm).toHaveBeenCalled() + expect(saved).toBeTruthy() it "returns false if the user cancels saving any modified file", -> confirm.andReturn(1) - saved = container.confirmClose() - expect(saved).toBeFalsy() - expect(confirm).toHaveBeenCalled() + waitsForPromise -> + container.confirmClose().then (saved) -> + expect(confirm).toHaveBeenCalled() + expect(saved).toBeFalsy() describe "::onDidAddPane(callback)", -> it "invokes the given callback when panes are added", -> diff --git a/spec/pane-spec.coffee b/spec/pane-spec.coffee index 11b80c66c..610f51fc8 100644 --- a/spec/pane-spec.coffee +++ b/spec/pane-spec.coffee @@ -460,11 +460,12 @@ describe "Pane", -> it "saves the item before destroying it", -> itemURI = "test" confirm.andReturn(0) - pane.destroyItem(item1) - expect(item1.save).toHaveBeenCalled() - expect(item1 in pane.getItems()).toBe false - expect(item1.isDestroyed()).toBe true + waitsForPromise -> + pane.destroyItem(item1).then -> + expect(item1.save).toHaveBeenCalled() + expect(item1 in pane.getItems()).toBe false + expect(item1.isDestroyed()).toBe true describe "when the item has no uri", -> it "presents a save-as dialog, then saves the item with the given uri before removing and destroying it", -> @@ -472,21 +473,23 @@ describe "Pane", -> showSaveDialog.andReturn("/selected/path") confirm.andReturn(0) - pane.destroyItem(item1) - expect(showSaveDialog).toHaveBeenCalled() - expect(item1.saveAs).toHaveBeenCalledWith("/selected/path") - expect(item1 in pane.getItems()).toBe false - expect(item1.isDestroyed()).toBe true + waitsForPromise -> + pane.destroyItem(item1).then -> + expect(showSaveDialog).toHaveBeenCalled() + expect(item1.saveAs).toHaveBeenCalledWith("/selected/path") + expect(item1 in pane.getItems()).toBe false + expect(item1.isDestroyed()).toBe true describe "if the [Don't Save] option is selected", -> it "removes and destroys the item without saving it", -> confirm.andReturn(2) - pane.destroyItem(item1) - expect(item1.save).not.toHaveBeenCalled() - expect(item1 in pane.getItems()).toBe false - expect(item1.isDestroyed()).toBe true + waitsForPromise -> + pane.destroyItem(item1).then -> + expect(item1.save).not.toHaveBeenCalled() + expect(item1 in pane.getItems()).toBe false + expect(item1.isDestroyed()).toBe true describe "if the [Cancel] option is selected", -> it "does not save, remove, or destroy the item", -> @@ -550,11 +553,14 @@ describe "Pane", -> it "destroys all items", -> pane = new Pane(paneParams(items: [new Item("A"), new Item("B"), new Item("C")])) [item1, item2, item3] = pane.getItems() - pane.destroyItems() - expect(item1.isDestroyed()).toBe true - expect(item2.isDestroyed()).toBe true - expect(item3.isDestroyed()).toBe true - expect(pane.getItems()).toEqual [] + + waitsForPromise -> pane.destroyItems() + + runs -> + expect(item1.isDestroyed()).toBe true + expect(item2.isDestroyed()).toBe true + expect(item3.isDestroyed()).toBe true + expect(pane.getItems()).toEqual [] describe "::observeItems()", -> it "invokes the observer with all current and future items", -> @@ -620,24 +626,22 @@ describe "Pane", -> pane.saveActiveItem() expect(showSaveDialog).not.toHaveBeenCalled() - describe "when the item's saveAs method throws a well-known IO error", -> - notificationSpy = null - beforeEach -> - atom.notifications.onDidAddNotification notificationSpy = jasmine.createSpy() - + describe "when the item's saveAs rejects with a well-known IO error", -> it "creates a notification", -> pane.getActiveItem().saveAs = -> error = new Error("EACCES, permission denied '/foo'") error.path = '/foo' error.code = 'EACCES' - throw error + Promise.reject(error) - pane.saveActiveItem() - expect(notificationSpy).toHaveBeenCalled() - notification = notificationSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'warning' - expect(notification.getMessage()).toContain 'Permission denied' - expect(notification.getMessage()).toContain '/foo' + waitsFor (done) -> + subscription = atom.notifications.onDidAddNotification (notification) -> + expect(notification.getType()).toBe 'warning' + expect(notification.getMessage()).toContain 'Permission denied' + expect(notification.getMessage()).toContain '/foo' + subscription.dispose() + done() + pane.saveActiveItem() describe "::saveActiveItemAs()", -> pane = null @@ -661,23 +665,21 @@ describe "Pane", -> expect(showSaveDialog).not.toHaveBeenCalled() describe "when the item's saveAs method throws a well-known IO error", -> - notificationSpy = null - beforeEach -> - atom.notifications.onDidAddNotification notificationSpy = jasmine.createSpy() - it "creates a notification", -> pane.getActiveItem().saveAs = -> error = new Error("EACCES, permission denied '/foo'") error.path = '/foo' error.code = 'EACCES' - throw error + Promise.reject(error) - pane.saveActiveItemAs() - expect(notificationSpy).toHaveBeenCalled() - notification = notificationSpy.mostRecentCall.args[0] - expect(notification.getType()).toBe 'warning' - expect(notification.getMessage()).toContain 'Permission denied' - expect(notification.getMessage()).toContain '/foo' + waitsFor (done) -> + subscription = atom.notifications.onDidAddNotification (notification) -> + expect(notification.getType()).toBe 'warning' + expect(notification.getMessage()).toContain 'Permission denied' + expect(notification.getMessage()).toContain '/foo' + subscription.dispose() + done() + pane.saveActiveItemAs() describe "::itemForURI(uri)", -> it "returns the item for which a call to .getURI() returns the given uri", -> @@ -787,7 +789,6 @@ describe "Pane", -> pane2.moveItemToPane(item5, pane1, 0) expect(pane1.getPendingItem()).toEqual item6 - describe "split methods", -> [pane1, item1, container] = [] @@ -926,11 +927,10 @@ describe "Pane", -> item1.save = jasmine.createSpy("save") confirm.andReturn(0) - pane.close() - - expect(confirm).toHaveBeenCalled() - expect(item1.save).toHaveBeenCalled() - expect(pane.isDestroyed()).toBe true + pane.close().then -> + expect(confirm).toHaveBeenCalled() + expect(item1.save).toHaveBeenCalled() + expect(pane.isDestroyed()).toBe true it "does not destroy the pane if cancel is called", -> pane = new Pane(paneParams(items: [new Item("A"), new Item("B")])) @@ -941,11 +941,12 @@ describe "Pane", -> item1.save = jasmine.createSpy("save") confirm.andReturn(1) - pane.close() - expect(confirm).toHaveBeenCalled() - expect(item1.save).not.toHaveBeenCalled() - expect(pane.isDestroyed()).toBe false + waitsForPromise -> + pane.close().then -> + expect(confirm).toHaveBeenCalled() + expect(item1.save).not.toHaveBeenCalled() + expect(pane.isDestroyed()).toBe false describe "when item fails to save", -> [pane, item1, item2] = [] @@ -972,12 +973,12 @@ describe "Pane", -> 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 + waitsForPromise -> + pane.close().then -> + 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) @@ -989,14 +990,14 @@ describe "Pane", -> 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 + waitsForPromise -> + pane.close().then -> + 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 -> @@ -1014,14 +1015,14 @@ describe "Pane", -> 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 + waitsForPromise -> + pane.close().then -> + 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/window-event-handler-spec.coffee b/spec/window-event-handler-spec.coffee index d4387f23a..9c9f4a098 100644 --- a/spec/window-event-handler-spec.coffee +++ b/spec/window-event-handler-spec.coffee @@ -48,32 +48,6 @@ describe "WindowEventHandler", -> window.dispatchEvent(new CustomEvent('window:close')) expect(atom.close).toHaveBeenCalled() - describe "beforeunload event", -> - beforeEach -> - jasmine.unspy(TextEditor.prototype, "shouldPromptToSave") - spyOn(atom, 'destroy') - spyOn(ipcRenderer, 'send') - - describe "when pane items are modified", -> - editor = null - beforeEach -> - waitsForPromise -> atom.workspace.open("sample.js").then (o) -> editor = o - runs -> editor.insertText("I look different, I feel different.") - - it "prompts the user to save them, and allows the unload to continue if they confirm", -> - spyOn(atom.workspace, 'confirmClose').andReturn(true) - window.dispatchEvent(new CustomEvent('beforeunload')) - expect(atom.workspace.confirmClose).toHaveBeenCalled() - expect(ipcRenderer.send).not.toHaveBeenCalledWith('did-cancel-window-unload') - expect(atom.destroy).toHaveBeenCalled() - - it "cancels the unload if the user selects cancel", -> - spyOn(atom.workspace, 'confirmClose').andReturn(false) - window.dispatchEvent(new CustomEvent('beforeunload')) - expect(atom.workspace.confirmClose).toHaveBeenCalled() - expect(ipcRenderer.send).toHaveBeenCalledWith('did-cancel-window-unload') - expect(atom.destroy).not.toHaveBeenCalled() - describe "when a link is clicked", -> it "opens the http/https links in an external application", -> {shell} = require 'electron' diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js index 5d955b452..2626e5dd0 100644 --- a/spec/workspace-spec.js +++ b/spec/workspace-spec.js @@ -2394,38 +2394,47 @@ i = /test/; #FIXME\ }) describe('::saveActivePaneItem()', () => { - let editor = null - beforeEach(() => - waitsForPromise(() => atom.workspace.open('sample.js').then(o => { editor = o })) - ) + let editor, notificationSpy + + beforeEach(() => { + waitsForPromise(() => atom.workspace.open('sample.js').then(o => { + editor = o + })) + + notificationSpy = jasmine.createSpy('did-add-notification') + atom.notifications.onDidAddNotification(notificationSpy) + }) describe('when there is an error', () => { it('emits a warning notification when the file cannot be saved', () => { - let addedSpy 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') + waitsForPromise(() => + atom.workspace.saveActivePaneItem().then(() => { + expect(notificationSpy).toHaveBeenCalled() + expect(notificationSpy.mostRecentCall.args[0].getType()).toBe('warning') + expect(notificationSpy.mostRecentCall.args[0].getMessage()).toContain('Unable to save') + }) + ) }) it('emits a warning notification when the directory cannot be written to', () => { - let addedSpy 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') + waitsForPromise(() => + atom.workspace.saveActivePaneItem().then(() => { + expect(notificationSpy).toHaveBeenCalled() + expect(notificationSpy.mostRecentCall.args[0].getType()).toBe('warning') + expect(notificationSpy.mostRecentCall.args[0].getMessage()).toContain('Unable to save') + }) + ) }) it('emits a warning notification when the user does not have permission', () => { - let addedSpy spyOn(editor, 'save').andCallFake(() => { const error = new Error("EACCES, permission denied '/Some/dir/and-a-file.js'") error.code = 'EACCES' @@ -2433,10 +2442,13 @@ i = /test/; #FIXME\ throw error }) - atom.notifications.onDidAddNotification(addedSpy = jasmine.createSpy()) - atom.workspace.saveActivePaneItem() - expect(addedSpy).toHaveBeenCalled() - expect(addedSpy.mostRecentCall.args[0].getType()).toBe('warning') + waitsForPromise(() => + atom.workspace.saveActivePaneItem().then(() => { + expect(notificationSpy).toHaveBeenCalled() + expect(notificationSpy.mostRecentCall.args[0].getType()).toBe('warning') + expect(notificationSpy.mostRecentCall.args[0].getMessage()).toContain('Unable to save') + }) + ) }) it('emits a warning notification when the operation is not permitted', () => { @@ -2446,10 +2458,17 @@ i = /test/; #FIXME\ error.path = '/Some/dir/and-a-file.js' throw error }) + + waitsForPromise(() => + atom.workspace.saveActivePaneItem().then(() => { + expect(notificationSpy).toHaveBeenCalled() + expect(notificationSpy.mostRecentCall.args[0].getType()).toBe('warning') + expect(notificationSpy.mostRecentCall.args[0].getMessage()).toContain('Unable to save') + }) + ) }) it('emits a warning notification when the file is already open by another app', () => { - let addedSpy spyOn(editor, 'save').andCallFake(() => { const error = new Error("EBUSY, resource busy or locked '/Some/dir/and-a-file.js'") error.code = 'EBUSY' @@ -2457,17 +2476,16 @@ i = /test/; #FIXME\ 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') - expect(notificaiton.getMessage()).toContain('Unable to save') + waitsForPromise(() => + atom.workspace.saveActivePaneItem().then(() => { + expect(notificationSpy).toHaveBeenCalled() + expect(notificationSpy.mostRecentCall.args[0].getType()).toBe('warning') + expect(notificationSpy.mostRecentCall.args[0].getMessage()).toContain('Unable to save') + }) + ) }) it('emits a warning notification when the file system is read-only', () => { - let addedSpy spyOn(editor, 'save').andCallFake(() => { const error = new Error("EROFS, read-only file system '/Some/dir/and-a-file.js'") error.code = 'EROFS' @@ -2475,13 +2493,13 @@ i = /test/; #FIXME\ 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') - expect(notification.getMessage()).toContain('Unable to save') + waitsForPromise(() => + atom.workspace.saveActivePaneItem().then(() => { + expect(notificationSpy).toHaveBeenCalled() + expect(notificationSpy.mostRecentCall.args[0].getType()).toBe('warning') + expect(notificationSpy.mostRecentCall.args[0].getMessage()).toContain('Unable to save') + }) + ) }) it('emits a warning notification when the file cannot be saved', () => { @@ -2489,8 +2507,9 @@ i = /test/; #FIXME\ throw new Error('no one knows') }) - const save = () => atom.workspace.saveActivePaneItem() - expect(save).toThrow() + waitsForPromise({shouldReject: true}, () => + atom.workspace.saveActivePaneItem() + ) }) }) }) diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index e69efa458..fdb7119e1 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -232,19 +232,14 @@ class ApplicationDelegate new Disposable -> ipcRenderer.removeListener('context-command', outerCallback) - onSaveWindowStateRequest: (callback) -> + onDidRequestUnload: (callback) -> outerCallback = (event, message) -> - callback(event) + callback(event).then (shouldUnload) -> + ipcRenderer.send('did-prepare-to-unload', shouldUnload) - ipcRenderer.on('save-window-state', outerCallback) + ipcRenderer.on('prepare-to-unload', outerCallback) new Disposable -> - ipcRenderer.removeListener('save-window-state', outerCallback) - - didSaveWindowState: -> - ipcRenderer.send('did-save-window-state') - - didCancelWindowUnload: -> - ipcRenderer.send('did-cancel-window-unload') + ipcRenderer.removeListener('prepare-to-unload', outerCallback) onDidChangeHistoryManager: (callback) -> outerCallback = (event, message) -> diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index 9f5896071..f35ed39d8 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -694,9 +694,14 @@ class AtomEnvironment extends Model @disposables.add(@applicationDelegate.onDidOpenLocations(@openLocations.bind(this))) @disposables.add(@applicationDelegate.onApplicationMenuCommand(@dispatchApplicationMenuCommand.bind(this))) @disposables.add(@applicationDelegate.onContextMenuCommand(@dispatchContextMenuCommand.bind(this))) - @disposables.add @applicationDelegate.onSaveWindowStateRequest => - callback = => @applicationDelegate.didSaveWindowState() - @saveState({isUnloading: true}).catch(callback).then(callback) + @disposables.add @applicationDelegate.onDidRequestUnload => + @saveState({isUnloading: true}) + .catch(console.error) + .then => + @workspace?.confirmClose({ + windowCloseRequested: true, + projectHasPaths: @project.getPaths().length > 0 + }) @listenForUpdates() diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 302257fb1..944f2783c 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -270,7 +270,7 @@ class AtomApplication unless @quitting event.preventDefault() @quitting = true - Promise.all(@windows.map((window) -> window.saveState())).then(-> app.quit()) + Promise.all(@windows.map((window) -> window.prepareToUnload())).then(-> app.quit()) @disposable.add ipcHelpers.on app, 'will-quit', => @killAllProcesses() @@ -373,11 +373,6 @@ class AtomApplication @disposable.add ipcHelpers.respondTo 'set-temporary-window-state', (win, state) -> win.temporaryState = state - @disposable.add ipcHelpers.on ipcMain, 'did-cancel-window-unload', => - @quitting = false - for window in @windows - window.didCancelWindowUnload() - clipboard = require '../safe-clipboard' @disposable.add ipcHelpers.on ipcMain, 'write-text-to-selection-clipboard', (event, selectedText) -> clipboard.writeText(selectedText, 'selection') diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index d920fa5d2..3547f3032 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -147,7 +147,8 @@ class AtomWindow event.preventDefault() @unloading = true @atomApplication.saveState(false) - @saveState().then(=> @close()) + @prepareToUnload().then (result) => + @close() if result @browserWindow.on 'closed', => @fileRecoveryService.didCloseWindow(this) @@ -191,21 +192,19 @@ class AtomWindow @browserWindow.on 'blur', => @browserWindow.focusOnWebView() - didCancelWindowUnload: -> - @unloading = false - - saveState: -> + prepareToUnload: -> if @isSpecWindow() return Promise.resolve() - - @lastSaveStatePromise = new Promise (resolve) => - callback = (event) => + @lastPrepareToUnloadPromise = new Promise (resolve) => + callback = (event, result) => if BrowserWindow.fromWebContents(event.sender) is @browserWindow - ipcMain.removeListener('did-save-window-state', callback) - resolve() - ipcMain.on('did-save-window-state', callback) - @browserWindow.webContents.send('save-window-state') - @lastSaveStatePromise + ipcMain.removeListener('did-prepare-to-unload', callback) + unless result + @unloading = false + @atomApplication.quitting = false + resolve(result) + ipcMain.on('did-prepare-to-unload', callback) + @browserWindow.webContents.send('prepare-to-unload') openPath: (pathToOpen, initialLine, initialColumn) -> @openLocations([{pathToOpen, initialLine, initialColumn}]) @@ -287,7 +286,8 @@ class AtomWindow reload: -> @loadedPromise = new Promise((@resolveLoadedPromise) =>) - @saveState().then => @browserWindow.reload() + @prepareToUnload().then (result) => + @browserWindow.reload() if result @loadedPromise showSaveDialog: (params) -> diff --git a/src/pane-container.js b/src/pane-container.js index 88922c000..0a6fc2694 100644 --- a/src/pane-container.js +++ b/src/pane-container.js @@ -172,18 +172,13 @@ class PaneContainer { } confirmClose (options) { - let allSaved = true - - for (let pane of this.getPanes()) { - for (let item of pane.getItems()) { - if (!pane.promptToSaveItem(item, options)) { - allSaved = false - break - } + const promises = [] + for (const pane of this.getPanes()) { + for (const item of pane.getItems()) { + promises.push(pane.promptToSaveItem(item, options)) } } - - return allSaved + return Promise.all(promises).then((results) => !results.includes(false)) } activateNextPane () { diff --git a/src/pane.coffee b/src/pane.coffee index 31ee92f77..ed2b3a639 100644 --- a/src/pane.coffee +++ b/src/pane.coffee @@ -602,39 +602,47 @@ class Pane # * `force` (optional) {Boolean} Destroy the item without prompting to save # it, even if the item's `isPermanentDockItem` method returns true. # - # Returns a {Boolean} indicating whether or not the item was destroyed. + # Returns a {Promise} that resolves with a {Boolean} indicating whether or not + # the item was destroyed. destroyItem: (item, force) -> index = @items.indexOf(item) if index isnt -1 return false if not force and @getContainer()?.getLocation() isnt 'center' and item.isPermanentDockItem?() @emitter.emit 'will-destroy-item', {item, index} @container?.willDestroyPaneItem({item, index, pane: this}) - if force or @promptToSaveItem(item) + if force or not item?.shouldPromptToSave?() @removeItem(item, false) item.destroy?() - true else - false + @promptToSaveItem(item).then (result) => + if result + @removeItem(item, false) + item.destroy?() + result # Public: Destroy all items. destroyItems: -> - @destroyItem(item) for item in @getItems() - return + Promise.all( + @getItems().map(@destroyItem.bind(this)) + ) # Public: Destroy all items except for the active item. destroyInactiveItems: -> - @destroyItem(item) for item in @getItems() when item isnt @activeItem - return + Promise.all( + @getItems() + .filter((item) => item isnt @activeItem) + .map(@destroyItem.bind(this)) + ) promptToSaveItem: (item, options={}) -> - return true unless item.shouldPromptToSave?(options) + return Promise.resolve(true) unless item.shouldPromptToSave?(options) if typeof item.getURI is 'function' uri = item.getURI() else if typeof item.getUri is 'function' uri = item.getUri() else - return true + return Promise.resolve(true) saveDialog = (saveButtonText, saveFn, message) => chosen = @applicationDelegate.confirm @@ -642,15 +650,21 @@ class Pane 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 + when 0 + new Promise (resolve) -> + saveFn item, (error) -> + console.log 'error', error + saveError(error).then(resolve) + when 1 + Promise.resolve(false) + when 2 + Promise.resolve(true) saveError = (error) => if error saveDialog("Save as", @saveItemAs, "'#{item.getTitle?() ? uri}' could not be saved.\nError: #{@getMessageForErrorCode(error.code)}") else - true + Promise.resolve(true) saveDialog("Save", @saveItem, "'#{item.getTitle?() ? uri}' has changes, do you want to save them?") @@ -663,6 +677,8 @@ class Pane # # * `nextAction` (optional) {Function} which will be called after the item is # successfully saved. + # + # Returns a {Promise} that resolves when the save is complete saveActiveItemAs: (nextAction) -> @saveItemAs(@getActiveItem(), nextAction) @@ -673,6 +689,8 @@ class Pane # 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 + # + # Returns a {Promise} that resolves when the save is complete saveItem: (item, nextAction) => if typeof item?.getURI is 'function' itemURI = item.getURI() @@ -680,14 +698,16 @@ class Pane itemURI = item.getUri() if itemURI? - try - item.save?() + if item.save? + promisify -> item.save() + .then -> nextAction?() + .catch (error) => + if nextAction + nextAction(error) + else + @handleSaveError(error, item) + else nextAction?() - catch error - if nextAction - nextAction(error) - else - @handleSaveError(error, item) else @saveItemAs(item, nextAction) @@ -706,14 +726,13 @@ class Pane saveOptions.defaultPath ?= item.getPath() newItemPath = @applicationDelegate.showSaveDialog(saveOptions) if newItemPath - try - item.saveAs(newItemPath) - nextAction?() - catch error - if nextAction - nextAction(error) - else - @handleSaveError(error, item) + promisify -> item.saveAs(newItemPath) + .then -> nextAction?() + .catch (error) => + if nextAction? + nextAction(error) + else + @handleSaveError(error, item) # Public: Save all items. saveItems: -> @@ -909,13 +928,13 @@ class Pane bottommostSibling = @findBottommostSibling() if bottommostSibling is this then @splitDown() else bottommostSibling + # Private: Close the pane unless the user cancels the action via a dialog. + # + # Returns a {Promise} that resolves once the pane is either closed, or the + # closing has been cancelled. close: -> - @destroy() if @confirmClose() - - confirmClose: -> - for item in @getItems() - return false unless @promptToSaveItem(item) - true + Promise.all(@getItems().map(@promptToSaveItem.bind(this))).then (results) => + @destroy() unless results.includes(false) handleSaveError: (error, item) -> itemPath = error.path ? item?.getPath?() @@ -948,3 +967,9 @@ class Pane when 'EROFS' then 'Read-only file system' when 'ESPIPE' then 'Invalid seek' when 'ETIMEDOUT' then 'Connection timed out' + +promisify = (callback) -> + try + Promise.resolve(callback()) + catch error + Promise.reject(error) \ No newline at end of file diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index aac0f0ba5..6a277b612 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -147,19 +147,12 @@ class WindowEventHandler @document.body.classList.remove("fullscreen") handleWindowBeforeunload: (event) => - projectHasPaths = @atomEnvironment.project.getPaths().length > 0 - confirmed = @atomEnvironment.workspace?.confirmClose(windowCloseRequested: true, projectHasPaths: projectHasPaths) - if confirmed and not @reloadRequested and not @atomEnvironment.inSpecMode() and @atomEnvironment.getCurrentWindow().isWebViewFocused() + if not @reloadRequested and not @atomEnvironment.inSpecMode() and @atomEnvironment.getCurrentWindow().isWebViewFocused() @atomEnvironment.hide() @reloadRequested = false - @atomEnvironment.storeWindowDimensions() - if confirmed - @atomEnvironment.unloadEditorWindow() - @atomEnvironment.destroy() - else - @applicationDelegate.didCancelWindowUnload() - event.returnValue = false + @atomEnvironment.unloadEditorWindow() + @atomEnvironment.destroy() handleWindowToggleFullScreen: => @atomEnvironment.toggleFullScreen() diff --git a/src/workspace.js b/src/workspace.js index c886167c8..11bc9ba6b 100644 --- a/src/workspace.js +++ b/src/workspace.js @@ -1299,9 +1299,9 @@ module.exports = class Workspace extends Model { } confirmClose (options) { - return this.getPaneContainers() - .map(container => container.confirmClose(options)) - .every(saved => saved) + return Promise.all(this.getPaneContainers().map(container => + container.confirmClose(options) + )).then((results) => !results.includes(false)) } // Save the active pane item. @@ -1311,7 +1311,7 @@ module.exports = class Workspace extends Model { // {::saveActivePaneItemAs} # will be called instead. This method does nothing // if the active item does not implement a `.save` method. saveActivePaneItem () { - this.getCenter().getActivePane().saveActiveItem() + return this.getCenter().getActivePane().saveActiveItem() } // Prompt the user for a path and save the active pane item to it.