From 5700479cf4430040f57005d2f12305d4954f6c14 Mon Sep 17 00:00:00 2001 From: Illia Panasenko Date: Sun, 22 Feb 2026 00:02:33 +0100 Subject: [PATCH 1/2] Add horizontal resize support for quick input widget Allow users to resize the command palette / quick input widget by dragging its left or right edge. The custom width is persisted in storage and restored across sessions. Double-clicking a resize handle resets the width to the default. Fixes microsoft/vscode#85374 Co-authored-by: Cursor --- .../quickinput/browser/media/quickInput.css | 18 ++ .../browser/quickInputController.ts | 166 +++++++++++++++++- 2 files changed, 182 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/quickinput/browser/media/quickInput.css b/src/vs/platform/quickinput/browser/media/quickInput.css index 0636687742da0..31a97a79e14f0 100644 --- a/src/vs/platform/quickinput/browser/media/quickInput.css +++ b/src/vs/platform/quickinput/browser/media/quickInput.css @@ -12,6 +12,24 @@ border-radius: 8px; } +/* Resize handles */ +.quick-input-widget-resize { + position: absolute; + top: 0; + bottom: 0; + width: 6px; + cursor: ew-resize; + z-index: 1; +} + +.quick-input-widget-resize.left { + left: -3px; +} + +.quick-input-widget-resize.right { + right: -3px; +} + .quick-input-titlebar { cursor: grab; display: flex; diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index fae2f7ffb4558..9cb69c266176e 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -45,6 +45,7 @@ const VIEWSTATE_STORAGE_KEY = 'workbench.quickInput.viewState'; type QuickInputViewState = { readonly top?: number; readonly left?: number; + readonly width?: number; // custom width in pixels }; export class QuickInputController extends Disposable { @@ -78,6 +79,7 @@ export class QuickInputController extends Disposable { private viewState: QuickInputViewState | undefined; private dndController: QuickInputDragAndDropController | undefined; + private resizeController: QuickInputResizeController | undefined; private readonly inQuickInputContext: IContextKey; private readonly quickInputTypeContext: IContextKey; @@ -398,6 +400,34 @@ export class QuickInputController extends Disposable { } })); + // Resize support + this.resizeController = this._register(new QuickInputResizeController( + this._container, + container, + this.viewState?.width + )); + + // Resize update layout + this._register(autorun(reader => { + const resizeState = this.resizeController?.resizeViewState.read(reader); + if (!resizeState) { + return; + } + + this.viewState = { + ...this.viewState, + width: resizeState.width, + left: resizeState.newLeftRatio ?? this.viewState?.left + }; + + this.updateLayout(); + + // Save width and position + if (resizeState.done) { + this.saveViewState(this.viewState); + } + })); + this.ui = { container, styleSheet, @@ -858,10 +888,19 @@ export class QuickInputController extends Disposable { this.updateLayout(); } + private getDefaultWidth(): number { + return Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH); + } + private updateLayout() { if (this.ui && this.isVisible()) { const style = this.ui.container.style; - const width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH); + const defaultWidth = this.getDefaultWidth(); + const customWidth = this.viewState?.width; + // Use custom width if set, but clamp to valid range + const width = customWidth !== undefined + ? Math.max(QuickInputResizeController.MIN_WIDTH, Math.min(customWidth, this.dimension!.width - 20)) + : defaultWidth; style.width = width + 'px'; // Position @@ -938,7 +977,7 @@ export class QuickInputController extends Disposable { private loadViewState(): QuickInputViewState | undefined { try { const data = JSON.parse(this.storageService.get(VIEWSTATE_STORAGE_KEY, StorageScope.APPLICATION, '{}')); - if (data.top !== undefined || data.left !== undefined) { + if (data.top !== undefined || data.left !== undefined || data.width !== undefined) { return data; } } catch { } @@ -1152,3 +1191,126 @@ class QuickInputDragAndDropController extends Disposable { return Math.round(this._container.clientWidth / 2) - Math.round(this._quickInputContainer.clientWidth / 2); } } + +class QuickInputResizeController extends Disposable { + static readonly MIN_WIDTH = 300; + static readonly MAX_WIDTH = 900; + + /** + * Observable resize state. `newLeftRatio` is the new absolute center-x ratio + * for the widget so that the opposite edge stays anchored during resize. + */ + readonly resizeViewState = observableValue<{ width?: number; newLeftRatio?: number; done: boolean } | undefined>(this, undefined); + + private readonly _leftHandle: HTMLElement; + private readonly _rightHandle: HTMLElement; + + constructor( + private _container: HTMLElement, + private readonly _quickInputContainer: HTMLElement, + initialWidth: number | undefined + ) { + super(); + + // Create resize handles + this._leftHandle = dom.append(this._quickInputContainer, $('.quick-input-widget-resize.left')); + this._rightHandle = dom.append(this._quickInputContainer, $('.quick-input-widget-resize.right')); + + this._registerMouseListeners(); + + if (initialWidth !== undefined) { + this.resizeViewState.set({ width: initialWidth, done: true }, undefined); + } + } + + private _registerMouseListeners(): void { + // Left handle + this._registerHandleListeners(this._leftHandle, 'left'); + + // Right handle + this._registerHandleListeners(this._rightHandle, 'right'); + + // Double-click on either handle resets width to default + for (const handle of [this._leftHandle, this._rightHandle]) { + this._register(dom.addDisposableGenericMouseUpListener(handle, (event: MouseEvent) => { + const originEvent = new StandardMouseEvent(dom.getWindow(handle), event); + if (originEvent.detail !== 2) { + return; + } + originEvent.preventDefault(); + originEvent.stopPropagation(); + + // Reset width to default + this.resizeViewState.set({ width: undefined, done: true }, undefined); + })); + } + } + + private _registerHandleListeners(handle: HTMLElement, side: 'left' | 'right'): void { + this._register(dom.addDisposableGenericMouseDownListener(handle, (e: MouseEvent) => { + const activeWindow = dom.getWindow(this._container); + const originEvent = new StandardMouseEvent(activeWindow, e); + originEvent.preventDefault(); + originEvent.stopPropagation(); + + const startX = originEvent.browserEvent.clientX; + const startRect = this._quickInputContainer.getBoundingClientRect(); + const startWidth = startRect.width; + const containerWidth = this._container.clientWidth; + // Capture the original center position at drag start + const startCenterX = startRect.left + startWidth / 2; + + let isResizing = false; + const mouseMoveListener = dom.addDisposableGenericMouseMoveListener(activeWindow, (e: MouseEvent) => { + const mouseMoveEvent = new StandardMouseEvent(activeWindow, e); + mouseMoveEvent.preventDefault(); + + if (!isResizing) { + isResizing = true; + } + + const deltaX = mouseMoveEvent.browserEvent.clientX - startX; + let newWidth: number; + + if (side === 'right') { + newWidth = startWidth + deltaX; + } else { + // For left handle, dragging left increases width + newWidth = startWidth - deltaX; + } + + // Clamp to min/max + newWidth = Math.max(QuickInputResizeController.MIN_WIDTH, Math.min(newWidth, QuickInputResizeController.MAX_WIDTH)); + // Also clamp to container width + newWidth = Math.min(newWidth, containerWidth - 20); + + // Compute the new center-x ratio so the opposite edge stays anchored. + // updateLayout uses: leftPx = (containerWidth * leftRatio) - (width / 2) + const widthDelta = newWidth - startWidth; + let newLeftRatio: number; + if (side === 'right') { + // Anchor left edge: left edge = startCenterX - startWidth/2 + // New center = leftEdge + newWidth/2 = startCenterX - startWidth/2 + newWidth/2 + newLeftRatio = (startCenterX + widthDelta / 2) / containerWidth; + } else { + // Anchor right edge: right edge = startCenterX + startWidth/2 + // New center = rightEdge - newWidth/2 = startCenterX + startWidth/2 - newWidth/2 + newLeftRatio = (startCenterX - widthDelta / 2) / containerWidth; + } + + this.resizeViewState.set({ width: newWidth, newLeftRatio, done: false }, undefined); + }); + + const mouseUpListener = dom.addDisposableGenericMouseUpListener(activeWindow, (e: MouseEvent) => { + if (isResizing) { + // Persist the width + const state = this.resizeViewState.get(); + this.resizeViewState.set({ width: state?.width, newLeftRatio: state?.newLeftRatio, done: true }, undefined); + } + + mouseMoveListener.dispose(); + mouseUpListener.dispose(); + }); + })); + } +} From f0c07d5ea13806def715e475f4d624226df49b74 Mon Sep 17 00:00:00 2001 From: Illia Panasenko Date: Sun, 22 Feb 2026 00:11:13 +0100 Subject: [PATCH 2/2] Keep quick input centered while resizing Remove edge-anchoring behavior so the widget stays centered during resize. Also remove the double-click-to-reset-width feature. Co-authored-by: Cursor --- .../browser/quickInputController.ts | 52 +++---------------- 1 file changed, 6 insertions(+), 46 deletions(-) diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 9cb69c266176e..bbd7ae63d19e8 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -417,7 +417,6 @@ export class QuickInputController extends Disposable { this.viewState = { ...this.viewState, width: resizeState.width, - left: resizeState.newLeftRatio ?? this.viewState?.left }; this.updateLayout(); @@ -1196,11 +1195,7 @@ class QuickInputResizeController extends Disposable { static readonly MIN_WIDTH = 300; static readonly MAX_WIDTH = 900; - /** - * Observable resize state. `newLeftRatio` is the new absolute center-x ratio - * for the widget so that the opposite edge stays anchored during resize. - */ - readonly resizeViewState = observableValue<{ width?: number; newLeftRatio?: number; done: boolean } | undefined>(this, undefined); + readonly resizeViewState = observableValue<{ width?: number; done: boolean } | undefined>(this, undefined); private readonly _leftHandle: HTMLElement; private readonly _rightHandle: HTMLElement; @@ -1230,20 +1225,6 @@ class QuickInputResizeController extends Disposable { // Right handle this._registerHandleListeners(this._rightHandle, 'right'); - // Double-click on either handle resets width to default - for (const handle of [this._leftHandle, this._rightHandle]) { - this._register(dom.addDisposableGenericMouseUpListener(handle, (event: MouseEvent) => { - const originEvent = new StandardMouseEvent(dom.getWindow(handle), event); - if (originEvent.detail !== 2) { - return; - } - originEvent.preventDefault(); - originEvent.stopPropagation(); - - // Reset width to default - this.resizeViewState.set({ width: undefined, done: true }, undefined); - })); - } } private _registerHandleListeners(handle: HTMLElement, side: 'left' | 'right'): void { @@ -1254,11 +1235,8 @@ class QuickInputResizeController extends Disposable { originEvent.stopPropagation(); const startX = originEvent.browserEvent.clientX; - const startRect = this._quickInputContainer.getBoundingClientRect(); - const startWidth = startRect.width; + const startWidth = this._quickInputContainer.getBoundingClientRect().width; const containerWidth = this._container.clientWidth; - // Capture the original center position at drag start - const startCenterX = startRect.left + startWidth / 2; let isResizing = false; const mouseMoveListener = dom.addDisposableGenericMouseMoveListener(activeWindow, (e: MouseEvent) => { @@ -1273,39 +1251,21 @@ class QuickInputResizeController extends Disposable { let newWidth: number; if (side === 'right') { - newWidth = startWidth + deltaX; + newWidth = startWidth + deltaX * 2; } else { - // For left handle, dragging left increases width - newWidth = startWidth - deltaX; + newWidth = startWidth - deltaX * 2; } - // Clamp to min/max newWidth = Math.max(QuickInputResizeController.MIN_WIDTH, Math.min(newWidth, QuickInputResizeController.MAX_WIDTH)); - // Also clamp to container width newWidth = Math.min(newWidth, containerWidth - 20); - // Compute the new center-x ratio so the opposite edge stays anchored. - // updateLayout uses: leftPx = (containerWidth * leftRatio) - (width / 2) - const widthDelta = newWidth - startWidth; - let newLeftRatio: number; - if (side === 'right') { - // Anchor left edge: left edge = startCenterX - startWidth/2 - // New center = leftEdge + newWidth/2 = startCenterX - startWidth/2 + newWidth/2 - newLeftRatio = (startCenterX + widthDelta / 2) / containerWidth; - } else { - // Anchor right edge: right edge = startCenterX + startWidth/2 - // New center = rightEdge - newWidth/2 = startCenterX + startWidth/2 - newWidth/2 - newLeftRatio = (startCenterX - widthDelta / 2) / containerWidth; - } - - this.resizeViewState.set({ width: newWidth, newLeftRatio, done: false }, undefined); + this.resizeViewState.set({ width: newWidth, done: false }, undefined); }); const mouseUpListener = dom.addDisposableGenericMouseUpListener(activeWindow, (e: MouseEvent) => { if (isResizing) { - // Persist the width const state = this.resizeViewState.get(); - this.resizeViewState.set({ width: state?.width, newLeftRatio: state?.newLeftRatio, done: true }, undefined); + this.resizeViewState.set({ width: state?.width, done: true }, undefined); } mouseMoveListener.dispose();