Inline chat widget tweaks (#206954)

* make status menu configurable

https://github.com/microsoft/vscode/issues/206940

* pull inline chat zone widget into its own file

* 💄

* remove expand/crop feature

* make chat message contents scrollable
This commit is contained in:
Johannes Rieken 2024-03-06 12:51:50 +01:00 committed by GitHub
parent 982998a94c
commit 9626fb0cd2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 267 additions and 360 deletions

View file

@ -790,8 +790,6 @@
"--vscode-hover-maxWidth",
"--vscode-hover-sourceWhiteSpace",
"--vscode-hover-whiteSpace",
"--vscode-inline-chat-cropped",
"--vscode-inline-chat-expanded",
"--vscode-inline-chat-quick-voice-height",
"--vscode-inline-chat-quick-voice-width",
"--vscode-editor-dictation-widget-height",

View file

@ -51,8 +51,6 @@ registerAction2(InlineChatActions.FocusInlineChat);
registerAction2(InlineChatActions.PreviousFromHistory);
registerAction2(InlineChatActions.NextFromHistory);
registerAction2(InlineChatActions.ViewInChatAction);
registerAction2(InlineChatActions.ExpandMessageAction);
registerAction2(InlineChatActions.ContractMessageAction);
registerAction2(InlineChatActions.ToggleDiffForChange);
registerAction2(InlineChatActions.FeebackHelpfulCommand);

View file

@ -175,37 +175,13 @@
cursor: pointer;
}
/* .monaco-editor .inline-chat .markdownMessage .message * {
margin: unset;
}
.monaco-editor .inline-chat .markdownMessage .message code {
font-family: var(--monaco-monospace-font);
font-size: 12px;
color: var(--vscode-textPreformat-foreground);
background-color: var(--vscode-textPreformat-background);
padding: 1px 3px;
border-radius: 4px;
} */
.monaco-editor .inline-chat .chatMessage .chatMessageContent .value {
-webkit-line-clamp: initial;
-webkit-box-orient: vertical;
overflow: hidden;
display: -webkit-box;
-webkit-user-select: text;
user-select: text;
}
.monaco-editor .inline-chat .chatMessage .chatMessageContent[state="cropped"] .value {
-webkit-line-clamp: var(--vscode-inline-chat-cropped, 3);
}
.monaco-editor .inline-chat .chatMessage .chatMessageContent[state="expanded"] .value {
-webkit-line-clamp: var(--vscode-inline-chat-expanded, 10);
}
.monaco-editor .inline-chat .followUps {
padding: 5px 5px;
}

View file

@ -11,7 +11,7 @@ import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/em
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { InlineChatController, InlineChatRunOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_HAS_PROVIDER, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET_DISCARD, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_EDIT_MODE, EditMode, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, CTX_INLINE_CHAT_RESPONSE_TYPES, InlineChatResponseTypes, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { localize, localize2 } from 'vs/nls';
import { Action2, IAction2Options, MenuRegistry } from 'vs/platform/actions/common/actions';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
@ -737,46 +737,6 @@ export class ViewInChatAction extends AbstractInlineChatAction {
}
}
export class ExpandMessageAction extends AbstractInlineChatAction {
constructor() {
super({
id: 'inlineChat.expandMessageAction',
title: localize('expandMessage', 'Show More'),
icon: Codicon.chevronDown,
precondition: CTX_INLINE_CHAT_VISIBLE,
menu: {
id: MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE,
when: ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Mixed)), CTX_INLINE_CHAT_MESSAGE_CROP_STATE.isEqualTo('cropped')),
group: '2_expandOrContract',
order: 1
}
});
}
override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void {
ctrl.updateExpansionState(true);
}
}
export class ContractMessageAction extends AbstractInlineChatAction {
constructor() {
super({
id: 'inlineChat.contractMessageAction',
title: localize('contractMessage', 'Show Less'),
icon: Codicon.chevronUp,
precondition: CTX_INLINE_CHAT_VISIBLE,
menu: {
id: MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE,
when: ContextKeyExpr.and(ContextKeyExpr.or(CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.OnlyMessages), CTX_INLINE_CHAT_RESPONSE_TYPES.isEqualTo(InlineChatResponseTypes.Mixed)), CTX_INLINE_CHAT_MESSAGE_CROP_STATE.isEqualTo('expanded')),
group: '2_expandOrContract',
order: 1
}
});
}
override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void {
ctrl.updateExpansionState(false);
}
}
export class InlineAccessibilityHelpContribution extends Disposable {
constructor() {
super();

View file

@ -39,10 +39,11 @@ import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IInlineChatSavingService } from './inlineChatSavingService';
import { EmptyResponse, ErrorResponse, ExpansionState, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionExchange, SessionPrompt } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { IInlineChatSessionService } from './inlineChatSessionService';
import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies';
import { IInlineChatMessageAppender, InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget';
import { IInlineChatMessageAppender } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget';
import { InlineChatZoneWidget } from './inlineChatZoneWidget';
import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { StashedSession } from './inlineChatSession';
@ -379,7 +380,6 @@ export class InlineChatController implements IEditorContribution {
this._zone.value.widget.updateSlashCommands(this._session.session.slashCommands ?? []);
this._updatePlaceholder();
this._zone.value.widget.updateInfo(this._session.session.message ?? localize('welcome.1', "AI-generated code may be incorrect"));
this._zone.value.widget.preferredExpansionState = this._session.lastExpansionState;
this._zone.value.widget.value = this._session.session.input ?? this._session.lastInput?.value ?? this._zone.value.widget.value;
if (this._session.session.input) {
this._zone.value.widget.selectAll();
@ -795,8 +795,6 @@ export class InlineChatController implements IEditorContribution {
const message = { message: response.mdContent, providerId: this._session.provider.debugName, requestId: response.requestId };
this._zone.value.widget.updateChatMessage(message);
//this._zone.value.widget.updateMarkdownMessage(response.mdContent);
this._session.lastExpansionState = this._zone.value.widget.expansionState;
this._zone.value.widget.updateToolbar(true);
newPosition = await this._strategy.renderChanges(response);
@ -1088,14 +1086,6 @@ export class InlineChatController implements IEditorContribution {
}
}
updateExpansionState(expand: boolean) {
if (this._session) {
const expansionState = expand ? ExpansionState.EXPANDED : ExpansionState.CROPPED;
this._zone.value.widget.updateChatMessageExpansionState(expansionState);
this._session.lastExpansionState = expansionState;
}
}
toggleDiff() {
this._strategy?.toggleDiff?.();
}

View file

@ -67,11 +67,6 @@ export type TelemetryDataClassification = {
responseTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Comma separated list of response types like edits, message, mixed' };
};
export enum ExpansionState {
EXPANDED = 'expanded',
CROPPED = 'cropped',
NOT_CROPPED = 'not_cropped'
}
export class SessionWholeRange {
@ -142,7 +137,6 @@ export class SessionWholeRange {
export class Session {
private _lastInput: SessionPrompt | undefined;
private _lastExpansionState: ExpansionState | undefined;
private _isUnstashed: boolean = false;
private readonly _exchange: SessionExchange[] = [];
private readonly _startTime = new Date();
@ -204,14 +198,6 @@ export class Session {
this._isUnstashed = true;
}
get lastExpansionState(): ExpansionState | undefined {
return this._lastExpansionState;
}
set lastExpansionState(state: ExpansionState) {
this._lastExpansionState = state;
}
get textModelNSnapshotAltVersion(): number | undefined {
return this._textModelNSnapshotAltVersion;
}

View file

@ -30,7 +30,7 @@ import { SaveReason } from 'vs/workbench/common/editor';
import { countWords } from 'vs/workbench/contrib/chat/common/chatWordCounter';
import { InlineChatFileCreatePreviewWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatFileCreationWidget';
import { HunkInformation, ReplyResponse, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget';
import { InlineChatZoneWidget } from './inlineChatZoneWidget';
import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, overviewRulerInlineChatDiffInserted } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { HunkState } from './inlineChatSession';
import { assertType } from 'vs/base/common/types';

View file

@ -13,7 +13,6 @@ import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { Lazy } from 'vs/base/common/lazy';
import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ISettableObservable, constObservable, derived, observableValue } from 'vs/base/common/observable';
import { assertType } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
import 'vs/css!./inlineChat';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
@ -23,10 +22,10 @@ import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditor/co
import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from 'vs/editor/browser/widget/diffEditor/components/accessibleDiffViewer';
import { EmbeddedDiffEditorWidget } from 'vs/editor/browser/widget/diffEditor/embeddedDiffEditorWidget';
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget';
import { EditorLayoutInfo, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions';
import { EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { Range } from 'vs/editor/common/core/range';
import { DetailedLineRangeMapping, RangeMapping } from 'vs/editor/common/diff/rangeMapping';
import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon';
import { LanguageSelector } from 'vs/editor/common/languageSelector';
@ -37,7 +36,6 @@ import { IModelService } from 'vs/editor/common/services/model';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController';
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
import { localize } from 'vs/nls';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar';
@ -63,11 +61,12 @@ import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatModel, ChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { CodeBlockModelCollection } from 'vs/workbench/contrib/chat/common/codeBlockModelCollection';
import { ExpansionState, HunkData, HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { HunkData, HunkInformation, Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
import { asRange, invertLineRange } from 'vs/workbench/contrib/inlineChat/browser/utils';
import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_RESPONSE_FOCUSED, CTX_INLINE_CHAT_VISIBLE, IInlineChatFollowup, IInlineChatSlashCommand, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_RESPONSE_FOCUSED, IInlineChatFollowup, IInlineChatSlashCommand } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel';
import { createInstantHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
const defaultAriaLabel = localize('aria-label', "Inline Chat Input");
@ -137,9 +136,25 @@ export interface InlineChatWidgetViewState {
}
export interface IInlineChatWidgetConstructionOptions {
menuId: MenuId;
/**
* The telemetry source for all commands of this widget
*/
telemetrySource: string;
/**
* The menu that is inside the input editor, use for send, dictation
*/
inputMenuId: MenuId;
/**
* The menu that next to the input editor, use for close, config etc
*/
widgetMenuId: MenuId;
statusMenuId: MenuId;
/**
* The menu that rendered as button bar, use for accept, discard etc
*/
statusMenuId: MenuId | { menu: MenuId; options: IWorkbenchButtonBarOptions };
/**
* The men that rendered in the lower right corner, use for feedback
*/
feedbackMenuId: MenuId;
}
@ -177,10 +192,7 @@ export class InlineChatWidget {
h('div.previewDiff.hidden@previewDiff'),
h('div.previewCreateTitle.show-file-icons@previewCreateTitle'),
h('div.previewCreate.hidden@previewCreate'),
h('div.chatMessage.hidden@chatMessage', [
h('div.chatMessageContent@chatMessageContent'),
h('div.messageActions@messageActions')
]),
h('div.chatMessage.hidden@chatMessage'),
h('div.followUps.hidden@followUps'),
h('div.accessibleViewer@accessibleViewer'),
h('div.status@status', [
@ -192,13 +204,15 @@ export class InlineChatWidget {
]
);
private readonly _chatMessageContents: HTMLDivElement;
private readonly _chatMessageScrollable: DomScrollableElement;
private readonly _store = new DisposableStore();
private readonly _slashCommands = this._store.add(new DisposableStore());
private readonly _inputEditor: IActiveCodeEditor;
private readonly _inputModel: ITextModel;
private readonly _ctxInputEmpty: IContextKey<boolean>;
private readonly _ctxMessageCropState: IContextKey<'cropped' | 'not_cropped' | 'expanded'>;
private readonly _ctxInnerCursorFirst: IContextKey<boolean>;
private readonly _ctxInnerCursorLast: IContextKey<boolean>;
private readonly _ctxInnerCursorStart: IContextKey<boolean>;
@ -229,8 +243,6 @@ export class InlineChatWidget {
private _lastDim: Dimension | undefined;
private _isLayouting: boolean = false;
private _preferredExpansionState: ExpansionState | undefined;
private _expansionState: ExpansionState = ExpansionState.NOT_CROPPED;
private _slashCommandDetails: { command: string; detail: string }[] = [];
private _slashCommandContentWidget: SlashCommandContentWidget;
@ -244,8 +256,8 @@ export class InlineChatWidget {
private readonly _codeBlockModelCollection: CodeBlockModelCollection;
constructor(
private readonly parentEditor: ICodeEditor,
_options: IInlineChatWidgetConstructionOptions,
private readonly _parentEditor: ICodeEditor,
options: IInlineChatWidgetConstructionOptions,
@IModelService private readonly _modelService: IModelService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
@ -268,20 +280,13 @@ export class InlineChatWidget {
])
};
this._inputEditor = <IActiveCodeEditor>this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, this.parentEditor);
this._inputEditor = <IActiveCodeEditor>this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.editor, _inputEditorOptions, codeEditorWidgetOptions, this._parentEditor);
this._updateAriaLabel();
this._store.add(this._inputEditor);
this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this)));
this._store.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire()));
this._store.add(this._inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire()));
this._store.add(addDisposableListener(this._elements.chatMessageContent, 'focus', () => this._ctxResponseFocused.set(true)));
this._store.add(addDisposableListener(this._elements.chatMessageContent, 'blur', () => this._ctxResponseFocused.reset()));
this._store.add(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) {
this._updateAriaLabel();
}
}));
const uri = URI.from({ scheme: 'vscode', authority: 'inline-chat', path: `/inline-chat/model${InlineChatWidget._modelPool++}.txt` });
this._inputModel = this._store.add(this._modelService.getModel(uri) ?? this._modelService.createModel('', null, uri));
@ -289,10 +294,24 @@ export class InlineChatWidget {
this._editorOptions = this._store.add(_instantiationService.createInstance(ChatEditorOptions, undefined, editorForeground, inputBackground, editorBackground));
this._chatMessageContents = document.createElement('div');
this._chatMessageContents.className = 'chatMessageContent';
this._chatMessageContents.tabIndex = 0;
this._chatMessageContents.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat);
this._chatMessageScrollable = new DomScrollableElement(this._chatMessageContents, { alwaysConsumeMouseWheel: true });
this._store.add(this._chatMessageScrollable);
this._elements.chatMessage.appendChild(this._chatMessageScrollable.getDomNode());
this._store.add(addDisposableListener(this._chatMessageContents, 'focus', () => this._ctxResponseFocused.set(true)));
this._store.add(addDisposableListener(this._chatMessageContents, 'blur', () => this._ctxResponseFocused.reset()));
this._store.add(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) {
this._updateAriaLabel();
}
}));
// --- context keys
this._ctxMessageCropState = CTX_INLINE_CHAT_MESSAGE_CROP_STATE.bindTo(this._contextKeyService);
this._ctxInputEmpty = CTX_INLINE_CHAT_EMPTY.bindTo(this._contextKeyService);
this._ctxInnerCursorFirst = CTX_INLINE_CHAT_INNER_CURSOR_FIRST.bindTo(this._contextKeyService);
@ -381,8 +400,8 @@ export class InlineChatWidget {
// toolbars
this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, _options.menuId, {
telemetrySource: 'interactiveEditorWidget-toolbar',
this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.editorToolbar, options.inputMenuId, {
telemetrySource: options.telemetrySource,
toolbarOptions: { primaryGroup: 'main' },
hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu
hoverDelegate
@ -392,25 +411,17 @@ export class InlineChatWidget {
this._store.add(this._progressBar);
this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, _options.widgetMenuId, {
telemetrySource: 'interactiveEditorWidget-toolbar',
this._store.add(this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.widgetToolbar, options.widgetMenuId, {
telemetrySource: options.telemetrySource,
toolbarOptions: { primaryGroup: 'main' },
hoverDelegate
}));
const workbenchMenubarOptions: IWorkbenchButtonBarOptions = {
telemetrySource: 'interactiveEditorWidget-toolbar',
buttonConfigProvider: action => {
if (action.id === ACTION_REGENERATE_RESPONSE) {
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 };
}
}
};
const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, _options.statusMenuId, workbenchMenubarOptions);
const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu;
const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options;
const statusButtonBar = this._instantiationService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, statusMenuId, statusMenuOptions);
this._store.add(statusButtonBar.onDidChange(() => this._onDidChangeHeight.fire()));
this._store.add(statusButtonBar);
@ -423,7 +434,7 @@ export class InlineChatWidget {
}
};
const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, _options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore });
const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore });
this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire()));
this._store.add(feedbackToolbar);
@ -432,24 +443,19 @@ export class InlineChatWidget {
useInlineViewWhenSpaceIsLimited: false,
..._previewEditorEditorOptions,
onlyShowAccessibleDiffViewer: this._accessibilityService.isScreenReaderOptimized(),
}, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, parentEditor)));
}, { modifiedEditor: codeEditorWidgetOptions, originalEditor: codeEditorWidgetOptions }, _parentEditor)));
this._previewCreateTitle = this._store.add(_instantiationService.createInstance(ResourceLabel, this._elements.previewCreateTitle, { supportIcons: true }));
this._previewCreateEditor = new Lazy(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, parentEditor)));
this._previewCreateEditor = new Lazy(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, _parentEditor)));
this._elements.chatMessageContent.tabIndex = 0;
this._elements.chatMessageContent.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat);
this._elements.followUps.tabIndex = 0;
this._elements.followUps.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat);
this._elements.statusLabel.tabIndex = 0;
const markdownMessageToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.messageActions, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, workbenchToolbarOptions);
this._store.add(markdownMessageToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire()));
this._store.add(markdownMessageToolbar);
this._store.add(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) {
this._elements.chatMessageContent.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat);
this._chatMessageContents.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat);
this._elements.followUps.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat);
}
}));
@ -475,7 +481,6 @@ export class InlineChatWidget {
dispose(): void {
this._store.dispose();
this._ctxInputEmpty.reset();
this._ctxMessageCropState.reset();
}
get domNode(): HTMLElement {
@ -488,6 +493,9 @@ export class InlineChatWidget {
if (this._accessibleViewer.value) {
this._accessibleViewer.value.width = _dim.width - 12;
}
this._chatMessageContents.style.width = `${_dim.width - 10}px`;
this._chatMessageContents.style.maxHeight = `270px`;
const widgetToolbarWidth = getTotalWidth(this._elements.widgetToolbar);
const editorToolbarWidth = getTotalWidth(this._elements.editorToolbar) + 8 /* L/R-padding */;
const innerEditorWidth = _dim.width - editorToolbarWidth - widgetToolbarWidth;
@ -510,12 +518,6 @@ export class InlineChatWidget {
this._elements.previewCreate.style.height = `${previewCreateDim.height}px`;
}
const lineHeight = this.parentEditor.getOption(EditorOption.lineHeight);
const editorHeight = this.parentEditor.getLayoutInfo().height;
const editorHeightInLines = Math.floor(editorHeight / lineHeight);
this._elements.root.style.setProperty('--vscode-inline-chat-cropped', String(Math.floor(editorHeightInLines / 5)));
this._elements.root.style.setProperty('--vscode-inline-chat-expanded', String(Math.floor(editorHeightInLines / 3)));
this._onDidChangeLayout.fire();
}
} finally {
@ -524,16 +526,17 @@ export class InlineChatWidget {
}
getHeight(): number {
const base = getTotalHeight(this._elements.progress) + getTotalHeight(this._elements.status);
const editorHeight = this._inputEditor.getContentHeight() + 12 /* padding and border */;
const progressHeight = getTotalHeight(this._elements.progress);
const detectedIntentHeight = getTotalHeight(this._elements.detectedIntent);
const chatResponseHeight = getTotalHeight(this._chatMessageContents) + 16 /*padding*/;
const followUpsHeight = getTotalHeight(this._elements.followUps);
const chatResponseHeight = getTotalHeight(this._elements.chatMessage);
const previewDiffHeight = this._previewDiffEditor.hasValue && this._previewDiffEditor.value.getModel() ? 12 + Math.min(300, Math.max(0, this._previewDiffEditor.value.getContentHeight())) : 0;
const previewCreateTitleHeight = getTotalHeight(this._elements.previewCreateTitle);
const previewCreateHeight = this._previewCreateEditor.hasValue && this._previewCreateEditor.value.getModel() ? 18 + Math.min(300, Math.max(0, this._previewCreateEditor.value.getContentHeight())) : 0;
const accessibleViewHeight = this._accessibleViewer.value?.height ?? 0;
return base + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + accessibleViewHeight + 18 /* padding */ + 8 /*shadow*/;
const statusHeight = getTotalHeight(this._elements.status);
return progressHeight + editorHeight + detectedIntentHeight + followUpsHeight + chatResponseHeight + previewDiffHeight + previewCreateTitleHeight + previewCreateHeight + accessibleViewHeight + statusHeight + 18 /* padding */ + 8 /*shadow*/;
}
updateProgress(show: boolean) {
@ -589,14 +592,6 @@ export class InlineChatWidget {
this._onDidChangeHeight.fire();
}
get expansionState(): ExpansionState {
return this._expansionState;
}
set preferredExpansionState(expansionState: ExpansionState | undefined) {
this._preferredExpansionState = expansionState;
}
get responseContent(): string | undefined {
return this._chatMessage?.value;
}
@ -604,42 +599,31 @@ export class InlineChatWidget {
updateChatMessage(message: IInlineChatMessage, isIncomplete: true): IInlineChatMessageAppender;
updateChatMessage(message: IInlineChatMessage | undefined): void;
updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean): IInlineChatMessageAppender | undefined {
let expansionState: ExpansionState;
this._chatMessageDisposables.clear();
this._chatMessage = message ? new MarkdownString(message.message.value) : undefined;
const hasMessage = message?.message.value;
this._elements.chatMessage.classList.toggle('hidden', !hasMessage);
reset(this._elements.chatMessageContent);
reset(this._chatMessageContents);
let resultingAppender: IInlineChatMessageAppender | undefined;
if (!hasMessage) {
this._ctxMessageCropState.reset();
expansionState = ExpansionState.NOT_CROPPED;
} else {
if (hasMessage) {
const sessionModel = this._chatMessageDisposables.add(new ChatModel(message.providerId, undefined, this._logService, this._chatAgentService, this._instantiationService));
const responseModel = this._chatMessageDisposables.add(new ChatResponseModel(message.message, sessionModel, undefined, undefined, message.requestId, !isIncomplete, false, undefined));
const viewModel = this._chatMessageDisposables.add(new ChatResponseViewModel(responseModel, this._logService));
const renderOptions: IChatListItemRendererOptions = { renderStyle: 'compact', noHeader: true, noPadding: true };
const chatRendererDelegate: IChatRendererDelegate = { getListLength() { return 1; } };
const renderer = this._chatMessageDisposables.add(this._instantiationService.createInstance(ChatListItemRenderer, this._editorOptions, renderOptions, chatRendererDelegate, this._codeBlockModelCollection, undefined));
renderer.layout(this._elements.chatMessageContent.clientWidth - 4); // 2 for the padding used for the tab index border
renderer.layout(this._chatMessageContents.clientWidth - 4); // 2 for the padding used for the tab index border
this._chatMessageDisposables.add(this._onDidChangeLayout.event(() => {
renderer.layout(this._elements.chatMessageContent.clientWidth - 4);
renderer.layout(this._chatMessageContents.clientWidth - 4);
this._chatMessageScrollable.scanDomNode();
}));
const template = renderer.renderTemplate(this._elements.chatMessageContent);
const template = renderer.renderTemplate(this._chatMessageContents);
this._chatMessageDisposables.add(template.elementDisposables);
this._chatMessageDisposables.add(template.templateDisposables);
renderer.renderChatTreeItem(viewModel, 0, template);
this._chatMessageDisposables.add(renderer.onDidChangeItemHeight(() => this._onDidChangeHeight.fire()));
if (this._preferredExpansionState) {
expansionState = this._preferredExpansionState;
this._preferredExpansionState = undefined;
} else {
this._updateLineClamp(ExpansionState.CROPPED);
expansionState = template.value.scrollHeight > template.value.clientHeight ? ExpansionState.CROPPED : ExpansionState.NOT_CROPPED;
}
this._ctxMessageCropState.set(expansionState);
this._updateLineClamp(expansionState);
resultingAppender = isIncomplete ? {
cancel: () => responseModel.cancel(),
complete: () => responseModel.complete(),
@ -649,7 +633,6 @@ export class InlineChatWidget {
}
} : undefined;
}
this._expansionState = expansionState;
this._onDidChangeHeight.fire();
return resultingAppender;
}
@ -667,21 +650,6 @@ export class InlineChatWidget {
this._onDidChangeHeight.fire();
}
updateChatMessageExpansionState(expansionState: ExpansionState) {
this._ctxMessageCropState.set(expansionState);
const heightBefore = this._elements.chatMessageContent.scrollHeight;
this._updateLineClamp(expansionState);
const heightAfter = this._elements.chatMessageContent.scrollHeight;
if (heightBefore === heightAfter) {
this._ctxMessageCropState.set(ExpansionState.NOT_CROPPED);
}
this._onDidChangeHeight.fire();
}
private _updateLineClamp(expansionState: ExpansionState) {
this._elements.chatMessageContent.setAttribute('state', expansionState);
}
updateSlashCommandUsed(command: string): void {
const details = this._slashCommandDetails.find(candidate => candidate.command === command);
if (!details) {
@ -948,7 +916,7 @@ export class InlineChatWidget {
this._elements.accessibleViewer,
session,
hunkData,
new AccessibleHunk(this.parentEditor, session, hunkData)
new AccessibleHunk(this._parentEditor, session, hunkData)
);
this._onDidChangeHeight.fire();
@ -956,160 +924,6 @@ export class InlineChatWidget {
}
}
export class InlineChatZoneWidget extends ZoneWidget {
readonly widget: InlineChatWidget;
private readonly _ctxVisible: IContextKey<boolean>;
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._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService);
this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService);
this._disposables.add(toDisposable(() => {
this._ctxVisible.reset();
this._ctxCursorPosition.reset();
}));
this.widget = this._instaService.createInstance(InlineChatWidget, this.editor, {
menuId: MENU_INLINE_CHAT_INPUT,
widgetMenuId: MENU_INLINE_CHAT_WIDGET,
statusMenuId: MENU_INLINE_CHAT_WIDGET_STATUS,
feedbackMenuId: MENU_INLINE_CHAT_WIDGET_FEEDBACK
});
this._disposables.add(this.widget.onDidChangeHeight(() => this._relayout()));
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.domNode.style.width = `${width}px`;
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 lineHeight = this.editor.getOption(EditorOption.lineHeight);
return this.widget.getHeight() / lineHeight;
}
protected override _relayout() {
if (this._dimension) {
this._doLayout(this._dimension.height);
}
super._relayout(this._computeHeightInLines());
}
override show(position: Position): void {
super.show(position, this._computeHeightInLines());
this.widget.focus();
this._ctxVisible.set(true);
}
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();
const startLineVisibleRange = visibleRange.startLineNumber;
const positionLine = position.lineNumber;
let indentationLineNumber: number | undefined;
let indentationLevel: number | undefined;
for (let lineNumber = positionLine; lineNumber >= startLineVisibleRange; lineNumber--) {
const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber);
if (currentIndentationLevel !== 0) {
indentationLineNumber = lineNumber;
indentationLevel = currentIndentationLevel;
break;
}
}
return this.editor.getOffsetForColumn(indentationLineNumber ?? positionLine, indentationLevel ?? viewModel.getLineFirstNonWhitespaceColumn(positionLine));
}
setContainerMargins(): void {
assertType(this.container);
const info = this.editor.getLayoutInfo();
const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth;
this.container.style.marginLeft = `${marginWithoutIndentation}px`;
}
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._ctxVisible.reset();
this._ctxCursorPosition.reset();
this.widget.reset();
super.hide();
aria.status(localize('inlineChatClosed', 'Closed inline chat widget'));
}
}
class HunkAccessibleDiffViewer extends AccessibleDiffViewer {
readonly height: number;

View file

@ -0,0 +1,187 @@
/*---------------------------------------------------------------------------------------------
* 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_VIEW_IN_CHAT, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_INPUT, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_FEEDBACK, MENU_INLINE_CHAT_WIDGET_STATUS } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { InlineChatWidget } from './inlineChatWidget';
export class InlineChatZoneWidget extends ZoneWidget {
readonly widget: InlineChatWidget;
private readonly _ctxVisible: IContextKey<boolean>;
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._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService);
this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService);
this._disposables.add(toDisposable(() => {
this._ctxVisible.reset();
this._ctxCursorPosition.reset();
}));
this.widget = this._instaService.createInstance(InlineChatWidget, this.editor, {
telemetrySource: 'interactiveEditorWidget-toolbar',
inputMenuId: MENU_INLINE_CHAT_INPUT,
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) {
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(() => this._relayout()));
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.domNode.style.width = `${width}px`;
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 lineHeight = this.editor.getOption(EditorOption.lineHeight);
return this.widget.getHeight() / lineHeight;
}
protected override _relayout() {
if (this._dimension) {
this._doLayout(this._dimension.height);
}
super._relayout(this._computeHeightInLines());
}
override show(position: Position): void {
super.show(position, this._computeHeightInLines());
this.widget.focus();
this._ctxVisible.set(true);
}
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();
const startLineVisibleRange = visibleRange.startLineNumber;
const positionLine = position.lineNumber;
let indentationLineNumber: number | undefined;
let indentationLevel: number | undefined;
for (let lineNumber = positionLine; lineNumber >= startLineVisibleRange; lineNumber--) {
const currentIndentationLevel = viewModel.getLineFirstNonWhitespaceColumn(lineNumber);
if (currentIndentationLevel !== 0) {
indentationLineNumber = lineNumber;
indentationLevel = currentIndentationLevel;
break;
}
}
return this.editor.getOffsetForColumn(indentationLineNumber ?? positionLine, indentationLevel ?? viewModel.getLineFirstNonWhitespaceColumn(positionLine));
}
setContainerMargins(): void {
assertType(this.container);
const info = this.editor.getLayoutInfo();
const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth;
this.container.style.marginLeft = `${marginWithoutIndentation}px`;
}
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._ctxVisible.reset();
this._ctxCursorPosition.reset();
this.widget.reset();
super.hide();
aria.status(localize('inlineChatClosed', 'Closed inline chat widget'));
}
}

View file

@ -156,7 +156,6 @@ export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey<boolean>('in
export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey<boolean>('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line"));
export const CTX_INLINE_CHAT_INNER_CURSOR_START = new RawContextKey<boolean>('inlineChatInnerCursorStart', false, localize('inlineChatInnerCursorStart', "Whether the cursor of the iteractive editor input is on the start of the input"));
export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey<boolean>('inlineChatInnerCursorEnd', false, localize('inlineChatInnerCursorEnd', "Whether the cursor of the iteractive editor input is on the end of the input"));
export const CTX_INLINE_CHAT_MESSAGE_CROP_STATE = new RawContextKey<'cropped' | 'not_cropped' | 'expanded'>('inlineChatMarkdownMessageCropState', 'not_cropped', localize('inlineChatMarkdownMessageCropState', "Whether the interactive editor message is cropped, not cropped or expanded"));
export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineChatOuterCursorPosition', '', localize('inlineChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input"));
export const CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST = new RawContextKey<boolean>('inlineChatHasActiveRequest', false, localize('inlineChatHasActiveRequest', "Whether interactive editor has an active request"));
export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey<boolean>('inlineChatHasStashedSession', false, localize('inlineChatHasStashedSession', "Whether interactive editor has kept a session for quick restore"));
@ -181,7 +180,6 @@ export const ACTION_VIEW_IN_CHAT = 'inlineChat.viewInChat';
export const MENU_INLINE_CHAT_INPUT = MenuId.for('inlineChatInput');
export const MENU_INLINE_CHAT_WIDGET = MenuId.for('inlineChatWidget');
export const MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE = MenuId.for('inlineChatWidget.markdownMessage');
export const MENU_INLINE_CHAT_WIDGET_STATUS = MenuId.for('inlineChatWidget.status');
export const MENU_INLINE_CHAT_WIDGET_FEEDBACK = MenuId.for('inlineChatWidget.feedback');
export const MENU_INLINE_CHAT_WIDGET_DISCARD = MenuId.for('inlineChatWidget.undo');

View file

@ -292,7 +292,8 @@ export class NotebookChatController extends Disposable implements INotebookEdito
InlineChatWidget,
fakeParentEditor,
{
menuId: MENU_CELL_CHAT_INPUT,
telemetrySource: 'notebook-generate-cell',
inputMenuId: MENU_CELL_CHAT_INPUT,
widgetMenuId: MENU_CELL_CHAT_WIDGET,
statusMenuId: MENU_CELL_CHAT_WIDGET_STATUS,
feedbackMenuId: MENU_CELL_CHAT_WIDGET_FEEDBACK
@ -895,4 +896,3 @@ export class EditStrategy {
registerNotebookContribution(NotebookChatController.id, NotebookChatController);