/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/project/ProjectManager.js
1 504 строки
58 KB
Nitesh Kumar
few corrections in naming conventions
05 мар 2020, 15:38
05 мар 2020, 15:38
7f5f064
Код
Авторство
О чём код?
/* * 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. * */ /** * ProjectManager glues together the project model and file tree view and integrates as needed with other parts * of Brackets. It is responsible for creating and updating the project tree when projects are opened * and when changes occur to the file tree. * * This module dispatches these events: * - beforeProjectClose -- before `_projectRoot` changes, but working set files still open * - projectClose -- *just* before `_projectRoot` changes; working set already cleared * & project root unwatched * - beforeAppClose -- before Brackets quits entirely * - projectOpen -- after `_projectRoot` changes and the tree is re-rendered * - projectRefresh -- when project tree is re-rendered for a reason other than * a project being opened (e.g. from the Refresh command) * * To listen for events, do something like this: (see EventDispatcher for details on this pattern) * ProjectManager.on("eventname", handler); */ define(function (require, exports, module) { "use strict"; require("utils/Global"); var _ = require("thirdparty/lodash"); // Load dependent modules var AppInit = require("utils/AppInit"), Async = require("utils/Async"), PreferencesDialogs = require("preferences/PreferencesDialogs"), PreferencesManager = require("preferences/PreferencesManager"), DocumentManager = require("document/DocumentManager"), MainViewManager = require("view/MainViewManager"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), Dialogs = require("widgets/Dialogs"), DefaultDialogs = require("widgets/DefaultDialogs"), EventDispatcher = require("utils/EventDispatcher"), LanguageManager = require("language/LanguageManager"), Menus = require("command/Menus"), StringUtils = require("utils/StringUtils"), Strings = require("strings"), FileSystem = require("filesystem/FileSystem"), FileViewController = require("project/FileViewController"), PerfUtils = require("utils/PerfUtils"), FileUtils = require("file/FileUtils"), FileSystemError = require("filesystem/FileSystemError"), Urls = require("i18n!nls/urls"), FileSyncManager = require("project/FileSyncManager"), ProjectModel = require("project/ProjectModel"), FileTreeView = require("project/FileTreeView"), ViewUtils = require("utils/ViewUtils"); // Needed to ensure that menus are set up when we need them. // See #10115 require("command/DefaultMenus"); /** * @private * Filename to use for project settings files. * @type {string} */ var SETTINGS_FILENAME = "." + PreferencesManager.SETTINGS_FILENAME; /** * Name of the preferences for sorting directories first * * @type {string} */ var SORT_DIRECTORIES_FIRST = "sortDirectoriesFirst"; /** * @private * Forward declarations to make JSLint happy. */ var _fileSystemChange, _fileSystemRename, _showErrorDialog, _saveTreeState, renameItemInline, _renderTreeSync, _renderTree; /** * @const * @private * Error context to show the correct error message * @type {int} */ var ERR_TYPE_CREATE = 1, ERR_TYPE_CREATE_EXISTS = 2, ERR_TYPE_RENAME = 3, ERR_TYPE_DELETE = 4, ERR_TYPE_LOADING_PROJECT = 5, ERR_TYPE_LOADING_PROJECT_NATIVE = 6, ERR_TYPE_MAX_FILES = 7, ERR_TYPE_OPEN_DIALOG = 8, ERR_TYPE_INVALID_FILENAME = 9, ERR_TYPE_MOVE = 10; /** * @private * Reference to the tree control container div. Initialized by * htmlReady handler * @type {jQueryObject} */ var $projectTreeContainer; /** * @private * * Reference to the container of the Preact component. Everything in this * node is managed by Preact. * @type {Element} */ var fileTreeViewContainer; /** * @private * * Does the file tree currently have the focus? * * @return {boolean} `true` if the file tree has the focus */ function _hasFileSelectionFocus() { return FileViewController.getFileSelectionFocus() === FileViewController.PROJECT_MANAGER; } /** * @private * Singleton ProjectModel object. * @type {ProjectModel.ProjectModel} */ var model = new ProjectModel.ProjectModel({ focused: _hasFileSelectionFocus() }); /** * @private * @type {boolean} * A flag to remember when user has been warned about too many files, so they * are only warned once per project/session. */ var _projectWarnedForTooManyFiles = false; /** * @private * * Event handler which displays an error based on a problem creating a file. * * @param {$.Event} e jQuery event object * @param {{type:any,isFolder:boolean}} errorInfo Information passed in the error events */ function _displayCreationError(e, errorInfo) { window.setTimeout(function () { var error = errorInfo.type, isFolder = errorInfo.isFolder, name = errorInfo.name; if (error === FileSystemError.ALREADY_EXISTS) { _showErrorDialog(ERR_TYPE_CREATE_EXISTS, isFolder, null, name); } else if (error === ProjectModel.ERROR_INVALID_FILENAME) { _showErrorDialog(ERR_TYPE_INVALID_FILENAME, isFolder, ProjectModel._invalidChars); } else { var errString = error === FileSystemError.NOT_WRITABLE ? Strings.NO_MODIFICATION_ALLOWED_ERR : StringUtils.format(Strings.GENERIC_ERROR, error); _showErrorDialog(ERR_TYPE_CREATE, isFolder, errString, name).getPromise(); } }, 10); } /** * @private * * Reverts to the previous selection (useful if there's an error). * * @param {string|File} previousPath The previously selected path. * @param {boolean} switchToWorkingSet True if we need to switch focus to the Working Set */ function _revertSelection(previousPath, switchToWorkingSet) { model.setSelected(previousPath); if (switchToWorkingSet) { FileViewController.setFileViewFocus(FileViewController.WORKING_SET_VIEW); } } /** * @constructor * @private * * Manages the interaction between the view and the model. This is loosely structured in * the style of [Flux](https://github.com/facebook/flux), but the initial implementation did * not need all of the parts of Flux yet. This ActionCreator could be replaced later with * a real ActionCreator that talks to a Dispatcher. * * Most of the methods just delegate to the ProjectModel. Some are responsible for integration * with other parts of Brackets. * * @param {ProjectModel} model store (in Flux terminology) with the project data */ function ActionCreator(model) { this.model = model; this._bindEvents(); } /** * @private * * Listen to events on the ProjectModel and cause the appropriate behavior within the rest of the system. */ ActionCreator.prototype._bindEvents = function () { // Change events are the standard Flux signal to rerender the view. Note that // current Flux style is to have the view itself listen to the Store for change events // and re-render itself. this.model.on(ProjectModel.EVENT_CHANGE, function () { _renderTree(); }); // The "should select" event signals that we need to open the document based on file tree // activity. this.model.on(ProjectModel.EVENT_SHOULD_SELECT, function (e, data) { if (data.add) { FileViewController.openFileAndAddToWorkingSet(data.path).fail(_.partial(_revertSelection, data.previousPath, !data.hadFocus)); } else { FileViewController.openAndSelectDocument(data.path, FileViewController.PROJECT_MANAGER).fail(_.partial(_revertSelection, data.previousPath, !data.hadFocus)); } }); this.model.on(ProjectModel.EVENT_SHOULD_FOCUS, function () { FileViewController.setFileViewFocus(FileViewController.PROJECT_MANAGER); }); this.model.on(ProjectModel.ERROR_CREATION, _displayCreationError); }; /** * Sets the directory at the given path to open in the tree and saves the open nodes to view state. * * See `ProjectModel.setDirectoryOpen` */ ActionCreator.prototype.setDirectoryOpen = function (path, open) { this.model.setDirectoryOpen(path, open).then(_saveTreeState); }; /** * See `ProjectModel.setSelected` */ ActionCreator.prototype.setSelected = function (path, doNotOpen) { this.model.setSelected(path, doNotOpen); }; /** * See `ProjectModel.selectInWorkingSet` */ ActionCreator.prototype.selectInWorkingSet = function (path) { this.model.selectInWorkingSet(path); }; /** * See `FileViewController.openWithExternalApplication` */ ActionCreator.prototype.openWithExternalApplication = function (path) { FileViewController.openWithExternalApplication(path); }; /** * See `ProjectModel.setContext` */ ActionCreator.prototype.setContext = function (path) { this.model.setContext(path); }; /** * See `ProjectModel.restoreContext` */ ActionCreator.prototype.restoreContext = function () { this.model.restoreContext(); }; /** * See `ProjectModel.startRename` */ ActionCreator.prototype.startRename = function (path, isMoved) { // This is very not Flux-like, which is a sign that Flux may not be the // right choice here *or* that this architecture needs to evolve subtly // in how errors are reported (more like the create case). // See #9284. renameItemInline(path, isMoved); }; /** * See `ProjectModel.setRenameValue` */ ActionCreator.prototype.setRenameValue = function (path) { this.model.setRenameValue(path); }; /** * See `ProjectModel.cancelRename` */ ActionCreator.prototype.cancelRename = function () { this.model.cancelRename(); }; /** * See `ProjectModel.performRename` */ ActionCreator.prototype.performRename = function () { return this.model.performRename(); }; /** * See `ProjectModel.startCreating` */ ActionCreator.prototype.startCreating = function (basedir, newName, isFolder) { return this.model.startCreating(basedir, newName, isFolder); }; /** * See `ProjectModel.setSortDirectoriesFirst` */ ActionCreator.prototype.setSortDirectoriesFirst = function (sortDirectoriesFirst) { this.model.setSortDirectoriesFirst(sortDirectoriesFirst); }; /** * See `ProjectModel.setFocused` */ ActionCreator.prototype.setFocused = function (focused) { this.model.setFocused(focused); }; /** * See `ProjectModel.setCurrentFile` */ ActionCreator.prototype.setCurrentFile = function (curFile) { this.model.setCurrentFile(curFile); }; /** * See `ProjectModel.toggleSubdirectories` */ ActionCreator.prototype.toggleSubdirectories = function (path, openOrClose) { this.model.toggleSubdirectories(path, openOrClose).then(_saveTreeState); }; /** * See `ProjectModel.closeSubtree` */ ActionCreator.prototype.closeSubtree = function (path) { this.model.closeSubtree(path); _saveTreeState(); }; ActionCreator.prototype.dragItem = function (path) { // Close open menus on drag and clear the context, but only if there's a menu open. if ($(".dropdown.open").length > 0) { Menus.closeAll(); this.setContext(null); } // Close directory, if dragged item is directory if (_.last(path) === '/') { this.setDirectoryOpen(path, false); } }; /** * Moves the item in the oldPath to the newDirectory directory */ ActionCreator.prototype.moveItem = function (oldPath, newDirectory) { var fileName = FileUtils.getBaseName(oldPath), newPath = newDirectory + fileName, self = this; // If item dropped onto itself or onto its parent directory, return if (oldPath === newDirectory || FileUtils.getParentPath(oldPath) === newDirectory) { return; } // Add trailing slash if directory is moved if (_.last(oldPath) === '/') { newPath = ProjectModel._ensureTrailingSlash(newPath); } this.startRename(oldPath, true); this.setRenameValue(newPath); this.performRename(); this.setDirectoryOpen(newDirectory, true); }; /** * See `ProjectModel.refresh` */ ActionCreator.prototype.refresh = function () { this.model.refresh(); }; /** * @private * @type {ActionCreator} * * Singleton actionCreator that is used for dispatching changes to the ProjectModel. */ var actionCreator = new ActionCreator(model); /** * Returns the File or Directory corresponding to the item that was right-clicked on in the file tree menu. * @return {?(File|Directory)} */ function getFileTreeContext() { var selectedEntry = model.getContext(); return selectedEntry; } /** * Returns the File or Directory corresponding to the item selected in the sidebar panel, whether in * the file tree OR in the working set; or null if no item is selected anywhere in the sidebar. * May NOT be identical to the current Document - a folder may be selected in the sidebar, or the sidebar may not * have the current document visible in the tree & working set. * @return {?(File|Directory)} */ function getSelectedItem() { // Prefer file tree context, then file tree selection, else use working set var selectedEntry = getFileTreeContext(); if (!selectedEntry) { selectedEntry = model.getSelected(); } if (!selectedEntry) { selectedEntry = MainViewManager.getCurrentlyViewedFile(); } return selectedEntry; } /** * @private * * Handler for changes in the focus between working set and file tree view. */ function _fileViewControllerChange() { actionCreator.setFocused(_hasFileSelectionFocus()); _renderTree(); } /** * @private * * Handler for changes in document selection. */ function _documentSelectionFocusChange() { var curFullPath = MainViewManager.getCurrentlyViewedPath(MainViewManager.ACTIVE_PANE); if (curFullPath && _hasFileSelectionFocus()) { actionCreator.setSelected(curFullPath, true); } else { actionCreator.setSelected(null); } _fileViewControllerChange(); } /** * @private * * Handler for changes to which file is currently viewed. * * @param {Object} e jQuery event object * @param {File} curFile Currently viewed file. */ function _currentFileChange(e, curFile) { actionCreator.setCurrentFile(curFile); } /** * @private * * Creates a context object for doing project view state lookups. */ function _getProjectViewStateContext() { return { location : { scope: "user", layer: "project", layerID: model.projectRoot.fullPath } }; } /** * Returns the encoded Base URL of the currently loaded project, or empty string if no project * is open (during startup, or running outside of app shell). * @return {String} */ function getBaseUrl() { return model.getBaseUrl(); } /** * Sets the encoded Base URL of the currently loaded project. * @param {String} */ function setBaseUrl(projectBaseUrl) { var context = _getProjectViewStateContext(); projectBaseUrl = model.setBaseUrl(projectBaseUrl); PreferencesManager.setViewState("project.baseUrl", projectBaseUrl, context); } /** * Returns true if absPath lies within the project, false otherwise. * Does not support paths containing ".." * @param {string|FileSystemEntry} absPathOrEntry * @return {boolean} */ function isWithinProject(absPathOrEntry) { return model.isWithinProject(absPathOrEntry); } /** * If absPath lies within the project, returns a project-relative path. Else returns absPath * unmodified. * Does not support paths containing ".." * @param {!string} absPath * @return {!string} */ function makeProjectRelativeIfPossible(absPath) { return model.makeProjectRelativeIfPossible(absPath); } /** * Returns the root folder of the currently loaded project, or null if no project is open (during * startup, or running outside of app shell). * @return {Directory} */ function getProjectRoot() { return model.projectRoot; } /** * @private * * Sets the project root to the given directory, resetting the ProjectModel and file tree in the process. * * @param {Directory} rootEntry directory object for the project root * @return {$.Promise} resolved when the project is done setting up */ function _setProjectRoot(rootEntry) { var d = new $.Deferred(); model.setProjectRoot(rootEntry).then(function () { d.resolve(); model.reopenNodes(PreferencesManager.getViewState("project.treeState", _getProjectViewStateContext())); }); return d.promise(); } /** * @private * * Saves the project path. */ var _saveProjectPath = function () { // save the current project PreferencesManager.setViewState("projectPath", model.projectRoot.fullPath); }; /** * @private * Save tree state. */ _saveTreeState = function () { var context = _getProjectViewStateContext(), openNodes = model.getOpenNodes(); // Store the open nodes by their full path and persist to storage PreferencesManager.setViewState("project.treeState", openNodes, context); }; /** * @private * * Displays an error dialog for problems when working with files in the file tree. * * @param {number} errType type of error that occurred * @param {boolean} isFolder did the error occur because of a folder operation? * @param {string} error message with detail about the error * @param {string} path path to file or folder that had the error * @return {Dialog|null} Dialog if the error message was created */ _showErrorDialog = function (errType, isFolder, error, path) { var titleType = isFolder ? Strings.DIRECTORY_TITLE : Strings.FILE_TITLE, entryType = isFolder ? Strings.DIRECTORY : Strings.FILE, title, message; path = StringUtils.breakableUrl(path); switch (errType) { case ERR_TYPE_CREATE: title = StringUtils.format(Strings.ERROR_CREATING_FILE_TITLE, titleType); message = StringUtils.format(Strings.ERROR_CREATING_FILE, entryType, path, error); break; case ERR_TYPE_CREATE_EXISTS: title = StringUtils.format(Strings.INVALID_FILENAME_TITLE, titleType); message = StringUtils.format(Strings.ENTRY_WITH_SAME_NAME_EXISTS, path); break; case ERR_TYPE_RENAME: title = StringUtils.format(Strings.ERROR_RENAMING_FILE_TITLE, titleType); message = StringUtils.format(Strings.ERROR_RENAMING_FILE, path, error, entryType); break; case ERR_TYPE_MOVE: title = StringUtils.format(Strings.ERROR_MOVING_FILE_TITLE, titleType); message = StringUtils.format(Strings.ERROR_MOVING_FILE, path, error, entryType); break; case ERR_TYPE_DELETE: title = StringUtils.format(Strings.ERROR_DELETING_FILE_TITLE, titleType); message = StringUtils.format(Strings.ERROR_DELETING_FILE, path, error, entryType); break; case ERR_TYPE_LOADING_PROJECT: title = Strings.ERROR_LOADING_PROJECT; message = StringUtils.format(Strings.READ_DIRECTORY_ENTRIES_ERROR, path, error); break; case ERR_TYPE_LOADING_PROJECT_NATIVE: title = Strings.ERROR_LOADING_PROJECT; message = StringUtils.format(Strings.REQUEST_NATIVE_FILE_SYSTEM_ERROR, path, error); break; case ERR_TYPE_MAX_FILES: title = Strings.ERROR_MAX_FILES_TITLE; message = Strings.ERROR_MAX_FILES; break; case ERR_TYPE_OPEN_DIALOG: title = Strings.ERROR_LOADING_PROJECT; message = StringUtils.format(Strings.OPEN_DIALOG_ERROR, error); break; case ERR_TYPE_INVALID_FILENAME: title = StringUtils.format(Strings.INVALID_FILENAME_TITLE, isFolder ? Strings.DIRECTORY_NAME : Strings.FILENAME); message = StringUtils.format(Strings.INVALID_FILENAME_MESSAGE, isFolder ? Strings.DIRECTORY_NAMES_LEDE : Strings.FILENAMES_LEDE, error); break; } if (title && message) { return Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, title, message ); } return null; }; var _RENDER_DEBOUNCE_TIME = 100; /** * @private * * Rerender the file tree view. * * @param {boolean} forceRender Force the tree to rerender. Should only be needed by extensions that call rerenderTree. */ _renderTreeSync = function (forceRender) { var projectRoot = getProjectRoot(); if (!projectRoot) { return; } model.setScrollerInfo($projectTreeContainer[0].scrollWidth, $projectTreeContainer.scrollTop(), $projectTreeContainer.scrollLeft(), $projectTreeContainer.offset().top); FileTreeView.render(fileTreeViewContainer, model._viewModel, projectRoot, actionCreator, forceRender, brackets.platform); }; _renderTree = _.debounce(_renderTreeSync, _RENDER_DEBOUNCE_TIME); /** * @private * * Returns the full path to the welcome project, which we open on first launch. * * @param {string} sampleUrl URL for getting started project * @param {string} initialPath Path to Brackets directory (see FileUtils.getNativeBracketsDirectoryPath()) * @return {!string} fullPath reference */ function _getWelcomeProjectPath() { return ProjectModel._getWelcomeProjectPath(Urls.GETTING_STARTED, FileUtils.getNativeBracketsDirectoryPath()); } /** * Adds the path to the list of welcome projects we've ever seen, if not on the list already. * * @param {string} path Path to possibly add */ function addWelcomeProjectPath(path) { var welcomeProjects = ProjectModel._addWelcomeProjectPath(path, PreferencesManager.getViewState("welcomeProjects")); PreferencesManager.setViewState("welcomeProjects", welcomeProjects); } /** * Returns true if the given path is the same as one of the welcome projects we've previously opened, * or the one for the current build. * * @param {string} path Path to check to see if it's a welcome project path * @return {boolean} true if this is a welcome project path */ function isWelcomeProjectPath(path) { return ProjectModel._isWelcomeProjectPath(path, _getWelcomeProjectPath(), PreferencesManager.getViewState("welcomeProjects")); } /** * If the provided path is to an old welcome project, returns the current one instead. */ function updateWelcomeProjectPath(path) { if (isWelcomeProjectPath(path)) { return _getWelcomeProjectPath(); } else { return path; } } /** * After failing to load a project, this function determines which project path to fallback to. * @return {$.Promise} Promise that resolves to a project path {string} */ function _getFallbackProjectPath() { var fallbackPaths = [], recentProjects = PreferencesManager.getViewState("recentProjects") || [], deferred = new $.Deferred(); // Build ordered fallback path array if (recentProjects.length > 1) { // *Most* recent project is the one that just failed to load, so use second most recent fallbackPaths.push(recentProjects[1]); } // Next is Getting Started project fallbackPaths.push(_getWelcomeProjectPath()); // Helper func for Async.firstSequentially() function processItem(path) { var deferred = new $.Deferred(), fileEntry = FileSystem.getDirectoryForPath(path); fileEntry.exists(function (err, exists) { if (!err && exists) { deferred.resolve(); } else { deferred.reject(); } }); return deferred.promise(); } // Find first path that exists Async.firstSequentially(fallbackPaths, processItem) .done(function (fallbackPath) { deferred.resolve(fallbackPath); }) .fail(function () { // Last resort is Brackets source folder which is guaranteed to exist deferred.resolve(FileUtils.getNativeBracketsDirectoryPath()); }); return deferred.promise(); } /** * Initial project path is stored in prefs, which defaults to the welcome project on * first launch. */ function getInitialProjectPath() { return updateWelcomeProjectPath(PreferencesManager.getViewState("projectPath")); } /** * @private * * Watches the project for filesystem changes so that the tree can be updated. */ function _watchProjectRoot(rootPath) { FileSystem.on("change", _fileSystemChange); FileSystem.on("rename", _fileSystemRename); FileSystem.watch(FileSystem.getDirectoryForPath(rootPath), ProjectModel._shouldShowName, ProjectModel.defaultIgnoreGlobs, function (err) { if (err === FileSystemError.TOO_MANY_ENTRIES) { if (!_projectWarnedForTooManyFiles) { _showErrorDialog(ERR_TYPE_MAX_FILES); _projectWarnedForTooManyFiles = true; } } else if (err) { console.error("Error watching project root: ", rootPath, err); } }); // Reset allFiles cache model._resetCache(); } /** * @private * Close the file system and remove listeners. * @return {$.Promise} A promise that's resolved when the root is unwatched. Rejected if * there is no project root or if the unwatch fails. */ function _unwatchProjectRoot() { var result = new $.Deferred(); if (!model.projectRoot) { result.reject(); } else { FileSystem.off("change", _fileSystemChange); FileSystem.off("rename", _fileSystemRename); FileSystem.unwatch(model.projectRoot, function (err) { if (err) { console.error("Error unwatching project root: ", model.projectRoot.fullPath, err); result.reject(err); } else { result.resolve(); } }); // Reset allFiles cache model._resetCache(); } return result.promise(); } /** * @private * Reloads the project preferences. */ function _reloadProjectPreferencesScope() { var root = getProjectRoot(); if (root) { // Alias the "project" Scope to the path Scope for the project-level settings file PreferencesManager._setProjectSettingsFile(root.fullPath + SETTINGS_FILENAME); } else { PreferencesManager._setProjectSettingsFile(); } } /** * Loads the given folder as a project. Does NOT prompt about any unsaved changes - use openProject() * instead to check for unsaved changes and (optionally) let the user choose the folder to open. * * @param {!string} rootPath Absolute path to the root folder of the project. * A trailing "/" on the path is optional (unlike many Brackets APIs that assume a trailing "/"). * @param {boolean=} isUpdating If true, indicates we're just updating the tree; * if false, a different project is being loaded. * @return {$.Promise} A promise object that will be resolved when the * project is loaded and tree is rendered, or rejected if the project path * fails to load. */ function _loadProject(rootPath, isUpdating) { var result = new $.Deferred(), startLoad = new $.Deferred(); // Some legacy code calls this API with a non-canonical path rootPath = ProjectModel._ensureTrailingSlash(rootPath); var projectPrefFullPath = (rootPath + SETTINGS_FILENAME), file = FileSystem.getFileForPath(projectPrefFullPath); //Verify that the project preferences file (.brackets.json) is NOT corrupted. //If corrupted, display the error message and open the file in editor for the user to edit. FileUtils.readAsText(file) .done(function (text) { try { if (text) { JSON.parse(text); } } catch (err) { // Cannot parse the text read from the project preferences file. var info = MainViewManager.findInAllWorkingSets(projectPrefFullPath); var paneId; if (info.length) { paneId = info[0].paneId; } FileViewController.openFileAndAddToWorkingSet(projectPrefFullPath, paneId) .done(function () { Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, Strings.ERROR_PREFS_CORRUPT_TITLE, Strings.ERROR_PROJ_PREFS_CORRUPT ).done(function () { // give the focus back to the editor with the pref file MainViewManager.focusActivePane(); }); }); } }); if (isUpdating) { // We're just refreshing. Don't need to unwatch the project root, so we can start loading immediately. startLoad.resolve(); } else { if (model.projectRoot && model.projectRoot.fullPath === rootPath) { return (new $.Deferred()).resolve().promise(); } // About to close current project (if any) if (model.projectRoot) { exports.trigger("beforeProjectClose", model.projectRoot); } // close all the old files MainViewManager._closeAll(MainViewManager.ALL_PANES); _unwatchProjectRoot().always(function () { // Done closing old project (if any) if (model.projectRoot) { LanguageManager._resetPathLanguageOverrides(); PreferencesManager._reloadUserPrefs(model.projectRoot); exports.trigger("projectClose", model.projectRoot); } startLoad.resolve(); }); } startLoad.done(function () { var context = { location : { scope: "user", layer: "project" } }; // Clear project path map if (!isUpdating) { PreferencesManager._stateProjectLayer.setProjectPath(rootPath); } // Populate file tree as long as we aren't running in the browser if (!brackets.inBrowser) { if (!isUpdating) { _watchProjectRoot(rootPath); } // Point at a real folder structure on local disk var rootEntry = FileSystem.getDirectoryForPath(rootPath); rootEntry.exists(function (err, exists) { if (exists) { var projectRootChanged = (!model.projectRoot || !rootEntry) || model.projectRoot.fullPath !== rootEntry.fullPath; // Success! var perfTimerName = PerfUtils.markStart("Load Project: " + rootPath); _projectWarnedForTooManyFiles = false; _setProjectRoot(rootEntry).always(function () { model.setBaseUrl(PreferencesManager.getViewState("project.baseUrl", context) || ""); if (projectRootChanged) { _reloadProjectPreferencesScope(); PreferencesManager._setCurrentFile(rootPath); } // If this is the most current welcome project, record it. In future launches, we want // to substitute the latest welcome project from the current build instead of using an // outdated one (when loading recent projects or the last opened project). if (rootPath === _getWelcomeProjectPath()) { addWelcomeProjectPath(rootPath); } if (projectRootChanged) { // Allow asynchronous event handlers to finish before resolving result by collecting promises from them exports.trigger("projectOpen", model.projectRoot); result.resolve(); } else { exports.trigger("projectRefresh", model.projectRoot); result.resolve(); } PerfUtils.addMeasurement(perfTimerName); }); } else { console.log("error loading project"); _showErrorDialog(ERR_TYPE_LOADING_PROJECT_NATIVE, true, err || FileSystemError.NOT_FOUND, rootPath) .done(function () { // Reset _projectRoot to null so that the following _loadProject call won't // run the 'beforeProjectClose' event a second time on the original project, // which is now partially torn down (see #6574). model.projectRoot = null; // The project folder stored in preference doesn't exist, so load the default // project directory. // TODO (issue #267): When Brackets supports having no project directory // defined this code will need to change _getFallbackProjectPath().done(function (path) { _loadProject(path).always(function () { // Make sure not to reject the original deferred until the fallback // project is loaded, so we don't violate expectations that there is always // a current project before continuing after _loadProject(). result.reject(); }); }); }); } }); } }); return result.promise(); } /** * @const * @private * @type {number} Minimum delay in milliseconds between calls to refreshFileTree */ var _refreshDelay = 1000; /** * Refresh the project's file tree, maintaining the current selection. * * Note that the original implementation of this returned a promise to be resolved when the refresh is complete. * That use is deprecated and `refreshFileTree` is now a "fire and forget" kind of function. */ var refreshFileTree = function refreshFileTree() { FileSystem.clearAllCaches(); return new $.Deferred().resolve().promise(); }; refreshFileTree = _.debounce(refreshFileTree, _refreshDelay); /** * Expands tree nodes to show the given file or folder and selects it. Silently no-ops if the * path lies outside the project, or if it doesn't exist. * * @param {!(File|Directory)} entry File or Directory to show * @return {$.Promise} Resolved when done; or rejected if not found */ function showInTree(entry) { return model.showInTree(entry).then(_saveTreeState); } /** * Open a new project. Currently, Brackets must always have a project open, so * this method handles both closing the current project and opening a new project. * * @param {string=} path Optional absolute path to the root folder of the project. * If path is undefined or null, displays a dialog where the user can choose a * folder to load. If the user cancels the dialog, nothing more happens. * @return {$.Promise} A promise object that will be resolved when the * project is loaded and tree is rendered, or rejected if the project path * fails to load. */ function openProject(path) { var result = new $.Deferred(); // Confirm any unsaved changes first. We run the command in "prompt-only" mode, meaning it won't // actually close any documents even on success; we'll do that manually after the user also oks // the folder-browse dialog. CommandManager.execute(Commands.FILE_CLOSE_ALL, { promptOnly: true }) .done(function () { if (path) { // use specified path _loadProject(path, false).then(result.resolve, result.reject); } else { // Pop up a folder browse dialog FileSystem.showOpenDialog(false, true, Strings.CHOOSE_FOLDER, model.projectRoot.fullPath, null, function (err, files) { if (!err) { // If length == 0, user canceled the dialog; length should never be > 1 if (files.length > 0) { // Load the new project into the folder tree _loadProject(files[0]).then(result.resolve, result.reject); } else { result.reject(); } } else { _showErrorDialog(ERR_TYPE_OPEN_DIALOG, null, err); result.reject(); } }); } }) .fail(function () { result.reject(); }); // if fail, don't open new project: user canceled (or we failed to save its unsaved changes) return result.promise(); } /** * Invoke project settings dialog. * @return {$.Promise} */ function _projectSettings() { return PreferencesDialogs.showProjectPreferencesDialog(getBaseUrl()).getPromise(); } /** * Create a new item in the current project. * * @param baseDir {string|Directory} Full path of the directory where the item should go. * Defaults to the project root if the entry is not valid or not within the project. * @param initialName {string} Initial name for the item * @param skipRename {boolean} If true, don't allow the user to rename the item * @param isFolder {boolean} If true, create a folder instead of a file * @return {$.Promise} A promise object that will be resolved with the File * of the created object, or rejected if the user cancelled or entered an illegal * filename. */ function createNewItem(baseDir, initialName, skipRename, isFolder) { baseDir = model.getDirectoryInProject(baseDir); if (skipRename) { if(isFolder) { return model.createAtPath(baseDir + initialName + "/"); } return model.createAtPath(baseDir + initialName); } return actionCreator.startCreating(baseDir, initialName, isFolder); } /** * Delete file or directore from project * @param {!(File|Directory)} entry File or Directory to delete */ function deleteItem(entry) { var result = new $.Deferred(); entry.moveToTrash(function (err) { if (!err) { DocumentManager.notifyPathDeleted(entry.fullPath); result.resolve(); } else { _showErrorDialog(ERR_TYPE_DELETE, entry.isDirectory, FileUtils.getFileErrorString(err), entry.fullPath); result.reject(err); } }); return result.promise(); } /** * Returns a filter for use with getAllFiles() that filters files based on LanguageManager language id * @param {!(string|Array.<string>)} languageId a single string of a language id or an array of language ids * @return {!function(File):boolean} */ function getLanguageFilter(languageId) { return function languageFilter(file) { var id = LanguageManager.getLanguageForPath(file.fullPath).getId(); if (typeof languageId === "string") { return (id === languageId); } else { return (languageId.indexOf(id) !== -1); } }; } /** * @private * * Respond to a FileSystem change event. Note that if renames are initiated * externally, they may be reported as a separate removal and addition. In * this case, the editor state isn't currently preserved. * * @param {$.Event} event * @param {?(File|Directory)} entry File or Directory changed * @param {Array.<FileSystemEntry>=} added If entry is a Directory, contains zero or more added children * @param {Array.<FileSystemEntry>=} removed If entry is a Directory, contains zero or more removed children */ _fileSystemChange = function (event, entry, added, removed) { FileSyncManager.syncOpenDocuments(); model.handleFSEvent(entry, added, removed); // @TODO: DocumentManager should implement its own fsChange handler // we can clean up the calls to DocumentManager.notifyPathDeleted // and privatize DocumentManager.notifyPathDeleted as well // We can also remove the _fileSystemRename handler below and move // it to DocumentManager if (removed) { removed.forEach(function (file) { // The call to syncOpenDocuemnts above will not nofify // document manager about deleted images that are // not in the working set -- try to clean that up here DocumentManager.notifyPathDeleted(file.fullPath); }); } }; /** * @private * Respond to a FileSystem rename event. */ _fileSystemRename = function (event, oldName, newName) { // Tell the document manager about the name change. This will update // all of the model information and send notification to all views DocumentManager.notifyPathNameChanged(oldName, newName); }; /** * Causes the rename operation that's in progress to complete. */ function forceFinishRename() { actionCreator.performRename(); } /** * @private * * Sets the width of the selection bar in the file tree. * * @param {int} width New width value */ function _setFileTreeSelectionWidth(width) { model.setSelectionWidth(width); _renderTreeSync(); } // Initialize variables and listeners that depend on the HTML DOM AppInit.htmlReady(function () { $projectTreeContainer = $("#project-files-container"); $projectTreeContainer.addClass("jstree jstree-brackets"); $projectTreeContainer.css("overflow", "auto"); $projectTreeContainer.css("position", "relative"); fileTreeViewContainer = $("<div>").appendTo($projectTreeContainer)[0]; model.setSelectionWidth($projectTreeContainer.width()); $(".main-view").click(function (jqEvent) { if (!jqEvent.target.classList.contains("jstree-rename-input")) { forceFinishRename(); actionCreator.setContext(null); } }); $("#working-set-list-container").on("contentChanged", function () { $projectTreeContainer.trigger("contentChanged"); }); Menus.getContextMenu(Menus.ContextMenuIds.PROJECT_MENU).on("beforeContextMenuOpen", function () { actionCreator.restoreContext(); }); Menus.getContextMenu(Menus.ContextMenuIds.PROJECT_MENU).on("beforeContextMenuClose", function () { model.setContext(null, false, true); }); $projectTreeContainer.on("contextmenu", function () { forceFinishRename(); }); $projectTreeContainer.on("dragover", function(e) { e.preventDefault(); }); // Add support for moving items to root directory $projectTreeContainer.on("drop", function(e) { var data = JSON.parse(e.originalEvent.dataTransfer.getData("text")); actionCreator.moveItem(data.path, getProjectRoot().fullPath); e.stopPropagation(); }); // When a context menu item is selected, we need to clear the context // because we don't get a beforeContextMenuClose event since Bootstrap // handles this directly. $("#project-context-menu").on("click.dropdown-menu", function () { model.setContext(null, true); }); $projectTreeContainer.on("scroll", function () { // Close open menus on scroll and clear the context, but only if there's a menu open. if ($(".dropdown.open").length > 0) { Menus.closeAll(); actionCreator.setContext(null); } // we need to render the tree without a delay to not cause selection extension issues (#10573) _renderTreeSync(); }); _renderTree(); ViewUtils.addScrollerShadow($projectTreeContainer[0]); }); EventDispatcher.makeEventDispatcher(exports); // Init default project path to welcome project PreferencesManager.stateManager.definePreference("projectPath", "string", _getWelcomeProjectPath()); exports.on("projectOpen", _reloadProjectPreferencesScope); exports.on("projectOpen", _saveProjectPath); exports.on("beforeAppClose", _unwatchProjectRoot); // Due to circular dependencies, not safe to call on() directly for other modules' events EventDispatcher.on_duringInit(FileViewController, "documentSelectionFocusChange", _documentSelectionFocusChange); EventDispatcher.on_duringInit(FileViewController, "fileViewFocusChange", _fileViewControllerChange); EventDispatcher.on_duringInit(MainViewManager, "currentFileChange", _currentFileChange); // Commands CommandManager.register(Strings.CMD_OPEN_FOLDER, Commands.FILE_OPEN_FOLDER, openProject); CommandManager.register(Strings.CMD_PROJECT_SETTINGS, Commands.FILE_PROJECT_SETTINGS, _projectSettings); CommandManager.register(Strings.CMD_FILE_REFRESH, Commands.FILE_REFRESH, refreshFileTree); // Define the preference to decide how to sort the Project Tree files PreferencesManager.definePreference(SORT_DIRECTORIES_FIRST, "boolean", brackets.platform !== "mac", { description: Strings.DESCRIPTION_SORT_DIRECTORIES_FIRST }) .on("change", function () { actionCreator.setSortDirectoriesFirst(PreferencesManager.get(SORT_DIRECTORIES_FIRST)); }); actionCreator.setSortDirectoriesFirst(PreferencesManager.get(SORT_DIRECTORIES_FIRST)); /** * Gets the filesystem object for the current context in the file tree. */ function getContext() { return model.getContext(); } /** * Starts a rename operation, completing the current operation if there is one. * * The Promise returned is resolved with an object with a `newPath` property with the renamed path. If the user cancels the operation, the promise is resolved with the value RENAME_CANCELLED. * * @param {FileSystemEntry} entry file or directory filesystem object to rename * @param {boolean=} isMoved optional flag which indicates whether the entry is being moved instead of renamed * @return {$.Promise} a promise resolved when the rename is done. */ renameItemInline = function (entry, isMoved) { var d = new $.Deferred(); model.startRename(entry, isMoved) .done(function () { d.resolve(); }) .fail(function (errorInfo) { // Need to do display the error message on the next event loop turn // because some errors can come up synchronously and then the dialog // is not displayed. window.setTimeout(function () { if (isMoved) { switch (errorInfo.type) { case FileSystemError.ALREADY_EXISTS: _showErrorDialog(ERR_TYPE_MOVE, errorInfo.isFolder, Strings.FILE_EXISTS_ERR, errorInfo.fullPath); break; case ProjectModel.ERROR_NOT_IN_PROJECT: _showErrorDialog(ERR_TYPE_MOVE, errorInfo.isFolder, Strings.ERROR_MOVING_NOT_IN_PROJECT, errorInfo.fullPath); break; default: _showErrorDialog(ERR_TYPE_MOVE, errorInfo.isFolder, FileUtils.getFileErrorString(errorInfo.type), errorInfo.fullPath); } } else { switch (errorInfo.type) { case ProjectModel.ERROR_INVALID_FILENAME: _showErrorDialog(ERR_TYPE_INVALID_FILENAME, errorInfo.isFolder, ProjectModel._invalidChars); break; case FileSystemError.ALREADY_EXISTS: _showErrorDialog(ERR_TYPE_RENAME, errorInfo.isFolder, Strings.FILE_EXISTS_ERR, errorInfo.fullPath); break; case ProjectModel.ERROR_NOT_IN_PROJECT: _showErrorDialog(ERR_TYPE_RENAME, errorInfo.isFolder, Strings.ERROR_RENAMING_NOT_IN_PROJECT, errorInfo.fullPath); break; default: _showErrorDialog(ERR_TYPE_RENAME, errorInfo.isFolder, FileUtils.getFileErrorString(errorInfo.type), errorInfo.fullPath); } } }, 10); d.reject(errorInfo); }); return d.promise(); }; /** * Returns an Array of all files for this project, optionally including * files in the working set that are *not* under the project root. Files are * filtered first by ProjectModel.shouldShow(), then by the custom filter * argument (if one was provided). * * @param {function (File, number):boolean=} filter Optional function to filter * the file list (does not filter directory traversal). API matches Array.filter(). * @param {boolean=} includeWorkingSet If true, include files in the working set * that are not under the project root (*except* for untitled documents). * @param {boolean=} sort If true, The files will be sorted by their paths * * @return {$.Promise} Promise that is resolved with an Array of File objects. */ function getAllFiles(filter, includeWorkingSet, sort) { var viewFiles, deferred; // The filter and includeWorkingSet params are both optional. // Handle the case where filter is omitted but includeWorkingSet is // specified. if (includeWorkingSet === undefined && typeof (filter) !== "function") { includeWorkingSet = filter; filter = null; } if (includeWorkingSet) { viewFiles = MainViewManager.getWorkingSet(MainViewManager.ALL_PANES); } deferred = new $.Deferred(); model.getAllFiles(filter, viewFiles, sort) .done(function (fileList) { deferred.resolve(fileList); }) .fail(function (err) { if (err === FileSystemError.TOO_MANY_ENTRIES && !_projectWarnedForTooManyFiles) { _showErrorDialog(ERR_TYPE_MAX_FILES); _projectWarnedForTooManyFiles = true; } // resolve with empty list deferred.resolve([]); }); return deferred.promise(); } /** * Adds an icon provider. The callback is invoked before each tree item is rendered, and can * return content to prepend to the item. * * @param {!function(!{name:string, fullPath:string, isFile:boolean}):?string|jQuery|DOMNode|Preact.DOM.ins} callback * * `name`: the file or directory name * * `fullPath`: full path to the file or directory * * `isFile`: true if it's a file, false if it's a directory * Return a string of HTML text, a Preact.DOM.ins instance, a jQuery object, or a DOM node; or undefined * to prepend nothing. */ function addIconProvider(callback) { return FileTreeView.addIconProvider(callback); } /** * Adds a CSS class provider, invoked before each tree item is rendered. * * @param {!function(!{name:string, fullPath:string, isFile:boolean}):?string} callback * * `name`: the file or directory name * * `fullPath`: full path to the file or directory * * `isFile`: true if it's a file, false if it's a directory * Return a string containing space-separated CSS class(es) to add, or undefined to leave CSS unchanged. */ function addClassesProvider(callback) { return FileTreeView.addClassesProvider(callback); } /** * Forces the file tree to rerender. Typically, the tree only rerenders the portions of the * tree that have changed data. If an extension that augments the tree has changes that it * needs to display, calling rerenderTree will cause the components for the whole tree to * be rerendered. */ function rerenderTree() { _renderTree(true); } // Private API helpful in testing exports._actionCreator = actionCreator; exports._RENDER_DEBOUNCE_TIME = _RENDER_DEBOUNCE_TIME; // Private API for use with SidebarView exports._setFileTreeSelectionWidth = _setFileTreeSelectionWidth; // Define public API exports.getProjectRoot = getProjectRoot; exports.getBaseUrl = getBaseUrl; exports.setBaseUrl = setBaseUrl; exports.isWithinProject = isWithinProject; exports.makeProjectRelativeIfPossible = makeProjectRelativeIfPossible; exports.shouldShow = ProjectModel.shouldShow; exports.openProject = openProject; exports.getFileTreeContext = getFileTreeContext; exports.getSelectedItem = getSelectedItem; exports.getContext = getContext; exports.getInitialProjectPath = getInitialProjectPath; exports.isWelcomeProjectPath = isWelcomeProjectPath; exports.updateWelcomeProjectPath = updateWelcomeProjectPath; exports.createNewItem = createNewItem; exports.renameItemInline = renameItemInline; exports.deleteItem = deleteItem; exports.forceFinishRename = forceFinishRename; exports.showInTree = showInTree; exports.refreshFileTree = refreshFileTree; exports.getAllFiles = getAllFiles; exports.getLanguageFilter = getLanguageFilter; exports.addIconProvider = addIconProvider; exports.addClassesProvider = addClassesProvider; exports.rerenderTree = rerenderTree; });