diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md index 0e2fef4988..42aa160645 100644 --- a/docs/API-Reference/command/Commands.md +++ b/docs/API-Reference/command/Commands.md @@ -122,6 +122,18 @@ Closes all open files ## FILE\_CLOSE\_LIST Closes files from list +**Kind**: global variable + + +## FILE\_PIN +Pins the selected file + +**Kind**: global variable + + +## FILE\_UNPIN +Unpins the selected file + **Kind**: global variable diff --git a/docs/API-Reference/view/MainViewManager.md b/docs/API-Reference/view/MainViewManager.md index f4a71dde3e..12de536613 100644 --- a/docs/API-Reference/view/MainViewManager.md +++ b/docs/API-Reference/view/MainViewManager.md @@ -346,6 +346,48 @@ Adds the given file list to the end of the workingset. Switch between panes **Kind**: global function + + +## pinFile(paneId, file) ⇒ boolean +This function is called from DocumentCommandHandlers.js handleFilePin function +it is to pin the file. the pinned file is displayed before the normal files in the working set and tab bar +we also prevent the pinned files from being closed during bulk close operations + +**Kind**: global function +**Returns**: boolean - true if the file was pinned, false if it was already pinned or not in the working set + +| Param | Type | Description | +| --- | --- | --- | +| paneId | string | the id of the pane in which to pin the file or ACTIVE_PANE | +| file | File | the File to pin | + + + +## unpinFile(paneId, file) ⇒ boolean +called from DocumentCommandHandlers.js handleFileUnpin function, +to unpin the file + +**Kind**: global function +**Returns**: boolean - true if the file was unpinned, false if it wasn't pinned + +| Param | Type | Description | +| --- | --- | --- | +| paneId | string | the id of the pane in which to unpin the file or ACTIVE_PANE | +| file | File | the File to unpin | + + + +## isPathPinned(paneId, path) ⇒ boolean +checks if a file path is pinned in the working set. + +**Kind**: global function +**Returns**: boolean - true if the file is pinned + +| Param | Type | Description | +| --- | --- | --- | +| paneId | string | the id of the pane in which to check or ACTIVE_PANE | +| path | string | the full path to check | + ## traverseToNextViewByMRU(direction) ⇒ Object diff --git a/docs/API-Reference/view/Pane.md b/docs/API-Reference/view/Pane.md index 399b3df8e7..20aab212e5 100644 --- a/docs/API-Reference/view/Pane.md +++ b/docs/API-Reference/view/Pane.md @@ -22,6 +22,9 @@ const Pane = brackets.getModule("view/Pane") * [.ITEM_NOT_FOUND](#Pane+ITEM_NOT_FOUND) * [.ITEM_FOUND_NO_SORT](#Pane+ITEM_FOUND_NO_SORT) * [.ITEM_FOUND_NEEDS_SORT](#Pane+ITEM_FOUND_NEEDS_SORT) + * [.pinPath(path)](#Pane+pinPath) ⇒ boolean + * [.unpinPath(path)](#Pane+unpinPath) ⇒ boolean + * [.isPathPinned(path)](#Pane+isPathPinned) ⇒ boolean * [.mergeFrom(other)](#Pane+mergeFrom) * [.destroy()](#Pane+destroy) * [.getViewList()](#Pane+getViewList) ⇒ Array.<File> @@ -143,6 +146,42 @@ and the workingset needs to be resorted **Kind**: instance constant of [Pane](#Pane) **See**: [reorderItem](#Pane+reorderItem) + + +### pane.pinPath(path) ⇒ boolean +this pins a file path + +**Kind**: instance method of [Pane](#Pane) +**Returns**: boolean - true if the file was pinned, false if it was already pinned or not in the view list + +| Param | Type | Description | +| --- | --- | --- | +| path | string | the full path of the file to pin | + + + +### pane.unpinPath(path) ⇒ boolean +this unpins a file path. + +**Kind**: instance method of [Pane](#Pane) +**Returns**: boolean - true if the file was unpinned, false if it wasn't pinned + +| Param | Type | Description | +| --- | --- | --- | +| path | string | the full path of the file to unpin | + + + +### pane.isPathPinned(path) ⇒ boolean +checks if a file path is pinned + +**Kind**: instance method of [Pane](#Pane) +**Returns**: boolean - true if the file is pinned false otherwise + +| Param | Type | Description | +| --- | --- | --- | +| path | string | the full path of the file | + ### pane.mergeFrom(other) diff --git a/src/command/Commands.js b/src/command/Commands.js index 216561b404..5befcf80eb 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -85,6 +85,12 @@ define(function (require, exports, module) { /** Closes files from list */ exports.FILE_CLOSE_LIST = "file.close_list"; // DocumentCommandHandlers.js handleFileCloseList() + /** Pins the selected file */ + exports.FILE_PIN = "file.pin"; // DocumentCommandHandlers.js handleFilePin() + + /** Unpins the selected file */ + exports.FILE_UNPIN = "file.unpin"; // DocumentCommandHandlers.js handleFileUnpin() + /** Reopens last closed file */ exports.FILE_REOPEN_CLOSED = "file.reopen_closed"; // DocumentCommandHandlers.js handleReopenClosed() diff --git a/src/command/DefaultMenus.js b/src/command/DefaultMenus.js index 9e713ecd19..89ebf298fb 100644 --- a/src/command/DefaultMenus.js +++ b/src/command/DefaultMenus.js @@ -32,7 +32,8 @@ define(function (require, exports, module) { Menus = require("command/Menus"), Strings = require("strings"), MainViewManager = require("view/MainViewManager"), - CommandManager = require("command/CommandManager"); + CommandManager = require("command/CommandManager"), + WorkingSetView = require("project/WorkingSetView"); /** * Disables menu items present in items if enabled is true. @@ -68,6 +69,14 @@ define(function (require, exports, module) { Commands.NAVIGATE_OPEN_IN_DEFAULT_APP]); }); } + + // Pin/Unpin logic: to hide the option when its not applicable + const contextFile = WorkingSetView.getContext(); + if (contextFile) { + const isPinned = MainViewManager.isPathPinned(MainViewManager.ACTIVE_PANE, contextFile.fullPath); + CommandManager.get(Commands.FILE_PIN).setEnabled(!isPinned); + CommandManager.get(Commands.FILE_UNPIN).setEnabled(isPinned); + } } const isBrowser = !Phoenix.isNativeApp; @@ -320,6 +329,9 @@ define(function (require, exports, module) { subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_DEFAULT_APP); } workingset_cmenu.addMenuDivider(); + workingset_cmenu.addMenuItem(Commands.FILE_PIN, null, null, null, { hideWhenCommandDisabled: true }); + workingset_cmenu.addMenuItem(Commands.FILE_UNPIN, null, null, null, { hideWhenCommandDisabled: true }); + workingset_cmenu.addMenuDivider(); workingset_cmenu.addMenuItem(Commands.FILE_COPY); workingset_cmenu.addMenuItem(Commands.FILE_COPY_PATH); workingset_cmenu.addMenuItem(Commands.FILE_DUPLICATE); diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 9029468d3d..195cdbed40 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -2002,6 +2002,35 @@ define(function (require, exports, module) { }); } + /** + * we call this function when the user selects the 'Pin' option from the context menu + * so we pin the file. the pinned file is displayed before the normal files in the working set and tab bar + * we also prevent the pinned files from being closed during bulk close operations + * @param {{file: File, paneId: string}} commandData - the file to pin and paneId, + * if unavailable we use the current file and active pane + */ + function handleFilePin(commandData = {}) { + const file = commandData.file || MainViewManager.getCurrentlyViewedFile(); + const paneId = commandData.paneId || MainViewManager.ACTIVE_PANE; + + if (file) { + MainViewManager.pinFile(paneId, file); + } + } + + /** + * this unpins the file so it goes back to normal behavior in the working set and tab bar + * @param {{file: File, paneId: string}} commandData - read the JSDoc for handleFilePin + */ + function handleFileUnpin(commandData = {}) { + const file = commandData.file || MainViewManager.getCurrentlyViewedFile(); + const paneId = commandData.paneId || MainViewManager.ACTIVE_PANE; + + if (file) { + MainViewManager.unpinFile(paneId, file); + } + } + /** Show the selected sidebar (tree or workingset) item in Finder/Explorer */ function handleShowInOS() { var entry = ProjectManager.getSelectedItem(); @@ -2380,6 +2409,8 @@ define(function (require, exports, module) { CommandManager.register(Strings.CMD_FILE_SAVE_AS, Commands.FILE_SAVE_AS, handleFileSaveAs); CommandManager.register(Strings.CMD_FILE_RENAME, Commands.FILE_RENAME, handleFileRename); CommandManager.register(Strings.CMD_FILE_DELETE, Commands.FILE_DELETE, handleFileDelete); + CommandManager.register(Strings.CMD_FILE_PIN, Commands.FILE_PIN, handleFilePin); + CommandManager.register(Strings.CMD_FILE_UNPIN, Commands.FILE_UNPIN, handleFileUnpin); // Close Commands CommandManager.register(Strings.CMD_FILE_CLOSE, Commands.FILE_CLOSE, handleFileClose); diff --git a/src/extensionsIntegrated/TabBar/drag-drop.js b/src/extensionsIntegrated/TabBar/drag-drop.js index 53bcb50062..c56e53bdaa 100644 --- a/src/extensionsIntegrated/TabBar/drag-drop.js +++ b/src/extensionsIntegrated/TabBar/drag-drop.js @@ -24,6 +24,7 @@ define(function (require, exports, module) { const MainViewManager = require("view/MainViewManager"); const CommandManager = require("command/CommandManager"); const Commands = require("command/Commands"); + const FileSystem = require("filesystem/FileSystem"); /** * These variables track the drag and drop state of tabs @@ -307,6 +308,46 @@ define(function (require, exports, module) { newPosition--; } MainViewManager._moveWorkingSetItem(paneId, draggedIndex, newPosition); + + // we check if the dragged file is pinned, cause if it is we might need to unpin it, + // if it is dropped after an unpinned file + const isDraggedFilePinned = MainViewManager.isPathPinned(paneId, draggedPath); + + if (isDraggedFilePinned && newPosition > 0) { + const newWorkingSet = MainViewManager.getWorkingSet(paneId); + const prevFilePath = newWorkingSet[newPosition - 1].fullPath; + + // if the prev file is not pinned, we unpin this file too! + if (!MainViewManager.isPathPinned(paneId, prevFilePath)) { + const fileObj = FileSystem.getFileForPath(draggedPath); + + // we consciously enable the unpin command here, because if the tab is pinned, + // the unpin command will be disabled by default as the last context menu item + // and the FILE_UNPIN command will not execute + CommandManager.get(Commands.FILE_UNPIN).setEnabled(true); + CommandManager.execute(Commands.FILE_UNPIN, { file: fileObj, paneId: paneId }); + } + } + + // if the dragged file is not pinned, we check if it should be pinned, + // if it is dropped before a pinned file + if (!isDraggedFilePinned) { + const newWorkingSet = MainViewManager.getWorkingSet(paneId); + + // check if there's a file after this one + if (newPosition < newWorkingSet.length - 1) { + const nextFilePath = newWorkingSet[newPosition + 1].fullPath; + + // if the next file is pinned, we pin this file too! + if (MainViewManager.isPathPinned(paneId, nextFilePath)) { + const fileObj = FileSystem.getFileForPath(draggedPath); + + // we consciously enable the pin command here, same reason as above + CommandManager.get(Commands.FILE_PIN).setEnabled(true); + CommandManager.execute(Commands.FILE_PIN, { file: fileObj, paneId: paneId }); + } + } + } } } diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 841f578743..44d72413de 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -143,6 +143,7 @@ define(function (require, exports, module) { const isDirty = Helper._isFileModified(FileSystem.getFileForPath(entry.path)); const isPlaceholder = entry.isPlaceholder === true; + const isPinned = MainViewManager.isPathPinned(paneId, entry.path); let gitStatus = ""; // this will be shown in the tooltip when a tab is hovered let gitStatusClass = ""; // for styling @@ -173,12 +174,15 @@ define(function (require, exports, module) { ${isActive ? "active" : ""} ${isDirty ? "dirty" : ""} ${isPlaceholder ? "placeholder" : ""} + ${isPinned ? "pinned" : ""} ${gitStatusClass}" data-path="${entry.path}" title="${Phoenix.app.getDisplayPath(entry.path)}${gitStatus ? " (" + gitStatus + ")" : ""}">
-
+ ${isPinned ? + '
' : + '
'} ` ); @@ -514,6 +518,18 @@ define(function (require, exports, module) { CommandManager.execute(Commands.FILE_CLOSE, { file: fileObj, paneId: paneId }); // close the file } + + // check if the clicked element is the pin icon, if yes then we need to unpin it + if ($(event.target).hasClass("fa-thumbtack") || $(event.target).closest(".tab-pin").length) { + event.preventDefault(); + event.stopPropagation(); + + // we consciously enable the unpin command here, because if the tab is pinned, + // the unpin command will be disabled by default as the last context menu item + // and the user will not be able to unpin the tab + CommandManager.get(Commands.FILE_UNPIN).setEnabled(true); + CommandManager.execute(Commands.FILE_UNPIN, { file: fileObj, paneId: paneId }); + } }); // add listener for tab close button to show the tooltip along with the keybinding @@ -538,6 +554,9 @@ define(function (require, exports, module) { if ($(event.target).hasClass("fa-times") || $(event.target).closest(".tab-close").length) { return; } + if ($(event.target).hasClass("fa-thumbtack") || $(event.target).closest(".tab-pin").length) { + return; + } // Get the file path from the data-path attribute of the parent tab const filePath = $(this).attr("data-path"); @@ -655,7 +674,9 @@ define(function (require, exports, module) { "workingSetAddList", "workingSetRemoveList", "workingSetUpdate", - "_workingSetDisableAutoSort" + "_workingSetDisableAutoSort", + "workingSetPinned", + "workingSetUnpinned" ]; MainViewManager.off(events.join(" "), updateTabs); MainViewManager.on(events.join(" "), updateTabs); diff --git a/src/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js index b6df56139a..cd9a5f0ff2 100644 --- a/src/extensionsIntegrated/TabBar/more-options.js +++ b/src/extensionsIntegrated/TabBar/more-options.js @@ -26,6 +26,7 @@ define(function (require, exports, module) { const CommandManager = require("command/CommandManager"); const Commands = require("command/Commands"); const FileSystem = require("filesystem/FileSystem"); + const MainViewManager = require("view/MainViewManager"); const Menus = require("command/Menus"); const Strings = require("strings"); @@ -43,6 +44,22 @@ define(function (require, exports, module) { // this is set inside the showMoreOptionsContextMenu. read that func for more details let _currentTabContext = { filePath: null, paneId: null }; + /** + * this function is called before the context menu is shown + * it updates the menu items based on the current tab context + * so we only show the relevant options + */ + function _updateMenuItems() { + // PIN/UNPIN logic + const isPinned = MainViewManager.isPathPinned( + _currentTabContext.paneId, + _currentTabContext.filePath + ); + + CommandManager.get(Commands.FILE_PIN).setEnabled(!isPinned); + CommandManager.get(Commands.FILE_UNPIN).setEnabled(isPinned); + } + // gets the working set (list of open files) for the given pane function _getWorkingSet(paneId) { return paneId === "first-pane" ? Global.firstPaneWorkingSet : Global.secondPaneWorkingSet; @@ -151,11 +168,18 @@ define(function (require, exports, module) { menu.addMenuItem(TABBAR_CLOSE_SAVED_TABS); menu.addMenuItem(TABBAR_CLOSE_ALL); menu.addMenuDivider(); + menu.addMenuItem(Commands.FILE_PIN, null, null, null, { hideWhenCommandDisabled: true }); + menu.addMenuItem(Commands.FILE_UNPIN, null, null, null, { hideWhenCommandDisabled: true }); + menu.addMenuDivider(); menu.addMenuItem(Commands.FILE_RENAME); menu.addMenuItem(Commands.FILE_DELETE); menu.addMenuItem(Commands.NAVIGATE_SHOW_IN_FILE_TREE); menu.addMenuDivider(); menu.addMenuItem(Commands.FILE_REOPEN_CLOSED); + + // _updateMenuItems function disables the button which are not needed for the current tab + // and those items are then hidden by the menu system automatically because of the hideWhenCommandDisabled flag + menu.on("beforeContextMenuOpen", _updateMenuItems); } module.exports = { diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 36f5a8c97e..3b9dfa48c6 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -565,6 +565,8 @@ define({ "CMD_PROJECT_SETTINGS": "Project Settings\u2026", "CMD_FILE_RENAME": "Rename", "CMD_FILE_DELETE": "Delete", + "CMD_FILE_PIN": "Pin", + "CMD_FILE_UNPIN": "Unpin", "CMD_INSTALL_EXTENSION": "Install Extension\u2026", "CMD_EXTENSION_MANAGER": "Extension Manager\u2026", "CMD_FILE_REFRESH": "Refresh File Tree", diff --git a/src/project/WorkingSetView.js b/src/project/WorkingSetView.js index bfa4cb7b95..b051d29f26 100644 --- a/src/project/WorkingSetView.js +++ b/src/project/WorkingSetView.js @@ -794,7 +794,37 @@ define(function (require, exports, module) { } } else if (sourceView.paneId === currentView.paneId) { // item was reordered - MainViewManager._moveWorkingSetItem(sourceView.paneId, startingIndex, $el.index()); + const newIndex = $el.index(); + MainViewManager._moveWorkingSetItem(sourceView.paneId, startingIndex, newIndex); + + // Check if the dragged file is pinned - if moved after an unpinned file, unpin it + const isPinned = MainViewManager.isPathPinned(sourceView.paneId, sourceFile.fullPath); + if (isPinned && newIndex > 0) { + const workingSet = MainViewManager.getWorkingSet(sourceView.paneId); + const prevFilePath = workingSet[newIndex - 1].fullPath; + + if (!MainViewManager.isPathPinned(sourceView.paneId, prevFilePath)) { + CommandManager.get(Commands.FILE_UNPIN).setEnabled(true); + CommandManager.execute(Commands.FILE_UNPIN, { file: sourceFile, paneId: sourceView.paneId }); + } + } + + // Check if the dragged file is not pinned - if moved before a pinned file, pin it + if (!isPinned) { + const workingSet = MainViewManager.getWorkingSet(sourceView.paneId); + + // check if there's a file after this one + if (newIndex < workingSet.length - 1) { + const nextFilePath = workingSet[newIndex + 1].fullPath; + + // if the next file is pinned, pin this file too + if (MainViewManager.isPathPinned(sourceView.paneId, nextFilePath)) { + CommandManager.get(Commands.FILE_PIN).setEnabled(true); + CommandManager.execute(Commands.FILE_PIN, { file: sourceFile, paneId: sourceView.paneId }); + } + } + } + postDropCleanup(); } else { // If the same doc view is present in the destination pane prevent drop @@ -1244,6 +1274,14 @@ define(function (require, exports, module) { $newItem.addClass(provider(data)); }); + // if the file is pinned, add the pin icon in the list item + const isPinned = MainViewManager.isPathPinned(this.paneId, file.fullPath); + if (isPinned) { + $newItem.addClass("pinned"); + const $pinIcon = $("
"); + $newItem.append($pinIcon); + } + // Update the listItem's apperance this._updateFileStatusIcon($newItem, _isOpenAndDirty(file), false); _updateListItemSelection($newItem, selectedFile); @@ -1421,6 +1459,15 @@ define(function (require, exports, module) { } }; + /** + * working set pin change (unpinned/pinned) event handler + */ + WorkingSetView.prototype._handleWorkingSetPinChange = function (e, file, paneId) { + if (paneId === this.paneId) { + this._rebuildViewList(true); + } + }; + /** * dirtyFlagChange event handler * @private @@ -1469,6 +1516,8 @@ define(function (require, exports, module) { MainViewManager.on(this._makeEventName("activePaneChange"), _.bind(this._handleActivePaneChange, this)); MainViewManager.on(this._makeEventName("paneLayoutChange"), _.bind(this._handlePaneLayoutChange, this)); MainViewManager.on(this._makeEventName("workingSetUpdate"), _.bind(this._handleWorkingSetUpdate, this)); + MainViewManager.on(this._makeEventName("workingSetPinned"), _.bind(this._handleWorkingSetPinChange, this)); + MainViewManager.on(this._makeEventName("workingSetUnpinned"), _.bind(this._handleWorkingSetPinChange, this)); DocumentManager.on(this._makeEventName("dirtyFlagChange"), _.bind(this._handleDirtyFlagChanged, this)); diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 59655a39fd..2002d1533c 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -254,6 +254,28 @@ background-color: rgba(255, 255, 255, 0.1); } +.tab-pin { + font-size: 0.625rem; + padding: 0.1rem 0.4rem; + margin-left: 0.25rem; + color: #666; + transition: all 0.2s ease; + border-radius: 0.2rem; +} + +.dark .tab-pin { + color: #CCC; +} + +.tab-pin:hover { + cursor: pointer; + background-color: rgba(0, 0, 0, 0.1); +} + +.dark .tab-pin:hover { + background-color: rgba(255, 255, 255, 0.1); +} + .tab.placeholder .tab-name { font-style: italic; color: #999; diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 1f1ef12c8b..28dc7017fe 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -1442,6 +1442,20 @@ a, img { .extension, .directory { color: @project-panel-text-2; } + + li.pinned .working-set-pin-icon { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + font-size: 9px; + color: @project-panel-text-2; + opacity: 0.7; + + &:hover { + opacity: 1; + } + } } diff --git a/src/view/MainViewManager.js b/src/view/MainViewManager.js index 9a437adfd7..33d2a06619 100644 --- a/src/view/MainViewManager.js +++ b/src/view/MainViewManager.js @@ -1001,6 +1001,68 @@ define(function (require, exports, module) { exports.trigger("_workingSetDisableAutoSort", pane.id); } + /** + * This function is called from DocumentCommandHandlers.js handleFilePin function + * it is to pin the file. the pinned file is displayed before the normal files in the working set and tab bar + * we also prevent the pinned files from being closed during bulk close operations + * + * @param {!string} paneId - the id of the pane in which to pin the file or ACTIVE_PANE + * @param {!File} file - the File to pin + * @return {boolean} true if the file was pinned, false if it was already pinned or not in the working set + */ + function pinFile(paneId, file) { + const pane = _getPane(paneId); + if (!pane) { + return false; + } + + const result = pane.pinPath(file.fullPath); + if (result) { + exports.trigger("workingSetPinned", file, pane.id); + exports.trigger("workingSetSort", pane.id); + } + + return result; + } + + /** + * called from DocumentCommandHandlers.js handleFileUnpin function, + * to unpin the file + * + * @param {!string} paneId - the id of the pane in which to unpin the file or ACTIVE_PANE + * @param {!File} file - the File to unpin + * @return {boolean} true if the file was unpinned, false if it wasn't pinned + */ + function unpinFile(paneId, file) { + const pane = _getPane(paneId); + if (!pane) { + return false; + } + + const result = pane.unpinPath(file.fullPath); + if (result) { + exports.trigger("workingSetUnpinned", file, pane.id); + } + + return result; + } + + /** + * checks if a file path is pinned in the working set. + * + * @param {!string} paneId - the id of the pane in which to check or ACTIVE_PANE + * @param {!string} path - the full path to check + * @return {boolean} true if the file is pinned + */ + function isPathPinned(paneId, path) { + const pane = _getPane(paneId); + if (!pane) { + return false; + } + + return pane.isPathPinned(path); + } + /** * Get the next or previous file in the MRU list. * @param {!number} direction - Must be 1 or -1 to traverse forward or backward @@ -1808,6 +1870,11 @@ define(function (require, exports, module) { exports.getWorkingSetSize = getWorkingSetSize; exports.getWorkingSet = getWorkingSet; + // Pin Management + exports.pinFile = pinFile; + exports.unpinFile = unpinFile; + exports.isPathPinned = isPathPinned; + // Pane state exports.cacheScrollState = cacheScrollState; exports.restoreAdjustedScrollState = restoreAdjustedScrollState; diff --git a/src/view/Pane.js b/src/view/Pane.js index e1c7527fde..660116fac8 100644 --- a/src/view/Pane.js +++ b/src/view/Pane.js @@ -515,9 +515,82 @@ define(function (require, exports, module) { this._viewListAddedOrder = []; this._views = {}; this._currentView = null; + this._pinnedPaths = new Set(); this.showInterstitial(true); }; + /** + * this pins a file path + * @param {string} path - the full path of the file to pin + * @return {boolean} true if the file was pinned, false if it was already pinned or not in the view list + */ + Pane.prototype.pinPath = function (path) { + // check if already pinned, if it is then ignore + if (this.isPathPinned(path)) { + return false; + } + + // check if the file is present in the open view list, if not then also ignore + const index = this.findInViewList(path); + if (index === -1) { + return false; + } + + // add the file path to the pinned paths set + // we also reorder the view list as pinned files should appear before regular files + this._pinnedPaths.add(path); + this._reorderViewListForPinning(); + return true; + }; + + /** + * this unpins a file path. + * @param {string} path - the full path of the file to unpin + * @return {boolean} true if the file was unpinned, false if it wasn't pinned + */ + Pane.prototype.unpinPath = function (path) { + // if the file is already unpinned, we just ignore + if (!this.isPathPinned(path)) { + return false; + } + + // we just remove the file path from the pinned paths set + // here we don't need to reorder the view list, the file can stay in its current position + this._pinnedPaths.delete(path); + return true; + }; + + /** + * checks if a file path is pinned + * @param {string} path - the full path of the file + * @return {boolean} true if the file is pinned false otherwise + */ + Pane.prototype.isPathPinned = function (path) { + return this._pinnedPaths.has(path); + }; + + /** + * this function is responsible to reorder the view list such that pinned files appear first + * the pinned files and regular files both maintain their relative order + * @private + */ + Pane.prototype._reorderViewListForPinning = function () { + const self = this; + + this._viewList.sort(function (a, b) { + // just the regular comparison sorting logic based on whether the files are pinned or not + const aPinned = self._pinnedPaths.has(a.fullPath); + const bPinned = self._pinnedPaths.has(b.fullPath); + if (aPinned && !bPinned) { + return -1; + } + if (!aPinned && bPinned) { + return 1; + } + return 0; + }); + }; + /** * Creates a pane event namespaced to this pane * (pass an empty string to generate just the namespace key to pass to jQuery to turn off all events handled by this pane) @@ -948,6 +1021,8 @@ define(function (require, exports, module) { this._viewList.splice(index, 1); this._viewListMRUOrder.splice(this.findInViewListMRUOrder(file.fullPath), 1); this._viewListAddedOrder.splice(this.findInViewListAddedOrder(file.fullPath), 1); + // also remove from pinned paths if it was pinned + this._pinnedPaths.delete(file.fullPath); } // Destroy the view @@ -1514,6 +1589,7 @@ define(function (require, exports, module) { Pane.prototype.loadState = function (state) { var filesToAdd = [], viewStates = {}, + pinnedPaths = [], activeFile, data, self = this; @@ -1530,10 +1606,22 @@ define(function (require, exports, module) { if (entry.viewState) { viewStates[entry.file] = entry.viewState; } + if (entry.pinned) { + pinnedPaths.push(entry.file); + } }); this.addListToViewList(filesToAdd); + // restore pinned state + pinnedPaths.forEach(function (path) { + self._pinnedPaths.add(path); + }); + // reorder to put pinned files first + if (pinnedPaths.length > 0) { + this._reorderViewListForPinning(); + } + ViewStateManager.addViewStates(viewStates); activeFile = activeFile || getInitialViewFilePath(); @@ -1551,6 +1639,7 @@ define(function (require, exports, module) { */ Pane.prototype.saveState = function () { var result = [], + self = this, currentlyViewedPath = this.getCurrentlyViewedPath(); // Save the current view state first @@ -1570,7 +1659,8 @@ define(function (require, exports, module) { result.push({ file: file.fullPath, active: (file.fullPath === currentlyViewedPath), - viewState: ViewStateManager.getViewState(file) + viewState: ViewStateManager.getViewState(file), + pinned: self._pinnedPaths.has(file.fullPath) }); } });