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";