Event triggered when File Save is cancelled, when prompted to save dirty files
var APP_QUIT_CANCELLED = "appQuitCancelled";
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);
String template for window title when a file is open.
var WINDOW_TITLE_STRING_DOC = "{0} ({1}) " + _osDash + " {2}";
String template for window title when no file is open.
var WINDOW_TITLE_STRING_NO_DOC = "{0} " + _osDash + " {1}";
Container for dirty dot; must be an inline element
var _$dirtydot = null;
Container for label shown above editor; must be an inline element
var _$title = null;
Container for _$titleWrapper; if changing title changes this element's height, must kick editor to resize
var _$titleContainerToolbar = null;
Container for _$title; need not be an inline element
var _$titleWrapper = null;
Are we already listening for a keyup to call detectDocumentNavEnd()?
var _addedNavKeyHandler = false;
Label shown above editor for current document: filename and potentially some of its path
var _currentTitlePath = null;
Last known height of _$titleContainerToolbar
var _lastToolbarHeight = null;
index to use for next, new Untitled document
var _nextUntitledIndexToUse = 1;
Determine the dash character for each platform. Use emdash on Mac and a standard dash on all other platforms.
var _osDash = brackets.platform === "mac" ? "\u2014" : "-";
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;
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;
}
}
};
}
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();
}
Disables Brackets' cache via the remote debugging protocol.
function _disableCache() {
var result = new $.Deferred();
if (brackets.inBrowser) {
result.resolve();
} else {
var port = brackets.app.getRemoteDebuggingPort ? brackets.app.getRemoteDebuggingPort() : 9234;
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;
});
}
return result.promise();
}
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();
}
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();
}
Reverts the Document to the current contents of its file on disk. Discards any unsaved changes in the Document.
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();
}
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.
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();
}
})
.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();
}
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();
}
Bottleneck function for creating new files and folders in the project tree.
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));
}
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();
}
});
}
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;
}
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.
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;
});
}
Returns a short title for a given document.
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);
}
}
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)
)
);
}
Updates the title bar with new file title or dirty indicator
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;
}
Does a full reload of the browser 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;
});
}
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.
function detectDocumentNavEnd(event) {
if (event.keyCode === KeyEvent.DOM_VK_CONTROL) { // Ctrl key
MainViewManager.endTraversal();
_addedNavKeyHandler = false;
$(window.document.body).off("keyup", detectDocumentNavEnd);
}
}
Dispatches the app quit cancelled event
function dispatchAppQuitCancelledEvent() {
exports.trigger(exports.APP_QUIT_CANCELLED);
}
Saves a document to its existing path. Does NOT support untitled documents.
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);
})
.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();
}
Navigate to the next/previous (MRU or list order) document. Don't update MRU order yet
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);
}
}
}
function handleBeforeMenuPopup() {
PopUpManager.trigger("beforeMenuPopup");
}
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();
}
}
Opens the given file, makes it the current file, does NOT add it to the workingset
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();
}
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();
}
Opens the given file, makes it the current file, AND adds it to the workingset
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);
});
}
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.
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);
}
}
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;
}
Closes all open files; equivalent to calling handleFileClose() for each document, except that unsaved changes are confirmed once, in bulk.
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.
function handleFileCloseList(commandData) {
return _closeList(commandData.fileList);
}
Confirms any unsaved changes, then closes the window
function handleFileCloseWindow(commandData) {
return _handleWindowGoingAway(
commandData,
function () {
window.close();
},
function () {
// if fail, tell the app to abort any pending quit operation.
brackets.app.abortQuit();
}
);
}
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);
}
});
}
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);
return new $.Deferred().resolve(doc).promise();
}
Create a new file in the project tree.
function handleFileNewInProject() {
_handleNewItemInProject(false);
}
Opens the given file and makes it the current file. Does NOT add it to the workingset.
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")
}
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();
}
);
}
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);
}
}
Saves the given file. If no file specified, assumes the current document.
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.
function handleFileSaveAll() {
return saveAll();
}
Next Doc command handler (MRU order) *
function handleGoNextDoc() {
goNextPrevDoc(+1);
}
Next Doc command handler (list order) *
function handleGoNextDocListOrder() {
goNextPrevDoc(+1, true);
}
Previous Doc command handler (MRU order) *
function handleGoPrevDoc() {
goNextPrevDoc(-1);
}
Previous Doc command handler (list order) *
function handleGoPrevDocListOrder() {
goNextPrevDoc(-1, true);
}
Create a new folder in the project tree.
function handleNewFolderInProject() {
_handleNewItemInProject(true);
}
Restarts brackets Handler
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);
}
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);
}
});
}
}
Show in File Tree command handler *
function handleShowInTree() {
ProjectManager.showInTree(MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE));
}
Saves all unsaved documents. See _saveFileList() for details on the semantics.
function saveAll() {
return _saveFileList(MainViewManager.getWorkingSet(MainViewManager.ALL_PANES));
}
Shows an error dialog indicating that the given file could not be opened due to the given error
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)
)
);
}