From f00efea7ae085876d88b771c8634dc7111c86dfc Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 18 Nov 2025 21:12:34 +0530 Subject: [PATCH 01/11] feat: tab bar context menu option includes keyboard shortcuts now --- src/extensionsIntegrated/TabBar/main.js | 3 +- .../TabBar/more-options.js | 392 +++++++----------- 2 files changed, 141 insertions(+), 254 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index bf5d0c4c27..ae80c2015f 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -553,7 +553,7 @@ define(function (require, exports, module) { const paneId = isSecondPane ? "second-pane" : "first-pane"; // show the context menu at mouse position - MoreOptions.showMoreOptionsContextMenu(paneId, event.pageX, event.pageY, filePath); + MoreOptions.showMoreOptionsContextMenu(paneId, filePath, event.pageX, event.pageY); }); } @@ -768,6 +768,7 @@ define(function (require, exports, module) { // handle when a single tab gets clicked handleTabClick(); + MoreOptions.init(); Overflow.init(); DragDrop.init($("#phoenix-tab-bar"), $("#phoenix-tab-bar-2")); diff --git a/src/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js index d8bbd41d91..26014f71d3 100644 --- a/src/extensionsIntegrated/TabBar/more-options.js +++ b/src/extensionsIntegrated/TabBar/more-options.js @@ -23,308 +23,194 @@ * The more options context menu is shown when a tab is right-clicked */ define(function (require, exports, module) { - const DropdownButton = require("widgets/DropdownButton"); - const Strings = require("strings"); const CommandManager = require("command/CommandManager"); const Commands = require("command/Commands"); const FileSystem = require("filesystem/FileSystem"); + const Menus = require("command/Menus"); + const Strings = require("strings"); const Global = require("./global"); - // List of items to show in the context menu - // Strings defined in `src/nls/root/strings.js` - const items = [ - Strings.CLOSE_TAB, - Strings.CLOSE_TABS_TO_THE_LEFT, - Strings.CLOSE_TABS_TO_THE_RIGHT, - Strings.CLOSE_SAVED_TABS, - Strings.CLOSE_ALL_TABS, - "---", - Strings.CMD_FILE_RENAME, - Strings.CMD_FILE_DELETE, - Strings.CMD_SHOW_IN_TREE, - "---", - Strings.REOPEN_CLOSED_FILE - ]; - - /** - * "CLOSE TAB" - * this function handles the closing of the tab that was right-clicked - * - * @param {String} filePath - path of the file to close - * @param {String} paneId - the id of the pane in which the file is present - */ - function handleCloseTab(filePath, paneId) { - if (filePath) { - // Get the file object using FileSystem - const fileObj = FileSystem.getFileForPath(filePath); - - // Execute close command with file object and pane ID - CommandManager.execute(Commands.FILE_CLOSE, { file: fileObj, paneId: paneId }); - } + // these are Tab bar specific commands for the context menu + // not added in the Commands.js as Tab bar is not a core module but an extension + // read init function + const TABBAR_CLOSE_TABS_LEFT = "tabbar.closeTabsLeft"; + const TABBAR_CLOSE_TABS_RIGHT = "tabbar.closeTabsRight"; + const TABBAR_CLOSE_SAVED_TABS = "tabbar.closeSavedTabs"; + const TABBAR_CLOSE_ALL = "tabbar.closeAllTabs"; + + // stores the context of the right-clicked tab (which file, which pane) + // this is set inside the showMoreOptionsContextMenu. read that func for more details + let _currentTabContext = { filePath: null, paneId: null }; + + // gets the working set (list of open files) for the given pane + function _getWorkingSet(paneId) { + return paneId === "first-pane" ? Global.firstPaneWorkingSet : Global.secondPaneWorkingSet; } - /** - * "CLOSE ALL TABS" - * This will close all tabs in the specified pane - * - * @param {String} paneId - the id of the pane ["first-pane", "second-pane"] - */ - function handleCloseAllTabs(paneId) { - if (!paneId) { - return; - } - - let workingSet; - workingSet = paneId === "first-pane" ? Global.firstPaneWorkingSet : Global.secondPaneWorkingSet; - if (!workingSet || workingSet.length === 0) { - return; - } - - // close each file in the pane, start from the rightmost [to avoid index shifts] - for (let i = workingSet.length - 1; i >= 0; i--) { - const fileObj = FileSystem.getFileForPath(workingSet[i].path); + // closes files from right to left to avoid index shifts during iteration + function _closeFiles(files, paneId) { + for (let i = files.length - 1; i >= 0; i--) { + const fileObj = FileSystem.getFileForPath(files[i].path); CommandManager.execute(Commands.FILE_CLOSE, { file: fileObj, paneId: paneId }); } } - /** - * "CLOSE SAVED TABS" - * This will close all tabs that are not dirty in the specified pane - * - * @param {String} paneId - the id of the pane ["first-pane", "second-pane"] - */ - function handleCloseSavedTabs(paneId) { - if (!paneId) { - return; - } - - let workingSet; - workingSet = paneId === "first-pane" ? Global.firstPaneWorkingSet : Global.secondPaneWorkingSet; - if (!workingSet || workingSet.length === 0) { - return; + // executes a command with the right-clicked tab's file as the target + function _executeWithFileContext(commandId, options = {}) { + if (_currentTabContext.filePath) { + // we need to get the file object from the file path, as the commandManager expects the file object + const fileObj = FileSystem.getFileForPath(_currentTabContext.filePath); + CommandManager.execute(commandId, { file: fileObj, ...options }); } + } - // get all those entries that are not dirty - const unmodifiedEntries = workingSet.filter((entry) => !entry.isDirty); - - // close each non-dirty file in the pane - for (let i = unmodifiedEntries.length - 1; i >= 0; i--) { - const fileObj = FileSystem.getFileForPath(unmodifiedEntries[i].path); - CommandManager.execute(Commands.FILE_CLOSE, { file: fileObj, paneId: paneId }); - } + // **Close** + // closes the right-clicked tab + function handleCloseTab() { + _executeWithFileContext(Commands.FILE_CLOSE, { paneId: _currentTabContext.paneId }); } - /** - * "CLOSE TABS TO THE LEFT" - * This function is responsible for closing all tabs to the left of the right-clicked tab - * - * @param {String} filePath - path of the file that was right-clicked - * @param {String} paneId - the id of the pane in which the file is present - */ - function handleCloseTabsToTheLeft(filePath, paneId) { - if (!filePath) { - return; + // **Close All Tabs** + // closes all tabs in the pane where the tab was right-clicked + function handleCloseAllTabs() { + const workingSet = _getWorkingSet(_currentTabContext.paneId); + if (workingSet && workingSet.length !== 0) { + // close everything in the pane + _closeFiles(workingSet, _currentTabContext.paneId); } + } - let workingSet; - workingSet = paneId === "first-pane" ? Global.firstPaneWorkingSet : Global.secondPaneWorkingSet; - if (!workingSet) { - return; + // **Close Saved Tabs** + // closes all saved tabs (not dirty) in the pane + function handleCloseSavedTabs() { + const workingSet = _getWorkingSet(_currentTabContext.paneId); + if (workingSet && workingSet.length !== 0) { + // filter out dirty tabs, only close the saved ones + const savedTabs = workingSet.filter(entry => !entry.isDirty); + _closeFiles(savedTabs, _currentTabContext.paneId); } + } - // find the index of the current file in the working set - const currentIndex = workingSet.findIndex((entry) => entry.path === filePath); + // **Close Tabs to the Left** + // closes all tabs to the left of the right-clicked tab + function handleCloseTabsToTheLeft() { + const workingSet = _getWorkingSet(_currentTabContext.paneId); + if (!workingSet) { return; } + // find where the right-clicked tab is in the list + const currentIndex = workingSet.findIndex(entry => entry.path === _currentTabContext.filePath); if (currentIndex > 0) { - // we only proceed if there are tabs to the left - // get all files to the left of the current file - const filesToClose = workingSet.slice(0, currentIndex); - - // Close each file, starting from the rightmost [to avoid index shifts] - for (let i = filesToClose.length - 1; i >= 0; i--) { - const fileObj = FileSystem.getFileForPath(filesToClose[i].path); - CommandManager.execute(Commands.FILE_CLOSE, { file: fileObj, paneId: paneId }); - } + // slice from start to currentIndex (not including currentIndex) + _closeFiles(workingSet.slice(0, currentIndex), _currentTabContext.paneId); } } - /** - * "CLOSE TABS TO THE RIGHT" - * This function is responsible for closing all tabs to the right of the right-clicked tab - * - * @param {String} filePath - path of the file that was right-clicked - * @param {String} paneId - the id of the pane in which the file is present - */ - function handleCloseTabsToTheRight(filePath, paneId) { - if (!filePath) { - return; - } - - let workingSet; - workingSet = paneId === "first-pane" ? Global.firstPaneWorkingSet : Global.secondPaneWorkingSet; - if (!workingSet) { - return; - } + // **Close Tabs to the Right** + // closes all tabs to the right of the right-clicked tab + function handleCloseTabsToTheRight() { + const workingSet = _getWorkingSet(_currentTabContext.paneId); + if (!workingSet) { return; } - // get the index of the current file in the working set - const currentIndex = workingSet.findIndex((entry) => entry.path === filePath); - // only proceed if there are tabs to the right + // find where the right-clicked tab is in the list + const currentIndex = workingSet.findIndex(entry => entry.path === _currentTabContext.filePath); if (currentIndex !== -1 && currentIndex < workingSet.length - 1) { - // get all files to the right of the current file - const filesToClose = workingSet.slice(currentIndex + 1); - - for (let i = filesToClose.length - 1; i >= 0; i--) { - const fileObj = FileSystem.getFileForPath(filesToClose[i].path); - CommandManager.execute(Commands.FILE_CLOSE, { file: fileObj, paneId: paneId }); - } + // slice from currentIndex+1 to end + _closeFiles(workingSet.slice(currentIndex + 1), _currentTabContext.paneId); } } - /** - * "REOPEN CLOSED FILE" - * This just calls the reopen closed file command. everthing else is handled there - * TODO: disable the command if there are no closed files, look into the file menu - */ - function reopenClosedFile() { - CommandManager.execute(Commands.FILE_REOPEN_CLOSED); + // **Rename** + // renames the right-clicked tab's file + function handleFileRename() { + CommandManager.execute(Commands.SHOW_SIDEBAR); + _executeWithFileContext(Commands.FILE_RENAME); } - /** - * "RENAME FILE" - * This function handles the renaming of the file that was right-clicked - * - * @param {String} filePath - path of the file to rename - */ - function handleFileRename(filePath) { - if (filePath) { - // First ensure the sidebar is visible so users can see the rename action - CommandManager.execute(Commands.SHOW_SIDEBAR); - - // Get the file object using FileSystem - const fileObj = FileSystem.getFileForPath(filePath); - - // Execute the rename command with the file object - CommandManager.execute(Commands.FILE_RENAME, { file: fileObj }); - } + // **Delete** + // deletes the right-clicked tab's file + function handleFileDelete() { + _executeWithFileContext(Commands.FILE_DELETE); } - /** - * "DELETE FILE" - * This function handles the deletion of the file that was right-clicked - * - * @param {String} filePath - path of the file to delete - */ - function handleFileDelete(filePath) { - if (filePath) { - // Get the file object using FileSystem - const fileObj = FileSystem.getFileForPath(filePath); - - // Execute the delete command with the file object - CommandManager.execute(Commands.FILE_DELETE, { file: fileObj }); - } + // **Show in File Tree** + // shows the right-clicked tab's file in the file tree + function handleShowInFileTree() { + CommandManager.execute(Commands.SHOW_SIDEBAR); + _executeWithFileContext(Commands.NAVIGATE_SHOW_IN_FILE_TREE); } - /** - * "SHOW IN FILE TREE" - * This function handles showing the file in the file tree - * - * @param {String} filePath - path of the file to show in file tree - */ - function handleShowInFileTree(filePath) { - if (filePath) { - // First ensure the sidebar is visible so users can see the file in the tree - CommandManager.execute(Commands.SHOW_SIDEBAR); - - // Get the file object using FileSystem - const fileObj = FileSystem.getFileForPath(filePath); - - // Execute the show in tree command with the file object - CommandManager.execute(Commands.NAVIGATE_SHOW_IN_FILE_TREE, { file: fileObj }); - } - } /** - * This function is called when a tab is right-clicked - * This will show the more options context menu - * + * this function is called from Tabbar/main.js when a tab is right clicked + * it is responsible to show the context menu and also set the currentTabContext * @param {String} paneId - the id of the pane ["first-pane", "second-pane"] + * @param {String} filePath - the path of the file that was right-clicked * @param {Number} x - the x coordinate for positioning the menu * @param {Number} y - the y coordinate for positioning the menu - * @param {String} filePath - [optional] the path of the file that was right-clicked */ - function showMoreOptionsContextMenu(paneId, x, y, filePath) { - const dropdown = new DropdownButton.DropdownButton("", items); - - // Append to document body for absolute positioning - $("body").append(dropdown.$button); - - // Position the dropdown at the mouse coordinates - dropdown.$button.css({ - position: "absolute", - left: x + "px", - top: y + "px", - zIndex: 1000 - }); - - // Add a custom class to override the max-height, not sure why a scroll bar was coming but this did the trick - dropdown.dropdownExtraClasses = "tabbar-context-menu"; - - dropdown.showDropdown(); - - $(".tabbar-context-menu").css("max-height", "300px"); + function showMoreOptionsContextMenu(paneId, filePath, x, y) { + _currentTabContext.filePath = filePath; + _currentTabContext.paneId = paneId; - // handle the option selection - dropdown.on("select", function (e, item) { - _handleSelection(item, filePath, paneId); - }); - - // Remove the button after the dropdown is hidden - dropdown.$button.css({ - display: "none" - }); + const contextMenu = Menus.getContextMenu("tabbar-context-menu"); + const event = $.Event("contextmenu", { pageX: x, pageY: y }); + contextMenu.open(event); } + /** - * Handles the selection of an option in the more options context menu - * - * @param {String} item - the item being selected - * @param {String} filePath - the path of the file that was right-clicked - * @param {String} paneId - the id of the pane ["first-pane", "second-pane"] + * this is the main function, it gets called only once on load from TabBar/main.js + * this registers the context menu and add the menu items inside it */ - function _handleSelection(item, filePath, paneId) { - switch (item) { - case Strings.CLOSE_TAB: - handleCloseTab(filePath, paneId); - break; - case Strings.CLOSE_TABS_TO_THE_LEFT: - handleCloseTabsToTheLeft(filePath, paneId); - break; - case Strings.CLOSE_TABS_TO_THE_RIGHT: - handleCloseTabsToTheRight(filePath, paneId); - break; - case Strings.CLOSE_ALL_TABS: - handleCloseAllTabs(paneId); - break; - case Strings.CLOSE_SAVED_TABS: - handleCloseSavedTabs(paneId); - break; - case Strings.CMD_FILE_RENAME: - handleFileRename(filePath); - break; - case Strings.CMD_FILE_DELETE: - handleFileDelete(filePath); - break; - case Strings.CMD_SHOW_IN_TREE: - handleShowInFileTree(filePath); - break; - case Strings.REOPEN_CLOSED_FILE: - reopenClosedFile(); - break; - } + function init() { + // these are the tab bar specific commands + CommandManager.register(Strings.CLOSE_TABS_TO_THE_LEFT, TABBAR_CLOSE_TABS_LEFT, handleCloseTabsToTheLeft); + CommandManager.register(Strings.CLOSE_TABS_TO_THE_RIGHT, TABBAR_CLOSE_TABS_RIGHT, handleCloseTabsToTheRight); + CommandManager.register(Strings.CLOSE_SAVED_TABS, TABBAR_CLOSE_SAVED_TABS, handleCloseSavedTabs); + CommandManager.register(Strings.CLOSE_ALL_TABS, TABBAR_CLOSE_ALL, handleCloseAllTabs); + + // these commands already exist for working files, just reusing them + const menu = Menus.registerContextMenu("tabbar-context-menu"); + menu.addMenuItem(Commands.FILE_CLOSE); + menu.addMenuItem(TABBAR_CLOSE_TABS_LEFT); + menu.addMenuItem(TABBAR_CLOSE_TABS_RIGHT); + menu.addMenuItem(TABBAR_CLOSE_SAVED_TABS); + menu.addMenuItem(TABBAR_CLOSE_ALL); + 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); + + // intercept clicks on existing commands + // this is done so that we can inject the right-clicked tab's context + menu.on("beforeContextMenuOpen", function () { + const $menu = $(`#${menu.id} > ul`); + if (!$menu.length) { return; } + + // for other commands, we have built-in context as those are our custom functions + const interceptors = [ + [Commands.FILE_CLOSE, handleCloseTab], + [Commands.FILE_RENAME, handleFileRename], + [Commands.FILE_DELETE, handleFileDelete], + [Commands.NAVIGATE_SHOW_IN_FILE_TREE, handleShowInFileTree] + ]; + + interceptors.forEach(([commandId, handler]) => { + $menu.find(`a[data-command="${commandId}"]`).off("click").on("click", function (e) { + e.stopPropagation(); + e.preventDefault(); + handler(); + menu.close(); + }); + }); + }); } module.exports = { - showMoreOptionsContextMenu + showMoreOptionsContextMenu, + init }; }); From e89b26b23c849e851d2edfb1ad987b6c950594bb Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 19 Nov 2025 11:30:48 +0530 Subject: [PATCH 02/11] fix: remove redundant strings from tab bar --- src/nls/root/strings.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 986e42901d..cf4475a86e 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -469,12 +469,10 @@ define({ "STATUSBAR_TASKS_RESTART": "Restart", // Tab bar Strings - "CLOSE_TAB": "Close Tab", "CLOSE_TABS_TO_THE_RIGHT": "Close Tabs to the Right", "CLOSE_TABS_TO_THE_LEFT": "Close Tabs to the Left", "CLOSE_ALL_TABS": "Close All Tabs", "CLOSE_SAVED_TABS": "Close Saved Tabs", - "REOPEN_CLOSED_FILE": "Reopen Closed File", // CodeInspection: errors/warnings "ERRORS_NO_FILE": "No File Open", From bb240e969a16a947c8379a6a32ddf64f7254d1c9 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 19 Nov 2025 11:46:57 +0530 Subject: [PATCH 03/11] fix: right clicks on tab is making the tab active --- src/extensionsIntegrated/TabBar/main.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index ae80c2015f..f80edba19a 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -512,6 +512,9 @@ define(function (require, exports, module) { // open tab on mousedown event $(document).on("mousedown", ".phoenix-tab-bar .tab", function (event) { + // to prevent right-clicks from making the tab active + if (event.button === 2) { return; } + if ($(event.target).hasClass("fa-times") || $(event.target).closest(".tab-close").length) { return; } From 594239088161545dd9623933461f953c5e8e4887 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 19 Nov 2025 14:43:59 +0530 Subject: [PATCH 04/11] fix: remove interceptor complexity, letting default handlers take care of the operation --- src/extensionsIntegrated/TabBar/main.js | 3 -- .../TabBar/more-options.js | 51 ------------------- 2 files changed, 54 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index f80edba19a..ae80c2015f 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -512,9 +512,6 @@ define(function (require, exports, module) { // open tab on mousedown event $(document).on("mousedown", ".phoenix-tab-bar .tab", function (event) { - // to prevent right-clicks from making the tab active - if (event.button === 2) { return; } - if ($(event.target).hasClass("fa-times") || $(event.target).closest(".tab-close").length) { return; } diff --git a/src/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js index 26014f71d3..b6df56139a 100644 --- a/src/extensionsIntegrated/TabBar/more-options.js +++ b/src/extensionsIntegrated/TabBar/more-options.js @@ -65,12 +65,6 @@ define(function (require, exports, module) { } } - // **Close** - // closes the right-clicked tab - function handleCloseTab() { - _executeWithFileContext(Commands.FILE_CLOSE, { paneId: _currentTabContext.paneId }); - } - // **Close All Tabs** // closes all tabs in the pane where the tab was right-clicked function handleCloseAllTabs() { @@ -120,27 +114,6 @@ define(function (require, exports, module) { } } - // **Rename** - // renames the right-clicked tab's file - function handleFileRename() { - CommandManager.execute(Commands.SHOW_SIDEBAR); - _executeWithFileContext(Commands.FILE_RENAME); - } - - // **Delete** - // deletes the right-clicked tab's file - function handleFileDelete() { - _executeWithFileContext(Commands.FILE_DELETE); - } - - // **Show in File Tree** - // shows the right-clicked tab's file in the file tree - function handleShowInFileTree() { - CommandManager.execute(Commands.SHOW_SIDEBAR); - _executeWithFileContext(Commands.NAVIGATE_SHOW_IN_FILE_TREE); - } - - /** * this function is called from Tabbar/main.js when a tab is right clicked * it is responsible to show the context menu and also set the currentTabContext @@ -183,30 +156,6 @@ define(function (require, exports, module) { menu.addMenuItem(Commands.NAVIGATE_SHOW_IN_FILE_TREE); menu.addMenuDivider(); menu.addMenuItem(Commands.FILE_REOPEN_CLOSED); - - // intercept clicks on existing commands - // this is done so that we can inject the right-clicked tab's context - menu.on("beforeContextMenuOpen", function () { - const $menu = $(`#${menu.id} > ul`); - if (!$menu.length) { return; } - - // for other commands, we have built-in context as those are our custom functions - const interceptors = [ - [Commands.FILE_CLOSE, handleCloseTab], - [Commands.FILE_RENAME, handleFileRename], - [Commands.FILE_DELETE, handleFileDelete], - [Commands.NAVIGATE_SHOW_IN_FILE_TREE, handleShowInFileTree] - ]; - - interceptors.forEach(([commandId, handler]) => { - $menu.find(`a[data-command="${commandId}"]`).off("click").on("click", function (e) { - e.stopPropagation(); - e.preventDefault(); - handler(); - menu.close(); - }); - }); - }); } module.exports = { From ee5bec7c539cbda237509f5a3559cf557399efc3 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 19 Nov 2025 15:16:52 +0530 Subject: [PATCH 05/11] feat: added add/remove to gitignore items in the tab bar context menu --- src/extensions/default/Git/src/Main.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/extensions/default/Git/src/Main.js b/src/extensions/default/Git/src/Main.js index 3b50a63ed7..1d05933d6d 100644 --- a/src/extensions/default/Git/src/Main.js +++ b/src/extensions/default/Git/src/Main.js @@ -344,27 +344,35 @@ define(function (require, exports) { var _toggleMenuEntriesState = false, _divider1 = null, - _divider2 = null; + _divider2 = null, + _divider3 = null; function toggleMenuEntries(bool) { if (bool === _toggleMenuEntriesState) { return; } var projectCmenu = Menus.getContextMenu(Menus.ContextMenuIds.PROJECT_MENU); var workingCmenu = Menus.getContextMenu(Menus.ContextMenuIds.WORKING_SET_CONTEXT_MENU); + var tabbarCmenu = Menus.getContextMenu("tabbar-context-menu"); if (bool) { _divider1 = projectCmenu.addMenuDivider(); _divider2 = workingCmenu.addMenuDivider(); + _divider3 = tabbarCmenu.addMenuDivider(); projectCmenu.addMenuItem(CMD_ADD_TO_IGNORE); workingCmenu.addMenuItem(CMD_ADD_TO_IGNORE); + tabbarCmenu.addMenuItem(CMD_ADD_TO_IGNORE); projectCmenu.addMenuItem(CMD_REMOVE_FROM_IGNORE); workingCmenu.addMenuItem(CMD_REMOVE_FROM_IGNORE); + tabbarCmenu.addMenuItem(CMD_REMOVE_FROM_IGNORE); } else { projectCmenu.removeMenuDivider(_divider1.id); workingCmenu.removeMenuDivider(_divider2.id); + tabbarCmenu.removeMenuDivider(_divider3.id); projectCmenu.removeMenuItem(CMD_ADD_TO_IGNORE); workingCmenu.removeMenuItem(CMD_ADD_TO_IGNORE); + tabbarCmenu.removeMenuItem(CMD_ADD_TO_IGNORE); projectCmenu.removeMenuItem(CMD_REMOVE_FROM_IGNORE); workingCmenu.removeMenuItem(CMD_REMOVE_FROM_IGNORE); + tabbarCmenu.removeMenuItem(CMD_REMOVE_FROM_IGNORE); } _toggleMenuEntriesState = bool; } From 5ad9e82a7b8b67c876df5deaa4eb7c159108a165 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 19 Nov 2025 20:49:49 +0530 Subject: [PATCH 06/11] feat: add git ignored support in tab bar --- .../default/Git/src/ProjectTreeMarks.js | 4 ++++ .../default/Git/src/TabBarIntegration.js | 17 ++++++++++++++++- src/extensionsIntegrated/TabBar/main.js | 11 ++++++++--- src/styles/Extn-TabBar.less | 18 ++++++++++++++++++ 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/extensions/default/Git/src/ProjectTreeMarks.js b/src/extensions/default/Git/src/ProjectTreeMarks.js index 966d4d5686..faeb6e1e23 100644 --- a/src/extensions/default/Git/src/ProjectTreeMarks.js +++ b/src/extensions/default/Git/src/ProjectTreeMarks.js @@ -201,4 +201,8 @@ define(function (require) { detachEvents(); }); + return { + isIgnored: isIgnored + }; + }); diff --git a/src/extensions/default/Git/src/TabBarIntegration.js b/src/extensions/default/Git/src/TabBarIntegration.js index 24453f3166..0336cf6a87 100644 --- a/src/extensions/default/Git/src/TabBarIntegration.js +++ b/src/extensions/default/Git/src/TabBarIntegration.js @@ -3,6 +3,7 @@ define(function (require) { const Events = require("src/Events"); const Git = require("src/git/Git"); const Preferences = require("src/Preferences"); + const ProjectTreeMarks = require("src/ProjectTreeMarks"); // the cache of file statuses by path let fileStatusCache = {}; @@ -55,6 +56,19 @@ define(function (require) { ); } + /** + * whether the file is gitignored or not + * + * @param {string} fullPath - the file path + * @returns {boolean} - if the file is gitignored it returns true otherwise false + */ + function isIgnored(fullPath) { + if (!ProjectTreeMarks || !ProjectTreeMarks.isIgnored) { + return false; + } + return ProjectTreeMarks.isIgnored(fullPath); + } + // Update file status cache when Git status results are received EventEmitter.on(Events.GIT_STATUS_RESULTS, function (files) { @@ -85,6 +99,7 @@ define(function (require) { return { getFileStatus: getFileStatus, isModified: isModified, - isUntracked: isUntracked + isUntracked: isUntracked, + isIgnored: isIgnored }; }); diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index ae80c2015f..fd6ae9975e 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -150,9 +150,14 @@ define(function (require, exports, module) { const TabBarIntegration = window.phoenixGitEvents.TabBarIntegration; // find the Git status - // if untracked we add the git-new class and U char - // if modified we add the git-modified class and M char - if (TabBarIntegration.isUntracked(entry.path)) { + // Check ignored FIRST (takes precedence over other statuses) + // if ignored we add the git-ignored class + // if untracked we add the git-new class + // if modified we add the git-modified class + if (TabBarIntegration.isIgnored(entry.path)) { + gitStatus = "Ignored"; + gitStatusClass = "git-ignored"; + } else if (TabBarIntegration.isUntracked(entry.path)) { gitStatus = "Untracked"; gitStatusClass = "git-new"; } else if (TabBarIntegration.isModified(entry.path)) { diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 912425a9f7..4cf1ce91aa 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -460,3 +460,21 @@ .dark .tab.git-new > .tab-icon:before { color: #91CC41; } + +.tab.git-ignored .tab-name { + color: #868888 !important; + font-style: italic; +} + +.tab.git-ignored .tab-dirname { + color: #868888 !important; + font-style: italic; +} + +.dark .tab.git-ignored .tab-name { + color: #868888 !important; +} + +.dark .tab.git-ignored .tab-dirname { + color: #868888 !important; +} From e9769324307c2bdf95012032395091edba9b7ea7 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 19 Nov 2025 21:09:26 +0530 Subject: [PATCH 07/11] refactor: placeholder tabs a bit more grayish to differentiate between gitignored tabs --- src/styles/Extn-TabBar.less | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 4cf1ce91aa..59655a39fd 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -256,19 +256,19 @@ .tab.placeholder .tab-name { font-style: italic; - color: #888; + color: #999; } .dark .tab.placeholder .tab-name { - color: #888; + color: #999; } .tab.placeholder.active .tab-name { - color: #666; + color: #777; } .dark .tab.placeholder.active .tab-name { - color: #aaa; + color: #bbb; } .tab.placeholder::after { From 5ec73fe4d4d19a04c1e9b39c0cee94be68e62f19 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 19 Nov 2025 21:43:59 +0530 Subject: [PATCH 08/11] fix: context menu opening on tab right click test failing --- test/spec/Extn-Tabbar-integ-test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/spec/Extn-Tabbar-integ-test.js b/test/spec/Extn-Tabbar-integ-test.js index 9292bce749..8fe50552c3 100644 --- a/test/spec/Extn-Tabbar-integ-test.js +++ b/test/spec/Extn-Tabbar-integ-test.js @@ -1837,7 +1837,7 @@ define(function (require, exports, module) { * @returns {jQuery} - The context menu element */ function getContextMenu() { - return $(".tabbar-context-menu"); + return $("#tabbar-context-menu"); } it("should open context menu when right-clicking on a tab", async function () { @@ -1846,22 +1846,22 @@ define(function (require, exports, module) { expect($tab.length).toBe(1); // Simulate a right-click (contextmenu) event on the tab - $tab.trigger("contextmenu", { + const event = $.Event("contextmenu", { pageX: 100, pageY: 100 }); + $tab.trigger(event); // Wait for the context menu to appear await awaitsFor( function () { - return getContextMenu().length > 0; + return getContextMenu().hasClass("open"); }, "Context menu to appear" ); - // Verify the context menu is visible - expect(getContextMenu().length).toBe(1); - expect(getContextMenu().is(":visible")).toBe(true); + // Verify the context menu is open + expect(getContextMenu().hasClass("open")).toBe(true); // Clean up - close the context menu by clicking elsewhere $("body").click(); @@ -1869,7 +1869,7 @@ define(function (require, exports, module) { // Wait for the context menu to disappear await awaitsFor( function () { - return getContextMenu().length === 0; + return !getContextMenu().hasClass("open"); }, "Context menu to disappear" ); From 9eea283c7de720b36b51dacb37a04bdd1561e42f Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 19 Nov 2025 22:13:07 +0530 Subject: [PATCH 09/11] fix: close tab context menu test failing --- test/spec/Extn-Tabbar-integ-test.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/test/spec/Extn-Tabbar-integ-test.js b/test/spec/Extn-Tabbar-integ-test.js index 8fe50552c3..68dac01121 100644 --- a/test/spec/Extn-Tabbar-integ-test.js +++ b/test/spec/Extn-Tabbar-integ-test.js @@ -1875,30 +1875,41 @@ define(function (require, exports, module) { ); }); - it("should close the tab when selecting 'Close Tab' from context menu", async function () { + it("should close the tab when selecting 'Close' from context menu", async function () { // Get the tab element const $tab = getTab(testFilePath); // Right-click on the tab to open context menu - $tab.trigger("contextmenu", { + // First trigger mousedown to make the tab active + const mousedownEvent = $.Event("mousedown", { + button: 2, + pageX: 100, + pageY: 100 + }); + $tab.trigger(mousedownEvent); + + // Then trigger contextmenu to open the menu + const contextmenuEvent = $.Event("contextmenu", { pageX: 100, pageY: 100 }); + $tab.trigger(contextmenuEvent); // Wait for context menu to appear await awaitsFor( function () { - return getContextMenu().length > 0; + return getContextMenu().hasClass("open"); }, "Context menu to appear" ); - // Find and click the "Close Tab" option + // Find and click the "Close" option const $closeTabOption = getContextMenu() - .find("a.stylesheet-link") + .find(".menu-name") .filter(function () { - return $(this).text().trim() === Strings.CLOSE_TAB; - }); + return $(this).text().trim() === "Close"; + }) + .closest("li"); expect($closeTabOption.length).toBe(1); $closeTabOption.click(); From 85c0d2e9eff7e2930a32555c32a5ec44321d23c0 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 19 Nov 2025 22:21:28 +0530 Subject: [PATCH 10/11] fix: custom function tests for tab bar failing --- test/spec/Extn-Tabbar-integ-test.js | 78 +++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 21 deletions(-) diff --git a/test/spec/Extn-Tabbar-integ-test.js b/test/spec/Extn-Tabbar-integ-test.js index 68dac01121..1293801e96 100644 --- a/test/spec/Extn-Tabbar-integ-test.js +++ b/test/spec/Extn-Tabbar-integ-test.js @@ -1907,7 +1907,7 @@ define(function (require, exports, module) { const $closeTabOption = getContextMenu() .find(".menu-name") .filter(function () { - return $(this).text().trim() === "Close"; + return $(this).text().trim() === Strings.CMD_FILE_CLOSE; }) .closest("li"); expect($closeTabOption.length).toBe(1); @@ -1957,25 +1957,34 @@ define(function (require, exports, module) { const $tab = getTab(testFilePath); // Right-click on the first tab to open context menu - $tab.trigger("contextmenu", { + const mousedownEvent = $.Event("mousedown", { + button: 2, + pageX: 100, + pageY: 100 + }); + $tab.trigger(mousedownEvent); + + const contextmenuEvent = $.Event("contextmenu", { pageX: 100, pageY: 100 }); + $tab.trigger(contextmenuEvent); // Wait for context menu to appear await awaitsFor( function () { - return getContextMenu().length > 0; + return getContextMenu().hasClass("open"); }, "Context menu to appear" ); - // Find and click the "Close tabs to the right" option + // Find and click the "Close Tabs to the Right" option const $closeTabsToRightOption = getContextMenu() - .find("a.stylesheet-link") + .find(".menu-name") .filter(function () { return $(this).text().trim() === Strings.CLOSE_TABS_TO_THE_RIGHT; - }); + }) + .closest("li"); expect($closeTabsToRightOption.length).toBe(1); $closeTabsToRightOption.click(); @@ -2029,25 +2038,34 @@ define(function (require, exports, module) { const $tab = getTab(testFilePath3); // Right-click on the third tab to open context menu - $tab.trigger("contextmenu", { + const mousedownEvent = $.Event("mousedown", { + button: 2, + pageX: 100, + pageY: 100 + }); + $tab.trigger(mousedownEvent); + + const contextmenuEvent = $.Event("contextmenu", { pageX: 100, pageY: 100 }); + $tab.trigger(contextmenuEvent); // Wait for context menu to appear await awaitsFor( function () { - return getContextMenu().length > 0; + return getContextMenu().hasClass("open"); }, "Context menu to appear" ); - // Find and click the "Close tabs to the left" option + // Find and click the "Close Tabs to the Left" option const $closeTabsToLeftOption = getContextMenu() - .find("a.stylesheet-link") + .find(".menu-name") .filter(function () { return $(this).text().trim() === Strings.CLOSE_TABS_TO_THE_LEFT; - }); + }) + .closest("li"); expect($closeTabsToLeftOption.length).toBe(1); $closeTabsToLeftOption.click(); @@ -2116,25 +2134,34 @@ define(function (require, exports, module) { const $tab = getTab(testFilePath); // Right-click on the tab to open context menu - $tab.trigger("contextmenu", { + const mousedownEvent = $.Event("mousedown", { + button: 2, + pageX: 100, + pageY: 100 + }); + $tab.trigger(mousedownEvent); + + const contextmenuEvent = $.Event("contextmenu", { pageX: 100, pageY: 100 }); + $tab.trigger(contextmenuEvent); // Wait for context menu to appear await awaitsFor( function () { - return getContextMenu().length > 0; + return getContextMenu().hasClass("open"); }, "Context menu to appear" ); - // Find and click the "Close saved tabs" option + // Find and click the "Close Saved Tabs" option const $closeSavedTabsOption = getContextMenu() - .find("a.stylesheet-link") + .find(".menu-name") .filter(function () { return $(this).text().trim() === Strings.CLOSE_SAVED_TABS; - }); + }) + .closest("li"); expect($closeSavedTabsOption.length).toBe(1); $closeSavedTabsOption.click(); @@ -2195,25 +2222,34 @@ define(function (require, exports, module) { const $tab = getTab(testFilePath); // Right-click on the tab to open context menu - $tab.trigger("contextmenu", { + const mousedownEvent = $.Event("mousedown", { + button: 2, + pageX: 100, + pageY: 100 + }); + $tab.trigger(mousedownEvent); + + const contextmenuEvent = $.Event("contextmenu", { pageX: 100, pageY: 100 }); + $tab.trigger(contextmenuEvent); // Wait for context menu to appear await awaitsFor( function () { - return getContextMenu().length > 0; + return getContextMenu().hasClass("open"); }, "Context menu to appear" ); - // Find and click the "Close all tabs" option + // Find and click the "Close All Tabs" option const $closeAllTabsOption = getContextMenu() - .find("a.stylesheet-link") + .find(".menu-name") .filter(function () { return $(this).text().trim() === Strings.CLOSE_ALL_TABS; - }); + }) + .closest("li"); expect($closeAllTabsOption.length).toBe(1); $closeAllTabsOption.click(); From cbff5a134051d6bc2ba27ad36b446f329b1f4063 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 20 Nov 2025 00:13:10 +0530 Subject: [PATCH 11/11] feat: learn more button in snippets panel now redirects to its docs --- src/nls/root/strings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index cf4475a86e..ed03f8e14b 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1662,7 +1662,7 @@ define({ "CUSTOM_SNIPPETS_SAVE": "Save", "CUSTOM_SNIPPETS_NO_DESCRIPTION": "No description", "CUSTOM_SNIPPETS_NO_MATCHES": "No snippets match \"{0}\"", - "CUSTOM_SNIPPETS_LEARN_MORE": "Add your own code hints to speed up coding - Learn More", + "CUSTOM_SNIPPETS_LEARN_MORE": "Add your own code hints to speed up coding - Learn More", "CUSTOM_SNIPPETS_DUPLICATE_ERROR": "A snippet with abbreviation \"{0}\" already exists.", "CUSTOM_SNIPPETS_SPACE_ERROR": "Space is not accepted as a valid abbreviation character.", "CUSTOM_SNIPPETS_ABBR_LENGTH_ERROR": "Abbreviation cannot be more than 30 characters.",