From aa88be5417cd112a8215cfbb219a3edccf1c86cf Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 21 Apr 2026 11:18:57 +0530 Subject: [PATCH 01/10] feat: move Show-in-File-Tree into sidebar + always scroll-to-selected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace the CCB file-label block with a hover-only binoculars button in the sidebar's #project-files-header, next to #collapse-folders. The file name was redundant with the live-preview heading and file tabs. - Merge the new button into the existing CollapseFolders extension so the two hover-revealed sidebar actions share a single owner and stylesheet. - DocumentCommandHandlers.handleShowInTree: after ProjectManager.showInTree resolves, scroll the selected tree node into view. Previously the auto-scroll in FileTreeView only fired on unselected→selected transitions, so re-invoking the command on an already-selected file was a no-op when the user had scrolled away. - Add mainview:CentralControlBar integ-test suite covering layout, CCB buttons, and the new sidebar hover button, using CommandManager's beforeExecuteCommand event instead of spies. --- src/document/DocumentCommandHandlers.js | 12 +- .../CollapseFolders/main.js | 36 ++- src/index.html | 5 - src/styles/CentralControlBar.less | 6 - src/styles/Extn-CollapseFolders.less | 25 +- src/view/CentralControlBar.js | 5 - test/UnitTestSuite.js | 1 + test/control-bar-tests-todo.md | 33 +-- test/spec/CentralControlBar-integ-test.js | 218 ++++++++++++++++++ 9 files changed, 293 insertions(+), 48 deletions(-) create mode 100644 test/spec/CentralControlBar-integ-test.js diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 7292015592..03ad037734 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -54,6 +54,7 @@ define(function (require, exports, module) { Menus = require("command/Menus"), UrlParams = require("utils/UrlParams").UrlParams, StatusBar = require("widgets/StatusBar"), + ViewUtils = require("utils/ViewUtils"), WorkspaceManager = require("view/WorkspaceManager"), LanguageManager = require("language/LanguageManager"), NewFileContentManager = require("features/NewFileContentManager"), @@ -1957,7 +1958,16 @@ define(function (require, exports, module) { CommandManager.execute(Commands.VIEW_HIDE_SIDEBAR); } SidebarTabs.setActiveTab(SidebarTabs.SIDEBAR_TAB_FILES); - ProjectManager.showInTree(activeFile); + // FileTreeView only auto-scrolls when the selection flips unselected→selected. + // Re-invoking the command on an already-selected file would otherwise be a + // no-op when the user has scrolled away — force-scroll the selected node + // into view so "Show in File Tree" always reveals the row. + ProjectManager.showInTree(activeFile).always(function () { + const $selected = $("#project-files-container .selected-node").first(); + if ($selected.length) { + ViewUtils.scrollElementIntoView($("#project-files-container"), $selected, true); + } + }); } } diff --git a/src/extensionsIntegrated/CollapseFolders/main.js b/src/extensionsIntegrated/CollapseFolders/main.js index f01ef52994..541d79537c 100644 --- a/src/extensionsIntegrated/CollapseFolders/main.js +++ b/src/extensionsIntegrated/CollapseFolders/main.js @@ -18,14 +18,21 @@ * */ -/* Displays a Collapse button in the sidebar area */ -/* when the button gets clicked, it closes all the directories recursively that are opened */ -/* Styling for the button is done in `../../styles/Extn-CollapseFolders.less` */ +/* Displays sidebar-hover action buttons: "show in file tree" (binoculars) and + * "collapse all folders" (stacked chevrons). Both appear on sidebar hover so the + * sidebar stays visually quiet when the user isn't interacting with it. */ +/* Styling for both buttons is done in `../../styles/Extn-CollapseFolders.less` */ define(function (require, exports, module) { const AppInit = require("utils/AppInit"); + const CommandManager = require("command/CommandManager"); + const Commands = require("command/Commands"); const ProjectManager = require("project/ProjectManager"); const Strings = require("strings"); + const SHOW_IN_TREE_SVG = ''; + /** * This is the main function that handles the closing of all the directories */ @@ -52,30 +59,37 @@ define(function (require, exports, module) { } } + function _handleShowInTreeClick() { + CommandManager.execute(Commands.NAVIGATE_SHOW_IN_FILE_TREE); + } + /** - * This function is responsible to create the 'Collapse All' button - * and append it to the sidebar area on the project-files-header + * Append the sidebar hover actions: a "Show in File Tree" binoculars button + * followed by the "Collapse All" chevron button. Both live in + * #project-files-header and become visible only on #sidebar:hover. */ - function createCollapseButton() { + function createSidebarHoverButtons() { const $projectFilesHeader = $("#project-files-header"); - // make sure that we were able to get the project-files-header DOM element if ($projectFilesHeader.length === 0) { return; } - // create the collapse btn + const $showInTreeBtn = $('
' + SHOW_IN_TREE_SVG + '
'); + $showInTreeBtn.on("click", _handleShowInTreeClick); + $projectFilesHeader.append($showInTreeBtn); + const $collapseBtn = $(`
`); - $collapseBtn.on("click", handleCollapseBtnClick); - $projectFilesHeader.append($collapseBtn); // append the btn to the project-files-header + $projectFilesHeader.append($collapseBtn); } AppInit.appReady(function () { - createCollapseButton(); + createSidebarHoverButtons(); }); }); diff --git a/src/index.html b/src/index.html index a29e28d7f3..c790b320ed 100644 --- a/src/index.html +++ b/src/index.html @@ -964,11 +964,6 @@
- - -
diff --git a/src/styles/CentralControlBar.less b/src/styles/CentralControlBar.less index 6252272771..8686b5f230 100644 --- a/src/styles/CentralControlBar.less +++ b/src/styles/CentralControlBar.less @@ -73,12 +73,6 @@ pointer-events: none; } - svg.ccb-binoculars-icon { - width: 15px; - height: 15px; - pointer-events: none; - } - &:hover { color: @project-panel-text-1; background-color: rgba(255, 255, 255, 0.08); diff --git a/src/styles/Extn-CollapseFolders.less b/src/styles/Extn-CollapseFolders.less index ecdfaab1ca..91b6f82b8b 100644 --- a/src/styles/Extn-CollapseFolders.less +++ b/src/styles/Extn-CollapseFolders.less @@ -1,17 +1,21 @@ -#collapse-folders { +#collapse-folders, +#show-in-file-tree { display: flex; - flex-direction: column; align-items: center; justify-content: center; padding: 0.2em 0.65em; margin-top: 0.1em; position: absolute !important; - right: 0; opacity: 0; visibility: hidden; transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out; +} + +#collapse-folders { + flex-direction: column; + right: 0; .collapse-icon { font-size: 0.5em; @@ -19,7 +23,20 @@ } } -#sidebar:hover #collapse-folders { +#show-in-file-tree { + // Sit to the left of #collapse-folders. + right: 24px; + + .show-in-tree-icon { + // Sized to match the combined stacked-chevron glyph of #collapse-folders. + width: 11px; + height: 11px; + pointer-events: none; + } +} + +#sidebar:hover #collapse-folders, +#sidebar:hover #show-in-file-tree { opacity: 1; visibility: visible; } diff --git a/src/view/CentralControlBar.js b/src/view/CentralControlBar.js index a25b77c884..b53b2987c1 100644 --- a/src/view/CentralControlBar.js +++ b/src/view/CentralControlBar.js @@ -261,10 +261,6 @@ define(function (require, exports, module) { e.preventDefault(); _executeCmd(Commands.VIEW_HIDE_SIDEBAR); }); - $("#ccbShowInTreeBtn").on("click", function (e) { - e.preventDefault(); - _executeCmd(Commands.NAVIGATE_SHOW_IN_FILE_TREE); - }); } const _toggleDesignModeCommand = CommandManager.register(Strings.CMD_TOGGLE_DESIGN_MODE, @@ -288,7 +284,6 @@ define(function (require, exports, module) { // navForwardButton get their localized titles from NavigationProvider.) $("#ccbCollapseEditorBtn").attr("title", Strings.CCB_SWITCH_TO_DESIGN_MODE); $("#ccbSidebarToggleBtn").attr("title", Strings.CMD_TOGGLE_SIDEBAR); - $("#ccbShowInTreeBtn").attr("title", Strings.CMD_SHOW_IN_TREE); $("#ccbUndoBtn").attr("title", Strings.CMD_UNDO); $("#ccbRedoBtn").attr("title", Strings.CMD_REDO); $("#ccbSaveBtn").attr("title", Strings.CMD_FILE_SAVE); diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js index c18b0e392c..97d84344be 100644 --- a/test/UnitTestSuite.js +++ b/test/UnitTestSuite.js @@ -92,6 +92,7 @@ define(function (require, exports, module) { require("spec/ExtensionUtils-integ-test"); require("spec/InlineEditorProviders-integ-test"); require("spec/PreferencesManager-integ-test"); + require("spec/CentralControlBar-integ-test"); require("spec/MainViewFactory-integ-test"); require("spec/MainViewManager-integ-test"); require("spec/SidebarTabs-integ-test"); diff --git a/test/control-bar-tests-todo.md b/test/control-bar-tests-todo.md index 42a5c192f8..453b2bde04 100644 --- a/test/control-bar-tests-todo.md +++ b/test/control-bar-tests-todo.md @@ -33,29 +33,30 @@ Keep this file updated as we add coverage; remove lines as suites land. - [ ] `#ccbSidebarToggleBtn` executes `VIEW_HIDE_SIDEBAR` and the icon flips `fa-angles-left` ↔ `fa-angles-right` on panelCollapsed/panelExpanded. - [ ] The old `#sidebar-toggle-btn` in the menubar is NOT in the DOM. -- [ ] `#ccbShowInTreeBtn` is rendered in `.ccb-group-nav` directly below - `#searchNav` and has a `title` of `Strings.CMD_SHOW_IN_TREE`. -- [ ] Clicking `#ccbShowInTreeBtn` executes `NAVIGATE_SHOW_IN_FILE_TREE` (if - sidebar was hidden, it re-opens as part of the command). -- [ ] Binoculars `` renders and inherits `.ccb-btn` color - (`currentColor` on the path). -- [ ] Neither `#ccbFileLabel`, `.ccb-group-file`, `.ccb-file-label`, - `.ccb-file-name`, nor `.ccb-file-dot` exists in the DOM or in the - compiled CSS. +- [ ] `.ccb-group-nav` holds exactly `searchNav`, `navBackButton`, + `navForwardButton` — no show-in-tree button lives in the CCB. + +## 2a. #show-in-file-tree button (sidebar) + +- [ ] `#show-in-file-tree` is a child of `#project-files-header` and sits + before `#collapse-folders` in DOM order. Title equals + `Strings.CMD_SHOW_IN_TREE`. +- [ ] Binoculars `` uses `fill="currentColor"` so the glyph tracks the + sidebar text color. +- [ ] Button is hidden by default (`opacity: 0`, `visibility: hidden`) and + only shows on `#sidebar:hover`, matching the `#collapse-folders` + affordance. +- [ ] Clicking `#show-in-file-tree` executes `NAVIGATE_SHOW_IN_FILE_TREE`. ## 3. Toggle Design Mode command - [ ] `Commands.VIEW_TOGGLE_DESIGN_MODE === "view.toggleDesignMode"` is registered at module load, visible via `CommandManager.get`. -- [ ] Default keybinding: `Ctrl-F11` (from `base-config/keyboard.json`). -- [ ] File menu has "Toggle Design Mode" directly below "Live Preview" and - above "Reload Live Preview". - [ ] Command's checked state mirrors `WorkspaceManager.isInDesignMode()` on both entry and exit. -- [ ] Clicking `#ccbCollapseEditorBtn` routes through the command - (verify via a spy on `CommandManager.execute`). -- [ ] Icon swap: `fa-feather` (expanded) ↔ `fa-code` (design mode). Title - swap: "Switch to Visual Edit" ↔ "Switch to Code Editor". +- [ ] Clicking `#ccbCollapseEditorBtn`toggles design mode +- [ ] Icon swap: `pen-nib svg` (expanded) ↔ `fa-code` (design mode). Title + swap: "Switch to desin mode" ↔ "Switch to Code Editor".(please see the exact string to check.) ## 4. Enter design mode diff --git a/test/spec/CentralControlBar-integ-test.js b/test/spec/CentralControlBar-integ-test.js new file mode 100644 index 0000000000..c1dd2c4d67 --- /dev/null +++ b/test/spec/CentralControlBar-integ-test.js @@ -0,0 +1,218 @@ +/* + * GNU AGPL-3.0 License + * + * Copyright (c) 2021 - present core.ai . All rights reserved. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License + * for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. + * + */ + +/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsFor */ + +define(function (require, exports, module) { + + const SpecRunnerUtils = require("spec/SpecRunnerUtils"), + Strings = require("strings"); + + const CCB_WIDTH = 30; + + describe("mainview:CentralControlBar", function () { + + let testWindow, + brackets, + CommandManager, + Commands, + SidebarView, + _$; + + beforeAll(async function () { + testWindow = await SpecRunnerUtils.createTestWindowAndRun(); + brackets = testWindow.brackets; + CommandManager = brackets.test.CommandManager; + Commands = brackets.test.Commands; + SidebarView = brackets.test.SidebarView; + _$ = testWindow.$; + }, 30000); + + afterAll(async function () { + // Make sure the sidebar is visible so we leave a clean state. + if (!SidebarView.isVisible()) { + SidebarView.show(); + } + testWindow = null; + brackets = null; + CommandManager = null; + Commands = null; + SidebarView = null; + _$ = null; + await SpecRunnerUtils.closeTestWindow(); + }, 30000); + + // Helper: record every command the CentralControlBar dispatches during the + // fn() body. Preferred over Jasmine spies because it exercises the real + // CommandManager dispatch path — the `beforeExecuteCommand` event fires on + // every `CommandManager.execute`, so we measure the actual effect of the + // click handler without monkey-patching. + function recordCommands(fn) { + const executed = []; + const handler = function (event, id) { executed.push(id); }; + CommandManager.on(CommandManager.EVENT_BEFORE_EXECUTE_COMMAND, handler); + try { + fn(); + } finally { + CommandManager.off(CommandManager.EVENT_BEFORE_EXECUTE_COMMAND, handler); + } + return executed; + } + + beforeEach(function () { + // Every test starts with the sidebar visible. + if (!SidebarView.isVisible()) { + SidebarView.show(); + } + }); + + describe("1. Layout", function () { + + it("should have #centralControlBar at boot, 30px wide, between sidebar and .content", function () { + const $ccb = _$("#centralControlBar"); + expect($ccb.length).toBe(1); + expect($ccb.outerWidth()).toBe(CCB_WIDTH); + + const sidebarRight = _$("#sidebar")[0].getBoundingClientRect().right; + const contentLeft = _$(".content")[0].getBoundingClientRect().left; + const ccbRect = $ccb[0].getBoundingClientRect(); + + // CCB sits immediately to the right of the sidebar and immediately + // to the left of .content (allow sub-pixel rounding). + expect(Math.abs(ccbRect.left - sidebarRight)).toBeLessThan(2); + expect(Math.abs(ccbRect.right - contentLeft)).toBeLessThan(2); + }); + + it("should set sidebar's data-minsize to 30 (so drag can't auto-collapse below CCB)", function () { + expect(_$("#sidebar").attr("data-minsize")).toBe("30"); + }); + + it("should shift the sidebar's resizer handle right by the CCB width via CSS", function () { + const $resizer = _$("#sidebar > .horz-resizer"); + expect($resizer.length).toBe(1); + const transform = testWindow.getComputedStyle($resizer[0]).transform; + // Computed transform is a matrix; translateX(30px) → matrix(1,0,0,1,30,0). + expect(transform).toMatch(/matrix\(1,\s*0,\s*0,\s*1,\s*30,\s*0\)/); + }); + + it("should keep the resizer-shift CSS applicable when sidebar is hidden", async function () { + SidebarView.hide(); + await awaitsFor(function () { return !SidebarView.isVisible(); }, "sidebar to hide", 2000); + + // When hidden, the Resizer moves the handle to be a sibling of #sidebar + // inside .main-view so the user can still grab it to re-expand. + const $resizer = _$(".main-view > .horz-resizer"); + expect($resizer.length).toBe(1); + const transform = testWindow.getComputedStyle($resizer[0]).transform; + expect(transform).toMatch(/matrix\(1,\s*0,\s*0,\s*1,\s*30,\s*0\)/); + + SidebarView.show(); + await awaitsFor(function () { return SidebarView.isVisible(); }, "sidebar to show again", 2000); + }); + }); + + describe("2. CCB buttons", function () { + + it("should fire EDIT_UNDO / EDIT_REDO / FILE_SAVE from undo, redo, save buttons", function () { + const executed = recordCommands(function () { + _$("#ccbUndoBtn").trigger("click"); + _$("#ccbRedoBtn").trigger("click"); + _$("#ccbSaveBtn").trigger("click"); + }); + + expect(executed).toContain(Commands.EDIT_UNDO); + expect(executed).toContain(Commands.EDIT_REDO); + expect(executed).toContain(Commands.FILE_SAVE); + }); + + it("should trigger CMD_FIND_IN_FILES when the search button is clicked", function () { + const executed = recordCommands(function () { + _$("#searchNav").trigger("click"); + }); + // searchNav routes through NavigationProvider._findInFiles which dispatches CMD_FIND_IN_FILES. + expect(executed).toContain(Commands.CMD_FIND_IN_FILES); + }); + + it("should execute VIEW_HIDE_SIDEBAR when #ccbSidebarToggleBtn is clicked, and the sidebar's visibility actually flips", async function () { + // Measure the effect: sidebar is visible → click → sidebar hides. + expect(SidebarView.isVisible()).toBe(true); + + const executed = recordCommands(function () { + _$("#ccbSidebarToggleBtn").trigger("click"); + }); + expect(executed).toContain(Commands.VIEW_HIDE_SIDEBAR); + + await awaitsFor(function () { return !SidebarView.isVisible(); }, + "sidebar to hide after toggle click", 2000); + + // And clicking again brings it back. + const executed2 = recordCommands(function () { + _$("#ccbSidebarToggleBtn").trigger("click"); + }); + expect(executed2).toContain(Commands.VIEW_HIDE_SIDEBAR); + await awaitsFor(function () { return SidebarView.isVisible(); }, + "sidebar to show after second toggle click", 2000); + }); + + it("should flip the toggle icon between fa-angles-left and fa-angles-right on sidebar hide/show", async function () { + const $icon = _$("#ccbSidebarToggleBtn > i"); + + // Starts visible, so the icon should be "collapse left". + expect($icon.hasClass("fa-angles-left")).toBe(true); + expect($icon.hasClass("fa-angles-right")).toBe(false); + + SidebarView.hide(); + await awaitsFor(function () { + return _$("#ccbSidebarToggleBtn > i").hasClass("fa-angles-right"); + }, "toggle icon to flip to angles-right", 2000); + expect(_$("#ccbSidebarToggleBtn > i").hasClass("fa-angles-left")).toBe(false); + + SidebarView.show(); + await awaitsFor(function () { + return _$("#ccbSidebarToggleBtn > i").hasClass("fa-angles-left"); + }, "toggle icon to flip back to angles-left", 2000); + expect(_$("#ccbSidebarToggleBtn > i").hasClass("fa-angles-right")).toBe(false); + }); + + it("should have no #sidebar-toggle-btn in the DOM (legacy menubar button removed)", function () { + expect(_$("#sidebar-toggle-btn").length).toBe(0); + }); + }); + + describe("3. #show-in-file-tree button in sidebar", function () { + + it("should render #show-in-file-tree inside #project-files-header, before #collapse-folders, with the localized title", function () { + const $btn = _$("#show-in-file-tree"); + expect($btn.length).toBe(1); + expect($btn.parent().attr("id")).toBe("project-files-header"); + // #collapse-folders sits after #show-in-file-tree in DOM order. + expect($btn.nextAll("#collapse-folders").length).toBe(1); + expect($btn.attr("title")).toBe(Strings.CMD_SHOW_IN_TREE); + }); + + it("should execute NAVIGATE_SHOW_IN_FILE_TREE when #show-in-file-tree is clicked", function () { + const executed = recordCommands(function () { + _$("#show-in-file-tree").trigger("click"); + }); + expect(executed).toContain(Commands.NAVIGATE_SHOW_IN_FILE_TREE); + }); + }); + }); +}); From 676b9f310a0c3b67bebdf3374d7e3ab1ed7ba388 Mon Sep 17 00:00:00 2001 From: abose Date: Tue, 21 Apr 2026 11:42:36 +0530 Subject: [PATCH 02/10] feat: allow sidebar to fully collapse via drag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change #sidebar's data-minsize from 30 to 0 so the Resizer's collapsible drag path (newSize < 10 → hide) fires when the user drags the right-edge handle to the left edge. The CCB sidebar-toggle button remains as the way to bring it back, so nothing is unreachable. - Add shared test/spec/DragTestUtils.js for programmatic drag input across the harness's browser engines, and use it to replace the data-minsize attribute assertion with a real drag that verifies the full-collapse behavior and that the CCB toggle is still visible. --- src/index.html | 2 +- test/control-bar-tests-todo.md | 24 ----- test/spec/CentralControlBar-integ-test.js | 28 +++++- test/spec/DragTestUtils.js | 110 ++++++++++++++++++++++ 4 files changed, 137 insertions(+), 27 deletions(-) create mode 100644 test/spec/DragTestUtils.js diff --git a/src/index.html b/src/index.html index c790b320ed..0f543fc9c4 100644 --- a/src/index.html +++ b/src/index.html @@ -905,7 +905,7 @@
-