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:
- `
-
- ${dirtyHtml}
- ${iconHtml}
- ${item.name}
-
- ${closeIconHtml}
-
`,
+ `
+
+ ${dirtyHtml}
+ ${iconHtml}
+ ${item.name}
+
+ ${closeIconHtml}
+
`,
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 @@
-
+