From 607766341a6d3b08c889e4d04bcc98993be99ac8 Mon Sep 17 00:00:00 2001 From: robmonte <17119716+robmonte@users.noreply.github.com> Date: Fri, 19 Jun 2026 00:43:53 -0700 Subject: [PATCH 1/6] add setting to maintain the editor size when toggling panels --- src/vs/platform/native/common/native.ts | 43 +++++++ .../electron-main/nativeHostMainService.ts | 9 +- .../native/test/common/native.test.ts | 57 +++++++++ src/vs/workbench/browser/layout.ts | 112 +++++++++++++++++- .../browser/workbench.contribution.ts | 6 + .../host/browser/browserHostService.ts | 5 + .../workbench/services/host/browser/host.ts | 9 +- .../electron-browser/nativeHostService.ts | 6 +- .../test/browser/workbenchTestServices.ts | 3 + .../electron-browser/workbenchTestServices.ts | 3 +- 10 files changed, 243 insertions(+), 10 deletions(-) create mode 100644 src/vs/platform/native/test/common/native.test.ts diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 8e669593b66a2f..776d8ab2646559 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -56,6 +56,38 @@ export interface INativeHostOptions { readonly targetWindowId?: number; } +/** + * The amount to grow or shrink a window by in pixels. + */ +export interface IWindowResizeDelta { + readonly width: number; + readonly height: number; +} + +/** + * The edges to keep fixed in place while resizing a window. When `right` is + * `true`, the window grows/shrinks to the left, otherwise the left edge stays + * fixed. When `bottom` is `true`, the window grows/shrinks upwards, otherwise + * the top edge stays fixed. + */ +export interface IWindowResizeAnchor { + readonly right: boolean; + readonly bottom: boolean; +} + +/** + * Computes new window bounds by applying the `delta` to `bounds` while keeping + * the edges indicated by `anchor` fixed in place. + */ +export function getResizedWindowBounds(bounds: IRectangle, delta: IWindowResizeDelta, anchor: IWindowResizeAnchor): IRectangle { + const width = bounds.width + delta.width; + const height = bounds.height + delta.height; + const x = anchor.right ? bounds.x + bounds.width - width : bounds.x; + const y = anchor.bottom ? bounds.y + bounds.height - height : bounds.y; + + return { x, y, width, height }; +} + export const enum FocusMode { /** @@ -143,6 +175,17 @@ export interface ICommonNativeHostService { moveWindowTop(options?: INativeHostOptions): Promise; positionWindow(position: IRectangle, options?: INativeHostOptions): Promise; + /** + * Resizes the window by the delta, keeping the edges as indicated by + * the anchor fixed in place. Has no effect when the window is maximized or in + * full screen. + * + * @param delta The amount to grow or shrink the window in pixels. + * @param anchor The edges to keep fixed while resizing. + * @param options Options to target a specific window. + */ + resizeWindow(delta: IWindowResizeDelta, anchor: IWindowResizeAnchor, options?: INativeHostOptions): Promise; + isWindowAlwaysOnTop(options?: INativeHostOptions): Promise; toggleWindowAlwaysOnTop(options?: INativeHostOptions): Promise; setWindowAlwaysOnTop(alwaysOnTop: boolean, options?: INativeHostOptions): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 0c4f61b14bfd82..c16de062fdc926 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -27,7 +27,7 @@ import { IEnvironmentMainService } from '../../environment/electron-main/environ import { createDecorator, IInstantiationService } from '../../instantiation/common/instantiation.js'; import { ILifecycleMainService, IRelaunchOptions } from '../../lifecycle/electron-main/lifecycleMainService.js'; import { ILogService } from '../../log/common/log.js'; -import { FocusMode, ICommonNativeHostService, INativeHostOptions, IOSProperties, IOSStatistics, IToastOptions, IToastResult, PowerSaveBlockerType, SystemIdleState, ThermalState } from '../common/native.js'; +import { FocusMode, ICommonNativeHostService, INativeHostOptions, IOSProperties, IOSStatistics, IToastOptions, IToastResult, IWindowResizeAnchor, IWindowResizeDelta, getResizedWindowBounds, PowerSaveBlockerType, SystemIdleState, ThermalState } from '../common/native.js'; import { IProductService } from '../../product/common/productService.js'; import { IPartsSplash } from '../../theme/common/themeService.js'; import { IThemeMainService } from '../../theme/electron-main/themeMainService.js'; @@ -385,6 +385,13 @@ export class NativeHostMainService extends Disposable implements INativeHostMain } } + async resizeWindow(windowId: number | undefined, delta: IWindowResizeDelta, anchor: IWindowResizeAnchor, options?: INativeHostOptions): Promise { + const window = this.windowById(options?.targetWindowId, windowId); + if (window?.win && !window.win.isFullScreen() && !window.win.isMaximized()) { + window.win.setBounds(getResizedWindowBounds(window.win.getBounds(), delta, anchor)); + } + } + async updateWindowControls(windowId: number | undefined, options: INativeHostOptions & { height?: number; backgroundColor?: string; foregroundColor?: string; dimmed?: boolean }): Promise { const window = this.windowById(options?.targetWindowId, windowId); window?.updateWindowControls(options); diff --git a/src/vs/platform/native/test/common/native.test.ts b/src/vs/platform/native/test/common/native.test.ts new file mode 100644 index 00000000000000..6f230161aa6d56 --- /dev/null +++ b/src/vs/platform/native/test/common/native.test.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { getResizedWindowBounds } from '../../common/native.js'; + +suite('getResizedWindowBounds', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + const bounds = { x: 100, y: 200, width: 800, height: 600 }; + + test('left/top anchor keeps the origin fixed when growing', () => { + assert.deepStrictEqual( + getResizedWindowBounds(bounds, { width: 50, height: 30 }, { right: false, bottom: false }), + { x: 100, y: 200, width: 850, height: 630 } + ); + }); + + test('right anchor keeps the right edge fixed when growing', () => { + assert.deepStrictEqual( + getResizedWindowBounds(bounds, { width: 50, height: 0 }, { right: true, bottom: false }), + { x: 50, y: 200, width: 850, height: 600 } + ); + }); + + test('bottom anchor keeps the bottom edge fixed when growing', () => { + assert.deepStrictEqual( + getResizedWindowBounds(bounds, { width: 0, height: 30 }, { right: false, bottom: true }), + { x: 100, y: 170, width: 800, height: 630 } + ); + }); + + test('right and bottom anchor keeps the bottom-right corner fixed when growing', () => { + assert.deepStrictEqual( + getResizedWindowBounds(bounds, { width: 50, height: 30 }, { right: true, bottom: true }), + { x: 50, y: 170, width: 850, height: 630 } + ); + }); + + test('negative delta shrinks the window toward the anchored edge', () => { + assert.deepStrictEqual( + getResizedWindowBounds(bounds, { width: -50, height: -30 }, { right: true, bottom: true }), + { x: 150, y: 230, width: 750, height: 570 } + ); + }); + + test('zero delta leaves the bounds unchanged', () => { + assert.deepStrictEqual( + getResizedWindowBounds(bounds, { width: 0, height: 0 }, { right: true, bottom: true }), + { x: 100, y: 200, width: 800, height: 600 } + ); + }); +}); diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index ea2da133d8609a..544f4dd87bb5d9 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -7,7 +7,7 @@ import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } import { Event, Emitter } from '../../base/common/event.js'; import { EventType, addDisposableListener, getClientArea, size, IDimension, isAncestorUsingFlowTo, computeScreenAwareSize, getActiveDocument, getWindows, getActiveWindow, isActiveDocument, getWindow, getWindowId, getActiveElement, Dimension } from '../../base/browser/dom.js'; import { onDidChangeFullscreen, isFullscreen, isWCOEnabled } from '../../base/browser/browser.js'; -import { isWindows, isLinux, isMacintosh, isWeb, isIOS } from '../../base/common/platform.js'; +import { isWindows, isLinux, isMacintosh, isWeb, isIOS, isNative } from '../../base/common/platform.js'; import { EditorInputCapabilities, GroupIdentifier, isResourceEditorInput, IUntypedEditorInput, pathsToEditors } from '../common/editor.js'; import { SidebarPart } from './parts/sidebar/sidebarPart.js'; import { PanelPart } from './parts/panel/panelPart.js'; @@ -40,7 +40,7 @@ import { DiffEditorInput } from '../common/editor/diffEditorInput.js'; import { mark } from '../../base/common/performance.js'; import { IExtensionService } from '../services/extensions/common/extensions.js'; import { ILogService } from '../../platform/log/common/log.js'; -import { DeferredPromise, Promises } from '../../base/common/async.js'; +import { DeferredPromise, Promises, raceTimeout } from '../../base/common/async.js'; import { IBannerService } from '../services/banner/browser/bannerService.js'; import { IPaneCompositePartService } from '../services/panecomposite/browser/panecomposite.js'; import { AuxiliaryBarPart } from './parts/auxiliarybar/auxiliaryBarPart.js'; @@ -69,6 +69,12 @@ interface IEditorToOpen { readonly viewColumn?: number; } +interface IPartToggleWindowResize { + readonly editorSize: IViewSize; + readonly delta: { readonly width: number; readonly height: number }; + readonly anchor: { readonly right: boolean; readonly bottom: boolean }; +} + interface ILayoutInitializationState { readonly views: { readonly defaults: string[] | undefined; @@ -1456,7 +1462,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.setPanelHidden(true, true); this.setAuxiliaryBarHidden(true, true); - this.setSideBarHidden(true); + this.setSideBarHidden(true, true); if (config.hideActivityBar) { this.setActivityBarHidden(true); @@ -1539,7 +1545,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } if (zenModeExitInfo.wasVisible.sideBar) { - this.setSideBarHidden(false); + this.setSideBarHidden(false, true); } if (!this.stateModel.getRuntimeValue(LayoutStateKeys.ACTIVITYBAR_HIDDEN, true)) { @@ -1654,7 +1660,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // visibility efficiently. if (part === sideBar) { - this.setSideBarHidden(!visible); + this.setSideBarHidden(!visible, true); } else if (part === panelPart && this.stateModel.getRuntimeValue(LayoutStateKeys.PANEL_HIDDEN) === visible) { this.setPanelHidden(!visible, true); } else if (part === auxiliaryBarPart) { @@ -1886,13 +1892,19 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi ]); } - private setSideBarHidden(hidden: boolean): void { + private setSideBarHidden(hidden: boolean, skipLayout?: boolean): void { if (!hidden && this.setAuxiliaryBarMaximized(false) && this.isVisible(Parts.SIDEBAR_PART)) { return; // return: leaving maximised auxiliary bar made this part visible } + const wasHidden = !this.isVisible(Parts.SIDEBAR_PART); + this.stateModel.setRuntimeValue(LayoutStateKeys.SIDEBAR_HIDDEN, hidden); + // Optionally resize the window so that the editor keeps its size when the + // primary side bar is shown or hidden (instead of the editor making room for it) + const sideBarToggleResize = this.getPartToggleWindowResize(this.sideBarPartView, this.getSideBarPosition(), hidden, wasHidden, skipLayout, false); + // Adjust CSS if (hidden) { this.mainContainer.classList.add(LayoutClasses.SIDEBAR_HIDDEN); @@ -1919,6 +1931,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.openViewContainer(ViewContainerLocation.Sidebar, viewletToOpen); } } + + // Apply the window resize that keeps the editor size stable (no-op on web) + if (sideBarToggleResize) { + this.resizeWindowToKeepEditorSize(sideBarToggleResize); + } } private hasViews(id: string): boolean { @@ -2037,6 +2054,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const panelOpensMaximized = this.panelOpensMaximized(); + // Optionally resize the window so that the editor keeps its size when + // the panel is shown or hidden (instead of the editor making room for it) + const panelToggleResize = this.getPartToggleWindowResize(this.panelPartView, this.getPanelPosition(), hidden, wasHidden, skipLayout, hidden ? isPanelMaximized : panelOpensMaximized); + // Adjust CSS if (hidden) { this.mainContainer.classList.add(LayoutClasses.PANEL_HIDDEN); @@ -2101,6 +2122,72 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (focusEditor) { this.editorGroupService.mainPart.activeGroup.focus(); // Pass focus to editor group if panel part is now hidden } + + // Apply the window resize that keeps the editor size stable (no-op on web) + if (panelToggleResize) { + this.resizeWindowToKeepEditorSize(panelToggleResize); + } + } + + private getPartToggleWindowResize(partView: ISerializableView, position: Position, hidden: boolean, wasHidden: boolean, skipLayout: boolean | undefined, willBeMaximized: boolean): IPartToggleWindowResize | undefined { + const visibilityChanged = wasHidden !== hidden; + const keepEditorSizeEnabled = this.configurationService.getValue(WorkbenchLayoutSettings.KEEP_EDITOR_SIZE_ON_TOGGLE); + const editorVisible = this.isVisible(Parts.EDITOR_PART, mainWindow); + + // Window resizing is only available on desktop and only when the window is + // free to grow or shrink (not maximized or in full screen) + const canResizeWindow = isNative && !this.state.runtime.mainWindowFullscreen && !this.isWindowMaximized(mainWindow); + + if ( + skipLayout || // Only on explicit user toggles, not during restore + willBeMaximized || // Editor is not visible when the part is maximized + this.inMaximizedAuxiliaryBarTransition || // Transition resizes all parts at once + !visibilityChanged || + !keepEditorSizeEnabled || + !canResizeWindow || + !editorVisible + ) { + return undefined; + } + + const horizontal = isHorizontal(position); + + // When hiding, the part is still visible in the grid, so use its current size + // When showing, the part is still hidden, so fall back to its last visible size + let partSizeAlongAxis: number; + if (hidden) { + const partSize = this.workbenchGrid.getViewSize(partView); + partSizeAlongAxis = horizontal ? partSize.height : partSize.width; + } else { + partSizeAlongAxis = this.workbenchGrid.getViewCachedVisibleSize(partView) ?? (horizontal ? partView.minimumHeight : partView.minimumWidth); + } + + if (partSizeAlongAxis <= 0) { + return undefined; + } + + const delta = hidden ? -partSizeAlongAxis : partSizeAlongAxis; + + return { + editorSize: this.workbenchGrid.getViewSize(this.editorPartView), + delta: { width: horizontal ? 0 : delta, height: horizontal ? delta : 0 }, + anchor: { right: position === Position.LEFT, bottom: position === Position.TOP } + }; + } + + private async resizeWindowToKeepEditorSize(resize: IPartToggleWindowResize): Promise { + + // Wait for the layout that the OS window resize triggers, then restore the + // editor to its previous size so that the panel absorbs the size change + const onDidRelayout = raceTimeout(Event.toPromise(Event.once(this.onDidLayoutMainContainer)), 1000); + await this.hostService.resizeMainWindow(resize.delta, resize.anchor); + await onDidRelayout; + + if (this.disposed || !this.workbenchGrid) { + return; + } + + this.workbenchGrid.resizeView(this.editorPartView, { width: resize.editorSize.width, height: resize.editorSize.height }); } private inMaximizedAuxiliaryBarTransition = false; @@ -2229,8 +2316,15 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return; // return: leaving maximised auxiliary bar made this part hidden } + const wasHidden = !this.isVisible(Parts.AUXILIARYBAR_PART); + this.stateModel.setRuntimeValue(LayoutStateKeys.AUXILIARYBAR_HIDDEN, hidden); + // Optionally resize the window so that the editor keeps its size when the + // secondary side bar is shown or hidden (instead of the editor making room for it). + // The secondary side bar is always on the opposite side of the primary side bar. + const auxiliaryBarToggleResize = this.getPartToggleWindowResize(this.auxiliaryBarPartView, this.getSideBarPosition() === Position.LEFT ? Position.RIGHT : Position.LEFT, hidden, wasHidden, skipLayout, false); + // Adjust CSS if (hidden) { this.mainContainer.classList.add(LayoutClasses.AUXILIARYBAR_HIDDEN); @@ -2263,6 +2357,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.openViewContainer(ViewContainerLocation.AuxiliaryBar, viewletToOpen, !skipLayout); } } + + // Apply the window resize that keeps the editor size stable (no-op on web) + if (auxiliaryBarToggleResize) { + this.resizeWindowToKeepEditorSize(auxiliaryBarToggleResize); + } } setPartHidden(hidden: boolean, part: Parts): void { @@ -2840,6 +2939,7 @@ enum WorkbenchLayoutSettings { ACTIVITY_BAR_VISIBLE = 'workbench.activityBar.visible', PANEL_POSITION = 'workbench.panel.defaultLocation', PANEL_OPENS_MAXIMIZED = 'workbench.panel.opensMaximized', + KEEP_EDITOR_SIZE_ON_TOGGLE = 'workbench.keepEditorSizeOnToggle', ZEN_MODE_CONFIG = 'zenMode', EDITOR_CENTERED_LAYOUT_AUTO_RESIZE = 'workbench.editor.centeredLayoutAutoResize', EDITOR_RESTORE_EDITORS = 'workbench.editor.restoreEditors', diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index eeddba006261bb..e96d9e10eaa0ba 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -594,6 +594,12 @@ const registry = Registry.as(ConfigurationExtensions.Con ], agentsWindow: { default: 'never', readOnly: true }, }, + 'workbench.keepEditorSizeOnToggle': { + 'type': 'boolean', + 'default': false, + 'description': localize('keepEditorSizeOnToggle', "Controls whether showing or hiding the panel, primary side bar, or secondary side bar resizes the window such that the editor keeps its size, instead of changing the editor size to make room for it. Only applies on desktop when the window is not maximized or in full screen."), + 'included': !isWeb, + }, 'workbench.secondarySideBar.defaultVisibility': { 'type': 'string', 'enum': ['hidden', 'visibleInWorkspace', 'visible', 'maximizedInWorkspace', 'maximized'], diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index 3815713423ed81..23407a6082d566 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -5,6 +5,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { IHostService, IToastOptions, IToastResult } from './host.js'; +import { IWindowResizeAnchor, IWindowResizeDelta } from '../../../../platform/native/common/native.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; import { IEditorService } from '../../editor/common/editorService.js'; @@ -581,6 +582,10 @@ export class BrowserHostService extends Disposable implements IHostService { // not supported in browser } + async resizeMainWindow(_delta: IWindowResizeDelta, _anchor: IWindowResizeAnchor): Promise { + // not supported in browser + } + async getCursorScreenPoint(): Promise { return undefined; } diff --git a/src/vs/workbench/services/host/browser/host.ts b/src/vs/workbench/services/host/browser/host.ts index a43eb767fbf440..0da34a49d95a68 100644 --- a/src/vs/workbench/services/host/browser/host.ts +++ b/src/vs/workbench/services/host/browser/host.ts @@ -7,7 +7,7 @@ import { VSBuffer } from '../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { FocusMode } from '../../../../platform/native/common/native.js'; +import { FocusMode, IWindowResizeAnchor, IWindowResizeDelta } from '../../../../platform/native/common/native.js'; import { IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions, IPoint, IRectangle, IOpenedMainWindow, IOpenedAuxiliaryWindow } from '../../../../platform/window/common/window.js'; export const IHostService = createDecorator('hostService'); @@ -111,6 +111,13 @@ export interface IHostService { */ setWindowDimmed(targetWindow: Window, dimmed: boolean): Promise; + /** + * Resizes the window by the delta, keeping the edges as indicated by + * the anchor fixed in place. Has no effect when the window is maximized or in + * full screen. + */ + resizeMainWindow(delta: IWindowResizeDelta, anchor: IWindowResizeAnchor): Promise; + /** * Get the location of the mouse cursor and its display bounds or `undefined` if unavailable. */ diff --git a/src/vs/workbench/services/host/electron-browser/nativeHostService.ts b/src/vs/workbench/services/host/electron-browser/nativeHostService.ts index 86a3d521e1bef4..ad3736a0fbb953 100644 --- a/src/vs/workbench/services/host/electron-browser/nativeHostService.ts +++ b/src/vs/workbench/services/host/electron-browser/nativeHostService.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { IHostService, IToastOptions, IToastResult } from '../browser/host.js'; -import { FocusMode, INativeHostService } from '../../../../platform/native/common/native.js'; +import { FocusMode, INativeHostService, IWindowResizeAnchor, IWindowResizeDelta } from '../../../../platform/native/common/native.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js'; import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js'; @@ -177,6 +177,10 @@ class WorkbenchHostService extends Disposable implements IHostService { return this.nativeHostService.updateWindowControls({ dimmed, targetWindowId: getWindowId(targetWindow) }); } + resizeMainWindow(delta: IWindowResizeDelta, anchor: IWindowResizeAnchor): Promise { + return this.nativeHostService.resizeWindow(delta, anchor); + } + getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle }> { return this.nativeHostService.getCursorScreenPoint(); } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 62f841bd9da5f5..3ceb3edc14b468 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -163,6 +163,7 @@ import { IElevatedFileService } from '../../services/files/common/elevatedFileSe import { FilesConfigurationService, IFilesConfigurationService } from '../../services/filesConfiguration/common/filesConfigurationService.js'; import { IHistoryService } from '../../services/history/common/history.js'; import { IHostService, IToastOptions, IToastResult } from '../../services/host/browser/host.js'; +import { IWindowResizeAnchor, IWindowResizeDelta } from '../../../platform/native/common/native.js'; import { LabelService } from '../../services/label/common/labelService.js'; import { ILanguageDetectionService } from '../../services/languageDetection/common/languageDetectionWorkerService.js'; import { IPartVisibilityChangeEvent, IWorkbenchLayoutService, PanelAlignment, Position as PartPosition, Parts, SINGLE_WINDOW_PARTS } from '../../services/layout/browser/layoutService.js'; @@ -1383,6 +1384,8 @@ export class TestHostService implements IHostService { async setWindowDimmed(_targetWindow: Window, _dimmed: boolean): Promise { } + async resizeMainWindow(_delta: IWindowResizeDelta, _anchor: IWindowResizeAnchor): Promise { } + readonly colorScheme = ColorScheme.DARK; onDidChangeColorScheme = Event.None; } diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 1ca6e7bf478781..d5b2dba72fd994 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -25,7 +25,7 @@ import { InMemoryFileSystemProvider } from '../../../platform/files/common/inMem import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { ISharedProcessService } from '../../../platform/ipc/electron-browser/services.js'; import { NullLogService } from '../../../platform/log/common/log.js'; -import { INativeHostOptions, INativeHostService, IOSProperties, IOSStatistics, IToastOptions, IToastResult, PowerSaveBlockerType, SystemIdleState, ThermalState } from '../../../platform/native/common/native.js'; +import { INativeHostOptions, INativeHostService, IOSProperties, IOSStatistics, IToastOptions, IToastResult, IWindowResizeAnchor, IWindowResizeDelta, PowerSaveBlockerType, SystemIdleState, ThermalState } from '../../../platform/native/common/native.js'; import { IProductService } from '../../../platform/product/common/productService.js'; import { AuthInfo, Credentials } from '../../../platform/request/common/request.js'; import { IStorageService } from '../../../platform/storage/common/storage.js'; @@ -117,6 +117,7 @@ export class TestNativeHostService implements INativeHostService { async setWindowAlwaysOnTop(alwaysOnTop: boolean, options?: INativeHostOptions): Promise { } async getCursorScreenPoint(): Promise<{ readonly point: IPoint; readonly display: IRectangle }> { throw new Error('Method not implemented.'); } async positionWindow(position: IRectangle, options?: INativeHostOptions): Promise { } + async resizeWindow(delta: IWindowResizeDelta, anchor: IWindowResizeAnchor, options?: INativeHostOptions): Promise { } async updateWindowControls(options: { height?: number; backgroundColor?: string; foregroundColor?: string }): Promise { } async updateWindowAccentColor(color: string): Promise { } async setMinimumSize(width: number | undefined, height: number | undefined): Promise { } From 19c2927d9016715f4dcfe699413f6e879072b7e2 Mon Sep 17 00:00:00 2001 From: Robert <17119716+robmonte@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:01:36 -0700 Subject: [PATCH 2/6] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../electron-main/nativeHostMainService.ts | 18 +++++++++++++-- src/vs/workbench/browser/layout.ts | 22 +++++++++++++------ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 0a517cbdc17c9a..6b2c7f732f4125 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -387,9 +387,23 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async resizeWindow(windowId: number | undefined, delta: IWindowResizeDelta, anchor: IWindowResizeAnchor, options?: INativeHostOptions): Promise { const window = this.windowById(options?.targetWindowId, windowId); - if (window?.win && !window.win.isFullScreen() && !window.win.isMaximized()) { - window.win.setBounds(getResizedWindowBounds(window.win.getBounds(), delta, anchor)); + if (!window?.win || window.win.isFullScreen() || window.win.isMaximized()) { + return; } + + const currentBounds = window.win.getBounds(); + const [minWidth, minHeight] = window.win.getMinimumSize(); + + const desiredBounds = getResizedWindowBounds(currentBounds, delta, anchor); + const width = Math.max(desiredBounds.width, minWidth); + const height = Math.max(desiredBounds.height, minHeight); + + window.win.setBounds({ + x: anchor.right ? currentBounds.x + currentBounds.width - width : currentBounds.x, + y: anchor.bottom ? currentBounds.y + currentBounds.height - height : currentBounds.y, + width, + height + }); } async updateWindowControls(windowId: number | undefined, options: INativeHostOptions & { height?: number; backgroundColor?: string; foregroundColor?: string; dimmed?: boolean }): Promise { diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 20e4c78047895d..0617e392dadadb 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -2195,15 +2195,23 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi }; } - private async resizeWindowToKeepEditorSize(resize: IPartToggleWindowResize): Promise { + private resizeWindowToKeepEditorSizeSeq = 0; - // Wait for the layout that the OS window resize triggers, then restore the - // editor to its previous size so that the panel absorbs the size change - const onDidRelayout = raceTimeout(Event.toPromise(Event.once(this.onDidLayoutMainContainer)), 1000); - await this.hostService.resizeMainWindow(resize.delta, resize.anchor); - await onDidRelayout; + private async resizeWindowToKeepEditorSize(resize: IPartToggleWindowResize): Promise { + const seq = ++this.resizeWindowToKeepEditorSizeSeq; + + try { + // Wait for the layout that the OS window resize triggers, then restore the + // editor to its previous size so that the panel absorbs the size change + const onDidRelayout = raceTimeout(Event.toPromise(Event.once(this.onDidLayoutMainContainer)), 1000); + await this.hostService.resizeMainWindow(resize.delta, resize.anchor); + await onDidRelayout; + } catch (error) { + this.logService.warn('[layout] resizeWindowToKeepEditorSize failed', error); + return; + } - if (this.disposed || !this.workbenchGrid) { + if (this.disposed || !this.workbenchGrid || seq !== this.resizeWindowToKeepEditorSizeSeq) { return; } From 72d6c0dbcfd7e9ced7c0a5ae98ade7d58ebaf341 Mon Sep 17 00:00:00 2001 From: Robert <17119716+robmonte@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:15:21 -0700 Subject: [PATCH 3/6] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/vs/workbench/browser/layout.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 0617e392dadadb..9666b2c8b88fda 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1954,7 +1954,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Apply the window resize that keeps the editor size stable (no-op on web) if (sideBarToggleResize) { - this.resizeWindowToKeepEditorSize(sideBarToggleResize); + void this.resizeWindowToKeepEditorSize(sideBarToggleResize); } } @@ -2145,7 +2145,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Apply the window resize that keeps the editor size stable (no-op on web) if (panelToggleResize) { - this.resizeWindowToKeepEditorSize(panelToggleResize); + void this.resizeWindowToKeepEditorSize(panelToggleResize); } } @@ -2388,7 +2388,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi // Apply the window resize that keeps the editor size stable (no-op on web) if (auxiliaryBarToggleResize) { - this.resizeWindowToKeepEditorSize(auxiliaryBarToggleResize); + void this.resizeWindowToKeepEditorSize(auxiliaryBarToggleResize); } } From 5eb0828a05f67a03f784343f024745ad2dbedab0 Mon Sep 17 00:00:00 2001 From: robmonte <17119716+robmonte@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:16:15 -0700 Subject: [PATCH 4/6] Fix comment --- src/vs/workbench/browser/layout.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 9666b2c8b88fda..2cc6f2e632c23b 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -2159,7 +2159,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const canResizeWindow = isNative && !this.state.runtime.mainWindowFullscreen && !this.isWindowMaximized(mainWindow); if ( - skipLayout || // Only on explicit user toggles, not during restore + skipLayout || // Skip programmatic visibility changes willBeMaximized || // Editor is not visible when the part is maximized this.inMaximizedAuxiliaryBarTransition || // Transition resizes all parts at once !visibilityChanged || From 86458466e1655b59814047ef9c2bb9dc458dbc98 Mon Sep 17 00:00:00 2001 From: Robert <17119716+robmonte@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:24:44 -0700 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../electron-main/nativeHostMainService.ts | 15 ++++++--------- src/vs/workbench/browser/layout.ts | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 6b2c7f732f4125..f660b8b9f0fd63 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -395,15 +395,12 @@ export class NativeHostMainService extends Disposable implements INativeHostMain const [minWidth, minHeight] = window.win.getMinimumSize(); const desiredBounds = getResizedWindowBounds(currentBounds, delta, anchor); - const width = Math.max(desiredBounds.width, minWidth); - const height = Math.max(desiredBounds.height, minHeight); - - window.win.setBounds({ - x: anchor.right ? currentBounds.x + currentBounds.width - width : currentBounds.x, - y: anchor.bottom ? currentBounds.y + currentBounds.height - height : currentBounds.y, - width, - height - }); + desiredBounds.width = Math.max(desiredBounds.width, minWidth); + desiredBounds.height = Math.max(desiredBounds.height, minHeight); + desiredBounds.x = anchor.right ? currentBounds.x + currentBounds.width - desiredBounds.width : currentBounds.x; + desiredBounds.y = anchor.bottom ? currentBounds.y + currentBounds.height - desiredBounds.height : currentBounds.y; + + window.win.setBounds(desiredBounds); } async updateWindowControls(windowId: number | undefined, options: INativeHostOptions & { height?: number; backgroundColor?: string; foregroundColor?: string; dimmed?: boolean }): Promise { diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 2cc6f2e632c23b..ae8d5b369a601f 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -2201,11 +2201,19 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const seq = ++this.resizeWindowToKeepEditorSizeSeq; try { - // Wait for the layout that the OS window resize triggers, then restore the - // editor to its previous size so that the panel absorbs the size change - const onDidRelayout = raceTimeout(Event.toPromise(Event.once(this.onDidLayoutMainContainer)), 1000); + const beforeWidth = this._mainContainerDimension.width; + const beforeHeight = this._mainContainerDimension.height; + await this.hostService.resizeMainWindow(resize.delta, resize.anchor); - await onDidRelayout; + + const didRelayout = await raceTimeout( + Event.toPromise(Event.once(Event.filter(this.onDidLayoutMainContainer, d => d.width !== beforeWidth || d.height !== beforeHeight))), + 1000 + ); + + if (!didRelayout) { + return; + } } catch (error) { this.logService.warn('[layout] resizeWindowToKeepEditorSize failed', error); return; From 2ac2e9f2a478cbf0ad2600328732440b41ad039b Mon Sep 17 00:00:00 2001 From: Robert <17119716+robmonte@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:21:37 -0700 Subject: [PATCH 6/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/vs/workbench/browser/layout.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index ae8d5b369a601f..a13a86d513c5c9 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -2204,12 +2204,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const beforeWidth = this._mainContainerDimension.width; const beforeHeight = this._mainContainerDimension.height; + const relayoutPromise = Event.toPromise(Event.once(Event.filter(this.onDidLayoutMainContainer, d => d.width !== beforeWidth || d.height !== beforeHeight))); + await this.hostService.resizeMainWindow(resize.delta, resize.anchor); - const didRelayout = await raceTimeout( - Event.toPromise(Event.once(Event.filter(this.onDidLayoutMainContainer, d => d.width !== beforeWidth || d.height !== beforeHeight))), - 1000 - ); + const didRelayout = await raceTimeout(relayoutPromise, 1000); if (!didRelayout) { return;