Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts
5240 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 { 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/widget/chatWidget.js';23import { ChatMode } from '../../chat/common/chatModes.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 _ctxCursorPosition: IContextKey<'above' | 'below' | ''>;46private _dimension?: Dimension;47private notebookEditor?: INotebookEditor;4849constructor(50location: IChatWidgetLocationOptions,51options: IChatWidgetViewOptions | undefined,52editors: { editor: ICodeEditor; notebookEditor?: INotebookEditor },53/** @deprecated should go away with inline2 */54clearDelegate: () => Promise<void>,55@IInstantiationService private readonly _instaService: IInstantiationService,56@ILogService private _logService: ILogService,57@IContextKeyService contextKeyService: IContextKeyService,58) {59super(editors.editor, InlineChatZoneWidget._options);60this.notebookEditor = editors.notebookEditor;6162this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService);6364this._disposables.add(toDisposable(() => {65this._ctxCursorPosition.reset();66}));6768this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, {69statusMenuId: {70menu: MENU_INLINE_CHAT_WIDGET_STATUS,71options: {72buttonConfigProvider: (action, index) => {73const isSecondary = index > 0;74if (new Set([ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, ACTION_REPORT_ISSUE]).has(action.id)) {75return { isSecondary, showIcon: true, showLabel: false };76} else {77return { isSecondary };78}79}80}81},82secondaryMenuId: MENU_INLINE_CHAT_WIDGET_SECONDARY,83inZoneWidget: true,84chatWidgetViewOptions: {85menus: {86telemetrySource: 'interactiveEditorWidget-toolbar',87inputSideToolbar: MENU_INLINE_CHAT_SIDE88},89clear: clearDelegate,90...options,91rendererOptions: {92renderTextEditsAsSummary: (uri) => {93// render when dealing with the current file in the editor94return isEqual(uri, editors.editor.getModel()?.uri);95},96renderDetectedCommandsWithRequest: true,97...options?.rendererOptions98},99defaultMode: ChatMode.Ask100}101});102this._disposables.add(this.widget);103104let revealFn: (() => void) | undefined;105this._disposables.add(this.widget.chatWidget.onWillMaybeChangeHeight(() => {106if (this.position) {107revealFn = this._createZoneAndScrollRestoreFn(this.position);108}109}));110this._disposables.add(this.widget.onDidChangeHeight(() => {111if (this.position && !this._usesResizeHeight) {112// only relayout when visible113revealFn ??= this._createZoneAndScrollRestoreFn(this.position);114const height = this._computeHeight();115this._relayout(height.linesValue);116revealFn?.();117revealFn = undefined;118}119}));120121this.create();122123this._disposables.add(autorun(r => {124const isBusy = this.widget.requestInProgress.read(r);125this.domNode.firstElementChild?.classList.toggle('busy', isBusy);126}));127128this._disposables.add(addDisposableListener(this.domNode, 'click', e => {129if (!this.editor.hasWidgetFocus() && !this.widget.hasFocus()) {130this.editor.focus();131}132}, true));133134135// todo@jrieken listen ONLY when showing136const updateCursorIsAboveContextKey = () => {137if (!this.position || !this.editor.hasModel()) {138this._ctxCursorPosition.reset();139} else if (this.position.lineNumber === this.editor.getPosition().lineNumber) {140this._ctxCursorPosition.set('above');141} else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) {142this._ctxCursorPosition.set('below');143} else {144this._ctxCursorPosition.reset();145}146};147this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey()));148this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey()));149updateCursorIsAboveContextKey();150}151152protected override _fillContainer(container: HTMLElement): void {153154container.style.setProperty('--vscode-inlineChat-background', 'var(--vscode-editor-background)');155156container.appendChild(this.widget.domNode);157}158159protected override _doLayout(heightInPixel: number): void {160161this._updatePadding();162163const info = this.editor.getLayoutInfo();164const width = info.contentWidth - info.verticalScrollbarWidth;165// width = Math.min(850, width);166167this._dimension = new Dimension(width, heightInPixel);168this.widget.layout(this._dimension);169}170171private _computeHeight(): { linesValue: number; pixelsValue: number } {172const chatContentHeight = this.widget.contentHeight;173const editorHeight = this.notebookEditor?.getLayoutInfo().height ?? this.editor.getLayoutInfo().height;174175const contentHeight = this._decoratingElementsHeight() + Math.min(chatContentHeight, Math.max(this.widget.minHeight, editorHeight * 0.42));176const heightInLines = contentHeight / this.editor.getOption(EditorOption.lineHeight);177return { linesValue: heightInLines, pixelsValue: contentHeight };178}179180protected override _getResizeBounds(): { minLines: number; maxLines: number } {181const lineHeight = this.editor.getOption(EditorOption.lineHeight);182const decoHeight = this._decoratingElementsHeight();183184const minHeightPx = decoHeight + this.widget.minHeight;185const maxHeightPx = decoHeight + this.widget.contentHeight;186187return {188minLines: minHeightPx / lineHeight,189maxLines: maxHeightPx / lineHeight190};191}192193protected override _onWidth(_widthInPixel: number): void {194if (this._dimension) {195this._doLayout(this._dimension.height);196}197}198199override show(position: Position): void {200assertType(this.container);201202this._updatePadding();203204const revealZone = this._createZoneAndScrollRestoreFn(position);205super.show(position, this._computeHeight().linesValue);206this.widget.chatWidget.setVisible(true);207this.widget.focus();208209revealZone();210}211212private _updatePadding() {213assertType(this.container);214215const info = this.editor.getLayoutInfo();216const marginWithoutIndentation = info.glyphMarginWidth + info.lineNumbersWidth + info.decorationsWidth;217this.container.style.paddingLeft = `${marginWithoutIndentation}px`;218}219220reveal(position: Position) {221const stickyScroll = this.editor.getOption(EditorOption.stickyScroll);222const magicValue = stickyScroll.enabled ? stickyScroll.maxLineCount : 0;223this.editor.revealLines(position.lineNumber + magicValue, position.lineNumber + magicValue, ScrollType.Immediate);224this.updatePositionAndHeight(position);225}226227override updatePositionAndHeight(position: Position): void {228const revealZone = this._createZoneAndScrollRestoreFn(position);229super.updatePositionAndHeight(position, !this._usesResizeHeight ? this._computeHeight().linesValue : undefined);230revealZone();231}232233private _createZoneAndScrollRestoreFn(position: Position): () => void {234235const scrollState = StableEditorBottomScrollState.capture(this.editor);236237const lineNumber = position.lineNumber <= 1 ? 1 : 1 + position.lineNumber;238239return () => {240scrollState.restore(this.editor);241242const scrollTop = this.editor.getScrollTop();243const lineTop = this.editor.getTopForLineNumber(lineNumber);244const zoneTop = lineTop - this._computeHeight().pixelsValue;245const editorHeight = this.editor.getLayoutInfo().height;246const lineBottom = this.editor.getBottomForLineNumber(lineNumber);247248let newScrollTop = zoneTop;249let forceScrollTop = false;250251if (lineBottom >= (scrollTop + editorHeight)) {252// revealing the top of the zone would push out the line we are interested in and253// therefore we keep the line in the viewport254newScrollTop = lineBottom - editorHeight;255forceScrollTop = true;256}257258if (newScrollTop < scrollTop || forceScrollTop) {259this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop });260this.editor.setScrollTop(newScrollTop, ScrollType.Immediate);261}262};263}264265protected override revealRange(range: Range, isLastLine: boolean): void {266// noop267}268269override hide(): void {270const scrollState = StableEditorBottomScrollState.capture(this.editor);271this._ctxCursorPosition.reset();272this.widget.chatWidget.setVisible(false);273super.hide();274aria.status(localize('inlineChatClosed', 'Closed inline chat widget'));275scrollState.restore(this.editor);276}277}278279280