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)
});
}
});