Files
vscode/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts
Johannes Rieken 9757f7c73a Make "Toggle Changes" more prominent and re-run less (#208917)
- add regenerage and regenerate w/o intent detection to response title menu
- have toggle changes command with accept and discard
- add context key for chat widget location
2024-03-27 17:30:18 +01:00

200 lines
8.0 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Dimension, addDisposableListener } from 'vs/base/browser/dom';
import * as aria from 'vs/base/browser/ui/aria/aria';
import { toDisposable } from 'vs/base/common/lifecycle';
import { assertType } from 'vs/base/common/types';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorLayoutInfo, EditorOption } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { IRange } from 'vs/editor/common/core/range';
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
import { localize } from 'vs/nls';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { EditorBasedInlineChatWidget } from './inlineChatWidget';
import { MenuId } from 'vs/platform/actions/common/actions';
export class InlineChatZoneWidget extends ZoneWidget {
readonly widget: EditorBasedInlineChatWidget;
private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>;
private _dimension?: Dimension;
private _indentationWidth: number | undefined;
constructor(
editor: ICodeEditor,
@IInstantiationService private readonly _instaService: IInstantiationService,
@IContextKeyService contextKeyService: IContextKeyService
) {
super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'inline-chat-widget', keepEditorSelection: true, showInHiddenAreas: true, ordinal: 10000 });
this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService);
this._disposables.add(toDisposable(() => {
this._ctxCursorPosition.reset();
}));
this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, this.editor, {
telemetrySource: 'interactiveEditorWidget-toolbar',
inputMenuId: MenuId.ChatExecute,
widgetMenuId: MENU_INLINE_CHAT_WIDGET,
feedbackMenuId: MENU_INLINE_CHAT_WIDGET_FEEDBACK,
statusMenuId: {
menu: MENU_INLINE_CHAT_WIDGET_STATUS,
options: {
buttonConfigProvider: action => {
if (action.id === ACTION_REGENERATE_RESPONSE || action.id === ACTION_TOGGLE_DIFF) {
return { showIcon: true, showLabel: false, isSecondary: true };
} else if (action.id === ACTION_VIEW_IN_CHAT || action.id === ACTION_ACCEPT_CHANGES) {
return { isSecondary: false };
} else {
return { isSecondary: true };
}
}
}
}
});
this._disposables.add(this.widget.onDidChangeHeight(() => {
if (this.position) {
// only relayout when visible
this._relayout(this._computeHeightInLines());
}
}));
this._disposables.add(this.widget);
this.create();
this._disposables.add(addDisposableListener(this.domNode, 'click', e => {
if (!this.widget.hasFocus()) {
this.widget.focus();
}
}, true));
// todo@jrieken listen ONLY when showing
const updateCursorIsAboveContextKey = () => {
if (!this.position || !this.editor.hasModel()) {
this._ctxCursorPosition.reset();
} else if (this.position.lineNumber === this.editor.getPosition().lineNumber) {
this._ctxCursorPosition.set('above');
} else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) {
this._ctxCursorPosition.set('below');
} else {
this._ctxCursorPosition.reset();
}
};
this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey()));
this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey()));
updateCursorIsAboveContextKey();
}
protected override _fillContainer(container: HTMLElement): void {
container.appendChild(this.widget.domNode);
}
protected override _doLayout(heightInPixel: number): void {
const maxWidth = !this.widget.showsAnyPreview() ? 640 : Number.MAX_SAFE_INTEGER;
const width = Math.min(maxWidth, this._availableSpaceGivenIndentation(this._indentationWidth));
this._dimension = new Dimension(width, heightInPixel);
this.widget.layout(this._dimension);
}
private _availableSpaceGivenIndentation(indentationWidth: number | undefined): number {
const info = this.editor.getLayoutInfo();
return info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth + (indentationWidth ?? 0));
}
private _computeHeightInLines(): number {
const chatContentHeight = this.widget.contentHeight;
const editorHeight = this.editor.getLayoutInfo().height;
const contentHeight = Math.min(chatContentHeight, Math.max(this.widget.minHeight, editorHeight * 0.42));
const heightInLines = contentHeight / this.editor.getOption(EditorOption.lineHeight);
return heightInLines;
}
protected override _onWidth(_widthInPixel: number): void {
if (this._dimension) {
this._doLayout(this._dimension.height);
}
}
override show(position: Position): void {
assertType(this.container);
const info = this.editor.getLayoutInfo();
const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth;
this.container.style.marginLeft = `${marginWithoutIndentation}px`;
super.show(position, this._computeHeightInLines());
this._setWidgetMargins(position);
this.widget.focus();
}
override updatePositionAndHeight(position: Position): void {
super.updatePositionAndHeight(position, this._computeHeightInLines());
this._setWidgetMargins(position);
}
protected override _getWidth(info: EditorLayoutInfo): number {
return info.width - info.minimap.minimapWidth;
}
updateBackgroundColor(newPosition: Position, wholeRange: IRange) {
assertType(this.container);
const widgetLineNumber = newPosition.lineNumber;
this.container.classList.toggle('inside-selection', widgetLineNumber > wholeRange.startLineNumber && widgetLineNumber < wholeRange.endLineNumber);
}
private _calculateIndentationWidth(position: Position): number {
const viewModel = this.editor._getViewModel();
if (!viewModel) {
return 0;
}
const visibleRange = viewModel.getCompletelyVisibleViewRange();
if (!visibleRange.containsPosition(position)) {
// this is needed because `getOffsetForColumn` won't work when the position
// isn't visible/rendered
return 0;
}
let indentationLevel = viewModel.getLineFirstNonWhitespaceColumn(position.lineNumber);
let indentationLineNumber = position.lineNumber;
for (let lineNumber = position.lineNumber; lineNumber >= visibleRange.startLineNumber; lineNumber--) {
const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber);
if (currentIndentationLevel !== 0) {
indentationLineNumber = lineNumber;
indentationLevel = currentIndentationLevel;
break;
}
}
return Math.max(0, this.editor.getOffsetForColumn(indentationLineNumber, indentationLevel)); // double-guard against invalie getOffsetForColumn-calls
}
private _setWidgetMargins(position: Position): void {
const indentationWidth = this._calculateIndentationWidth(position);
if (this._indentationWidth === indentationWidth) {
return;
}
this._indentationWidth = this._availableSpaceGivenIndentation(indentationWidth) > 400 ? indentationWidth : 0;
this.widget.domNode.style.marginLeft = `${this._indentationWidth}px`;
this.widget.domNode.style.marginRight = `${this.editor.getLayoutInfo().minimap.minimapWidth}px`;
}
override hide(): void {
this.container!.classList.remove('inside-selection');
this._ctxCursorPosition.reset();
this.widget.reset();
super.hide();
aria.status(localize('inlineChatClosed', 'Closed inline chat widget'));
}
}