From 4b31e6372d26380ce18f31662a26f814c0ec7034 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 2 Mar 2025 02:01:12 +0530 Subject: [PATCH 01/35] feat: tab bar working prototype --- src/extensionsIntegrated/TabBar/helper.js | 136 ++++++++ .../TabBar/html/tabbar-pane.html | 5 + .../TabBar/html/tabbar-second-pane.html | 5 + src/extensionsIntegrated/TabBar/main.js | 299 ++++++++++++++++++ src/extensionsIntegrated/TabBar/preference.js | 43 +++ src/extensionsIntegrated/loader.js | 1 + src/nls/root/strings.js | 5 + src/styles/Extn-TabBar.less | 86 +++++ src/styles/brackets.less | 1 + 9 files changed, 581 insertions(+) create mode 100644 src/extensionsIntegrated/TabBar/helper.js create mode 100644 src/extensionsIntegrated/TabBar/html/tabbar-pane.html create mode 100644 src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html create mode 100644 src/extensionsIntegrated/TabBar/main.js create mode 100644 src/extensionsIntegrated/TabBar/preference.js create mode 100644 src/styles/Extn-TabBar.less diff --git a/src/extensionsIntegrated/TabBar/helper.js b/src/extensionsIntegrated/TabBar/helper.js new file mode 100644 index 0000000000..64a4f3fed4 --- /dev/null +++ b/src/extensionsIntegrated/TabBar/helper.js @@ -0,0 +1,136 @@ +define(function (require, exports, module) { + + const WorkspaceManager = require("view/WorkspaceManager"); + const DocumentManager = require("document/DocumentManager"); + const ViewUtils = require("utils/ViewUtils"); + const WorkingSetView = require("project/WorkingSetView"); + const FileUtils = require("file/FileUtils"); + + + /** + * Shows the tab bar, when its hidden. + * Its only shown when tab bar is enabled and there is atleast one working file + * + * @param {$.Element} $tabBar - The tab bar element + */ + function _showTabBar($tabBar) { + if ($tabBar) { + $tabBar.show(); + // when we add/remove something from the editor, the editor shifts up/down which leads to blank space + // so we need to recompute the layout to make sure things are in the right place + WorkspaceManager.recomputeLayout(true); + } + } + + /** + * Hides the tab bar. + * Its hidden when tab bar feature is disabled or there are no working files + * + * @param {$.Element} $tabBar - The tab bar element + */ + function _hideTabBar($tabBar) { + if ($tabBar) { + $tabBar.hide(); + WorkspaceManager.recomputeLayout(true); + } + } + + + /** + * Entry is a single object that comes from MainViewManager's getWorkingSet + * We extract the required data from the entry + * + * @param {Object} entry - A single file entry from MainViewManager.getWorkingSet() + * @returns {Object} - the required data + */ + function _getRequiredDataFromEntry(entry) { + return { + path: entry.fullPath, + name: entry.name, + isFile: entry.isFile, + isDirty: entry.isDirty, + isPinned: entry.isPinned, + displayName: entry.name // Initialize displayName with name, it will be updated if duplicates are found + }; + } + + /** + * checks whether a file is dirty or not + * + * @param {File} file - the file to check + * @return {boolean} true if the file is dirty, false otherwise + */ + function _isFileModified(file) { + const doc = DocumentManager.getOpenDocumentForPath(file.fullPath); + return doc && doc.isDirty; + } + + + /** + * Returns a jQuery object containing the file icon for a given file + * + * @param {Object} fileData - The file data object + * @returns {jQuery} jQuery object containing the file icon + */ + function _getFileIcon(fileData) { + const $link = $("").html( + ViewUtils.getFileEntryDisplay({ name: fileData.name }) + ); + WorkingSetView.useIconProviders({ + fullPath: fileData.path, + name: fileData.name, + isFile: true + }, $link); + return $link.children().first(); + } + + + /** + * Checks for duplicate file names in the working set and updates displayName accordingly + * if duplicate file names are found, we update the displayName to include the directory + * + * @param {Array} workingSet - The working set to check for duplicates + */ + function _handleDuplicateFileNames(workingSet) { + // Create a map to track filename occurrences + const fileNameCount = {}; + + // First, count occurrences of each filename + workingSet.forEach(entry => { + if (!fileNameCount[entry.name]) { + fileNameCount[entry.name] = 1; + } else { + fileNameCount[entry.name]++; + } + }); + + // Then, update the displayName for files with duplicate names + workingSet.forEach(entry => { + if (fileNameCount[entry.name] > 1) { + // Get the parent directory name + const path = entry.path; + const parentDir = FileUtils.getDirectoryPath(path); + + // Get just the directory name, not the full path + const dirName = parentDir.split("/"); + // Get the parent directory name (second-to-last part of the path) + const parentDirName = dirName[dirName.length - 2] || ""; + + // Set the display name to include the parent directory + entry.displayName = parentDirName + "/" + entry.name; + } else { + entry.displayName = entry.name; + } + }); + } + + + module.exports = { + _showTabBar, + _hideTabBar, + _getRequiredDataFromEntry, + _isFileModified, + _getFileIcon, + _handleDuplicateFileNames + }; +}); diff --git a/src/extensionsIntegrated/TabBar/html/tabbar-pane.html b/src/extensionsIntegrated/TabBar/html/tabbar-pane.html new file mode 100644 index 0000000000..60fe28e430 --- /dev/null +++ b/src/extensionsIntegrated/TabBar/html/tabbar-pane.html @@ -0,0 +1,5 @@ +
+
+ +
+
\ No newline at end of file diff --git a/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html b/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html new file mode 100644 index 0000000000..ce20e5eace --- /dev/null +++ b/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html @@ -0,0 +1,5 @@ +
+
+ +
+
\ No newline at end of file diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js new file mode 100644 index 0000000000..e5ab6e795b --- /dev/null +++ b/src/extensionsIntegrated/TabBar/main.js @@ -0,0 +1,299 @@ +define(function (require, exports, module) { + const AppInit = require("utils/AppInit"); + const MainViewManager = require("view/MainViewManager"); + const EditorManager = require("editor/EditorManager"); + const FileSystem = require("filesystem/FileSystem"); + const PreferencesManager = require("preferences/PreferencesManager"); + const CommandManager = require("command/CommandManager"); + const Commands = require("command/Commands"); + + + const Helper = require("./helper"); + const Preference = require("./preference"); + const TabBarHTML = require("text!./html/tabbar-pane.html"); + const TabBarHTML2 = require("text!./html/tabbar-second-pane.html"); + + + /** + * This array's represents the current working set + * It holds all the working set items that are to be displayed in the tab bar + * Properties of each object: + * path: {String} full path of the file + * name: {String} name of the file + * isFile: {Boolean} whether the file is a file or a directory + * isDirty: {Boolean} whether the file is dirty + * isPinned: {Boolean} whether the file is pinned + * displayName: {String} name to display in the tab (may include directory info for duplicate files) + */ + let firstPaneWorkingSet = []; + let secondPaneWorkingSet = []; + + + /** + * This holds the tab bar element + * For tab bar structure, refer to `./html/tabbar-pane.html` and `./html/tabbar-second-pane.html` + * $tabBar is for the first pane and $tabBar2 is for the second pane + * + * @type {$.Element} + */ + let $tabBar = null; + let $tabBar2 = null; + + + /** + * This function is responsible to take all the files from the working set and gets the working sets ready + * This is placed here instead of helper.js because it modifies the working sets + */ + function getAllFilesFromWorkingSet() { + firstPaneWorkingSet = []; + secondPaneWorkingSet = []; + + // this gives the list of panes. When both panes are open, it will be ['first-pane', 'second-pane'] + const paneList = MainViewManager.getPaneIdList(); + + // to make sure atleast one pane is open + if (paneList && paneList.length > 0) { + + // this gives the working set of the first pane + const currFirstPaneWorkingSet = MainViewManager.getWorkingSet(paneList[0]); + + for (let i = 0; i < currFirstPaneWorkingSet.length; i++) { + // MainViewManager.getWorkingSet gives the working set of the first pane, + // but it has lot of details we don't need. Hence we use Helper._getRequiredDataFromEntry + firstPaneWorkingSet.push(Helper._getRequiredDataFromEntry(currFirstPaneWorkingSet[i])); + } + // if there are duplicate file names, we update the displayName to include the directory + Helper._handleDuplicateFileNames(firstPaneWorkingSet); + + // check if second pane is open + if (paneList.length > 1) { + const currSecondPaneWorkingSet = MainViewManager.getWorkingSet(paneList[1]); + + for (let i = 0; i < currSecondPaneWorkingSet.length; i++) { + secondPaneWorkingSet.push(Helper._getRequiredDataFromEntry(currSecondPaneWorkingSet[i])); + } + Helper._handleDuplicateFileNames(secondPaneWorkingSet); + + } + } + } + + + + /** + * Responsible for creating the tab element + * Note: this creates a tab (for a single file) not the tab bar + * + * @param {Object} entry - the working set entry + * @returns {$.Element} the tab element + */ + function createTab(entry) { + if (!$tabBar) { + return; + } + + // set up all the necessary properties + const activeEditor = EditorManager.getActiveEditor(); + const activePath = activeEditor ? activeEditor.document.file.fullPath : null; + const isActive = entry.path === activePath; // if the file is the currently active file + const isDirty = Helper._isFileModified(FileSystem.getFileForPath(entry.path)); // if the file is dirty + + // Create the tab element with the structure we need + // tab name is written as a separate div because it may include directory info which we style differently + const $tab = $( + `
+
+
+
`); + + // Add the file icon + const $icon = Helper._getFileIcon(entry); + $tab.find('.tab-icon').append($icon); + + // Check if we have a directory part in the displayName + const $tabName = $tab.find('.tab-name'); + if (entry.displayName && entry.displayName !== entry.name) { + // Split the displayName into directory and filename parts + const parts = entry.displayName.split('/'); + const dirName = parts[0]; + const fileName = parts[1]; + + // create the HTML for different styling for directory and filename + $tabName.html(`${dirName}/${fileName}`); + } else { + // Just the filename + $tabName.text(entry.name); + } + + return $tab; + } + + + /** + * Sets up the tab bar + */ + function setupTabBar() { + // this populates the working sets + getAllFilesFromWorkingSet(); + + // make sure there is atleast one file in the first pane working set + if (firstPaneWorkingSet.length > 0) { + for (let i = 0; i < firstPaneWorkingSet.length; i++) { + // Note: here we add the element to the tab bar directly and not the tab-container + $('#phoenix-tab-bar').append(createTab(firstPaneWorkingSet[i])); + } + } + + if (secondPaneWorkingSet.length > 0) { + for (let i = 0; i < secondPaneWorkingSet.length; i++) { + $('#phoenix-tab-bar-2').append(createTab(secondPaneWorkingSet[i])); + } + } + } + + + /** + * Creates the tab bar and adds it to the DOM + */ + function createTabBar() { + if (!Preference.tabBarEnabled || !EditorManager.getActiveEditor()) { + return; + } + + // clean up any existing tab bars first and start fresh + cleanupTabBar(); + + if ($('.not-editor').length === 1) { + $tabBar = $(TabBarHTML); + // since we need to add the tab bar before the editor area, we target the `.not-editor` class + $(".not-editor").before($tabBar); + } else if ($('.not-editor').length === 2) { + $tabBar = $(TabBarHTML); + $tabBar2 = $(TabBarHTML2); + + // eq(0) is for the first pane and eq(1) is for the second pane + $(".not-editor").eq(0).before($tabBar); + $(".not-editor").eq(1).before($tabBar2); + } + + setupTabBar(); + } + + + /** + * Removes existing tab bar and cleans up + */ + function cleanupTabBar() { + if ($tabBar) { + $tabBar.remove(); + $tabBar = null; + } + if ($tabBar2) { + $tabBar2.remove(); + $tabBar2 = null; + } + // Also check for any orphaned tab bars that might exist + $(".tab-bar-container").remove(); + } + + + /** + * When any change is made to the working set, we just recreate the tab bar + * The changes may be adding/removing a file or changing the active file + */ + function workingSetChanged() { + cleanupTabBar(); + createTabBar(); + } + + + /** + * handle click events on the tabs to open the file + */ + function handleTabClick() { + + // delegate event handling for both tab bars + $(document).on("click", ".tab", function (event) { + // Skip if this is a click on a close button or other control + if ($(event.target).hasClass('tab-close') || $(event.target).closest('.tab-close').length) { + return; + } + + // Get the file path from the data-path attribute + const filePath = $(this).attr("data-path"); + + if (filePath) { + // we need to determine which pane the tab belongs to + const isSecondPane = $(this).closest("#phoenix-tab-bar-2").length > 0; + const paneId = isSecondPane ? "second-pane" : "first-pane"; + + // Set the active pane and open the file + MainViewManager.setActivePaneId(paneId); + CommandManager.execute(Commands.FILE_OPEN, { fullPath: filePath }); + + // Prevent default behavior + event.preventDefault(); + event.stopPropagation(); + } + }); + } + + + /** + * Registers the event handlers + */ + function registerHandlers() { + + // pane handlers + MainViewManager.off("activePaneChange paneCreate paneDestroy paneLayoutChange", createTabBar); + MainViewManager.on("activePaneChange paneCreate paneDestroy paneLayoutChange", createTabBar); + + + // editor handlers + EditorManager.off("activeEditorChange", createTabBar); + EditorManager.on("activeEditorChange", createTabBar); + + // when working set changes, recreate the tab bar + MainViewManager.on("workingSetAdd", workingSetChanged); + MainViewManager.on("workingSetRemove", workingSetChanged); + MainViewManager.on("workingSetListChange", workingSetChanged); + + } + + + /** + * This is called when the tab bar preference is changed + * It takes care of creating or cleaning up the tab bar + * + * TODO: handle the number of tabs functionality + */ + function preferenceChanged() { + Preference.tabBarEnabled = PreferencesManager.get(Preference.PREFERENCES_TAB_BAR).showTabBar; + Preference.tabBarNumberOfTabs = PreferencesManager.get(Preference.PREFERENCES_TAB_BAR).numberOfTabs; + + if (Preference.tabBarEnabled) { + createTabBar(); + } else { + cleanupTabBar(); + } + } + + + + AppInit.appReady(function () { + + PreferencesManager.on("change", Preference.PREFERENCES_TAB_BAR, preferenceChanged); + // calling preference changed here itself to check if the tab bar is enabled, + // because if it is enabled, preferenceChange automatically calls createTabBar + preferenceChanged(); + + // this should be called at the last as everything should be setup before registering handlers + registerHandlers(); + + // handle when a single tab gets clicked + handleTabClick(); + }); +}); diff --git a/src/extensionsIntegrated/TabBar/preference.js b/src/extensionsIntegrated/TabBar/preference.js new file mode 100644 index 0000000000..3602a8eb9f --- /dev/null +++ b/src/extensionsIntegrated/TabBar/preference.js @@ -0,0 +1,43 @@ + +/* + * This script contains the preference settings for the tab bar. + * TODO: It also will have the tab bar options that will be added in menu bar and other places. + */ +define(function (require, exports, module) { + const PreferencesManager = require("preferences/PreferencesManager"); + const Strings = require("strings"); + + + // Preference settings + const PREFERENCES_TAB_BAR = "tabBar.options"; // Preference name + let tabBarEnabled = true; // preference to check if the tab bar is enabled + let tabBarNumberOfTabs = -1; // preference to check the number of tabs, -1 means all tabs + + + PreferencesManager.definePreference( + PREFERENCES_TAB_BAR, + "object", + { showTabBar: true, numberOfTabs: -1 }, + { + description: Strings.DESCRIPTION_TABBAR, + keys: { + showTabBar: { + type: "boolean", + description: Strings.DESCRIPTION_SHOW_TABBAR, + initial: true + }, + numberOfTabs: { + type: "number", + description: Strings.DESCRIPTION_NUMBER_OF_TABS, + initial: -1 + } + } + }); + + + module.exports = { + PREFERENCES_TAB_BAR, + tabBarEnabled, + tabBarNumberOfTabs + }; +}); diff --git a/src/extensionsIntegrated/loader.js b/src/extensionsIntegrated/loader.js index 337e3f82f9..2da9f10f68 100644 --- a/src/extensionsIntegrated/loader.js +++ b/src/extensionsIntegrated/loader.js @@ -43,4 +43,5 @@ define(function (require, exports, module) { require("./HtmlTagSyncEdit/main"); require("./indentGuides/main"); require("./CSSColorPreview/main"); + require("./TabBar/main"); }); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 8db12e1a1a..51585f9f41 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1268,6 +1268,11 @@ define({ // Emmet "DESCRIPTION_EMMET": "true to enable Emmet, else false.", + // Tabbar + "DESCRIPTION_TABBAR": "Set the tab bar settings.", + "DESCRIPTION_SHOW_TABBAR": "true to show the tab bar, else false.", + "DESCRIPTION_NUMBER_OF_TABS": "The number of tabs to show in the tab bar. Set to -1 to show all tabs", + // Git extension "ENABLE_GIT": "Enable Git", "ACTION": "Action", diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less new file mode 100644 index 0000000000..b7cc9cf724 --- /dev/null +++ b/src/styles/Extn-TabBar.less @@ -0,0 +1,86 @@ +.tab-bar-container { + position: relative; + background-color: #1E1E1E; + border-bottom: 1px solid #333; +} + +.phoenix-tab-bar { + height: 28px; + background-color: #1E1E1E; + display: flex; + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; + transition: height 0.3s ease; +} + +.phoenix-tab-bar .hover { + background-color: #333; +} + +.tab { + display: inline-flex; + align-items: center; + padding: 0 12px; + padding-top: 1px; + height: 100%; + background-color: #2D2D2D; + border-right: 1px solid #333; + cursor: pointer; + position: relative; + flex: 0 0 auto; + min-width: fit-content; +} + +.tab-icon { + display: flex; + align-items: center; + margin-bottom: -2px; +} + +.tab-name { + display: inline-flex; + align-items: center; + font-size: 12px; + letter-spacing: 0.4px; + word-spacing: 0.75px; +} + +.tab .tab-dirname { + font-size: 10px; + opacity: 0.7; + font-weight: normal; +} + +.tab.active { + background-color: #3D3D3D; +} + +.tab:hover { + background-color: #4d4949; +} + +.tab.active::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background-color: #75BEFF; +} + +.tab.dirty::before { + content: "•"; + color: #8D8D8E; + font-size: 26px; + margin-right: 4px; + margin-top: -8px; + position: absolute; + left: 4px; + top: 8px; +} + +.tab.dirty .tab-icon { + margin-left: 10px; +} \ No newline at end of file diff --git a/src/styles/brackets.less b/src/styles/brackets.less index e9d547bf1d..6b4b43a611 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -41,6 +41,7 @@ // Integrated extensions import @import "Extn-NavigationAndHistory.less"; @import "Extn-RecentProjects.less"; +@import "Extn-TabBar.less"; @import "Extn-DisplayShortcuts.less"; @import "Extn-CSSColorPreview.less"; From f3799759fdbafd4cb578d0828afb5e61fd27138b Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 2 Mar 2025 02:41:49 +0530 Subject: [PATCH 02/35] feat: add close button in tabs to remove the tab --- src/extensionsIntegrated/TabBar/main.js | 65 +++++++++++++++++++++++-- src/styles/Extn-TabBar.less | 27 +++++++++- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index e5ab6e795b..5f23ad7048 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -101,12 +101,12 @@ define(function (require, exports, module) { // Create the tab element with the structure we need // tab name is written as a separate div because it may include directory info which we style differently const $tab = $( - `
+
`); // Add the file icon @@ -211,14 +211,66 @@ define(function (require, exports, module) { /** - * handle click events on the tabs to open the file + * Handle close button click on tabs + * This function will remove the file from the working set + * + * @param {String} filePath - path of the file to close + */ + function handleTabClose(filePath) { + // Logic: First open the file we want to close, then close it and finally restore focus + // Why? Because FILE_CLOSE removes the currently active file from the working set + + // Get the current active editor to restore focus later + const currentActiveEditor = EditorManager.getActiveEditor(); + const currentActivePath = currentActiveEditor ? currentActiveEditor.document.file.fullPath : null; + + // Only need to open the file first if it's not the currently active one + if (currentActivePath !== filePath) { + // open the file we want to close + CommandManager.execute(Commands.FILE_OPEN, { fullPath: filePath }) + .done(function () { + // close it + CommandManager.execute(Commands.FILE_CLOSE) + .done(function () { + // If we had a different file active before, restore focus to it + if (currentActivePath && currentActivePath !== filePath) { + CommandManager.execute(Commands.FILE_OPEN, { fullPath: currentActivePath }); + } + }) + .fail(function (error) { + console.error("Failed to close file:", filePath, error); + }); + }) + .fail(function (error) { + console.error("Failed to open file for closing:", filePath, error); + }); + } else { + // if it's already the active file, just close it + CommandManager.execute(Commands.FILE_CLOSE) + .fail(function (error) { + console.error("Failed to close file:", filePath, error); + }); + } + } + + + /** + * handle click events on the tabs to open the file or close the tab */ function handleTabClick() { // delegate event handling for both tab bars $(document).on("click", ".tab", function (event) { - // Skip if this is a click on a close button or other control - if ($(event.target).hasClass('tab-close') || $(event.target).closest('.tab-close').length) { + // check if the clicked element is the close button + if ($(event.target).hasClass('fa-xmark') || $(event.target).closest('.tab-close').length) { + // Get the file path from the data-path attribute of the parent tab + const filePath = $(this).attr("data-path"); + if (filePath) { + handleTabClose(filePath); + // Prevent default behavior + event.preventDefault(); + event.stopPropagation(); + } return; } @@ -297,3 +349,6 @@ define(function (require, exports, module) { handleTabClick(); }); }); + + +// TODO: Bug (when we have two panes and one pane gets empty by closing all files in it, the other pane tab bar also gets removed) \ No newline at end of file diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index b7cc9cf724..30812e7098 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -21,7 +21,7 @@ .tab { display: inline-flex; align-items: center; - padding: 0 12px; + padding: 0 8px; padding-top: 1px; height: 100%; background-color: #2D2D2D; @@ -83,4 +83,29 @@ .tab.dirty .tab-icon { margin-left: 10px; +} + +.tab-close { + font-size: 12px; + font-weight: lighter; + padding: 1px 4px 0.4px 4px; + margin-left: 3px; + color: #808080; + transition: all 0.2s ease; + border-radius: 3px; + visibility: hidden; + opacity: 0; + pointer-events: none; +} + +.tab:hover .tab-close, +.tab.active .tab-close { + visibility: visible; + opacity: 1; + pointer-events: auto; +} + +.tab-close:hover { + color: #CCCCCC; + background-color: rgba(255, 255, 255, 0.1); } \ No newline at end of file From 62ac120f8b4d6b8d6b444fe726b5947a6a260d48 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 2 Mar 2025 15:27:36 +0530 Subject: [PATCH 03/35] fix: tab bar for both panes gets closed when one pane tab bar gets empty --- src/extensionsIntegrated/TabBar/main.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 5f23ad7048..4ca1c70414 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -139,7 +139,16 @@ define(function (require, exports, module) { // this populates the working sets getAllFilesFromWorkingSet(); - // make sure there is atleast one file in the first pane working set + // if no files are present in a pane, we want to hide the tab bar for that pane + if(firstPaneWorkingSet.length === 0) { + Helper._hideTabBar($('#phoenix-tab-bar')); + } + + if(secondPaneWorkingSet.length === 0) { + Helper._hideTabBar($('#phoenix-tab-bar-2')); + } + + // to add tabs one by one to the tab bar if (firstPaneWorkingSet.length > 0) { for (let i = 0; i < firstPaneWorkingSet.length; i++) { // Note: here we add the element to the tab bar directly and not the tab-container @@ -159,7 +168,7 @@ define(function (require, exports, module) { * Creates the tab bar and adds it to the DOM */ function createTabBar() { - if (!Preference.tabBarEnabled || !EditorManager.getActiveEditor()) { + if (!Preference.tabBarEnabled) { return; } @@ -349,6 +358,3 @@ define(function (require, exports, module) { handleTabClick(); }); }); - - -// TODO: Bug (when we have two panes and one pane gets empty by closing all files in it, the other pane tab bar also gets removed) \ No newline at end of file From 7d82ae6336ddb60d0ef8f0cf60c6187482c4835e Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 2 Mar 2025 15:42:49 +0530 Subject: [PATCH 04/35] refactor: styling issues --- src/extensionsIntegrated/TabBar/main.js | 4 ++-- src/styles/Extn-TabBar.less | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 4ca1c70414..0f5c5a8aff 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -106,7 +106,7 @@ define(function (require, exports, module) { title="${entry.path}">
-
+
`); // Add the file icon @@ -271,7 +271,7 @@ define(function (require, exports, module) { // delegate event handling for both tab bars $(document).on("click", ".tab", function (event) { // check if the clicked element is the close button - if ($(event.target).hasClass('fa-xmark') || $(event.target).closest('.tab-close').length) { + if ($(event.target).hasClass('fa-times') || $(event.target).closest('.tab-close').length) { // Get the file path from the data-path attribute of the parent tab const filePath = $(this).attr("data-path"); if (filePath) { diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 30812e7098..08527e1592 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -14,6 +14,10 @@ transition: height 0.3s ease; } +.phoenix-tab-bar::-webkit-scrollbar { + height: 4px; +} + .phoenix-tab-bar .hover { background-color: #333; } @@ -66,7 +70,7 @@ bottom: 0; left: 0; right: 0; - height: 2px; + height: 2.2px; background-color: #75BEFF; } @@ -87,10 +91,11 @@ .tab-close { font-size: 12px; - font-weight: lighter; + font-weight: 300; padding: 1px 4px 0.4px 4px; - margin-left: 3px; - color: #808080; + margin-left: 4px; + margin-top: -1px; + color: #CCC; transition: all 0.2s ease; border-radius: 3px; visibility: hidden; @@ -106,6 +111,5 @@ } .tab-close:hover { - color: #CCCCCC; background-color: rgba(255, 255, 255, 0.1); } \ No newline at end of file From 94478580bb6f302b0ac021ba50807ff4d1c67955 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 2 Mar 2025 22:57:43 +0530 Subject: [PATCH 05/35] fix: tab bar not found in DOM when no files in pane --- src/extensionsIntegrated/TabBar/main.js | 7 ++-- src/styles/Extn-TabBar.less | 45 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 0f5c5a8aff..335c43c9d5 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -140,11 +140,11 @@ define(function (require, exports, module) { getAllFilesFromWorkingSet(); // if no files are present in a pane, we want to hide the tab bar for that pane - if(firstPaneWorkingSet.length === 0) { + if (firstPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar'))) { Helper._hideTabBar($('#phoenix-tab-bar')); } - if(secondPaneWorkingSet.length === 0) { + if (secondPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar-2'))) { Helper._hideTabBar($('#phoenix-tab-bar-2')); } @@ -320,8 +320,7 @@ define(function (require, exports, module) { // when working set changes, recreate the tab bar MainViewManager.on("workingSetAdd", workingSetChanged); MainViewManager.on("workingSetRemove", workingSetChanged); - MainViewManager.on("workingSetListChange", workingSetChanged); - + MainViewManager.on("workingSetSort", workingSetChanged); } diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 08527e1592..23eee4c5b1 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -12,6 +12,7 @@ overflow-y: hidden; white-space: nowrap; transition: height 0.3s ease; + scroll-behavior: smooth; } .phoenix-tab-bar::-webkit-scrollbar { @@ -34,6 +35,12 @@ position: relative; flex: 0 0 auto; min-width: fit-content; + user-select: none; + transition: transform 60ms ease, opacity 60ms ease; +} + +.tab, .tab-close, .tab-icon, .tab-name { + transition: all 120ms ease-out; } .tab-icon { @@ -62,6 +69,7 @@ .tab:hover { background-color: #4d4949; + cursor: pointer; } .tab.active::after { @@ -111,5 +119,42 @@ } .tab-close:hover { + cursor: pointer; background-color: rgba(255, 255, 255, 0.1); +} + +.tab.dragging { + opacity: 0.7; + transform: scale(0.95); + cursor: grabbing !important; + z-index: 10000; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); +} + +.tab.drag-target { + background-color: #383838; +} + +.tab-drag-indicator { + position: fixed; + width: 2px; + background-color: #75BEFF; + pointer-events: none; + z-index: 10001; + box-shadow: 0 0 3px rgba(117, 190, 255, 0.5); + display: none; + animation: pulse 1.5s infinite; +} + + +@keyframes pulse { + 0% { + opacity: 0.7; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.7; + } } \ No newline at end of file From 2738ae8be6c385253804abc14a0ef2ba62e72d86 Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 3 Mar 2025 00:05:09 +0530 Subject: [PATCH 06/35] feat: add numberOfTabs preference feature --- src/extensionsIntegrated/TabBar/main.js | 64 ++++++++++++++++++++----- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 335c43c9d5..99cc268dcd 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -140,6 +140,9 @@ define(function (require, exports, module) { getAllFilesFromWorkingSet(); // if no files are present in a pane, we want to hide the tab bar for that pane + const $firstTabBar = $('#phoenix-tab-bar'); + const $secondTabBar = $('#phoenix-tab-bar-2'); + if (firstPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar'))) { Helper._hideTabBar($('#phoenix-tab-bar')); } @@ -148,18 +151,56 @@ define(function (require, exports, module) { Helper._hideTabBar($('#phoenix-tab-bar-2')); } - // to add tabs one by one to the tab bar - if (firstPaneWorkingSet.length > 0) { - for (let i = 0; i < firstPaneWorkingSet.length; i++) { - // Note: here we add the element to the tab bar directly and not the tab-container - $('#phoenix-tab-bar').append(createTab(firstPaneWorkingSet[i])); + // get the count of tabs that we want to display in the tab bar (from preference settings) + // from preference settings or working set whichever smaller + let tabsCountP1 = Math.min(firstPaneWorkingSet.length, Preference.tabBarNumberOfTabs); + let tabsCountP2 = Math.min(secondPaneWorkingSet.length, Preference.tabBarNumberOfTabs); + + // the value is generally '-1', but we check for less than 0 so that it can handle edge cases gracefully + // if the value is negative then we display all tabs + if (Preference.tabBarNumberOfTabs < 0) { + tabsCountP1 = firstPaneWorkingSet.length; + tabsCountP2 = secondPaneWorkingSet.length; + } + + // get the active editor and path once to reuse for both panes + const activeEditor = EditorManager.getActiveEditor(); + const activePath = activeEditor ? activeEditor.document.file.fullPath : null; + + // handle the first pane tabs + if (firstPaneWorkingSet.length > 0 && tabsCountP1 > 0 && $firstTabBar.length) { + // get the top n entries for the first pane + let displayedEntries = firstPaneWorkingSet.slice(0, tabsCountP1); + + // if the active file isn't already visible but exists in the working set, force-include it + if (activePath && !displayedEntries.some(entry => entry.path === activePath)) { + let activeEntry = firstPaneWorkingSet.find(entry => entry.path === activePath); + if (activeEntry) { + // replace the last tab with the active file. + displayedEntries[displayedEntries.length - 1] = activeEntry; + } } + + // add each tab to the first pane's tab bar + displayedEntries.forEach(function (entry) { + $firstTabBar.append(createTab(entry)); + }); } - if (secondPaneWorkingSet.length > 0) { - for (let i = 0; i < secondPaneWorkingSet.length; i++) { - $('#phoenix-tab-bar-2').append(createTab(secondPaneWorkingSet[i])); + // for second pane tabs + if (secondPaneWorkingSet.length > 0 && tabsCountP2 > 0 && $secondTabBar.length) { + let displayedEntries2 = secondPaneWorkingSet.slice(0, tabsCountP2); + + if (activePath && !displayedEntries2.some(entry => entry.path === activePath)) { + let activeEntry = secondPaneWorkingSet.find(entry => entry.path === activePath); + if (activeEntry) { + displayedEntries2[displayedEntries2.length - 1] = activeEntry; + } } + + displayedEntries2.forEach(function (entry) { + $secondTabBar.append(createTab(entry)); + }); } } @@ -168,7 +209,7 @@ define(function (require, exports, module) { * Creates the tab bar and adds it to the DOM */ function createTabBar() { - if (!Preference.tabBarEnabled) { + if (!Preference.tabBarEnabled || Preference.numberOfTabs === 0) { return; } @@ -327,14 +368,13 @@ define(function (require, exports, module) { /** * This is called when the tab bar preference is changed * It takes care of creating or cleaning up the tab bar - * - * TODO: handle the number of tabs functionality */ function preferenceChanged() { Preference.tabBarEnabled = PreferencesManager.get(Preference.PREFERENCES_TAB_BAR).showTabBar; Preference.tabBarNumberOfTabs = PreferencesManager.get(Preference.PREFERENCES_TAB_BAR).numberOfTabs; - if (Preference.tabBarEnabled) { + // preference should be enabled and number of tabs should be greater than 0 + if (Preference.tabBarEnabled && Preference.tabBarNumberOfTabs !== 0) { createTabBar(); } else { cleanupTabBar(); From 5749246089f14581705863403a5e1c76a3785539 Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 3 Mar 2025 00:31:55 +0530 Subject: [PATCH 07/35] feat: add dirty icon if file is modified and show popup to save before closing --- src/extensionsIntegrated/TabBar/main.js | 11 +++++++++-- src/styles/Extn-TabBar.less | 5 ++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 99cc268dcd..19e5827a50 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -6,6 +6,7 @@ define(function (require, exports, module) { const PreferencesManager = require("preferences/PreferencesManager"); const CommandManager = require("command/CommandManager"); const Commands = require("command/Commands"); + const DocumentManager = require("document/DocumentManager"); const Helper = require("./helper"); @@ -101,7 +102,7 @@ define(function (require, exports, module) { // Create the tab element with the structure we need // tab name is written as a separate div because it may include directory info which we style differently const $tab = $( - `
@@ -353,7 +354,6 @@ define(function (require, exports, module) { MainViewManager.off("activePaneChange paneCreate paneDestroy paneLayoutChange", createTabBar); MainViewManager.on("activePaneChange paneCreate paneDestroy paneLayoutChange", createTabBar); - // editor handlers EditorManager.off("activeEditorChange", createTabBar); EditorManager.on("activeEditorChange", createTabBar); @@ -362,6 +362,13 @@ define(function (require, exports, module) { MainViewManager.on("workingSetAdd", workingSetChanged); MainViewManager.on("workingSetRemove", workingSetChanged); MainViewManager.on("workingSetSort", workingSetChanged); + + // file dirty flag change handler + DocumentManager.on("dirtyFlagChange", function (event, doc) { + const filePath = doc.file.fullPath; + const $tab = $tabBar.find(`.tab[data-path="${filePath}"]`); + $tab.toggleClass('dirty', doc.isDirty); + }); } diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 23eee4c5b1..1310aa0b95 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -87,10 +87,9 @@ color: #8D8D8E; font-size: 26px; margin-right: 4px; - margin-top: -8px; position: absolute; - left: 4px; - top: 8px; + left: 5px; + top: 4px; } .tab.dirty .tab-icon { From 043862543d6084ca1129404f3509d78807602015 Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 3 Mar 2025 01:42:51 +0530 Subject: [PATCH 08/35] feat: add more option button in tab bar --- .../TabBar/html/tabbar-pane.html | 5 +- .../TabBar/html/tabbar-second-pane.html | 5 +- src/styles/Extn-TabBar.less | 91 ++++++++++++++----- 3 files changed, 78 insertions(+), 23 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/html/tabbar-pane.html b/src/extensionsIntegrated/TabBar/html/tabbar-pane.html index 60fe28e430..e63e8cc465 100644 --- a/src/extensionsIntegrated/TabBar/html/tabbar-pane.html +++ b/src/extensionsIntegrated/TabBar/html/tabbar-pane.html @@ -1,5 +1,8 @@
- + +
+
+
\ No newline at end of file diff --git a/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html b/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html index ce20e5eace..1aadbcbccd 100644 --- a/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html +++ b/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html @@ -1,5 +1,8 @@
- + +
+
+
\ No newline at end of file diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 1310aa0b95..558e2a52a1 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -1,33 +1,64 @@ .tab-bar-container { - position: relative; + display: flex; + align-items: center; background-color: #1E1E1E; border-bottom: 1px solid #333; + position: relative; + overflow: hidden; } + .phoenix-tab-bar { - height: 28px; - background-color: #1E1E1E; + flex: 1; + height: 2rem; display: flex; overflow-x: auto; overflow-y: hidden; white-space: nowrap; transition: height 0.3s ease; scroll-behavior: smooth; + background-color: #1E1E1E; } + .phoenix-tab-bar::-webkit-scrollbar { - height: 4px; + height: 0.25rem; +} + + +.tab-bar-more-options { + display: flex; + align-items: center; + height: 2rem; + padding: 0 0.5rem; + cursor: pointer; + position: relative; + z-index: 2; + margin-left: auto; } -.phoenix-tab-bar .hover { + +.tab-bar-more-options:hover { background-color: #333; } + +.tab-bar-more-options::before { + content: ""; + position: absolute; + top: 0; + left: -1rem; + width: 1rem; + height: 100%; + pointer-events: none; + background: linear-gradient(to right, rgba(30, 30, 30, 0), #1E1E1E); +} + + .tab { display: inline-flex; align-items: center; - padding: 0 8px; - padding-top: 1px; + padding: 0 0.5rem; height: 100%; background-color: #2D2D2D; border-right: 1px solid #333; @@ -39,77 +70,90 @@ transition: transform 60ms ease, opacity 60ms ease; } -.tab, .tab-close, .tab-icon, .tab-name { + +.tab, +.tab-close, +.tab-icon, +.tab-name { transition: all 120ms ease-out; } + .tab-icon { display: flex; align-items: center; margin-bottom: -2px; } + .tab-name { display: inline-flex; align-items: center; - font-size: 12px; + font-size: 0.75rem; letter-spacing: 0.4px; word-spacing: 0.75px; } + .tab .tab-dirname { - font-size: 10px; + font-size: 0.65rem; opacity: 0.7; font-weight: normal; } + .tab.active { background-color: #3D3D3D; } + .tab:hover { background-color: #4d4949; cursor: pointer; } + .tab.active::after { content: ""; position: absolute; bottom: 0; left: 0; right: 0; - height: 2.2px; + height: 0.15rem; background-color: #75BEFF; } + .tab.dirty::before { content: "•"; color: #8D8D8E; - font-size: 26px; - margin-right: 4px; + font-size: 1.6rem; + margin-right: 0.25rem; position: absolute; - left: 5px; - top: 4px; + left: 0.3rem; + top: 0.25rem; } .tab.dirty .tab-icon { - margin-left: 10px; + margin-left: 1rem; } + .tab-close { - font-size: 12px; + font-size: 0.75rem; font-weight: 300; - padding: 1px 4px 0.4px 4px; - margin-left: 4px; - margin-top: -1px; + padding: 0.2rem 0.5rem; + margin-left: 0.5rem; + margin-top: -0.1rem; color: #CCC; transition: all 0.2s ease; - border-radius: 3px; + border-radius: 0.25rem; visibility: hidden; opacity: 0; pointer-events: none; } + .tab:hover .tab-close, .tab.active .tab-close { visibility: visible; @@ -117,11 +161,13 @@ pointer-events: auto; } + .tab-close:hover { cursor: pointer; background-color: rgba(255, 255, 255, 0.1); } + .tab.dragging { opacity: 0.7; transform: scale(0.95); @@ -134,6 +180,7 @@ background-color: #383838; } + .tab-drag-indicator { position: fixed; width: 2px; @@ -150,9 +197,11 @@ 0% { opacity: 0.7; } + 50% { opacity: 1; } + 100% { opacity: 0.7; } From 95d55ea3644683726bc68d2c62045ee0fa3d4bfc Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 3 Mar 2025 23:22:09 +0530 Subject: [PATCH 09/35] feat: add dropdown in more-options --- src/extensionsIntegrated/TabBar/main.js | 6 +++- .../TabBar/more-options.js | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/extensionsIntegrated/TabBar/more-options.js diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 19e5827a50..d1319e053e 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -1,3 +1,4 @@ +/* eslint-disable no-invalid-this */ define(function (require, exports, module) { const AppInit = require("utils/AppInit"); const MainViewManager = require("view/MainViewManager"); @@ -11,6 +12,7 @@ define(function (require, exports, module) { const Helper = require("./helper"); const Preference = require("./preference"); + const MoreOptions = require("./more-options"); const TabBarHTML = require("text!./html/tabbar-pane.html"); const TabBarHTML2 = require("text!./html/tabbar-second-pane.html"); @@ -306,7 +308,7 @@ define(function (require, exports, module) { /** - * handle click events on the tabs to open the file or close the tab + * handle click events on the tabs to open the file */ function handleTabClick() { @@ -402,5 +404,7 @@ define(function (require, exports, module) { // handle when a single tab gets clicked handleTabClick(); + + MoreOptions.handleMoreOptionsClick(); }); }); diff --git a/src/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js new file mode 100644 index 0000000000..e5f16518eb --- /dev/null +++ b/src/extensionsIntegrated/TabBar/more-options.js @@ -0,0 +1,36 @@ +define(function (require, exports, module) { + const DropdownButton = brackets.getModule("widgets/DropdownButton"); + + function showMoreOptionsContextMenu(x, y) { + const items = [ + "close all tabs", + "close unmodified tabs" + ]; + + const dropdown = new DropdownButton.DropdownButton("", items); + + $(".tab-bar-more-options").append(dropdown.$button); + dropdown.showDropdown(); + + dropdown.$button.on(DropdownButton.EVENT_SELECTED, function (e, item, index) { + console.log("Selected item:", item); + // TODO: Add the logic here + // check for index and then call the items + }); + + $(document).one("click", function () { + dropdown.closeDropdown(); + }); + } + + function handleMoreOptionsClick() { + $(document).on("click", ".tab-bar-more-options", function (event) { + event.stopPropagation(); + showMoreOptionsContextMenu(event.pageX, event.pageY); + }); + } + + module.exports = { + handleMoreOptionsClick + }; +}); From dcd0a33bc3b01018890195bdb72af06aefeebc34 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 6 Mar 2025 18:05:23 +0530 Subject: [PATCH 10/35] feat: handle close all tabs from more-options button --- src/extensionsIntegrated/TabBar/helper.js | 14 +++- .../TabBar/html/tabbar-second-pane.html | 2 +- src/extensionsIntegrated/TabBar/main.js | 16 +++-- .../TabBar/more-options.js | 69 +++++++++++++------ src/nls/root/strings.js | 4 ++ src/styles/Extn-TabBar.less | 9 ++- 6 files changed, 84 insertions(+), 30 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/helper.js b/src/extensionsIntegrated/TabBar/helper.js index 64a4f3fed4..694d6dadb6 100644 --- a/src/extensionsIntegrated/TabBar/helper.js +++ b/src/extensionsIntegrated/TabBar/helper.js @@ -10,12 +10,17 @@ define(function (require, exports, module) { /** * Shows the tab bar, when its hidden. * Its only shown when tab bar is enabled and there is atleast one working file + * We need to show both the tab bar and the more options * * @param {$.Element} $tabBar - The tab bar element + * @param {$.Element} $moreOptions - The more options element */ - function _showTabBar($tabBar) { + function _showTabBar($tabBar, $moreOptions) { if ($tabBar) { $tabBar.show(); + if($moreOptions) { + $moreOptions.show(); + } // when we add/remove something from the editor, the editor shifts up/down which leads to blank space // so we need to recompute the layout to make sure things are in the right place WorkspaceManager.recomputeLayout(true); @@ -25,12 +30,17 @@ define(function (require, exports, module) { /** * Hides the tab bar. * Its hidden when tab bar feature is disabled or there are no working files + * Both the tab bar and the more options should be hidden to hide the tab bar container * * @param {$.Element} $tabBar - The tab bar element + * @param {$.Element} $moreOptions - The more options element */ - function _hideTabBar($tabBar) { + function _hideTabBar($tabBar, $moreOptions) { if ($tabBar) { $tabBar.hide(); + if($moreOptions) { + $moreOptions.hide(); + } WorkspaceManager.recomputeLayout(true); } } diff --git a/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html b/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html index 1aadbcbccd..5af9d94870 100644 --- a/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html +++ b/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html @@ -2,7 +2,7 @@
-
+
\ No newline at end of file diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index d1319e053e..038924e85e 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -147,11 +147,11 @@ define(function (require, exports, module) { const $secondTabBar = $('#phoenix-tab-bar-2'); if (firstPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar'))) { - Helper._hideTabBar($('#phoenix-tab-bar')); + Helper._hideTabBar($('#phoenix-tab-bar'), $('#tab-bar-more-options')); } if (secondPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar-2'))) { - Helper._hideTabBar($('#phoenix-tab-bar-2')); + Helper._hideTabBar($('#phoenix-tab-bar-2'), $('#tab-bar-more-options-2')); } // get the count of tabs that we want to display in the tab bar (from preference settings) @@ -371,6 +371,16 @@ define(function (require, exports, module) { const $tab = $tabBar.find(`.tab[data-path="${filePath}"]`); $tab.toggleClass('dirty', doc.isDirty); }); + + // handle click events on the tab bar more options button + $(document).on("click", ".tab-bar-more-options", function (event) { + event.stopPropagation(); + MoreOptions.showMoreOptionsContextMenu("first-pane"); + }); + $(document).on("click", ".tab-bar-more-options-2", function (event) { + event.stopPropagation(); + MoreOptions.showMoreOptionsContextMenu("second-pane"); + }); } @@ -404,7 +414,5 @@ define(function (require, exports, module) { // handle when a single tab gets clicked handleTabClick(); - - MoreOptions.handleMoreOptionsClick(); }); }); diff --git a/src/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js index e5f16518eb..d258b440ef 100644 --- a/src/extensionsIntegrated/TabBar/more-options.js +++ b/src/extensionsIntegrated/TabBar/more-options.js @@ -1,36 +1,65 @@ +/* + * This file manages the more options context menu. + * The more option button is present at the right side of the tab bar. + * When clicked, it will show the more options context menu. + * which will have various options related to the tab bar + */ define(function (require, exports, module) { - const DropdownButton = brackets.getModule("widgets/DropdownButton"); + const DropdownButton = require("widgets/DropdownButton"); + const Strings = require("strings"); + const MainViewManager = require("view/MainViewManager"); + const CommandManager = require("command/CommandManager"); + const Commands = require("command/Commands"); - function showMoreOptionsContextMenu(x, y) { - const items = [ - "close all tabs", - "close unmodified tabs" - ]; + // List of items to show in the context menu + // Strings defined in `src/nls/root/strings.js` + const items = [ + Strings.CLOSE_ALL_TABS, + Strings.CLOSE_UNMODIFIED_TABS + ]; + + + /** + * This function is called when the close all tabs option is selected from the context menu + * This will close all tabs no matter whether they are in first pane or second pane + */ + function handleCloseAllTabs() { + CommandManager.execute(Commands.FILE_CLOSE_ALL); + } + + + /** + * This function is called when the more options button is clicked + * This will show the more options context menu + * @param {String} paneId - the id of the pane ["first-pane", "second-pane"] + */ + function showMoreOptionsContextMenu(paneId) { const dropdown = new DropdownButton.DropdownButton("", items); - $(".tab-bar-more-options").append(dropdown.$button); + // we need to determine which pane the tab belongs to show the context menu at the right place + if (paneId === "first-pane") { + $("#tab-bar-more-options").append(dropdown.$button); + } else { + $("#tab-bar-more-options-2").append(dropdown.$button); + } + dropdown.showDropdown(); - dropdown.$button.on(DropdownButton.EVENT_SELECTED, function (e, item, index) { - console.log("Selected item:", item); - // TODO: Add the logic here - // check for index and then call the items + // handle the option selection + dropdown.on("select", function (e, item, index) { + if (index === 0) { + handleCloseAllTabs(paneId); + } }); - $(document).one("click", function () { - dropdown.closeDropdown(); + dropdown.$button.css({ + display: "none" }); - } - function handleMoreOptionsClick() { - $(document).on("click", ".tab-bar-more-options", function (event) { - event.stopPropagation(); - showMoreOptionsContextMenu(event.pageX, event.pageY); - }); } module.exports = { - handleMoreOptionsClick + showMoreOptionsContextMenu }; }); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 51585f9f41..fafe17e475 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -426,6 +426,10 @@ define({ "STATUSBAR_TASKS_STOP": "Stop", "STATUSBAR_TASKS_RESTART": "Restart", + // Tab bar Strings + "CLOSE_ALL_TABS": "Close All Tabs", + "CLOSE_UNMODIFIED_TABS": "Close Unmodified Tabs", + // CodeInspection: errors/warnings "ERRORS_NO_FILE": "No File Open", "ERRORS_PANEL_TITLE_MULTIPLE": "{0} Problems - {1}", diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 558e2a52a1..6d73889d2f 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -26,7 +26,8 @@ } -.tab-bar-more-options { +.tab-bar-more-options, +.tab-bar-more-options-2 { display: flex; align-items: center; height: 2rem; @@ -38,12 +39,14 @@ } -.tab-bar-more-options:hover { +.tab-bar-more-options:hover, +.tab-bar-more-options-2:hover { background-color: #333; } -.tab-bar-more-options::before { +.tab-bar-more-options::before, +.tab-bar-more-options-2::before { content: ""; position: absolute; top: 0; From fd6086b7a792c00763b9433273069042be9e4ef9 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 9 Mar 2025 01:50:42 +0530 Subject: [PATCH 11/35] feat: add reopen closed file in tab bar context menu --- src/extensionsIntegrated/TabBar/global.js | 20 ++++ src/extensionsIntegrated/TabBar/main.js | 94 ++++++++++++------- .../TabBar/more-options.js | 30 +++++- src/nls/root/strings.js | 1 + 4 files changed, 109 insertions(+), 36 deletions(-) create mode 100644 src/extensionsIntegrated/TabBar/global.js diff --git a/src/extensionsIntegrated/TabBar/global.js b/src/extensionsIntegrated/TabBar/global.js new file mode 100644 index 0000000000..3650b9c44f --- /dev/null +++ b/src/extensionsIntegrated/TabBar/global.js @@ -0,0 +1,20 @@ +define(function (require, exports, module) { + /** + * This array's represents the current working set + * It holds all the working set items that are to be displayed in the tab bar + * Properties of each object: + * path: {String} full path of the file + * name: {String} name of the file + * isFile: {Boolean} whether the file is a file or a directory + * isDirty: {Boolean} whether the file is dirty + * isPinned: {Boolean} whether the file is pinned + * displayName: {String} name to display in the tab (may include directory info for duplicate files) + */ + let firstPaneWorkingSet = []; + let secondPaneWorkingSet = []; + + module.exports = { + firstPaneWorkingSet, + secondPaneWorkingSet + }; +}); diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 038924e85e..1f2af6a1d2 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -9,7 +9,7 @@ define(function (require, exports, module) { const Commands = require("command/Commands"); const DocumentManager = require("document/DocumentManager"); - + const Global = require("./global"); const Helper = require("./helper"); const Preference = require("./preference"); const MoreOptions = require("./more-options"); @@ -17,20 +17,6 @@ define(function (require, exports, module) { const TabBarHTML2 = require("text!./html/tabbar-second-pane.html"); - /** - * This array's represents the current working set - * It holds all the working set items that are to be displayed in the tab bar - * Properties of each object: - * path: {String} full path of the file - * name: {String} name of the file - * isFile: {Boolean} whether the file is a file or a directory - * isDirty: {Boolean} whether the file is dirty - * isPinned: {Boolean} whether the file is pinned - * displayName: {String} name to display in the tab (may include directory info for duplicate files) - */ - let firstPaneWorkingSet = []; - let secondPaneWorkingSet = []; - /** * This holds the tab bar element @@ -48,8 +34,8 @@ define(function (require, exports, module) { * This is placed here instead of helper.js because it modifies the working sets */ function getAllFilesFromWorkingSet() { - firstPaneWorkingSet = []; - secondPaneWorkingSet = []; + Global.firstPaneWorkingSet = []; + Global.secondPaneWorkingSet = []; // this gives the list of panes. When both panes are open, it will be ['first-pane', 'second-pane'] const paneList = MainViewManager.getPaneIdList(); @@ -63,21 +49,36 @@ define(function (require, exports, module) { for (let i = 0; i < currFirstPaneWorkingSet.length; i++) { // MainViewManager.getWorkingSet gives the working set of the first pane, // but it has lot of details we don't need. Hence we use Helper._getRequiredDataFromEntry - firstPaneWorkingSet.push(Helper._getRequiredDataFromEntry(currFirstPaneWorkingSet[i])); + Global.firstPaneWorkingSet.push(Helper._getRequiredDataFromEntry(currFirstPaneWorkingSet[i])); } // if there are duplicate file names, we update the displayName to include the directory - Helper._handleDuplicateFileNames(firstPaneWorkingSet); + Helper._handleDuplicateFileNames(Global.firstPaneWorkingSet); // check if second pane is open if (paneList.length > 1) { const currSecondPaneWorkingSet = MainViewManager.getWorkingSet(paneList[1]); for (let i = 0; i < currSecondPaneWorkingSet.length; i++) { - secondPaneWorkingSet.push(Helper._getRequiredDataFromEntry(currSecondPaneWorkingSet[i])); + Global.secondPaneWorkingSet.push(Helper._getRequiredDataFromEntry(currSecondPaneWorkingSet[i])); } - Helper._handleDuplicateFileNames(secondPaneWorkingSet); - + Helper._handleDuplicateFileNames(Global.secondPaneWorkingSet); } + + // Update dirty status for files in the first pane working set + Global.firstPaneWorkingSet.forEach(function (entry) { + const doc = DocumentManager.getOpenDocumentForPath(entry.path); + if (doc) { + entry.isDirty = doc.isDirty; + } + }); + + // Update dirty status for files in the second pane working set + Global.secondPaneWorkingSet.forEach(function (entry) { + const doc = DocumentManager.getOpenDocumentForPath(entry.path); + if (doc) { + entry.isDirty = doc.isDirty; + } + }); } } @@ -146,24 +147,24 @@ define(function (require, exports, module) { const $firstTabBar = $('#phoenix-tab-bar'); const $secondTabBar = $('#phoenix-tab-bar-2'); - if (firstPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar'))) { + if (Global.firstPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar'))) { Helper._hideTabBar($('#phoenix-tab-bar'), $('#tab-bar-more-options')); } - if (secondPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar-2'))) { + if (Global.secondPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar-2'))) { Helper._hideTabBar($('#phoenix-tab-bar-2'), $('#tab-bar-more-options-2')); } // get the count of tabs that we want to display in the tab bar (from preference settings) // from preference settings or working set whichever smaller - let tabsCountP1 = Math.min(firstPaneWorkingSet.length, Preference.tabBarNumberOfTabs); - let tabsCountP2 = Math.min(secondPaneWorkingSet.length, Preference.tabBarNumberOfTabs); + let tabsCountP1 = Math.min(Global.firstPaneWorkingSet.length, Preference.tabBarNumberOfTabs); + let tabsCountP2 = Math.min(Global.secondPaneWorkingSet.length, Preference.tabBarNumberOfTabs); // the value is generally '-1', but we check for less than 0 so that it can handle edge cases gracefully // if the value is negative then we display all tabs if (Preference.tabBarNumberOfTabs < 0) { - tabsCountP1 = firstPaneWorkingSet.length; - tabsCountP2 = secondPaneWorkingSet.length; + tabsCountP1 = Global.firstPaneWorkingSet.length; + tabsCountP2 = Global.secondPaneWorkingSet.length; } // get the active editor and path once to reuse for both panes @@ -171,13 +172,13 @@ define(function (require, exports, module) { const activePath = activeEditor ? activeEditor.document.file.fullPath : null; // handle the first pane tabs - if (firstPaneWorkingSet.length > 0 && tabsCountP1 > 0 && $firstTabBar.length) { + if (Global.firstPaneWorkingSet.length > 0 && tabsCountP1 > 0 && $firstTabBar.length) { // get the top n entries for the first pane - let displayedEntries = firstPaneWorkingSet.slice(0, tabsCountP1); + let displayedEntries = Global.firstPaneWorkingSet.slice(0, tabsCountP1); // if the active file isn't already visible but exists in the working set, force-include it if (activePath && !displayedEntries.some(entry => entry.path === activePath)) { - let activeEntry = firstPaneWorkingSet.find(entry => entry.path === activePath); + let activeEntry = Global.firstPaneWorkingSet.find(entry => entry.path === activePath); if (activeEntry) { // replace the last tab with the active file. displayedEntries[displayedEntries.length - 1] = activeEntry; @@ -191,11 +192,11 @@ define(function (require, exports, module) { } // for second pane tabs - if (secondPaneWorkingSet.length > 0 && tabsCountP2 > 0 && $secondTabBar.length) { - let displayedEntries2 = secondPaneWorkingSet.slice(0, tabsCountP2); + if (Global.secondPaneWorkingSet.length > 0 && tabsCountP2 > 0 && $secondTabBar.length) { + let displayedEntries2 = Global.secondPaneWorkingSet.slice(0, tabsCountP2); if (activePath && !displayedEntries2.some(entry => entry.path === activePath)) { - let activeEntry = secondPaneWorkingSet.find(entry => entry.path === activePath); + let activeEntry = Global.secondPaneWorkingSet.find(entry => entry.path === activePath); if (activeEntry) { displayedEntries2[displayedEntries2.length - 1] = activeEntry; } @@ -368,8 +369,33 @@ define(function (require, exports, module) { // file dirty flag change handler DocumentManager.on("dirtyFlagChange", function (event, doc) { const filePath = doc.file.fullPath; + + // Update UI const $tab = $tabBar.find(`.tab[data-path="${filePath}"]`); $tab.toggleClass('dirty', doc.isDirty); + + // Also update the $tab2 if it exists + if ($tabBar2) { + const $tab2 = $tabBar2.find(`.tab[data-path="${filePath}"]`); + $tab2.toggleClass('dirty', doc.isDirty); + } + + // Update the working set data + // First pane + for (let i = 0; i < Global.firstPaneWorkingSet.length; i++) { + if (Global.firstPaneWorkingSet[i].path === filePath) { + Global.firstPaneWorkingSet[i].isDirty = doc.isDirty; + break; + } + } + + // Second pane + for (let i = 0; i < Global.secondPaneWorkingSet.length; i++) { + if (Global.secondPaneWorkingSet[i].path === filePath) { + Global.secondPaneWorkingSet[i].isDirty = doc.isDirty; + break; + } + } }); // handle click events on the tab bar more options button diff --git a/src/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js index d258b440ef..21e77086cf 100644 --- a/src/extensionsIntegrated/TabBar/more-options.js +++ b/src/extensionsIntegrated/TabBar/more-options.js @@ -11,11 +11,15 @@ define(function (require, exports, module) { const CommandManager = require("command/CommandManager"); const Commands = require("command/Commands"); + const Global = require("./global"); + const Helper = require("./helper"); + // List of items to show in the context menu // Strings defined in `src/nls/root/strings.js` const items = [ Strings.CLOSE_ALL_TABS, - Strings.CLOSE_UNMODIFIED_TABS + Strings.CLOSE_UNMODIFIED_TABS, + Strings.REOPEN_CLOSED_FILE ]; @@ -27,6 +31,24 @@ define(function (require, exports, module) { CommandManager.execute(Commands.FILE_CLOSE_ALL); } + /** + * Called when the close unmodified tabs option is selected from the context menu + * This will close all tabs that are not modified + * TODO: implement the functionality + */ + function handleCloseUnmodifiedTabs() { + + // pass + } + + /** + * Called when the reopen closed file option is selected from the context menu + * This just calls the reopen closed file command. everthing else is handled there + */ + function reopenClosedFile() { + CommandManager.execute(Commands.FILE_REOPEN_CLOSED); + } + /** * This function is called when the more options button is clicked @@ -49,7 +71,11 @@ define(function (require, exports, module) { // handle the option selection dropdown.on("select", function (e, item, index) { if (index === 0) { - handleCloseAllTabs(paneId); + handleCloseAllTabs(); + } else if(index === 1) { + handleCloseUnmodifiedTabs(); + } else if(index === 2) { + reopenClosedFile(); } }); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index fafe17e475..c35434aee2 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -429,6 +429,7 @@ define({ // Tab bar Strings "CLOSE_ALL_TABS": "Close All Tabs", "CLOSE_UNMODIFIED_TABS": "Close Unmodified Tabs", + "REOPEN_CLOSED_FILE": "Reopen Closed File", // CodeInspection: errors/warnings "ERRORS_NO_FILE": "No File Open", From 6dda8f26497f60daa091b60101e85926d8763e98 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 9 Mar 2025 02:20:37 +0530 Subject: [PATCH 12/35] feat: replace more options button with tabs right click handler --- src/extensionsIntegrated/TabBar/helper.js | 8 ----- .../TabBar/html/tabbar-pane.html | 3 -- .../TabBar/html/tabbar-second-pane.html | 3 -- src/extensionsIntegrated/TabBar/main.js | 27 +++++++++------- .../TabBar/more-options.js | 30 ++++++++++------- src/styles/Extn-TabBar.less | 32 ------------------- 6 files changed, 33 insertions(+), 70 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/helper.js b/src/extensionsIntegrated/TabBar/helper.js index 694d6dadb6..1783755514 100644 --- a/src/extensionsIntegrated/TabBar/helper.js +++ b/src/extensionsIntegrated/TabBar/helper.js @@ -10,7 +10,6 @@ define(function (require, exports, module) { /** * Shows the tab bar, when its hidden. * Its only shown when tab bar is enabled and there is atleast one working file - * We need to show both the tab bar and the more options * * @param {$.Element} $tabBar - The tab bar element * @param {$.Element} $moreOptions - The more options element @@ -18,9 +17,6 @@ define(function (require, exports, module) { function _showTabBar($tabBar, $moreOptions) { if ($tabBar) { $tabBar.show(); - if($moreOptions) { - $moreOptions.show(); - } // when we add/remove something from the editor, the editor shifts up/down which leads to blank space // so we need to recompute the layout to make sure things are in the right place WorkspaceManager.recomputeLayout(true); @@ -30,7 +26,6 @@ define(function (require, exports, module) { /** * Hides the tab bar. * Its hidden when tab bar feature is disabled or there are no working files - * Both the tab bar and the more options should be hidden to hide the tab bar container * * @param {$.Element} $tabBar - The tab bar element * @param {$.Element} $moreOptions - The more options element @@ -38,9 +33,6 @@ define(function (require, exports, module) { function _hideTabBar($tabBar, $moreOptions) { if ($tabBar) { $tabBar.hide(); - if($moreOptions) { - $moreOptions.hide(); - } WorkspaceManager.recomputeLayout(true); } } diff --git a/src/extensionsIntegrated/TabBar/html/tabbar-pane.html b/src/extensionsIntegrated/TabBar/html/tabbar-pane.html index e63e8cc465..b50a7b4c92 100644 --- a/src/extensionsIntegrated/TabBar/html/tabbar-pane.html +++ b/src/extensionsIntegrated/TabBar/html/tabbar-pane.html @@ -2,7 +2,4 @@
-
- -
\ No newline at end of file diff --git a/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html b/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html index 5af9d94870..5a012709cd 100644 --- a/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html +++ b/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html @@ -2,7 +2,4 @@
-
- -
\ No newline at end of file diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 1f2af6a1d2..a1ff604128 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -148,11 +148,11 @@ define(function (require, exports, module) { const $secondTabBar = $('#phoenix-tab-bar-2'); if (Global.firstPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar'))) { - Helper._hideTabBar($('#phoenix-tab-bar'), $('#tab-bar-more-options')); + Helper._hideTabBar($('#phoenix-tab-bar')); } if (Global.secondPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar-2'))) { - Helper._hideTabBar($('#phoenix-tab-bar-2'), $('#tab-bar-more-options-2')); + Helper._hideTabBar($('#phoenix-tab-bar-2')); } // get the count of tabs that we want to display in the tab bar (from preference settings) @@ -345,6 +345,19 @@ define(function (require, exports, module) { event.stopPropagation(); } }); + + // Add contextmenu (right-click) handler + $(document).on("contextmenu", ".tab", function (event) { + event.preventDefault(); + event.stopPropagation(); + + // Determine which pane the tab belongs to + const isSecondPane = $(this).closest("#phoenix-tab-bar-2").length > 0; + const paneId = isSecondPane ? "second-pane" : "first-pane"; + + // Show context menu at mouse position + MoreOptions.showMoreOptionsContextMenu(paneId, event.pageX, event.pageY); + }); } @@ -397,16 +410,6 @@ define(function (require, exports, module) { } } }); - - // handle click events on the tab bar more options button - $(document).on("click", ".tab-bar-more-options", function (event) { - event.stopPropagation(); - MoreOptions.showMoreOptionsContextMenu("first-pane"); - }); - $(document).on("click", ".tab-bar-more-options-2", function (event) { - event.stopPropagation(); - MoreOptions.showMoreOptionsContextMenu("second-pane"); - }); } diff --git a/src/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js index 21e77086cf..e70cf29d28 100644 --- a/src/extensionsIntegrated/TabBar/more-options.js +++ b/src/extensionsIntegrated/TabBar/more-options.js @@ -51,20 +51,26 @@ define(function (require, exports, module) { /** - * This function is called when the more options button is clicked + * This function is called when a tab is right-clicked * This will show the more options context menu + * * @param {String} paneId - the id of the pane ["first-pane", "second-pane"] + * @param {Number} x - the x coordinate for positioning the menu + * @param {Number} y - the y coordinate for positioning the menu */ - function showMoreOptionsContextMenu(paneId) { - + function showMoreOptionsContextMenu(paneId, x, y) { const dropdown = new DropdownButton.DropdownButton("", items); - // we need to determine which pane the tab belongs to show the context menu at the right place - if (paneId === "first-pane") { - $("#tab-bar-more-options").append(dropdown.$button); - } else { - $("#tab-bar-more-options-2").append(dropdown.$button); - } + // 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 + }); dropdown.showDropdown(); @@ -72,17 +78,17 @@ define(function (require, exports, module) { dropdown.on("select", function (e, item, index) { if (index === 0) { handleCloseAllTabs(); - } else if(index === 1) { + } else if (index === 1) { handleCloseUnmodifiedTabs(); - } else if(index === 2) { + } else if (index === 2) { reopenClosedFile(); } }); + // Remove the button after the dropdown is hidden dropdown.$button.css({ display: "none" }); - } module.exports = { diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 6d73889d2f..4fc8562c4f 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -26,38 +26,6 @@ } -.tab-bar-more-options, -.tab-bar-more-options-2 { - display: flex; - align-items: center; - height: 2rem; - padding: 0 0.5rem; - cursor: pointer; - position: relative; - z-index: 2; - margin-left: auto; -} - - -.tab-bar-more-options:hover, -.tab-bar-more-options-2:hover { - background-color: #333; -} - - -.tab-bar-more-options::before, -.tab-bar-more-options-2::before { - content: ""; - position: absolute; - top: 0; - left: -1rem; - width: 1rem; - height: 100%; - pointer-events: none; - background: linear-gradient(to right, rgba(30, 30, 30, 0), #1E1E1E); -} - - .tab { display: inline-flex; align-items: center; From 83b8f448d2db6221e4e46cde1b35176137c42406 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 9 Mar 2025 19:13:56 +0530 Subject: [PATCH 13/35] feat: create overflow button in tabbar --- src/extensionsIntegrated/TabBar/helper.js | 15 +- .../TabBar/html/tabbar-pane.html | 4 + .../TabBar/html/tabbar-second-pane.html | 4 + src/extensionsIntegrated/TabBar/main.js | 33 +++-- src/extensionsIntegrated/TabBar/overflow.js | 129 ++++++++++++++++++ src/styles/Extn-TabBar.less | 36 +++++ 6 files changed, 202 insertions(+), 19 deletions(-) create mode 100644 src/extensionsIntegrated/TabBar/overflow.js diff --git a/src/extensionsIntegrated/TabBar/helper.js b/src/extensionsIntegrated/TabBar/helper.js index 1783755514..34e1b52ac4 100644 --- a/src/extensionsIntegrated/TabBar/helper.js +++ b/src/extensionsIntegrated/TabBar/helper.js @@ -12,11 +12,14 @@ define(function (require, exports, module) { * Its only shown when tab bar is enabled and there is atleast one working file * * @param {$.Element} $tabBar - The tab bar element - * @param {$.Element} $moreOptions - The more options element + * @param {$.Element} $overflowButton - The overflow button element */ - function _showTabBar($tabBar, $moreOptions) { + function _showTabBar($tabBar, $overflowButton) { if ($tabBar) { $tabBar.show(); + if($overflowButton) { + $overflowButton.show(); + } // when we add/remove something from the editor, the editor shifts up/down which leads to blank space // so we need to recompute the layout to make sure things are in the right place WorkspaceManager.recomputeLayout(true); @@ -26,13 +29,17 @@ define(function (require, exports, module) { /** * Hides the tab bar. * Its hidden when tab bar feature is disabled or there are no working files + * both the tab bar and the overflow button are to be hidden to hide the tab bar container * * @param {$.Element} $tabBar - The tab bar element - * @param {$.Element} $moreOptions - The more options element + * @param {$.Element} $overflowButton - The overflow button element */ - function _hideTabBar($tabBar, $moreOptions) { + function _hideTabBar($tabBar, $overflowButton) { if ($tabBar) { $tabBar.hide(); + if($overflowButton) { + $overflowButton.hide(); + } WorkspaceManager.recomputeLayout(true); } } diff --git a/src/extensionsIntegrated/TabBar/html/tabbar-pane.html b/src/extensionsIntegrated/TabBar/html/tabbar-pane.html index b50a7b4c92..c424ac2bc0 100644 --- a/src/extensionsIntegrated/TabBar/html/tabbar-pane.html +++ b/src/extensionsIntegrated/TabBar/html/tabbar-pane.html @@ -2,4 +2,8 @@
+ +
+ +
\ No newline at end of file diff --git a/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html b/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html index 5a012709cd..451f0322a2 100644 --- a/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html +++ b/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html @@ -2,4 +2,8 @@
+ +
+ +
\ No newline at end of file diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index a1ff604128..984561c23e 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -13,6 +13,7 @@ define(function (require, exports, module) { const Helper = require("./helper"); const Preference = require("./preference"); const MoreOptions = require("./more-options"); + const Overflow = require("./overflow"); const TabBarHTML = require("text!./html/tabbar-pane.html"); const TabBarHTML2 = require("text!./html/tabbar-second-pane.html"); @@ -148,11 +149,11 @@ define(function (require, exports, module) { const $secondTabBar = $('#phoenix-tab-bar-2'); if (Global.firstPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar'))) { - Helper._hideTabBar($('#phoenix-tab-bar')); + Helper._hideTabBar($('#phoenix-tab-bar'), $('#overflow-button')); } if (Global.secondPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar-2'))) { - Helper._hideTabBar($('#phoenix-tab-bar-2')); + Helper._hideTabBar($('#phoenix-tab-bar-2'), $('#overflow-button-2')); } // get the count of tabs that we want to display in the tab bar (from preference settings) @@ -188,6 +189,7 @@ define(function (require, exports, module) { // add each tab to the first pane's tab bar displayedEntries.forEach(function (entry) { $firstTabBar.append(createTab(entry)); + Overflow.toggleOverflowVisibility("first-pane"); }); } @@ -204,6 +206,7 @@ define(function (require, exports, module) { displayedEntries2.forEach(function (entry) { $secondTabBar.append(createTab(entry)); + Overflow.toggleOverflowVisibility("second-pane"); }); } } @@ -254,16 +257,6 @@ define(function (require, exports, module) { } - /** - * When any change is made to the working set, we just recreate the tab bar - * The changes may be adding/removing a file or changing the active file - */ - function workingSetChanged() { - cleanupTabBar(); - createTabBar(); - } - - /** * Handle close button click on tabs * This function will remove the file from the working set @@ -375,9 +368,17 @@ define(function (require, exports, module) { EditorManager.on("activeEditorChange", createTabBar); // when working set changes, recreate the tab bar - MainViewManager.on("workingSetAdd", workingSetChanged); - MainViewManager.on("workingSetRemove", workingSetChanged); - MainViewManager.on("workingSetSort", workingSetChanged); + // we listen to all of these events to ensure that the tab bar is always up to date + // refer to `MainViewManager.js` for more details + MainViewManager.on("workingSetAdd", createTabBar); + MainViewManager.on("workingSetRemove", createTabBar); + MainViewManager.on("workingSetSort", createTabBar); + MainViewManager.on("workingSetMove", createTabBar); + MainViewManager.on("workingSetAddList", createTabBar); + MainViewManager.on("workingSetRemoveList", createTabBar); + MainViewManager.on("workingSetUpdate", createTabBar); + MainViewManager.on("_workingSetDisableAutoSort", createTabBar); + // file dirty flag change handler DocumentManager.on("dirtyFlagChange", function (event, doc) { @@ -443,5 +444,7 @@ define(function (require, exports, module) { // handle when a single tab gets clicked handleTabClick(); + + Overflow.init(); }); }); diff --git a/src/extensionsIntegrated/TabBar/overflow.js b/src/extensionsIntegrated/TabBar/overflow.js new file mode 100644 index 0000000000..4c3d8c48d9 --- /dev/null +++ b/src/extensionsIntegrated/TabBar/overflow.js @@ -0,0 +1,129 @@ +define(function (require, exports, module) { + + /** + * This function determines which tabs are hidden in the tab bar due to overflow + * and returns them as an array of tab data objects + * + * @param {String} paneId - The ID of the pane ("first-pane" or "second-pane") + * @returns {Array} - Array of hidden tab data objects + */ + function _getListOfHiddenTabs(paneId) { + // Select the appropriate tab bar based on pane ID + const $currentTabBar = paneId === "first-pane" + ? $("#phoenix-tab-bar") + : $("#phoenix-tab-bar-2"); + + // Need to access the DOM element to get its bounding rectangle + const tabBarRect = $currentTabBar[0].getBoundingClientRect(); + + // Get the overflow button element + const $overflowButton = paneId === "first-pane" + ? $("#overflow-button") + : $("#overflow-button-2"); + + // Account for overflow button width in calculation + const overflowButtonWidth = $overflowButton.is(":visible") ? $overflowButton.outerWidth() : 0; + + const hiddenTabs = []; + + // Examine each tab to determine if it's visible + $currentTabBar.find('.tab').each(function () { + const tabRect = this.getBoundingClientRect(); + + // A tab is considered hidden if it extends beyond the right edge of the tab bar + // minus the width of the overflow button (if visible) + const isVisible = tabRect.left >= tabBarRect.left && + tabRect.right <= (tabBarRect.right - overflowButtonWidth); + + if (!isVisible) { + // Extract and store information about the hidden tab + const $tab = $(this); + const tabData = { + path: $tab.data('path'), + name: $tab.find('.tab-name').text(), + isActive: $tab.hasClass('active'), + isDirty: $tab.hasClass('dirty'), + $icon: $tab.find('.tab-icon').clone() + }; + + hiddenTabs.push(tabData); + } + }); + + return hiddenTabs; + } + + function _getNamesFromTabsData(tabsData) { + return tabsData.map(tab => tab.name); + } + + + /** + * Toggles the visibility of the overflow button based on whether + * there are any hidden tabs + * Hidden tabs are tabs that are currently not visible in the tab bar + * + * @param {String} paneId - The ID of the pane ("first-pane" or "second-pane") + */ + function toggleOverflowVisibility(paneId) { + const hiddenTabs = _getListOfHiddenTabs(paneId); + + if (paneId === "first-pane") { + // for the html elements, refer to ./html/tabbar-pane.html + const $tabBar = $("#phoenix-tab-bar"); + const $overflowButton = $("#overflow-button"); + + if (hiddenTabs.length > 0) { + $overflowButton.removeClass("hidden"); + } else { + $overflowButton.addClass("hidden"); + } + } else { + // for the html elements, refer to ./html/tabbar-second-pane.html + const $tabBar = $("#phoenix-tab-bar-2"); + const $overflowButton = $("#overflow-button-2"); + + if (hiddenTabs.length > 0) { + $overflowButton.removeClass("hidden"); + } else { + $overflowButton.addClass("hidden"); + } + } + } + + function showOverflowMenu(paneId, x, y) { + const hiddenTabs = _getListOfHiddenTabs(paneId); + + + + } + + + + + /** + * To setup the handlers for the overflow menu + */ + function setupOverflowHandlers() { + + $(document).on("click", "#overflow-button", function (e) { + e.stopPropagation(); + showOverflowMenu("first-pane", e.pageX, e.pageY); + }); + + $(document).on("click", "#overflow-button-2", function (e) { + e.stopPropagation(); + showOverflowMenu("second-pane", e.pageX, e.pageY); + }); + } + + // initialize the handling of the overflow buttons + function init() { + setupOverflowHandlers(); + } + + module.exports = { + init, + toggleOverflowVisibility + }; +}); diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 4fc8562c4f..90a5e27f34 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -25,6 +25,42 @@ height: 0.25rem; } +.overflow-button, +.overflow-button-2 { + display: flex; + align-items: center; + height: 2rem; + padding: 0 0.5rem; + cursor: pointer; + position: relative; + z-index: 2; + margin-left: auto; +} + +.overflow-button.hidden, +.overflow-button-2.hidden { + display: none; +} + + +.overflow-button:hover, +.overflow-button-2:hover { + background-color: #333; +} + + +.overflow-button::before, +.overflow-button-2::before { + content: ""; + position: absolute; + top: 0; + left: -1rem; + width: 1rem; + height: 100%; + pointer-events: none; + background: linear-gradient(to right, rgba(30, 30, 30, 0), #1E1E1E); +} + .tab { display: inline-flex; From 9a4f60d17a450add05eb8c3256e2a5726f37020c Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 10 Mar 2025 18:46:20 +0530 Subject: [PATCH 14/35] feat: scroll to active tab when tab is not visible --- src/extensionsIntegrated/TabBar/main.js | 153 ++++++++++++++--- src/extensionsIntegrated/TabBar/overflow.js | 172 +++++++++++++++++--- src/styles/Extn-TabBar.less | 48 ++++++ 3 files changed, 325 insertions(+), 48 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 984561c23e..8d2ab8882b 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -8,6 +8,7 @@ define(function (require, exports, module) { const CommandManager = require("command/CommandManager"); const Commands = require("command/Commands"); const DocumentManager = require("document/DocumentManager"); + const WorkspaceManager = require("view/WorkspaceManager"); const Global = require("./global"); const Helper = require("./helper"); @@ -141,7 +142,7 @@ define(function (require, exports, module) { * Sets up the tab bar */ function setupTabBar() { - // this populates the working sets + // this populates the working sets present in `global.js` getAllFilesFromWorkingSet(); // if no files are present in a pane, we want to hide the tab bar for that pane @@ -190,6 +191,7 @@ define(function (require, exports, module) { displayedEntries.forEach(function (entry) { $firstTabBar.append(createTab(entry)); Overflow.toggleOverflowVisibility("first-pane"); + Overflow.scrollToActiveTab($firstTabBar); }); } @@ -207,6 +209,7 @@ define(function (require, exports, module) { displayedEntries2.forEach(function (entry) { $secondTabBar.append(createTab(entry)); Overflow.toggleOverflowVisibility("second-pane"); + Overflow.scrollToActiveTab($firstTabBar); }); } } @@ -225,21 +228,129 @@ define(function (require, exports, module) { if ($('.not-editor').length === 1) { $tabBar = $(TabBarHTML); - // since we need to add the tab bar before the editor area, we target the `.not-editor` class - $(".not-editor").before($tabBar); + // since we need to add the tab bar before the editor area, we target the `#editor-holder` class and prepend + $("#editor-holder").prepend($tabBar); + setTimeout(function () { + WorkspaceManager.recomputeLayout(true); + }, 0); + } else if ($('.not-editor').length === 2) { $tabBar = $(TabBarHTML); $tabBar2 = $(TabBarHTML2); // eq(0) is for the first pane and eq(1) is for the second pane + // here #editor-holder cannot be used as in split view, we only have one #editor-holder + // so, right now we are using .not-editor. Maybe we need to look for some better selector + // TODO: Fix bug where the tab bar gets hidden inside the editor in horizontal split $(".not-editor").eq(0).before($tabBar); $(".not-editor").eq(1).before($tabBar2); + setTimeout(function () { + WorkspaceManager.recomputeLayout(true); + }, 0); } setupTabBar(); } + /** + * This function updates the tabs in the tab bar + * It is called when the working set changes. So instead of creating a new tab bar, we just update the existing one + */ + function updateTabs() { + // Get all files from the working set. refer to `global.js` + getAllFilesFromWorkingSet(); + + // When there is only one file, we enforce the creation of the tab bar + // this is done because, given the situation: + // In a vertical split, when no files are present in 'second-pane' so the tab bar is hidden. + // Now, when the user adds a file in 'second-pane', the tab bar should be shown but since updateTabs() only, + // updates the tabs, so the tab bar never gets created. + if (Global.firstPaneWorkingSet.length === 1 || Global.secondPaneWorkingSet.length === 1) { + createTabBar(); + return; + } + + const $firstTabBar = $('#phoenix-tab-bar'); + // Update first pane's tabs + if ($firstTabBar.length) { + $firstTabBar.empty(); + if (Global.firstPaneWorkingSet.length > 0) { + + // get the count of tabs that we want to display in the tab bar (from preference settings) + // from preference settings or working set whichever smaller + let tabsCountP1 = Math.min(Global.firstPaneWorkingSet.length, Preference.tabBarNumberOfTabs); + + // the value is generally '-1', but we check for less than 0 so that it can handle edge cases gracefully + // if the value is negative then we display all tabs + if (Preference.tabBarNumberOfTabs < 0) { + tabsCountP1 = Global.firstPaneWorkingSet.length; + } + + let displayedEntries = Global.firstPaneWorkingSet.slice(0, tabsCountP1); + + const activeEditor = EditorManager.getActiveEditor(); + const activePath = activeEditor ? activeEditor.document.file.fullPath : null; + if (activePath && !displayedEntries.some(entry => entry.path === activePath)) { + let activeEntry = Global.firstPaneWorkingSet.find(entry => entry.path === activePath); + if (activeEntry) { + displayedEntries[displayedEntries.length - 1] = activeEntry; + } + } + displayedEntries.forEach(function (entry) { + $firstTabBar.append(createTab(entry)); + }); + } + } + + const $secondTabBar = $('#phoenix-tab-bar-2'); + // Update second pane's tabs + if ($secondTabBar.length) { + $secondTabBar.empty(); + if (Global.secondPaneWorkingSet.length > 0) { + + let tabsCountP2 = Math.min(Global.secondPaneWorkingSet.length, Preference.tabBarNumberOfTabs); + if (Preference.tabBarNumberOfTabs < 0) { + tabsCountP2 = Global.secondPaneWorkingSet.length; + } + + let displayedEntries2 = Global.secondPaneWorkingSet.slice(0, tabsCountP2); + const activeEditor = EditorManager.getActiveEditor(); + const activePath = activeEditor ? activeEditor.document.file.fullPath : null; + if (activePath && !displayedEntries2.some(entry => entry.path === activePath)) { + let activeEntry = Global.secondPaneWorkingSet.find(entry => entry.path === activePath); + if (activeEntry) { + displayedEntries2[displayedEntries2.length - 1] = activeEntry; + } + } + displayedEntries2.forEach(function (entry) { + $secondTabBar.append(createTab(entry)); + }); + } + } + + // if no files are present in a pane, we want to hide the tab bar for that pane + if (Global.firstPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar'))) { + Helper._hideTabBar($('#phoenix-tab-bar'), $('#overflow-button')); + } + + if (Global.secondPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar-2'))) { + Helper._hideTabBar($('#phoenix-tab-bar-2'), $('#overflow-button-2')); + } + + // Now that tabs are updated, scroll to the active tab if necessary. + if ($firstTabBar.length) { + Overflow.toggleOverflowVisibility("first-pane"); + Overflow.scrollToActiveTab($firstTabBar); + } + + if ($secondTabBar.length) { + Overflow.toggleOverflowVisibility("second-pane"); + Overflow.scrollToActiveTab($secondTabBar); + } + } + + /** * Removes existing tab bar and cleans up */ @@ -358,29 +469,25 @@ define(function (require, exports, module) { * Registers the event handlers */ function registerHandlers() { - - // pane handlers + // For pane changes, still recreate the entire tab bar container. MainViewManager.off("activePaneChange paneCreate paneDestroy paneLayoutChange", createTabBar); MainViewManager.on("activePaneChange paneCreate paneDestroy paneLayoutChange", createTabBar); - // editor handlers - EditorManager.off("activeEditorChange", createTabBar); - EditorManager.on("activeEditorChange", createTabBar); - - // when working set changes, recreate the tab bar - // we listen to all of these events to ensure that the tab bar is always up to date - // refer to `MainViewManager.js` for more details - MainViewManager.on("workingSetAdd", createTabBar); - MainViewManager.on("workingSetRemove", createTabBar); - MainViewManager.on("workingSetSort", createTabBar); - MainViewManager.on("workingSetMove", createTabBar); - MainViewManager.on("workingSetAddList", createTabBar); - MainViewManager.on("workingSetRemoveList", createTabBar); - MainViewManager.on("workingSetUpdate", createTabBar); - MainViewManager.on("_workingSetDisableAutoSort", createTabBar); - - - // file dirty flag change handler + // For editor changes, update only the tabs. + EditorManager.off("activeEditorChange", updateTabs); + EditorManager.on("activeEditorChange", updateTabs); + + // For working set changes, update only the tabs. + MainViewManager.on("workingSetAdd", updateTabs); + MainViewManager.on("workingSetRemove", updateTabs); + MainViewManager.on("workingSetSort", updateTabs); + MainViewManager.on("workingSetMove", updateTabs); + MainViewManager.on("workingSetAddList", updateTabs); + MainViewManager.on("workingSetRemoveList", updateTabs); + MainViewManager.on("workingSetUpdate", updateTabs); + MainViewManager.on("_workingSetDisableAutoSort", updateTabs); + + // file dirty flag change remains unchanged. DocumentManager.on("dirtyFlagChange", function (event, doc) { const filePath = doc.file.fullPath; diff --git a/src/extensionsIntegrated/TabBar/overflow.js b/src/extensionsIntegrated/TabBar/overflow.js index 4c3d8c48d9..a013b927a7 100644 --- a/src/extensionsIntegrated/TabBar/overflow.js +++ b/src/extensionsIntegrated/TabBar/overflow.js @@ -1,5 +1,13 @@ +/* eslint-disable no-invalid-this */ define(function (require, exports, module) { + const DropdownButton = require("widgets/DropdownButton"); + const MainViewManager = require("view/MainViewManager"); + const CommandManager = require("command/CommandManager"); + const Commands = require("command/Commands"); + const EditorManager = require("editor/EditorManager"); + + /** * This function determines which tabs are hidden in the tab bar due to overflow * and returns them as an array of tab data objects @@ -8,35 +16,31 @@ define(function (require, exports, module) { * @returns {Array} - Array of hidden tab data objects */ function _getListOfHiddenTabs(paneId) { - // Select the appropriate tab bar based on pane ID + // get the appropriate tab bar based on pane ID const $currentTabBar = paneId === "first-pane" ? $("#phoenix-tab-bar") : $("#phoenix-tab-bar-2"); - // Need to access the DOM element to get its bounding rectangle + // access the DOM element to get its bounding rectangle const tabBarRect = $currentTabBar[0].getBoundingClientRect(); - // Get the overflow button element - const $overflowButton = paneId === "first-pane" - ? $("#overflow-button") - : $("#overflow-button-2"); - - // Account for overflow button width in calculation - const overflowButtonWidth = $overflowButton.is(":visible") ? $overflowButton.outerWidth() : 0; - + // an array of hidden tabs objects which will store properties like + // path, name, isActive, isDirty and $icon const hiddenTabs = []; - // Examine each tab to determine if it's visible + // check each tab to determine if it's visible $currentTabBar.find('.tab').each(function () { const tabRect = this.getBoundingClientRect(); - // A tab is considered hidden if it extends beyond the right edge of the tab bar - // minus the width of the overflow button (if visible) + // A tab is considered hidden if it is not completely visible + // 2 is added here, because when we scroll till the very end of the tab bar even though, + // the last tab was shown in the overflow menu even though it was completely visible + // this bug was coming because a part of the last tab got hidden inside the dropdown icon const isVisible = tabRect.left >= tabBarRect.left && - tabRect.right <= (tabBarRect.right - overflowButtonWidth); + tabRect.right <= (tabBarRect.right + 2); if (!isVisible) { - // Extract and store information about the hidden tab + // extract and store information about the hidden tab const $tab = $(this); const tabData = { path: $tab.data('path'), @@ -53,10 +57,6 @@ define(function (require, exports, module) { return hiddenTabs; } - function _getNamesFromTabsData(tabsData) { - return tabsData.map(tab => tab.name); - } - /** * Toggles the visibility of the overflow button based on whether @@ -69,8 +69,7 @@ define(function (require, exports, module) { const hiddenTabs = _getListOfHiddenTabs(paneId); if (paneId === "first-pane") { - // for the html elements, refer to ./html/tabbar-pane.html - const $tabBar = $("#phoenix-tab-bar"); + // for the html element, refer to ./html/tabbar-pane.html const $overflowButton = $("#overflow-button"); if (hiddenTabs.length > 0) { @@ -79,8 +78,7 @@ define(function (require, exports, module) { $overflowButton.addClass("hidden"); } } else { - // for the html elements, refer to ./html/tabbar-second-pane.html - const $tabBar = $("#phoenix-tab-bar-2"); + // for the html element, refer to ./html/tabbar-second-pane.html const $overflowButton = $("#overflow-button-2"); if (hiddenTabs.length > 0) { @@ -91,26 +89,149 @@ define(function (require, exports, module) { } } + + /** + * This function is called when the overflow button is clicked + * This will show the overflow context menu + * + * @param {String} paneId - the id of the pane ["first-pane", "second-pane"] + * @param {Number} x - x coordinate for positioning the menu + * @param {Number} y - y coordinate for positioning the menu + */ function showOverflowMenu(paneId, x, y) { const hiddenTabs = _getListOfHiddenTabs(paneId); + // first, remove any existing dropdown menus to prevent duplicates + $(".dropdown-overflow-menu").remove(); + + // create the dropdown + const dropdown = new DropdownButton.DropdownButton("", hiddenTabs, function (item, index) { + const iconHtml = item.$icon[0].outerHTML; // the file icon + const dirtyHtml = item.isDirty + ? '' + : ''; // to display the dirty icon in the overflow menu + + const closeIconHtml = + ` + × + `; + + // return html for this item + return { + html: + ``, + enabled: true // make sure items are enabled + }; + }); + + // add the custom classes for stlying the dropdown + dropdown.dropdownExtraClasses = "dropdown-overflow-menu"; + dropdown.$button.addClass("btn-overflow-tabs"); + // appending to document body. we'll position this with absolute positioning + $("body").append(dropdown.$button); + // position the dropdown where the user clicked + dropdown.$button.css({ + position: "absolute", + left: x + "px", + top: y + "px", + zIndex: 1000 + }); + + // not sure why we need this but without it, the dropdown doesn't work + dropdown.showDropdown(); + + // handle the option selection + // the file that is selected will be opened + dropdown.on("select", function (e, item, index) { + const filePath = item.path; + if (filePath) { + // Set the active pane and open the file + MainViewManager.setActivePaneId(paneId); + CommandManager.execute(Commands.FILE_OPEN, { fullPath: filePath }); + } + }); + + // clean up when the dropdown is closed + dropdown.$button.on("dropdown-closed", function () { + $(document).off("click", ".tab-close-icon-overflow"); + dropdown.$button.remove(); + }); + + // a button was getting displayed on the screen wherever a click was made. not sure why + // but this fixes it + dropdown.$button.css({ + display: "none" + }); } + /** + * Scrolls the tab bar to the active tab + * + * @param {JQuery} $tabBarElement - The tab bar element, + * this is either $('#phoenix-tab-bar') or $('phoenix-tab-bar-2') + */ + function scrollToActiveTab($tabBarElement) { + if (!$tabBarElement || !$tabBarElement.length) { + return; + } + + // make sure there is an active editor + const activeEditor = EditorManager.getActiveEditor(); + if (!activeEditor || !activeEditor.document || !activeEditor.document.file) { + return; + } + + const activePath = activeEditor.document.file.fullPath; + // get the active tab. the active tab is the tab that is currently open + const $activeTab = $tabBarElement.find(`.tab[data-path="${activePath}"]`); + + if ($activeTab.length) { + // get the tab bar container's dimensions + const tabBarRect = $tabBarElement[0].getBoundingClientRect(); + const tabBarVisibleWidth = tabBarRect.width; + + // get the active tab's dimensions + const tabRect = $activeTab[0].getBoundingClientRect(); + + // calculate the tab's position relative to the tab bar container + const tabLeftRelative = tabRect.left - tabBarRect.left; + const tabRightRelative = tabRect.right - tabBarRect.left; + + // get the current scroll position + const currentScroll = $tabBarElement.scrollLeft(); + + // Check if the active tab is fully visible + if (tabLeftRelative < 0) { + // tab is scrolled too far to the left + $tabBarElement.scrollLeft(currentScroll + tabLeftRelative - 10); // 10px padding + } else if (tabRightRelative > tabBarVisibleWidth) { + // tab is scrolled too far to the right - make sure it's visible + const scrollAdjustment = tabRightRelative - tabBarVisibleWidth + 10; // 10px padding + $tabBarElement.scrollLeft(currentScroll + scrollAdjustment); + } + } + } /** * To setup the handlers for the overflow menu */ function setupOverflowHandlers() { - + // handle when the overflow button is clicked for the first pane $(document).on("click", "#overflow-button", function (e) { e.stopPropagation(); showOverflowMenu("first-pane", e.pageX, e.pageY); }); + // for second pane $(document).on("click", "#overflow-button-2", function (e) { e.stopPropagation(); showOverflowMenu("second-pane", e.pageX, e.pageY); @@ -124,6 +245,7 @@ define(function (require, exports, module) { module.exports = { init, - toggleOverflowVisibility + toggleOverflowVisibility, + scrollToActiveTab }; }); diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 90a5e27f34..3347bae7ac 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -212,4 +212,52 @@ 100% { opacity: 0.7; } +} + +.dropdown-tab-item { + display: flex; + align-items: center; + justify-content: flex-start; + cursor: pointer; +} + +.dropdown-tab-item:hover { + background-color: #2A3B50; +} + +.tab-icon-container { + margin-right: 0.25rem; + display: inline-flex; + align-items: center; +} + +.tab-name-container { + flex-grow: 1; + overflow: hidden; + text-overflow: ellipsis; +} + +.tab-dirty-icon { + color: #8D8D8E; + font-size: 1.6rem; + margin-right: 0.25rem; + position: absolute; + left: 0.3rem; + top: 0.25rem; +} + +.tab-close-icon-overflow { + font-size: 0.75rem; + font-weight: 300; + padding: 0.2rem 0.5rem; + margin-left: 0.5rem; + margin-top: -0.1rem; + color: #CCC; + transition: all 0.2s ease; + border-radius: 0.25rem; + pointer-events: none; +} + +.tab-close-icon-overflow:hover { + background-color: rgba(255, 255, 255, 0.1); } \ No newline at end of file From 74475b15af64e51cf3743ee9dd746654d928e003 Mon Sep 17 00:00:00 2001 From: Pluto Date: Mon, 10 Mar 2025 19:56:12 +0530 Subject: [PATCH 15/35] feat: implement close and close active tab in more options context menu --- src/extensionsIntegrated/TabBar/main.js | 69 ++++--------- .../TabBar/more-options.js | 97 +++++++++++++++---- src/nls/root/strings.js | 2 + 3 files changed, 101 insertions(+), 67 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 8d2ab8882b..d9af0e8908 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -368,50 +368,6 @@ define(function (require, exports, module) { } - /** - * Handle close button click on tabs - * This function will remove the file from the working set - * - * @param {String} filePath - path of the file to close - */ - function handleTabClose(filePath) { - // Logic: First open the file we want to close, then close it and finally restore focus - // Why? Because FILE_CLOSE removes the currently active file from the working set - - // Get the current active editor to restore focus later - const currentActiveEditor = EditorManager.getActiveEditor(); - const currentActivePath = currentActiveEditor ? currentActiveEditor.document.file.fullPath : null; - - // Only need to open the file first if it's not the currently active one - if (currentActivePath !== filePath) { - // open the file we want to close - CommandManager.execute(Commands.FILE_OPEN, { fullPath: filePath }) - .done(function () { - // close it - CommandManager.execute(Commands.FILE_CLOSE) - .done(function () { - // If we had a different file active before, restore focus to it - if (currentActivePath && currentActivePath !== filePath) { - CommandManager.execute(Commands.FILE_OPEN, { fullPath: currentActivePath }); - } - }) - .fail(function (error) { - console.error("Failed to close file:", filePath, error); - }); - }) - .fail(function (error) { - console.error("Failed to open file for closing:", filePath, error); - }); - } else { - // if it's already the active file, just close it - CommandManager.execute(Commands.FILE_CLOSE) - .fail(function (error) { - console.error("Failed to close file:", filePath, error); - }); - } - } - - /** * handle click events on the tabs to open the file */ @@ -423,8 +379,20 @@ define(function (require, exports, module) { if ($(event.target).hasClass('fa-times') || $(event.target).closest('.tab-close').length) { // Get the file path from the data-path attribute of the parent tab const filePath = $(this).attr("data-path"); + if (filePath) { - handleTabClose(filePath); + // determine the pane inside which the tab belongs + const isSecondPane = $(this).closest("#phoenix-tab-bar-2").length > 0; + const paneId = isSecondPane ? "second-pane" : "first-pane"; + + // get the file object + const fileObj = FileSystem.getFileForPath(filePath); + // close the file + CommandManager.execute( + Commands.FILE_CLOSE, + { file: fileObj, paneId: paneId } + ); + // Prevent default behavior event.preventDefault(); event.stopPropagation(); @@ -450,17 +418,20 @@ define(function (require, exports, module) { } }); - // Add contextmenu (right-click) handler + // Add the contextmenu (right-click) handler $(document).on("contextmenu", ".tab", function (event) { event.preventDefault(); event.stopPropagation(); - // Determine which pane the tab belongs to + // get the file path from the data-path attribute + const filePath = $(this).attr("data-path"); + + // determine which pane the tab belongs to const isSecondPane = $(this).closest("#phoenix-tab-bar-2").length > 0; const paneId = isSecondPane ? "second-pane" : "first-pane"; - // Show context menu at mouse position - MoreOptions.showMoreOptionsContextMenu(paneId, event.pageX, event.pageY); + // show the context menu at mouse position + MoreOptions.showMoreOptionsContextMenu(paneId, event.pageX, event.pageY, filePath); }); } diff --git a/src/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js index e70cf29d28..fb195f6ba3 100644 --- a/src/extensionsIntegrated/TabBar/more-options.js +++ b/src/extensionsIntegrated/TabBar/more-options.js @@ -1,49 +1,83 @@ /* * This file manages the more options context menu. - * The more option button is present at the right side of the tab bar. - * When clicked, it will show the more options context menu. - * which will have various options related to the tab bar + * 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 MainViewManager = require("view/MainViewManager"); const CommandManager = require("command/CommandManager"); const Commands = require("command/Commands"); + const FileSystem = require("filesystem/FileSystem"); - const Global = require("./global"); - const Helper = require("./helper"); // List of items to show in the context menu // Strings defined in `src/nls/root/strings.js` const items = [ + Strings.CLOSE_TAB, + Strings.CLOSE_ACTIVE_TAB, Strings.CLOSE_ALL_TABS, Strings.CLOSE_UNMODIFIED_TABS, + "---", Strings.REOPEN_CLOSED_FILE ]; /** - * This function is called when the close all tabs option is selected from the context menu + * "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 } + ); + } + } + + + /** + * "CLOSE ACTIVE TAB" + * this closes the currently active tab + * doesn't matter if the context menu is opened from this tab or some other tab + */ + function handleCloseActiveTab() { + // This simply executes the FILE_CLOSE command without parameters + // which will close the currently active file + CommandManager.execute(Commands.FILE_CLOSE); + } + + + /** + * "CLOSE ALL TABS" * This will close all tabs no matter whether they are in first pane or second pane */ function handleCloseAllTabs() { CommandManager.execute(Commands.FILE_CLOSE_ALL); } + /** - * Called when the close unmodified tabs option is selected from the context menu + * "CLOSE UNMODIFIED TABS" * This will close all tabs that are not modified * TODO: implement the functionality */ function handleCloseUnmodifiedTabs() { - // pass } + /** - * Called when the reopen closed file option is selected from the context menu + * "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); @@ -57,8 +91,9 @@ define(function (require, exports, module) { * @param {String} paneId - the id of the pane ["first-pane", "second-pane"] * @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) { + function showMoreOptionsContextMenu(paneId, x, y, filePath) { const dropdown = new DropdownButton.DropdownButton("", items); // Append to document body for absolute positioning @@ -76,13 +111,7 @@ define(function (require, exports, module) { // handle the option selection dropdown.on("select", function (e, item, index) { - if (index === 0) { - handleCloseAllTabs(); - } else if (index === 1) { - handleCloseUnmodifiedTabs(); - } else if (index === 2) { - reopenClosedFile(); - } + _handleSelection(index, filePath, paneId); }); // Remove the button after the dropdown is hidden @@ -91,6 +120,38 @@ define(function (require, exports, module) { }); } + /** + * Handles the selection of an option in the more options context menu + * + * @param {Number} index - the index of the selected option + * @param {String} filePath - the path of the file that was right-clicked + * @param {String} paneId - the id of the pane ["first-pane", "second-pane"] + */ + function _handleSelection(index, filePath, paneId) { + switch (index) { + case 0: + // Close tab (the one that was right-clicked) + handleCloseTab(filePath, paneId); + break; + case 1: + // Close active tab + handleCloseActiveTab(); + break; + case 2: + // Close all tabs + handleCloseAllTabs(); + break; + case 3: + // Close unmodified tabs + handleCloseUnmodifiedTabs(); + break; + case 5: + // Reopen closed file + reopenClosedFile(); + break; + } + } + module.exports = { showMoreOptionsContextMenu }; diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index c35434aee2..0147022f4a 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -427,6 +427,8 @@ define({ "STATUSBAR_TASKS_RESTART": "Restart", // Tab bar Strings + "CLOSE_TAB": "Close Tab", + "CLOSE_ACTIVE_TAB": "Close Active Tab", "CLOSE_ALL_TABS": "Close All Tabs", "CLOSE_UNMODIFIED_TABS": "Close Unmodified Tabs", "REOPEN_CLOSED_FILE": "Reopen Closed File", From 302b856814bbed33e272039eb2e2a7e0f2ff4eb3 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 11 Mar 2025 02:30:00 +0530 Subject: [PATCH 16/35] feat: add close button in overflow menu to work --- src/extensionsIntegrated/TabBar/main.js | 31 +++++++++-- src/extensionsIntegrated/TabBar/overflow.js | 59 +++++++++++++++++---- src/styles/Extn-TabBar.less | 7 ++- 3 files changed, 79 insertions(+), 18 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index d9af0e8908..2941de74b3 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -191,7 +191,9 @@ define(function (require, exports, module) { displayedEntries.forEach(function (entry) { $firstTabBar.append(createTab(entry)); Overflow.toggleOverflowVisibility("first-pane"); - Overflow.scrollToActiveTab($firstTabBar); + setTimeout(function () { + Overflow.scrollToActiveTab($firstTabBar); + }, 0); }); } @@ -209,7 +211,9 @@ define(function (require, exports, module) { displayedEntries2.forEach(function (entry) { $secondTabBar.append(createTab(entry)); Overflow.toggleOverflowVisibility("second-pane"); - Overflow.scrollToActiveTab($firstTabBar); + setTimeout(function () { + Overflow.scrollToActiveTab($secondTabBar); + }, 0); }); } } @@ -341,12 +345,16 @@ define(function (require, exports, module) { // Now that tabs are updated, scroll to the active tab if necessary. if ($firstTabBar.length) { Overflow.toggleOverflowVisibility("first-pane"); - Overflow.scrollToActiveTab($firstTabBar); + setTimeout(function () { + Overflow.scrollToActiveTab($firstTabBar); + }, 0); } if ($secondTabBar.length) { Overflow.toggleOverflowVisibility("second-pane"); - Overflow.scrollToActiveTab($secondTabBar); + setTimeout(function () { + Overflow.scrollToActiveTab($secondTabBar); + }, 0); } } @@ -449,13 +457,28 @@ define(function (require, exports, module) { EditorManager.on("activeEditorChange", updateTabs); // For working set changes, update only the tabs. + MainViewManager.off("workingSetAdd", updateTabs); MainViewManager.on("workingSetAdd", updateTabs); + + MainViewManager.off("workingSetRemove", updateTabs); MainViewManager.on("workingSetRemove", updateTabs); + + MainViewManager.off("workingSetSort", updateTabs); MainViewManager.on("workingSetSort", updateTabs); + + MainViewManager.off("workingSetMove", updateTabs); MainViewManager.on("workingSetMove", updateTabs); + + MainViewManager.off("workingSetAddList", updateTabs); MainViewManager.on("workingSetAddList", updateTabs); + + MainViewManager.off("workingSetRemoveList", updateTabs); MainViewManager.on("workingSetRemoveList", updateTabs); + + MainViewManager.off("workingSetUpdate", updateTabs); MainViewManager.on("workingSetUpdate", updateTabs); + + MainViewManager.off("_workingSetDisableAutoSort", updateTabs); MainViewManager.on("_workingSetDisableAutoSort", updateTabs); // file dirty flag change remains unchanged. diff --git a/src/extensionsIntegrated/TabBar/overflow.js b/src/extensionsIntegrated/TabBar/overflow.js index a013b927a7..c82b5a3e49 100644 --- a/src/extensionsIntegrated/TabBar/overflow.js +++ b/src/extensionsIntegrated/TabBar/overflow.js @@ -6,6 +6,7 @@ define(function (require, exports, module) { const CommandManager = require("command/CommandManager"); const Commands = require("command/Commands"); const EditorManager = require("editor/EditorManager"); + const FileSystem = require("filesystem/FileSystem"); /** @@ -104,6 +105,10 @@ define(function (require, exports, module) { // first, remove any existing dropdown menus to prevent duplicates $(".dropdown-overflow-menu").remove(); + // Create a map to track tabs that are being closed + // Using paths as keys for quick lookup + const closingTabPaths = {}; + // create the dropdown const dropdown = new DropdownButton.DropdownButton("", hiddenTabs, function (item, index) { const iconHtml = item.$icon[0].outerHTML; // the file icon @@ -112,9 +117,9 @@ define(function (require, exports, module) { : ''; // to display the dirty icon in the overflow menu const closeIconHtml = - ` - × - `; + ` + + `; // return html for this item return { @@ -129,7 +134,7 @@ define(function (require, exports, module) { }; }); - // add the custom classes for stlying the dropdown + // add the custom classes for styling the dropdown dropdown.dropdownExtraClasses = "dropdown-overflow-menu"; dropdown.$button.addClass("btn-overflow-tabs"); @@ -144,12 +149,46 @@ define(function (require, exports, module) { zIndex: 1000 }); - // not sure why we need this but without it, the dropdown doesn't work + + // custom handler for close button clicks - must be set up BEFORE showing dropdown + // using one because we only want to run this handler once + $(document).one("mousedown", ".tab-close-icon-overflow", function (e) { + // store the path of the tab being closed + const tabPath = $(this).data("tab-path"); + closingTabPaths[tabPath] = true; + + // we don't stop propagation here - let the event bubble up + // because we want the tab click handler to run as we are not closing the tab here + // Why all this is done instead of simply closing the tab? + // There was no way to stop the tab click handler from running. + // Tried propagating the event, and all other stuff but that didn't work. + // So what used to happen was that the tab used to get closed but then appeared again + + // But we do prevent the default action + e.preventDefault(); + }); + dropdown.showDropdown(); // handle the option selection - // the file that is selected will be opened dropdown.on("select", function (e, item, index) { + // check if this tab was marked for closing + if (closingTabPaths[item.path]) { + // this tab is being closed, so handle the close operation + const file = FileSystem.getFileForPath(item.path); + + if (file) { + // use setTimeout to ensure this happens after all event handlers + setTimeout(function () { + CommandManager.execute(Commands.FILE_CLOSE, { file: file, paneId: paneId }); + // clean up + delete closingTabPaths[item.path]; + }, 0); + } + + return false; + } + // regular tab selection - open the file const filePath = item.path; if (filePath) { // Set the active pane and open the file @@ -160,7 +199,7 @@ define(function (require, exports, module) { // clean up when the dropdown is closed dropdown.$button.on("dropdown-closed", function () { - $(document).off("click", ".tab-close-icon-overflow"); + $(document).off("mousedown", ".tab-close-icon-overflow"); dropdown.$button.remove(); }); @@ -208,12 +247,12 @@ define(function (require, exports, module) { // get the current scroll position const currentScroll = $tabBarElement.scrollLeft(); - // Check if the active tab is fully visible + // Adjust scroll position if the tab is off-screen if (tabLeftRelative < 0) { - // tab is scrolled too far to the left + // tab is too far to the left $tabBarElement.scrollLeft(currentScroll + tabLeftRelative - 10); // 10px padding } else if (tabRightRelative > tabBarVisibleWidth) { - // tab is scrolled too far to the right - make sure it's visible + // tab is too far to the right const scrollAdjustment = tabRightRelative - tabBarVisibleWidth + 10; // 10px padding $tabBarElement.scrollLeft(currentScroll + scrollAdjustment); } diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 3347bae7ac..0988d4fd0c 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -249,15 +249,14 @@ .tab-close-icon-overflow { font-size: 0.75rem; font-weight: 300; - padding: 0.2rem 0.5rem; + padding: 0.3rem 0.6rem; margin-left: 0.5rem; margin-top: -0.1rem; color: #CCC; transition: all 0.2s ease; border-radius: 0.25rem; - pointer-events: none; } -.tab-close-icon-overflow:hover { - background-color: rgba(255, 255, 255, 0.1); +.tab-close-icon-overflow .fa-solid.fa-times:hover { + color: #FFF; } \ No newline at end of file From c24efebb70a83f78fb95db1034535c87483654e3 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 11 Mar 2025 17:14:11 +0530 Subject: [PATCH 17/35] feat: make dirty indicator work in overflow menu --- src/extensionsIntegrated/TabBar/overflow.js | 14 +++++++------ src/styles/Extn-TabBar.less | 23 +++++++++++++++------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/overflow.js b/src/extensionsIntegrated/TabBar/overflow.js index c82b5a3e49..291c2d47df 100644 --- a/src/extensionsIntegrated/TabBar/overflow.js +++ b/src/extensionsIntegrated/TabBar/overflow.js @@ -113,8 +113,8 @@ define(function (require, exports, module) { const dropdown = new DropdownButton.DropdownButton("", hiddenTabs, function (item, index) { const iconHtml = item.$icon[0].outerHTML; // the file icon const dirtyHtml = item.isDirty - ? '' - : ''; // to display the dirty icon in the overflow menu + ? '' + : ''; // adding an empty span for better alignment const closeIconHtml = ` @@ -125,12 +125,14 @@ define(function (require, exports, module) { return { html: ``, - enabled: true // make sure items are enabled + enabled: true }; }); diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 0988d4fd0c..0bbd8d726b 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -217,14 +217,20 @@ .dropdown-tab-item { display: flex; align-items: center; - justify-content: flex-start; cursor: pointer; + justify-content: space-between; } .dropdown-tab-item:hover { background-color: #2A3B50; } +.tab-info-container { + display: flex; + align-items: center; + flex-grow: 1; +} + .tab-icon-container { margin-right: 0.25rem; display: inline-flex; @@ -237,13 +243,18 @@ text-overflow: ellipsis; } -.tab-dirty-icon { +.tab-dirty-icon-overflow { color: #8D8D8E; - font-size: 1.6rem; + font-size: 1.2rem; + width: 1rem; + display: inline-flex; + align-items: center; + justify-content: center; margin-right: 0.25rem; - position: absolute; - left: 0.3rem; - top: 0.25rem; +} + +.tab-dirty-icon-overflow.empty { + visibility: hidden; } .tab-close-icon-overflow { From e6f6e5e205d97f418b5345d2db622c9b78449a49 Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 11 Mar 2025 20:11:32 +0530 Subject: [PATCH 18/35] feat: setup drag and drop for tab bar --- src/extensionsIntegrated/TabBar/drag-drop.js | 402 +++++++++++++++++++ src/extensionsIntegrated/TabBar/main.js | 5 + src/styles/Extn-TabBar.less | 2 +- 3 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/extensionsIntegrated/TabBar/drag-drop.js diff --git a/src/extensionsIntegrated/TabBar/drag-drop.js b/src/extensionsIntegrated/TabBar/drag-drop.js new file mode 100644 index 0000000000..942e8d43a0 --- /dev/null +++ b/src/extensionsIntegrated/TabBar/drag-drop.js @@ -0,0 +1,402 @@ +/* eslint-disable no-invalid-this */ +define(function (require, exports, module) { + const MainViewManager = require("view/MainViewManager"); + + /** + * These variables track the drag and drop state of tabs + * draggedTab: The tab that is currently being dragged + * dragOverTab: The tab that is currently being hovered over + * dragIndicator: Visual indicator showing where the tab will be dropped + * scrollInterval: Used for automatic scrolling when dragging near edges + */ + let draggedTab = null; + let dragOverTab = null; + let dragIndicator = null; + let scrollInterval = null; + + + /** + * Initialize drag and drop functionality for tab bars + * This is called from `main.js` + * This function sets up event listeners for both panes' tab bars + * and creates the visual drag indicator + * + * @param {String} firstPaneSelector - Selector for the first pane tab bar $("#phoenix-tab-bar") + * @param {String} secondPaneSelector - Selector for the second pane tab bar $("#phoenix-tab-bar-2") + */ + function init(firstPaneSelector, secondPaneSelector) { + // setup both the tab bars + setupDragForTabBar(firstPaneSelector); + setupDragForTabBar(secondPaneSelector); + + // setup the container-level drag events to catch drops in empty areas and enable auto-scroll + setupContainerDrag(firstPaneSelector); + setupContainerDrag(secondPaneSelector); + + // Create drag indicator element if it doesn't exist + if (!dragIndicator) { + dragIndicator = $('
'); + $('body').append(dragIndicator); + } + } + + + /** + * Setup drag and drop for a specific tab bar + * Makes tabs draggable and adds all the necessary event listeners + * + * @param {String} tabBarSelector - The selector for the tab bar + */ + function setupDragForTabBar(tabBarSelector) { + const $tabs = $(tabBarSelector).find(".tab"); + + // Make tabs draggable + $tabs.attr("draggable", "true"); + + // Remove any existing event listeners first to prevent duplicates + // This is important when the tab bar is recreated or updated + $tabs.off("dragstart dragover dragenter dragleave drop dragend"); + + // Add drag event listeners to each tab + // Each event has its own handler function for better organization + $tabs.on("dragstart", handleDragStart); + $tabs.on("dragover", handleDragOver); + $tabs.on("dragenter", handleDragEnter); + $tabs.on("dragleave", handleDragLeave); + $tabs.on("drop", handleDrop); + $tabs.on("dragend", handleDragEnd); + } + + + /** + * Setup container-level drag events + * This enables dropping tabs in empty spaces and auto-scrolling + * when dragging near the container's edges + * + * @param {String} containerSelector - The selector for the tab bar container + */ + function setupContainerDrag(containerSelector) { + const $container = $(containerSelector); + + // When dragging over the container but not directly over a tab element + $container.on("dragover", function (e) { + if (e.preventDefault) { + e.preventDefault(); + } + + // Clear any existing scroll interval + if (scrollInterval) { + clearInterval(scrollInterval); + } + + // auto-scroll if near container edge + autoScrollContainer(this, e.originalEvent.clientX); + + // Set up interval for continuous scrolling while dragging near the edge + scrollInterval = setInterval(() => { + if (draggedTab) { // Only continue scrolling if still dragging + autoScrollContainer(this, e.originalEvent.clientX); + } else { + clearInterval(scrollInterval); + scrollInterval = null; + } + }, 16); // this is almost about 60fps + + + // if the target is not a tab, update the drag indicator using the container bounds + if ($(e.target).closest('.tab').length === 0) { + const containerRect = this.getBoundingClientRect(); + const mouseX = e.originalEvent.clientX; + + // determine if dropping on left or right half of container + const onLeftSide = mouseX < (containerRect.left + containerRect.width / 2); + + const $tabs = $container.find('.tab'); + if ($tabs.length) { + // choose the first tab for left drop, last tab for right drop + const targetTab = onLeftSide ? $tabs.first()[0] : $tabs.last()[0]; + updateDragIndicator(targetTab, onLeftSide); + } + } + }); + + // handle drop on the container (empty space) + $container.on("drop", function (e) { + if (e.preventDefault) { + e.preventDefault(); + } + // hide the drag indicator + updateDragIndicator(null); + + // get container dimensions to determine drop position + const containerRect = this.getBoundingClientRect(); + const mouseX = e.originalEvent.clientX; + // determine if dropping on left or right half of container + const onLeftSide = mouseX < (containerRect.left + containerRect.width / 2); + + const $tabs = $container.find('.tab'); + if ($tabs.length) { + // If dropping on left half, target the first tab; otherwise, target the last tab + const targetTab = onLeftSide ? $tabs.first()[0] : $tabs.last()[0]; + + // mkae sure that the draggedTab exists and isn't the same as the target + if (draggedTab && targetTab && draggedTab !== targetTab) { + // check which pane the container belongs to + const isSecondPane = $container.attr("id") === "phoenix-tab-bar-2"; + const paneId = isSecondPane ? "second-pane" : "first-pane"; + const draggedPath = $(draggedTab).attr("data-path"); + const targetPath = $(targetTab).attr("data-path"); + moveWorkingSetItem(paneId, draggedPath, targetPath, onLeftSide); + } + } + }); + } + + + /** + * enhanced auto-scroll function for container when the mouse is near its left or right edge + * creates a smooth scrolling effect with speed based on proximity to the edge + * + * @param {HTMLElement} container - The scrollable container element + * @param {Number} mouseX - The current mouse X coordinate + */ + function autoScrollContainer(container, mouseX) { + const rect = container.getBoundingClientRect(); + const edgeThreshold = 50; // teh threshold distance from the edge + + // Calculate distance from edges + const distanceFromLeft = mouseX - rect.left; + const distanceFromRight = rect.right - mouseX; + + // Determine scroll speed based on distance from edge (closer = faster scroll) + let scrollSpeed = 0; + + if (distanceFromLeft < edgeThreshold) { + // exponential scroll speed: faster as you get closer to the edge + scrollSpeed = -Math.pow(1 - (distanceFromLeft / edgeThreshold), 2) * 15; + } else if (distanceFromRight < edgeThreshold) { + scrollSpeed = Math.pow(1 - (distanceFromRight / edgeThreshold), 2) * 15; + } + + // apply scrolling if needed + if (scrollSpeed !== 0) { + container.scrollLeft += scrollSpeed; + + // If we're already at the edge, don't keep trying to scroll + if ((scrollSpeed < 0 && container.scrollLeft <= 0) || + (scrollSpeed > 0 && container.scrollLeft >= container.scrollWidth - container.clientWidth)) { + return; + } + } + } + + + /** + * Handle the start of a drag operation + * Stores the tab being dragged and adds visual styling + * + * @param {Event} e - The event object + */ + function handleDragStart(e) { + // store reference to the dragged tab + draggedTab = this; + + // set data transfer (required for Firefox) + // Firefox requires data to be set for the drag operation to work + e.originalEvent.dataTransfer.effectAllowed = 'move'; + e.originalEvent.dataTransfer.setData('text/html', this.innerHTML); + + // Add dragging class for styling + $(this).addClass('dragging'); + + // Use a timeout to let the dragging class apply before taking measurements + // This ensures visual updates are applied before we calculate positions + setTimeout(() => { + updateDragIndicator(null); + }, 0); + } + + + /** + * Handle the dragover event to enable drop + * Updates the visual indicator showing where the tab will be dropped + * + * @param {Event} e - The event object + */ + function handleDragOver(e) { + if (e.preventDefault) { + e.preventDefault(); // Allows us to drop + } + e.originalEvent.dataTransfer.dropEffect = 'move'; + + // Update the drag indicator position + // We need to determine if it should be on the left or right side of the target tab + const targetRect = this.getBoundingClientRect(); + const mouseX = e.originalEvent.clientX; + const midPoint = targetRect.left + (targetRect.width / 2); + const onLeftSide = mouseX < midPoint; + + updateDragIndicator(this, onLeftSide); + + return false; + } + + + /** + * Handle entering a potential drop target + * Applies styling to indicate the current drop target + * + * @param {Event} e - The event object + */ + function handleDragEnter(e) { + dragOverTab = this; + $(this).addClass('drag-target'); + } + + + /** + * Handle leaving a potential drop target + * Removes styling when no longer hovering over a drop target + * + * @param {Event} e - The event object + */ + function handleDragLeave(e) { + const relatedTarget = e.originalEvent.relatedTarget; + // Only remove the class if we're truly leaving this tab + // This prevents flickering when moving over child elements + if (!$(this).is(relatedTarget) && !$(this).has(relatedTarget).length) { + $(this).removeClass('drag-target'); + if (dragOverTab === this) { + dragOverTab = null; + } + } + } + + + /** + * Handle dropping a tab onto a target + * Moves the file in the working set to the new position + * + * @param {Event} e - The event object + */ + function handleDrop(e) { + if (e.stopPropagation) { + e.stopPropagation(); // Stops browser from redirecting + } + updateDragIndicator(null); + + // Only process the drop if the dragged tab is different from the drop target + if (draggedTab !== this) { + // Determine which pane the drop target belongs to + const isSecondPane = $(this).closest("#phoenix-tab-bar-2").length > 0; + const paneId = isSecondPane ? "second-pane" : "first-pane"; + const draggedPath = $(draggedTab).attr("data-path"); + const targetPath = $(this).attr("data-path"); + + // Determine if we're dropping to the left or right of the target + const targetRect = this.getBoundingClientRect(); + const mouseX = e.originalEvent.clientX; + const midPoint = targetRect.left + (targetRect.width / 2); + const onLeftSide = mouseX < midPoint; + + // Move the tab in the working set + moveWorkingSetItem(paneId, draggedPath, targetPath, onLeftSide); + } + return false; + } + + + /** + * Handle the end of a drag operation + * Cleans up classes and resets state variables + * + * @param {Event} e - The event object + */ + function handleDragEnd(e) { + $(".tab").removeClass('dragging drag-target'); + updateDragIndicator(null); + draggedTab = null; + dragOverTab = null; + + // Clear scroll interval if it exists + if (scrollInterval) { + clearInterval(scrollInterval); + scrollInterval = null; + } + } + + + /** + * Update the drag indicator position and visibility + * The indicator shows where the tab will be dropped + * + * @param {HTMLElement} targetTab - The tab being dragged over, or null to hide + * @param {Boolean} onLeftSide - Whether the indicator should be on the left or right side + */ + function updateDragIndicator(targetTab, onLeftSide) { + if (!targetTab) { + dragIndicator.hide(); + return; + } + // Get the target tab's position and size + const targetRect = targetTab.getBoundingClientRect(); + if (onLeftSide) { + // Position indicator at the left edge of the target tab + dragIndicator.css({ + top: targetRect.top, + left: targetRect.left, + height: targetRect.height + }); + } else { + // Position indicator at the right edge of the target tab + dragIndicator.css({ + top: targetRect.top, + left: targetRect.right, + height: targetRect.height + }); + } + dragIndicator.show(); + } + + /** + * Move an item in the working set + * This function actually performs the reordering of tabs + * + * @param {String} paneId - The ID of the pane ("first-pane" or "second-pane") + * @param {String} draggedPath - Path of the dragged file + * @param {String} targetPath - Path of the drop target file + * @param {Boolean} beforeTarget - Whether to place before or after the target + */ + function moveWorkingSetItem(paneId, draggedPath, targetPath, beforeTarget) { + const workingSet = MainViewManager.getWorkingSet(paneId); + let draggedIndex = -1; + let targetIndex = -1; + + // Find the indices of both the dragged item and the target item + for (let i = 0; i < workingSet.length; i++) { + if (workingSet[i].fullPath === draggedPath) { + draggedIndex = i; + } + if (workingSet[i].fullPath === targetPath) { + targetIndex = i; + } + } + + // Only move if we found both items + if (draggedIndex !== -1 && targetIndex !== -1) { + // Calculate the new position based on whether we're inserting before or after the target + let newPosition = beforeTarget ? targetIndex : targetIndex + 1; + // Adjust position if the dragged item is before the target + // This is necessary because removing the dragged item will shift all following items + if (draggedIndex < newPosition) { + newPosition--; + } + // Perform the actual move in the MainViewManager + MainViewManager._moveWorkingSetItem(paneId, draggedIndex, newPosition); + } + } + + module.exports = { + init + }; +}); diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 2941de74b3..c5d6356bde 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -15,6 +15,7 @@ define(function (require, exports, module) { const Preference = require("./preference"); const MoreOptions = require("./more-options"); const Overflow = require("./overflow"); + const DragDrop = require("./drag-drop"); const TabBarHTML = require("text!./html/tabbar-pane.html"); const TabBarHTML2 = require("text!./html/tabbar-second-pane.html"); @@ -356,6 +357,9 @@ define(function (require, exports, module) { Overflow.scrollToActiveTab($secondTabBar); }, 0); } + + // handle drag and drop + DragDrop.init($('#phoenix-tab-bar'), $('#phoenix-tab-bar-2')); } @@ -547,5 +551,6 @@ define(function (require, exports, module) { handleTabClick(); Overflow.init(); + DragDrop.init($('#phoenix-tab-bar'), $('#phoenix-tab-bar-2')); }); }); diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 0bbd8d726b..7045446938 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -246,7 +246,7 @@ .tab-dirty-icon-overflow { color: #8D8D8E; font-size: 1.2rem; - width: 1rem; + width: 0.2rem; display: inline-flex; align-items: center; justify-content: center; From 70808093787bcd488bc309567c085c3a91b50954 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 12 Mar 2025 20:08:52 +0530 Subject: [PATCH 19/35] fix: when sidebar gets resized the tabs get updated for correct overflow status --- src/extensionsIntegrated/TabBar/main.js | 40 +++++++++++-------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index c5d6356bde..bdf35e3311 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -461,29 +461,23 @@ define(function (require, exports, module) { EditorManager.on("activeEditorChange", updateTabs); // For working set changes, update only the tabs. - MainViewManager.off("workingSetAdd", updateTabs); - MainViewManager.on("workingSetAdd", updateTabs); - - MainViewManager.off("workingSetRemove", updateTabs); - MainViewManager.on("workingSetRemove", updateTabs); - - MainViewManager.off("workingSetSort", updateTabs); - MainViewManager.on("workingSetSort", updateTabs); - - MainViewManager.off("workingSetMove", updateTabs); - MainViewManager.on("workingSetMove", updateTabs); - - MainViewManager.off("workingSetAddList", updateTabs); - MainViewManager.on("workingSetAddList", updateTabs); - - MainViewManager.off("workingSetRemoveList", updateTabs); - MainViewManager.on("workingSetRemoveList", updateTabs); - - MainViewManager.off("workingSetUpdate", updateTabs); - MainViewManager.on("workingSetUpdate", updateTabs); - - MainViewManager.off("_workingSetDisableAutoSort", updateTabs); - MainViewManager.on("_workingSetDisableAutoSort", updateTabs); + const events = [ + "workingSetAdd", + "workingSetRemove", + "workingSetSort", + "workingSetMove", + "workingSetAddList", + "workingSetRemoveList", + "workingSetUpdate", + "_workingSetDisableAutoSort" + ]; + MainViewManager.off(events.join(" "), updateTabs); + MainViewManager.on(events.join(" "), updateTabs); + + // When the sidebar UI changes, update the tabs to ensure the overflow menu is correct. + // Without this, if the sidebar is hidden, and **all tabs become visible**, the overflow icon still appears. + $("#sidebar").off("panelCollapsed panelExpanded panelResizeEnd", updateTabs); + $("#sidebar").on("panelCollapsed panelExpanded panelResizeEnd", updateTabs); // file dirty flag change remains unchanged. DocumentManager.on("dirtyFlagChange", function (event, doc) { From 90c48e27ece0fd879f4ca5ff257f522f28a695c0 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 13 Mar 2025 15:22:50 +0530 Subject: [PATCH 20/35] fix: udpate tab bar when live preview gets resized --- src/extensionsIntegrated/TabBar/main.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index bdf35e3311..f9b6ae7d2e 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -479,6 +479,10 @@ define(function (require, exports, module) { $("#sidebar").off("panelCollapsed panelExpanded panelResizeEnd", updateTabs); $("#sidebar").on("panelCollapsed panelExpanded panelResizeEnd", updateTabs); + // also update the tabs when the main plugin panel resizes + // main-plugin-panel[0] = live preview panel + new ResizeObserver(updateTabs).observe($("#main-plugin-panel")[0]); + // file dirty flag change remains unchanged. DocumentManager.on("dirtyFlagChange", function (event, doc) { const filePath = doc.file.fullPath; From cf8f718887d576344d22ffec22fb65931488f3fa Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 14 Mar 2025 21:59:08 +0530 Subject: [PATCH 21/35] fix: tab bar disappearing bug --- src/extensionsIntegrated/TabBar/main.js | 20 ++++++++++++-------- src/extensionsIntegrated/TabBar/overflow.js | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index f9b6ae7d2e..a461c1a694 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -233,8 +233,9 @@ define(function (require, exports, module) { if ($('.not-editor').length === 1) { $tabBar = $(TabBarHTML); - // since we need to add the tab bar before the editor area, we target the `#editor-holder` class and prepend - $("#editor-holder").prepend($tabBar); + // since we need to add the tab bar before the editor which has .not-editor class + // we target the `.not-editor` class and add the tab bar before it + $(".not-editor").before($tabBar); setTimeout(function () { WorkspaceManager.recomputeLayout(true); }, 0); @@ -452,9 +453,13 @@ define(function (require, exports, module) { * Registers the event handlers */ function registerHandlers() { - // For pane changes, still recreate the entire tab bar container. - MainViewManager.off("activePaneChange paneCreate paneDestroy paneLayoutChange", createTabBar); - MainViewManager.on("activePaneChange paneCreate paneDestroy paneLayoutChange", createTabBar); + // For pane layout changes, recreate the entire tab bar container + MainViewManager.off("paneCreate paneDestroy paneLayoutChange", createTabBar); + MainViewManager.on("paneCreate paneDestroy paneLayoutChange", createTabBar); + + // For active pane changes, update only the tabs + MainViewManager.off("activePaneChange", updateTabs); + MainViewManager.on("activePaneChange", updateTabs); // For editor changes, update only the tabs. EditorManager.off("activeEditorChange", updateTabs); @@ -474,8 +479,7 @@ define(function (require, exports, module) { MainViewManager.off(events.join(" "), updateTabs); MainViewManager.on(events.join(" "), updateTabs); - // When the sidebar UI changes, update the tabs to ensure the overflow menu is correct. - // Without this, if the sidebar is hidden, and **all tabs become visible**, the overflow icon still appears. + // When the sidebar UI changes, update the tabs to ensure the overflow menu is correct $("#sidebar").off("panelCollapsed panelExpanded panelResizeEnd", updateTabs); $("#sidebar").on("panelCollapsed panelExpanded panelResizeEnd", updateTabs); @@ -483,7 +487,7 @@ define(function (require, exports, module) { // main-plugin-panel[0] = live preview panel new ResizeObserver(updateTabs).observe($("#main-plugin-panel")[0]); - // file dirty flag change remains unchanged. + // File dirty flag change handling DocumentManager.on("dirtyFlagChange", function (event, doc) { const filePath = doc.file.fullPath; diff --git a/src/extensionsIntegrated/TabBar/overflow.js b/src/extensionsIntegrated/TabBar/overflow.js index 291c2d47df..1c020918b1 100644 --- a/src/extensionsIntegrated/TabBar/overflow.js +++ b/src/extensionsIntegrated/TabBar/overflow.js @@ -249,14 +249,20 @@ define(function (require, exports, module) { // get the current scroll position const currentScroll = $tabBarElement.scrollLeft(); - // Adjust scroll position if the tab is off-screen + // animate the scroll change over 5 for a very fast effect if (tabLeftRelative < 0) { - // tab is too far to the left - $tabBarElement.scrollLeft(currentScroll + tabLeftRelative - 10); // 10px padding + // Tab is too far to the left + $tabBarElement.animate( + { scrollLeft: currentScroll + tabLeftRelative - 10 }, + 5 + ); } else if (tabRightRelative > tabBarVisibleWidth) { - // tab is too far to the right - const scrollAdjustment = tabRightRelative - tabBarVisibleWidth + 10; // 10px padding - $tabBarElement.scrollLeft(currentScroll + scrollAdjustment); + // Tab is too far to the right + const scrollAdjustment = tabRightRelative - tabBarVisibleWidth + 10; + $tabBarElement.animate( + { scrollLeft: currentScroll + scrollAdjustment }, + 5 + ); } } } From 435d565989035e36d7cf1fc9cd463185c8f9b1e9 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sat, 15 Mar 2025 15:07:22 +0530 Subject: [PATCH 22/35] fix: if both panes have same file, we open that file only on desired pane --- src/extensionsIntegrated/TabBar/main.js | 48 +++++++++++++++---------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index a461c1a694..63e5a2c816 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -92,17 +92,22 @@ define(function (require, exports, module) { * Note: this creates a tab (for a single file) not the tab bar * * @param {Object} entry - the working set entry + * @param {String} paneId - the pane id 'first-pane' or 'second-pane' * @returns {$.Element} the tab element */ - function createTab(entry) { - if (!$tabBar) { + function createTab(entry, paneId) { + if (!$tabBar || !paneId) { return; } // set up all the necessary properties const activeEditor = EditorManager.getActiveEditor(); const activePath = activeEditor ? activeEditor.document.file.fullPath : null; - const isActive = entry.path === activePath; // if the file is the currently active file + + const currentActivePane = MainViewManager.getActivePaneId(); + // if the file is the currently active file + // also verify that the tab belongs to the active pane + const isActive = (entry.path === activePath && paneId === currentActivePane); const isDirty = Helper._isFileModified(FileSystem.getFileForPath(entry.path)); // if the file is dirty // Create the tab element with the structure we need @@ -190,7 +195,7 @@ define(function (require, exports, module) { // add each tab to the first pane's tab bar displayedEntries.forEach(function (entry) { - $firstTabBar.append(createTab(entry)); + $firstTabBar.append(createTab(entry, "first-pane")); Overflow.toggleOverflowVisibility("first-pane"); setTimeout(function () { Overflow.scrollToActiveTab($firstTabBar); @@ -210,7 +215,7 @@ define(function (require, exports, module) { } displayedEntries2.forEach(function (entry) { - $secondTabBar.append(createTab(entry)); + $secondTabBar.append(createTab(entry, "second-pane")); Overflow.toggleOverflowVisibility("second-pane"); setTimeout(function () { Overflow.scrollToActiveTab($secondTabBar); @@ -304,7 +309,7 @@ define(function (require, exports, module) { } } displayedEntries.forEach(function (entry) { - $firstTabBar.append(createTab(entry)); + $firstTabBar.append(createTab(entry, "first-pane")); }); } } @@ -330,7 +335,7 @@ define(function (require, exports, module) { } } displayedEntries2.forEach(function (entry) { - $secondTabBar.append(createTab(entry)); + $secondTabBar.append(createTab(entry, "second-pane")); }); } } @@ -344,19 +349,30 @@ define(function (require, exports, module) { Helper._hideTabBar($('#phoenix-tab-bar-2'), $('#overflow-button-2')); } + const activePane = MainViewManager.getActivePaneId(); + // Now that tabs are updated, scroll to the active tab if necessary. if ($firstTabBar.length) { Overflow.toggleOverflowVisibility("first-pane"); - setTimeout(function () { - Overflow.scrollToActiveTab($firstTabBar); - }, 0); + + // we scroll only in the active pane + // this is because, lets say we have a same file in both the panes + // then when the file is opened in one of the pane and is towards the end of the tab bar, + // then we need to show the scrolling animation only on that pane and not on both the panes + if (activePane === "first-pane") { + setTimeout(function () { + Overflow.scrollToActiveTab($firstTabBar); + }, 0); + } } if ($secondTabBar.length) { Overflow.toggleOverflowVisibility("second-pane"); - setTimeout(function () { - Overflow.scrollToActiveTab($secondTabBar); - }, 0); + if (activePane === "second-pane") { + setTimeout(function () { + Overflow.scrollToActiveTab($secondTabBar); + }, 0); + } } // handle drag and drop @@ -417,12 +433,6 @@ define(function (require, exports, module) { const filePath = $(this).attr("data-path"); if (filePath) { - // we need to determine which pane the tab belongs to - const isSecondPane = $(this).closest("#phoenix-tab-bar-2").length > 0; - const paneId = isSecondPane ? "second-pane" : "first-pane"; - - // Set the active pane and open the file - MainViewManager.setActivePaneId(paneId); CommandManager.execute(Commands.FILE_OPEN, { fullPath: filePath }); // Prevent default behavior From ed2ff7497290dfb4a31869fb5b16b696a4cc66d4 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 16 Mar 2025 14:22:23 +0530 Subject: [PATCH 23/35] chore: improve styling --- src/extensionsIntegrated/TabBar/main.js | 6 ++--- src/styles/Extn-TabBar.less | 35 +++++++++++++------------ 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 63e5a2c816..aff4bb7913 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -240,8 +240,8 @@ define(function (require, exports, module) { $tabBar = $(TabBarHTML); // since we need to add the tab bar before the editor which has .not-editor class // we target the `.not-editor` class and add the tab bar before it - $(".not-editor").before($tabBar); setTimeout(function () { + $(".not-editor").before($tabBar); WorkspaceManager.recomputeLayout(true); }, 0); @@ -253,9 +253,9 @@ define(function (require, exports, module) { // here #editor-holder cannot be used as in split view, we only have one #editor-holder // so, right now we are using .not-editor. Maybe we need to look for some better selector // TODO: Fix bug where the tab bar gets hidden inside the editor in horizontal split - $(".not-editor").eq(0).before($tabBar); - $(".not-editor").eq(1).before($tabBar2); setTimeout(function () { + $(".not-editor").eq(0).before($tabBar); + $(".not-editor").eq(1).before($tabBar2); WorkspaceManager.recomputeLayout(true); }, 0); } diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 7045446938..0c6581b890 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -10,7 +10,7 @@ .phoenix-tab-bar { flex: 1; - height: 2rem; + height: 2.2rem; display: flex; overflow-x: auto; overflow-y: hidden; @@ -94,11 +94,17 @@ .tab-name { + color: #aaa; display: inline-flex; align-items: center; - font-size: 0.75rem; + font-size: 0.9rem; letter-spacing: 0.4px; word-spacing: 0.75px; + white-space: nowrap; +} + +.tab.active .tab-name { + color: #dedede; } @@ -126,7 +132,7 @@ bottom: 0; left: 0; right: 0; - height: 0.15rem; + height: 0.12rem; background-color: #75BEFF; } @@ -135,26 +141,23 @@ content: "•"; color: #8D8D8E; font-size: 1.6rem; - margin-right: 0.25rem; position: absolute; - left: 0.3rem; - top: 0.25rem; + left: 0.4rem; + top: 0.3rem; } .tab.dirty .tab-icon { - margin-left: 1rem; + margin-left: 0.8rem; } .tab-close { font-size: 0.75rem; - font-weight: 300; - padding: 0.2rem 0.5rem; - margin-left: 0.5rem; - margin-top: -0.1rem; + padding: 0.08rem 0.4rem; + margin-left: 0.25rem; color: #CCC; transition: all 0.2s ease; - border-radius: 0.25rem; + border-radius: 0.2rem; visibility: hidden; opacity: 0; pointer-events: none; @@ -163,7 +166,7 @@ .tab:hover .tab-close, .tab.active .tab-close { - visibility: visible; + visibility: visible;; opacity: 1; pointer-events: auto; } @@ -245,12 +248,10 @@ .tab-dirty-icon-overflow { color: #8D8D8E; - font-size: 1.2rem; + font-size: 1.6rem; width: 0.2rem; display: inline-flex; - align-items: center; - justify-content: center; - margin-right: 0.25rem; + margin: 0.125rem 0.6rem 0.4rem -0.2rem; } .tab-dirty-icon-overflow.empty { From 05f6aed79f843c068a088d808e1cabfa81091fb3 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 16 Mar 2025 15:09:09 +0530 Subject: [PATCH 24/35] fix: tab bar disappearing when only one file is added in split view case --- src/extensionsIntegrated/TabBar/main.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index aff4bb7913..1adfbb4bb0 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -243,6 +243,7 @@ define(function (require, exports, module) { setTimeout(function () { $(".not-editor").before($tabBar); WorkspaceManager.recomputeLayout(true); + updateTabs(); }, 0); } else if ($('.not-editor').length === 2) { @@ -257,6 +258,7 @@ define(function (require, exports, module) { $(".not-editor").eq(0).before($tabBar); $(".not-editor").eq(1).before($tabBar2); WorkspaceManager.recomputeLayout(true); + updateTabs(); }, 0); } @@ -277,9 +279,14 @@ define(function (require, exports, module) { // In a vertical split, when no files are present in 'second-pane' so the tab bar is hidden. // Now, when the user adds a file in 'second-pane', the tab bar should be shown but since updateTabs() only, // updates the tabs, so the tab bar never gets created. - if (Global.firstPaneWorkingSet.length === 1 || Global.secondPaneWorkingSet.length === 1) { + if (Global.firstPaneWorkingSet.length === 1 && + (!$('#phoenix-tab-bar').length || $('#phoenix-tab-bar').is(':hidden'))) { + createTabBar(); + } + + if (Global.secondPaneWorkingSet.length === 1 && + (!$('#phoenix-tab-bar-2').length || $('#phoenix-tab-bar-2').is(':hidden'))) { createTabBar(); - return; } const $firstTabBar = $('#phoenix-tab-bar'); From 71cdd2bd67a1bb9d871a694e5f85f5bfe381e242 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 16 Mar 2025 15:47:15 +0530 Subject: [PATCH 25/35] fix: tab bar not appearing when opening image file --- src/styles/Extn-TabBar.less | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 0c6581b890..69b8fc3f11 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -5,12 +5,13 @@ border-bottom: 1px solid #333; position: relative; overflow: hidden; + z-index: 2; } .phoenix-tab-bar { flex: 1; - height: 2.2rem; + height: 2.1rem; display: flex; overflow-x: auto; overflow-y: hidden; @@ -97,7 +98,7 @@ color: #aaa; display: inline-flex; align-items: center; - font-size: 0.9rem; + font-size: 0.85rem; letter-spacing: 0.4px; word-spacing: 0.75px; white-space: nowrap; From 233f29a2844b12b8af6d61abc22f31e94c676791 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 16 Mar 2025 16:41:41 +0530 Subject: [PATCH 26/35] chore: improve styling --- 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 69b8fc3f11..35320ac10c 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -66,7 +66,7 @@ .tab { display: inline-flex; align-items: center; - padding: 0 0.5rem; + padding: 0 0.5rem 0 0.85rem; height: 100%; background-color: #2D2D2D; border-right: 1px solid #333; @@ -143,8 +143,8 @@ color: #8D8D8E; font-size: 1.6rem; position: absolute; - left: 0.4rem; - top: 0.3rem; + left: 0.75rem; + top: 0.25rem; } .tab.dirty .tab-icon { @@ -167,7 +167,7 @@ .tab:hover .tab-close, .tab.active .tab-close { - visibility: visible;; + visibility: visible; opacity: 1; pointer-events: auto; } From 12001d5aa9d9dbffb036122d29896b0b3fe35f77 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 16 Mar 2025 17:05:49 +0530 Subject: [PATCH 27/35] feat: implement close unmodified tabs feature --- .../TabBar/more-options.js | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js index fb195f6ba3..f5b973e943 100644 --- a/src/extensionsIntegrated/TabBar/more-options.js +++ b/src/extensionsIntegrated/TabBar/more-options.js @@ -8,7 +8,9 @@ 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 Global = require("./global"); // List of items to show in the context menu // Strings defined in `src/nls/root/strings.js` @@ -67,10 +69,37 @@ define(function (require, exports, module) { /** * "CLOSE UNMODIFIED TABS" * This will close all tabs that are not modified - * TODO: implement the functionality */ function handleCloseUnmodifiedTabs() { - // pass + const paneList = MainViewManager.getPaneIdList(); + + // for the first pane + if (paneList.length > 0 && Global.firstPaneWorkingSet.length > 0) { + // get all those entries that are not dirty + const unmodifiedEntries = Global.firstPaneWorkingSet.filter(entry => !entry.isDirty); + + // close each unmodified file in the first pane + unmodifiedEntries.forEach(entry => { + const fileObj = FileSystem.getFileForPath(entry.path); + CommandManager.execute( + Commands.FILE_CLOSE, + { file: fileObj, paneId: "first-pane" } + ); + }); + } + + // for second pane + if (paneList.length > 1 && Global.secondPaneWorkingSet.length > 0) { + const unmodifiedEntries = Global.secondPaneWorkingSet.filter(entry => !entry.isDirty); + + unmodifiedEntries.forEach(entry => { + const fileObj = FileSystem.getFileForPath(entry.path); + CommandManager.execute( + Commands.FILE_CLOSE, + { file: fileObj, paneId: "second-pane" } + ); + }); + } } From c601e6188dee5649d54e23b1dc8fce2dea007477 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 16 Mar 2025 17:47:00 +0530 Subject: [PATCH 28/35] chore: make tab bar style compatible for light theme --- src/styles/Extn-TabBar.less | 118 ++++++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 26 deletions(-) diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less index 35320ac10c..d67ac2bfd9 100644 --- a/src/styles/Extn-TabBar.less +++ b/src/styles/Extn-TabBar.less @@ -1,13 +1,17 @@ .tab-bar-container { display: flex; align-items: center; - background-color: #1E1E1E; - border-bottom: 1px solid #333; + background-color: #f5f5f5; + border-bottom: 1px solid #ddd; position: relative; overflow: hidden; z-index: 2; } +.dark .tab-bar-container { + background-color: #1E1E1E; + border-bottom: 1px solid #333; +} .phoenix-tab-bar { flex: 1; @@ -18,9 +22,12 @@ white-space: nowrap; transition: height 0.3s ease; scroll-behavior: smooth; - background-color: #1E1E1E; + background-color: #f5f5f5; } +.dark .phoenix-tab-bar { + background-color: #1E1E1E; +} .phoenix-tab-bar::-webkit-scrollbar { height: 0.25rem; @@ -43,12 +50,15 @@ display: none; } - .overflow-button:hover, .overflow-button-2:hover { - background-color: #333; + background-color: #e0e0e0; } +.dark .overflow-button:hover, +.dark .overflow-button-2:hover { + background-color: #333; +} .overflow-button::before, .overflow-button-2::before { @@ -59,17 +69,21 @@ width: 1rem; height: 100%; pointer-events: none; - background: linear-gradient(to right, rgba(30, 30, 30, 0), #1E1E1E); + background: linear-gradient(to right, rgba(245, 245, 245, 0), #f5f5f5); } +.dark .overflow-button::before, +.dark .overflow-button-2::before { + background: linear-gradient(to right, rgba(30, 30, 30, 0), #1E1E1E); +} .tab { display: inline-flex; align-items: center; padding: 0 0.5rem 0 0.85rem; height: 100%; - background-color: #2D2D2D; - border-right: 1px solid #333; + background-color: #F8F8F8; + border-right: 1px solid #ddd; cursor: pointer; position: relative; flex: 0 0 auto; @@ -78,6 +92,10 @@ transition: transform 60ms ease, opacity 60ms ease; } +.dark .tab { + background-color: #2D2D2D; + border-right: 1px solid #333; +} .tab, .tab-close, @@ -86,16 +104,14 @@ transition: all 120ms ease-out; } - .tab-icon { display: flex; align-items: center; margin-bottom: -2px; } - .tab-name { - color: #aaa; + color: #555; display: inline-flex; align-items: center; font-size: 0.85rem; @@ -104,10 +120,17 @@ white-space: nowrap; } +.dark .tab-name { + color: #aaa; +} + .tab.active .tab-name { - color: #dedede; + color: #333; } +.dark .tab.active .tab-name { + color: #dedede; +} .tab .tab-dirname { font-size: 0.65rem; @@ -115,17 +138,22 @@ font-weight: normal; } - .tab.active { - background-color: #3D3D3D; + background-color: #fff; } +.dark .tab.active { + background-color: #3D3D3D; +} .tab:hover { - background-color: #4d4949; + background-color: #f0f0f0; cursor: pointer; } +.dark .tab:hover { + background-color: #4d4949; +} .tab.active::after { content: ""; @@ -134,29 +162,35 @@ left: 0; right: 0; height: 0.12rem; - background-color: #75BEFF; + background-color: #0078D7; } +.dark .tab.active::after { + background-color: #75BEFF; +} .tab.dirty::before { content: "•"; - color: #8D8D8E; + color: #888; font-size: 1.6rem; position: absolute; left: 0.75rem; top: 0.25rem; } +.dark .tab.dirty::before { + color: #8D8D8E; +} + .tab.dirty .tab-icon { margin-left: 0.8rem; } - .tab-close { font-size: 0.75rem; padding: 0.08rem 0.4rem; margin-left: 0.25rem; - color: #CCC; + color: #666; transition: all 0.2s ease; border-radius: 0.2rem; visibility: hidden; @@ -164,6 +198,9 @@ pointer-events: none; } +.dark .tab-close { + color: #CCC; +} .tab:hover .tab-close, .tab.active .tab-close { @@ -172,37 +209,50 @@ pointer-events: auto; } - .tab-close:hover { cursor: pointer; - background-color: rgba(255, 255, 255, 0.1); + background-color: rgba(0, 0, 0, 0.1); } +.dark .tab-close:hover { + background-color: rgba(255, 255, 255, 0.1); +} .tab.dragging { opacity: 0.7; transform: scale(0.95); cursor: grabbing !important; z-index: 10000; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.dark .tab.dragging { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); } .tab.drag-target { - background-color: #383838; + background-color: #e5e5e5; } +.dark .tab.drag-target { + background-color: #383838; +} .tab-drag-indicator { position: fixed; width: 2px; - background-color: #75BEFF; + background-color: #0078D7; pointer-events: none; z-index: 10001; - box-shadow: 0 0 3px rgba(117, 190, 255, 0.5); + box-shadow: 0 0 3px rgba(0, 120, 215, 0.5); display: none; animation: pulse 1.5s infinite; } +.dark .tab-drag-indicator { + background-color: #75BEFF; + box-shadow: 0 0 3px rgba(117, 190, 255, 0.5); +} @keyframes pulse { 0% { @@ -226,6 +276,10 @@ } .dropdown-tab-item:hover { + background-color: #e8f0fa; +} + +.dark .dropdown-tab-item:hover { background-color: #2A3B50; } @@ -248,13 +302,17 @@ } .tab-dirty-icon-overflow { - color: #8D8D8E; + color: #888; font-size: 1.6rem; width: 0.2rem; display: inline-flex; margin: 0.125rem 0.6rem 0.4rem -0.2rem; } +.dark .tab-dirty-icon-overflow { + color: #8D8D8E; +} + .tab-dirty-icon-overflow.empty { visibility: hidden; } @@ -265,11 +323,19 @@ padding: 0.3rem 0.6rem; margin-left: 0.5rem; margin-top: -0.1rem; - color: #CCC; + color: #666; transition: all 0.2s ease; border-radius: 0.25rem; } +.dark .tab-close-icon-overflow { + color: #CCC; +} + .tab-close-icon-overflow .fa-solid.fa-times:hover { + color: #333; +} + +.dark .tab-close-icon-overflow .fa-solid.fa-times:hover { color: #FFF; } \ No newline at end of file From fd6bd125d0410656e385df8ceae6f136914ddace Mon Sep 17 00:00:00 2001 From: Pluto Date: Tue, 18 Mar 2025 00:52:02 +0530 Subject: [PATCH 29/35] fix: unit tests failing issue --- src/extensionsIntegrated/TabBar/main.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 1adfbb4bb0..2a3e013ef5 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -239,27 +239,20 @@ define(function (require, exports, module) { if ($('.not-editor').length === 1) { $tabBar = $(TabBarHTML); // since we need to add the tab bar before the editor which has .not-editor class - // we target the `.not-editor` class and add the tab bar before it - setTimeout(function () { - $(".not-editor").before($tabBar); - WorkspaceManager.recomputeLayout(true); - updateTabs(); - }, 0); + $(".not-editor").before($tabBar); + WorkspaceManager.recomputeLayout(true); + updateTabs(); } else if ($('.not-editor').length === 2) { $tabBar = $(TabBarHTML); $tabBar2 = $(TabBarHTML2); // eq(0) is for the first pane and eq(1) is for the second pane - // here #editor-holder cannot be used as in split view, we only have one #editor-holder - // so, right now we are using .not-editor. Maybe we need to look for some better selector // TODO: Fix bug where the tab bar gets hidden inside the editor in horizontal split - setTimeout(function () { - $(".not-editor").eq(0).before($tabBar); - $(".not-editor").eq(1).before($tabBar2); - WorkspaceManager.recomputeLayout(true); - updateTabs(); - }, 0); + $(".not-editor").eq(0).before($tabBar); + $(".not-editor").eq(1).before($tabBar2); + WorkspaceManager.recomputeLayout(true); + updateTabs(); } setupTabBar(); From 344dfade0d9caf72dfb860e986101daabc7f0f16 Mon Sep 17 00:00:00 2001 From: Pluto Date: Wed, 19 Mar 2025 18:06:24 +0530 Subject: [PATCH 30/35] fix: integ tests failing --- src/extensions/default/Phoenix-prettier/unittests.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/extensions/default/Phoenix-prettier/unittests.js b/src/extensions/default/Phoenix-prettier/unittests.js index 71ec32f0ac..42b73a2a9b 100644 --- a/src/extensions/default/Phoenix-prettier/unittests.js +++ b/src/extensions/default/Phoenix-prettier/unittests.js @@ -32,6 +32,10 @@ define(function (require, exports, module) { const PLATFORM_LINE_ENDINGS = (FileUtils.getPlatformLineEndings() === 'CRLF' ? "\r\n" : "\n"); + function normalizeLineEndings(str) { + return str.replace(/\r\n/g, "\n"); + } + require("./main"); const jsFile = require("text!../../../../test/spec/prettier-test-files/js/test.js"), @@ -154,7 +158,9 @@ define(function (require, exports, module) { createMockEditor(jsFile, "javascript", "/test.js"); testEditor.setSelection({line: 4, ch: 0}, {line: 6, ch: 0}); await BeautificationManager.beautifyEditor(testEditor); - expect(testEditor.document.getText(true)+ PLATFORM_LINE_ENDINGS).toBe(jsPrettySelectionOffset); + const actual = normalizeLineEndings(testEditor.document.getText(true) + PLATFORM_LINE_ENDINGS); + const expected = normalizeLineEndings(jsPrettySelectionOffset); + expect(actual).toBe(expected); }); it("should not beautify editor on incomplete syntax selection for js", async function () { @@ -209,7 +215,9 @@ define(function (require, exports, module) { createMockEditor(htmlFile, "html", "/test.html"); testEditor.setSelection({line: 4, ch: 0}, {line: 6, ch: 0}); await BeautificationManager.beautifyEditor(testEditor); - expect(testEditor.document.getText(true)+ PLATFORM_LINE_ENDINGS).toBe(htmlPrettySelectionOffset); + const actual = normalizeLineEndings(testEditor.document.getText(true) + PLATFORM_LINE_ENDINGS); + const expected = normalizeLineEndings(htmlPrettySelectionOffset); + expect(actual).toBe(expected); }); it("should not beautify editor on incomplete syntax selection for html", async function () { From 48b85b2231ac80a212f8722a8933a30a3a87f570 Mon Sep 17 00:00:00 2001 From: Pluto Date: Thu, 20 Mar 2025 19:32:27 +0530 Subject: [PATCH 31/35] fix: integ tests failing in firefox --- src/extensionsIntegrated/TabBar/main.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 2a3e013ef5..479b988425 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -1,5 +1,6 @@ /* eslint-disable no-invalid-this */ define(function (require, exports, module) { + const _ = require("thirdparty/lodash"); const AppInit = require("utils/AppInit"); const MainViewManager = require("view/MainViewManager"); const EditorManager = require("editor/EditorManager"); @@ -473,7 +474,11 @@ define(function (require, exports, module) { // For editor changes, update only the tabs. EditorManager.off("activeEditorChange", updateTabs); - EditorManager.on("activeEditorChange", updateTabs); + // debounce is used to prevent rapid consecutive calls to updateTabs, + // which was causing integration tests to fail in Firefox. Without it, + // the event fires too frequently when switching editors, leading to unexpected behavior + const debounceUpdateTabs = _.debounce(updateTabs, 2); + EditorManager.on("activeEditorChange", debounceUpdateTabs); // For working set changes, update only the tabs. const events = [ From 59e93c2a7c22479c40e616aec6f16b1f036800e9 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 21 Mar 2025 00:20:14 +0530 Subject: [PATCH 32/35] fix: duplicate tabs when tab bar was turned off and on again --- src/extensionsIntegrated/TabBar/main.js | 121 ++++-------------------- 1 file changed, 21 insertions(+), 100 deletions(-) diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 479b988425..b467759925 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -145,87 +145,6 @@ define(function (require, exports, module) { } - /** - * Sets up the tab bar - */ - function setupTabBar() { - // this populates the working sets present in `global.js` - getAllFilesFromWorkingSet(); - - // if no files are present in a pane, we want to hide the tab bar for that pane - const $firstTabBar = $('#phoenix-tab-bar'); - const $secondTabBar = $('#phoenix-tab-bar-2'); - - if (Global.firstPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar'))) { - Helper._hideTabBar($('#phoenix-tab-bar'), $('#overflow-button')); - } - - if (Global.secondPaneWorkingSet.length === 0 && ($('#phoenix-tab-bar-2'))) { - Helper._hideTabBar($('#phoenix-tab-bar-2'), $('#overflow-button-2')); - } - - // get the count of tabs that we want to display in the tab bar (from preference settings) - // from preference settings or working set whichever smaller - let tabsCountP1 = Math.min(Global.firstPaneWorkingSet.length, Preference.tabBarNumberOfTabs); - let tabsCountP2 = Math.min(Global.secondPaneWorkingSet.length, Preference.tabBarNumberOfTabs); - - // the value is generally '-1', but we check for less than 0 so that it can handle edge cases gracefully - // if the value is negative then we display all tabs - if (Preference.tabBarNumberOfTabs < 0) { - tabsCountP1 = Global.firstPaneWorkingSet.length; - tabsCountP2 = Global.secondPaneWorkingSet.length; - } - - // get the active editor and path once to reuse for both panes - const activeEditor = EditorManager.getActiveEditor(); - const activePath = activeEditor ? activeEditor.document.file.fullPath : null; - - // handle the first pane tabs - if (Global.firstPaneWorkingSet.length > 0 && tabsCountP1 > 0 && $firstTabBar.length) { - // get the top n entries for the first pane - let displayedEntries = Global.firstPaneWorkingSet.slice(0, tabsCountP1); - - // if the active file isn't already visible but exists in the working set, force-include it - if (activePath && !displayedEntries.some(entry => entry.path === activePath)) { - let activeEntry = Global.firstPaneWorkingSet.find(entry => entry.path === activePath); - if (activeEntry) { - // replace the last tab with the active file. - displayedEntries[displayedEntries.length - 1] = activeEntry; - } - } - - // add each tab to the first pane's tab bar - displayedEntries.forEach(function (entry) { - $firstTabBar.append(createTab(entry, "first-pane")); - Overflow.toggleOverflowVisibility("first-pane"); - setTimeout(function () { - Overflow.scrollToActiveTab($firstTabBar); - }, 0); - }); - } - - // for second pane tabs - if (Global.secondPaneWorkingSet.length > 0 && tabsCountP2 > 0 && $secondTabBar.length) { - let displayedEntries2 = Global.secondPaneWorkingSet.slice(0, tabsCountP2); - - if (activePath && !displayedEntries2.some(entry => entry.path === activePath)) { - let activeEntry = Global.secondPaneWorkingSet.find(entry => entry.path === activePath); - if (activeEntry) { - displayedEntries2[displayedEntries2.length - 1] = activeEntry; - } - } - - displayedEntries2.forEach(function (entry) { - $secondTabBar.append(createTab(entry, "second-pane")); - Overflow.toggleOverflowVisibility("second-pane"); - setTimeout(function () { - Overflow.scrollToActiveTab($secondTabBar); - }, 0); - }); - } - } - - /** * Creates the tab bar and adds it to the DOM */ @@ -255,8 +174,6 @@ define(function (require, exports, module) { WorkspaceManager.recomputeLayout(true); updateTabs(); } - - setupTabBar(); } @@ -507,29 +424,33 @@ define(function (require, exports, module) { const filePath = doc.file.fullPath; // Update UI - const $tab = $tabBar.find(`.tab[data-path="${filePath}"]`); - $tab.toggleClass('dirty', doc.isDirty); + if ($tabBar) { + const $tab = $tabBar.find(`.tab[data-path="${filePath}"]`); + $tab.toggleClass('dirty', doc.isDirty); + + + // Update the working set data + // First pane + for (let i = 0; i < Global.firstPaneWorkingSet.length; i++) { + if (Global.firstPaneWorkingSet[i].path === filePath) { + Global.firstPaneWorkingSet[i].isDirty = doc.isDirty; + break; + } + } + } + // Also update the $tab2 if it exists if ($tabBar2) { const $tab2 = $tabBar2.find(`.tab[data-path="${filePath}"]`); $tab2.toggleClass('dirty', doc.isDirty); - } - - // Update the working set data - // First pane - for (let i = 0; i < Global.firstPaneWorkingSet.length; i++) { - if (Global.firstPaneWorkingSet[i].path === filePath) { - Global.firstPaneWorkingSet[i].isDirty = doc.isDirty; - break; - } - } - // Second pane - for (let i = 0; i < Global.secondPaneWorkingSet.length; i++) { - if (Global.secondPaneWorkingSet[i].path === filePath) { - Global.secondPaneWorkingSet[i].isDirty = doc.isDirty; - break; + // Second pane + for (let i = 0; i < Global.secondPaneWorkingSet.length; i++) { + if (Global.secondPaneWorkingSet[i].path === filePath) { + Global.secondPaneWorkingSet[i].isDirty = doc.isDirty; + break; + } } } }); From cfc2655a70a620a5ec2143c04c1f568fb3df6dbb Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 21 Mar 2025 01:49:37 +0530 Subject: [PATCH 33/35] feat: add tab bar toggle option inside view menu --- src/command/Commands.js | 4 +++ src/extensionsIntegrated/TabBar/main.js | 36 ++++++++++++++++--- src/extensionsIntegrated/TabBar/preference.js | 1 - src/nls/root/strings.js | 1 + 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/command/Commands.js b/src/command/Commands.js index 7b0bd6e8d5..2314c47e32 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -260,6 +260,10 @@ define(function (require, exports, module) { /** Toggles sidebar visibility */ exports.VIEW_HIDE_SIDEBAR = "view.toggleSidebar"; // SidebarView.js toggle() + /** Toggles tabbar visibility */ + exports.TOGGLE_TABBAR = "view.toggleTabbar"; + // extensionsIntegrated/TabBar/main.js + /** Zooms in the editor view */ exports.VIEW_ZOOM_IN = "view.zoomIn"; // ViewCommandHandlers.js _handleZoomIn() diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index b467759925..3cfe2927c4 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -10,6 +10,8 @@ define(function (require, exports, module) { const Commands = require("command/Commands"); const DocumentManager = require("document/DocumentManager"); const WorkspaceManager = require("view/WorkspaceManager"); + const Menus = require("command/Menus"); + const Strings = require("strings"); const Global = require("./global"); const Helper = require("./helper"); @@ -458,14 +460,18 @@ define(function (require, exports, module) { /** - * This is called when the tab bar preference is changed + * This is called when the tab bar preference is changed either, + * from the preferences file or the menu bar * It takes care of creating or cleaning up the tab bar */ function preferenceChanged() { - Preference.tabBarEnabled = PreferencesManager.get(Preference.PREFERENCES_TAB_BAR).showTabBar; - Preference.tabBarNumberOfTabs = PreferencesManager.get(Preference.PREFERENCES_TAB_BAR).numberOfTabs; + const prefs = PreferencesManager.get(Preference.PREFERENCES_TAB_BAR); + Preference.tabBarEnabled = prefs.showTabBar; + Preference.tabBarNumberOfTabs = prefs.numberOfTabs; + + // Update menu checkmark + CommandManager.get(Commands.TOGGLE_TABBAR).setChecked(prefs.showTabBar); - // preference should be enabled and number of tabs should be greater than 0 if (Preference.tabBarEnabled && Preference.tabBarNumberOfTabs !== 0) { createTabBar(); } else { @@ -473,9 +479,31 @@ define(function (require, exports, module) { } } + /** + * Registers the commands, + * for toggling the tab bar from the menu bar + */ + function _registerCommands() { + CommandManager.register( + Strings.CMD_TOGGLE_TABBAR, + Commands.TOGGLE_TABBAR, + () => { + const currentPref = PreferencesManager.get(Preference.PREFERENCES_TAB_BAR); + PreferencesManager.set(Preference.PREFERENCES_TAB_BAR, { + ...currentPref, + showTabBar: !currentPref.showTabBar + }); + } + ); + } AppInit.appReady(function () { + _registerCommands(); + + // add the toggle tab bar command to the view menu + const viewMenu = Menus.getMenu(Menus.AppMenuBar.VIEW_MENU); + viewMenu.addMenuItem(Commands.TOGGLE_TABBAR, "", Menus.AFTER, Commands.VIEW_HIDE_SIDEBAR); PreferencesManager.on("change", Preference.PREFERENCES_TAB_BAR, preferenceChanged); // calling preference changed here itself to check if the tab bar is enabled, diff --git a/src/extensionsIntegrated/TabBar/preference.js b/src/extensionsIntegrated/TabBar/preference.js index 3602a8eb9f..b0b77c731b 100644 --- a/src/extensionsIntegrated/TabBar/preference.js +++ b/src/extensionsIntegrated/TabBar/preference.js @@ -1,7 +1,6 @@ /* * This script contains the preference settings for the tab bar. - * TODO: It also will have the tab bar options that will be added in menu bar and other places. */ define(function (require, exports, module) { const PreferencesManager = require("preferences/PreferencesManager"); diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 0147022f4a..b019c5745f 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -552,6 +552,7 @@ define({ "CMD_HIDE_SIDEBAR": "Hide Sidebar", "CMD_SHOW_SIDEBAR": "Show Sidebar", "CMD_TOGGLE_SIDEBAR": "Toggle Sidebar", + "CMD_TOGGLE_TABBAR": "Toggle Tab Bar", "CMD_TOGGLE_PANELS": "Toggle Panels", "CMD_TOGGLE_PURE_CODE": "No Distractions", "CMD_TOGGLE_FULLSCREEN": "Fullscreen", From 9b14578fe79d6525701cc8e1be9f9908e455ed4e Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 21 Mar 2025 01:55:11 +0530 Subject: [PATCH 34/35] chore: update api docs --- docs/API-Reference/command/Commands.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md index d3ef7ac883..6e7a0950a7 100644 --- a/docs/API-Reference/command/Commands.md +++ b/docs/API-Reference/command/Commands.md @@ -458,6 +458,12 @@ Opens theme settings ## VIEW\_HIDE\_SIDEBAR Toggles sidebar visibility +**Kind**: global variable + + +## TOGGLE\_TABBAR +Toggles tabbar visibility + **Kind**: global variable From 7a9f81767487e8626fbc46aa3c06b9a176062201 Mon Sep 17 00:00:00 2001 From: Pluto Date: Fri, 21 Mar 2025 14:39:08 +0530 Subject: [PATCH 35/35] chore: add license --- src/extensionsIntegrated/TabBar/drag-drop.js | 22 +++++++++++++++ src/extensionsIntegrated/TabBar/global.js | 23 ++++++++++++++++ src/extensionsIntegrated/TabBar/helper.js | 23 +++++++++++++++- src/extensionsIntegrated/TabBar/main.js | 20 ++++++++++++++ .../TabBar/more-options.js | 20 ++++++++++++++ src/extensionsIntegrated/TabBar/overflow.js | 27 +++++++++++++++++++ src/extensionsIntegrated/TabBar/preference.js | 19 +++++++++++++ 7 files changed, 153 insertions(+), 1 deletion(-) diff --git a/src/extensionsIntegrated/TabBar/drag-drop.js b/src/extensionsIntegrated/TabBar/drag-drop.js index 942e8d43a0..176561eb46 100644 --- a/src/extensionsIntegrated/TabBar/drag-drop.js +++ b/src/extensionsIntegrated/TabBar/drag-drop.js @@ -1,3 +1,25 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + + +/* This file houses the functionality for dragging and dropping tabs */ /* eslint-disable no-invalid-this */ define(function (require, exports, module) { const MainViewManager = require("view/MainViewManager"); diff --git a/src/extensionsIntegrated/TabBar/global.js b/src/extensionsIntegrated/TabBar/global.js index 3650b9c44f..0b0a93fbb6 100644 --- a/src/extensionsIntegrated/TabBar/global.js +++ b/src/extensionsIntegrated/TabBar/global.js @@ -1,3 +1,26 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/* + * This file houses the global working set array + */ define(function (require, exports, module) { /** * This array's represents the current working set diff --git a/src/extensionsIntegrated/TabBar/helper.js b/src/extensionsIntegrated/TabBar/helper.js index 34e1b52ac4..a60d8f76d3 100644 --- a/src/extensionsIntegrated/TabBar/helper.js +++ b/src/extensionsIntegrated/TabBar/helper.js @@ -1,3 +1,24 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/* This file contains helper functions for the tab bar */ define(function (require, exports, module) { const WorkspaceManager = require("view/WorkspaceManager"); @@ -8,7 +29,7 @@ define(function (require, exports, module) { /** - * Shows the tab bar, when its hidden. + * Shows the tab bar, when it is hidden. * Its only shown when tab bar is enabled and there is atleast one working file * * @param {$.Element} $tabBar - The tab bar element diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index 3cfe2927c4..1ee8728142 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -1,3 +1,23 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + /* eslint-disable no-invalid-this */ define(function (require, exports, module) { const _ = require("thirdparty/lodash"); diff --git a/src/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js index f5b973e943..27e2ab0982 100644 --- a/src/extensionsIntegrated/TabBar/more-options.js +++ b/src/extensionsIntegrated/TabBar/more-options.js @@ -1,3 +1,23 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + /* * This file manages the more options context menu. * The more options context menu is shown when a tab is right-clicked diff --git a/src/extensionsIntegrated/TabBar/overflow.js b/src/extensionsIntegrated/TabBar/overflow.js index 1c020918b1..2c36acae3d 100644 --- a/src/extensionsIntegrated/TabBar/overflow.js +++ b/src/extensionsIntegrated/TabBar/overflow.js @@ -1,3 +1,30 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + + +/* + * This file houses the functionality for overflow tabs, + * the overflow tabs appear when there are too many tabs in a pane + * overflow button when clicked opens up a dropdown menu which displays all the hidden tabs + * and allows the user to open them, close them and to achieve other functionalities from there + */ /* eslint-disable no-invalid-this */ define(function (require, exports, module) { diff --git a/src/extensionsIntegrated/TabBar/preference.js b/src/extensionsIntegrated/TabBar/preference.js index b0b77c731b..f62a51fd70 100644 --- a/src/extensionsIntegrated/TabBar/preference.js +++ b/src/extensionsIntegrated/TabBar/preference.js @@ -1,3 +1,22 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ /* * This script contains the preference settings for the tab bar.