-
Notifications
You must be signed in to change notification settings - Fork 37.6k
Chat resize sash #290621
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Chat resize sash #290621
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,7 @@ import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from '../. | |||||
| import * as aria from '../../../../../../base/browser/ui/aria/aria.js'; | ||||||
| import { ButtonWithIcon } from '../../../../../../base/browser/ui/button/button.js'; | ||||||
| import { createInstantHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; | ||||||
| import { IHorizontalSashLayoutProvider, Orientation, Sash } from '../../../../../../base/browser/ui/sash/sash.js'; | ||||||
| import { renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; | ||||||
| import { IAction } from '../../../../../../base/common/actions.js'; | ||||||
| import { equals as arraysEqual } from '../../../../../../base/common/arrays.js'; | ||||||
|
|
@@ -123,6 +124,7 @@ import { WorkspacePickerActionItem } from './workspacePickerActionItem.js'; | |||||
| const $ = dom.$; | ||||||
|
|
||||||
| const INPUT_EDITOR_MAX_HEIGHT = 250; | ||||||
| const INPUT_EDITOR_MIN_HEIGHT = 30; | ||||||
| const CachedLanguageModelsKey = 'chat.cachedLanguageModels.v2'; | ||||||
|
|
||||||
| export interface IChatInputStyles { | ||||||
|
|
@@ -252,6 +254,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge | |||||
| private inputEditorHeight: number = 0; | ||||||
| private container!: HTMLElement; | ||||||
|
|
||||||
| private _sash: Sash | undefined; | ||||||
| private _sashStartHeight: number | undefined; | ||||||
| private _userSetMinHeight: number | undefined; | ||||||
| private _containerHeight: number | undefined; | ||||||
|
|
||||||
| private inputSideToolbarContainer?: HTMLElement; | ||||||
|
|
||||||
| private followupsContainer!: HTMLElement; | ||||||
|
|
@@ -1726,6 +1733,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge | |||||
| // This isolates chatSessionOption.* context keys to this specific chat input instance | ||||||
| this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.container)); | ||||||
|
|
||||||
| // Create the resize sash for the input part | ||||||
| this._createSash(this.container); | ||||||
|
|
||||||
| this.followupsContainer = elements.followupsContainer; | ||||||
| const inputAndSideToolbar = elements.inputAndSideToolbar; // The chat input and toolbar to the right | ||||||
| const inputContainer = elements.inputContainer; // The chat editor, attachments, and toolbars | ||||||
|
|
@@ -1829,7 +1839,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge | |||||
| })); | ||||||
|
|
||||||
| this._register(this._inputEditor.onDidChangeModelContent(() => { | ||||||
| const currentHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight); | ||||||
| const contentHeight = Math.min(this._inputEditor.getContentHeight(), this._getMaxAllowedHeight()); | ||||||
| const currentHeight = Math.max(contentHeight, this._getEffectiveMinHeight()); | ||||||
| if (currentHeight !== this.inputEditorHeight) { | ||||||
| this.inputEditorHeight = currentHeight; | ||||||
| // Directly update editor layout - ResizeObserver will notify parent about height change | ||||||
|
|
@@ -2647,9 +2658,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge | |||||
| /** | ||||||
| * Layout the input part with the given width. Height is intrinsic - determined by content | ||||||
| * and detected via ResizeObserver, which updates `inputPartHeight` for the parent to observe. | ||||||
| * @param width The width of the input part | ||||||
| * @param containerHeight The total height available in the chat widget container, used to constrain sash resizing | ||||||
| */ | ||||||
| layout(width: number) { | ||||||
| layout(width: number, containerHeight?: number) { | ||||||
| this.cachedWidth = width; | ||||||
| if (containerHeight !== undefined) { | ||||||
| this._containerHeight = containerHeight; | ||||||
| } | ||||||
|
|
||||||
| return this._layout(width); | ||||||
| } | ||||||
|
|
@@ -2663,7 +2679,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge | |||||
|
|
||||||
| const initialEditorScrollWidth = this._inputEditor.getScrollWidth(); | ||||||
| const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.inputPartHorizontalPaddingInside - data.toolbarsWidth - data.sideToolbarWidth; | ||||||
| const inputEditorHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight); | ||||||
| const contentHeight = Math.min(this._inputEditor.getContentHeight(), this._getMaxAllowedHeight()); | ||||||
| const inputEditorHeight = Math.max(contentHeight, this._getEffectiveMinHeight()); | ||||||
| const newDimension = { width: newEditorWidth, height: inputEditorHeight }; | ||||||
| if (!this.previousInputEditorDimension || (this.previousInputEditorDimension.width !== newDimension.width || this.previousInputEditorDimension.height !== newDimension.height)) { | ||||||
| // This layout call has side-effects that are hard to understand. eg if we are calling this inside a onDidChangeContent handler, this can trigger the next onDidChangeContent handler | ||||||
|
|
@@ -2708,6 +2725,65 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge | |||||
| sideToolbarWidth: inputSideToolbarWidth > 0 ? inputSideToolbarWidth + 4 /*gap*/ : 0, | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| private _createSash(container: HTMLElement): void { | ||||||
| const layoutProvider: IHorizontalSashLayoutProvider = { | ||||||
| getHorizontalSashTop: () => 0 | ||||||
| }; | ||||||
|
|
||||||
| this._sash = this._register(new Sash(container, layoutProvider, { orientation: Orientation.HORIZONTAL })); | ||||||
|
|
||||||
| this._register(this._sash.onDidStart(() => { | ||||||
| this._sashStartHeight = this._getEffectiveMinHeight(); | ||||||
|
||||||
| this._sashStartHeight = this._getEffectiveMinHeight(); | |
| this._sashStartHeight = this.inputEditorHeight; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| /*--------------------------------------------------------------------------------------------- | ||
| * 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'; | ||
|
|
||
| /** | ||
| * Unit tests for the sash resize logic used in ChatInputPart. | ||
| * These tests verify the mathematical constraints for editor height resizing. | ||
| * | ||
| * The actual sash creation and DOM integration is tested through manual testing | ||
| * since ChatInputPart has many dependencies that make integration testing complex. | ||
| */ | ||
| suite('ChatInputPart Sash Resize Logic', () => { | ||
|
|
||
| ensureNoDisposablesAreLeakedInTestSuite(); | ||
|
|
||
| const INPUT_EDITOR_MAX_HEIGHT = 250; | ||
| const INPUT_EDITOR_MIN_HEIGHT = 50; | ||
|
|
||
| /** | ||
| * Simulates the height calculation logic used in the sash's onDidChange handler. | ||
| */ | ||
| function calculateNewHeight(startHeight: number, startY: number, currentY: number, maxAllowed: number): number { | ||
| const delta = startY - currentY; | ||
| return Math.max( | ||
| INPUT_EDITOR_MIN_HEIGHT, | ||
| Math.min(startHeight + delta, maxAllowed) | ||
| ); | ||
| } | ||
|
|
||
| test('dragging up increases editor height', () => { | ||
| const startHeight = INPUT_EDITOR_MAX_HEIGHT; | ||
| const startY = 200; | ||
| const currentY = 150; // Drag up by 50px | ||
| const maxAllowed = 750; | ||
|
|
||
| const newHeight = calculateNewHeight(startHeight, startY, currentY, maxAllowed); | ||
| assert.strictEqual(newHeight, 300, 'Dragging up by 50px should increase height by 50'); | ||
| }); | ||
|
|
||
| test('dragging down decreases editor height', () => { | ||
| const startHeight = INPUT_EDITOR_MAX_HEIGHT; | ||
| const startY = 200; | ||
| const currentY = 230; // Drag down by 30px | ||
| const maxAllowed = 750; | ||
|
|
||
| const newHeight = calculateNewHeight(startHeight, startY, currentY, maxAllowed); | ||
| assert.strictEqual(newHeight, 220, 'Dragging down by 30px should decrease height by 30'); | ||
| }); | ||
|
|
||
| test('height respects minimum constraint', () => { | ||
| const startHeight = INPUT_EDITOR_MAX_HEIGHT; | ||
| const startY = 200; | ||
| const currentY = 500; // Drag down a lot | ||
| const maxAllowed = 750; | ||
|
|
||
| const newHeight = calculateNewHeight(startHeight, startY, currentY, maxAllowed); | ||
| assert.strictEqual(newHeight, INPUT_EDITOR_MIN_HEIGHT, 'Height should not go below minimum'); | ||
| }); | ||
|
|
||
| test('height respects maximum constraint', () => { | ||
| const startHeight = INPUT_EDITOR_MAX_HEIGHT; | ||
| const startY = 200; | ||
| const currentY = -400; // Drag up a lot | ||
| const maxAllowed = 500; | ||
|
|
||
| const newHeight = calculateNewHeight(startHeight, startY, currentY, maxAllowed); | ||
| assert.strictEqual(newHeight, maxAllowed, 'Height should not exceed maximum allowed'); | ||
| }); | ||
|
|
||
| test('no change when currentY equals startY', () => { | ||
| const startHeight = INPUT_EDITOR_MAX_HEIGHT; | ||
| const startY = 200; | ||
| const currentY = 200; // No movement | ||
| const maxAllowed = 750; | ||
|
|
||
| const newHeight = calculateNewHeight(startHeight, startY, currentY, maxAllowed); | ||
| assert.strictEqual(newHeight, startHeight, 'Height should remain unchanged when no drag occurs'); | ||
| }); | ||
|
|
||
| /** | ||
| * Simulates the editor height calculation used in _layout. | ||
| * The editor height should be the content height (capped at max allowed), | ||
| * but at least the user-set minimum height from sash dragging. | ||
| */ | ||
| function calculateEditorHeight(contentHeight: number, maxAllowed: number, userSetMinHeight: number | undefined): number { | ||
| const cappedContent = Math.min(contentHeight, maxAllowed); | ||
| const effectiveMin = userSetMinHeight ?? 0; | ||
| return Math.max(cappedContent, effectiveMin); | ||
| } | ||
|
|
||
| test('editor height respects user-set minimum from sash', () => { | ||
| const contentHeight = 100; // Small content | ||
| const maxAllowed = 750; | ||
| const userSetMinHeight = 300; // User dragged sash to make it taller | ||
|
|
||
| const editorHeight = calculateEditorHeight(contentHeight, maxAllowed, userSetMinHeight); | ||
| assert.strictEqual(editorHeight, userSetMinHeight, 'Editor should be at least as tall as user-set minimum'); | ||
| }); | ||
|
|
||
| test('editor height grows with content beyond user-set minimum', () => { | ||
| const contentHeight = 400; // Large content | ||
| const maxAllowed = 750; | ||
| const userSetMinHeight = 200; // User set a smaller minimum | ||
|
|
||
| const editorHeight = calculateEditorHeight(contentHeight, maxAllowed, userSetMinHeight); | ||
| assert.strictEqual(editorHeight, contentHeight, 'Editor should grow with content when larger than minimum'); | ||
| }); | ||
|
|
||
| test('editor height respects max allowed even with large content', () => { | ||
| const contentHeight = 1000; // Very large content | ||
| const maxAllowed = 750; | ||
| const userSetMinHeight = 200; | ||
|
|
||
| const editorHeight = calculateEditorHeight(contentHeight, maxAllowed, userSetMinHeight); | ||
| assert.strictEqual(editorHeight, maxAllowed, 'Editor should not exceed max allowed'); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The height calculation logic needs to be consistent. While the onDidChangeModelContent handler now correctly applies constraints using
_getMaxAllowedHeight()and_getEffectiveMinHeight(), the onDidContentSizeChange handler at line 1860 (outside this diff) sets height to e.contentHeight directly without these constraints. This could cause inconsistent behavior between the two handlers.