diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md
index 783376af39..ef97165764 100644
--- a/docs/API-Reference/command/Commands.md
+++ b/docs/API-Reference/command/Commands.md
@@ -824,6 +824,18 @@ Sorts working set by file type
## CMD\_WORKING\_SORT\_TOGGLE\_AUTO
Toggles automatic working set sorting
+**Kind**: global variable
+
+
+## CMD\_TOGGLE\_SHOW\_WORKING\_SET
+Toggles working set visibility
+
+**Kind**: global variable
+
+
+## CMD\_TOGGLE\_SHOW\_FILE\_TABS
+Toggles file tabs visibility
+
**Kind**: global variable
diff --git a/src/extensionsIntegrated/TabBar/drag-drop.js b/src/extensionsIntegrated/TabBar/drag-drop.js
index ef0d912653..0a4dadf2ef 100644
--- a/src/extensionsIntegrated/TabBar/drag-drop.js
+++ b/src/extensionsIntegrated/TabBar/drag-drop.js
@@ -39,6 +39,48 @@ define(function (require, exports, module) {
let scrollInterval = null;
let dragSourcePane = null;
+ /**
+ * this function is responsible to make sure that all the drag state is properly cleaned up
+ * it is needed to make sure that the tab bar doesn't get unresponsive
+ * because of handlers not being attached properly
+ */
+ function cleanupDragState() {
+ $(".tab").removeClass("dragging drag-target");
+ $(".empty-pane-drop-target").removeClass("empty-pane-drop-target");
+
+ // this is to make sure that the drag indicator is hidden and remove any inline styles
+ if (dragIndicator) {
+ dragIndicator.hide().css({
+ top: '',
+ left: '',
+ height: ''
+ });
+ }
+
+ // Reset all drag state variables
+ draggedTab = null;
+ dragOverTab = null;
+ dragSourcePane = null;
+
+ if (scrollInterval) {
+ clearInterval(scrollInterval);
+ scrollInterval = null;
+ }
+
+ $("#tab-drag-extended-zone").remove();
+
+ // this is needed to make sure that all the drag-active styling are properly hidden
+ // it is required because noticed a bug where sometimes some styles remain when drop fails
+ $(".phoenix-tab-bar").removeClass("drag-active");
+
+ setTimeout(() => {
+ // a double check just to make sure that the drag indicator is still hidden
+ if (dragIndicator && dragIndicator.is(':visible')) {
+ dragIndicator.hide();
+ }
+ }, 5);
+ }
+
/**
* Initialize drag and drop functionality for tab bars
* This is called from `main.js`
@@ -62,6 +104,9 @@ define(function (require, exports, module) {
// add initialization for empty panes
initEmptyPaneDropTargets();
+
+ // Set up global drag cleanup handlers to ensure drag state is always cleaned up
+ setupGlobalDragCleanup();
}
/**
@@ -144,10 +189,6 @@ define(function (require, exports, module) {
}
};
- const removeOuterDropZone = () => {
- $("#tab-drag-extended-zone").remove();
- };
-
// When dragging over the container but not directly over a tab element
$container.on("dragover", function (e) {
if (e.preventDefault) {
@@ -202,9 +243,6 @@ define(function (require, exports, module) {
if (e.preventDefault) {
e.preventDefault();
}
- // hide the drag indicator
- updateDragIndicator(null);
- removeOuterDropZone();
// get container dimensions to determine drop position
const containerRect = this.getBoundingClientRect();
@@ -235,6 +273,9 @@ define(function (require, exports, module) {
}
}
}
+
+ // ensure all drag state is cleaned up
+ cleanupDragState();
});
/**
@@ -255,14 +296,10 @@ define(function (require, exports, module) {
if (mouseX > containerRect.right) {
targetTab = $tabs.last()[0];
onLeftSide = false;
- }
- // If beyond the left edge, use the first tab
- else if (mouseX < containerRect.left) {
+ } else if (mouseX < containerRect.left) { // If beyond the left edge, use the first tab
targetTab = $tabs.first()[0];
onLeftSide = true;
- }
- // If within bounds, find the closest tab
- else {
+ } else { // If within bounds, find the closest tab
onLeftSide = mouseX < containerRect.left + containerRect.width / 2;
targetTab = onLeftSide ? $tabs.first()[0] : $tabs.last()[0];
}
@@ -311,9 +348,7 @@ define(function (require, exports, module) {
}
}
- // Clean up
- updateDragIndicator(null);
- removeOuterDropZone();
+ cleanupDragState();
}
}
@@ -365,6 +400,10 @@ define(function (require, exports, module) {
* @param {Event} e - The event object
*/
function handleDragStart(e) {
+ if (draggedTab) {
+ cleanupDragState();
+ }
+
// store reference to the dragged tab
draggedTab = this;
@@ -382,7 +421,10 @@ define(function (require, exports, module) {
// 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);
+ // Ensure the drag indicator is properly hidden at the start
+ if (dragIndicator) {
+ dragIndicator.hide();
+ }
}, 0);
}
@@ -446,35 +488,43 @@ define(function (require, exports, module) {
* @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 targetPaneId = 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;
-
- // Check if dragging between different panes
- if (dragSourcePane !== targetPaneId) {
- // Move the tab between panes
- moveTabBetweenPanes(dragSourcePane, targetPaneId, draggedPath, targetPath, onLeftSide);
- } else {
- // Move within the same pane
- moveWorkingSetItem(targetPaneId, draggedPath, targetPath, onLeftSide);
+ try {
+ if (e.stopPropagation) {
+ e.stopPropagation(); // Stops browser from redirecting
+ }
+
+ // 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 targetPaneId = 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;
+
+ // Check if dragging between different panes
+ if (dragSourcePane !== targetPaneId) {
+ // Move the tab between panes
+ moveTabBetweenPanes(dragSourcePane, targetPaneId, draggedPath, targetPath, onLeftSide);
+ } else {
+ // Move within the same pane
+ moveWorkingSetItem(targetPaneId, draggedPath, targetPath, onLeftSide);
+ }
}
+
+ cleanupDragState();
+ return false;
+ } catch (error) {
+ console.error("Error during tab drop operation:", error);
+ // Ensure cleanup happens even if there's an error
+ cleanupDragState();
+ return false;
}
- return false;
}
/**
@@ -484,20 +534,50 @@ define(function (require, exports, module) {
* @param {Event} e - The event object
*/
function handleDragEnd(e) {
- $(".tab").removeClass("dragging drag-target");
- updateDragIndicator(null);
- draggedTab = null;
- dragOverTab = null;
- dragSourcePane = null;
+ setTimeout(() => {
+ cleanupDragState();
+ }, 10);
+ }
- // Clear scroll interval if it exists
- if (scrollInterval) {
- clearInterval(scrollInterval);
- scrollInterval = null;
- }
+ /**
+ * Global document event listeners to ensure drag state is always cleaned up
+ * This handles cases where drag operations fail or are cancelled outside
+ * the normal tab bar drop zones
+ */
+ function setupGlobalDragCleanup() {
+ // Listen for drags ending anywhere on the document
+ $(document).on('dragend', function(e) {
+ // Only clean up if we were tracking a drag operation
+ if (draggedTab) {
+ setTimeout(() => {
+ cleanupDragState();
+ }, 10);
+ }
+ });
- // Remove the extended drop zone if it exists
- $("#tab-drag-extended-zone").remove();
+ // Listen for global mouse up events to catch cancelled drags
+ $(document).on('mouseup', function(e) {
+ // If we have an active drag but mouse is released, clean up
+ if (draggedTab && !e.originalEvent.dataTransfer) {
+ setTimeout(() => {
+ cleanupDragState();
+ }, 10);
+ }
+ });
+
+ // Listen for ESC key to cancel drag operations
+ $(document).on('keydown', function(e) {
+ if (e.key === 'Escape' && draggedTab) {
+ cleanupDragState();
+ }
+ });
+
+ // Listen for page visibility changes (like alt-tab) to clean up
+ $(document).on('visibilitychange', function() {
+ if (document.hidden && draggedTab) {
+ cleanupDragState();
+ }
+ });
}
/**
@@ -592,56 +672,81 @@ define(function (require, exports, module) {
* @param {Boolean} beforeTarget - Whether to place before or after the target
*/
function moveTabBetweenPanes(sourcePaneId, targetPaneId, draggedPath, targetPath, beforeTarget) {
- const sourceWorkingSet = MainViewManager.getWorkingSet(sourcePaneId);
- const targetWorkingSet = MainViewManager.getWorkingSet(targetPaneId);
-
- let draggedIndex = -1;
- let targetIndex = -1;
- let draggedFile = null;
-
- // Find the dragged file and its index in the source pane
- for (let i = 0; i < sourceWorkingSet.length; i++) {
- if (sourceWorkingSet[i].fullPath === draggedPath) {
- draggedIndex = i;
- draggedFile = sourceWorkingSet[i];
- break;
+ try {
+ const sourceWorkingSet = MainViewManager.getWorkingSet(sourcePaneId);
+ const targetWorkingSet = MainViewManager.getWorkingSet(targetPaneId);
+
+ let draggedIndex = -1;
+ let targetIndex = -1;
+ let draggedFile = null;
+
+ // Find the dragged file and its index in the source pane
+ for (let i = 0; i < sourceWorkingSet.length; i++) {
+ if (sourceWorkingSet[i].fullPath === draggedPath) {
+ draggedIndex = i;
+ draggedFile = sourceWorkingSet[i];
+ break;
+ }
}
- }
- // Find the target index in the target pane
- for (let i = 0; i < targetWorkingSet.length; i++) {
- if (targetWorkingSet[i].fullPath === targetPath) {
- targetIndex = i;
- break;
+ // Find the target index in the target pane
+ for (let i = 0; i < targetWorkingSet.length; i++) {
+ if (targetWorkingSet[i].fullPath === targetPath) {
+ targetIndex = i;
+ break;
+ }
}
- }
- // Only continue if we found the dragged file
- if (draggedIndex !== -1 && draggedFile) {
- // Remove the file from source pane
- CommandManager.execute(Commands.FILE_CLOSE, { file: draggedFile, paneId: sourcePaneId });
+ // Only continue if we found the dragged file
+ if (draggedIndex !== -1 && draggedFile) {
+ // Check if the dragged file is currently active in the source pane
+ const currentActiveFileInSource = MainViewManager.getCurrentlyViewedFile(sourcePaneId);
+ const isActiveFileBeingMoved = currentActiveFileInSource &&
+ currentActiveFileInSource.fullPath === draggedPath;
+
+ // If the active file is being moved and there are other files in the source pane,
+ // switch to another file first to prevent placeholder creation
+ if (isActiveFileBeingMoved && sourceWorkingSet.length > 1) {
+ // Find another file to make active (prefer the next file, or previous if this is the last)
+ let newActiveIndex = draggedIndex + 1;
+ if (newActiveIndex >= sourceWorkingSet.length) {
+ newActiveIndex = draggedIndex - 1;
+ }
- // Calculate where to add it in the target pane
- let targetInsertIndex;
+ if (newActiveIndex >= 0 && newActiveIndex < sourceWorkingSet.length) {
+ const newActiveFile = sourceWorkingSet[newActiveIndex];
+ // Open the new active file in the source pane before removing the dragged file
+ CommandManager.execute(Commands.FILE_OPEN, {
+ fullPath: newActiveFile.fullPath,
+ paneId: sourcePaneId
+ });
+ }
+ }
- if (targetIndex !== -1) {
- // We have a specific target index to aim for
- targetInsertIndex = beforeTarget ? targetIndex : targetIndex + 1;
- } else {
- // No specific target, add to end of the working set
- targetInsertIndex = targetWorkingSet.length;
- }
+ // Remove the file from source pane
+ CommandManager.execute(Commands.FILE_CLOSE, { file: draggedFile, paneId: sourcePaneId });
- // Add to the target pane at the calculated position
- MainViewManager.addToWorkingSet(targetPaneId, draggedFile, targetInsertIndex);
+ // Calculate where to add it in the target pane
+ let targetInsertIndex;
- // If the tab was the active one in the source pane,
- // make it active in the target pane too
- const activeFile = MainViewManager.getCurrentlyViewedFile(sourcePaneId);
- if (activeFile && activeFile.fullPath === draggedPath) {
- // Open the file in the target pane and make it active
+ if (targetIndex !== -1) {
+ // We have a specific target index to aim for
+ targetInsertIndex = beforeTarget ? targetIndex : targetIndex + 1;
+ } else {
+ // No specific target, add to end of the working set
+ targetInsertIndex = targetWorkingSet.length;
+ }
+
+ // Add to the target pane at the calculated position
+ MainViewManager.addToWorkingSet(targetPaneId, draggedFile, targetInsertIndex);
+
+ // we always need to make the dragged tab active in the target pane when moving between panes
CommandManager.execute(Commands.FILE_OPEN, { fullPath: draggedPath, paneId: targetPaneId });
}
+ } catch (error) {
+ console.error("Error during cross-pane tab move:", error);
+ // Even if there's an error, ensure the drag state is cleaned up
+ cleanupDragState();
}
}
@@ -671,11 +776,7 @@ define(function (require, exports, module) {
// Handle drag over empty pane
$paneHolder.on("dragover dragenter", function (e) {
- // we only want to process if this pane is empty (has no tab bar or has hidden tab bar)
- const $tabBar = paneId === "first-pane" ? $("#phoenix-tab-bar") : $("#phoenix-tab-bar-2");
- const isEmptyPane = !$tabBar.length || $tabBar.is(":hidden") || $tabBar.children(".tab").length === 0;
-
- if (isEmptyPane && draggedTab) {
+ if (draggedTab) {
e.preventDefault();
e.stopPropagation();
@@ -697,7 +798,7 @@ define(function (require, exports, module) {
const $tabBar = paneId === "first-pane" ? $("#phoenix-tab-bar") : $("#phoenix-tab-bar-2");
const isEmptyPane = !$tabBar.length || $tabBar.is(":hidden") || $tabBar.children(".tab").length === 0;
- if (isEmptyPane && draggedTab) {
+ if (draggedTab) {
e.preventDefault();
e.stopPropagation();
@@ -711,9 +812,11 @@ define(function (require, exports, module) {
const sourcePaneId =
$(draggedTab).closest("#phoenix-tab-bar-2").length > 0 ? "second-pane" : "first-pane";
- // we don't want to do anything if dropping in the same pane
- if (sourcePaneId !== paneId) {
+ // we only want to proceed if we're not dropping in the same pane or,
+ // allow if it's the same pane with existing tabs
+ if (sourcePaneId !== paneId || !isEmptyPane) {
const sourceWorkingSet = MainViewManager.getWorkingSet(sourcePaneId);
+ const targetWorkingSet = MainViewManager.getWorkingSet(paneId);
let draggedFile = null;
// Find the dragged file in the source pane
@@ -725,20 +828,85 @@ define(function (require, exports, module) {
}
if (draggedFile) {
- // close in the source pane
- CommandManager.execute(Commands.FILE_CLOSE, { file: draggedFile, paneId: sourcePaneId });
-
- // and open in the target pane
- MainViewManager.addToWorkingSet(paneId, draggedFile);
- CommandManager.execute(Commands.FILE_OPEN, { fullPath: draggedPath, paneId: paneId });
+ if (sourcePaneId !== paneId) {
+ // Check if the dragged file is currently active in the source pane
+ const currentActiveFileInSource = MainViewManager.getCurrentlyViewedFile(sourcePaneId);
+ const isActiveFileBeingMoved = currentActiveFileInSource &&
+ currentActiveFileInSource.fullPath === draggedPath;
+
+ // If the active file is being moved and there are other files in the source pane,
+ // switch to another file first to prevent placeholder creation
+ if (isActiveFileBeingMoved && sourceWorkingSet.length > 1) {
+ // Find another file to make active
+ let draggedIndex = -1;
+ for (let i = 0; i < sourceWorkingSet.length; i++) {
+ if (sourceWorkingSet[i].fullPath === draggedPath) {
+ draggedIndex = i;
+ break;
+ }
+ }
+
+ if (draggedIndex !== -1) {
+ let newActiveIndex = draggedIndex + 1;
+ if (newActiveIndex >= sourceWorkingSet.length) {
+ newActiveIndex = draggedIndex - 1;
+ }
+
+ if (newActiveIndex >= 0 && newActiveIndex < sourceWorkingSet.length) {
+ const newActiveFile = sourceWorkingSet[newActiveIndex];
+ // Open the new active file in the source pane before removing the dragged file
+ CommandManager.execute(Commands.FILE_OPEN, {
+ fullPath: newActiveFile.fullPath,
+ paneId: sourcePaneId
+ });
+ }
+ }
+ }
+
+ // If different panes, close in source pane
+ CommandManager.execute(Commands.FILE_CLOSE, { file: draggedFile, paneId: sourcePaneId });
+
+ // For non-empty panes, find current active file to place tab after it
+ if (!isEmptyPane && targetWorkingSet.length > 0) {
+ const currentActiveFile = MainViewManager.getCurrentlyViewedFile(paneId);
+
+ if (currentActiveFile) {
+ // Find index of current active file
+ let targetIndex = -1;
+ for (let i = 0; i < targetWorkingSet.length; i++) {
+ if (targetWorkingSet[i].fullPath === currentActiveFile.fullPath) {
+ targetIndex = i;
+ break;
+ }
+ }
+
+ if (targetIndex !== -1) {
+ // Add after current active file
+ MainViewManager.addToWorkingSet(paneId, draggedFile, targetIndex + 1);
+ } else {
+ // Fallback to adding at the end
+ MainViewManager.addToWorkingSet(paneId, draggedFile);
+ }
+ } else {
+ // No active file, add to the end
+ MainViewManager.addToWorkingSet(paneId, draggedFile);
+ }
+ } else {
+ // Empty pane, just add it
+ MainViewManager.addToWorkingSet(paneId, draggedFile);
+ }
+
+ // Open file in target pane
+ CommandManager.execute(Commands.FILE_OPEN, { fullPath: draggedPath, paneId: paneId });
+ } else if (isEmptyPane) {
+ // Same pane, empty pane case (should never happen but kept for safety)
+ MainViewManager.addToWorkingSet(paneId, draggedFile);
+ CommandManager.execute(Commands.FILE_OPEN, { fullPath: draggedPath, paneId: paneId });
+ }
}
}
- // reset all drag state stuff
- updateDragIndicator(null);
- draggedTab = null;
- dragOverTab = null;
- dragSourcePane = null;
+ cleanupDragState();
}
});
}
diff --git a/src/styles/Extn-TabBar.less b/src/styles/Extn-TabBar.less
index 6f17f686d4..3f3ba370df 100644
--- a/src/styles/Extn-TabBar.less
+++ b/src/styles/Extn-TabBar.less
@@ -138,7 +138,7 @@
opacity: 0.7;
font-weight: normal;
position: relative;
- top: 0.1rem;
+ top: 0.05rem;
}
.tab.active {
@@ -200,7 +200,7 @@
font-size: 1.5rem;
position: absolute;
left: 0.4rem;
- top: 0.33rem;
+ top: 0.28rem;
}
.tab.dirty::before {
@@ -409,7 +409,7 @@
}
.empty-pane-drop-target {
- border: 2px dashed #6db6ff !important;
+ border: 1px solid #6db6ff !important;
}
.dropdown-tab-item.placeholder-item .tab-name-container,