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,