src/document/DocumentCommandHandlers.js
/*
* Copyright (c) 2012 - present 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 regexp: true */
define(function (require, exports, module) {
"use strict";
// Load dependent modules
var AppInit = require("utils/AppInit"),
CommandManager = require("command/CommandManager"),
Commands = require("command/Commands"),
DeprecationWarning = require("utils/DeprecationWarning"),
EventDispatcher = require("utils/EventDispatcher"),
ProjectManager = require("project/ProjectManager"),
DocumentManager = require("document/DocumentManager"),
MainViewManager = require("view/MainViewManager"),
EditorManager = require("editor/EditorManager"),
FileSystem = require("filesystem/FileSystem"),
FileSystemError = require("filesystem/FileSystemError"),
FileUtils = require("file/FileUtils"),
FileViewController = require("project/FileViewController"),
InMemoryFile = require("document/InMemoryFile"),
StringUtils = require("utils/StringUtils"),
Async = require("utils/Async"),
HealthLogger = require("utils/HealthLogger"),
Dialogs = require("widgets/Dialogs"),
DefaultDialogs = require("widgets/DefaultDialogs"),
Strings = require("strings"),
PopUpManager = require("widgets/PopUpManager"),
PreferencesManager = require("preferences/PreferencesManager"),
PerfUtils = require("utils/PerfUtils"),
KeyEvent = require("utils/KeyEvent"),
Inspector = require("LiveDevelopment/Inspector/Inspector"),
Menus = require("command/Menus"),
UrlParams = require("utils/UrlParams").UrlParams,
StatusBar = require("widgets/StatusBar"),
WorkspaceManager = require("view/WorkspaceManager"),
LanguageManager = require("language/LanguageManager"),
_ = require("thirdparty/lodash");
/**
* Handlers for commands related to document handling (opening, saving, etc.)
*/
/**
* Container for label shown above editor; must be an inline element
* @type {jQueryObject}
*/
var _$title = null;
/**
* Container for dirty dot; must be an inline element
* @type {jQueryObject}
*/
var _$dirtydot = null;
/**
* Container for _$title; need not be an inline element
* @type {jQueryObject}
*/
var _$titleWrapper = null;
/**
* Label shown above editor for current document: filename and potentially some of its path
* @type {string}
*/
var _currentTitlePath = null;
/**
* Determine the dash character for each platform. Use emdash on Mac
* and a standard dash on all other platforms.
* @type {string}
*/
var _osDash = brackets.platform === "mac" ? "\u2014" : "-";
/**
* String template for window title when no file is open.
* @type {string}
*/
var WINDOW_TITLE_STRING_NO_DOC = "{0} " + _osDash + " {1}";
/**
* String template for window title when a file is open.
* @type {string}
*/
var WINDOW_TITLE_STRING_DOC = "{0} ({1}) " + _osDash + " {2}";
/**
* Container for _$titleWrapper; if changing title changes this element's height, must kick editor to resize
* @type {jQueryObject}
*/
var _$titleContainerToolbar = null;
/**
* Last known height of _$titleContainerToolbar
* @type {number}
*/
var _lastToolbarHeight = null;
/**
* index to use for next, new Untitled document
* @type {number}
*/
var _nextUntitledIndexToUse = 1;
/**
* prevents reentrancy of browserReload()
* @type {boolean}
*/
var _isReloading = false;
/** Unique token used to indicate user-driven cancellation of Save As (as opposed to file IO error) */
var USER_CANCELED = { userCanceled: true };
PreferencesManager.definePreference("defaultExtension", "string", "", {
excludeFromHints: true
});
EventDispatcher.makeEventDispatcher(exports);
/**
* Event triggered when File Save is cancelled, when prompted to save dirty files
*/
var APP_QUIT_CANCELLED = "appQuitCancelled";
/**
* JSLint workaround for circular dependency
* @type {function}
*/
var handleFileSaveAs;
/**
* Updates the title bar with new file title or dirty indicator
* @private
*/
function _updateTitle() {
var currentDoc = DocumentManager.getCurrentDocument(),
windowTitle = brackets.config.app_title,
currentlyViewedFile = MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE),
currentlyViewedPath = currentlyViewedFile && currentlyViewedFile.fullPath,
readOnlyString = (currentlyViewedFile && currentlyViewedFile.readOnly) ? "[Read Only] - " : "";
if (!brackets.nativeMenus) {
if (currentlyViewedPath) {
_$title.text(_currentTitlePath);
_$title.attr("title", currentlyViewedPath);
if (currentDoc) {
// dirty dot is always in DOM so layout doesn't change, and visibility is toggled
_$dirtydot.css("visibility", (currentDoc.isDirty) ? "visible" : "hidden");
} else {
// hide dirty dot if there is no document
_$dirtydot.css("visibility", "hidden");
}
} else {
_$title.text("");
_$title.attr("title", "");
_$dirtydot.css("visibility", "hidden");
}
// Set _$titleWrapper to a fixed width just large enough to accommodate _$title. This seems equivalent to what
// the browser would do automatically, but the CSS trick we use for layout requires _$titleWrapper to have a
// fixed width set on it (see the "#titlebar" CSS rule for details).
_$titleWrapper.css("width", "");
var newWidth = _$title.width();
_$titleWrapper.css("width", newWidth);
// Changing the width of the title may cause the toolbar layout to change height, which needs to resize the
// editor beneath it (toolbar changing height due to window resize is already caught by EditorManager).
var newToolbarHeight = _$titleContainerToolbar.height();
if (_lastToolbarHeight !== newToolbarHeight) {
_lastToolbarHeight = newToolbarHeight;
WorkspaceManager.recomputeLayout();
}
}
var projectRoot = ProjectManager.getProjectRoot();
if (projectRoot) {
var projectName = projectRoot.name;
// Construct shell/browser window title, e.g. "• index.html (myProject) — Brackets"
if (currentlyViewedPath) {
windowTitle = StringUtils.format(WINDOW_TITLE_STRING_DOC, readOnlyString + _currentTitlePath, projectName, brackets.config.app_title);
// Display dirty dot when there are unsaved changes
if (currentDoc && currentDoc.isDirty) {
windowTitle = "• " + windowTitle;
}
} else {
// A document is not open
windowTitle = StringUtils.format(WINDOW_TITLE_STRING_NO_DOC, projectName, brackets.config.app_title);
}
}
window.document.title = windowTitle;
}
/**
* Returns a short title for a given document.
*
* @param {Document} doc - the document to compute the short title for
* @return {string} - a short title for doc.
*/
function _shortTitleForDocument(doc) {
var fullPath = doc.file.fullPath;
// If the document is untitled then return the filename, ("Untitled-n.ext");
// otherwise show the project-relative path if the file is inside the
// current project or the full absolute path if it's not in the project.
if (doc.isUntitled()) {
return fullPath.substring(fullPath.lastIndexOf("/") + 1);
} else {
return ProjectManager.makeProjectRelativeIfPossible(fullPath);
}
}
/**
* Handles currentFileChange and filenameChanged events and updates the titlebar
*/
function handleCurrentFileChange() {
var newFile = MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE);
if (newFile) {
var newDocument = DocumentManager.getOpenDocumentForPath(newFile.fullPath);
if (newDocument) {
_currentTitlePath = _shortTitleForDocument(newDocument);
} else {
_currentTitlePath = ProjectManager.makeProjectRelativeIfPossible(newFile.fullPath);
}
} else {
_currentTitlePath = null;
}
// Update title text & "dirty dot" display
_updateTitle();
}
/**
* Handles dirtyFlagChange event and updates the title bar if necessary
*/
function handleDirtyChange(event, changedDoc) {
var currentDoc = DocumentManager.getCurrentDocument();
if (currentDoc && changedDoc.file.fullPath === currentDoc.file.fullPath) {
_updateTitle();
}
}
/**
* Shows an error dialog indicating that the given file could not be opened due to the given error
* @param {!FileSystemError} name
* @return {!Dialog}
*/
function showFileOpenError(name, path) {
return Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_ERROR,
Strings.ERROR_OPENING_FILE_TITLE,
StringUtils.format(
Strings.ERROR_OPENING_FILE,
StringUtils.breakableUrl(path),
FileUtils.getFileErrorString(name)
)
);
}
/**
* @private
* Creates a document and displays an editor for the specified file path.
* @param {!string} fullPath
* @param {boolean=} silent If true, don't show error message
* @param {string=} paneId, the id oi the pane in which to open the file. Can be undefined, a valid pane id or ACTIVE_PANE.
* @param {{*}=} options, command options
* @return {$.Promise} a jQuery promise that will either
* - be resolved with a file for the specified file path or
* - be rejected with FileSystemError if the file can not be read.
* If paneId is undefined, the ACTIVE_PANE constant
*/
function _doOpen(fullPath, silent, paneId, options) {
var result = new $.Deferred();
// workaround for https://github.com/adobe/brackets/issues/6001
// TODO should be removed once bug is closed.
// if we are already displaying a file do nothing but resolve immediately.
// this fixes timing issues in test cases.
if (MainViewManager.getCurrentlyViewedPath(paneId || MainViewManager.ACTIVE_PANE) === fullPath) {
result.resolve(MainViewManager.getCurrentlyViewedFile(paneId || MainViewManager.ACTIVE_PANE));
return result.promise();
}
function _cleanup(fileError, fullFilePath) {
if (fullFilePath) {
// For performance, we do lazy checking of file existence, so it may be in workingset
MainViewManager._removeView(paneId, FileSystem.getFileForPath(fullFilePath));
MainViewManager.focusActivePane();
}
result.reject(fileError);
}
function _showErrorAndCleanUp(fileError, fullFilePath) {
if (silent) {
_cleanup(fileError, fullFilePath);
} else {
showFileOpenError(fileError, fullFilePath).done(function () {
_cleanup(fileError, fullFilePath);
});
}
}
if (!fullPath) {
throw new Error("_doOpen() called without fullPath");
} else {
var perfTimerName = PerfUtils.markStart("Open File:\t" + fullPath);
result.always(function () {
PerfUtils.addMeasurement(perfTimerName);
});
var file = FileSystem.getFileForPath(fullPath);
if (options && options.encoding) {
file._encoding = options.encoding;
} else {
var projectRoot = ProjectManager.getProjectRoot(),
context = {
location : {
scope: "user",
layer: "project",
layerID: projectRoot.fullPath
}
};
var encoding = PreferencesManager.getViewState("encoding", context);
if (encoding && encoding[fullPath]) {
file._encoding = encoding[fullPath];
}
}
MainViewManager._open(paneId, file, options)
.done(function () {
result.resolve(file);
})
.fail(function (fileError) {
_showErrorAndCleanUp(fileError, fullPath);
result.reject();
});
}
return result.promise();
}
/**
* @private
* Used to track the default directory for the file open dialog
*/
var _defaultOpenDialogFullPath = null;
/**
* @private
* Opens a file and displays its view (editor, image view, etc...) for the specified path.
* If no path is specified, a file prompt is provided for input.
* @param {?string} fullPath - The path of the file to open; if it's null we'll prompt for it
* @param {boolean=} silent - If true, don't show error message
* @param {string=} paneId - the pane in which to open the file. Can be undefined, a valid pane id or ACTIVE_PANE
* @param {{*}=} options - options to pass to MainViewManager._open
* @return {$.Promise} a jQuery promise resolved with a Document object or
* rejected with an err
*/
function _doOpenWithOptionalPath(fullPath, silent, paneId, options) {
var result;
paneId = paneId || MainViewManager.ACTIVE_PANE;
if (!fullPath) {
// Create placeholder deferred
result = new $.Deferred();
//first time through, default to the current project path
if (!_defaultOpenDialogFullPath) {
_defaultOpenDialogFullPath = ProjectManager.getProjectRoot().fullPath;
}
// Prompt the user with a dialog
FileSystem.showOpenDialog(true, false, Strings.OPEN_FILE, _defaultOpenDialogFullPath, null, function (err, paths) {
if (!err) {
if (paths.length > 0) {
// Add all files to the workingset without verifying that
// they still exist on disk (for faster opening)
var filesToOpen = [];
paths.forEach(function (path) {
filesToOpen.push(FileSystem.getFileForPath(path));
});
MainViewManager.addListToWorkingSet(paneId, filesToOpen);
_doOpen(paths[paths.length - 1], silent, paneId, options)
.done(function (file) {
_defaultOpenDialogFullPath =
FileUtils.getDirectoryPath(
MainViewManager.getCurrentlyViewedPath(paneId)
);
})
// Send the resulting document that was opened
.then(result.resolve, result.reject);
} else {
// Reject if the user canceled the dialog
result.reject();
}
}
});
} else {
result = _doOpen(fullPath, silent, paneId, options);
}
return result.promise();
}
/**
* @private
* Splits a decorated file path into its parts.
* @param {?string} path - a string of the form "fullpath[:lineNumber[:columnNumber]]"
* @return {{path: string, line: ?number, column: ?number}}
*/
function _parseDecoratedPath(path) {
var result = {path: path, line: null, column: null};
if (path) {
// If the path has a trailing :lineNumber and :columnNumber, strip
// these off and assign to result.line and result.column.
var matchResult = /(.+?):([0-9]+)(:([0-9]+))?$/.exec(path);
if (matchResult) {
result.path = matchResult[1];
if (matchResult[2]) {
result.line = parseInt(matchResult[2], 10);
}
if (matchResult[4]) {
result.column = parseInt(matchResult[4], 10);
}
}
}
return result;
}
/**
* @typedef {{fullPath:?string=, silent:boolean=, paneId:string=}} FileCommandData
* fullPath: is in the form "path[:lineNumber[:columnNumber]]"
* lineNumber and columnNumber are 1-origin: lines and columns are 1-based
*/
/**
* @typedef {{fullPath:?string=, index:number=, silent:boolean=, forceRedraw:boolean=, paneId:string=}} PaneCommandData
* fullPath: is in the form "path[:lineNumber[:columnNumber]]"
* lineNumber and columnNumber are 1-origin: lines and columns are 1-based
*/
/**
* Opens the given file and makes it the current file. Does NOT add it to the workingset.
* @param {FileCommandData=} commandData - record with the following properties:
* fullPath: File to open;
* silent: optional flag to suppress error messages;
* paneId: optional PaneId (defaults to active pane)
* @return {$.Promise} a jQuery promise that will be resolved with a file object
*/
function handleFileOpen(commandData) {
var fileInfo = _parseDecoratedPath(commandData ? commandData.fullPath : null),
silent = (commandData && commandData.silent) || false,
paneId = (commandData && commandData.paneId) || MainViewManager.ACTIVE_PANE,
result = new $.Deferred();
_doOpenWithOptionalPath(fileInfo.path, silent, paneId, commandData && commandData.options)
.done(function (file) {
HealthLogger.fileOpened(file._path, false, file._encoding);
if (!commandData || !commandData.options || !commandData.options.noPaneActivate) {
MainViewManager.setActivePaneId(paneId);
}
// If a line and column number were given, position the editor accordingly.
if (fileInfo.line !== null) {
if (fileInfo.column === null || (fileInfo.column <= 0)) {
fileInfo.column = 1;
}
// setCursorPos expects line/column numbers as 0-origin, so we subtract 1
EditorManager.getCurrentFullEditor().setCursorPos(fileInfo.line - 1,
fileInfo.column - 1,
true);
}
result.resolve(file);
})
.fail(function (err) {
result.reject(err);
});
return result;
// Testing notes: here are some recommended manual tests for handleFileOpen, on Macintosh.
// Do all tests with brackets already running, and also with brackets not already running.
//
// drag a file onto brackets icon in desktop (this uses undecorated paths)
// drag a file onto brackets icon in taskbar (this uses undecorated paths)
// open a file from brackets sidebar (this uses undecorated paths)
// from command line: ...../Brackets.app/Contents path - where 'path' is undecorated
// from command line: ...../Brackets.app path - where 'path' has the form "path:line"
// from command line: ...../Brackets.app path - where 'path' has the form "path:line:column"
// from command line: open -a ...../Brackets.app path - where 'path' is undecorated
// do "View Source" from Adobe Scout version 1.2 or newer (this will use decorated paths of the form "path:line:column")
}
/**
* Opens the given file, makes it the current file, does NOT add it to the workingset
* @param {FileCommandData} commandData
* fullPath: File to open;
* silent: optional flag to suppress error messages;
* paneId: optional PaneId (defaults to active pane)
* @return {$.Promise} a jQuery promise that will be resolved with @type {Document}
*/
function handleDocumentOpen(commandData) {
var result = new $.Deferred();
handleFileOpen(commandData)
.done(function (file) {
// if we succeeded with an open file
// then we need to resolve that to a document.
// getOpenDocumentForPath will return null if there isn't a
// supporting document for that file (e.g. an image)
var doc = DocumentManager.getOpenDocumentForPath(file.fullPath);
result.resolve(doc);
})
.fail(function (err) {
result.reject(err);
});
return result.promise();
}
/**
* Opens the given file, makes it the current file, AND adds it to the workingset
* @param {!PaneCommandData} commandData - record with the following properties:
* fullPath: File to open;
* index: optional index to position in workingset (defaults to last);
* silent: optional flag to suppress error messages;
* forceRedraw: flag to force the working set view redraw;
* paneId: optional PaneId (defaults to active pane)
* @return {$.Promise} a jQuery promise that will be resolved with a @type {File}
*/
function handleFileAddToWorkingSetAndOpen(commandData) {
return handleFileOpen(commandData).done(function (file) {
var paneId = (commandData && commandData.paneId) || MainViewManager.ACTIVE_PANE;
MainViewManager.addToWorkingSet(paneId, file, commandData.index, commandData.forceRedraw);
HealthLogger.fileOpened(file.fullPath, true);
});
}
/**
* @deprecated
* Opens the given file, makes it the current document, AND adds it to the workingset
* @param {!PaneCommandData} commandData - record with the following properties:
* fullPath: File to open;
* index: optional index to position in workingset (defaults to last);
* silent: optional flag to suppress error messages;
* forceRedraw: flag to force the working set view redraw;
* paneId: optional PaneId (defaults to active pane)
* @return {$.Promise} a jQuery promise that will be resolved with @type {File}
*/
function handleFileAddToWorkingSet(commandData) {
// This is a legacy deprecated command that
// will use the new command and resolve with a document
// as the legacy command would only support.
DeprecationWarning.deprecationWarning("Commands.FILE_ADD_TO_WORKING_SET has been deprecated. Use Commands.CMD_ADD_TO_WORKINGSET_AND_OPEN instead.");
var result = new $.Deferred();
handleFileAddToWorkingSetAndOpen(commandData)
.done(function (file) {
// if we succeeded with an open file
// then we need to resolve that to a document.
// getOpenDocumentForPath will return null if there isn't a
// supporting document for that file (e.g. an image)
var doc = DocumentManager.getOpenDocumentForPath(file.fullPath);
result.resolve(doc);
})
.fail(function (err) {
result.reject(err);
});
return result.promise();
}
/**
* @private
* Ensures the suggested file name doesn't already exit.
* @param {Directory} dir The directory to use
* @param {string} baseFileName The base to start with, "-n" will get appended to make unique
* @param {boolean} isFolder True if the suggestion is for a folder name
* @return {$.Promise} a jQuery promise that will be resolved with a unique name starting with
* the given base name
*/
function _getUntitledFileSuggestion(dir, baseFileName, isFolder) {
var suggestedName = baseFileName + "-" + _nextUntitledIndexToUse++,
deferred = $.Deferred();
if (_nextUntitledIndexToUse > 9999) {
//we've tried this enough
deferred.reject();
} else {
var path = dir.fullPath + suggestedName,
entry = isFolder ? FileSystem.getDirectoryForPath(path)
: FileSystem.getFileForPath(path);
entry.exists(function (err, exists) {
if (err || exists) {
_getUntitledFileSuggestion(dir, baseFileName, isFolder)
.then(deferred.resolve, deferred.reject);
} else {
deferred.resolve(suggestedName);
}
});
}
return deferred.promise();
}
/**
* Prevents re-entrancy into handleFileNewInProject()
*
* handleFileNewInProject() first prompts the user to name a file and then asynchronously writes the file when the
* filename field loses focus. This boolean prevent additional calls to handleFileNewInProject() when an existing
* file creation call is outstanding
*/
var fileNewInProgress = false;
/**
* Bottleneck function for creating new files and folders in the project tree.
* @private
* @param {boolean} isFolder - true if creating a new folder, false if creating a new file
*/
function _handleNewItemInProject(isFolder) {
if (fileNewInProgress) {
ProjectManager.forceFinishRename();
return;
}
fileNewInProgress = true;
// Determine the directory to put the new file
// If a file is currently selected in the tree, put it next to it.
// If a directory is currently selected in the tree, put it in it.
// If an Untitled document is selected or nothing is selected in the tree, put it at the root of the project.
var baseDirEntry,
selected = ProjectManager.getFileTreeContext();
if ((!selected) || (selected instanceof InMemoryFile)) {
selected = ProjectManager.getProjectRoot();
}
if (selected.isFile) {
baseDirEntry = FileSystem.getDirectoryForPath(selected.parentPath);
}
baseDirEntry = baseDirEntry || selected;
// Create the new node. The createNewItem function does all the heavy work
// of validating file name, creating the new file and selecting.
function createWithSuggestedName(suggestedName) {
return ProjectManager.createNewItem(baseDirEntry, suggestedName, false, isFolder)
.always(function () { fileNewInProgress = false; });
}
return _getUntitledFileSuggestion(baseDirEntry, Strings.UNTITLED, isFolder)
.then(createWithSuggestedName, createWithSuggestedName.bind(undefined, Strings.UNTITLED));
}
/**
* Create a new untitled document in the workingset, and make it the current document.
* Promise is resolved (synchronously) with the newly-created Document.
*/
function handleFileNew() {
//var defaultExtension = PreferencesManager.get("defaultExtension");
//if (defaultExtension) {
// defaultExtension = "." + defaultExtension;
//}
var defaultExtension = ""; // disable preference setting for now
var doc = DocumentManager.createUntitledDocument(_nextUntitledIndexToUse++, defaultExtension);
MainViewManager._edit(MainViewManager.ACTIVE_PANE, doc);
HealthLogger.sendAnalyticsData(
HealthLogger.commonStrings.USAGE +
HealthLogger.commonStrings.FILE_OPEN +
HealthLogger.commonStrings.FILE_NEW,
HealthLogger.commonStrings.USAGE,
HealthLogger.commonStrings.FILE_OPEN,
HealthLogger.commonStrings.FILE_NEW
);
return new $.Deferred().resolve(doc).promise();
}
/**
* Create a new file in the project tree.
*/
function handleFileNewInProject() {
_handleNewItemInProject(false);
}
/**
* Create a new folder in the project tree.
*/
function handleNewFolderInProject() {
_handleNewItemInProject(true);
}
/**
* @private
* Shows an Error modal dialog
* @param {string} name
* @param {string} path
* @return {Dialog}
*/
function _showSaveFileError(name, path) {
return Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_ERROR,
Strings.ERROR_SAVING_FILE_TITLE,
StringUtils.format(
Strings.ERROR_SAVING_FILE,
StringUtils.breakableUrl(path),
FileUtils.getFileErrorString(name)
)
);
}
/**
* Saves a document to its existing path. Does NOT support untitled documents.
* @param {!Document} docToSave
* @param {boolean=} force Ignore CONTENTS_MODIFIED errors from the FileSystem
* @return {$.Promise} a promise that is resolved with the File of docToSave (to mirror
* the API of _doSaveAs()). Rejected in case of IO error (after error dialog dismissed).
*/
function doSave(docToSave, force) {
var result = new $.Deferred(),
file = docToSave.file;
function handleError(error) {
_showSaveFileError(error, file.fullPath)
.done(function () {
result.reject(error);
});
}
function handleContentsModified() {
Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_ERROR,
Strings.EXT_MODIFIED_TITLE,
StringUtils.format(
Strings.EXT_MODIFIED_WARNING,
StringUtils.breakableUrl(docToSave.file.fullPath)
),
[
{
className : Dialogs.DIALOG_BTN_CLASS_LEFT,
id : Dialogs.DIALOG_BTN_SAVE_AS,
text : Strings.SAVE_AS
},
{
className : Dialogs.DIALOG_BTN_CLASS_NORMAL,
id : Dialogs.DIALOG_BTN_CANCEL,
text : Strings.CANCEL
},
{
className : Dialogs.DIALOG_BTN_CLASS_PRIMARY,
id : Dialogs.DIALOG_BTN_OK,
text : Strings.SAVE_AND_OVERWRITE
}
]
)
.done(function (id) {
if (id === Dialogs.DIALOG_BTN_CANCEL) {
result.reject();
} else if (id === Dialogs.DIALOG_BTN_OK) {
// Re-do the save, ignoring any CONTENTS_MODIFIED errors
doSave(docToSave, true).then(result.resolve, result.reject);
} else if (id === Dialogs.DIALOG_BTN_SAVE_AS) {
// Let the user choose a different path at which to write the file
handleFileSaveAs({doc: docToSave}).then(result.resolve, result.reject);
}
});
}
function trySave() {
// We don't want normalized line endings, so it's important to pass true to getText()
FileUtils.writeText(file, docToSave.getText(true), force)
.done(function () {
docToSave.notifySaved();
result.resolve(file);
HealthLogger.fileSaved(docToSave);
})
.fail(function (err) {
if (err === FileSystemError.CONTENTS_MODIFIED) {
handleContentsModified();
} else {
handleError(err);
}
});
}
if (docToSave.isDirty) {
if (docToSave.keepChangesTime) {
// The user has decided to keep conflicting changes in the editor. Check to make sure
// the file hasn't changed since they last decided to do that.
docToSave.file.stat(function (err, stat) {
// If the file has been deleted on disk, the stat will return an error, but that's fine since
// that means there's no file to overwrite anyway, so the save will succeed without us having
// to set force = true.
if (!err && docToSave.keepChangesTime === stat.mtime.getTime()) {
// OK, it's safe to overwrite the file even though we never reloaded the latest version,
// since the user already said s/he wanted to ignore the disk version.
force = true;
}
trySave();
});
} else {
trySave();
}
} else {
result.resolve(file);
}
result.always(function () {
MainViewManager.focusActivePane();
});
return result.promise();
}
/**
* Reverts the Document to the current contents of its file on disk. Discards any unsaved changes
* in the Document.
* @private
* @param {Document} doc
* @param {boolean=} suppressError If true, then a failure to read the file will be ignored and the
* resulting promise will be resolved rather than rejected.
* @return {$.Promise} a Promise that's resolved when done, or (if suppressError is false)
* rejected with a FileSystemError if the file cannot be read (after showing an error
* dialog to the user).
*/
function _doRevert(doc, suppressError) {
var result = new $.Deferred();
FileUtils.readAsText(doc.file)
.done(function (text, readTimestamp) {
doc.refreshText(text, readTimestamp);
result.resolve();
})
.fail(function (error) {
if (suppressError) {
result.resolve();
} else {
showFileOpenError(error, doc.file.fullPath)
.done(function () {
result.reject(error);
});
}
});
return result.promise();
}
/**
* Dispatches the app quit cancelled event
*/
function dispatchAppQuitCancelledEvent() {
exports.trigger(exports.APP_QUIT_CANCELLED);
}
/**
* Opens the native OS save as dialog and saves document.
* The original document is reverted in case it was dirty.
* Text selection and cursor position from the original document
* are preserved in the new document.
* When saving to the original document the document is saved as if save was called.
* @param {Document} doc
* @param {?{cursorPos:!Object, selection:!Object, scrollPos:!Object}} settings - properties of
* the original document's editor that need to be carried over to the new document
* i.e. scrollPos, cursorPos and text selection
* @return {$.Promise} a promise that is resolved with the saved document's File. Rejected in
* case of IO error (after error dialog dismissed), or if the Save dialog was canceled.
*/
function _doSaveAs(doc, settings) {
var origPath,
saveAsDefaultPath,
defaultName,
result = new $.Deferred();
function _doSaveAfterSaveDialog(path) {
var newFile;
// Reconstruct old doc's editor's view state, & finally resolve overall promise
function _configureEditorAndResolve() {
var editor = EditorManager.getActiveEditor();
if (editor) {
if (settings) {
editor.setSelections(settings.selections);
editor.setScrollPos(settings.scrollPos.x, settings.scrollPos.y);
}
}
result.resolve(newFile);
}
// Replace old document with new one in open editor & workingset
function openNewFile() {
var fileOpenPromise;
if (FileViewController.getFileSelectionFocus() === FileViewController.PROJECT_MANAGER) {
// If selection is in the tree, leave workingset unchanged - even if orig file is in the list
fileOpenPromise = FileViewController
.openAndSelectDocument(path, FileViewController.PROJECT_MANAGER);
} else {
// If selection is in workingset, replace orig item in place with the new file
var info = MainViewManager.findInAllWorkingSets(doc.file.fullPath).shift();
// Remove old file from workingset; no redraw yet since there's a pause before the new file is opened
MainViewManager._removeView(info.paneId, doc.file, true);
// Add new file to workingset, and ensure we now redraw (even if index hasn't changed)
fileOpenPromise = handleFileAddToWorkingSetAndOpen({fullPath: path, paneId: info.paneId, index: info.index, forceRedraw: true});
}
// always configure editor after file is opened
fileOpenPromise.always(function () {
_configureEditorAndResolve();
});
}
// Same name as before - just do a regular Save
if (path === origPath) {
doSave(doc).then(result.resolve, result.reject);
return;
}
doc.isSaving = true; // mark that we're saving the document
// First, write document's current text to new file
if (doc.file._encoding && doc.file._encoding !== "UTF-8") {
var projectRoot = ProjectManager.getProjectRoot(),
context = {
location : {
scope: "user",
layer: "project",
layerID: projectRoot.fullPath
}
};
var encoding = PreferencesManager.getViewState("encoding", context);
encoding[path] = doc.file._encoding;
PreferencesManager.setViewState("encoding", encoding, context);
}
newFile = FileSystem.getFileForPath(path);
newFile._encoding = doc.file._encoding;
// Save as warns you when you're about to overwrite a file, so we
// explicitly allow "blind" writes to the filesystem in this case,
// ignoring warnings about the contents being modified outside of
// the editor.
FileUtils.writeText(newFile, doc.getText(true), true)
.done(function () {
// If there were unsaved changes before Save As, they don't stay with the old
// file anymore - so must revert the old doc to match disk content.
// Only do this if the doc was dirty: _doRevert on a file that is not dirty and
// not in the workingset has the side effect of adding it to the workingset.
if (doc.isDirty && !(doc.isUntitled())) {
// if the file is dirty it must be in the workingset
// _doRevert is side effect free in this case
_doRevert(doc).always(openNewFile);
} else {
openNewFile();
}
HealthLogger.fileSaved(doc);
})
.fail(function (error) {
_showSaveFileError(error, path)
.done(function () {
result.reject(error);
});
})
.always(function () {
// mark that we're done saving the document
doc.isSaving = false;
});
}
if (doc) {
origPath = doc.file.fullPath;
// If the document is an untitled document, we should default to project root.
if (doc.isUntitled()) {
// (Issue #4489) if we're saving an untitled document, go ahead and switch to this document
// in the editor, so that if we're, for example, saving several files (ie. Save All),
// then the user can visually tell which document we're currently prompting them to save.
var info = MainViewManager.findInAllWorkingSets(origPath).shift();
if (info) {
MainViewManager._open(info.paneId, doc.file);
}
// If the document is untitled, default to project root.
saveAsDefaultPath = ProjectManager.getProjectRoot().fullPath;
} else {
saveAsDefaultPath = FileUtils.getDirectoryPath(origPath);
}
defaultName = FileUtils.getBaseName(origPath);
var file = FileSystem.getFileForPath(origPath);
if (file instanceof InMemoryFile) {
var language = LanguageManager.getLanguageForPath(origPath);
if (language) {
var fileExtensions = language.getFileExtensions();
if (fileExtensions && fileExtensions.length > 0) {
defaultName += "." + fileExtensions[0];
}
}
}
FileSystem.showSaveDialog(Strings.SAVE_FILE_AS, saveAsDefaultPath, defaultName, function (err, selectedPath) {
if (!err) {
if (selectedPath) {
_doSaveAfterSaveDialog(selectedPath);
} else {
dispatchAppQuitCancelledEvent();
result.reject(USER_CANCELED);
}
} else {
result.reject(err);
}
});
} else {
result.reject();
}
return result.promise();
}
/**
* Saves the given file. If no file specified, assumes the current document.
* @param {?{doc: ?Document}} commandData Document to close, or null
* @return {$.Promise} resolved with the saved document's File (which MAY DIFFER from the doc
* passed in, if the doc was untitled). Rejected in case of IO error (after error dialog
* dismissed), or if doc was untitled and the Save dialog was canceled (will be rejected with
* USER_CANCELED object).
*/
function handleFileSave(commandData) {
var activeEditor = EditorManager.getActiveEditor(),
activeDoc = activeEditor && activeEditor.document,
doc = (commandData && commandData.doc) || activeDoc,
settings;
if (doc && !doc.isSaving) {
if (doc.isUntitled()) {
if (doc === activeDoc) {
settings = {
selections: activeEditor.getSelections(),
scrollPos: activeEditor.getScrollPos()
};
}
return _doSaveAs(doc, settings);
} else {
return doSave(doc);
}
}
return $.Deferred().reject().promise();
}
/**
* Saves all unsaved documents corresponding to 'fileList'. 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 but after dismissing we continue saving the other files; after all files have
* been processed, the Promise is rejected if any ONE save operation failed (the error given is the
* first one encountered). If the user cancels any Save As dialog (for untitled files), the
* Promise is immediately rejected.
*
* @param {!Array.<File>} fileList
* @return {!$.Promise} Resolved with {!Array.<File>}, which may differ from 'fileList'
* if any of the files were Unsaved documents. Or rejected with {?FileSystemError}.
*/
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,
filesAfterSave = [];
return Async.doSequentially(
fileList,
function (file) {
// Abort remaining saves if user canceled any Save As dialog
if (userCanceled) {
return (new $.Deferred()).reject().promise();
}
var doc = DocumentManager.getOpenDocumentForPath(file.fullPath);
if (doc) {
var savePromise = handleFileSave({doc: doc});
savePromise
.done(function (newFile) {
filesAfterSave.push(newFile);
})
.fail(function (error) {
if (error === USER_CANCELED) {
userCanceled = true;
}
});
return savePromise;
} else {
// workingset entry that was never actually opened - ignore
filesAfterSave.push(file);
return (new $.Deferred()).resolve().promise();
}
},
false // if any save fails, continue trying to save other files anyway; then reject at end
).then(function () {
return filesAfterSave;
});
}
/**
* Saves all unsaved documents. See _saveFileList() for details on the semantics.
* @return {$.Promise}
*/
function saveAll() {
return _saveFileList(MainViewManager.getWorkingSet(MainViewManager.ALL_PANES));
}
/**
* Prompts user with save as dialog and saves document.
* @return {$.Promise} a promise that is resolved once the save has been completed
*/
handleFileSaveAs = function (commandData) {
// Default to current document if doc is null
var doc = null,
settings;
if (commandData) {
doc = commandData.doc;
} else {
var activeEditor = EditorManager.getActiveEditor();
if (activeEditor) {
doc = activeEditor.document;
settings = {};
settings.selections = activeEditor.getSelections();
settings.scrollPos = activeEditor.getScrollPos();
}
}
// doc may still be null, e.g. if no editors are open, but _doSaveAs() does a null check on
// doc.
return _doSaveAs(doc, settings);
};
/**
* Saves all unsaved documents.
* @return {$.Promise} a promise that is resolved once ALL the saves have been completed; or rejected
* after all operations completed if any ONE of them failed.
*/
function handleFileSaveAll() {
return saveAll();
}
/**
* Closes the specified file: removes it from the workingset, and closes the main editor if one
* is open. Prompts user about saving changes first, if document is dirty.
*
* @param {?{file: File, promptOnly:boolean}} commandData Optional bag of arguments:
* file - File to close; assumes the current document if not specified.
* promptOnly - If true, only displays the relevant confirmation UI and does NOT actually
* close the document. This is useful when chaining file-close together with other user
* prompts that may be cancelable.
* _forceClose - If true, closes the document without prompting even if there are unsaved
* changes. Only for use in unit tests.
* @return {$.Promise} a promise that is resolved when the file is closed, or if no file is open.
* FUTURE: should we reject the promise if no file is open?
*/
function handleFileClose(commandData) {
var file,
promptOnly,
_forceClose,
_spawnedRequest,
paneId = MainViewManager.ACTIVE_PANE;
if (commandData) {
file = commandData.file;
promptOnly = commandData.promptOnly;
_forceClose = commandData._forceClose;
paneId = commandData.paneId || paneId;
_spawnedRequest = commandData.spawnedRequest || false;
}
// utility function for handleFileClose: closes document & removes from workingset
function doClose(file) {
if (!promptOnly) {
MainViewManager._close(paneId, file);
HealthLogger.fileClosed(file);
}
}
var result = new $.Deferred(), promise = result.promise();
// Default to current document if doc is null
if (!file) {
file = MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE);
}
// No-op if called when nothing is open; TODO: (issue #273) should command be grayed out instead?
if (!file) {
result.resolve();
return promise;
}
var doc = DocumentManager.getOpenDocumentForPath(file.fullPath);
if (doc && doc.isDirty && !_forceClose && (MainViewManager.isExclusiveToPane(doc.file, paneId) || _spawnedRequest)) {
// Document is dirty: prompt to save changes before closing if only the document is exclusively
// listed in the requested pane or this is part of a list close request
var filename = FileUtils.getBaseName(doc.file.fullPath);
Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_SAVE_CLOSE,
Strings.SAVE_CLOSE_TITLE,
StringUtils.format(
Strings.SAVE_CLOSE_MESSAGE,
StringUtils.breakableUrl(filename)
),
[
{
className : Dialogs.DIALOG_BTN_CLASS_LEFT,
id : Dialogs.DIALOG_BTN_DONTSAVE,
text : Strings.DONT_SAVE
},
{
className : Dialogs.DIALOG_BTN_CLASS_NORMAL,
id : Dialogs.DIALOG_BTN_CANCEL,
text : Strings.CANCEL
},
{
className : Dialogs.DIALOG_BTN_CLASS_PRIMARY,
id : Dialogs.DIALOG_BTN_OK,
text : Strings.SAVE
}
]
)
.done(function (id) {
if (id === Dialogs.DIALOG_BTN_CANCEL) {
dispatchAppQuitCancelledEvent();
result.reject();
} else if (id === Dialogs.DIALOG_BTN_OK) {
// "Save" case: wait until we confirm save has succeeded before closing
handleFileSave({doc: doc})
.done(function (newFile) {
doClose(newFile);
result.resolve();
})
.fail(function () {
result.reject();
});
} else {
// "Don't Save" case: even though we're closing the main editor, other views of
// the Document may remain in the UI. So we need to revert the Document to a clean
// copy of whatever's on disk.
doClose(file);
// Only reload from disk if we've executed the Close for real.
if (promptOnly) {
result.resolve();
} else {
// Even if there are no listeners attached to the document at this point, we want
// to do the revert anyway, because clients who are listening to the global documentChange
// event from the Document module (rather than attaching to the document directly),
// such as the Find in Files panel, should get a change event. However, in that case,
// we want to ignore errors during the revert, since we don't want a failed revert
// to throw a dialog if the document isn't actually open in the UI.
var suppressError = !DocumentManager.getOpenDocumentForPath(file.fullPath);
_doRevert(doc, suppressError)
.then(result.resolve, result.reject);
}
}
});
result.always(function () {
MainViewManager.focusActivePane();
});
} else {
// File is not open, or IS open but Document not dirty: close immediately
doClose(file);
MainViewManager.focusActivePane();
result.resolve();
}
return promise;
}
/**
* @param {!Array.<File>} list - the list of files to close
* @param {boolean} promptOnly - true to just prompt for saving documents with actually closing them.
* @param {boolean} _forceClose Whether to force all the documents to close even if they have unsaved changes. For unit testing only.
* @return {jQuery.Promise} promise that is resolved or rejected when the function finishes.
*/
function _closeList(list, promptOnly, _forceClose) {
var result = new $.Deferred(),
unsavedDocs = [];
list.forEach(function (file) {
var doc = DocumentManager.getOpenDocumentForPath(file.fullPath);
if (doc && doc.isDirty) {
unsavedDocs.push(doc);
}
});
if (unsavedDocs.length === 0 || _forceClose) {
// No unsaved changes or we want to ignore them, so we can proceed without a prompt
result.resolve();
} else if (unsavedDocs.length === 1) {
// Only one unsaved file: show the usual single-file-close confirmation UI
var fileCloseArgs = { file: unsavedDocs[0].file, promptOnly: promptOnly, spawnedRequest: true };
handleFileClose(fileCloseArgs).done(function () {
// still need to close any other, non-unsaved documents
result.resolve();
}).fail(function () {
result.reject();
});
} else {
// Multiple unsaved files: show a single bulk prompt listing all files
var message = Strings.SAVE_CLOSE_MULTI_MESSAGE + FileUtils.makeDialogFileList(_.map(unsavedDocs, _shortTitleForDocument));
Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_SAVE_CLOSE,
Strings.SAVE_CLOSE_TITLE,
message,
[
{
className : Dialogs.DIALOG_BTN_CLASS_LEFT,
id : Dialogs.DIALOG_BTN_DONTSAVE,
text : Strings.DONT_SAVE
},
{
className : Dialogs.DIALOG_BTN_CLASS_NORMAL,
id : Dialogs.DIALOG_BTN_CANCEL,
text : Strings.CANCEL
},
{
className : Dialogs.DIALOG_BTN_CLASS_PRIMARY,
id : Dialogs.DIALOG_BTN_OK,
text : Strings.SAVE
}
]
)
.done(function (id) {
if (id === Dialogs.DIALOG_BTN_CANCEL) {
dispatchAppQuitCancelledEvent();
result.reject();
} else if (id === Dialogs.DIALOG_BTN_OK) {
// Save all unsaved files, then if that succeeds, close all
_saveFileList(list).done(function (listAfterSave) {
// List of files after save may be different, if any were Untitled
result.resolve(listAfterSave);
}).fail(function () {
result.reject();
});
} else {
// "Don't Save" case--we can just go ahead and close all files.
result.resolve();
}
});
}
// If all the unsaved-changes confirmations pan out above, then go ahead & close all editors
// NOTE: this still happens before any done() handlers added by our caller, because jQ
// guarantees that handlers run in the order they are added.
result.done(function (listAfterSave) {
listAfterSave = listAfterSave || list;
if (!promptOnly) {
MainViewManager._closeList(MainViewManager.ALL_PANES, listAfterSave);
}
});
return result.promise();
}
/**
* Closes all open files; equivalent to calling handleFileClose() for each document, except
* that unsaved changes are confirmed once, in bulk.
* @param {?{promptOnly: boolean, _forceClose: boolean}}
* If promptOnly is 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.
* If _forceClose is true, forces the files to close with no confirmation even if dirty.
* Should only be used for unit test cleanup.
* @return {$.Promise} a promise that is resolved when all files are closed
*/
function handleFileCloseAll(commandData) {
return _closeList(MainViewManager.getAllOpenFiles(),
(commandData && commandData.promptOnly), (commandData && commandData._forceClose));
}
/**
* Closes a list of open files; equivalent to calling handleFileClose() for each document, except
* that unsaved changes are confirmed once, in bulk.
* @param {?{promptOnly: boolean, _forceClose: boolean}}
* If promptOnly is 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.
* If _forceClose is true, forces the files to close with no confirmation even if dirty.
* Should only be used for unit test cleanup.
* @return {$.Promise} a promise that is resolved when all files are closed
*/
function handleFileCloseList(commandData) {
return _closeList(commandData.fileList);
}
/**
* @private - tracks our closing state if we get called again
*/
var _windowGoingAway = false;
/**
* @private
* Common implementation for close/quit/reload which all mostly
* the same except for the final step
* @param {Object} commandData - (not referenced)
* @param {!function()} postCloseHandler - called after close
* @param {!function()} failHandler - called when the save fails to cancel closing the window
*/
function _handleWindowGoingAway(commandData, postCloseHandler, failHandler) {
if (_windowGoingAway) {
//if we get called back while we're closing, then just return
return (new $.Deferred()).reject().promise();
}
return CommandManager.execute(Commands.FILE_CLOSE_ALL, { promptOnly: true })
.done(function () {
_windowGoingAway = true;
// Give everyone a chance to save their state - but don't let any problems block
// us from quitting
try {
ProjectManager.trigger("beforeAppClose");
} catch (ex) {
console.error(ex);
}
postCloseHandler();
})
.fail(function () {
_windowGoingAway = false;
if (failHandler) {
failHandler();
}
});
}
/**
* @private
* Implementation for abortQuit callback to reset quit sequence settings
*/
function handleAbortQuit() {
_windowGoingAway = false;
}
/**
* @private
* Implementation for native APP_BEFORE_MENUPOPUP callback to trigger beforeMenuPopup event
*/
function handleBeforeMenuPopup() {
PopUpManager.trigger("beforeMenuPopup");
}
/**
* Confirms any unsaved changes, then closes the window
* @param {Object} command data
*/
function handleFileCloseWindow(commandData) {
return _handleWindowGoingAway(
commandData,
function () {
window.close();
},
function () {
// if fail, tell the app to abort any pending quit operation.
brackets.app.abortQuit();
}
);
}
/** Show a textfield to rename whatever is currently selected in the sidebar (or current doc if nothing else selected) */
function handleFileRename() {
// Prefer selected sidebar item (which could be a folder)
var entry = ProjectManager.getContext();
if (!entry) {
// Else use current file (not selected in ProjectManager if not visible in tree or workingset)
entry = MainViewManager.getCurrentlyViewedFile();
}
if (entry) {
ProjectManager.renameItemInline(entry);
}
}
/** Closes the window, then quits the app */
function handleFileQuit(commandData) {
return _handleWindowGoingAway(
commandData,
function () {
brackets.app.quit();
},
function () {
// if fail, don't exit: user canceled (or asked us to save changes first, but we failed to do so)
brackets.app.abortQuit();
}
);
}
/** Are we already listening for a keyup to call detectDocumentNavEnd()? */
var _addedNavKeyHandler = false;
/**
* When the Ctrl key is released, if we were in the middle of a next/prev document navigation
* sequence, now is the time to end it and update the MRU order. If we allowed the order to update
* on every next/prev increment, the 1st & 2nd entries would just switch places forever and we'd
* never get further down the list.
* @param {jQueryEvent} event Key-up event
*/
function detectDocumentNavEnd(event) {
if (event.keyCode === KeyEvent.DOM_VK_CONTROL) { // Ctrl key
MainViewManager.endTraversal();
_addedNavKeyHandler = false;
$(window.document.body).off("keyup", detectDocumentNavEnd);
}
}
/**
* Navigate to the next/previous (MRU or list order) document. Don't update MRU order yet
* @param {!number} inc Delta indicating in which direction we're going
* @param {?boolean} listOrder Whether to navigate using MRU or list order. Defaults to MRU order
*/
function goNextPrevDoc(inc, listOrder) {
var result;
if (listOrder) {
result = MainViewManager.traverseToNextViewInListOrder(inc);
} else {
result = MainViewManager.traverseToNextViewByMRU(inc);
}
if (result) {
var file = result.file,
paneId = result.paneId;
MainViewManager.beginTraversal();
CommandManager.execute(Commands.FILE_OPEN, {fullPath: file.fullPath,
paneId: paneId });
// Listen for ending of Ctrl+Tab sequence
if (!_addedNavKeyHandler) {
_addedNavKeyHandler = true;
$(window.document.body).keyup(detectDocumentNavEnd);
}
}
}
/** Next Doc command handler (MRU order) **/
function handleGoNextDoc() {
goNextPrevDoc(+1);
}
/** Previous Doc command handler (MRU order) **/
function handleGoPrevDoc() {
goNextPrevDoc(-1);
}
/** Next Doc command handler (list order) **/
function handleGoNextDocListOrder() {
goNextPrevDoc(+1, true);
}
/** Previous Doc command handler (list order) **/
function handleGoPrevDocListOrder() {
goNextPrevDoc(-1, true);
}
/** Show in File Tree command handler **/
function handleShowInTree() {
ProjectManager.showInTree(MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE));
}
/** Delete file command handler **/
function handleFileDelete() {
var entry = ProjectManager.getSelectedItem();
Dialogs.showModalDialog(
DefaultDialogs.DIALOG_ID_EXT_DELETED,
Strings.CONFIRM_DELETE_TITLE,
StringUtils.format(
entry.isFile ? Strings.CONFIRM_FILE_DELETE : Strings.CONFIRM_FOLDER_DELETE,
StringUtils.breakableUrl(entry.name)
),
[
{
className : Dialogs.DIALOG_BTN_CLASS_NORMAL,
id : Dialogs.DIALOG_BTN_CANCEL,
text : Strings.CANCEL
},
{
className : Dialogs.DIALOG_BTN_CLASS_PRIMARY,
id : Dialogs.DIALOG_BTN_OK,
text : Strings.DELETE
}
]
)
.done(function (id) {
if (id === Dialogs.DIALOG_BTN_OK) {
ProjectManager.deleteItem(entry);
}
});
}
/** Show the selected sidebar (tree or workingset) item in Finder/Explorer */
function handleShowInOS() {
var entry = ProjectManager.getSelectedItem();
if (entry) {
brackets.app.showOSFolder(entry.fullPath, function (err) {
if (err) {
console.error("Error showing '" + entry.fullPath + "' in OS folder:", err);
}
});
}
}
/**
* Disables Brackets' cache via the remote debugging protocol.
* @return {$.Promise} A jQuery promise that will be resolved when the cache is disabled and be rejected in any other case
*/
function _disableCache() {
var result = new $.Deferred();
if (brackets.inBrowser) {
result.resolve();
} else {
brackets.app.getRemoteDebuggingPort(function (err, port){
if ((!err) && port && port > 0) {
Inspector.getDebuggableWindows("127.0.0.1", port)
.fail(result.reject)
.done(function (response) {
var page = response[0];
if (!page || !page.webSocketDebuggerUrl) {
result.reject();
return;
}
var _socket = new WebSocket(page.webSocketDebuggerUrl);
// Disable the cache
_socket.onopen = function _onConnect() {
_socket.send(JSON.stringify({ id: 1, method: "Network.setCacheDisabled", params: { "cacheDisabled": true } }));
};
// The first message will be the confirmation => disconnected to allow remote debugging of Brackets
_socket.onmessage = function _onMessage(e) {
_socket.close();
result.resolve();
};
// In case of an error
_socket.onerror = result.reject;
});
} else {
result.reject();
}
});
}
return result.promise();
}
/**
* Does a full reload of the browser window
* @param {string} href The url to reload into the window
*/
function browserReload(href) {
if (_isReloading) {
return;
}
_isReloading = true;
return CommandManager.execute(Commands.FILE_CLOSE_ALL, { promptOnly: true }).done(function () {
// Give everyone a chance to save their state - but don't let any problems block
// us from quitting
try {
ProjectManager.trigger("beforeAppClose");
} catch (ex) {
console.error(ex);
}
// Disable the cache to make reloads work
_disableCache().always(function () {
// Remove all menus to assure every part of Brackets is reloaded
_.forEach(Menus.getAllMenus(), function (value, key) {
Menus.removeMenu(key);
});
// If there's a fragment in both URLs, setting location.href won't actually reload
var fragment = href.indexOf("#");
if (fragment !== -1) {
href = href.substr(0, fragment);
}
// Defer for a more successful reload - issue #11539
setTimeout(function () {
window.location.href = href;
}, 1000);
});
}).fail(function () {
_isReloading = false;
});
}
/**
* Restarts brackets Handler
* @param {boolean=} loadWithoutExtensions - true to restart without extensions,
* otherwise extensions are loadeed as it is durning a typical boot
*/
function handleReload(loadWithoutExtensions) {
var href = window.location.href,
params = new UrlParams();
// Make sure the Reload Without User Extensions parameter is removed
params.parse();
if (loadWithoutExtensions) {
if (!params.get("reloadWithoutUserExts")) {
params.put("reloadWithoutUserExts", true);
}
} else {
if (params.get("reloadWithoutUserExts")) {
params.remove("reloadWithoutUserExts");
}
}
if (href.indexOf("?") !== -1) {
href = href.substring(0, href.indexOf("?"));
}
if (!params.isEmpty()) {
href += "?" + params.toString();
}
// Give Mac native menus extra time to update shortcut highlighting.
// Prevents the menu highlighting from getting messed up after reload.
window.setTimeout(function () {
browserReload(href);
}, 100);
}
/** Reload Without Extensions commnad handler **/
var handleReloadWithoutExts = _.partial(handleReload, true);
/**
* Attach a beforeunload handler to notify user about unsaved changes and URL redirection in CEF.
* Prevents data loss in scenario reported under #13708
* Make sure we don't attach this handler if the current window is actually a test window
**/
var isTestWindow = (new window.URLSearchParams(window.location.search || "")).get("testEnvironment");
if (!isTestWindow) {
window.onbeforeunload = function(e) {
var openDocs = DocumentManager.getAllOpenDocuments();
// Detect any unsaved changes
openDocs = openDocs.filter(function(doc) {
return doc && doc.isDirty;
});
// Ensure we are not in normal app-quit or reload workflow
if (!_isReloading && !_windowGoingAway) {
if (openDocs.length > 0) {
return Strings.WINDOW_UNLOAD_WARNING_WITH_UNSAVED_CHANGES;
} else {
return Strings.WINDOW_UNLOAD_WARNING;
}
}
};
}
/** Do some initialization when the DOM is ready **/
AppInit.htmlReady(function () {
// If in Reload Without User Extensions mode, update UI and log console message
var params = new UrlParams(),
$icon = $("#toolbar-extension-manager"),
$indicator = $("<div>" + Strings.STATUSBAR_USER_EXTENSIONS_DISABLED + "</div>");
params.parse();
if (params.get("reloadWithoutUserExts") === "true") {
CommandManager.get(Commands.FILE_EXTENSION_MANAGER).setEnabled(false);
$icon.css({display: "none"});
StatusBar.addIndicator("status-user-exts", $indicator, true);
console.log("Brackets reloaded with extensions disabled");
}
// Init DOM elements
_$titleContainerToolbar = $("#titlebar");
_$titleWrapper = $(".title-wrapper", _$titleContainerToolbar);
_$title = $(".title", _$titleWrapper);
_$dirtydot = $(".dirty-dot", _$titleWrapper);
});
// Exported for unit testing only
exports._parseDecoratedPath = _parseDecoratedPath;
// Set some command strings
var quitString = Strings.CMD_QUIT,
showInOS = Strings.CMD_SHOW_IN_OS;
if (brackets.platform === "win") {
quitString = Strings.CMD_EXIT;
showInOS = Strings.CMD_SHOW_IN_EXPLORER;
} else if (brackets.platform === "mac") {
showInOS = Strings.CMD_SHOW_IN_FINDER;
}
// Define public API
exports.showFileOpenError = showFileOpenError;
exports.APP_QUIT_CANCELLED = APP_QUIT_CANCELLED;
// Deprecated commands
CommandManager.register(Strings.CMD_ADD_TO_WORKING_SET, Commands.FILE_ADD_TO_WORKING_SET, handleFileAddToWorkingSet);
CommandManager.register(Strings.CMD_FILE_OPEN, Commands.FILE_OPEN, handleDocumentOpen);
// New commands
CommandManager.register(Strings.CMD_ADD_TO_WORKING_SET, Commands.CMD_ADD_TO_WORKINGSET_AND_OPEN, handleFileAddToWorkingSetAndOpen);
CommandManager.register(Strings.CMD_FILE_OPEN, Commands.CMD_OPEN, handleFileOpen);
// File Commands
CommandManager.register(Strings.CMD_FILE_NEW_UNTITLED, Commands.FILE_NEW_UNTITLED, handleFileNew);
CommandManager.register(Strings.CMD_FILE_NEW, Commands.FILE_NEW, handleFileNewInProject);
CommandManager.register(Strings.CMD_FILE_NEW_FOLDER, Commands.FILE_NEW_FOLDER, handleNewFolderInProject);
CommandManager.register(Strings.CMD_FILE_SAVE, Commands.FILE_SAVE, handleFileSave);
CommandManager.register(Strings.CMD_FILE_SAVE_ALL, Commands.FILE_SAVE_ALL, handleFileSaveAll);
CommandManager.register(Strings.CMD_FILE_SAVE_AS, Commands.FILE_SAVE_AS, handleFileSaveAs);
CommandManager.register(Strings.CMD_FILE_RENAME, Commands.FILE_RENAME, handleFileRename);
CommandManager.register(Strings.CMD_FILE_DELETE, Commands.FILE_DELETE, handleFileDelete);
// Close Commands
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);
// Traversal
CommandManager.register(Strings.CMD_NEXT_DOC, Commands.NAVIGATE_NEXT_DOC, handleGoNextDoc);
CommandManager.register(Strings.CMD_PREV_DOC, Commands.NAVIGATE_PREV_DOC, handleGoPrevDoc);
CommandManager.register(Strings.CMD_NEXT_DOC_LIST_ORDER, Commands.NAVIGATE_NEXT_DOC_LIST_ORDER, handleGoNextDocListOrder);
CommandManager.register(Strings.CMD_PREV_DOC_LIST_ORDER, Commands.NAVIGATE_PREV_DOC_LIST_ORDER, handleGoPrevDocListOrder);
// Special Commands
CommandManager.register(showInOS, Commands.NAVIGATE_SHOW_IN_OS, handleShowInOS);
CommandManager.register(quitString, Commands.FILE_QUIT, handleFileQuit);
CommandManager.register(Strings.CMD_SHOW_IN_TREE, Commands.NAVIGATE_SHOW_IN_FILE_TREE, handleShowInTree);
// These commands have no UI representation and are only used internally
CommandManager.registerInternal(Commands.APP_ABORT_QUIT, handleAbortQuit);
CommandManager.registerInternal(Commands.APP_BEFORE_MENUPOPUP, handleBeforeMenuPopup);
CommandManager.registerInternal(Commands.FILE_CLOSE_WINDOW, handleFileCloseWindow);
CommandManager.registerInternal(Commands.APP_RELOAD, handleReload);
CommandManager.registerInternal(Commands.APP_RELOAD_WITHOUT_EXTS, handleReloadWithoutExts);
// Listen for changes that require updating the editor titlebar
ProjectManager.on("projectOpen", _updateTitle);
DocumentManager.on("dirtyFlagChange", handleDirtyChange);
DocumentManager.on("fileNameChange", handleCurrentFileChange);
MainViewManager.on("currentFileChange", handleCurrentFileChange);
// Reset the untitled document counter before changing projects
ProjectManager.on("beforeProjectClose", function () { _nextUntitledIndexToUse = 1; });
});