From c21ca6a110486bfc82b666d6ca60849525a720de Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 21 Jan 2026 20:40:31 +0530 Subject: [PATCH 01/17] feat: register the pin unpin commands --- src/command/Commands.js | 6 ++++++ src/document/DocumentCommandHandlers.js | 10 ++++++++++ src/nls/root/strings.js | 2 ++ 3 files changed, 18 insertions(+) diff --git a/src/command/Commands.js b/src/command/Commands.js index 216561b40..5befcf80e 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/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 9029468d3..dcebff523 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -2002,6 +2002,14 @@ define(function (require, exports, module) { }); } + function handleFilePin() { + // @devansh: implement the pin functionality here... + } + + function handleFileUnpin() { + // @devansh: implement the unpin functionality here... + } + /** Show the selected sidebar (tree or workingset) item in Finder/Explorer */ function handleShowInOS() { var entry = ProjectManager.getSelectedItem(); @@ -2380,6 +2388,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/nls/root/strings.js b/src/nls/root/strings.js index 36f5a8c97..3b9dfa48c 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", From 08250f70c5af67dd0f94a0e8218a3dd93624daf1 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 21 Jan 2026 20:44:25 +0530 Subject: [PATCH 02/17] feat: add pin unpin to working files and tab bar context menu --- src/command/DefaultMenus.js | 3 +++ src/extensionsIntegrated/TabBar/more-options.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/command/DefaultMenus.js b/src/command/DefaultMenus.js index 9e713ecd1..0b031c467 100644 --- a/src/command/DefaultMenus.js +++ b/src/command/DefaultMenus.js @@ -320,6 +320,9 @@ define(function (require, exports, module) { subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_DEFAULT_APP); } workingset_cmenu.addMenuDivider(); + workingset_cmenu.addMenuItem(Commands.FILE_PIN); + workingset_cmenu.addMenuItem(Commands.FILE_UNPIN); + 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/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js index b6df56139..d7f040611 100644 --- a/src/extensionsIntegrated/TabBar/more-options.js +++ b/src/extensionsIntegrated/TabBar/more-options.js @@ -151,6 +151,9 @@ define(function (require, exports, module) { menu.addMenuItem(TABBAR_CLOSE_SAVED_TABS); menu.addMenuItem(TABBAR_CLOSE_ALL); menu.addMenuDivider(); + menu.addMenuItem(Commands.FILE_PIN); + menu.addMenuItem(Commands.FILE_UNPIN); + menu.addMenuDivider(); menu.addMenuItem(Commands.FILE_RENAME); menu.addMenuItem(Commands.FILE_DELETE); menu.addMenuItem(Commands.NAVIGATE_SHOW_IN_FILE_TREE); From 536715b821269a1f3ba3dc299b88d56d9e28c09d Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 21 Jan 2026 23:35:56 +0530 Subject: [PATCH 03/17] feat: setup file pinning architecture --- src/document/DocumentCommandHandlers.js | 29 ++++++-- src/view/MainViewManager.js | 67 ++++++++++++++++++ src/view/Pane.js | 92 ++++++++++++++++++++++++- 3 files changed, 183 insertions(+), 5 deletions(-) diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index dcebff523..195cdbed4 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -2002,12 +2002,33 @@ define(function (require, exports, module) { }); } - function handleFilePin() { - // @devansh: implement the pin functionality here... + /** + * 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); + } } - function handleFileUnpin() { - // @devansh: implement the unpin functionality here... + /** + * 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 */ diff --git a/src/view/MainViewManager.js b/src/view/MainViewManager.js index 9a437adfd..33d2a0661 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 e1c7527fd..660116fac 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) }); } }); From 8f1273366ea323d197c6820102a6521d79ca5a53 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 22 Jan 2026 21:09:30 +0530 Subject: [PATCH 04/17] feat: show the pin icon for pinned files in tab bar --- src/extensionsIntegrated/TabBar/main.js | 18 +++++++++++++-- src/styles/Extn-TabBar.less | 30 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 841f57874..d3e5c15af 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,14 @@ 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(); + + CommandManager.execute(Commands.FILE_UNPIN, { file: fileObj, paneId: paneId }); + } }); // add listener for tab close button to show the tooltip along with the keybinding @@ -655,7 +667,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/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 59655a39f..db67eecc0 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -254,6 +254,36 @@ background-color: rgba(255, 255, 255, 0.1); } +.tab-pin { + font-size: 0.75rem; + 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.pinned { + background-color: rgba(0, 100, 200, 0.05); +} + +.dark .tab.pinned { + background-color: rgba(100, 150, 255, 0.1); +} + .tab.placeholder .tab-name { font-style: italic; color: #999; From 8508312d5785970df750a88716ba38a5f723d688 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 22 Jan 2026 21:26:46 +0530 Subject: [PATCH 05/17] feat: pin files support in working files --- src/project/WorkingSetView.js | 19 +++++++++++++++++++ src/styles/brackets.less | 14 ++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/project/WorkingSetView.js b/src/project/WorkingSetView.js index bfa4cb7b9..d406b246d 100644 --- a/src/project/WorkingSetView.js +++ b/src/project/WorkingSetView.js @@ -1244,6 +1244,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 +1429,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 +1486,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/brackets.less b/src/styles/brackets.less index 1f1ef12c8..57fc1c4da 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 .pin-icon { + position: absolute; + right: 4px; + top: 50%; + transform: translateY(-50%); + font-size: 10px; + color: @project-panel-text-2; + opacity: 0.7; + + &:hover { + opacity: 1; + } + } } From 5f71fbe489dcf47f49b850ab75db0665e1f9ee2c Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 23 Jan 2026 12:53:40 +0530 Subject: [PATCH 06/17] chore: auto update API docs --- docs/API-Reference/command/Commands.md | 12 +++++++ docs/API-Reference/view/MainViewManager.md | 42 ++++++++++++++++++++++ docs/API-Reference/view/Pane.md | 39 ++++++++++++++++++++ 3 files changed, 93 insertions(+) diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md index 0e2fef498..42aa16064 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 f4a71dde3..12de53661 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 399b3df8e..20aab212e 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) From d3f81e48601da4def50b2391c7be5d6c848f59d5 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 23 Jan 2026 13:17:52 +0530 Subject: [PATCH 07/17] feat: show/hide pin unpin option based on the current tab status --- .../TabBar/more-options.js | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js index d7f040611..cd9a5f0ff 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,14 +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); - menu.addMenuItem(Commands.FILE_UNPIN); + 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 = { From 33306453146dcb8d106f4d92096ab7d8fc6adaa5 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 23 Jan 2026 13:24:09 +0530 Subject: [PATCH 08/17] fix: 2 pin icons coming up in the working files because of conflict --- src/project/WorkingSetView.js | 2 +- src/styles/brackets.less | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/project/WorkingSetView.js b/src/project/WorkingSetView.js index d406b246d..138fd7939 100644 --- a/src/project/WorkingSetView.js +++ b/src/project/WorkingSetView.js @@ -1248,7 +1248,7 @@ define(function (require, exports, module) { const isPinned = MainViewManager.isPathPinned(this.paneId, file.fullPath); if (isPinned) { $newItem.addClass("pinned"); - const $pinIcon = $("
"); + const $pinIcon = $("
"); $newItem.append($pinIcon); } diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 57fc1c4da..8b05042d2 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -1443,7 +1443,7 @@ a, img { color: @project-panel-text-2; } - li.pinned .pin-icon { + li.pinned .working-set-pin-icon { position: absolute; right: 4px; top: 50%; From e17d32248dce28712e7c601b73da3b6404c19954 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 23 Jan 2026 14:05:09 +0530 Subject: [PATCH 09/17] feat: hide the pin/unpin option when its not applicable --- src/command/DefaultMenus.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/command/DefaultMenus.js b/src/command/DefaultMenus.js index 0b031c467..89ebf298f 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,8 +329,8 @@ define(function (require, exports, module) { subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_DEFAULT_APP); } workingset_cmenu.addMenuDivider(); - workingset_cmenu.addMenuItem(Commands.FILE_PIN); - workingset_cmenu.addMenuItem(Commands.FILE_UNPIN); + 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); From ca13d6758b4fb9604acc7ac9fa7ed6a60057cd98 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 23 Jan 2026 14:25:49 +0530 Subject: [PATCH 10/17] fix: unable to unpin tab in tab bar when its pinned using working files context menu --- src/extensionsIntegrated/TabBar/main.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index d3e5c15af..44d72413d 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -524,6 +524,10 @@ define(function (require, exports, module) { 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 }); } }); @@ -550,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"); From 8b003a8f622b5f441963686e82affabff292ee65 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 23 Jan 2026 14:46:51 +0530 Subject: [PATCH 11/17] refactor: improve pin icon styles in tab bar and working files --- src/styles/Extn-TabBar.less | 2 +- src/styles/brackets.less | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index db67eecc0..bd01c45bd 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -255,7 +255,7 @@ } .tab-pin { - font-size: 0.75rem; + font-size: 0.625rem; padding: 0.1rem 0.4rem; margin-left: 0.25rem; color: #666; diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 8b05042d2..28dc7017f 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -1445,10 +1445,10 @@ a, img { li.pinned .working-set-pin-icon { position: absolute; - right: 4px; + right: 10px; top: 50%; transform: translateY(-50%); - font-size: 10px; + font-size: 9px; color: @project-panel-text-2; opacity: 0.7; From 6425f8817eaa84748b6e4be835a8c86bd30ee233 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 23 Jan 2026 21:56:17 +0530 Subject: [PATCH 12/17] fix: remove special styling from pinned tabs --- src/styles/Extn-TabBar.less | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index bd01c45bd..2002d1533 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -276,14 +276,6 @@ background-color: rgba(255, 255, 255, 0.1); } -.tab.pinned { - background-color: rgba(0, 100, 200, 0.05); -} - -.dark .tab.pinned { - background-color: rgba(100, 150, 255, 0.1); -} - .tab.placeholder .tab-name { font-style: italic; color: #999; From 2d84780fe6f901f7706539068f7a6725b8abea8c Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 23 Jan 2026 22:15:45 +0530 Subject: [PATCH 13/17] feat: unpin the pinned file if it is dropped after pinned files --- src/extensionsIntegrated/TabBar/drag-drop.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/extensionsIntegrated/TabBar/drag-drop.js b/src/extensionsIntegrated/TabBar/drag-drop.js index 53bcb5006..4bb8a480b 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,21 @@ 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); + CommandManager.execute(Commands.FILE_UNPIN, { file: fileObj, paneId: paneId }); + } + } } } From 03776774395da35dbc4d9500d56702a541013d88 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 23 Jan 2026 22:32:06 +0530 Subject: [PATCH 14/17] fix: unpin command silently fails when tab is moved between panes --- src/extensionsIntegrated/TabBar/drag-drop.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/extensionsIntegrated/TabBar/drag-drop.js b/src/extensionsIntegrated/TabBar/drag-drop.js index 4bb8a480b..f83bed6ce 100644 --- a/src/extensionsIntegrated/TabBar/drag-drop.js +++ b/src/extensionsIntegrated/TabBar/drag-drop.js @@ -320,6 +320,11 @@ define(function (require, exports, module) { // 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 }); } } From a859e0eadbd2ca97198dd826be147f14695ff817 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 23 Jan 2026 22:41:32 +0530 Subject: [PATCH 15/17] feat: unpin file in working set if moved after a regular file --- src/project/WorkingSetView.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/project/WorkingSetView.js b/src/project/WorkingSetView.js index 138fd7939..b80ac4fb3 100644 --- a/src/project/WorkingSetView.js +++ b/src/project/WorkingSetView.js @@ -794,7 +794,21 @@ 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 }); + } + } + postDropCleanup(); } else { // If the same doc view is present in the destination pane prevent drop From 1c30d280b11b8b7443f2891aed7ed54ff94d7145 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 23 Jan 2026 22:55:31 +0530 Subject: [PATCH 16/17] feat: pin the new tab if its dropped before a pinned tab --- src/extensionsIntegrated/TabBar/drag-drop.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/extensionsIntegrated/TabBar/drag-drop.js b/src/extensionsIntegrated/TabBar/drag-drop.js index f83bed6ce..c56e53bda 100644 --- a/src/extensionsIntegrated/TabBar/drag-drop.js +++ b/src/extensionsIntegrated/TabBar/drag-drop.js @@ -328,6 +328,26 @@ define(function (require, exports, module) { 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 }); + } + } + } } } From da72a5b4d0f0289ed77e2ec4d927ac8a6371a9b9 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 23 Jan 2026 22:56:10 +0530 Subject: [PATCH 17/17] feat: pin a regular file if dropped before a pinned file in working set --- src/project/WorkingSetView.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/project/WorkingSetView.js b/src/project/WorkingSetView.js index b80ac4fb3..b051d29f2 100644 --- a/src/project/WorkingSetView.js +++ b/src/project/WorkingSetView.js @@ -809,6 +809,22 @@ define(function (require, exports, module) { } } + // 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