mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-23 10:08:49 +01:00
- 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
200 lines
8.0 KiB
TypeScript
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'));
|
|
}
|
|
}
|