diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts index e256be7a65c..9585c9972d0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -51,15 +51,15 @@ export class InlineChatInputWidget extends Disposable { private readonly _input: IActiveCodeEditor; private readonly _position = observableValue(this, null); readonly position: IObservable = this._position; - readonly minContentWidthInPx = constObservable(0); + private readonly _showStore = this._store.add(new DisposableStore()); + private readonly _stickyScrollHeight: IObservable; private _inlineStartAction: IAction | undefined; private _anchorLineNumber: number = 0; private _anchorLeft: number = 0; private _anchorAbove: boolean = false; - readonly allowEditorOverflow = true; constructor( private readonly _editorObs: ObservableCodeEditor, @@ -108,6 +108,10 @@ export class InlineChatInputWidget extends Disposable { this._input.setModel(model); this._input.layout({ width: 200, height: 18 }); + // Initialize sticky scroll height observable + const stickyScrollController = StickyScrollController.get(this._editorObs.editor); + this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); + // Update placeholder based on selection state this._store.add(autorun(r => { const selection = this._editorObs.cursorSelection.read(r); @@ -216,8 +220,8 @@ export class InlineChatInputWidget extends Disposable { this._showStore.add(this._editorObs.createOverlayWidget({ domNode: this._domNode, position: this._position, - minContentWidthInPx: this.minContentWidthInPx, - allowEditorOverflow: this.allowEditorOverflow, + minContentWidthInPx: constObservable(0), + allowEditorOverflow: true, })); // If anchoring above, adjust position after render to account for widget height @@ -225,13 +229,14 @@ export class InlineChatInputWidget extends Disposable { this._updatePosition(); } - // Update position on scroll, hide if anchor line is out of view + // Update position on scroll, hide if anchor line is out of view (only when input is empty) this._showStore.add(this._editorObs.editor.onDidScrollChange(() => { const visibleRanges = this._editorObs.editor.getVisibleRanges(); const isLineVisible = visibleRanges.some(range => this._anchorLineNumber >= range.startLineNumber && this._anchorLineNumber <= range.endLineNumber ); - if (!isLineVisible) { + const hasContent = !!this._input.getModel().getValue(); + if (!isLineVisible && !hasContent) { this._hide(); } else { this._updatePosition(); @@ -244,6 +249,7 @@ export class InlineChatInputWidget extends Disposable { private _updatePosition(): void { const editor = this._editorObs.editor; + const lineHeight = editor.getOption(EditorOption.lineHeight); const top = editor.getTopForLineNumber(this._anchorLineNumber) - editor.getScrollTop(); let adjustedTop = top; @@ -251,12 +257,22 @@ export class InlineChatInputWidget extends Disposable { const widgetHeight = this._domNode.offsetHeight; adjustedTop = top - widgetHeight; } else { - const lineHeight = editor.getOption(EditorOption.lineHeight); adjustedTop = top + lineHeight; } + // Clamp to viewport bounds when anchor line is out of view + const stickyScrollHeight = this._stickyScrollHeight.get(); + const layoutInfo = editor.getLayoutInfo(); + const widgetHeight = this._domNode.offsetHeight; + const minTop = stickyScrollHeight; + const maxTop = layoutInfo.height - widgetHeight; + + const clampedTop = Math.max(minTop, Math.min(adjustedTop, maxTop)); + const isClamped = clampedTop !== adjustedTop; + this._domNode.classList.toggle('clamped', isClamped); + this._position.set({ - preference: { top: adjustedTop, left: this._anchorLeft }, + preference: { top: clampedTop, left: this._anchorLeft }, stackOrdinal: 10000, }, undefined); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css index 864a19e8ede..1af3ff339a1 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +.inline-chat-gutter-menu.clamped { + transition: top 100ms; +} + .inline-chat-gutter-menu .input .monaco-editor-background { background-color: var(--vscode-menu-background); }