diff --git a/src/command/Commands.js b/src/command/Commands.js index 85509a9e97c..89061d93e01 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -43,6 +43,7 @@ define(function (require, exports, module) { exports.FILE_SAVE_AS = "file.saveAs"; // DocumentCommandHandlers.js handleFileSaveAs() exports.FILE_CLOSE = "file.close"; // DocumentCommandHandlers.js handleFileClose() exports.FILE_CLOSE_ALL = "file.close_all"; // DocumentCommandHandlers.js handleFileCloseAll() + exports.FILE_CLOSE_LIST = "file.close_list"; // DocumentCommandHandlers.js handleFileCloseList() exports.FILE_ADD_TO_WORKING_SET = "file.addToWorkingSet"; // DocumentCommandHandlers.js handleFileAddToWorkingSet() exports.FILE_LIVE_FILE_PREVIEW = "file.liveFilePreview"; // LiveDevelopment/main.js _handleGoLiveCommand() exports.FILE_LIVE_HIGHLIGHT = "file.previewHighlight"; // LiveDevelopment/main.js _handlePreviewHighlightCommand() diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index c80fa475506..9330f3b856f 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -715,20 +715,12 @@ define(function (require, exports, module) { return $.Deferred().reject().promise(); } - /** - * Saves all unsaved documents. Returns a Promise that will be resolved once ALL the save - * operations have been completed. If ANY save operation fails, an error dialog is immediately - * shown and the other files wait to save until it is dismissed; after all files have been - * processed, the Promise is rejected if any ONE save operation failed. - * - * @return {$.Promise} - */ - function saveAll() { + function _saveFileList(fileList) { // Do in serial because doSave shows error UI for each file, and we don't want to stack // multiple dialogs on top of each other var userCanceled = false; return Async.doSequentially( - DocumentManager.getWorkingSet(), + fileList, function (file) { // Abort remaining saves if user canceled any Save dialog if (userCanceled) { @@ -738,11 +730,16 @@ define(function (require, exports, module) { var doc = DocumentManager.getOpenDocumentForPath(file.fullPath); if (doc) { var savePromise = handleFileSave({doc: doc}); - savePromise.fail(function (error) { - if (error === USER_CANCELED) { - userCanceled = true; - } - }); + savePromise + .done(function (newFile) { + file.fullPath = newFile.fullPath; + file.name = newFile.name; + }) + .fail(function (error) { + if (error === USER_CANCELED) { + userCanceled = true; + } + }); return savePromise; } else { // working set entry that was never actually opened - ignore @@ -753,6 +750,18 @@ define(function (require, exports, module) { ); } + /** + * Saves all unsaved documents. Returns a Promise that will be resolved once ALL the save + * operations have been completed. If ANY save operation fails, an error dialog is immediately + * shown and the other files wait to save until it is dismissed; after all files have been + * processed, the Promise is rejected if any ONE save operation failed. + * + * @return {$.Promise} + */ + function saveAll() { + return _saveFileList(DocumentManager.getWorkingSet()); + } + /** * Prompts user with save as dialog and saves document. * @return {$.Promise} a promise that is resolved once the save has been completed @@ -908,21 +917,12 @@ define(function (require, exports, module) { } return promise; } - - /** - * Closes all open documents; equivalent to calling handleFileClose() for each document, except - * that unsaved changes are confirmed once, in bulk. - * @param {?{promptOnly: boolean}} If true, only displays the relevant confirmation UI and does NOT - * actually close any documents. This is useful when chaining close-all together with - * other user prompts that may be cancelable. - * @return {$.Promise} a promise that is resolved when all files are closed - */ - function handleFileCloseAll(commandData) { - var result = new $.Deferred(), - promptOnly = commandData && commandData.promptOnly; - var unsavedDocs = []; - DocumentManager.getWorkingSet().forEach(function (file) { + function _doCloseDocumentList(list, promptOnly, clearCurrentDoc) { + var result = new $.Deferred(), + unsavedDocs = []; + + list.forEach(function (file) { var doc = DocumentManager.getOpenDocumentForPath(file.fullPath); if (doc && doc.isDirty) { unsavedDocs.push(doc); @@ -985,7 +985,7 @@ define(function (require, exports, module) { result.reject(); } else if (id === Dialogs.DIALOG_BTN_OK) { // Save all unsaved files, then if that succeeds, close all - saveAll().done(function () { + _saveFileList(list).done(function () { result.resolve(); }).fail(function () { result.reject(); @@ -1002,13 +1002,29 @@ define(function (require, exports, module) { // guarantees that handlers run in the order they are added. result.done(function () { if (!promptOnly) { - DocumentManager.closeAll(); + DocumentManager.removeListFromWorkingSet(list, (clearCurrentDoc || true)); } }); return result.promise(); } + /** + * Closes all open documents; equivalent to calling handleFileClose() for each document, except + * that unsaved changes are confirmed once, in bulk. + * @param {?{promptOnly: boolean}} If true, only displays the relevant confirmation UI and does NOT + * actually close any documents. This is useful when chaining close-all together with + * other user prompts that may be cancelable. + * @return {$.Promise} a promise that is resolved when all files are closed + */ + function handleFileCloseAll(commandData) { + return _doCloseDocumentList(DocumentManager.getWorkingSet(), (commandData && commandData.promptOnly)); + } + + function handleFileCloseList(commandData) { + return _doCloseDocumentList((commandData && commandData.documentList), false); + } + /** * @private - tracks our closing state if we get called again */ @@ -1233,6 +1249,7 @@ define(function (require, exports, module) { CommandManager.register(Strings.CMD_FILE_CLOSE, Commands.FILE_CLOSE, handleFileClose); CommandManager.register(Strings.CMD_FILE_CLOSE_ALL, Commands.FILE_CLOSE_ALL, handleFileCloseAll); + CommandManager.register(Strings.CMD_FILE_CLOSE_LIST, Commands.FILE_CLOSE_LIST, handleFileCloseList); if (brackets.platform === "win") { CommandManager.register(Strings.CMD_EXIT, Commands.FILE_QUIT, handleFileQuit); diff --git a/src/document/DocumentManager.js b/src/document/DocumentManager.js index 867dd54de04..37e1f0785f0 100644 --- a/src/document/DocumentManager.js +++ b/src/document/DocumentManager.js @@ -568,6 +568,32 @@ define(function (require, exports, module) { _clearCurrentDocument(); _removeAllFromWorkingSet(); } + + function removeListFromWorkingSet(list, clearCurrentDocument) { + var fileList = [], index; + + if (!list) { + return; + } + + if (clearCurrentDocument) { + _clearCurrentDocument(); + } + + list.forEach(function (file) { + index = findInWorkingSet(file.fullPath); + + if (index !== -1) { + fileList.push(_workingSet[index]); + + _workingSet.splice(index, 1); + _workingSetMRUOrder.splice(findInWorkingSet(file.fullPath, _workingSetMRUOrder), 1); + _workingSetAddedOrder.splice(findInWorkingSet(file.fullPath, _workingSetAddedOrder), 1); + } + }); + + $(exports).triggerHandler("workingSetRemoveList", [fileList]); + } /** @@ -940,6 +966,7 @@ define(function (require, exports, module) { exports.addToWorkingSet = addToWorkingSet; exports.addListToWorkingSet = addListToWorkingSet; exports.removeFromWorkingSet = removeFromWorkingSet; + exports.removeListFromWorkingSet = removeListFromWorkingSet; exports.getNextPrevFile = getNextPrevFile; exports.swapWorkingSetIndexes = swapWorkingSetIndexes; exports.sortWorkingSet = sortWorkingSet; diff --git a/src/extensions/default/CloseOthers/main.js b/src/extensions/default/CloseOthers/main.js new file mode 100644 index 00000000000..a569be514f3 --- /dev/null +++ b/src/extensions/default/CloseOthers/main.js @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define, $, brackets, window, document */ + +define(function (require, exports, module) { + "use strict"; + + var Menus = brackets.getModule("command/Menus"), + CommandManager = brackets.getModule("command/CommandManager"), + Commands = brackets.getModule("command/Commands"), + dm = brackets.getModule("document/DocumentManager"), + docCH = brackets.getModule("document/DocumentCommandHandlers"), + strings = brackets.getModule("i18n!nls/strings"), + settings = JSON.parse(require("text!settings.json")), + working_set_cmenu = Menus.getContextMenu(Menus.ContextMenuIds.WORKING_SET_MENU), + close_others = "file.close_others", + close_above = "file.close_above", + close_below = "file.close_below"; + + function handleClose(mode) { + + var targetIndex = dm.findInWorkingSet(dm.getCurrentDocument().file.fullPath), + workingSet = dm.getWorkingSet().slice(0), + start = (mode === close_below) ? (targetIndex + 1) : 0, + end = (mode === close_above) ? (targetIndex) : (workingSet.length), + docList = [], + i; + + if (mode === close_others) { + end--; + workingSet.splice(targetIndex, 1); + } + + for (i = start; i < end; i++) { + docList.push(workingSet[i]); + } + + CommandManager.execute(Commands.FILE_CLOSE_LIST, {documentList: docList}); + } + + if (settings.close_below) { + CommandManager.register(strings.CMD_FILE_CLOSE_BELOW, close_below, function () { + handleClose(close_below); + }); + working_set_cmenu.addMenuItem(close_below, "", Menus.AFTER, Commands.FILE_CLOSE); + } + + if (settings.close_others) { + CommandManager.register(strings.CMD_FILE_CLOSE_OTHERS, close_others, function () { + handleClose(close_others); + }); + working_set_cmenu.addMenuItem(close_others, "", Menus.AFTER, Commands.FILE_CLOSE); + } + + if (settings.close_above) { + CommandManager.register(strings.CMD_FILE_CLOSE_ABOVE, close_above, function () { + handleClose(close_above); + }); + working_set_cmenu.addMenuItem(close_above, "", Menus.AFTER, Commands.FILE_CLOSE); + } +}); \ No newline at end of file diff --git a/src/extensions/default/CloseOthers/settings.json b/src/extensions/default/CloseOthers/settings.json new file mode 100644 index 00000000000..d2250e6e2a6 --- /dev/null +++ b/src/extensions/default/CloseOthers/settings.json @@ -0,0 +1,5 @@ +{ + "close_others": true, + "close_above": true, + "close_below": true +} \ No newline at end of file diff --git a/src/extensions/default/CloseOthers/unittest-files/dummy.js b/src/extensions/default/CloseOthers/unittest-files/dummy.js new file mode 100644 index 00000000000..7ed0ff9bdbc --- /dev/null +++ b/src/extensions/default/CloseOthers/unittest-files/dummy.js @@ -0,0 +1 @@ +//This is dummy file. To commit unnittest-files folder git needs a file. \ No newline at end of file diff --git a/src/extensions/default/CloseOthers/unittests.js b/src/extensions/default/CloseOthers/unittests.js new file mode 100644 index 00000000000..452388ea905 --- /dev/null +++ b/src/extensions/default/CloseOthers/unittests.js @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */ +/*global define, describe, it, expect, beforeEach, afterEach, runs, brackets, waitsForDone, spyOn */ + +define(function (require, exports, module) { + "use strict"; + + var SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"), + FileUtils = brackets.getModule("file/FileUtils"), + CommandManager, + Commands, + Dialogs, + EditorManager, + DocumentManager; + + describe("CloseOthers", function () { + var extensionPath = FileUtils.getNativeModuleDirectoryPath(module), + testPath = extensionPath + "/unittest-files/", + testWindow, + $, + docSelectIndex, + cmdToRun, + brackets; + + function createUntitled(count) { + function doCreateUntitled(content) { + runs(function () { + var promise = CommandManager.execute(Commands.FILE_NEW_UNTITLED); + promise.done(function (untitledDoc) { + untitledDoc.replaceRange(content, {line: 0, ch: 0}); + }); + waitsForDone(promise, "FILE_NEW_UNTITLED"); + }); + } + + var i; + for (i = 0; i < count; i++) { + doCreateUntitled(String(i)); + } + } + + /** Expect a file to exist (failing test if not) and then delete it */ + function expectAndDelete(fullPath) { + runs(function () { + var promise = SpecRunnerUtils.resolveNativeFileSystemPath(fullPath); + waitsForDone(promise, "Verify file exists: " + fullPath); + }); + runs(function () { + var promise = SpecRunnerUtils.deletePath(fullPath); + waitsForDone(promise, "Remove testfile " + fullPath); + }); + } + + function getFilename(i) { + return testPath + "test_closeothers" + i + ".js"; + } + + beforeEach(function () { + + runs(function () { + SpecRunnerUtils.createTestWindowAndRun(this, function (w) { + testWindow = w; + $ = testWindow.$; + brackets = testWindow.brackets; + DocumentManager = testWindow.brackets.test.DocumentManager; + CommandManager = testWindow.brackets.test.CommandManager; + EditorManager = testWindow.brackets.test.EditorManager; + Dialogs = testWindow.brackets.test.Dialogs; + Commands = testWindow.brackets.test.Commands; + }); + }); + + runs(function () { + SpecRunnerUtils.loadProjectInTestWindow(testPath); + }); + + createUntitled(5); + + runs(function () { + var fileI = 0; + spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + callback(undefined, getFilename(fileI)); + fileI++; + }); + + var promise = CommandManager.execute(Commands.FILE_SAVE_ALL); + waitsForDone(promise, "FILE_SAVE_ALL"); + }); + }); + + afterEach(function () { + // Verify files exist & clean up + [0, 1, 2, 3, 4].forEach(function (i) { + expectAndDelete(getFilename(i)); + }); + + testWindow = null; + $ = null; + brackets = null; + EditorManager = null; + SpecRunnerUtils.closeTestWindow(); + }); + + + function runCloseOthers() { + var ws = DocumentManager.getWorkingSet(), + promise; + + if (ws.length > docSelectIndex) { + DocumentManager.getDocumentForPath(ws[docSelectIndex].fullPath).done(function (doc) { + DocumentManager.setCurrentDocument(doc); + }); + + promise = CommandManager.execute(cmdToRun); + waitsForDone(promise, cmdToRun); + } + } + + it("Close others", function () { + docSelectIndex = 2; + cmdToRun = "file.close_others"; + + runs(runCloseOthers); + + runs(function () { + expect(DocumentManager.getWorkingSet().length).toEqual(1); + }); + }); + + it("Close others above", function () { + docSelectIndex = 2; + cmdToRun = "file.close_above"; + + runs(runCloseOthers); + + runs(function () { + expect(DocumentManager.getWorkingSet().length).toEqual(3); + }); + }); + + it("Close others below", function () { + docSelectIndex = 1; + cmdToRun = "file.close_below"; + + runs(runCloseOthers); + + runs(function () { + expect(DocumentManager.getWorkingSet().length).toEqual(2); + }); + }); + }); +}); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 495dedcd199..77742450379 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -209,6 +209,10 @@ define({ "CMD_OPEN_FOLDER" : "Open Folder\u2026", "CMD_FILE_CLOSE" : "Close", "CMD_FILE_CLOSE_ALL" : "Close All", + "CMD_FILE_CLOSE_LIST" : "Close List", + "CMD_FILE_CLOSE_OTHERS" : "Close Others", + "CMD_FILE_CLOSE_ABOVE" : "Close Others Above", + "CMD_FILE_CLOSE_BELOW" : "Close Others Below", "CMD_FILE_SAVE" : "Save", "CMD_FILE_SAVE_ALL" : "Save All", "CMD_FILE_SAVE_AS" : "Save As\u2026",