/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
release-1.14
src/view/MainViewManager.js
1 767 строк
64 KB
Swagatam Mitra
Enable opening of remote(http|https) files in Brackets (#14153)
04 май 2018, 11:43
04 май 2018, 11:43
421ed1a
Код
Авторство
О чём код?
/* * Copyright (c) 2014 - 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. * */ /** * MainViewManager manages the arrangement of all open panes as well as provides the controller * logic behind all views in the MainView (e.g. ensuring that a file doesn't appear in 2 lists) * * Each pane contains one or more views wich are created by a view factory and inserted into a pane list. * There may be several panes managed by the MainViewManager with each pane containing a list of views. * The panes are always visible and the layout is determined by the MainViewManager and the user. * * Currently we support only 2 panes. * * All of the WorkingSet APIs take a paneId Argument. This can be an actual pane Id, ALL_PANES (in most cases) * or ACTIVE_PANE. ALL_PANES may not be supported for some APIs. See the API for details. * * This module dispatches several events: * * - activePaneChange - When the active pane changes. There will always be an active pane. * (e, newPaneId:string, oldPaneId:string) * - currentFileChange -- When the user has switched to another pane, file, document. When the user closes a view * and there are no other views to show the current file will be null. * (e, newFile:File, newPaneId:string, oldFile:File, oldPaneId:string) * - paneLayoutChange -- When Orientation changes. * (e, orientation:string) * - paneCreate -- When a pane is created * (e, paneId:string) * - paneDestroy -- When a pane is destroyed * (e, paneId:string) * * * To listen for working set changes, you must listen to *all* of these events: * - workingSetAdd -- When a file is added to the working set * (e, fileAdded:File, index:number, paneId:string) * - workingSetAddList -- When multiple files are added to the working set * (e, fileAdded:Array.<File>, paneId:string) * - workingSetMove - When a File has moved to a different working set * (e, File:FILE, sourcePaneId:string, destinationPaneId:string) * - workingSetRemove -- When a file is removed from the working set * (e, fileRemoved:File, suppressRedraw:boolean, paneId:string) * - workingSetRemoveList -- When multiple files are removed from the working set * (e, filesRemoved:Array.<File>, paneId:string) * - workingSetSort -- When a pane's view array is reordered without additions or removals. * (e, paneId:string) * - workingSetUpdate -- When changes happen due to system events such as a file being deleted. * listeners should discard all working set info and rebuilt it from the pane * by calling getWorkingSet() * (e, paneId:string) * - _workingSetDisableAutoSort -- When the working set is reordered by manually dragging a file. * (e, paneId:string) For Internal Use Only. * * To listen for events, do something like this: (see EventDispatcher for details on this pattern) * `MainViewManager.on("eventname", handler);` */ define(function (require, exports, module) { "use strict"; var _ = require("thirdparty/lodash"), EventDispatcher = require("utils/EventDispatcher"), Strings = require("strings"), AppInit = require("utils/AppInit"), CommandManager = require("command/CommandManager"), MainViewFactory = require("view/MainViewFactory"), ViewStateManager = require("view/ViewStateManager"), Commands = require("command/Commands"), EditorManager = require("editor/EditorManager"), FileSystemError = require("filesystem/FileSystemError"), DocumentManager = require("document/DocumentManager"), PreferencesManager = require("preferences/PreferencesManager"), ProjectManager = require("project/ProjectManager"), WorkspaceManager = require("view/WorkspaceManager"), AsyncUtils = require("utils/Async"), ViewUtils = require("utils/ViewUtils"), Resizer = require("utils/Resizer"), Pane = require("view/Pane").Pane, KeyBindingManager = brackets.getModule("command/KeyBindingManager"); /** * Preference setting name for the MainView Saved State * @const * @private */ var PREFS_NAME = "mainView.state"; /** * Legacy Preference setting name used to migrate old preferences * @const * @private */ var OLD_PREFS_NAME = "project.files"; /** * Special paneId shortcut that can be used to specify that * all panes should be targeted by the API. * Not all APIs support this constnant. * Check the API documentation before use. * @const */ var ALL_PANES = "ALL_PANES"; /** * Special paneId shortcut that can be used to specify that * the API should target the focused pane only. * All APIs support this shortcut. * @const */ var ACTIVE_PANE = "ACTIVE_PANE"; /** * Internal pane id * @const * @private */ var FIRST_PANE = "first-pane"; /** * Internal pane id * @const * @private */ var SECOND_PANE = "second-pane"; /* * NOTE: The following commands and constants will change * when implementing the UX UI Treatment @larz0 */ /** * Vertical layout state name * @const * @private */ var VERTICAL = "VERTICAL"; /** * Horizontal layout state name * @const * @private */ var HORIZONTAL = "HORIZONTAL"; /** * The minimum width or height that a pane can be * @const * @private */ var MIN_PANE_SIZE = 75; /** * current orientation (null, VERTICAL or HORIZONTAL) * @type {string=} * @private */ var _orientation = null; /** * current pane id. May not be null * @type {!string} * @private */ var _activePaneId = null; /** * DOM element hosting the Main View. * @type {jQuery} * @private */ var _$el; /** * Maps paneId to Pane objects * @type {Object.<string, Pane>} * @private */ var _panes = {}; /** * map of pane scroll states * @type {Object.map<string, *>} * @private */ var _paneScrollStates = {}; /** * flag indicating if traversing is currently taking place * When True, changes the current pane's MRU list will not be updated. * Useful for next/previous keyboard navigation (until Ctrl is released) * or for incremental-search style document preview like Quick Open will eventually have. * @type {!boolean} * @private */ var _traversingFileList = false; /** * The global MRU list (for traversing) * @type {Array.<file:File, paneId:string>} */ var _mruList = []; /** * localized pane titles * @type {Object.<FIRST_PANE|SECOND_PANE, <VERTICAL.string, HORIZONTAL.string>}} * Localized string for first and second panes in the current orientation. * @see {@link #getPaneTitle} for more information * @private */ var _paneTitles = {}; /* * Initialize _paneTitles */ _paneTitles[FIRST_PANE] = {}; _paneTitles[SECOND_PANE] = {}; _paneTitles[FIRST_PANE][VERTICAL] = Strings.LEFT; _paneTitles[FIRST_PANE][HORIZONTAL] = Strings.TOP; _paneTitles[SECOND_PANE][VERTICAL] = Strings.RIGHT; _paneTitles[SECOND_PANE][HORIZONTAL] = Strings.BOTTOM; /** * Makes a MRU List Entry * @param {!File} File - the file * @param {!string} paneId - the paneId * @return {{file:File, paneId:string}} * @private */ function _makeMRUListEntry(file, paneId) { return {file: file, paneId: paneId}; } /** * Locates the first MRU entry of a file for the requested pane * @param {!string} paneId - the paneId * @param {!File} File - the file * @return {{file:File, paneId:string}} * @private */ function _findFileInMRUList(paneId, file) { return _.findIndex(_mruList, function (record) { return (record.file.fullPath === file.fullPath && record.paneId === paneId); }); } /** * Checks whether a file is listed exclusively in the provided pane * @param {!File} File - the file * @return {{file:File, paneId:string}} */ function isExclusiveToPane(file, paneId) { paneId = paneId === ACTIVE_PANE && _activePaneId ? _activePaneId : paneId; var index = _.findIndex(_mruList, function (record) { return (record.file.fullPath === file.fullPath && record.paneId !== paneId); }); return index === -1; } /** * Retrieves the currently active Pane Id * @return {!string} Active Pane's ID. */ function getActivePaneId() { return _activePaneId; } /** * Resolve paneId to actual pane. * @param {?string} paneId - id of the desired pane. May be symbolic or null (to indicate current pane) * @return {string} id of the pane in which to open the document */ function _resolvePaneId(paneId) { if (!paneId || paneId === ACTIVE_PANE) { return getActivePaneId(); } return paneId; } /** * Retrieves the Pane object for the given paneId * @param {!string} paneId - id of the pane to retrieve * @return {?Pane} the Pane object or null if a pane object doesn't exist for the pane * @private */ function _getPane(paneId) { paneId = _resolvePaneId(paneId); if (_panes[paneId]) { return _panes[paneId]; } return null; } /** * Focuses the current pane. If the current pane has a current view, then the pane will focus the view. */ function focusActivePane() { _getPane(ACTIVE_PANE).focus(); } /** * Determines if the pane id is a special pane id * @param {!string} paneId - the id to test * @return {boolean} true if the pane id is a special identifier, false if not */ function _isSpecialPaneId(paneId) { return paneId === ACTIVE_PANE || paneId === ALL_PANES; } /** * Makes the file the most recent for the pane and the global mru lists * @param {!string} paneId - id of the pane to mae th file most recent or ACTIVE_PANE * @param {!File} file - File object to make most recent * @private */ function _makeFileMostRecent(paneId, file) { var index, entry, pane = _getPane(paneId); if (!_traversingFileList) { pane.makeViewMostRecent(file); index = _findFileInMRUList(pane.id, file); entry = _makeMRUListEntry(file, pane.id); if (index !== -1) { _mruList.splice(index, 1); } if (_findFileInMRUList(pane.id, file) !== -1) { console.log(file.fullPath + " duplicated in mru list"); } // add it to the front of the list _mruList.unshift(entry); } } /** * Makes the Pane's current file the most recent * @param {!string} paneId - id of the pane to make the file most recent, or ACTIVE_PANE * @param {!File} file - File object to make most recent * @private */ function _makePaneMostRecent(paneId) { var pane = _getPane(paneId); if (pane.getCurrentlyViewedFile()) { _makeFileMostRecent(paneId, pane.getCurrentlyViewedFile()); } } /** * Switch active pane to the specified pane id (or ACTIVE_PANE/ALL_PANES, in which case this * call does nothing). * @param {!string} paneId - the id of the pane to activate */ function setActivePaneId(newPaneId) { if (!_isSpecialPaneId(newPaneId) && newPaneId !== _activePaneId) { var oldPaneId = _activePaneId, oldPane = _getPane(ACTIVE_PANE), newPane = _getPane(newPaneId); if (!newPane) { throw new Error("invalid pane id: " + newPaneId); } _activePaneId = newPaneId; exports.trigger("activePaneChange", newPaneId, oldPaneId); exports.trigger("currentFileChange", _getPane(ACTIVE_PANE).getCurrentlyViewedFile(), newPaneId, oldPane.getCurrentlyViewedFile(), oldPaneId); _makePaneMostRecent(_activePaneId); focusActivePane(); } } /** * Retrieves the Pane ID for the specified container * @param {!jQuery} $el - the element of the pane to fetch * @return {?string} the id of the pane that matches the container or undefined if a pane doesn't exist for that container */ function _getPaneFromElement($el) { return _.find(_panes, function (pane) { if (pane.$el[0] === $el[0]) { return pane; } }); } /** * Retrieves the currently viewed file of the specified paneId * @param {?string} paneId - the id of the pane in which to retrieve the currently viewed file * @return {?File} File object of the currently viewed file, or null if there isn't one or there's no such pane */ function getCurrentlyViewedFile(paneId) { var pane = _getPane(paneId); return pane ? pane.getCurrentlyViewedFile() : null; } /** * Retrieves the currently viewed path of the pane specified by paneId * @param {?string} paneId - the id of the pane in which to retrieve the currently viewed path * @return {?string} the path of the currently viewed file or null if there isn't one */ function getCurrentlyViewedPath(paneId) { var file = getCurrentlyViewedFile(paneId); return file ? file.fullPath : null; } /** * EditorManager.activeEditorChange handler * This event is triggered when an visible editor gains focus * Therefore we need to Activate the pane that the active editor belongs to * @private * @param {!jQuery.Event} e - jQuery Event object * @param {Editor=} current - editor being made the current editor */ function _activeEditorChange(e, current) { if (current) { var $container = current.$el.parent().parent(), pane = _getPaneFromElement($container); if (pane) { // Editor is a full editor if (pane.id !== _activePaneId) { // we just need to set the active pane in this case // it will dispatch the currentFileChange message as well // as dispatching other events when the active pane changes setActivePaneId(pane.id); } } else { // Editor is an inline editor, find the parent pane var parents = $container.parents(".view-pane"); if (parents.length === 1) { $container = $(parents[0]); pane = _getPaneFromElement($container); if (pane) { if (pane.id !== _activePaneId) { // activate the pane which will put focus in the pane's doc setActivePaneId(pane.id); // reset the focus to the inline editor current.focus(); } } } } } } /** * Iterates over the pane or ALL_PANES and calls the callback function for each. * @param {!string} paneId - id of the pane in which to adjust the scroll state, ALL_PANES or ACTIVE_PANE * @param {!function(!pane:Pane):boolean} callback - function to callback on to perform work. * The callback will receive a Pane and should return false to stop iterating. * @private */ function _forEachPaneOrPanes(paneId, callback) { if (paneId === ALL_PANES) { _.forEach(_panes, callback); } else { callback(_getPane(paneId)); } } /** * Caches the specified pane's current scroll state * If there was already cached state for the specified pane, it is discarded and overwritten * @param {!string} paneId - id of the pane in which to cache the scroll state, * ALL_PANES or ACTIVE_PANE */ function cacheScrollState(paneId) { _forEachPaneOrPanes(paneId, function (pane) { _paneScrollStates[pane.id] = pane.getScrollState(); }); } /** * Restores the scroll state from cache and applies the heightDelta * The view implementation is responsible for applying or ignoring the heightDelta. * This is used primarily when a modal bar opens to keep the editor from scrolling the current * page out of view in order to maintain the appearance. * The state is removed from the cache after calling this function. * @param {!string} paneId - id of the pane in which to adjust the scroll state, * ALL_PANES or ACTIVE_PANE * @param {!number} heightDelta - delta H to apply to the scroll state */ function restoreAdjustedScrollState(paneId, heightDelta) { _forEachPaneOrPanes(paneId, function (pane) { pane.restoreAndAdjustScrollState(_paneScrollStates[pane.id], heightDelta); delete _paneScrollStates[pane.id]; }); } /** * Retrieves the WorkingSet for the given paneId not including temporary views * @param {!string} paneId - id of the pane in which to get the view list, ALL_PANES or ACTIVE_PANE * @return {Array.<File>} */ function getWorkingSet(paneId) { var result = []; _forEachPaneOrPanes(paneId, function (pane) { var viewList = pane.getViewList(); result = _.union(result, viewList); }); return result; } /** * Retrieves the list of all open files including temporary views * @return {array.<File>} the list of all open files in all open panes */ function getAllOpenFiles() { var result = getWorkingSet(ALL_PANES); _.forEach(_panes, function (pane) { var file = pane.getCurrentlyViewedFile(); if (file) { result = _.union(result, [file]); } }); return result; } /** * Retrieves the list of all open pane ids * @return {array.<string>} the list of all open panes */ function getPaneIdList() { return Object.keys(_panes); } /** * Retrieves the size of the selected pane's view list * @param {!string} paneId - id of the pane in which to get the workingset size. * Can use `ALL_PANES` or `ACTIVE_PANE` * @return {!number} the number of items in the specified pane */ function getWorkingSetSize(paneId) { var result = 0; _forEachPaneOrPanes(paneId, function (pane) { result += pane.getViewListSize(); }); return result; } /** * Retrieves the title to display in the workingset view * @param {!string} paneId - id of the pane in which to get the title * @return {?string} title */ function getPaneTitle(paneId) { return _paneTitles[paneId][_orientation]; } /** * Retrieves the number of panes * @return {number} */ function getPaneCount() { return Object.keys(_panes).length; } /** * Helper to abastract the common working set search functions * @param {!string} paneId - id of the pane to search or ALL_PANES to search all panes * @param {!string} fullPath - path of the file to locate * @param {!string} method - name of the method to use for searching * "findInViewList", "findInViewListAddedOrder" or "FindInViewListMRUOrder" * * @private */ function _doFindInWorkingSet(paneId, fullPath, method) { var result = -1; _forEachPaneOrPanes(paneId, function (pane) { var index = pane[method].call(pane, fullPath); if (index >= 0) { result = index; return false; } }); return result; } /** * Finds all instances of the specified file in all working sets. * If there is a temporary view of the file, it is not part of the result set * @param {!string} fullPath - path of the file to find views of * @return {Array.<{pane:string, index:number}>} an array of paneId/index records */ function findInAllWorkingSets(fullPath) { var index, result = []; _.forEach(_panes, function (pane) { index = pane.findInViewList(fullPath); if (index >= 0) { result.push({paneId: pane.id, index: index}); } }); return result; } /** * Gets the index of the file matching fullPath in the workingset * @param {!string} paneId - id of the pane in which to search or ALL_PANES or ACTIVE_PANE * @param {!string} fullPath - full path of the file to search for * @return {number} index, -1 if not found. */ function findInWorkingSet(paneId, fullPath) { return _doFindInWorkingSet(paneId, fullPath, "findInViewList"); } /** * Gets the index of the file matching fullPath in the added order workingset * @param {!string} paneId - id of the pane in which to search or ALL_PANES or ACTIVE_PANE * @param {!string} fullPath - full path of the file to search for * @return {number} index, -1 if not found. */ function findInWorkingSetByAddedOrder(paneId, fullPath) { return _doFindInWorkingSet(paneId, fullPath, "findInViewListAddedOrder"); } /** * Gets the index of the file matching fullPath in the MRU order workingset * @param {!string} paneId - id of the pane in which to search or ALL_PANES or ACTIVE_PANE * @param {!string} fullPath - full path of the file to search for * @return {number} index, -1 if not found. */ function findInWorkingSetByMRUOrder(paneId, fullPath) { return _doFindInWorkingSet(paneId, fullPath, "findInViewListMRUOrder"); } /** * @private * Retrieves pane id where the specified file has been opened. Used to ensure that a file * is open in only one pane so this will change once support for multiple views is added * The result includes panes with a temporary view of the file not just working set instances * @param {!string} fullPath - full path of the file to search for * @return {?string} pane id where the file has been opened or null if it wasn't found */ function _getPaneIdForPath(fullPath) { // Search all working sets and pull off the first one var info = findInAllWorkingSets(fullPath).shift(); // Look for a view that has not been added to a working set if (!info) { _.forEach(_panes, function (pane) { if (pane.getCurrentlyViewedPath() === fullPath) { info = {paneId: pane.id}; return false; } }); } if (!info) { return null; } return info.paneId; } /** * Adds the given file to the end of the workingset, if it is not already there. * This API does not create a view of the file, it just adds it to the working set * Views of files in the working set are persisted and are not destroyed until the user * closes the file using FILE_CLOSE; Views are created using FILE_OPEN and, when opened, are * made the current view. If a File is already opened then the file is just made current * and its view is shown. * @param {!string} paneId - The id of the pane in which to add the file object to or ACTIVE_PANE * @param {!File} file - The File object to add to the workingset * @param {number=} index - Position to add to list (defaults to last); -1 is ignored * @param {boolean=} forceRedraw - If true, a workingset change notification is always sent * (useful if suppressRedraw was used with removeView() earlier) */ function addToWorkingSet(paneId, file, index, force) { // look for the file to have already been added to another pane var pane = _getPane(paneId); if (!pane) { throw new Error("invalid pane id: " + paneId); } var result = pane.reorderItem(file, index, force), entry = _makeMRUListEntry(file, pane.id); // handles the case of save as so that the file remains in the // the same location in the working set as the file that was renamed if (result === pane.ITEM_FOUND_NEEDS_SORT) { console.warn("pane.reorderItem returned pane.ITEM_FOUND_NEEDS_SORT which shouldn't happen " + file); exports.trigger("workingSetSort", pane.id); } else if (result === pane.ITEM_NOT_FOUND) { index = pane.addToViewList(file, index); if (_findFileInMRUList(pane.id, file) === -1) { // Add to or update the position in MRU if (pane.getCurrentlyViewedFile() === file) { _mruList.unshift(entry); } else { _mruList.push(entry); } } exports.trigger("workingSetAdd", file, index, pane.id); } } /** * Adds the given file list to the end of the workingset. * @param {!string} paneId - The id of the pane in which to add the file object to or ACTIVE_PANE * @param {!Array.<File>} fileList - Array of files to add to the pane */ function addListToWorkingSet(paneId, fileList) { var uniqueFileList, pane = _getPane(paneId); uniqueFileList = pane.addListToViewList(fileList); uniqueFileList.forEach(function (file) { if (_findFileInMRUList(pane.id, file) !== -1) { console.log(file.fullPath + " duplicated in mru list"); } _mruList.push(_makeMRUListEntry(file, pane.id)); }); exports.trigger("workingSetAddList", uniqueFileList, pane.id); // find all of the files that could be added but were not var unsolvedList = fileList.filter(function (item) { // if the file open in another pane, then add it to the list of unsolvedList return (pane.findInViewList(item.fullPath) === -1 && _getPaneIdForPath(item.fullPath)); }); // Use the pane id of the first one in the list for pane id and recurse // if we add more panes, then this will recurse until all items in the list are satisified if (unsolvedList.length) { addListToWorkingSet(_getPaneIdForPath(unsolvedList[0].fullPath), unsolvedList); } } /** * Removes a file from the global MRU list. Future versions of this * implementation may support the ALL_PANES constant but FOCUS_PANE is not allowed * @param {!string} paneId - Must be a valid paneId (not a shortcut e.g. ALL_PANES) @ @param {File} file The file object to remove. * @private */ function _removeFileFromMRU(paneId, file) { var index, compare = function (record) { return (record.file === file && record.paneId === paneId); }; // find and remove all instances do { index = _.findIndex(_mruList, compare); if (index !== -1) { _mruList.splice(index, 1); } } while (index !== -1); } /** * Removes a file the specified pane * @param {!string} paneId - Must be a valid paneId (not a shortcut e.g. ALL_PANES) * @param {!File} file - the File to remove * @param {boolean=} suppressRedraw - true to tell listeners not to redraw * Use the suppressRedraw flag when calling this function along with many changes to prevent flicker * @private */ function _removeView(paneId, file, suppressRedraw) { var pane = _getPane(paneId); if (pane.removeView(file)) { _removeFileFromMRU(pane.id, file); exports.trigger("workingSetRemove", file, suppressRedraw, pane.id); } } /** * moves a view from one pane to another * @param {!string} sourcePaneId - id of the source pane * @param {!string} destinationPaneId - id of the destination pane * @param {!File} file - the File to move * @param {Number} destinationIndex - the working set index of the file in the destination pane * @return {jQuery.Promise} a promise that resolves when the move has completed. * @private */ function _moveView(sourcePaneId, destinationPaneId, file, destinationIndex) { var result = new $.Deferred(), sourcePane = _getPane(sourcePaneId), destinationPane = _getPane(destinationPaneId); sourcePane.moveView(file, destinationPane, destinationIndex) .done(function () { // remove existing entry from mrulist for the same document if present _removeFileFromMRU(destinationPane.id, file); // update the mru list _mruList.every(function (record) { if (record.file === file && record.paneId === sourcePane.id) { record.paneId = destinationPane.id; return false; } return true; }); exports.trigger("workingSetMove", file, sourcePane.id, destinationPane.id); result.resolve(); }); return result.promise(); } /** * Switch between panes */ function switchPaneFocus() { var $firstPane = $('#first-pane'), $secondPane = $('#second-pane'); if($firstPane.hasClass('active-pane')) { $secondPane.click(); } else { $firstPane.click(); } } /** * DocumentManager.pathDeleted Event handler to remove a file * from the MRU list * @param {!jQuery.event} e - * @param {!string} fullPath - path of the file to remove * @private */ function _removeDeletedFileFromMRU(e, fullPath) { var index, compare = function (record) { return (record.file.fullPath === fullPath); }; // find and remove all instances do { index = _.findIndex(_mruList, compare); if (index !== -1) { _mruList.splice(index, 1); } } while (index !== -1); } /** * sorts the pane's view list * @param {!string} paneId - id of the pane to sort, ALL_PANES or ACTIVE_PANE * @param {sortFunctionCallback} compareFn - callback to determine sort order (called on each item) * @see {@link Pane#sortViewList} for more information * @see {@link https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort|Sort Array - MDN} * @private */ function _sortWorkingSet(paneId, compareFn) { _forEachPaneOrPanes(paneId, function (pane) { pane.sortViewList(compareFn); exports.trigger("workingSetSort", pane.id); }); } /** * moves a working set item from one index to another shifting the items * after in the working set up and reinserting it at the desired location * @param {!string} paneId - id of the pane to sort * @param {!number} fromIndex - the index of the item to move * @param {!number} toIndex - the index to move to * @private */ function _moveWorkingSetItem(paneId, fromIndex, toIndex) { var pane = _getPane(paneId); pane.moveWorkingSetItem(fromIndex, toIndex); exports.trigger("workingSetSort", pane.id); exports.trigger("_workingSetDisableAutoSort", pane.id); } /** * Mutually exchanges the files at the indexes passed by parameters. * @param {!string} paneId - id of the pane to swap indices or ACTIVE_PANE * @param {!number} index1 - the index on the left * @param {!number} index2 - the index on the rigth * @private */ function _swapWorkingSetListIndexes(paneId, index1, index2) { var pane = _getPane(paneId); pane.swapViewListIndexes(index1, index2); exports.trigger("workingSetSort", pane.id); exports.trigger("_workingSetDisableAutoSort", pane.id); } /** * Get the next or previous file in the MRU list. * @param {!number} direction - Must be 1 or -1 to traverse forward or backward * @return {?{file:File, paneId:string}} The File object of the next item in the traversal order or null if there aren't any files to traverse. * May return current file if there are no other files to traverse. */ function traverseToNextViewByMRU(direction) { var file = getCurrentlyViewedFile(), paneId = getActivePaneId(), index = _.findIndex(_mruList, function (record) { return (record.file === file && record.paneId === paneId); }); return ViewUtils.traverseViewArray(_mruList, index, direction); } /** * Get the next or previous file in list order. * @param {!number} direction - Must be 1 or -1 to traverse forward or backward * @return {?{file:File, paneId:string}} The File object of the next item in the traversal order or null if there aren't any files to traverse. * May return current file if there are no other files to traverse. */ function traverseToNextViewInListOrder(direction) { var file = getCurrentlyViewedFile(), curPaneId = getActivePaneId(), allFiles = [], index; getPaneIdList().forEach(function (paneId) { var paneFiles = getWorkingSet(paneId).map(function (file) { return { file: file, pane: paneId }; }); allFiles = allFiles.concat(paneFiles); }); index = _.findIndex(allFiles, function (record) { return (record.file === file && record.pane === curPaneId); }); return ViewUtils.traverseViewArray(allFiles, index, direction); } /** * Indicates that traversal has begun. * Can be called any number of times. */ function beginTraversal() { _traversingFileList = true; } /** * Un-freezes the MRU list after one or more beginTraversal() calls. * Whatever file is current is bumped to the front of the MRU list. */ function endTraversal() { var pane = _getPane(ACTIVE_PANE); if (_traversingFileList) { _traversingFileList = false; _makeFileMostRecent(pane.id, pane.getCurrentlyViewedFile()); } } /** * Synchronizes the pane's sizer element, updates the pane's resizer maxsize value * and tells the pane to update its layout * @param {boolean} forceRefresh - true to force a resize and refresh of the entire view * @private */ function _synchronizePaneSize(pane, forceRefresh) { var available; if (_orientation === VERTICAL) { available = _$el.innerWidth(); } else { available = _$el.innerHeight(); } // Update the pane's sizer element if it has one and update the max size Resizer.resyncSizer(pane.$el); pane.$el.data("maxsize", available - MIN_PANE_SIZE); pane.updateLayout(forceRefresh); } /** * Event handler for "workspaceUpdateLayout" to update the layout * @param {jQuery.Event} event - jQuery event object * @param {number} viewAreaHeight - unused * @param {boolean} forceRefresh - true to force a resize and refresh of the entire view * @private */ function _updateLayout(event, viewAreaHeight, forceRefresh) { var available; if (_orientation === VERTICAL) { available = _$el.innerWidth(); } else { available = _$el.innerHeight(); } _.forEach(_panes, function (pane) { // For VERTICAL orientation, we set the second pane to be width: auto // so that it resizes to fill the available space in the containing div // unfortunately, that doesn't work in the HORIZONTAL orientation so we // must update the height and convert it into a percentage if (pane.id === SECOND_PANE && _orientation === HORIZONTAL) { var percentage = ((_panes[FIRST_PANE].$el.height() + 1) / available); pane.$el.css("height", 100 - (percentage * 100) + "%"); } _synchronizePaneSize(pane, forceRefresh); }); } /** * Sets up the initial layout so panes are evenly distributed * This also sets css properties that aid in the layout when _updateLayout is called * @param {boolean} forceRefresh - true to force a resize and refresh of the entire view * @private */ function _initialLayout(forceRefresh) { var panes = Object.keys(_panes), size = 100 / panes.length; _.forEach(_panes, function (pane) { if (pane.id === FIRST_PANE) { if (_orientation === VERTICAL) { pane.$el.css({height: "100%", width: size + "%", float: "left" }); } else { pane.$el.css({ height: size + "%", width: "100%" }); } } else { if (_orientation === VERTICAL) { pane.$el.css({ height: "100%", width: "auto", float: "none" }); } else { pane.$el.css({ width: "100%", height: "50%" }); } } _synchronizePaneSize(pane, forceRefresh); }); } /** * Updates the header text for all panes */ function _updatePaneHeaders() { _forEachPaneOrPanes(ALL_PANES, function (pane) { pane.updateHeaderText(); }); } /** * Creates a pane for paneId if one doesn't already exist * @param {!string} paneId - id of the pane to create * @private * @return {?Pane} - the pane object of the new pane, or undefined if no pane created */ function _createPaneIfNecessary(paneId) { var newPane; if (!_panes.hasOwnProperty(paneId)) { newPane = new Pane(paneId, _$el); _panes[paneId] = newPane; exports.trigger("paneCreate", newPane.id); newPane.$el.on("click.mainview dragover.mainview", function () { setActivePaneId(newPane.id); }); newPane.on("viewListChange.mainview", function () { _updatePaneHeaders(); exports.trigger("workingSetUpdate", newPane.id); }); newPane.on("currentViewChange.mainview", function (e, newView, oldView) { _updatePaneHeaders(); if (_activePaneId === newPane.id) { exports.trigger("currentFileChange", newView && newView.getFile(), newPane.id, oldView && oldView.getFile(), newPane.id); } }); newPane.on("viewDestroy.mainView", function (e, view) { _removeFileFromMRU(newPane.id, view.getFile()); }); } return newPane; } /** * Makes the first pane resizable * @private */ function _makeFirstPaneResizable() { var firstPane = _panes[FIRST_PANE]; Resizer.makeResizable(firstPane.$el, _orientation === HORIZONTAL ? Resizer.DIRECTION_VERTICAL : Resizer.DIRECTION_HORIZONTAL, _orientation === HORIZONTAL ? Resizer.POSITION_BOTTOM : Resizer.POSITION_RIGHT, MIN_PANE_SIZE, false, false, false, true, true); firstPane.$el.on("panelResizeUpdate", function () { _updateLayout(); }); } /** * Creates a split for the specified orientation * @private * @param {!string} orientation (VERTICAL|HORIZONTAL) */ function _doSplit(orientation) { var firstPane, newPane; if (orientation === _orientation) { return; } firstPane = _panes[FIRST_PANE]; Resizer.removeSizable(firstPane.$el); if (_orientation) { _$el.removeClass("split-" + _orientation.toLowerCase()); } _$el.addClass("split-" + orientation.toLowerCase()); _orientation = orientation; newPane = _createPaneIfNecessary(SECOND_PANE); _makeFirstPaneResizable(); // reset the layout to 50/50 split // if we changed orientation then // the percentages are reset as well _initialLayout(); exports.trigger("paneLayoutChange", _orientation); // if new pane was created, and original pane is not empty, make new pane the active pane if (newPane && getCurrentlyViewedFile(firstPane.id)) { setActivePaneId(newPane.id); } } /** * Edits a document in the specified pane. * This function is only used by: * - Unit Tests (which construct Mock Document objects), * - by File > New because there is yet to be an established File object * - by Find In Files which needs to open documents synchronously in some cases * Do not use this API it is for internal use only * @param {!string} paneId - id of the pane in which to open the document * @param {!Document} doc - document to edit * @param {{noPaneActivate:boolean=}=} optionsIn - options * @private */ function _edit(paneId, doc, optionsIn) { var options = optionsIn || {}; var pane = _getPane(paneId); // If file is untitled or otherwise not within project tree, add it to // working set right now (don't wait for it to become dirty) if (doc.isUntitled() || !ProjectManager.isWithinProject(doc.file.fullPath)) { addToWorkingSet(paneId, doc.file); } // open document will show the editor if there is one already EditorManager.openDocument(doc, pane, options); _makeFileMostRecent(paneId, doc.file); if (!options.noPaneActivate) { setActivePaneId(paneId); } } /** * Opens a file in the specified pane this can be used to open a file with a custom viewer * or a document for editing. If it's a document for editing, edit is called on the document * @param {!string} paneId - id of the pane in which to open the document * @param {!File} file - file to open * @param {{noPaneActivate:boolean=}=} optionsIn - options * @return {jQuery.Promise} promise that resolves to a File object or * rejects with a File error or string */ function _open(paneId, file, optionsIn) { var result = new $.Deferred(), options = optionsIn || {}; function doPostOpenActivation() { if (!options.noPaneActivate) { setActivePaneId(paneId); } } if (!file || !_getPane(paneId)) { return result.reject("bad argument").promise(); } // See if there is already a view for the file var pane = _getPane(paneId); // See if there is a factory to create a view for this file // we want to do this first because, we don't want our internal // editor to edit files for which there are suitable viewfactories var factory = MainViewFactory.findSuitableFactoryForPath(file.fullPath); if (factory) { file.exists(function (fileError, fileExists) { if (fileExists) { // let the factory open the file and create a view for it factory.openFile(file, pane) .done(function () { // if we opened a file that isn't in the project // then add the file to the working set if (!ProjectManager.isWithinProject(file.fullPath)) { addToWorkingSet(paneId, file); } doPostOpenActivation(); result.resolve(file); }) .fail(function (fileError) { result.reject(fileError); }); } else { result.reject(fileError || FileSystemError.NOT_FOUND); } }); } else { DocumentManager.getDocumentForPath(file.fullPath, file) .done(function (doc) { if (doc) { _edit(paneId, doc, $.extend({}, options, { noPaneActivate: true })); doPostOpenActivation(); result.resolve(doc.file); } else { result.resolve(null); } }) .fail(function (fileError) { result.reject(fileError); }); } result.done(function () { _makeFileMostRecent(paneId, file); }); return result; } /** * Merges second pane into first pane and opens the current file * @private */ function _mergePanes() { if (_panes.hasOwnProperty(SECOND_PANE)) { var firstPane = _panes[FIRST_PANE], secondPane = _panes[SECOND_PANE], fileList = secondPane.getViewList(), lastViewed = getCurrentlyViewedFile(); Resizer.removeSizable(firstPane.$el); firstPane.mergeFrom(secondPane); exports.trigger("workingSetRemoveList", fileList, secondPane.id); setActivePaneId(firstPane.id); secondPane.$el.off(".mainview"); secondPane.off(".mainview"); secondPane.destroy(); delete _panes[SECOND_PANE]; exports.trigger("paneDestroy", secondPane.id); exports.trigger("workingSetAddList", fileList, firstPane.id); _mruList.forEach(function (record) { if (record.paneId === secondPane.id) { record.paneId = firstPane.id; } }); _$el.removeClass("split-" + _orientation.toLowerCase()); _orientation = null; // this will set the remaining pane to 100% _initialLayout(); exports.trigger("paneLayoutChange", _orientation); // if the current view before the merger was in the pane // that went away then reopen it so that it's now the current view again if (lastViewed && getCurrentlyViewedFile() !== lastViewed) { exports._open(firstPane.id, lastViewed); } } } /** * Closes a file in the specified pane or panes * @param {!string} paneId - id of the pane in which to open the document * @param {!File} file - file to close * @param {Object={noOpenNextFile:boolean}} optionsIn - options * This function does not fail if the file is not open */ function _close(paneId, file, optionsIn) { var options = optionsIn || {}; _forEachPaneOrPanes(paneId, function (pane) { if (pane.removeView(file, options.noOpenNextFile) && (paneId === ACTIVE_PANE || pane.id === paneId)) { _removeFileFromMRU(pane.id, file); exports.trigger("workingSetRemove", file, false, pane.id); return false; } }); } /** * Closes a list of file in the specified pane or panes * @param {!string} paneId - id of the pane in which to open the document * @param {!Array.<File>} fileList - files to close * This function does not fail if the file is not open */ function _closeList(paneId, fileList) { _forEachPaneOrPanes(paneId, function (pane) { var closedList = pane.removeViews(fileList); closedList.forEach(function (file) { _removeFileFromMRU(pane.id, file); }); exports.trigger("workingSetRemoveList", closedList, pane.id); }); } /** * Closes all files in the specified pane or panes * @param {!string} paneId - id of the pane in which to open the document * This function does not fail if the file is not open */ function _closeAll(paneId) { _forEachPaneOrPanes(paneId, function (pane) { var closedList = pane.getViewList(); closedList.forEach(function (file) { _removeFileFromMRU(pane.id, file); }); pane._reset(); exports.trigger("workingSetRemoveList", closedList, pane.id); }); } /** * Finds which pane a document belongs to * @param {!Document} document - the document to locate * @return {?Pane} the pane where the document lives or NULL if it isn't in a pane * @private */ function _findPaneForDocument(document) { // First check for an editor view of the document var pane = _getPaneFromElement($(document._masterEditor.$el.parent().parent())); if (!pane) { // No view of the document, it may be in a working set and not yet opened var info = findInAllWorkingSets(document.file.fullPath).shift(); if (info) { pane = _panes[info.paneId]; } } return pane; } /** * Destroys an editor object if a document is no longer referenced * @param {!Document} doc - document to destroy */ function _destroyEditorIfNotNeeded(document) { if (!(document instanceof DocumentManager.Document)) { throw new Error("_destroyEditorIfUnneeded() should be passed a Document"); } if (document._masterEditor) { // findPaneForDocument tries to locate the pane in which the document // is either opened or will be opened (in the event that the document is // in a working set but has yet to be opened) and then asks the pane // to destroy the view if it doesn't need it anymore var pane = _findPaneForDocument(document); if (pane) { // let the pane deceide if it wants to destroy the view if it's no needed pane.destroyViewIfNotNeeded(document._masterEditor); } else { // in this case, the document isn't referenced at all so just destroy it document._masterEditor.destroy(); } } } /** * Loads the workingset state * @private */ function _loadViewState(e) { // file root is appended for each project var panes, promises = [], context = { location : { scope: "user", layer: "project" } }, state = PreferencesManager.getViewState(PREFS_NAME, context); function convertViewState() { var context = { location : { scope: "user", layer: "project" } }, files = PreferencesManager.getViewState(OLD_PREFS_NAME, context); if (!files) { // nothing to convert return; } var result = { orientation: null, activePaneId: FIRST_PANE, panes: { "first-pane": [] } }; // Add all files to the workingset without verifying that // they still exist on disk (for faster project switching) files.forEach(function (value) { result.panes[FIRST_PANE].push(value); }); return result; } if (!state) { // not converted yet state = convertViewState(); } // reset _mergePanes(); _mruList = []; ViewStateManager.reset(); if (state) { panes = Object.keys(state.panes); _orientation = (panes.length > 1) ? state.orientation : null; _.forEach(state.panes, function (paneState, paneId) { _createPaneIfNecessary(paneId); promises.push(_panes[paneId].loadState(paneState)); }); AsyncUtils.waitForAll(promises).then(function (opensList) { // this will set the default layout of 50/50 or 100 // based on the number of panes _initialLayout(); // More than 1 pane, then make it resizable // and layout the panes from serialized state if (panes.length > 1) { _makeFirstPaneResizable(); // If the split state was serialized correctly // then setup the splits according to was serialized // Avoid a zero and negative split percentages if ($.isNumeric(state.splitPercentage) && state.splitPercentage > 0) { var prop; if (_orientation === VERTICAL) { prop = "width"; } else { prop = "height"; } _panes[FIRST_PANE].$el.css(prop, state.splitPercentage * 100 + "%"); _updateLayout(); } } if (_orientation) { _$el.addClass("split-" + _orientation.toLowerCase()); exports.trigger("paneLayoutChange", _orientation); } _.forEach(_panes, function (pane) { var fileList = pane.getViewList(); fileList.forEach(function (file) { if (_findFileInMRUList(pane.id, file) !== -1) { console.log(file.fullPath + " duplicated in mru list"); } _mruList.push(_makeMRUListEntry(file, pane.id)); }); exports.trigger("workingSetAddList", fileList, pane.id); }); promises = []; opensList.forEach(function (openData) { if (openData) { promises.push(CommandManager.execute(Commands.FILE_OPEN, openData)); } }); // finally set the active pane AsyncUtils.waitForAll(promises).then(function () { setActivePaneId(state.activePaneId); }); }); } } /** * Saves the workingset state * @private */ function _saveViewState() { function _computeSplitPercentage() { var available, used; if (getPaneCount() === 1) { // just short-circuit here and // return 100% to avoid any rounding issues return 1; } else { if (_orientation === VERTICAL) { available = _$el.innerWidth(); used = _panes[FIRST_PANE].$el.width(); } else { available = _$el.innerHeight(); used = _panes[FIRST_PANE].$el.height(); } return used / available; } } var projectRoot = ProjectManager.getProjectRoot(), context = { location : { scope: "user", layer: "project", layerID: projectRoot.fullPath } }, state = { orientation: _orientation, activePaneId: getActivePaneId(), splitPercentage: _computeSplitPercentage(), panes: { } }; if (!projectRoot) { return; } _.forEach(_panes, function (pane) { state.panes[pane.id] = pane.saveState(); }); PreferencesManager.setViewState(PREFS_NAME, state, context); } /** * Initializes the MainViewManager's view state * @param {jQuery} $container - the container where the main view will live * @private */ function _initialize($container) { if (_activePaneId) { throw new Error("MainViewManager has already been initialized"); } _$el = $container; _createPaneIfNecessary(FIRST_PANE); _activePaneId = FIRST_PANE; // One-time init so the pane has the "active" appearance _panes[FIRST_PANE]._handleActivePaneChange(undefined, _activePaneId); _initialLayout(); // This ensures that unit tests that use this function // get an event handler for workspace events and we don't listen // to the event before we've been initialized WorkspaceManager.on("workspaceUpdateLayout", _updateLayout); // Listen to key Alt-W to toggle between panes CommandManager.register(Strings.CMD_SWITCH_PANE_FOCUS, Commands.CMD_SWITCH_PANE_FOCUS, switchPaneFocus); KeyBindingManager.addBinding(Commands.CMD_SWITCH_PANE_FOCUS, {key: 'Alt-W'}); } /** * Changes the layout scheme * @param {!number} rows (may be 1 or 2) * @param {!number} columns (may be 1 or 2) * @summay Rows or Columns may be 1 or 2 but both cannot be 2. 1x2, 2x1 or 1x1 are the legal values */ function setLayoutScheme(rows, columns) { if ((rows < 1) || (rows > 2) || (columns < 1) || (columns > 2) || (columns === 2 && rows === 2)) { console.error("setLayoutScheme unsupported layout " + rows + ", " + columns); return false; } if (rows === columns) { _mergePanes(); } else if (rows > columns) { _doSplit(HORIZONTAL); } else { _doSplit(VERTICAL); } return true; } /** * Retrieves the current layout scheme * @return {!{rows: number, columns: number>}} */ function getLayoutScheme() { var result = { rows: 1, columns: 1 }; if (_orientation === HORIZONTAL) { result.rows = 2; } else if (_orientation === VERTICAL) { result.columns = 2; } return result; } /** * Setup a ready event to initialize ourself */ AppInit.htmlReady(function () { _initialize($("#editor-holder")); }); // Event handlers - not safe to call on() directly, due to circular dependencies EventDispatcher.on_duringInit(ProjectManager, "projectOpen", _loadViewState); EventDispatcher.on_duringInit(ProjectManager, "beforeProjectClose beforeAppClose", _saveViewState); EventDispatcher.on_duringInit(EditorManager, "activeEditorChange", _activeEditorChange); EventDispatcher.on_duringInit(DocumentManager, "pathDeleted", _removeDeletedFileFromMRU); EventDispatcher.makeEventDispatcher(exports); // Unit Test Helpers exports._initialize = _initialize; exports._getPane = _getPane; // Private Helpers exports._removeView = _removeView; exports._moveView = _moveView; // Private API exports._sortWorkingSet = _sortWorkingSet; exports._moveWorkingSetItem = _moveWorkingSetItem; exports._swapWorkingSetListIndexes = _swapWorkingSetListIndexes; exports._destroyEditorIfNotNeeded = _destroyEditorIfNotNeeded; exports._edit = _edit; exports._open = _open; exports._close = _close; exports._closeAll = _closeAll; exports._closeList = _closeList; exports._getPaneIdForPath = _getPaneIdForPath; // WorkingSet Management exports.addToWorkingSet = addToWorkingSet; exports.addListToWorkingSet = addListToWorkingSet; exports.getWorkingSetSize = getWorkingSetSize; exports.getWorkingSet = getWorkingSet; // Pane state exports.cacheScrollState = cacheScrollState; exports.restoreAdjustedScrollState = restoreAdjustedScrollState; // Searching exports.findInWorkingSet = findInWorkingSet; exports.findInWorkingSetByAddedOrder = findInWorkingSetByAddedOrder; exports.findInWorkingSetByMRUOrder = findInWorkingSetByMRUOrder; exports.findInAllWorkingSets = findInAllWorkingSets; exports.findInGlobalMRUList = _findFileInMRUList; // Traversal exports.beginTraversal = beginTraversal; exports.endTraversal = endTraversal; exports.traverseToNextViewByMRU = traverseToNextViewByMRU; exports.traverseToNextViewInListOrder = traverseToNextViewInListOrder; // PaneView Attributes exports.getActivePaneId = getActivePaneId; exports.setActivePaneId = setActivePaneId; exports.getPaneIdList = getPaneIdList; exports.getPaneTitle = getPaneTitle; exports.getPaneCount = getPaneCount; exports.isExclusiveToPane = isExclusiveToPane; exports.getAllOpenFiles = getAllOpenFiles; exports.focusActivePane = focusActivePane; exports.switchPaneFocus = switchPaneFocus; // Layout exports.setLayoutScheme = setLayoutScheme; exports.getLayoutScheme = getLayoutScheme; // Convenience exports.getCurrentlyViewedFile = getCurrentlyViewedFile; exports.getCurrentlyViewedPath = getCurrentlyViewedPath; // Constants exports.ALL_PANES = ALL_PANES; exports.ACTIVE_PANE = ACTIVE_PANE; exports.FIRST_PANE = FIRST_PANE; exports.SECOND_PANE = SECOND_PANE; });