diff --git a/docs/API-Reference/project/WorkingSetView.md b/docs/API-Reference/project/WorkingSetView.md
index c202ff693c..9b05a76404 100644
--- a/docs/API-Reference/project/WorkingSetView.md
+++ b/docs/API-Reference/project/WorkingSetView.md
@@ -3,6 +3,14 @@
const WorkingSetView = brackets.getModule("project/WorkingSetView")
```
+
+
+## refreshSelection()
+Recomputes the selection marker of all Pane View List Views and scrolls
+the selected item into view. Needed after the working set becomes visible
+again, as marker positions computed while it was display:none are all 0.
+
+**Kind**: global function
## refresh()
diff --git a/docs/API-Reference/utils/NodeUtils.md b/docs/API-Reference/utils/NodeUtils.md
index 5a1acd91b6..f8ef63a400 100644
--- a/docs/API-Reference/utils/NodeUtils.md
+++ b/docs/API-Reference/utils/NodeUtils.md
@@ -123,7 +123,7 @@ This is only available in the native app.
-## execFileWithInput(command, [args], [options]) ⇒ Promise.<{code: number, stdout: string, stderr: string}>
+## execFileWithInput(command, [args], [options]) ⇒ [Promise.<ExecFileResult>](#ExecFileResult)
Runs an executable with the given args, feeding it text on stdin and capturing its output -
a one-shot filter-style invocation (e.g. `ruff format -` for the Python beautifier). No
shell is involved. Resolves with the exit code rather than rejecting on non-zero, so
@@ -271,3 +271,15 @@ operation - the promise then rejects with a "cancelled" error.
| --- | --- | --- |
| cancel | function | aborts the in-flight operation |
+
+
+## ExecFileResult : Object
+**Kind**: global typedef
+**Properties**
+
+| Name | Type | Description |
+| --- | --- | --- |
+| code | number | the process's exit code |
+| stdout | string | |
+| stderr | string | |
+
diff --git a/src/project/SidebarView.js b/src/project/SidebarView.js
index ed17a37d88..27a94e7a7f 100644
--- a/src/project/SidebarView.js
+++ b/src/project/SidebarView.js
@@ -301,6 +301,9 @@ define(function (require, exports, module) {
if(getPref) {
// refer to brackets.less file for styles
$workingSet.removeClass("working-set-hidden");
+ // marker positions computed while the container was display:none are
+ // all 0, so recompute the selection now that it is visible again
+ WorkingSetView.refreshSelection();
} else {
$workingSet.addClass("working-set-hidden");
}
diff --git a/src/project/WorkingSetView.js b/src/project/WorkingSetView.js
index bfa4cb7b95..cb8da7106c 100644
--- a/src/project/WorkingSetView.js
+++ b/src/project/WorkingSetView.js
@@ -121,6 +121,17 @@ define(function (require, exports, module) {
*/
var _DRAG_MOVE_DETECTION_START = 3;
+ /**
+ * Recomputes the selection marker of all Pane View List Views and scrolls
+ * the selected item into view. Needed after the working set becomes visible
+ * again, as marker positions computed while it was display:none are all 0.
+ */
+ function refreshSelection() {
+ _.forEach(_views, function (view) {
+ view._updateListSelection();
+ });
+ }
+
/**
* Refreshes all Pane View List Views
*/
@@ -298,7 +309,8 @@ define(function (require, exports, module) {
offset,
$copy,
$ghost,
- draggingCurrentFile;
+ draggingCurrentFile,
+ openedOnMouseDown = false;
function initDragging() {
itemHeight = $el.height();
@@ -778,6 +790,10 @@ define(function (require, exports, module) {
.always(function () {
postDropCleanup();
});
+ } else if (openedOnMouseDown) {
+ // file was already opened on mouse down for a fast switch feel,
+ // nothing left to do here
+ postDropCleanup();
} else {
// Normal right and left click - select the item
FileViewController.setFileViewFocus(FileViewController.WORKING_SET_VIEW);
@@ -853,7 +869,16 @@ define(function (require, exports, module) {
return;
}
-
+ // open the file right away on mouse down like the file tree and the
+ // editor tab bar do, so switching files feels fast instead of waiting
+ // for mouse up. A drag can still follow - the item just becomes the
+ // current file as soon as it is pressed.
+ if (!tryClosing) {
+ openedOnMouseDown = true;
+ FileViewController.setFileViewFocus(FileViewController.WORKING_SET_VIEW);
+ CommandManager.execute(Commands.FILE_OPEN, {fullPath: sourceFile.fullPath,
+ paneId: sourceView.paneId});
+ }
e.stopPropagation();
});
@@ -1628,6 +1653,7 @@ define(function (require, exports, module) {
// Public API
exports.createWorkingSetViewForPane = createWorkingSetViewForPane;
exports.refresh = refresh;
+ exports.refreshSelection = refreshSelection;
exports.addIconProvider = addIconProvider;
exports.addClassProvider = addClassProvider;
exports.syncSelectionIndicator = syncSelectionIndicator;
diff --git a/src/utils/NodeUtils.js b/src/utils/NodeUtils.js
index bcca700659..a99eb6b8c7 100644
--- a/src/utils/NodeUtils.js
+++ b/src/utils/NodeUtils.js
@@ -233,6 +233,13 @@ define(function (require, exports, module) {
return utilsConnector.execPeer("setExecutableBits", {filePath});
}
+ /**
+ * @typedef {Object} ExecFileResult
+ * @property {number} code - the process's exit code
+ * @property {string} stdout
+ * @property {string} stderr
+ */
+
/**
* Runs an executable with the given args, feeding it text on stdin and capturing its output -
* a one-shot filter-style invocation (e.g. `ruff format -` for the Python beautifier). No
@@ -246,7 +253,7 @@ define(function (require, exports, module) {
* @param {string} [options.stdinText] - written to the process's stdin, then closed
* @param {string} [options.cwd] - working directory
* @param {number} [options.timeoutMs] - kill the process and reject after this long
- * @return {Promise<{code: number, stdout: string, stderr: string}>}
+ * @return {Promise}
*/
async function execFileWithInput(command, args, options) {
if(!Phoenix.isNativeApp) {
diff --git a/src/view/SidebarTabs.js b/src/view/SidebarTabs.js
index 97736b9edc..4774f4fe72 100644
--- a/src/view/SidebarTabs.js
+++ b/src/view/SidebarTabs.js
@@ -40,7 +40,8 @@ define(function (require, exports, module) {
const AppInit = require("utils/AppInit"),
EventDispatcher = require("utils/EventDispatcher"),
Metrics = require("utils/Metrics"),
- PreferencesManager = require("preferences/PreferencesManager");
+ PreferencesManager = require("preferences/PreferencesManager"),
+ WorkingSetView = require("project/WorkingSetView");
// --- Constants -----------------------------------------------------------
@@ -447,6 +448,11 @@ define(function (require, exports, module) {
}
if (previousTabId !== id) {
+ if (id === SIDEBAR_TAB_FILES) {
+ // working set marker positions computed while the files tab content was
+ // display:none are all 0, so recompute the selection now that it is visible
+ WorkingSetView.refreshSelection();
+ }
exports.trigger(EVENT_TAB_CHANGED, id, previousTabId);
}
}
diff --git a/test/spec/WorkingSetView-integ-test.js b/test/spec/WorkingSetView-integ-test.js
index de04c7f32b..f0e4c741f7 100644
--- a/test/spec/WorkingSetView-integ-test.js
+++ b/test/spec/WorkingSetView-integ-test.js
@@ -107,6 +107,11 @@ define(function (require, exports, module) {
});
afterEach(async function () {
+ // restore state in case a selection marker test failed midway
+ testWindow.brackets.test.PreferencesManager.set("showWorkingSet", true);
+ const SidebarTabs = testWindow.brackets.test.SidebarTabs;
+ SidebarTabs.setActiveTab(SidebarTabs.SIDEBAR_TAB_FILES);
+ testWindow.$(".open-files-container").css("height", "");
await testWindow.closeAllFiles();
});
@@ -341,6 +346,89 @@ define(function (require, exports, module) {
});
});
+ function _isMarkerAlignedWithSelection() {
+ const $ = testWindow.$;
+ const $selected = $(".open-files-container li.selected");
+ const $marker = $(".open-files-container .sidebar-selection");
+ return $selected.length === 1 && $marker.length === 1 &&
+ Math.abs($marker.offset().top - $selected.offset().top) < 2;
+ }
+
+ it("should reposition selection marker and reveal selection when working set is shown again", async function () {
+ const $ = testWindow.$;
+ const PreferencesManager = testWindow.brackets.test.PreferencesManager;
+ const fileList = MainViewManager.getWorkingSet(MainViewManager.ACTIVE_PANE);
+ const $container = $(".open-files-container");
+
+ // view the first file
+ await awaitsForDone(CommandManager.execute(Commands.FILE_OPEN, { fullPath: fileList[0].fullPath }),
+ "FILE_OPEN first file");
+
+ // shrink the list so the second item overflows and must be scrolled to
+ $container.css("height", "30px");
+ $container.scrollTop(0);
+
+ // hide the working set
+ PreferencesManager.set("showWorkingSet", false);
+ await awaitsFor(function () {
+ return $("#working-set-list-container").hasClass("working-set-hidden");
+ }, "working set to hide");
+
+ // switch to the second file while hidden - marker offsets all compute
+ // to 0 on display:none elements, so it cannot be positioned now
+ await awaitsForDone(CommandManager.execute(Commands.FILE_OPEN, { fullPath: fileList[1].fullPath }),
+ "FILE_OPEN second file");
+
+ // show the working set again
+ PreferencesManager.set("showWorkingSet", true);
+ await awaitsFor(function () {
+ return !$("#working-set-list-container").hasClass("working-set-hidden");
+ }, "working set to show");
+
+ // the marker must align with the newly selected item and the item
+ // must be scrolled into view
+ await awaitsFor(_isMarkerAlignedWithSelection, "selection marker to align with selected item");
+ await awaitsFor(function () {
+ const $selected = $(".open-files-container li.selected");
+ const containerTop = $container.offset().top;
+ const containerBottom = containerTop + $container.height();
+ return $selected.offset().top >= containerTop &&
+ ($selected.offset().top + $selected.height()) <= containerBottom;
+ }, "selected item to be scrolled into view");
+ });
+
+ it("should reposition selection marker when switching back to the files sidebar tab", async function () {
+ const $ = testWindow.$;
+ const SidebarTabs = testWindow.brackets.test.SidebarTabs;
+ const fileList = MainViewManager.getWorkingSet(MainViewManager.ACTIVE_PANE);
+ const TEST_TAB = "sidebar-tab-wsv-selection-test";
+
+ // view the first file
+ await awaitsForDone(CommandManager.execute(Commands.FILE_OPEN, { fullPath: fileList[0].fullPath }),
+ "FILE_OPEN first file");
+
+ // switch to another sidebar tab, which hides the files tab content
+ SidebarTabs.addTab(TEST_TAB, "wsv test", "fa-solid fa-flask");
+ SidebarTabs.setActiveTab(TEST_TAB);
+ await awaitsFor(function () {
+ return !$("#working-set-list-container").is(":visible");
+ }, "working set to be hidden by tab switch");
+
+ // switch to the second file while the files tab content is hidden
+ await awaitsForDone(CommandManager.execute(Commands.FILE_OPEN, { fullPath: fileList[1].fullPath }),
+ "FILE_OPEN second file");
+
+ // switch back to the files tab
+ SidebarTabs.setActiveTab(SidebarTabs.SIDEBAR_TAB_FILES);
+ await awaitsFor(function () {
+ return $("#working-set-list-container").is(":visible");
+ }, "working set to be visible again");
+
+ await awaitsFor(_isMarkerAlignedWithSelection, "selection marker to align with selected item");
+
+ SidebarTabs.removeTab(TEST_TAB);
+ });
+
it("should allow refresh to be used to update the class list", async function () {
function classProvider(file) {
return "one";