Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import { addDisposableListener, Dimension } from '../../../../base/browser/dom.js';5import * as aria from '../../../../base/browser/ui/aria/aria.js';6import { MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';7import { autorun } from '../../../../base/common/observable.js';8import { isEqual } from '../../../../base/common/resources.js';9import { assertType } from '../../../../base/common/types.js';10import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';11import { StableEditorBottomScrollState } from '../../../../editor/browser/stableEditorScroll.js';12import { EditorOption } from '../../../../editor/common/config/editorOptions.js';13import { Position } from '../../../../editor/common/core/position.js';14import { Range } from '../../../../editor/common/core/range.js';15import { ScrollType } from '../../../../editor/common/editorCommon.js';16import { IOptions, ZoneWidget } from '../../../../editor/contrib/zoneWidget/browser/zoneWidget.js';17import { localize } from '../../../../nls.js';18import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';19import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';20import { ILogService } from '../../../../platform/log/common/log.js';21import { IChatWidgetViewOptions } from '../../chat/browser/chat.js';22import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js';23import { isResponseVM } from '../../chat/common/chatViewModel.js';24import { INotebookEditor } from '../../notebook/browser/notebookBrowser.js';25import { ACTION_REGENERATE_RESPONSE, ACTION_REPORT_ISSUE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_SIDE, MENU_INLINE_CHAT_WIDGET_SECONDARY, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js';26import { EditorBasedInlineChatWidget } from './inlineChatWidget.js';2728export class InlineChatZoneWidget extends ZoneWidget {2930private static readonly _options: IOptions = {31showFrame: true,32frameWidth: 1,33// frameColor: 'var(--vscode-inlineChat-border)',34isResizeable: true,35showArrow: false,36isAccessible: true,37className: 'inline-chat-widget',38keepEditorSelection: true,39showInHiddenAreas: true,40ordinal: 50000,41};4243readonly widget: EditorBasedInlineChatWidget;4445private readonly _scrollUp = this._disposables.add(new ScrollUpState(this.editor));46private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>;47private _dimension?: Dimension;48private notebookEditor?: INotebookEditor;4950constructor(51location: IChatWidgetLocationOptions,52options: IChatWidgetViewOptions | undefined,53editors: { editor: ICodeEditor; notebookEditor?: INotebookEditor },54@IInstantiationService private readonly _instaService: IInstantiationService,55@ILogService private _logService: ILogService,56@IContextKeyService contextKeyService: IContextKeyService,57) {58super(editors.editor, InlineChatZoneWidget._options);59this.notebookEditor = editors.notebookEditor;6061this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService);6263this._disposables.add(toDisposable(() => {64this._ctxCursorPosition.reset();65}));6667this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, {68statusMenuId: {69menu: MENU_INLINE_CHAT_WIDGET_STATUS,70options: {71buttonConfigProvider: (action, index) => {72const isSecondary = index > 0;73if (new Set([ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, ACTION_REPORT_ISSUE]).has(action.id)) {74return { isSecondary, showIcon: true, showLabel: false };75} else {76return { isSecondary };77}78}79}80},81secondaryMenuId: MENU_INLINE_CHAT_WIDGET_SECONDARY,82inZoneWidget: true,83chatWidgetViewOptions: {84menus: {85telemetrySource: 'interactiveEditorWidget-toolbar',86inputSideToolbar: MENU_INLINE_CHAT_SIDE87},88...options,89rendererOptions: {90renderTextEditsAsSummary: (uri) => {91// render when dealing with the current file in the editor92return isEqual(uri, editors.editor.getModel()?.uri);93},94renderDetectedCommandsWithRequest: true,95...options?.rendererOptions96},97}98});99this._disposables.add(this.widget);100101let revealFn: (() => void) | undefined;102this._disposables.add(this.widget.chatWidget.onWillMaybeChangeHeight(() => {103if (this.position) {104revealFn = this._createZoneAndScrollRestoreFn(this.position);105}106}));107this._disposables.add(this.widget.onDidChangeHeight(() => {108if (this.position && !this._usesResizeHeight) {109// only relayout when visible110revealFn ??= this._createZoneAndScrollRestoreFn(this.position);111const height = this._computeHeight();112this._relayout(height.linesValue);113revealFn?.();114revealFn = undefined;115}116}));117118this.create();119120this._disposables.add(autorun(r => {121const isBusy = this.widget.requestInProgress.read(r);122this.domNode.firstElementChild?.classList.toggle('busy', isBusy);123}));124125this._disposables.add(addDisposableListener(this.domNode, 'click', e => {126if (!this.editor.hasWidgetFocus() && !this.widget.hasFocus()) {127this.editor.focus();128}129}, true));130131132// todo@jrieken listen ONLY when showing133const updateCursorIsAboveContextKey = () => {134if (!this.position || !this.editor.hasModel()) {135this._ctxCursorPosition.reset();136} else if (this.position.lineNumber === this.editor.getPosition().lineNumber) {137this._ctxCursorPosition.set('above');138} else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) {139this._ctxCursorPosition.set('below');140} else {141this._ctxCursorPosition.reset();142}143};144this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey()));145this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey()));146updateCursorIsAboveContextKey();147}148149protected override _fillContainer(container: HTMLElement): void {150151container.style.setProperty('--vscode-inlineChat-background', 'var(--vscode-editor-background)');152153container.appendChild(this.widget.domNode);154}155156protected override _doLayout(heightInPixel: number): void {157158this._updatePadding();159160const info = this.editor.getLayoutInfo();161const width = info.contentWidth - info.verticalScrollbarWidth;162// width = Math.min(850, width);163164this._dimension = new Dimension(width, heightInPixel);165this.widget.layout(this._dimension);166}167168private _computeHeight(): { linesValue: number; pixelsValue: number } {169const chatContentHeight = this.widget.contentHeight;170const editorHeight = this.notebookEditor?.getLayoutInfo().height ?? this.editor.getLayoutInfo().height;171172const contentHeight = this._decoratingElementsHeight() + Math.min(chatContentHeight, Math.max(this.widget.minHeight, editorHeight * 0.42));173const heightInLines = contentHeight / this.editor.getOption(EditorOption.lineHeight);174return { linesValue: heightInLines, pixelsValue: contentHeight };175}176177protected override _getResizeBounds(): { minLines: number; maxLines: number } {178const lineHeight = this.editor.getOption(EditorOption.lineHeight);179const decoHeight = this._decoratingElementsHeight();180181const minHeightPx = decoHeight + this.widget.minHeight;182const maxHeightPx = decoHeight + this.widget.contentHeight;183184return {185minLines: minHeightPx / lineHeight,186maxLines: maxHeightPx / lineHeight187};188}189190protected override _onWidth(_widthInPixel: number): void {191if (this._dimension) {192this._doLayout(this._dimension.height);193}194}195196override show(position: Position): void {197assertType(this.container);198199this._updatePadding();200201const revealZone = this._createZoneAndScrollRestoreFn(position);202super.show(position, this._computeHeight().linesValue);203this.widget.chatWidget.setVisible(true);204this.widget.focus();205206revealZone();207this._scrollUp.enable();208}209210private _updatePadding() {211assertType(this.container);212213const info = this.editor.getLayoutInfo();214const marginWithoutIndentation = info.glyphMarginWidth + info.lineNumbersWidth + info.decorationsWidth;215this.container.style.paddingLeft = `${marginWithoutIndentation}px`;216}217218reveal(position: Position) {219const stickyScroll = this.editor.getOption(EditorOption.stickyScroll);220const magicValue = stickyScroll.enabled ? stickyScroll.maxLineCount : 0;221this.editor.revealLines(position.lineNumber + magicValue, position.lineNumber + magicValue, ScrollType.Immediate);222this._scrollUp.reset();223this.updatePositionAndHeight(position);224}225226override updatePositionAndHeight(position: Position): void {227const revealZone = this._createZoneAndScrollRestoreFn(position);228super.updatePositionAndHeight(position, !this._usesResizeHeight ? this._computeHeight().linesValue : undefined);229revealZone();230}231232private _createZoneAndScrollRestoreFn(position: Position): () => void {233234const scrollState = StableEditorBottomScrollState.capture(this.editor);235236const lineNumber = position.lineNumber <= 1 ? 1 : 1 + position.lineNumber;237const scrollTop = this.editor.getScrollTop();238const lineTop = this.editor.getTopForLineNumber(lineNumber);239const zoneTop = lineTop - this._computeHeight().pixelsValue;240241const hasResponse = this.widget.chatWidget.viewModel?.getItems().find(candidate => {242return isResponseVM(candidate) && candidate.response.value.length > 0;243});244245if (hasResponse && zoneTop < scrollTop || this._scrollUp.didScrollUpOrDown) {246// don't reveal the zone if it is already out of view (unless we are still getting ready)247// or if an outside scroll-up happened (e.g the user scrolled up/down to see the new content)248return this._scrollUp.runIgnored(() => {249scrollState.restore(this.editor);250});251}252253return this._scrollUp.runIgnored(() => {254scrollState.restore(this.editor);255256const scrollTop = this.editor.getScrollTop();257const lineTop = this.editor.getTopForLineNumber(lineNumber);258const zoneTop = lineTop - this._computeHeight().pixelsValue;259const editorHeight = this.editor.getLayoutInfo().height;260const lineBottom = this.editor.getBottomForLineNumber(lineNumber);261262let newScrollTop = zoneTop;263let forceScrollTop = false;264265if (lineBottom >= (scrollTop + editorHeight)) {266// revealing the top of the zone would push out the line we are interested in and267// therefore we keep the line in the viewport268newScrollTop = lineBottom - editorHeight;269forceScrollTop = true;270}271272if (newScrollTop < scrollTop || forceScrollTop) {273this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop });274this.editor.setScrollTop(newScrollTop, ScrollType.Immediate);275}276});277}278279protected override revealRange(range: Range, isLastLine: boolean): void {280// noop281}282283override hide(): void {284const scrollState = StableEditorBottomScrollState.capture(this.editor);285this._scrollUp.disable();286this._ctxCursorPosition.reset();287this.widget.reset();288this.widget.chatWidget.setVisible(false);289super.hide();290aria.status(localize('inlineChatClosed', 'Closed inline chat widget'));291scrollState.restore(this.editor);292}293}294295class ScrollUpState {296297private _didScrollUpOrDown?: boolean;298private _ignoreEvents = false;299300private readonly _listener = new MutableDisposable();301302constructor(private readonly _editor: ICodeEditor) { }303304dispose(): void {305this._listener.dispose();306}307308reset(): void {309this._didScrollUpOrDown = undefined;310}311312enable(): void {313this._didScrollUpOrDown = undefined;314this._listener.value = this._editor.onDidScrollChange(e => {315if (!e.scrollTopChanged || this._ignoreEvents) {316return;317}318this._listener.clear();319this._didScrollUpOrDown = true;320});321}322323disable(): void {324this._listener.clear();325this._didScrollUpOrDown = undefined;326}327328runIgnored(callback: () => void): () => void {329return () => {330this._ignoreEvents = true;331try {332return callback();333} finally {334this._ignoreEvents = false;335}336};337}338339get didScrollUpOrDown(): boolean | undefined {340return this._didScrollUpOrDown;341}342343}344345346