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
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/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 () {
diff --git a/src/extensionsIntegrated/TabBar/drag-drop.js b/src/extensionsIntegrated/TabBar/drag-drop.js
new file mode 100644
index 0000000000..176561eb46
--- /dev/null
+++ b/src/extensionsIntegrated/TabBar/drag-drop.js
@@ -0,0 +1,424 @@
+/*
+ * 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");
+
+ /**
+ * 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/global.js b/src/extensionsIntegrated/TabBar/global.js
new file mode 100644
index 0000000000..0b0a93fbb6
--- /dev/null
+++ b/src/extensionsIntegrated/TabBar/global.js
@@ -0,0 +1,43 @@
+/*
+ * 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
+ * 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/helper.js b/src/extensionsIntegrated/TabBar/helper.js
new file mode 100644
index 0000000000..a60d8f76d3
--- /dev/null
+++ b/src/extensionsIntegrated/TabBar/helper.js
@@ -0,0 +1,166 @@
+/*
+ * 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");
+ 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 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
+ * @param {$.Element} $overflowButton - The overflow button element
+ */
+ 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);
+ }
+ }
+
+ /**
+ * 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} $overflowButton - The overflow button element
+ */
+ function _hideTabBar($tabBar, $overflowButton) {
+ if ($tabBar) {
+ $tabBar.hide();
+ if($overflowButton) {
+ $overflowButton.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..c424ac2bc0
--- /dev/null
+++ b/src/extensionsIntegrated/TabBar/html/tabbar-pane.html
@@ -0,0 +1,9 @@
+
\ 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..451f0322a2
--- /dev/null
+++ b/src/extensionsIntegrated/TabBar/html/tabbar-second-pane.html
@@ -0,0 +1,9 @@
+
\ 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..1ee8728142
--- /dev/null
+++ b/src/extensionsIntegrated/TabBar/main.js
@@ -0,0 +1,542 @@
+/*
+ * 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");
+ 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 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");
+ 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");
+
+
+
+ /**
+ * 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() {
+ 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();
+
+ // 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
+ Global.firstPaneWorkingSet.push(Helper._getRequiredDataFromEntry(currFirstPaneWorkingSet[i]));
+ }
+ // if there are duplicate file names, we update the displayName to include the directory
+ 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++) {
+ Global.secondPaneWorkingSet.push(Helper._getRequiredDataFromEntry(currSecondPaneWorkingSet[i]));
+ }
+ 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;
+ }
+ });
+ }
+ }
+
+
+
+ /**
+ * 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
+ * @param {String} paneId - the pane id 'first-pane' or 'second-pane'
+ * @returns {$.Element} the tab element
+ */
+ 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 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
+ // 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;
+ }
+
+
+ /**
+ * Creates the tab bar and adds it to the DOM
+ */
+ function createTabBar() {
+ if (!Preference.tabBarEnabled || Preference.numberOfTabs === 0) {
+ 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 which has .not-editor class
+ $(".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
+ // 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);
+ WorkspaceManager.recomputeLayout(true);
+ updateTabs();
+ }
+ }
+
+
+ /**
+ * 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 &&
+ (!$('#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();
+ }
+
+ 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, "first-pane"));
+ });
+ }
+ }
+
+ 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, "second-pane"));
+ });
+ }
+ }
+
+ // 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'));
+ }
+
+ const activePane = MainViewManager.getActivePaneId();
+
+ // Now that tabs are updated, scroll to the active tab if necessary.
+ if ($firstTabBar.length) {
+ Overflow.toggleOverflowVisibility("first-pane");
+
+ // 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");
+ if (activePane === "second-pane") {
+ setTimeout(function () {
+ Overflow.scrollToActiveTab($secondTabBar);
+ }, 0);
+ }
+ }
+
+ // handle drag and drop
+ DragDrop.init($('#phoenix-tab-bar'), $('#phoenix-tab-bar-2'));
+ }
+
+
+ /**
+ * 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();
+ }
+
+
+ /**
+ * 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) {
+ // check if the clicked element is the close button
+ 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) {
+ // 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();
+ }
+ return;
+ }
+
+ // Get the file path from the data-path attribute
+ const filePath = $(this).attr("data-path");
+
+ if (filePath) {
+ CommandManager.execute(Commands.FILE_OPEN, { fullPath: filePath });
+
+ // Prevent default behavior
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ });
+
+ // Add the contextmenu (right-click) handler
+ $(document).on("contextmenu", ".tab", function (event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ // 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 the context menu at mouse position
+ MoreOptions.showMoreOptionsContextMenu(paneId, event.pageX, event.pageY, filePath);
+ });
+ }
+
+
+ /**
+ * Registers the event handlers
+ */
+ function registerHandlers() {
+ // 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);
+ // 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 = [
+ "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
+ $("#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 handling
+ DocumentManager.on("dirtyFlagChange", function (event, doc) {
+ const filePath = doc.file.fullPath;
+
+ // Update UI
+ 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);
+
+ // Second pane
+ for (let i = 0; i < Global.secondPaneWorkingSet.length; i++) {
+ if (Global.secondPaneWorkingSet[i].path === filePath) {
+ Global.secondPaneWorkingSet[i].isDirty = doc.isDirty;
+ break;
+ }
+ }
+ }
+ });
+ }
+
+
+ /**
+ * 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() {
+ 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);
+
+ if (Preference.tabBarEnabled && Preference.tabBarNumberOfTabs !== 0) {
+ createTabBar();
+ } else {
+ cleanupTabBar();
+ }
+ }
+
+ /**
+ * 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,
+ // 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();
+
+ Overflow.init();
+ DragDrop.init($('#phoenix-tab-bar'), $('#phoenix-tab-bar-2'));
+ });
+});
diff --git a/src/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js
new file mode 100644
index 0000000000..27e2ab0982
--- /dev/null
+++ b/src/extensionsIntegrated/TabBar/more-options.js
@@ -0,0 +1,207 @@
+/*
+ * 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
+ */
+define(function (require, exports, module) {
+ const DropdownButton = require("widgets/DropdownButton");
+ const Strings = require("strings");
+ const CommandManager = require("command/CommandManager");
+ const Commands = require("command/Commands");
+ const FileSystem = require("filesystem/FileSystem");
+ const 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`
+ const items = [
+ Strings.CLOSE_TAB,
+ Strings.CLOSE_ACTIVE_TAB,
+ Strings.CLOSE_ALL_TABS,
+ Strings.CLOSE_UNMODIFIED_TABS,
+ "---",
+ Strings.REOPEN_CLOSED_FILE
+ ];
+
+
+ /**
+ * "CLOSE TAB"
+ * this function handles the closing of the tab that was right-clicked
+ *
+ * @param {String} filePath - path of the file to close
+ * @param {String} paneId - the id of the pane in which the file is present
+ */
+ function handleCloseTab(filePath, paneId) {
+ if (filePath) {
+ // Get the file object using FileSystem
+ const fileObj = FileSystem.getFileForPath(filePath);
+
+ // Execute close command with file object and pane ID
+ CommandManager.execute(
+ Commands.FILE_CLOSE,
+ { file: fileObj, paneId: paneId }
+ );
+ }
+ }
+
+
+ /**
+ * "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);
+ }
+
+
+ /**
+ * "CLOSE UNMODIFIED TABS"
+ * This will close all tabs that are not modified
+ */
+ function handleCloseUnmodifiedTabs() {
+ 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" }
+ );
+ });
+ }
+ }
+
+
+ /**
+ * "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);
+ }
+
+
+ /**
+ * 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
+ * @param {String} filePath - [optional] the path of the file that was right-clicked
+ */
+ function showMoreOptionsContextMenu(paneId, x, y, filePath) {
+ const dropdown = new DropdownButton.DropdownButton("", items);
+
+ // Append to document body for absolute positioning
+ $("body").append(dropdown.$button);
+
+ // Position the dropdown at the mouse coordinates
+ dropdown.$button.css({
+ position: "absolute",
+ left: x + "px",
+ top: y + "px",
+ zIndex: 1000
+ });
+
+ dropdown.showDropdown();
+
+ // handle the option selection
+ dropdown.on("select", function (e, item, index) {
+ _handleSelection(index, filePath, paneId);
+ });
+
+ // Remove the button after the dropdown is hidden
+ dropdown.$button.css({
+ display: "none"
+ });
+ }
+
+ /**
+ * 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/extensionsIntegrated/TabBar/overflow.js b/src/extensionsIntegrated/TabBar/overflow.js
new file mode 100644
index 0000000000..2c36acae3d
--- /dev/null
+++ b/src/extensionsIntegrated/TabBar/overflow.js
@@ -0,0 +1,325 @@
+/*
+ * 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) {
+
+ 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");
+ const FileSystem = require("filesystem/FileSystem");
+
+
+ /**
+ * 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) {
+ // get the appropriate tab bar based on pane ID
+ const $currentTabBar = paneId === "first-pane"
+ ? $("#phoenix-tab-bar")
+ : $("#phoenix-tab-bar-2");
+
+ // access the DOM element to get its bounding rectangle
+ const tabBarRect = $currentTabBar[0].getBoundingClientRect();
+
+ // an array of hidden tabs objects which will store properties like
+ // path, name, isActive, isDirty and $icon
+ const hiddenTabs = [];
+
+ // 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 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 + 2);
+
+ 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;
+ }
+
+
+ /**
+ * 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 element, refer to ./html/tabbar-pane.html
+ const $overflowButton = $("#overflow-button");
+
+ if (hiddenTabs.length > 0) {
+ $overflowButton.removeClass("hidden");
+ } else {
+ $overflowButton.addClass("hidden");
+ }
+ } else {
+ // for the html element, refer to ./html/tabbar-second-pane.html
+ const $overflowButton = $("#overflow-button-2");
+
+ if (hiddenTabs.length > 0) {
+ $overflowButton.removeClass("hidden");
+ } else {
+ $overflowButton.addClass("hidden");
+ }
+ }
+ }
+
+
+ /**
+ * 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 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
+ const dirtyHtml = item.isDirty
+ ? '•'
+ : ''; // adding an empty span for better alignment
+
+ const closeIconHtml =
+ `
+
+ `;
+
+ // return html for this item
+ return {
+ html:
+ `
+
+ ${dirtyHtml}
+ ${iconHtml}
+ ${item.name}
+
+ ${closeIconHtml}
+
`,
+ enabled: true
+ };
+ });
+
+ // add the custom classes for styling 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
+ });
+
+
+ // 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
+ 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
+ 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("mousedown", ".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();
+
+ // animate the scroll change over 5 for a very fast effect
+ if (tabLeftRelative < 0) {
+ // 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;
+ $tabBarElement.animate(
+ { scrollLeft: currentScroll + scrollAdjustment },
+ 5
+ );
+ }
+ }
+ }
+
+
+ /**
+ * 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);
+ });
+ }
+
+ // initialize the handling of the overflow buttons
+ function init() {
+ setupOverflowHandlers();
+ }
+
+ module.exports = {
+ init,
+ toggleOverflowVisibility,
+ scrollToActiveTab
+ };
+});
diff --git a/src/extensionsIntegrated/TabBar/preference.js b/src/extensionsIntegrated/TabBar/preference.js
new file mode 100644
index 0000000000..f62a51fd70
--- /dev/null
+++ b/src/extensionsIntegrated/TabBar/preference.js
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+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..b019c5745f 100644
--- a/src/nls/root/strings.js
+++ b/src/nls/root/strings.js
@@ -426,6 +426,13 @@ define({
"STATUSBAR_TASKS_STOP": "Stop",
"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",
+
// CodeInspection: errors/warnings
"ERRORS_NO_FILE": "No File Open",
"ERRORS_PANEL_TITLE_MULTIPLE": "{0} Problems - {1}",
@@ -545,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",
@@ -1268,6 +1276,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..d67ac2bfd9
--- /dev/null
+++ b/src/styles/Extn-TabBar.less
@@ -0,0 +1,341 @@
+.tab-bar-container {
+ display: flex;
+ align-items: center;
+ 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;
+ height: 2.1rem;
+ display: flex;
+ overflow-x: auto;
+ overflow-y: hidden;
+ white-space: nowrap;
+ transition: height 0.3s ease;
+ scroll-behavior: smooth;
+ background-color: #f5f5f5;
+}
+
+.dark .phoenix-tab-bar {
+ background-color: #1E1E1E;
+}
+
+.phoenix-tab-bar::-webkit-scrollbar {
+ 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: #e0e0e0;
+}
+
+.dark .overflow-button:hover,
+.dark .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(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: #F8F8F8;
+ border-right: 1px solid #ddd;
+ cursor: pointer;
+ position: relative;
+ flex: 0 0 auto;
+ min-width: fit-content;
+ user-select: none;
+ transition: transform 60ms ease, opacity 60ms ease;
+}
+
+.dark .tab {
+ background-color: #2D2D2D;
+ border-right: 1px solid #333;
+}
+
+.tab,
+.tab-close,
+.tab-icon,
+.tab-name {
+ transition: all 120ms ease-out;
+}
+
+.tab-icon {
+ display: flex;
+ align-items: center;
+ margin-bottom: -2px;
+}
+
+.tab-name {
+ color: #555;
+ display: inline-flex;
+ align-items: center;
+ font-size: 0.85rem;
+ letter-spacing: 0.4px;
+ word-spacing: 0.75px;
+ white-space: nowrap;
+}
+
+.dark .tab-name {
+ color: #aaa;
+}
+
+.tab.active .tab-name {
+ color: #333;
+}
+
+.dark .tab.active .tab-name {
+ color: #dedede;
+}
+
+.tab .tab-dirname {
+ font-size: 0.65rem;
+ opacity: 0.7;
+ font-weight: normal;
+}
+
+.tab.active {
+ background-color: #fff;
+}
+
+.dark .tab.active {
+ background-color: #3D3D3D;
+}
+
+.tab:hover {
+ background-color: #f0f0f0;
+ cursor: pointer;
+}
+
+.dark .tab:hover {
+ background-color: #4d4949;
+}
+
+.tab.active::after {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 0.12rem;
+ background-color: #0078D7;
+}
+
+.dark .tab.active::after {
+ background-color: #75BEFF;
+}
+
+.tab.dirty::before {
+ content: "•";
+ 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: #666;
+ transition: all 0.2s ease;
+ border-radius: 0.2rem;
+ visibility: hidden;
+ opacity: 0;
+ pointer-events: none;
+}
+
+.dark .tab-close {
+ color: #CCC;
+}
+
+.tab:hover .tab-close,
+.tab.active .tab-close {
+ visibility: visible;
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.tab-close:hover {
+ cursor: pointer;
+ 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: #e5e5e5;
+}
+
+.dark .tab.drag-target {
+ background-color: #383838;
+}
+
+.tab-drag-indicator {
+ position: fixed;
+ width: 2px;
+ background-color: #0078D7;
+ pointer-events: none;
+ z-index: 10001;
+ 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% {
+ opacity: 0.7;
+ }
+
+ 50% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0.7;
+ }
+}
+
+.dropdown-tab-item {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ justify-content: space-between;
+}
+
+.dropdown-tab-item:hover {
+ background-color: #e8f0fa;
+}
+
+.dark .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;
+ align-items: center;
+}
+
+.tab-name-container {
+ flex-grow: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.tab-dirty-icon-overflow {
+ 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;
+}
+
+.tab-close-icon-overflow {
+ font-size: 0.75rem;
+ font-weight: 300;
+ padding: 0.3rem 0.6rem;
+ margin-left: 0.5rem;
+ margin-top: -0.1rem;
+ 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
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";