diff --git a/src/vs/sessions/LAYOUT_CONTROLLER.md b/src/vs/sessions/LAYOUT_CONTROLLER.md index 499eaa83c0e17f..bb1af231401a4d 100644 --- a/src/vs/sessions/LAYOUT_CONTROLLER.md +++ b/src/vs/sessions/LAYOUT_CONTROLLER.md @@ -55,7 +55,7 @@ This guarantees that after collapsing back to a single session the **default vis Skipped entirely on mobile web (`isWeb && isMobile`) to avoid disruptive auto-expand on narrow viewports. -> **Docked detail panel (experimental).** With `sessions.layout.singlePaneDetailPanel` enabled, the auxiliary bar is docked inside the editor part rather than being a grid column (see [Editor presentation](LAYOUT.md#editor-presentation)). `SinglePaneExistingSessionStrategy` persists one shared Existing Session Editor/Details profile (via `SinglePaneVisibilityProfileStore`) under `sessions.singlePane.sidePaneVisibility`. New Sessions do not apply or capture an Editor profile; submitting preserves Editor visibility and seeds the Existing profile. `SinglePaneQuickChatStrategy` shares the Existing profile's overall side-pane visibility when Quick Chat has a saved editor working set, mapping any visible composition to Editor-only because Quick Chat has no Details. Opening the first editor or changing visibility in an editor-bearing Quick Chat updates that shared profile, even before the chat has a saved working set. A Quick Chat without editors hides the side pane transiently without changing the profile, so navigating away restores the shared visibility. The per-session rules below apply to the classic layout only. The docked detail panel opens at a 300px preferred width unless the user explicitly resized it; cached editor node sizes and temporary sidebar-collapse growth are not allowed to widen the first/opened detail-only pane. Docked sash collapse is also expressed through the same visibility API: the left grid sash hides editor content when the editor node reaches the detail width, and the middle docked sash hides the auxiliary bar when the raw dragged detail width reaches ~0. Single-pane also keeps new-session views Files-first without owning side-pane visibility: when an uncreated workspace session is entered and its restored editor set contains only Empty Files, `SinglePaneNewSessionStrategy` hides Editor once under editor-auto-visibility suppression. Auxiliary Bar visibility is unchanged. A completed Toggle Side Panel reopen is a separate transition: after managed tabs settle, a sole Empty Files input produces dock-only Files. Closing the last non-Empty input is a third, authoritative transition that restores Empty Files and the exact pre-close visibility. New, Existing, and Quick Chat share one `SinglePaneDetailPanelCoordinator` for Changes/Files content selection and context publication. Auxiliary Bar visibility is not shared: each lifecycle strategy applies its own visibility rules before publishing its content target. +> **Docked detail panel (experimental).** With `sessions.layout.singlePaneDetailPanel` enabled, the auxiliary bar is docked inside the editor part rather than being a grid column (see [Editor presentation](LAYOUT.md#editor-presentation)). `SinglePaneExistingSessionStrategy` persists one shared Existing Session Editor/Details profile (via `SinglePaneVisibilityProfileStore`) under `sessions.singlePane.sidePaneVisibility`. New Sessions do not apply or capture an Editor profile; submitting preserves Editor visibility and seeds the Existing profile. `SinglePaneQuickChatStrategy` shares the Existing profile's overall side-pane visibility when Quick Chat has a saved editor working set, mapping any visible composition to Editor-only because Quick Chat has no Details. Opening the first editor or changing visibility in an editor-bearing Quick Chat updates that shared profile, even before the chat has a saved working set. A Quick Chat without editors hides the side pane transiently without changing the profile, so navigating away restores the shared visibility. The per-session rules below apply to the classic layout only. The docked detail panel opens at a 300px preferred width unless the user explicitly resized it; cached editor node sizes and temporary sidebar-collapse growth are not allowed to widen the first/opened detail-only pane. Docked sash collapse is also expressed through the same visibility API: the left grid sash hides editor content when the editor node reaches the detail width, and the middle docked sash hides the auxiliary bar when the raw dragged detail width reaches ~0. Single-pane also keeps new-session views Files-first without owning side-pane visibility: when an uncreated workspace session is entered and its restored editor set contains only Empty Files, `SinglePaneNewSessionStrategy` reveals Files Details before hiding Editor once under editor-auto-visibility suppression. Both the New and Existing strategies reveal Files Details when Empty Files is opened or restored in a visible editor area, while later user visibility changes remain authoritative until the editor is opened again. A completed Toggle Side Panel reopen is a separate transition: after managed tabs settle, a sole Empty Files input produces dock-only Files. Closing the last input closes the whole side pane without replacing a non-Empty input with Empty Files. New, Existing, and Quick Chat share one `SinglePaneDetailPanelCoordinator` for Changes/Files content selection and context publication. Auxiliary Bar visibility is not shared: each lifecycle strategy applies its own visibility rules before publishing its content target. ### 3.1 Switching away — capture diff --git a/src/vs/sessions/SINGLE_PANE_SCENARIOS.md b/src/vs/sessions/SINGLE_PANE_SCENARIOS.md index c84a6e9ec1e112..84a85b4c987567 100644 --- a/src/vs/sessions/SINGLE_PANE_SCENARIOS.md +++ b/src/vs/sessions/SINGLE_PANE_SCENARIOS.md @@ -21,7 +21,7 @@ The third pane is a single visual card containing three regions: | **Editor content** | The editor pane below the tab bar (multi-diff Changes, a file, a browser) | Editor part, inset on the right by the detail width | | **Detail panel** | The docked auxiliary bar on the right (Branch Changes + Checks, or Explorer) | `DockedAuxiliaryBarController` (docks the aux bar inside the editor part) | -**Invariant:** the **tab bar is always visible** whenever the pane is shown — including when the editor content is hidden and in the new-session view. It is kept laid out by `MainEditorPart.layout`'s `keepForDockedTabBar` path (single-pane + detail visible), even while the editor part is logically hidden. +**Invariants:** the **tab bar is always visible** whenever the pane is shown — including when the editor content is hidden and in the new-session view. It is kept laid out by `MainEditorPart.layout`'s `keepForDockedTabBar` path (single-pane + detail visible), even while the editor part is logically hidden. Opening or restoring Empty Files in the visible side pane reveals the Files detail. The user may hide that detail afterward; it is revealed again the next time Empty Files is opened. --- @@ -40,9 +40,9 @@ Only **Existing Sessions** share a persisted Editor/Details visibility profile. **Size distribution when opening the side pane.** Opening the side pane from *closed* (e.g. clicking **Changes** while the chat is full-width) reveals the editor with `Sizing.Distribute`. The grid uses the revealed view's location to distribute its containing split. The Sessions part and side pane therefore receive equal space without either part computing a width. After that, side-pane sizes are **workbench-level, not per session**: the editor grid node width is owned by the workbench grid and persisted globally (`workbench.sessions.partSizes`), so once the user resizes the side pane it keeps that width — including across **session switches** (switching sessions does not change the side-pane width) and across reloads. -**Size distribution when toggling Details.** While Editor is visible, opening Details grows the editor grid node by the current Details width, taking that space from Sessions/chat. Hiding Details shrinks the node by the rendered Details width and returns that space to Sessions/chat. Grid minimum widths still take precedence when Sessions cannot yield the full width. +**Size distribution when toggling Details.** While Editor is visible, opening and hiding Details leave the editor grid node and Sessions/chat width unchanged. Details opens inside the existing side pane and takes its width from Editor content; hiding Details returns that width to Editor content. -**Reload is flicker-free (workbench owns the geometry).** On reload the workbench restores the editor node width from its own persisted part-sizes (`workbench.sessions.partSizes`, consumed by `createDesktopGridDescriptor`), so the grid is painted at the correct size in a single pass. (At the workbench level, hiding the editor still collapses the grid node to the detail width and caches it, and a captured editor-hide width `_dockedEditorSizeBeforeHide` takes precedence for the immediate re-show only when Details remains visible; Editor-only restoration uses the persisted pure Editor-content width.) **Reopening after the sessions list is collapsed.** Closing the **whole** side pane collapses the editor grid node to `0px`, but its Editor-before-Details close order first captures the current combined width while the node is still visible. Reopening restores that composition without treating `0px` as a user width. If a New Session then settles to Files-only, hiding Editor while Details remains visible shrinks the node to the Details width and captures the combined width for the next file open. Returning instead to an Existing Session's Editor-only profile restores its pure Editor width, so repeated session switches do not add the hidden Details width. +**Reload is flicker-free (workbench owns the geometry).** On reload the workbench restores the side-pane editor node width from its own persisted part-sizes (`workbench.sessions.partSizes`, consumed by `createDesktopGridDescriptor`), so the grid is painted at the correct size in a single pass. (At the workbench level, hiding the editor still collapses the grid node to the detail width and caches it, and a captured editor-hide width `_dockedEditorSizeBeforeHide` takes precedence for the immediate re-show regardless of the current Details visibility.) **Reopening after the sessions list is collapsed.** Closing the **whole** side pane collapses the editor grid node to `0px`, but its Editor-before-Details close order first captures the current side-pane width while the node is still visible. Reopening restores that composition without treating `0px` as a user width. If a New Session then settles to Files-only, hiding Editor while Details remains visible shrinks the node to the Details width and captures the side-pane width for the next file open. Returning instead to an Existing Session's Editor-only profile restores that same width, so repeated session switches do not change the Sessions/chat boundary. --- @@ -58,16 +58,16 @@ Only **Existing Sessions** share a persisted Editor/Details visibility profile. | **`+` Add Tab** | End of the tab strip | Opens the Add Tab menu (Browser `⇧⌘K B`; Search `⌘K S` for workspace-backed sessions; a **Changes** entry when the Changes editor tab is absent, and a **Files** entry `⌘K B` when the Files tab is absent — both for any workspace session). Restored managed Changes/Files tabs are inserted at the **end** of the tab strip. Search opens a new Search editor and is unavailable for Quick Chats. **Hidden when the editor area is closed.** | | **Toggle Side Panel** | Command / keybinding | Closes/opens the **whole** side pane (editor + detail together) → chat-only and back. The mechanics live on the workbench layout service (`toggleSidePane`); while the editor area is maximized, the shared `Workbench.toggleSidePane()` remembers maximization, un-maximizes, then performs the collapse so the restored detail is also hidden. Reopening restores the complete side-pane composition before re-maximizing the editor. Hiding a focused side pane moves focus to the sessions list. | | **Toggle Sessions List** | Title bar / command | Collapses/opens the left sessions list. Collapsing it gives the freed width to the editor/detail side pane (not the chat); reopening restores the previous editor/detail width so the chat gets that space back. No single-pane editor or detail action changes this visibility. | -| **Grid sash** | Between the chat and the third pane | Dragging the side pane never changes Details visibility. Details keeps its minimum width while editor content yields and eventually collapses under width pressure. Dragging a detail-only side pane wider keeps the editor content closed. Double-clicking with Details visible preserves the current Details width and splits all remaining width equally between chat and editor content, with no 600px cap. Grid minimum widths take precedence in narrow layouts. Hiding Details after a reset restores an equal chat/Editor split even when the reset itself did not visibly move the sash. With Details hidden it uses the native equal split, and in detail-only mode it resets Details to 300px. | +| **Grid sash** | Between the chat and the third pane | Dragging the side pane never changes Details visibility. Details keeps its minimum width while editor content yields and eventually collapses under width pressure. Dragging a detail-only side pane wider keeps the editor content closed. Double-clicking with Details visible preserves the current Details width and splits all remaining width equally between chat and editor content, with no 600px cap. Grid minimum widths take precedence in narrow layouts. Hiding Details after a reset leaves the chat/side-pane boundary unchanged and returns the Details width to Editor content. With Details hidden the sash uses the native equal split, and in detail-only mode it resets Details to 300px. | | **Changes pill** | Session header meta row | Opens the managed Changes multi-diff editor and explicitly reveals the editor area when the side pane was closed or in detail-only mode. The managed Changes tab still remains excluded from automatic reveal-on-open, so merely activating its tab does not reveal the editor. | -**Editor action visibility.** Maximize/Restore, Toggle Details, and Open in Modal are hidden while the **editor area is closed** (`MainEditorAreaVisibleContext`). Hide Editor and Show Editor are the mutually-exclusive pair that controls that very state: both render in the tab strip's editor-title layout cluster (`MenuId.EditorTitleLayout`), immediately after Maximize/Restore, gated only on `MainEditorAreaVisibleContext` being true/false respectively — unlike Toggle Details, they always show and are always enabled regardless of whether the active tab has a docked detail panel or the detail panel is currently visible (no `HasDockedDetailsContext` gate and no `AuxiliaryBarVisibleContext` precondition), consistent with Maximize/Restore's own always-shown behavior in that same cluster. Hide Editor unconditionally reveals the auxiliary bar as part of its `run()`, so it always has somewhere to fall back to even if the detail panel was hidden beforehand — the New/Existing Session strategy's detail-panel mapping (via the shared `SinglePaneDetailPanelCoordinator`) decides what that panel actually shows (the active tab's own detail, or the Changes/Files fallback for a Browser tab with none of its own; see §5). Show Editor reveals the editor via the same explicit-reveal API (`revealEditorPartExplicitly()`) used by the session-header Changes pill, then focuses the editor group. Toggle Details remains alone in its own trailing editor-header cluster and keeps its **has a docked detail panel** (`HasDockedDetailsContext`) gating — a managed Changes/Files tab or a text file editor — since toggling a nonexistent detail panel is never meaningful. +**Editor action visibility.** Maximize/Restore, Toggle Details, and Open in Modal are hidden while the **editor area is closed** (`MainEditorAreaVisibleContext`). Toggle Details is ordered immediately before Maximize/Restore. Hide Editor and Show Editor are the mutually-exclusive pair that controls the editor-area state: both render in the tab strip's editor-title layout cluster (`MenuId.EditorTitleLayout`), immediately after Maximize/Restore, gated only on `MainEditorAreaVisibleContext` being true/false respectively — unlike Toggle Details, they always show and are always enabled regardless of whether the active tab has a docked detail panel or the detail panel is currently visible (no `HasDockedDetailsContext` gate and no `AuxiliaryBarVisibleContext` precondition), consistent with Maximize/Restore's own always-shown behavior in that same cluster. Hide Editor unconditionally reveals the auxiliary bar as part of its `run()`, so it always has somewhere to fall back to even if the detail panel was hidden beforehand — the New/Existing Session strategy's detail-panel mapping (via the shared `SinglePaneDetailPanelCoordinator`) decides what that panel actually shows (the active tab's own detail, or the Changes/Files fallback for a Browser tab with none of its own; see §5). Show Editor reveals the editor via the same explicit-reveal API (`revealEditorPartExplicitly()`) used by the session-header Changes pill, then focuses the editor group. Toggle Details keeps its **has a docked detail panel** (`HasDockedDetailsContext`) gating — a managed Changes/Files tab or a text file editor — since toggling a nonexistent detail panel is never meaningful. -**Managed Files tab.** The empty Files placeholder tab (and the Changes tab) is opened when the editor group is **empty** on a view-open trigger (a session switch or a side-pane reveal), and both remain present whenever the layout is **Detail only**. The agent-feedback navigation overlay is hidden while the empty Files placeholder is active. Opening a real workspace file **tidies away** the empty placeholder (a `[Changes][file]` strip) as a **one-shot reaction to that open** — not a standing rule — so the user can still add the Files tab via **`+` Files** while a real file is open (that opens an `EmptyFileEditorInput`, not a real file, so it is not tidied away). Existing Sessions do not re-add the placeholder when that file closes while Editor is visible; a New Session instead uses its close fallback to replace the last non-Empty input with Empty Files while preserving Editor/Detail visibility. +**Managed Files tab.** The empty Files placeholder tab (and the Changes tab) is opened when the editor group is **empty** on a view-open trigger (a session switch or a side-pane reveal), and both remain present whenever the layout is **Detail only**. The agent-feedback navigation overlay is hidden while the empty Files placeholder is active. Opening a real workspace file **tidies away** the empty placeholder (a `[Changes][file]` strip) as a **one-shot reaction to that open** — not a standing rule — so the user can still add the Files tab via **`+` Files** while a real file is open (that opens an `EmptyFileEditorInput`, not a real file, so it is not tidied away). Neither Existing nor New Sessions re-add the placeholder when a real file closes. They close the whole side pane only when that close leaves every main editor group empty. -**New-session transitions have separate owners.** Entry owns only the one-shot redundant-Editor hide after session restoration. A completed closed-to-open **Toggle Side Panel** transition owns only the dock-only Files conversion after managed tabs settle. Last-editor close listens to the editor service's did-close event and uses the shared all-main-groups-empty predicate; it ignores programmatic closes while editor-part auto-visibility is suppressed, then installs Empty Files in the exact closing group, preserves Editor visibility, and opens Files Details. Generic side-pane reveal notifications never start the toggle rule, so editor opens and close-fallback restoration cannot feed back into it. +**New-session transitions have separate owners.** Entry owns only the one-shot redundant-Editor hide after session restoration. A completed closed-to-open **Toggle Side Panel** transition owns only the dock-only Files conversion after managed tabs settle. Last-editor close listens to the editor service's did-close event and uses the shared all-main-groups-empty predicate; it ignores programmatic closes while editor-part auto-visibility is suppressed, then closes the whole side pane. Generic side-pane reveal notifications never start the toggle rule, so editor opens cannot feed back into it. -**Empty editor groups are lifecycle-owned.** `SinglePaneWorkbench` does not change visibility when all editors close. New Session replaces a last non-Empty editor with Empty Files, but closing Empty Files itself closes the whole side pane; Existing Session closes the whole side pane when its last editor closes; Quick Chat leaves the side pane open. +**Empty editor groups are lifecycle-owned.** `SinglePaneWorkbench` does not change visibility when all editors close. New and Existing Sessions close the whole side pane when the last editor closes; Quick Chat leaves the side pane open. **Layout-driven vs user editor changes.** The default docked tabs are (re)opened into an empty group on a **settled** session-switch restore — the base controller fires `onDidEndSessionLayoutRestore` once the restore epoch (working-set apply + aux restore) completes, and the strategy reconciles off that. This matters for a new session: its **empty** working set closes the previous session's docked tabs, emptying the group *after* the switch; reconciling on the settled restore-end reads the reliably-empty group and re-opens both managed tabs. Reacting to the transient editor-change *during* the async apply would race the empty state. A **user-driven** editor change (opening a file, closing a tab) does not re-open defaults while Editor is visible; in Detail only, standard close actions cannot remove the managed inputs and every reconcile restores either input removed by lifecycle work. diff --git a/src/vs/sessions/browser/singlePaneWorkbench.ts b/src/vs/sessions/browser/singlePaneWorkbench.ts index 27a79d4c476b60..8047c56f78a682 100644 --- a/src/vs/sessions/browser/singlePaneWorkbench.ts +++ b/src/vs/sessions/browser/singlePaneWorkbench.ts @@ -36,7 +36,6 @@ export class SinglePaneWorkbench extends Workbench { private _dockedAuxiliaryBarWidth = DockedAuxiliaryBarController.DEFAULT_WIDTH; private _syncingEditorVisibility = false; - private _restoreEqualSplitOnDetailsHide = false; private readonly _memento = new DockedEditorSizeMemento(); override get isSinglePaneLayoutEnabled(): boolean { @@ -110,7 +109,6 @@ export class SinglePaneWorkbench extends Workbench { const sessionsWidth = this.workbenchGrid.getViewSize(this.sessionsPartView).width; const editorNodeWidth = this.workbenchGrid.getViewSize(this.editorPartView).width; const totalWidth = sessionsWidth + editorNodeWidth; - this._restoreEqualSplitOnDetailsHide = true; return Math.round(this._dockedAuxiliaryBarWidth + (totalWidth - this._dockedAuxiliaryBarWidth) / 2); } @@ -309,10 +307,6 @@ export class SinglePaneWorkbench extends Workbench { } protected override _applyEditorVisibility(hidden: boolean): void { - if (hidden) { - this._restoreEqualSplitOnDetailsHide = false; - } - // Part sizes are workbench-global, so hiding the side pane must not discard the // user's chosen editor width. Capture the current editor content width before the // grid collapses the node, so revealing later — e.g. switching back from a session @@ -330,7 +324,7 @@ export class SinglePaneWorkbench extends Workbench { const dockedEditorSizeBeforeHide = this._memento.dockedEditorSizeBeforeHide; const savedEditorWidth = this._savedPartSizes.editor; const canRestoreSavedWidth = savedEditorWidth !== undefined && savedEditorWidth >= EDITOR_PART_MINIMUM_WIDTH; - const shouldRestoreDockedEditorSize = !hidden && this.partVisibility.auxiliaryBar && !!dockedEditorSizeBeforeHide; + const shouldRestoreDockedEditorSize = !hidden && !!dockedEditorSizeBeforeHide; const shouldRestoreSavedWidth = !hidden && !shouldRestoreDockedEditorSize && canRestoreSavedWidth; const shouldApplyEvenSplit = !hidden && !shouldRestoreDockedEditorSize && !shouldRestoreSavedWidth; @@ -407,34 +401,6 @@ export class SinglePaneWorkbench extends Workbench { this.editorPartView, this._editorNodeShouldBeVisible() ); - if (hidden && !source && this._effectiveVisible(Parts.EDITOR_PART)) { - const editorNodeSize = this.workbenchGrid.getViewSize(this.editorPartView); - const targetWidth = this._restoreEqualSplitOnDetailsHide - ? Math.round((this.workbenchGrid.getViewSize(this.sessionsPartView).width + editorNodeSize.width) / 2) - : editorNodeSize.width - DockedAuxiliaryBarController.getEffectiveWidth(this._dockedAuxiliaryBarWidth, editorNodeSize.width); - this._restoreEqualSplitOnDetailsHide = false; - this._runWithEditorResizeSyncSuspended(() => { - this.workbenchGrid.resizeView(this.editorPartView, { - width: Math.max(this.editorPartView.minimumWidth, targetWidth), - height: editorNodeSize.height - }); - }); - } else if (!hidden && !source && this._effectiveVisible(Parts.EDITOR_PART)) { - const editorNodeSize = this.workbenchGrid.getViewSize(this.editorPartView); - const savedEditorWidth = this._savedPartSizes.editor; - const canRestoreSavedWidth = this._isEditorPartAutoVisibilitySuppressed - && savedEditorWidth !== undefined - && savedEditorWidth >= EDITOR_PART_MINIMUM_WIDTH; - const targetWidth = canRestoreSavedWidth - ? savedEditorWidth + this._dockedAuxiliaryBarWidth - : editorNodeSize.width + this._dockedAuxiliaryBarWidth; - this._runWithEditorResizeSyncSuspended(() => { - this.workbenchGrid.resizeView(this.editorPartView, { - width: targetWidth, - height: editorNodeSize.height - }); - }); - } if (!hidden && !this.partVisibility.editor) { this._syncingEditorVisibility = true; try { diff --git a/src/vs/sessions/contrib/editor/browser/addTabActions.ts b/src/vs/sessions/contrib/editor/browser/addTabActions.ts index e47880c69f9cba..b0462f1d840f54 100644 --- a/src/vs/sessions/contrib/editor/browser/addTabActions.ts +++ b/src/vs/sessions/contrib/editor/browser/addTabActions.ts @@ -17,7 +17,7 @@ import { openNewSearchEditor } from '../../../../workbench/contrib/searchEditor/ import { IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; import { EditorTabsVisibleContext, IsAuxiliaryWindowContext, IsSessionsWindowContext, IsTopRightEditorGroupContext } from '../../../../workbench/common/contextkeys.js'; -import { IsQuickChatSessionContext, SessionIsCreatedContext, SinglePaneChangesTabAvailableContext, SinglePaneChangesTabMissingContext, SinglePaneFilesTabAvailableContext, SinglePaneFilesTabMissingContext } from '../../../common/contextkeys.js'; +import { IsQuickChatSessionContext, SinglePaneChangesTabAvailableContext, SinglePaneChangesTabMissingContext, SinglePaneFilesTabAvailableContext, SinglePaneFilesTabMissingContext } from '../../../common/contextkeys.js'; import { SessionsCategories } from '../../../common/categories.js'; import { NEW_FILE_TAB_COMMAND_ID } from '../../../common/sessionCommands.js'; import { ISessionChangesService } from '../../changes/browser/sessionChangesService.js'; @@ -43,7 +43,6 @@ const singleEditorTitleWhen = EditorTabsVisibleContext.negate(); const changesTabActionWhen = ContextKeyExpr.and( addTabActionWhen, - SessionIsCreatedContext, SinglePaneChangesTabAvailableContext); const filesTabActionWhen = ContextKeyExpr.and( @@ -192,7 +191,7 @@ export class NewChangesTabAction extends Action2 { const sessionChangesService = accessor.get(ISessionChangesService); const session = sessionsService.activeSession.get(); - if (session?.isCreated.get()) { + if (session) { const group = editorGroupsService.mainPart.activeGroup; await sessionChangesService.openChangesEditor(session.resource, { index: group.count }, group); } diff --git a/src/vs/sessions/contrib/editor/test/browser/editor.contribution.test.ts b/src/vs/sessions/contrib/editor/test/browser/editor.contribution.test.ts index e4e7349d9c6d97..11c8f1ebe22a7e 100644 --- a/src/vs/sessions/contrib/editor/test/browser/editor.contribution.test.ts +++ b/src/vs/sessions/contrib/editor/test/browser/editor.contribution.test.ts @@ -38,7 +38,7 @@ import { NewChangesTabAction, NewFileTabAction, NewSearchTabAction } from '../.. import { EmptyFileEditorInput, EmptyFileEditorSerializer } from '../../browser/emptyFileEditorInput.js'; import { EditorTabsVisibleContext, IsAuxiliaryWindowContext, IsSessionsWindowContext, IsTopRightEditorGroupContext } from '../../../../../workbench/common/contextkeys.js'; import { TestEnvironmentService } from '../../../../../workbench/test/browser/workbenchTestServices.js'; -import { IsQuickChatSessionContext, SessionIsCreatedContext, SinglePaneChangesTabAvailableContext, SinglePaneChangesTabMissingContext, SinglePaneFilesTabAvailableContext, SinglePaneFilesTabMissingContext } from '../../../../common/contextkeys.js'; +import { IsQuickChatSessionContext, SinglePaneChangesTabAvailableContext, SinglePaneChangesTabMissingContext, SinglePaneFilesTabAvailableContext, SinglePaneFilesTabMissingContext } from '../../../../common/contextkeys.js'; // Import editor contribution to trigger action registration. import '../../browser/editor.contribution.js'; @@ -137,7 +137,6 @@ suite('Sessions - Editor Contribution', () => { [IsSessionsWindowContext.key]: true, [IsAuxiliaryWindowContext.key]: false, [IsTopRightEditorGroupContext.key]: true, - [SessionIsCreatedContext.key]: true, }; const scenarios = (availableKey: string, missingKey: string) => { const when = availableKey === SinglePaneFilesTabAvailableContext.key @@ -165,22 +164,30 @@ suite('Sessions - Editor Contribution', () => { }); }); - test('new changes tab action requires a created session with Changes available', () => { + test('new changes tab action is enabled for an uncreated workspace session with Changes available', () => { const action = new NewChangesTabAction(); const precondition = action.desc.precondition?.serialize() ?? ''; const keybinding = Array.isArray(action.desc.keybinding) ? action.desc.keybinding[0] : action.desc.keybinding; const when = keybinding?.when?.serialize() ?? ''; + const values: Record = { + [IsSessionsWindowContext.key]: true, + [IsAuxiliaryWindowContext.key]: false, + [SinglePaneChangesTabAvailableContext.key]: true, + }; + const context: IContext = { + getValue: (key: string) => values[key] as T | undefined + }; assert.deepStrictEqual({ - preconditionHasCreated: precondition.includes(SessionIsCreatedContext.key), preconditionHasAvailability: precondition.includes(SinglePaneChangesTabAvailableContext.key), - keybindingHasCreated: when.includes(SessionIsCreatedContext.key), keybindingHasAvailability: when.includes(SinglePaneChangesTabAvailableContext.key), + preconditionEnabled: action.desc.precondition?.evaluate(context), + keybindingEnabled: keybinding?.when?.evaluate(context), }, { - preconditionHasCreated: true, preconditionHasAvailability: true, - keybindingHasCreated: true, keybindingHasAvailability: true, + preconditionEnabled: true, + keybindingEnabled: true, }); }); @@ -341,23 +348,24 @@ suite('Sessions - Editor Contribution', () => { assert.deepStrictEqual(opened, [{ resource, index: 5 }]); }); - test('new changes tab action is a no-op for an uncreated session', async () => { + test('new changes tab action opens the changes editor for an uncreated session', async () => { const instantiationService = store.add(new TestInstantiationService()); - stubEditorGroupCount(instantiationService, 0); + const resource = URI.parse('session:new'); + stubEditorGroupCount(instantiationService, 2); instantiationService.stub(ISessionsService, new class extends mock() { - override readonly activeSession = constObservable({ resource: URI.parse('session:new'), isCreated: constObservable(false) } as IActiveSession); + override readonly activeSession = constObservable({ resource, isCreated: constObservable(false) } as IActiveSession); }); - let opened = false; + const opened: { resource: URI; index: number | undefined }[] = []; instantiationService.stub(ISessionChangesService, new class extends mock() { - override async openChangesEditor(): Promise { - opened = true; + override async openChangesEditor(sessionResource: URI, options?: IEditorOptions): Promise { + opened.push({ resource: sessionResource, index: options?.index }); return undefined; } }); await new NewChangesTabAction().run(instantiationService); - assert.strictEqual(opened, false); + assert.deepStrictEqual(opened, [{ resource, index: 2 }]); }); test('new changes tab action is a no-op when there is no active session', async () => { diff --git a/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneExistingSessionStrategy.ts b/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneExistingSessionStrategy.ts index ddabd6460666b3..98671811db9d49 100644 --- a/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneExistingSessionStrategy.ts +++ b/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneExistingSessionStrategy.ts @@ -25,6 +25,7 @@ import { HasDockedDetailsContext, SinglePaneLayoutEnabledContext } from '../../. import { ISessionsService } from '../../../../services/sessions/browser/sessionsService.js'; import { IActiveSession } from '../../../../services/sessions/common/sessionsManagement.js'; import { ISessionChangesService } from '../../../changes/browser/sessionChangesService.js'; +import { EmptyFileEditorInput } from '../../../editor/browser/emptyFileEditorInput.js'; import { DetailPanelTarget, SinglePaneDetailPanelCoordinator } from './singlePaneDetailPanelCoordinator.js'; import { SinglePaneDockedTabsCoordinator } from './singlePaneDockedTabsCoordinator.js'; import { isChangesEditorInput, isEditorWithoutDockedDetails, isFileEditorInput, isMainPartEmpty } from './singlePaneSharedHelpers.js'; @@ -33,7 +34,7 @@ import { SessionVisibilityProfile, SinglePaneVisibilityProfileStore } from './si /** Command that toggles the single-pane detail panel (auxiliary bar) from the editor header. */ export const TOGGLE_DETAILS_COMMAND_ID = 'workbench.action.agentSessions.toggleDetails'; -const singlePaneHeaderToggleDetailsOrder = 10; +const singlePaneHeaderToggleDetailsOrder = 9; /** * Behaviour for the **Existing Session** lifecycle stage — a created, workspace-backed @@ -269,6 +270,9 @@ export class SinglePaneExistingSessionStrategy extends SinglePaneLayoutStrategy let activeSessionKey: string | undefined; let pendingSessionKey: string | undefined; let pendingOutgoingEditor: EditorInput | undefined; + let previousActiveEditor: EditorInput | undefined; + let previousEditorPartVisible = false; + let previousEditorSessionKey: string | undefined; const sync = (reader: IReader | undefined) => { const activeSession = this._sessionsService.activeSession.read(reader); @@ -277,6 +281,9 @@ export class SinglePaneExistingSessionStrategy extends SinglePaneLayoutStrategy || !activeSession.workspace.read(reader) || !activeSession.isCreated.read(reader)) { wasExistingActive = false; + previousActiveEditor = undefined; + previousEditorPartVisible = false; + previousEditorSessionKey = undefined; return; } @@ -304,9 +311,15 @@ export class SinglePaneExistingSessionStrategy extends SinglePaneLayoutStrategy return; } + const emptyFilesShown = activeEditor instanceof EmptyFileEditorInput + && editorPartVisible + && (activeEditor !== previousActiveEditor || !previousEditorPartVisible || sessionKey !== previousEditorSessionKey); + previousActiveEditor = activeEditor; + previousEditorPartVisible = editorPartVisible; + previousEditorSessionKey = sessionKey; const target = this._computeTarget(activeEditor, mainPartEmpty, editorMaximized, editorPartVisible); const revealOnly = this._ctx.multipleSessionsVisibleObs.read(reader); - this._syncDetailVisibility(target, revealOnly); + this._syncDetailVisibility(target, revealOnly, emptyFilesShown); this._detailPanel.sync(target); }; @@ -328,12 +341,18 @@ export class SinglePaneExistingSessionStrategy extends SinglePaneLayoutStrategy })); } - private _syncDetailVisibility(target: DetailPanelTarget, revealOnly: boolean): void { + private _syncDetailVisibility(target: DetailPanelTarget, revealOnly: boolean, emptyFilesShown: boolean): void { + const detailVisible = this._layoutService.isVisible(Parts.AUXILIARYBAR_PART); + if (emptyFilesShown && !detailVisible) { + this._detailHiddenTransiently = false; + this._detailHiddenByEditor = false; + this._setDetailHiddenTransiently(false); + return; + } if (this._ctx.isRestoringSessionLayout || target === DetailPanelTarget.Preserve) { return; } - const detailVisible = this._layoutService.isVisible(Parts.AUXILIARYBAR_PART); if (target === DetailPanelTarget.Hidden || target === DetailPanelTarget.EditorHidden) { if ((target === DetailPanelTarget.EditorHidden || !revealOnly) && detailVisible) { this._detailHiddenTransiently = true; diff --git a/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneNewSessionStrategy.ts b/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneNewSessionStrategy.ts index 2b17fdba22115d..6556fd9cd692e7 100644 --- a/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneNewSessionStrategy.ts +++ b/src/vs/sessions/contrib/layout/browser/singlePane/singlePaneNewSessionStrategy.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { mainWindow } from '../../../../../base/browser/window.js'; -import { onUnexpectedError } from '../../../../../base/common/errors.js'; import { Event } from '../../../../../base/common/event.js'; import { autorun, @@ -13,13 +12,8 @@ import { observableFromEvent, observableSignalFromEvent, } from '../../../../../base/common/observable.js'; -import { EditorActivation } from '../../../../../platform/editor/common/editor.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { EditorInput } from '../../../../../workbench/common/editor/editorInput.js'; -import { - IEditorGroup, - IEditorGroupsService, -} from '../../../../../workbench/services/editor/common/editorGroupsService.js'; +import { IEditorGroupsService } from '../../../../../workbench/services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../../workbench/services/editor/common/editorService.js'; import { Parts } from '../../../../../workbench/services/layout/browser/layoutService.js'; import { IAgentWorkbenchLayoutService } from '../../../../browser/workbench.js'; @@ -42,7 +36,7 @@ import { } from './singlePaneLayoutStrategy.js'; /** - * Owns the independent entry, side-pane-toggle, close-fallback, and detail transitions for New Sessions. + * Owns the independent entry, side-pane-toggle, last-editor-close, and detail transitions for New Sessions. */ export class SinglePaneNewSessionStrategy extends SinglePaneLayoutStrategy { private _pendingEntryHideSessionKey: string | undefined; @@ -61,14 +55,12 @@ export class SinglePaneNewSessionStrategy extends SinglePaneLayoutStrategy { private readonly _editorGroupsService: IEditorGroupsService, @ISessionChangesService private readonly _sessionChangesService: ISessionChangesService, - @IInstantiationService - private readonly _instantiationService: IInstantiationService, ) { super(ctx); this._registerEntryEditorHide(); this._registerSidePaneOpenEditorHide(); - this._registerEmptyFilesCloseFallback(); + this._registerLastEditorClose(); this._registerDetailPanel(); } @@ -114,6 +106,7 @@ export class SinglePaneNewSessionStrategy extends SinglePaneLayoutStrategy { if (!editors.every(editor => editor instanceof EmptyFileEditorInput || isChangesEditorInput(editor, this._sessionChangesService))) { return; } + const hasEmptyFiles = editors.some(editor => editor instanceof EmptyFileEditorInput); this._pendingEntryHideSessionKey = undefined; if (!this._layoutService.isVisible(Parts.EDITOR_PART, mainWindow)) { @@ -123,6 +116,9 @@ export class SinglePaneNewSessionStrategy extends SinglePaneLayoutStrategy { const suppression = this._layoutService.suppressEditorPartAutoVisibility(); try { + if (hasEmptyFiles && !this._layoutService.isVisible(Parts.AUXILIARYBAR_PART)) { + this._layoutService.setPartHidden(false, Parts.AUXILIARYBAR_PART); + } this._layoutService.setPartHidden(true, Parts.EDITOR_PART); } finally { suppression.dispose(); @@ -234,12 +230,11 @@ export class SinglePaneNewSessionStrategy extends SinglePaneLayoutStrategy { ); } - private _registerEmptyFilesCloseFallback(): void { + private _registerLastEditorClose(): void { this._register( - this._editorService.onDidCloseEditor((event) => { - const sessionKey = this._getActiveNewSessionKey(); + this._editorService.onDidCloseEditor(() => { if ( - !sessionKey || + !this._getActiveNewSessionKey() || this._ctx.multipleSessionsVisibleObs.get() || this._ctx.isRestoringSessionLayout || this._layoutService.isEditorPartAutoVisibilitySuppressed() || @@ -249,77 +244,11 @@ export class SinglePaneNewSessionStrategy extends SinglePaneLayoutStrategy { } this._pendingEntryHideSessionKey = undefined; this._pendingSidePaneOpenHideSessionKey = undefined; - if (event.editor instanceof EmptyFileEditorInput) { - this._hideSidePane(); - return; - } - const group = this._editorGroupsService.mainPart.getGroup( - event.groupId, - ); - if (!group) { - return; - } - const suppression = - this._layoutService.suppressEditorPartAutoVisibility(); - void this._openEmptyFiles( - group, - sessionKey, - this._layoutService.isVisible(Parts.EDITOR_PART, mainWindow), - ) - .finally(() => suppression.dispose()) - .catch(onUnexpectedError); + this._layoutService.hideSidePane(); }), ); } - private _hideSidePane(): void { - this._layoutService.hideSidePane(); - } - - private async _openEmptyFiles( - group: IEditorGroup, - sessionKey: string, - editorVisible: boolean, - ): Promise { - const session = this._sessionsService.activeSession.get(); - const workspace = session?.workspace.get(); - if ( - !session || - this._getActiveNewSessionKey() !== sessionKey || - !workspace || - !isMainPartEmpty(this._editorGroupsService) - ) { - return; - } - await this._editorService.openEditor( - this._instantiationService.createInstance( - EmptyFileEditorInput, - workspace, - ), - { - pinned: true, - inactive: true, - preserveFocus: true, - activation: EditorActivation.PRESERVE, - isExplicit: false, - }, - group, - ); - if (this._getActiveNewSessionKey() !== sessionKey) { - return; - } - if ( - this._layoutService.isVisible(Parts.EDITOR_PART, mainWindow) !== - editorVisible - ) { - this._layoutService.setPartHidden(!editorVisible, Parts.EDITOR_PART); - } - if (!this._layoutService.isVisible(Parts.AUXILIARYBAR_PART)) { - this._layoutService.setPartHidden(false, Parts.AUXILIARYBAR_PART); - } - this._detailPanel.sync(DetailPanelTarget.FilesForced); - } - private _getMainPartEditors(): EditorInput[] { return this._editorGroupsService.mainPart.groups.flatMap((group) => [ ...group.editors, @@ -372,28 +301,45 @@ export class SinglePaneNewSessionStrategy extends SinglePaneLayoutStrategy { this._layoutService.onDidChangeEditorMaximized, () => this._layoutService.isEditorMaximized(), ); + let previousActiveEditor: EditorInput | undefined; + let previousEditorPartVisible = false; + let previousEditorSessionKey: string | undefined; this._register( autorun((reader) => { const activeSession = this._sessionsService.activeSession.read(reader); if (!activeSession) { + previousActiveEditor = undefined; + previousEditorPartVisible = false; + previousEditorSessionKey = undefined; return; } const isQuickChat = activeSession.isQuickChat?.read(reader) ?? false; const workspace = activeSession.workspace.read(reader); if (isQuickChat || !workspace || activeSession.isCreated.read(reader)) { + previousActiveEditor = undefined; + previousEditorPartVisible = false; + previousEditorSessionKey = undefined; return; } const activeEditor = activeEditorObs.read(reader); + const editorPartVisible = editorPartVisibleObs.read(reader); + const sessionKey = activeSession.resource.toString(); + const emptyFilesShown = activeEditor instanceof EmptyFileEditorInput + && editorPartVisible + && (activeEditor !== previousActiveEditor || !previousEditorPartVisible || sessionKey !== previousEditorSessionKey); + previousActiveEditor = activeEditor; + previousEditorPartVisible = editorPartVisible; + previousEditorSessionKey = sessionKey; const target = this._computeTarget( reader, activeEditor, editorMaximizedObs, - editorPartVisibleObs, + editorPartVisible, ); const revealOnly = this._ctx.multipleSessionsVisibleObs.read(reader); - this._syncDetailVisibility(target, revealOnly); + this._syncDetailVisibility(target, revealOnly, emptyFilesShown); this._detailPanel.sync(target); }), ); @@ -413,7 +359,15 @@ export class SinglePaneNewSessionStrategy extends SinglePaneLayoutStrategy { private _syncDetailVisibility( target: DetailPanelTarget, revealOnly: boolean, + emptyFilesShown: boolean, ): void { + const detailVisible = this._layoutService.isVisible(Parts.AUXILIARYBAR_PART); + if (emptyFilesShown && !detailVisible) { + this._detailHiddenTransiently = false; + this._detailHiddenByEditor = false; + this._layoutService.setAuxiliaryBarHiddenForResize(false); + return; + } if ( this._ctx.isRestoringSessionLayout || target === DetailPanelTarget.Preserve @@ -421,9 +375,6 @@ export class SinglePaneNewSessionStrategy extends SinglePaneLayoutStrategy { return; } - const detailVisible = this._layoutService.isVisible( - Parts.AUXILIARYBAR_PART, - ); if ( target === DetailPanelTarget.Hidden || target === DetailPanelTarget.EditorHidden @@ -455,7 +406,7 @@ export class SinglePaneNewSessionStrategy extends SinglePaneLayoutStrategy { reader: IReader, activeEditor: EditorInput | undefined, editorMaximizedObs: IObservable, - editorPartVisibleObs: IObservable, + editorPartVisible: boolean, ): DetailPanelTarget { // A New Session's empty editor group is normal (the Files detail is owned by the // managed-tabs reconcile while its Files tab is (re)ensured), unlike an Existing @@ -463,7 +414,7 @@ export class SinglePaneNewSessionStrategy extends SinglePaneLayoutStrategy { // Existing, New never hides on an empty group. if (activeEditor && isEditorWithoutDockedDetails(activeEditor)) { - return editorPartVisibleObs.read(reader) ? DetailPanelTarget.EditorHidden : DetailPanelTarget.Files; + return editorPartVisible ? DetailPanelTarget.EditorHidden : DetailPanelTarget.Files; } if (editorMaximizedObs.read(reader)) { diff --git a/src/vs/sessions/contrib/layout/test/browser/desktopSessionLayoutController.test.ts b/src/vs/sessions/contrib/layout/test/browser/desktopSessionLayoutController.test.ts index 5f01ccb083e264..aa89b34957fdad 100644 --- a/src/vs/sessions/contrib/layout/test/browser/desktopSessionLayoutController.test.ts +++ b/src/vs/sessions/contrib/layout/test/browser/desktopSessionLayoutController.test.ts @@ -36,6 +36,7 @@ import '../../../changes/browser/changesActions.js'; import { SESSIONS_FILES_CONTAINER_ID } from '../../../files/browser/files.contribution.js'; import { NewChangesTabAction, NewFileTabAction } from '../../../editor/browser/addTabActions.js'; import { createTestHarness, ICreateOptions, ITestLayoutHarness, makeChange, makeSession, TestStubEditorInput } from './layoutControllerTestUtils.js'; +import '../../../editor/browser/editor.contribution.js'; suite('LayoutController (desktop)', () => { @@ -1472,7 +1473,7 @@ suite('LayoutController (desktop)', () => { assert.ok(!harness.openedViews.includes(CHANGES_VIEW_ID), 'untitled sessions are governed by D3b/D4, not D8'); }); - test('[single-pane] entering a new-session view hides only Editor when Empty Files is the only input', async () => { + test('[single-pane] entering a new-session view shows Files Details and hides Editor when Empty Files is the only input', async () => { createSinglePaneController({ activateAux: true }); await timeout(0); const existing = makeSession(URI.parse('session:existing')); @@ -1495,8 +1496,9 @@ suite('LayoutController (desktop)', () => { call.part === Parts.EDITOR_PART || call.part === Parts.AUXILIARYBAR_PART), }, { editorVisible: false, - detailVisible: false, + detailVisible: true, visibilityRestores: [ + { part: Parts.AUXILIARYBAR_PART, hidden: false }, { part: Parts.EDITOR_PART, hidden: true }, ], }); @@ -1551,7 +1553,7 @@ suite('LayoutController (desktop)', () => { }); }); - test('[single-pane] closing the last non-Empty editor while Editor is hidden opens Empty Files', async () => { + test('[single-pane] closing the last non-Empty editor while Editor is hidden closes the side pane', async () => { createSinglePaneController({ activateAux: true, singlePaneLayoutEnabled: true }); await settle(); harness.activeSessionObs.set(makeSession(URI.parse('session:new'), { status: SessionStatus.Untitled, isCreated: false }), undefined); @@ -1574,13 +1576,13 @@ suite('LayoutController (desktop)', () => { editorVisible: harness.partVisibility.get(Parts.EDITOR_PART), auxiliaryBarVisible: harness.partVisibility.get(Parts.AUXILIARYBAR_PART), }, { - hasFilesTab: true, + hasFilesTab: false, editorVisible: false, - auxiliaryBarVisible: true, + auxiliaryBarVisible: false, }); }); - test('[single-pane] closing the last visible file editor opens Empty Files and keeps Editor visible', async () => { + test('[single-pane] closing the last visible file editor closes the side pane without opening Empty Files', async () => { createSinglePaneController({ activateAux: true, singlePaneLayoutEnabled: true }); await settle(); harness.activeSessionObs.set(makeSession(URI.parse('session:new'), { status: SessionStatus.Untitled, isCreated: false }), undefined); @@ -1603,9 +1605,9 @@ suite('LayoutController (desktop)', () => { editorVisible: harness.partVisibility.get(Parts.EDITOR_PART), auxiliaryBarVisible: harness.partVisibility.get(Parts.AUXILIARYBAR_PART), }, { - hasFilesTab: true, - editorVisible: true, - auxiliaryBarVisible: true, + hasFilesTab: false, + editorVisible: false, + auxiliaryBarVisible: false, }); }); @@ -2480,26 +2482,32 @@ suite('LayoutController (desktop)', () => { }); }); - test('[D7 single-pane] contributes Toggle Details with the editor title layout actions', () => { + test('[D7 single-pane] contributes Toggle Details before Maximize with the editor title layout actions', () => { createSinglePaneController(); const items = MenuRegistry.getMenuItems(MenuId.EditorTitleLayout) .filter(isIMenuItem) .filter(item => item.command.id === TOGGLE_DETAILS_COMMAND_ID); + const maximizeItem = MenuRegistry.getMenuItems(MenuId.EditorTitleLayout) + .filter(isIMenuItem) + .find(item => item.command.id === 'workbench.action.agentSessions.maximizeMainEditorPart'); assert.strictEqual(items.length, 1, 'exactly one Toggle Details item on the editor header'); + assert.ok(maximizeItem, 'Maximize item should be registered'); const when = items[0].when?.serialize() ?? ''; assert.deepStrictEqual({ group: items[0].group, icon: ThemeIcon.isThemeIcon(items[0].command.icon) ? items[0].command.icon.id : undefined, order: items[0].order, + beforeMaximize: (items[0].order ?? 0) < (maximizeItem.order ?? 0), hasToggled: !!items[0].command.toggled, gatedOnEditorArea: when.includes(MainEditorAreaVisibleContext.key), gatedOnDockedDetails: when.includes(HasDockedDetailsContext.key), }, { group: 'navigation', icon: Codicon.listSelection.id, - order: 10, + order: 9, + beforeMaximize: true, hasToggled: true, gatedOnEditorArea: true, gatedOnDockedDetails: true, diff --git a/src/vs/sessions/contrib/layout/test/browser/singlePaneStrategies.test.ts b/src/vs/sessions/contrib/layout/test/browser/singlePaneStrategies.test.ts index b08404d952cab0..38b470bf5d7b82 100644 --- a/src/vs/sessions/contrib/layout/test/browser/singlePaneStrategies.test.ts +++ b/src/vs/sessions/contrib/layout/test/browser/singlePaneStrategies.test.ts @@ -121,21 +121,139 @@ suite('SinglePane layout strategies', () => { }); }); - test('New Session entry hides Editor when Empty Files is the only input', () => { + test('New Session entry shows Files Details before hiding Editor when Empty Files is the only input', () => { const ctx = setup(); const session = makeSession(URI.parse('session:/new'), { status: SessionStatus.Untitled, isCreated: false }); - harness.activeGroupEditors.push(store.add(harness.instaService.createInstance(EmptyFileEditorInput, session.workspace.get()))); + const emptyFiles = store.add(harness.instaService.createInstance(EmptyFileEditorInput, session.workspace.get())); + harness.activeGroupEditors.push(emptyFiles); + harness.activeEditorInput = emptyFiles; + harness.partVisibility.set(Parts.AUXILIARYBAR_PART, false); store.add(harness.instaService.createInstance(SinglePaneNewSessionStrategy, ctx, createDetailPanel())); harness.setPartHiddenCalls.length = 0; activate(session); - assert.deepStrictEqual(harness.setPartHiddenCalls.filter(call => call.part === Parts.EDITOR_PART), [ + assert.deepStrictEqual(harness.setPartHiddenCalls, [ + { hidden: false, part: Parts.AUXILIARYBAR_PART }, { hidden: true, part: Parts.EDITOR_PART }, ]); }); - test('New Session close fallback replaces the last file and opens Details', async () => { + test('New Session allows Details to stay hidden after Empty Files opens it', () => { + const ctx = setup(); + const session = makeSession(URI.parse('session:/new'), { status: SessionStatus.Untitled, isCreated: false }); + const emptyFiles = store.add(harness.instaService.createInstance(EmptyFileEditorInput, session.workspace.get())); + harness.activeGroupEditors.push(emptyFiles); + harness.activeEditorInput = emptyFiles; + store.add(harness.instaService.createInstance(SinglePaneNewSessionStrategy, ctx, createDetailPanel())); + activate(session); + harness.partVisibility.set(Parts.EDITOR_PART, true); + harness.onDidChangePartVisibility.fire({ partId: Parts.EDITOR_PART, visible: true }); + harness.setPartHiddenCalls.length = 0; + + harness.partVisibility.set(Parts.AUXILIARYBAR_PART, false); + harness.onDidChangePartVisibility.fire({ partId: Parts.AUXILIARYBAR_PART, visible: false }); + harness.editorMaximized = true; + harness.onDidChangeEditorMaximized.fire(); + + assert.deepStrictEqual({ + auxiliaryBarVisible: harness.partVisibility.get(Parts.AUXILIARYBAR_PART), + visibilityChanges: harness.setPartHiddenCalls, + }, { + auxiliaryBarVisible: false, + visibilityChanges: [], + }); + }); + + test('Existing Session restoration shows Details when Empty Files is active', () => { + const ctx = setup(); + const session = makeSession(URI.parse('session:/existing')); + const emptyFiles = store.add(harness.instaService.createInstance(EmptyFileEditorInput, session.workspace.get())); + const visibilityStore = createVisibilityStore(); + visibilityStore.set(SessionVisibilityProfile.Existing, { editorVisible: true, auxiliaryBarVisible: false }); + harness.activeGroupEditors.push(emptyFiles); + harness.activeEditorInput = emptyFiles; + harness.partVisibility.set(Parts.AUXILIARYBAR_PART, false); + store.add(harness.instaService.createInstance( + SinglePaneExistingSessionStrategy, + ctx, + visibilityStore, + createDetailPanel() + )); + harness.setPartHiddenCalls.length = 0; + + activate(session); + + assert.deepStrictEqual({ + editorVisible: harness.partVisibility.get(Parts.EDITOR_PART), + auxiliaryBarVisible: harness.partVisibility.get(Parts.AUXILIARYBAR_PART), + visibilityChanges: harness.setPartHiddenCalls, + }, { + editorVisible: true, + auxiliaryBarVisible: true, + visibilityChanges: [ + { hidden: false, part: Parts.AUXILIARYBAR_PART }, + ], + }); + }); + + test('Existing Session allows Details to hide until Empty Files is opened again', () => { + const ctx = setup(); + const session = makeSession(URI.parse('session:/existing')); + const otherEditor = store.add(new TestStubEditorInput(URI.parse('search-editor://other'))); + const emptyFiles = store.add(harness.instaService.createInstance(EmptyFileEditorInput, session.workspace.get())); + harness.activeGroupEditors.push(otherEditor, emptyFiles); + harness.activeEditorInput = emptyFiles; + const strategy = store.add(harness.instaService.createInstance( + SinglePaneExistingSessionStrategy, + ctx, + createVisibilityStore(), + createDetailPanel() + )); + activate(session); + harness.setPartHiddenCalls.length = 0; + + const nowVisible = strategy.toggleDetails(); + + assert.deepStrictEqual({ + nowVisible, + auxiliaryBarVisible: harness.partVisibility.get(Parts.AUXILIARYBAR_PART), + visibilityChanges: harness.setPartHiddenCalls, + }, { + nowVisible: false, + auxiliaryBarVisible: false, + visibilityChanges: [ + { hidden: true, part: Parts.AUXILIARYBAR_PART }, + ], + }); + + harness.editorMaximized = true; + harness.onDidChangeEditorMaximized.fire(); + + assert.deepStrictEqual({ + auxiliaryBarVisible: harness.partVisibility.get(Parts.AUXILIARYBAR_PART), + lastVisibilityChange: harness.setPartHiddenCalls.at(-1), + }, { + auxiliaryBarVisible: false, + lastVisibilityChange: { hidden: true, part: Parts.AUXILIARYBAR_PART }, + }); + + harness.editorMaximized = false; + harness.activeEditorInput = otherEditor; + harness.onDidActiveEditorChange.fire(); + harness.activeEditorInput = emptyFiles; + harness.onDidActiveEditorChange.fire(); + + assert.deepStrictEqual({ + auxiliaryBarVisible: harness.partVisibility.get(Parts.AUXILIARYBAR_PART), + lastVisibilityChange: harness.setPartHiddenCalls.at(-1), + }, { + auxiliaryBarVisible: true, + lastVisibilityChange: { hidden: false, part: Parts.AUXILIARYBAR_PART }, + }); + }); + + test('New Session closes the side pane instead of opening Empty Files when its last file closes', () => { const ctx = setup(); const session = makeSession(URI.parse('session:/new'), { status: SessionStatus.Untitled, isCreated: false }); const editor = store.add(new TestStubEditorInput(URI.file('/repo/file.ts'))); @@ -148,20 +266,16 @@ suite('SinglePane layout strategies', () => { harness.activeGroupEditors.length = 0; harness.editorGroupsHaveContent = false; harness.onDidCloseEditor.fire({ editor, groupId: 1 }); - const replacementDuringClose = harness.activeGroupEditors.find(input => input instanceof EmptyFileEditorInput); harness.onDidEditorsChange.fire(); - await Promise.resolve(); assert.deepStrictEqual({ - replacementPreservedAfterClose: replacementDuringClose === harness.activeGroupEditors[0], editorsAfterCloseCompleted: harness.activeGroupEditors.map(input => input.typeId), editorVisible: harness.partVisibility.get(Parts.EDITOR_PART), auxiliaryBarVisible: harness.partVisibility.get(Parts.AUXILIARYBAR_PART), }, { - replacementPreservedAfterClose: true, - editorsAfterCloseCompleted: [EmptyFileEditorInput.ID], - editorVisible: true, - auxiliaryBarVisible: true, + editorsAfterCloseCompleted: [], + editorVisible: false, + auxiliaryBarVisible: false, }); }); diff --git a/src/vs/sessions/test/browser/workbench.test.ts b/src/vs/sessions/test/browser/workbench.test.ts index ea049d2a10ca0b..defff36019518b 100644 --- a/src/vs/sessions/test/browser/workbench.test.ts +++ b/src/vs/sessions/test/browser/workbench.test.ts @@ -90,7 +90,6 @@ suite('Sessions - Workbench', () => { _restoreSidePaneEditorMaximizedOnShow: boolean; _hasAppliedInitialEditorSplit: boolean; _dockedAuxiliaryBarWidth: number; - _restoreEqualSplitOnDetailsHide: boolean; _memento: DockedEditorSizeMemento; readonly resizes: IViewSize[]; readonly distributions: object[]; @@ -307,7 +306,6 @@ suite('Sessions - Workbench', () => { viewDescriptorService: options.viewDescriptorService ?? { getDefaultViewContainer: () => undefined }, // docked bookkeeping _dockedAuxiliaryBarWidth: options.dockedWidth ?? DockedAuxiliaryBarController.DEFAULT_WIDTH, - _restoreEqualSplitOnDetailsHide: false, _syncingEditorVisibility: false, _memento: new DockedEditorSizeMemento(), // stubs for the heavy base helpers the hooks call @@ -1009,12 +1007,9 @@ suite('Sessions - Workbench', () => { }); test('persisted editor width excludes the detail only when the detail is visible', () => { - // Editor + detail visible: the node includes the detail, so it is excluded - // to store the pure editor-content width (reconstructed by adding it back). + // The persisted editor width represents editor content; the descriptor adds + // Details back when reconstructing the shared side-pane node. const withDetail = createHost({ single: true, dockedWidth: 300, partVisibility: { editor: true, auxiliaryBar: true } }); - // Editor-only (detail closed): the node is pure editor content, so nothing - // is subtracted — otherwise the side pane would shrink by the detail width - // on every reload (compounding toward zero). const editorOnly = createHost({ single: true, dockedWidth: 300, partVisibility: { editor: true, auxiliaryBar: false } }); assert.deepStrictEqual({ @@ -1443,13 +1438,11 @@ suite('Sessions - Workbench', () => { existingWidths, editorVisible: host.partVisibility.editor, detailsVisible: host.partVisibility.auxiliaryBar, - persistedEditorWidth: host._savedPartSizes.editor, snapshot: host._memento.dockedEditorSizeBeforeHide, }, { existingWidths: [758, 758], editorVisible: true, detailsVisible: false, - persistedEditorWidth: 758, snapshot: undefined, }); }); @@ -1500,7 +1493,6 @@ suite('Sessions - Workbench', () => { assert.deepStrictEqual({ medium: SinglePaneWorkbench.prototype.getPreferredEditorPartWidth.call(medium), - mediumRestoreEqualSplitOnHide: medium._restoreEqualSplitOnDetailsHide, wide: SinglePaneWorkbench.prototype.getPreferredEditorPartWidth.call(wide), wideDetails: SinglePaneWorkbench.prototype.getPreferredEditorPartWidth.call(wideDetails), constrained: SinglePaneWorkbench.prototype.getPreferredEditorPartWidth.call(constrained), @@ -1510,7 +1502,6 @@ suite('Sessions - Workbench', () => { editorHidden: SinglePaneWorkbench.prototype.getPreferredEditorPartWidth.call(editorHidden), }, { medium: 850, - mediumRestoreEqualSplitOnHide: true, wide: 950, wideDetails: 1050, constrained: 650, @@ -1521,7 +1512,7 @@ suite('Sessions - Workbench', () => { }); }); - test('closing Details after a no-op balanced sash reset restores an equal Sessions and Editor split', () => { + test('closing Details after a balanced sash reset leaves the side-pane boundary unchanged', () => { const host = createHost({ single: true, sessionsWidth: 560, editorWidth: 840, dockedWidth: 280, partVisibility: { editor: true, auxiliaryBar: true } }); const resetWidth = SinglePaneWorkbench.prototype.getPreferredEditorPartWidth.call(host); @@ -1529,16 +1520,14 @@ suite('Sessions - Workbench', () => { assert.deepStrictEqual({ resetWidth, - restoreEqualSplitOnHide: host._restoreEqualSplitOnDetailsHide, resizes: host.resizes, }, { resetWidth: 840, - restoreEqualSplitOnHide: false, - resizes: [{ width: 700, height: 800 }], + resizes: [], }); }); - test('hiding Details after a balanced reset uses the live width for an equal split', () => { + test('hiding Details after a balanced reset leaves a later sash resize unchanged', () => { const host = createHost({ single: true, sessionsWidth: 560, editorWidth: 840, dockedWidth: 280, partVisibility: { editor: true, auxiliaryBar: true } }); SinglePaneWorkbench.prototype.getPreferredEditorPartWidth.call(host); host.workbenchGrid.resizeView(host.sessionsPartView, { width: 800, height: 800 }); @@ -1547,16 +1536,10 @@ suite('Sessions - Workbench', () => { setAuxiliaryBarHidden.call(host, true); - assert.deepStrictEqual({ - restoreEqualSplitOnHide: host._restoreEqualSplitOnDetailsHide, - resizes: host.resizes, - }, { - restoreEqualSplitOnHide: false, - resizes: [{ width: 1000, height: 800 }], - }); + assert.deepStrictEqual(host.resizes, []); }); - test('manual sash resize preserves the pending Details-hide reset behavior', () => { + test('manual sash resize does not make hiding Details move the side-pane boundary', () => { const host = createHost({ single: true, sessionsWidth: 560, editorWidth: 840, dockedWidth: 280, partVisibility: { editor: true, auxiliaryBar: true } }); SinglePaneWorkbench.prototype.getPreferredEditorPartWidth.call(host); host.workbenchGrid.resizeView(host.sessionsPartView, { width: 700, height: 800 }); @@ -1566,33 +1549,20 @@ suite('Sessions - Workbench', () => { onEditorNodeResized.call(host, 700); setAuxiliaryBarHidden.call(host, true); - assert.deepStrictEqual({ - restoreEqualSplitOnHide: host._restoreEqualSplitOnDetailsHide, - resizes: host.resizes, - }, { - restoreEqualSplitOnHide: false, - resizes: [{ width: 700, height: 800 }], - }); + assert.deepStrictEqual(host.resizes, []); }); - test('hiding Editor clears the pending Details-hide reset behavior', () => { + test('hiding Details after Editor restores the captured side-pane width', () => { const host = createHost({ single: true, sessionsWidth: 560, editorWidth: 840, dockedWidth: 280, partVisibility: { editor: true, auxiliaryBar: true } }); SinglePaneWorkbench.prototype.getPreferredEditorPartWidth.call(host); setEditorHidden.call(host, true, true); setAuxiliaryBarHidden.call(host, true); - assert.deepStrictEqual({ - restoreEqualSplitOnHide: host._restoreEqualSplitOnDetailsHide, - resizes: host.resizes, - }, { - restoreEqualSplitOnHide: false, - resizes: [ - { width: 280, height: 800 }, - { width: 840, height: 800 }, - { width: 560, height: 800 }, - ], - }); + assert.deepStrictEqual(host.resizes, [ + { width: 280, height: 800 }, + { width: 840, height: 800 }, + ]); }); test('single-pane editor part is a snap view only while editor content is hidden (docked detail-only)', () => { @@ -2442,34 +2412,32 @@ suite('Sessions - Workbench', () => { // --- Docked auxiliary bar visibility ----------------------------------- - test('docked auxiliary bar takes its toggle width from the Sessions pane', () => { - const host = createHost({ single: true, editorWidth: 640, dockedWidth: 280, partVisibility: { editor: true, auxiliaryBar: false } }); + test('docked auxiliary bar takes its toggle width from the editor area', () => { + const host = createHost({ single: true, sessionsWidth: 720, editorWidth: 640, dockedWidth: 280, partVisibility: { editor: true, auxiliaryBar: false } }); setAuxiliaryBarHidden.call(host, false); setAuxiliaryBarHidden.call(host, true); assert.deepStrictEqual({ auxiliaryBarVisible: host.partVisibility.auxiliaryBar, + sessionsWidth: host.workbenchGrid.getViewSize(host.sessionsPartView).width, + editorWidth: host.workbenchGrid.getViewSize(host.editorPartView).width, resizes: host.resizes, }, { auxiliaryBarVisible: false, - resizes: [ - { width: 920, height: 800 }, - { width: 640, height: 800 }, - ], + sessionsWidth: 720, + editorWidth: 640, + resizes: [], }); }); - test('docked auxiliary bar takes its toggle width from Sessions even when the editor is wide', () => { + test('docked auxiliary bar leaves a wide editor node unchanged when toggled', () => { const host = createHost({ single: true, editorWidth: 900, dockedWidth: 300, partVisibility: { editor: true, auxiliaryBar: false } }); setAuxiliaryBarHidden.call(host, false); setAuxiliaryBarHidden.call(host, true); - assert.deepStrictEqual(host.resizes, [ - { width: 1200, height: 800 }, - { width: 900, height: 800 }, - ]); + assert.deepStrictEqual(host.resizes, []); }); test('[reload] restoring docked auxiliary bar uses the persisted combined width without cumulative growth', () => { @@ -2480,28 +2448,22 @@ suite('Sessions - Workbench', () => { suppressionCount: 1, partVisibility: { editor: true, auxiliaryBar: false } }); - host._savedPartSizes.editor = 600; + host._savedPartSizes.editor = 900; setAuxiliaryBarHidden.call(host, false); setAuxiliaryBarHiddenForResize.call(host, true); setAuxiliaryBarHidden.call(host, false); - assert.deepStrictEqual(host.resizes, [ - { width: 900, height: 800 }, - { width: 900, height: 800 }, - ]); + assert.deepStrictEqual(host.resizes, []); }); - test('docked auxiliary bar returns its full width to Sessions when hidden', () => { + test('docked auxiliary bar returns its full width to the editor area when hidden', () => { const host = createHost({ single: true, editorWidth: 420, dockedWidth: 300, partVisibility: { editor: true, auxiliaryBar: false } }); setAuxiliaryBarHidden.call(host, false); setAuxiliaryBarHidden.call(host, true); - assert.deepStrictEqual(host.resizes, [ - { width: 720, height: 800 }, - { width: 420, height: 800 }, - ]); + assert.deepStrictEqual(host.resizes, []); }); test('docked auxiliary bar hide reveals hidden editor content', () => {