diff --git a/.github/workflows/desktop-linux-prod-test-pull.yml b/.github/workflows/desktop-linux-prod-test-pull.yml index b18755b96c..57e41d7e3f 100644 --- a/.github/workflows/desktop-linux-prod-test-pull.yml +++ b/.github/workflows/desktop-linux-prod-test-pull.yml @@ -20,6 +20,8 @@ jobs: node-version: 20 - name: install Rust stable uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.85.1 - name: install dependencies (ubuntu only) run: | diff --git a/.github/workflows/desktop-linux-test-pull.yml b/.github/workflows/desktop-linux-test-pull.yml index 4e7efdb888..ac734955cf 100644 --- a/.github/workflows/desktop-linux-test-pull.yml +++ b/.github/workflows/desktop-linux-test-pull.yml @@ -19,6 +19,8 @@ jobs: node-version: 20 - name: install Rust stable uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.85.1 - name: install dependencies (ubuntu only) run: | diff --git a/.github/workflows/desktop-mac-m1-test-pull.yml b/.github/workflows/desktop-mac-m1-test-pull.yml index 5ae0ea9f87..15993c36d8 100644 --- a/.github/workflows/desktop-mac-m1-test-pull.yml +++ b/.github/workflows/desktop-mac-m1-test-pull.yml @@ -19,6 +19,8 @@ jobs: node-version: 20 - name: install Rust stable uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.85.1 - name: build phoenix dist-test run: | diff --git a/.github/workflows/desktop-mac-test-pull.yml b/.github/workflows/desktop-mac-test-pull.yml index ae9a92c5c7..a4b3fc8bc1 100644 --- a/.github/workflows/desktop-mac-test-pull.yml +++ b/.github/workflows/desktop-mac-test-pull.yml @@ -19,6 +19,8 @@ jobs: node-version: 20 - name: install Rust stable uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.85.1 - name: build phoenix dist-test run: | diff --git a/.github/workflows/desktop-windows-test-pull.yml b/.github/workflows/desktop-windows-test-pull.yml index aa02987ecd..504c101f08 100644 --- a/.github/workflows/desktop-windows-test-pull.yml +++ b/.github/workflows/desktop-windows-test-pull.yml @@ -19,6 +19,8 @@ jobs: node-version: 20 - name: install Rust stable uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.85.1 - name: build phoenix dist-test run: | diff --git a/docs/API-Reference/command/Commands.md b/docs/API-Reference/command/Commands.md index 6e7a0950a7..783376af39 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/package-lock.json b/package-lock.json index b9a6b8e218..778cf7d205 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "phoenix", - "version": "4.1.0-0", + "version": "4.1.1-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "phoenix", - "version": "4.1.0-0", + "version": "4.1.1-0", "dependencies": { "@bugsnag/js": "^7.18.0", "@floating-ui/dom": "^0.5.4", diff --git a/package.json b/package.json index 69496f4468..c121240959 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "phoenix", - "version": "4.1.0-0", - "apiVersion": "4.1.0", + "version": "4.1.1-0", + "apiVersion": "4.1.1", "homepage": "https://core.ai", "issues": { "url": "https://github.com/phcode-dev/phoenix/issues" @@ -117,4 +117,4 @@ "tinycolor2": "^1.4.2", "underscore": "^1.13.4" } -} +} \ No newline at end of file diff --git a/src-node/package-lock.json b/src-node/package-lock.json index 6177376876..6183efc95f 100644 --- a/src-node/package-lock.json +++ b/src-node/package-lock.json @@ -1,12 +1,12 @@ { "name": "@phcode/node-core", - "version": "4.1.0-0", + "version": "4.1.1-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@phcode/node-core", - "version": "4.1.0-0", + "version": "4.1.1-0", "license": "GNU-AGPL3.0", "dependencies": { "@phcode/fs": "^3.0.1", diff --git a/src-node/package.json b/src-node/package.json index e2abbf30f3..8bb880a30e 100644 --- a/src-node/package.json +++ b/src-node/package.json @@ -1,8 +1,8 @@ { "name": "@phcode/node-core", "description": "Phoenix Node Core", - "version": "4.1.0-0", - "apiVersion": "4.1.0", + "version": "4.1.1-0", + "apiVersion": "4.1.1", "keywords": [], "author": "arun@core.ai", "homepage": "https://github.com/phcode-dev/phoenix", diff --git a/src/assets/sample-projects/explore/out_on_a_limb.html b/src/assets/sample-projects/explore/out_on_a_limb.html deleted file mode 100644 index dcd1874a4f..0000000000 --- a/src/assets/sample-projects/explore/out_on_a_limb.html +++ /dev/null @@ -1,319 +0,0 @@ - - - - - Out On A Limb - - - -
- -
- - \ No newline at end of file diff --git a/src/command/Commands.js b/src/command/Commands.js index 2314c47e32..d94c550fd2 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -451,6 +451,12 @@ define(function (require, exports, module) { /** Toggles automatic working set sorting */ exports.CMD_WORKING_SORT_TOGGLE_AUTO = "cmd.sortWorkingSetToggleAuto"; // WorkingSetSort.js _handleToggleAutoSort() + /** Toggles working set visibility */ + exports.CMD_TOGGLE_SHOW_WORKING_SET = "cmd.toggleShowWorkingSet"; // SidebarView.js _handleToggleWorkingSet() + + /** Toggles file tabs visibility */ + exports.CMD_TOGGLE_SHOW_FILE_TABS = "cmd.toggleShowFileTabs"; // SidebarView.js _handleToggleFileTabs() + /** Opens keyboard navigation UI overlay */ exports.CMD_KEYBOARD_NAV_UI_OVERLAY = "cmd.keyboardNavUI"; // WorkingSetSort.js _handleToggleAutoSort() diff --git a/src/command/DefaultMenus.js b/src/command/DefaultMenus.js index b488abbe9b..9f94eb825c 100644 --- a/src/command/DefaultMenus.js +++ b/src/command/DefaultMenus.js @@ -345,6 +345,9 @@ define(function (require, exports, module) { splitview_menu.addMenuDivider(); splitview_menu.addMenuItem(Commands.CMD_WORKING_SORT_TOGGLE_AUTO); splitview_menu.addMenuItem(Commands.FILE_SHOW_FOLDERS_FIRST); + splitview_menu.addMenuDivider(); + splitview_menu.addMenuItem(Commands.CMD_TOGGLE_SHOW_WORKING_SET); + splitview_menu.addMenuItem(Commands.CMD_TOGGLE_SHOW_FILE_TABS); var project_cmenu = Menus.registerContextMenu(Menus.ContextMenuIds.PROJECT_MENU); project_cmenu.addMenuItem(Commands.FILE_NEW); @@ -474,4 +477,4 @@ define(function (require, exports, module) { Menus.getContextMenu(Menus.ContextMenuIds.WORKING_SET_CONTEXT_MENU).on("beforeContextMenuOpen", _setMenuItemsVisible); Menus.getContextMenu(Menus.ContextMenuIds.PROJECT_MENU).on("beforeContextMenuOpen", _setMenuItemsVisible); }); -}); \ No newline at end of file +}); diff --git a/src/config.json b/src/config.json index 9b39d0d024..a3c6c88410 100644 --- a/src/config.json +++ b/src/config.json @@ -38,8 +38,8 @@ "bugsnagEnv": "development" }, "name": "Phoenix Code", - "version": "4.1.0-0", - "apiVersion": "4.1.0", + "version": "4.1.1-0", + "apiVersion": "4.1.1", "homepage": "https://core.ai", "issues": { "url": "https://github.com/phcode-dev/phoenix/issues" diff --git a/src/extensions/default/Git/main.js b/src/extensions/default/Git/main.js index e1921ca047..aaf38e8458 100644 --- a/src/extensions/default/Git/main.js +++ b/src/extensions/default/Git/main.js @@ -27,7 +27,8 @@ define(function (require, exports, module) { "src/History", "src/NoRepo", "src/ProjectTreeMarks", - "src/Remotes" + "src/Remotes", + "src/TabBarIntegration" ]; require(modules); @@ -48,10 +49,12 @@ define(function (require, exports, module) { // export API's for other extensions if (typeof window === "object") { + const TabBarIntegration = require("src/TabBarIntegration"); window.phoenixGitEvents = { EventEmitter: EventEmitter, Events: Events, - Git + Git, + TabBarIntegration }; } }); diff --git a/src/extensions/default/Git/src/ErrorHandler.js b/src/extensions/default/Git/src/ErrorHandler.js index 9378171b17..c275cafc9b 100644 --- a/src/extensions/default/Git/src/ErrorHandler.js +++ b/src/extensions/default/Git/src/ErrorHandler.js @@ -46,7 +46,6 @@ define(function (require, exports) { exports.showError = function (err, title, options = {}) { const dontStripError = options.dontStripError; const errorMetric = options.errorMetric; - Metrics.countEvent(Metrics.EVENT_TYPE.GIT, 'dialogErr', errorMetric || "Show"); if (err.__shown) { return err; } exports.logError(err); @@ -71,6 +70,7 @@ define(function (require, exports) { errorBody = window.debugMode ? `${errorBody}\n${errorStack}` : errorBody; if(options.useNotification){ + Metrics.countEvent(Metrics.EVENT_TYPE.GIT, 'notifyErr', errorMetric || "Show"); NotificationUI.createToastFromTemplate(title, ``, { toastStyle: NotificationUI.NOTIFICATION_STYLES_CSS_CLASS.ERROR, @@ -78,6 +78,7 @@ define(function (require, exports) { instantOpen: true }); } else { + Metrics.countEvent(Metrics.EVENT_TYPE.GIT, 'dialogErr', errorMetric || "Show"); const compiledTemplate = Mustache.render(errorDialogTemplate, { title: title, body: errorBody, diff --git a/src/extensions/default/Git/src/TabBarIntegration.js b/src/extensions/default/Git/src/TabBarIntegration.js new file mode 100644 index 0000000000..24453f3166 --- /dev/null +++ b/src/extensions/default/Git/src/TabBarIntegration.js @@ -0,0 +1,90 @@ +define(function (require) { + const EventEmitter = require("src/EventEmitter"); + const Events = require("src/Events"); + const Git = require("src/git/Git"); + const Preferences = require("src/Preferences"); + + // the cache of file statuses by path + let fileStatusCache = {}; + + /** + * this function is responsible to get the Git status for a file path + * + * @param {string} fullPath - the file path + * @returns {Array|null} - Array of status strings or null if no status + */ + function getFileStatus(fullPath) { + return fileStatusCache[fullPath] || null; + } + + /** + * whether the file is modified or not + * + * @param {string} fullPath - the file path + * @returns {boolean} - True if the file is modified otherwise false + */ + function isModified(fullPath) { + const status = getFileStatus(fullPath); + if (!status) { + return false; + } + return status.some( + (statusType) => + statusType === Git.FILE_STATUS.MODIFIED || + statusType === Git.FILE_STATUS.RENAMED || + statusType === Git.FILE_STATUS.COPIED + ); + } + + /** + * whether the file is untracked or not + * + * @param {string} fullPath - the file path + * @returns {boolean} - True if the file is untracked otherwise false + */ + function isUntracked(fullPath) { + const status = getFileStatus(fullPath); + if (!status) { + return false; + } + + // return true if it's untracked or if it's newly added (which means it was untracked before staging) + return ( + status.includes(Git.FILE_STATUS.UNTRACKED) || + (status.includes(Git.FILE_STATUS.ADDED) && status.includes(Git.FILE_STATUS.STAGED)) + ); + } + + + // Update file status cache when Git status results are received + EventEmitter.on(Events.GIT_STATUS_RESULTS, function (files) { + // reset the cache + fileStatusCache = {}; + + const gitRoot = Preferences.get("currentGitRoot"); + if (!gitRoot) { + return; + } + + // we need to update cache with new status results + files.forEach(function (entry) { + const fullPath = gitRoot + entry.file; + fileStatusCache[fullPath] = entry.status; + }); + + // notify that file statuses have been updated + EventEmitter.emit("GIT_FILE_STATUS_CHANGED", fileStatusCache); + }); + + // clear cache when Git is disabled + EventEmitter.on(Events.GIT_DISABLED, function () { + fileStatusCache = {}; + EventEmitter.emit("GIT_FILE_STATUS_CHANGED", fileStatusCache); + }); + + return { + getFileStatus: getFileStatus, + isModified: isModified, + isUntracked: isUntracked + }; +}); diff --git a/src/extensionsIntegrated/Phoenix/html/login-dialog.html b/src/extensionsIntegrated/Phoenix/html/login-dialog.html new file mode 100644 index 0000000000..dbe692d262 --- /dev/null +++ b/src/extensionsIntegrated/Phoenix/html/login-dialog.html @@ -0,0 +1,17 @@ +
+ + +
diff --git a/src/extensionsIntegrated/Phoenix/html/profile-panel.html b/src/extensionsIntegrated/Phoenix/html/profile-panel.html new file mode 100644 index 0000000000..52dc6fcb59 --- /dev/null +++ b/src/extensionsIntegrated/Phoenix/html/profile-panel.html @@ -0,0 +1,39 @@ +
+ + +
diff --git a/src/extensionsIntegrated/Phoenix/main.js b/src/extensionsIntegrated/Phoenix/main.js index 94f0015324..5bb8dcacbc 100644 --- a/src/extensionsIntegrated/Phoenix/main.js +++ b/src/extensionsIntegrated/Phoenix/main.js @@ -32,23 +32,24 @@ define(function (require, exports, module) { Strings = require("strings"), Dialogs = require("widgets/Dialogs"), NotificationUI = require("widgets/NotificationUI"), - DefaultDialogs = require("widgets/DefaultDialogs"); + DefaultDialogs = require("widgets/DefaultDialogs"), + ProfileMenu = require("./profile-menu"); const PERSIST_STORAGE_DIALOG_DELAY_SECS = 60000; let $icon; function _addToolbarIcon() { - const helpButtonID = "help-button"; + const helpButtonID = "user-profile-button"; $icon = $("") .attr({ id: helpButtonID, href: "#", - class: "help", - title: Strings.CMD_SUPPORT + class: "user", + title: Strings.CMD_USER_PROFILE }) .appendTo($("#main-toolbar .bottom-buttons")); $icon.on('click', ()=>{ - Phoenix.app.openURLInDefaultBrowser(brackets.config.support_url); + ProfileMenu.init(); }); } function _showUnSupportedBrowserDialogue() { diff --git a/src/extensionsIntegrated/Phoenix/profile-menu.js b/src/extensionsIntegrated/Phoenix/profile-menu.js new file mode 100644 index 0000000000..4b4e6e927b --- /dev/null +++ b/src/extensionsIntegrated/Phoenix/profile-menu.js @@ -0,0 +1,221 @@ +define(function (require, exports, module) { + const Mustache = require("thirdparty/mustache/mustache"); + const PopUpManager = require("widgets/PopUpManager"); + + // HTML templates + const loginTemplate = require("text!./html/login-dialog.html"); + const profileTemplate = require("text!./html/profile-panel.html"); + + // for the popup DOM element + let $popup = null; + + // this is to track whether the popup is visible or not + let isPopupVisible = false; + + // if user is logged in we show the profile menu, otherwise we show the login menu + const isLoggedIn = false; + + const defaultLoginData = { + welcomeTitle: "Welcome to Phoenix Code", + signInBtnText: "Sign in to your account", + supportBtnText: "Contact support" + }; + + const defaultProfileData = { + initials: "CA", + userName: "Charly A.", + planName: "Paid Plan", + quotaLabel: "AI quota used", + quotaUsed: "7,000", + quotaTotal: "10,000", + quotaUnit: "tokens", + quotaPercent: 70, + accountBtnText: "Account details", + supportBtnText: "Contact support", + signOutBtnText: "Sign out" + }; + + function _handleSignInBtnClick() { + console.log("User clicked sign in button"); + } + + function _handleSignOutBtnClick() { + console.log("User clicked sign out"); + } + + function _handleContactSupportBtnClick() { + Phoenix.app.openURLInDefaultBrowser(brackets.config.support_url); + } + + function _handleAccountDetailsBtnClick() { + console.log("User clicked account details"); + } + + /** + * Close the popup if it's open + * this is called at various instances like when the user click on the profile icon even if the popup is open + * or when user clicks somewhere else on the document + */ + function closePopup() { + if ($popup) { + PopUpManager.removePopUp($popup); + $popup = null; + isPopupVisible = false; + } + } + + /** + * this function is to position the popup near the profile button + */ + function positionPopup() { + const $profileButton = $("#user-profile-button"); + + if ($profileButton.length && $popup) { + const buttonPos = $profileButton.offset(); + const popupWidth = $popup.outerWidth(); + const windowWidth = $(window).width(); + + // pos above the profile button + let top = buttonPos.top - $popup.outerHeight() - 10; + + // If popup would go off the right edge of the window, align right edge of popup with right edge of button + let left = Math.min( + buttonPos.left - popupWidth + $profileButton.outerWidth(), + windowWidth - popupWidth - 10 + ); + + // never go off left edge + left = Math.max(10, left); + + $popup.css({ + top: top + "px", + left: left + "px" + }); + } + } + + /** + * Shows the sign-in popup when the user is not logged in + * @param {Object} loginData - Data to populate the template (optional) + */ + function showLoginPopup(loginData) { + // If popup is already visible, just close it + if (isPopupVisible) { + closePopup(); + return; + } + + // Merge provided data with defaults + const templateData = $.extend({}, defaultLoginData, loginData || {}); + + // create the popup element + closePopup(); // close any existing popup first + + // Render template with data + const renderedTemplate = Mustache.render(loginTemplate, templateData); + $popup = $(renderedTemplate); + + $("body").append($popup); + isPopupVisible = true; + + positionPopup(); + + PopUpManager.addPopUp($popup, function() { + $popup.remove(); + $popup = null; + isPopupVisible = false; + }, true, { closeCurrentPopups: true }); + + // event handlers for buttons + $popup.find("#phoenix-signin-btn").on("click", function () { + _handleSignInBtnClick(); + closePopup(); + }); + + $popup.find("#phoenix-support-btn").on("click", function () { + _handleContactSupportBtnClick(); + closePopup(); + }); + + // handle window resize to reposition popup + $(window).on("resize.profilePopup", function () { + if (isPopupVisible) { + positionPopup(); + } + }); + } + + /** + * Shows the user profile popup when the user is logged in + * @param {Object} profileData - Data to populate the template (optional) + */ + function showProfilePopup(profileData) { + // If popup is already visible, just close it + if (isPopupVisible) { + closePopup(); + return; + } + + // Merge provided data with defaults + const templateData = $.extend({}, defaultProfileData, profileData || {}); + + closePopup(); + + // Render template with data + const renderedTemplate = Mustache.render(profileTemplate, templateData); + $popup = $(renderedTemplate); + + $("body").append($popup); + isPopupVisible = true; + positionPopup(); + + PopUpManager.addPopUp($popup, function() { + $popup.remove(); + $popup = null; + isPopupVisible = false; + }, true, { closeCurrentPopups: true }); + + $popup.find("#phoenix-account-btn").on("click", function () { + _handleAccountDetailsBtnClick(); + closePopup(); + }); + + $popup.find("#phoenix-support-btn").on("click", function () { + _handleContactSupportBtnClick(); + closePopup(); + }); + + $popup.find("#phoenix-signout-btn").on("click", function () { + _handleSignOutBtnClick(); + closePopup(); + }); + + // handle window resize to reposition popup + $(window).on("resize.profilePopup", function () { + if (isPopupVisible) { + positionPopup(); + } + }); + } + + /** + * Toggle the profile popup based on the user's login status + * this function is called inside the src/extensionsIntegrated/Phoenix/main.js when user clicks on the profile icon + * @param {Object} data - Data to populate the templates (optional) + */ + function init(data) { + // check if the popup is already visible or not. if visible close it + if (isPopupVisible) { + closePopup(); + return; + } + + if (isLoggedIn) { + showProfilePopup(data); + } else { + showLoginPopup(data); + } + } + + exports.init = init; +}); diff --git a/src/extensionsIntegrated/TabBar/drag-drop.js b/src/extensionsIntegrated/TabBar/drag-drop.js index 7ece013fe9..ef0d912653 100644 --- a/src/extensionsIntegrated/TabBar/drag-drop.js +++ b/src/extensionsIntegrated/TabBar/drag-drop.js @@ -18,7 +18,6 @@ * */ - /* This file houses the functionality for dragging and dropping tabs */ /* eslint-disable no-invalid-this */ define(function (require, exports, module) { @@ -40,7 +39,6 @@ define(function (require, exports, module) { let scrollInterval = null; let dragSourcePane = null; - /** * Initialize drag and drop functionality for tab bars * This is called from `main.js` @@ -59,14 +57,13 @@ define(function (require, exports, module) { // Create drag indicator element if it doesn't exist if (!dragIndicator) { dragIndicator = $('
'); - $('body').append(dragIndicator); + $("body").append(dragIndicator); } // add initialization for empty panes initEmptyPaneDropTargets(); } - /** * Setup drag and drop for a specific tab bar * Makes tabs draggable and adds all the necessary event listeners @@ -93,7 +90,6 @@ define(function (require, exports, module) { $tabs.on("dragend", handleDragEnd); } - /** * Setup container-level drag events * This enables dropping tabs in empty spaces and auto-scrolling @@ -103,6 +99,54 @@ define(function (require, exports, module) { */ function setupContainerDrag(containerSelector) { const $container = $(containerSelector); + let lastKnownMousePosition = { x: 0 }; + const boundaryTolerance = 50; // px tolerance outside the container that still allows dropping + + // create a larger drop zone around the container + // this is done to make sure that even if the tab is not exactly over the tab bar, we still allow drag-drop + const createOuterDropZone = () => { + if (draggedTab && !$("#tab-drag-extended-zone").length) { + // an invisible larger zone around the container that can still receive drops + const containerRect = $container[0].getBoundingClientRect(); + const $outerZone = $('
').css({ + position: "fixed", + top: containerRect.top - boundaryTolerance, + left: containerRect.left - boundaryTolerance, + width: containerRect.width + boundaryTolerance * 2, + height: containerRect.height + boundaryTolerance * 2, + zIndex: 9999, + pointerEvents: "all" + }); + + $("body").append($outerZone); + + $outerZone.on("dragover", function (e) { + e.preventDefault(); + e.stopPropagation(); + lastKnownMousePosition.x = e.originalEvent.clientX; + + autoScrollContainer($container[0], lastKnownMousePosition.x); + + updateDragIndicatorFromOuterZone($container, lastKnownMousePosition.x); + + return false; + }); + + $outerZone.on("drop", function (e) { + e.preventDefault(); + e.stopPropagation(); + + // to handle drop the same way as if it happened in the container + handleOuterZoneDrop($container, lastKnownMousePosition.x); + + return false; + }); + } + }; + + const removeOuterDropZone = () => { + $("#tab-drag-extended-zone").remove(); + }; // When dragging over the container but not directly over a tab element $container.on("dragover", function (e) { @@ -110,6 +154,8 @@ define(function (require, exports, module) { e.preventDefault(); } + lastKnownMousePosition.x = e.originalEvent.clientX; + // Clear any existing scroll interval if (scrollInterval) { clearInterval(scrollInterval); @@ -120,30 +166,35 @@ define(function (require, exports, module) { // 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); + if (draggedTab) { + // Only continue scrolling if still dragging + autoScrollContainer(this, lastKnownMousePosition.x); } 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) { + 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 onLeftSide = mouseX < containerRect.left + containerRect.width / 2; - const $tabs = $container.find('.tab'); + 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); } } + + // Create the extended drop zone if we're actively dragging + if (draggedTab) { + createOuterDropZone(); + } }); // handle drop on the container (empty space) @@ -153,14 +204,15 @@ define(function (require, exports, module) { } // hide the drag indicator updateDragIndicator(null); + removeOuterDropZone(); // 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 onLeftSide = mouseX < containerRect.left + containerRect.width / 2; - const $tabs = $container.find('.tab'); + 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]; @@ -184,8 +236,86 @@ define(function (require, exports, module) { } } }); - } + /** + * Updates the drag indicator when mouse is in the extended zone (outside actual tab bar) + * @param {jQuery} $container - The tab bar container + * @param {number} mouseX - Current mouse X position + */ + function updateDragIndicatorFromOuterZone($container, mouseX) { + const containerRect = $container[0].getBoundingClientRect(); + const $tabs = $container.find(".tab"); + + if ($tabs.length) { + // Determine if dropping on left half or right half + let onLeftSide = true; + let targetTab; + + // If beyond the right edge, use the last tab + if (mouseX > containerRect.right) { + targetTab = $tabs.last()[0]; + onLeftSide = false; + } + // If beyond the left edge, use the first tab + else if (mouseX < containerRect.left) { + targetTab = $tabs.first()[0]; + onLeftSide = true; + } + // If within bounds, find the closest tab + else { + onLeftSide = mouseX < containerRect.left + containerRect.width / 2; + targetTab = onLeftSide ? $tabs.first()[0] : $tabs.last()[0]; + } + + updateDragIndicator(targetTab, onLeftSide); + } + } + + /** + * Handles drops that occur in the extended drop zone + * @param {jQuery} $container - The tab bar container + * @param {number} mouseX - Current mouse X position + */ + function handleOuterZoneDrop($container, mouseX) { + const containerRect = $container[0].getBoundingClientRect(); + const $tabs = $container.find(".tab"); + + if ($tabs.length && draggedTab) { + // Determine drop position similar to updateDragIndicatorFromOuterZone + let onLeftSide = true; + let targetTab; + + if (mouseX > containerRect.right) { + targetTab = $tabs.last()[0]; + onLeftSide = false; + } else if (mouseX < containerRect.left) { + targetTab = $tabs.first()[0]; + onLeftSide = true; + } else { + onLeftSide = mouseX < containerRect.left + containerRect.width / 2; + targetTab = onLeftSide ? $tabs.first()[0] : $tabs.last()[0]; + } + + // Process the drop + const isSecondPane = $container.attr("id") === "phoenix-tab-bar-2"; + const targetPaneId = isSecondPane ? "second-pane" : "first-pane"; + const draggedPath = $(draggedTab).attr("data-path"); + const targetPath = $(targetTab).attr("data-path"); + + if (dragSourcePane !== targetPaneId) { + // cross-pane drop + moveTabBetweenPanes(dragSourcePane, targetPaneId, draggedPath, targetPath, onLeftSide); + } else { + // same pane drop + moveWorkingSetItem(targetPaneId, draggedPath, targetPath, onLeftSide); + } + } + + // Clean up + updateDragIndicator(null); + removeOuterDropZone(); + } + } /** * enhanced auto-scroll function for container when the mouse is near its left or right edge @@ -196,35 +326,38 @@ define(function (require, exports, module) { */ function autoScrollContainer(container, mouseX) { const rect = container.getBoundingClientRect(); - const edgeThreshold = 50; // teh threshold distance from the edge + const edgeThreshold = 100; // Increased threshold for edge detection (was 50) + const outerThreshold = 50; // Distance outside the container that still triggers scrolling - // Calculate distance from edges - const distanceFromLeft = mouseX - rect.left; - const distanceFromRight = rect.right - mouseX; + // Calculate distance from edges, allowing for mouse to be slightly outside bounds + const distanceFromLeft = mouseX - (rect.left - outerThreshold); + const distanceFromRight = rect.right + outerThreshold - 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; + // Only activate scrolling when within the threshold (including the outer buffer) + if (distanceFromLeft < edgeThreshold + outerThreshold && mouseX < rect.right) { + // Non-linear scroll speed: faster as you get closer to the edge + scrollSpeed = -Math.pow(1 - distanceFromLeft / (edgeThreshold + outerThreshold), 2) * 25; + } else if (distanceFromRight < edgeThreshold + outerThreshold && mouseX > rect.left) { + scrollSpeed = Math.pow(1 - distanceFromRight / (edgeThreshold + outerThreshold), 2) * 25; } - // apply scrolling if needed + // 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)) { + 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 @@ -237,14 +370,14 @@ define(function (require, exports, module) { // 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); + e.originalEvent.dataTransfer.effectAllowed = "move"; + e.originalEvent.dataTransfer.setData("text/html", this.innerHTML); // Store which pane this tab came from dragSourcePane = $(this).closest("#phoenix-tab-bar-2").length > 0 ? "second-pane" : "first-pane"; // Add dragging class for styling - $(this).addClass('dragging'); + $(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 @@ -253,7 +386,6 @@ define(function (require, exports, module) { }, 0); } - /** * Handle the dragover event to enable drop * Updates the visual indicator showing where the tab will be dropped @@ -264,13 +396,13 @@ define(function (require, exports, module) { if (e.preventDefault) { e.preventDefault(); // Allows us to drop } - e.originalEvent.dataTransfer.dropEffect = 'move'; + 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 midPoint = targetRect.left + targetRect.width / 2; const onLeftSide = mouseX < midPoint; updateDragIndicator(this, onLeftSide); @@ -278,7 +410,6 @@ define(function (require, exports, module) { return false; } - /** * Handle entering a potential drop target * Applies styling to indicate the current drop target @@ -287,10 +418,9 @@ define(function (require, exports, module) { */ function handleDragEnter(e) { dragOverTab = this; - $(this).addClass('drag-target'); + $(this).addClass("drag-target"); } - /** * Handle leaving a potential drop target * Removes styling when no longer hovering over a drop target @@ -302,14 +432,13 @@ define(function (require, exports, module) { // 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'); + $(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 @@ -333,7 +462,7 @@ define(function (require, exports, module) { // 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 midPoint = targetRect.left + targetRect.width / 2; const onLeftSide = mouseX < midPoint; // Check if dragging between different panes @@ -348,7 +477,6 @@ define(function (require, exports, module) { return false; } - /** * Handle the end of a drag operation * Cleans up classes and resets state variables @@ -356,7 +484,7 @@ define(function (require, exports, module) { * @param {Event} e - The event object */ function handleDragEnd(e) { - $(".tab").removeClass('dragging drag-target'); + $(".tab").removeClass("dragging drag-target"); updateDragIndicator(null); draggedTab = null; dragOverTab = null; @@ -367,12 +495,15 @@ define(function (require, exports, module) { clearInterval(scrollInterval); scrollInterval = null; } - } + // Remove the extended drop zone if it exists + $("#tab-drag-extended-zone").remove(); + } /** * Update the drag indicator position and visibility * The indicator shows where the tab will be dropped + * Ensures the indicator stays within the bounds of the tab bar * * @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 @@ -382,20 +513,30 @@ define(function (require, exports, module) { dragIndicator.hide(); return; } + // Get the target tab's position and size const targetRect = targetTab.getBoundingClientRect(); + + // Find the containing tab bar to ensure the indicator stays within bounds + const $tabBar = $(targetTab).closest("#phoenix-tab-bar, #phoenix-tab-bar-2"); + const tabBarRect = $tabBar[0] ? $tabBar[0].getBoundingClientRect() : null; + if (onLeftSide) { // Position indicator at the left edge of the target tab + // Ensure it doesn't go beyond the tab bar's left edge + const leftPos = tabBarRect ? Math.max(targetRect.left, tabBarRect.left) : targetRect.left; dragIndicator.css({ top: targetRect.top, - left: targetRect.left, + left: leftPos, height: targetRect.height }); } else { // Position indicator at the right edge of the target tab + // Ensure it doesn't go beyond the tab bar's right edge + const rightPos = tabBarRect ? Math.min(targetRect.right, tabBarRect.right) : targetRect.right; dragIndicator.css({ top: targetRect.top, - left: targetRect.right, + left: rightPos, height: targetRect.height }); } @@ -478,10 +619,7 @@ define(function (require, exports, module) { // 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 } - ); + CommandManager.execute(Commands.FILE_CLOSE, { file: draggedFile, paneId: sourcePaneId }); // Calculate where to add it in the target pane let targetInsertIndex; @@ -521,7 +659,6 @@ define(function (require, exports, module) { setupEmptyPaneDropTarget($secondPaneHolder, "second-pane"); } - /** * sets up the whole pane as a drop target when it has no tabs * @@ -546,7 +683,7 @@ define(function (require, exports, module) { $(this).addClass("empty-pane-drop-target"); // set the drop effect - e.originalEvent.dataTransfer.dropEffect = 'move'; + e.originalEvent.dataTransfer.dropEffect = "move"; } }); @@ -571,8 +708,8 @@ define(function (require, exports, module) { const draggedPath = $(draggedTab).attr("data-path"); // Determine source pane - const sourcePaneId = $(draggedTab) - .closest("#phoenix-tab-bar-2").length > 0 ? "second-pane" : "first-pane"; + 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) { @@ -589,10 +726,7 @@ define(function (require, exports, module) { if (draggedFile) { // close in the source pane - CommandManager.execute( - Commands.FILE_CLOSE, - { file: draggedFile, paneId: sourcePaneId } - ); + CommandManager.execute(Commands.FILE_CLOSE, { file: draggedFile, paneId: sourcePaneId }); // and open in the target pane MainViewManager.addToWorkingSet(paneId, draggedFile); @@ -609,7 +743,6 @@ define(function (require, exports, module) { }); } - module.exports = { init }; diff --git a/src/extensionsIntegrated/TabBar/main.js b/src/extensionsIntegrated/TabBar/main.js index b1a778f93b..9c43888f3f 100644 --- a/src/extensionsIntegrated/TabBar/main.js +++ b/src/extensionsIntegrated/TabBar/main.js @@ -23,9 +23,9 @@ 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 FileUtils = require("file/FileUtils"); const CommandManager = require("command/CommandManager"); const Commands = require("command/Commands"); const DocumentManager = require("document/DocumentManager"); @@ -42,8 +42,6 @@ define(function (require, exports, module) { 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` @@ -54,7 +52,6 @@ define(function (require, exports, module) { 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 @@ -68,7 +65,6 @@ define(function (require, exports, module) { // 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]); @@ -108,8 +104,6 @@ define(function (require, exports, module) { } } - - /** * Responsible for creating the tab element * Note: this creates a tab (for a single file) not the tab bar @@ -128,34 +122,57 @@ define(function (require, exports, module) { const activePathInPane = activeFileInPane ? activeFileInPane.fullPath : null; // Check if this file is active in its pane - const isActive = (entry.path === activePathInPane); + const isActive = entry.path === activePathInPane; // Current active pane (used to determine whether to add the blue underline) const currentActivePane = MainViewManager.getActivePaneId(); - const isPaneActive = (paneId === currentActivePane); + const isPaneActive = paneId === currentActivePane; const isDirty = Helper._isFileModified(FileSystem.getFileForPath(entry.path)); + const isPlaceholder = entry.isPlaceholder === true; + + let gitStatus = ""; // this will be shown in the tooltip when a tab is hovered + let gitStatusClass = ""; // for styling + + if (window.phoenixGitEvents && window.phoenixGitEvents.TabBarIntegration) { + const TabBarIntegration = window.phoenixGitEvents.TabBarIntegration; + + // find the Git status + // if untracked we add the git-new class and U char + // if modified we add the git-modified class and M char + if (TabBarIntegration.isUntracked(entry.path)) { + gitStatus = "Untracked"; + gitStatusClass = "git-new"; + } else if (TabBarIntegration.isModified(entry.path)) { + gitStatus = "Modified"; + gitStatusClass = "git-modified"; + } + } - // create tab with active class + // create tab with all the appropriate classes const $tab = $( `
-
-
-
-
` + ${isActive ? "active" : ""} + ${isDirty ? "dirty" : ""} + ${isPlaceholder ? "placeholder" : ""} + ${gitStatusClass}" + data-path="${entry.path}" + title="${Phoenix.app.getDisplayPath(entry.path)}${gitStatus ? " (" + gitStatus + ")" : ""}"> +
+
+
+ ` ); // Add the file icon const $icon = Helper._getFileIcon(entry); - $tab.find('.tab-icon').append($icon); + $tab.find(".tab-icon").append($icon); // Check if we have a directory part in the displayName - const $tabName = $tab.find('.tab-name'); + 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 parts = entry.displayName.split("/"); const dirName = parts[0]; const fileName = parts[1]; @@ -170,32 +187,38 @@ define(function (require, exports, module) { if (isActive && !isPaneActive) { // if it's active but in a non-active pane, we add a special class // to style differently in CSS to indicate that it's active but not in the active pane - $tab.addClass('active-in-inactive-pane'); + $tab.addClass("active-in-inactive-pane"); + } + + // if this is a placeholder tab in inactive pane, we need to use the brown styling + // instead of the blue one for active tabs + if (isPlaceholder && isActive && !isPaneActive) { + $tab.removeClass("active"); + $tab.addClass("active-in-inactive-pane"); } return $tab; } - /** * Creates the tab bar and adds it to the DOM */ function createTabBar() { - if (!Preference.tabBarEnabled || Preference.numberOfTabs === 0) { + if (!Preference.tabBarEnabled || Preference.tabBarNumberOfTabs === 0) { + cleanupTabBar(); return; } // clean up any existing tab bars first and start fresh cleanupTabBar(); - const $paneHeader = $('.pane-header'); + const $paneHeader = $(".pane-header"); if ($paneHeader.length === 1) { $tabBar = $(TabBarHTML); // since we need to add the tab bar before the editor which has .not-editor class $(".pane-header").after($tabBar); WorkspaceManager.recomputeLayout(true); updateTabs(); - } else if ($paneHeader.length === 2) { $tabBar = $(TabBarHTML); $tabBar2 = $(TabBarHTML2); @@ -209,7 +232,6 @@ define(function (require, exports, module) { } } - /** * 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 @@ -218,27 +240,91 @@ define(function (require, exports, module) { // 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'))) { + // just to make sure that the number of tabs is not set to 0 + if (Preference.tabBarNumberOfTabs === 0) { + cleanupTabBar(); + return; + } + + // Check for active files not in working set in any pane + const activePane = MainViewManager.getActivePaneId(); + const firstPaneFile = MainViewManager.getCurrentlyViewedFile("first-pane"); + const secondPaneFile = MainViewManager.getCurrentlyViewedFile("second-pane"); + + // when a file is opened from the filetree and is not present in the working set, then it is a placeholder + let firstPanePlaceholder = null; + let secondPanePlaceholder = null; + + // Check if active file in first pane is not in the working set + if (firstPaneFile && !Global.firstPaneWorkingSet.some((entry) => entry.path === firstPaneFile.fullPath)) { + firstPanePlaceholder = { + path: firstPaneFile.fullPath, + name: firstPaneFile.name, + isPlaceholder: true, + displayName: firstPaneFile.name // for now we initialize with name, will check for duplicates later + }; + } + + // Check if active file in second pane is not in the working set + if (secondPaneFile && !Global.secondPaneWorkingSet.some((entry) => entry.path === secondPaneFile.fullPath)) { + secondPanePlaceholder = { + path: secondPaneFile.fullPath, + name: secondPaneFile.name, + isPlaceholder: true, + displayName: secondPaneFile.name + }; + } + + // check for duplicate file names between placeholder tabs and working set entries + if (firstPanePlaceholder) { + // if any working set file has the same name as the placeholder + const hasDuplicate = Global.firstPaneWorkingSet.some((entry) => entry.name === firstPanePlaceholder.name); + + if (hasDuplicate) { + // extract directory name from path + const path = firstPanePlaceholder.path; + const parentDir = FileUtils.getDirectoryPath(path); + const dirParts = parentDir.split("/"); + const parentDirName = dirParts[dirParts.length - 2] || ""; + + // Update displayName with directory + firstPanePlaceholder.displayName = parentDirName + "/" + firstPanePlaceholder.name; + } + } + + if (secondPanePlaceholder) { + const hasDuplicate = Global.secondPaneWorkingSet.some((entry) => entry.name === secondPanePlaceholder.name); + + if (hasDuplicate) { + const path = secondPanePlaceholder.path; + const parentDir = FileUtils.getDirectoryPath(path); + const dirParts = parentDir.split("/"); + const parentDirName = dirParts[dirParts.length - 2] || ""; + + secondPanePlaceholder.displayName = parentDirName + "/" + secondPanePlaceholder.name; + } + } + + // Create tab bar if there's a placeholder or a file in the working set + if ( + (Global.firstPaneWorkingSet.length > 0 || firstPanePlaceholder) && + (!$("#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'))) { + if ( + (Global.secondPaneWorkingSet.length > 0 || secondPanePlaceholder) && + (!$("#phoenix-tab-bar-2").length || $("#phoenix-tab-bar-2").is(":hidden")) + ) { createTabBar(); } - const $firstTabBar = $('#phoenix-tab-bar'); + const $firstTabBar = $("#phoenix-tab-bar"); // Update first pane's tabs if ($firstTabBar.length) { $firstTabBar.empty(); - if (Global.firstPaneWorkingSet.length > 0) { - + if (Global.firstPaneWorkingSet.length > 0 || firstPanePlaceholder) { // 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); @@ -249,59 +335,91 @@ define(function (require, exports, module) { tabsCountP1 = Global.firstPaneWorkingSet.length; } - let displayedEntries = Global.firstPaneWorkingSet.slice(0, tabsCountP1); + let displayedEntries = []; - 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; + // check if active file is in the working set but would be excluded by tab count + if (firstPaneFile && Preference.tabBarNumberOfTabs > 0) { + const activeFileIndex = Global.firstPaneWorkingSet.findIndex( + (entry) => entry.path === firstPaneFile.fullPath + ); + + if (activeFileIndex >= 0 && activeFileIndex >= Preference.tabBarNumberOfTabs) { + // active file is in working set but would be excluded by tab count + // Show active file and one less from the top N files + displayedEntries = [ + ...Global.firstPaneWorkingSet.slice(0, Preference.tabBarNumberOfTabs - 1), + Global.firstPaneWorkingSet[activeFileIndex] + ]; + } else { + // Active file is either not in working set or already included in top N files + displayedEntries = Global.firstPaneWorkingSet.slice(0, tabsCountP1); } + } else { + displayedEntries = Global.firstPaneWorkingSet.slice(0, tabsCountP1); } + + // Add working set tabs displayedEntries.forEach(function (entry) { $firstTabBar.append(createTab(entry, "first-pane")); }); + + // Add placeholder tab if needed + if (firstPanePlaceholder) { + $firstTabBar.append(createTab(firstPanePlaceholder, "first-pane")); + } } } - const $secondTabBar = $('#phoenix-tab-bar-2'); + const $secondTabBar = $("#phoenix-tab-bar-2"); // Update second pane's tabs if ($secondTabBar.length) { $secondTabBar.empty(); - if (Global.secondPaneWorkingSet.length > 0) { - + if (Global.secondPaneWorkingSet.length > 0 || secondPanePlaceholder) { 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; + let displayedEntries2 = []; + + if (secondPaneFile && Preference.tabBarNumberOfTabs > 0) { + const activeFileIndex = Global.secondPaneWorkingSet.findIndex( + (entry) => entry.path === secondPaneFile.fullPath + ); + + if (activeFileIndex >= 0 && activeFileIndex >= Preference.tabBarNumberOfTabs) { + displayedEntries2 = [ + ...Global.secondPaneWorkingSet.slice(0, Preference.tabBarNumberOfTabs - 1), + Global.secondPaneWorkingSet[activeFileIndex] + ]; + } else { + displayedEntries2 = Global.secondPaneWorkingSet.slice(0, tabsCountP2); } + } else { + displayedEntries2 = Global.secondPaneWorkingSet.slice(0, tabsCountP2); } + + // Add working set tabs displayedEntries2.forEach(function (entry) { $secondTabBar.append(createTab(entry, "second-pane")); }); + + // Add placeholder tab if needed + if (secondPanePlaceholder) { + $secondTabBar.append(createTab(secondPanePlaceholder, "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 no files are present in a pane and no placeholder, we want to hide the tab bar for that pane + if (Global.firstPaneWorkingSet.length === 0 && !firstPanePlaceholder && $("#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')); + if (Global.secondPaneWorkingSet.length === 0 && !secondPanePlaceholder && $("#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"); @@ -327,10 +445,9 @@ define(function (require, exports, module) { } // handle drag and drop - DragDrop.init($('#phoenix-tab-bar'), $('#phoenix-tab-bar-2')); + DragDrop.init($("#phoenix-tab-bar"), $("#phoenix-tab-bar-2")); } - /** * Removes existing tab bar and cleans up */ @@ -348,16 +465,14 @@ define(function (require, exports, module) { WorkspaceManager.recomputeLayout(true); } - /** * handle click events on the tabs to open the file */ function handleTabClick() { - // delegate event handling for both tab bars $(document).on("click", ".phoenix-tab-bar .tab", function (event) { // check if the clicked element is the close button - if ($(event.target).hasClass('fa-times') || $(event.target).closest('.tab-close').length) { + if ($(event.target).hasClass("fa-times") || $(event.target).closest(".tab-close").length) { // Get the file path from the data-path attribute of the parent tab const filePath = $(this).attr("data-path"); @@ -369,10 +484,7 @@ define(function (require, exports, module) { // get the file object const fileObj = FileSystem.getFileForPath(filePath); // close the file - CommandManager.execute( - Commands.FILE_CLOSE, - { file: fileObj, paneId: paneId } - ); + CommandManager.execute(Commands.FILE_CLOSE, { file: fileObj, paneId: paneId }); // Prevent default behavior event.preventDefault(); @@ -383,7 +495,7 @@ define(function (require, exports, module) { // delegate event handling for both tab bars $(document).on("mousedown", ".phoenix-tab-bar .tab", function (event) { - if ($(event.target).hasClass('fa-times') || $(event.target).closest('.tab-close').length) { + if ($(event.target).hasClass("fa-times") || $(event.target).closest(".tab-close").length) { return; } // Get the file path from the data-path attribute @@ -394,9 +506,17 @@ define(function (require, exports, module) { const isSecondPane = $(this).closest("#phoenix-tab-bar-2").length > 0; const paneId = isSecondPane ? "second-pane" : "first-pane"; const currentActivePane = MainViewManager.getActivePaneId(); - const isPaneActive = (paneId === currentActivePane); + const isPaneActive = paneId === currentActivePane; const currentFile = MainViewManager.getCurrentlyViewedFile(currentActivePane); - if(isPaneActive && currentFile && currentFile.fullPath === filePath) { + + // Check if this is a placeholder tab + if ($(this).hasClass("placeholder")) { + // Add the file to the working set when placeholder tab is clicked + const fileObj = FileSystem.getFileForPath(filePath); + MainViewManager.addToWorkingSet(paneId, fileObj); + } + + if (isPaneActive && currentFile && currentFile.fullPath === filePath) { return; } CommandManager.execute(Commands.FILE_OPEN, { fullPath: filePath, paneId: paneId }); @@ -422,7 +542,6 @@ define(function (require, exports, module) { }); } - // 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 @@ -441,6 +560,12 @@ define(function (require, exports, module) { // For editor changes, update only the tabs. MainViewManager.on(MainViewManager.EVENT_CURRENT_FILE_CHANGE, debounceUpdateTabs); + // to listen for the Git status changes + // make sure that the git extension is available + if (window.phoenixGitEvents && window.phoenixGitEvents.EventEmitter) { + window.phoenixGitEvents.EventEmitter.on("GIT_FILE_STATUS_CHANGED", debounceUpdateTabs); + } + // For working set changes, update only the tabs. const events = [ "workingSetAdd", @@ -470,8 +595,7 @@ define(function (require, exports, module) { // Update UI if ($tabBar) { const $tab = $tabBar.find(`.tab[data-path="${filePath}"]`); - $tab.toggleClass('dirty', doc.isDirty); - + $tab.toggleClass("dirty", doc.isDirty); // Update the working set data // First pane @@ -483,11 +607,10 @@ define(function (require, exports, module) { } } - // Also update the $tab2 if it exists if ($tabBar2) { const $tab2 = $tabBar2.find(`.tab[data-path="${filePath}"]`); - $tab2.toggleClass('dirty', doc.isDirty); + $tab2.toggleClass("dirty", doc.isDirty); // Second pane for (let i = 0; i < Global.secondPaneWorkingSet.length; i++) { @@ -500,7 +623,6 @@ define(function (require, exports, module) { }); } - /** * This is called when the tab bar preference is changed either, * from the preferences file or the menu bar @@ -526,17 +648,13 @@ define(function (require, exports, module) { * 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 - }); - } - ); + 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 + }); + }); } /** @@ -545,7 +663,6 @@ define(function (require, exports, module) { * when its scrolled down, the tab bar will scroll to the right */ function setupTabBarScrolling() { - // common handler for both the tab bars function handleMouseWheel(e) { // get the tab bar element that is being scrolled @@ -565,7 +682,7 @@ define(function (require, exports, module) { } // attach the wheel event handler to both tab bars - $(document).on('wheel', '#phoenix-tab-bar, #phoenix-tab-bar-2', handleMouseWheel); + $(document).on("wheel", "#phoenix-tab-bar, #phoenix-tab-bar-2", handleMouseWheel); } AppInit.appReady(function () { @@ -587,7 +704,7 @@ define(function (require, exports, module) { handleTabClick(); Overflow.init(); - DragDrop.init($('#phoenix-tab-bar'), $('#phoenix-tab-bar-2')); + DragDrop.init($("#phoenix-tab-bar"), $("#phoenix-tab-bar-2")); // setup the mouse wheel scrolling setupTabBarScrolling(); diff --git a/src/extensionsIntegrated/TabBar/more-options.js b/src/extensionsIntegrated/TabBar/more-options.js index f734741f44..a1b5a37d80 100644 --- a/src/extensionsIntegrated/TabBar/more-options.js +++ b/src/extensionsIntegrated/TabBar/more-options.js @@ -40,10 +40,13 @@ define(function (require, exports, module) { Strings.CLOSE_ALL_TABS, Strings.CLOSE_UNMODIFIED_TABS, "---", + Strings.CMD_FILE_RENAME, + Strings.CMD_FILE_DELETE, + Strings.CMD_SHOW_IN_TREE, + "---", Strings.REOPEN_CLOSED_FILE ]; - /** * "CLOSE TAB" * this function handles the closing of the tab that was right-clicked @@ -57,26 +60,10 @@ define(function (require, exports, module) { const fileObj = FileSystem.getFileForPath(filePath); // Execute close command with file object and pane ID - CommandManager.execute( - Commands.FILE_CLOSE, - { file: fileObj, paneId: paneId } - ); + 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 in the specified pane @@ -97,14 +84,10 @@ define(function (require, exports, module) { // close each file in the pane, start from the rightmost [to avoid index shifts] for (let i = workingSet.length - 1; i >= 0; i--) { const fileObj = FileSystem.getFileForPath(workingSet[i].path); - CommandManager.execute( - Commands.FILE_CLOSE, - { file: fileObj, paneId: paneId } - ); + CommandManager.execute(Commands.FILE_CLOSE, { file: fileObj, paneId: paneId }); } } - /** * "CLOSE UNMODIFIED TABS" * This will close all tabs that are not modified in the specified pane @@ -123,19 +106,15 @@ define(function (require, exports, module) { } // get all those entries that are not dirty - const unmodifiedEntries = workingSet.filter(entry => !entry.isDirty); + const unmodifiedEntries = workingSet.filter((entry) => !entry.isDirty); // close each unmodified file in the pane for (let i = unmodifiedEntries.length - 1; i >= 0; i--) { const fileObj = FileSystem.getFileForPath(unmodifiedEntries[i].path); - CommandManager.execute( - Commands.FILE_CLOSE, - { file: fileObj, paneId: paneId } - ); + CommandManager.execute(Commands.FILE_CLOSE, { file: fileObj, paneId: paneId }); } } - /** * "CLOSE TABS TO THE LEFT" * This function is responsible for closing all tabs to the left of the right-clicked tab @@ -155,24 +134,21 @@ define(function (require, exports, module) { } // find the index of the current file in the working set - const currentIndex = workingSet.findIndex(entry => entry.path === filePath); + const currentIndex = workingSet.findIndex((entry) => entry.path === filePath); - if (currentIndex > 0) { // we only proceed if there are tabs to the left + if (currentIndex > 0) { + // we only proceed if there are tabs to the left // get all files to the left of the current file const filesToClose = workingSet.slice(0, currentIndex); // Close each file, starting from the rightmost [to avoid index shifts] for (let i = filesToClose.length - 1; i >= 0; i--) { const fileObj = FileSystem.getFileForPath(filesToClose[i].path); - CommandManager.execute( - Commands.FILE_CLOSE, - { file: fileObj, paneId: paneId } - ); + CommandManager.execute(Commands.FILE_CLOSE, { file: fileObj, paneId: paneId }); } } } - /** * "CLOSE TABS TO THE RIGHT" * This function is responsible for closing all tabs to the right of the right-clicked tab @@ -192,7 +168,7 @@ define(function (require, exports, module) { } // get the index of the current file in the working set - const currentIndex = workingSet.findIndex(entry => entry.path === filePath); + const currentIndex = workingSet.findIndex((entry) => entry.path === filePath); // only proceed if there are tabs to the right if (currentIndex !== -1 && currentIndex < workingSet.length - 1) { // get all files to the right of the current file @@ -200,15 +176,11 @@ define(function (require, exports, module) { for (let i = filesToClose.length - 1; i >= 0; i--) { const fileObj = FileSystem.getFileForPath(filesToClose[i].path); - CommandManager.execute( - Commands.FILE_CLOSE, - { file: fileObj, paneId: paneId } - ); + CommandManager.execute(Commands.FILE_CLOSE, { file: fileObj, paneId: paneId }); } } } - /** * "REOPEN CLOSED FILE" * This just calls the reopen closed file command. everthing else is handled there @@ -218,6 +190,59 @@ define(function (require, exports, module) { CommandManager.execute(Commands.FILE_REOPEN_CLOSED); } + /** + * "RENAME FILE" + * This function handles the renaming of the file that was right-clicked + * + * @param {String} filePath - path of the file to rename + */ + function handleFileRename(filePath) { + if (filePath) { + // First ensure the sidebar is visible so users can see the rename action + CommandManager.execute(Commands.SHOW_SIDEBAR); + + // Get the file object using FileSystem + const fileObj = FileSystem.getFileForPath(filePath); + + // Execute the rename command with the file object + CommandManager.execute(Commands.FILE_RENAME, { file: fileObj }); + } + } + + /** + * "DELETE FILE" + * This function handles the deletion of the file that was right-clicked + * + * @param {String} filePath - path of the file to delete + */ + function handleFileDelete(filePath) { + if (filePath) { + // Get the file object using FileSystem + const fileObj = FileSystem.getFileForPath(filePath); + + // Execute the delete command with the file object + CommandManager.execute(Commands.FILE_DELETE, { file: fileObj }); + } + } + + /** + * "SHOW IN FILE TREE" + * This function handles showing the file in the file tree + * + * @param {String} filePath - path of the file to show in file tree + */ + function handleShowInFileTree(filePath) { + if (filePath) { + // First ensure the sidebar is visible so users can see the file in the tree + CommandManager.execute(Commands.SHOW_SIDEBAR); + + // Get the file object using FileSystem + const fileObj = FileSystem.getFileForPath(filePath); + + // Execute the show in tree command with the file object + CommandManager.execute(Commands.NAVIGATE_SHOW_IN_FILE_TREE, { file: fileObj }); + } + } /** * This function is called when a tab is right-clicked @@ -242,8 +267,13 @@ define(function (require, exports, module) { zIndex: 1000 }); + // Add a custom class to override the max-height, not sure why a scroll bar was coming but this did the trick + dropdown.dropdownExtraClasses = "tabbar-context-menu"; + dropdown.showDropdown(); + $(".tabbar-context-menu").css("max-height", "300px"); + // handle the option selection dropdown.on("select", function (e, item) { _handleSelection(item, filePath, paneId); @@ -279,6 +309,15 @@ define(function (require, exports, module) { case Strings.CLOSE_UNMODIFIED_TABS: handleCloseUnmodifiedTabs(paneId); break; + case Strings.CMD_FILE_RENAME: + handleFileRename(filePath); + break; + case Strings.CMD_FILE_DELETE: + handleFileDelete(filePath); + break; + case Strings.CMD_SHOW_IN_TREE: + handleShowInFileTree(filePath); + break; case Strings.REOPEN_CLOSED_FILE: reopenClosedFile(); break; diff --git a/src/extensionsIntegrated/TabBar/overflow.js b/src/extensionsIntegrated/TabBar/overflow.js index 317989e877..169e6e63fd 100644 --- a/src/extensionsIntegrated/TabBar/overflow.js +++ b/src/extensionsIntegrated/TabBar/overflow.js @@ -75,6 +75,7 @@ define(function (require, exports, module) { name: $tab.find('.tab-name').text(), isActive: $tab.hasClass('active'), isDirty: $tab.hasClass('dirty'), + isPlaceholder: $tab.hasClass('placeholder'), $icon: $tab.find('.tab-icon').clone() }; @@ -148,17 +149,22 @@ define(function (require, exports, module) { `; + // add placeholder class to style it differently + const placeholderClass = item.isPlaceholder ? ' placeholder-name' : ''; + // return html for this item return { html: - ``, + ``, enabled: true }; }); @@ -223,6 +229,14 @@ define(function (require, exports, module) { // Set the active pane and open the file MainViewManager.setActivePaneId(paneId); CommandManager.execute(Commands.FILE_OPEN, { fullPath: filePath }); + + // get the tab bar element based on paneId and scroll to the active tab + // we use setTimeout to ensure that the DOM has updated after the file open command + setTimeout(function () { + const $tabBarElement = paneId === "first-pane" ? + $("#phoenix-tab-bar") : $("#phoenix-tab-bar-2"); + scrollToActiveTab($tabBarElement); + }, 100); } }); diff --git a/src/index.html b/src/index.html index c13f595ff0..d147cf1df0 100644 --- a/src/index.html +++ b/src/index.html @@ -68,7 +68,7 @@ - +