Path: blob/main/src/vs/workbench/contrib/chat/browser/attachments/chatDynamicVariables.ts
4780 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*--------------------------------------------------------------------------------------------*/45import { coalesce } from '../../../../../base/common/arrays.js';6import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';7import { Disposable, dispose, isDisposable } from '../../../../../base/common/lifecycle.js';8import { URI } from '../../../../../base/common/uri.js';9import { IRange, Range } from '../../../../../editor/common/core/range.js';10import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js';11import { Command, isLocation } from '../../../../../editor/common/languages.js';12import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';13import { ICommandService } from '../../../../../platform/commands/common/commands.js';14import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';15import { ILabelService } from '../../../../../platform/label/common/label.js';16import { IChatRequestVariableValue, IDynamicVariable } from '../../common/attachments/chatVariables.js';17import { IChatWidget } from '../chat.js';18import { IChatWidgetContrib } from '../widget/chatWidget.js';1920export const dynamicVariableDecorationType = 'chat-dynamic-variable';21222324export class ChatDynamicVariableModel extends Disposable implements IChatWidgetContrib {25public static readonly ID = 'chatDynamicVariableModel';2627private _variables: IDynamicVariable[] = [];2829get variables(): ReadonlyArray<IDynamicVariable> {30return [...this._variables];31}3233get id() {34return ChatDynamicVariableModel.ID;35}3637private decorationData: { id: string; text: string }[] = [];3839constructor(40private readonly widget: IChatWidget,41@ILabelService private readonly labelService: ILabelService,42) {43super();4445this._register(widget.inputEditor.onDidChangeModelContent(e => {4647const removed: IDynamicVariable[] = [];48let didChange = false;4950// Don't mutate entries in _variables, since they will be returned from the getter51this._variables = coalesce(this._variables.map((ref, idx): IDynamicVariable | null => {52const model = widget.inputEditor.getModel();5354if (!model) {55removed.push(ref);56return null;57}5859const data = this.decorationData[idx];60const newRange = model.getDecorationRange(data.id);6162if (!newRange) {63// gone64removed.push(ref);65return null;66}6768const newText = model.getValueInRange(newRange);69if (newText !== data.text) {7071this.widget.inputEditor.executeEdits(this.id, [{72range: newRange,73text: '',74}]);75this.widget.refreshParsedInput();7677removed.push(ref);78return null;79}8081if (newRange.equalsRange(ref.range)) {82// all good83return ref;84}8586didChange = true;8788return { ...ref, range: newRange };89}));9091// cleanup disposable variables92dispose(removed.filter(isDisposable));9394if (didChange || removed.length > 0) {95this.widget.refreshParsedInput();96}9798this.updateDecorations();99}));100}101102getInputState(contrib: Record<string, unknown>): void {103contrib[ChatDynamicVariableModel.ID] = this.variables;104}105106setInputState(contrib: Readonly<Record<string, unknown>>): void {107let s = contrib[ChatDynamicVariableModel.ID] as unknown[];108if (!Array.isArray(s)) {109s = [];110}111112this.disposeVariables();113this._variables = [];114115for (const variable of s) {116if (!isDynamicVariable(variable)) {117continue;118}119120this.addReference(variable);121}122}123124addReference(ref: IDynamicVariable): void {125this._variables.push(ref);126this.updateDecorations();127this.widget.refreshParsedInput();128}129130private updateDecorations(): void {131132const decorationIds = this.widget.inputEditor.setDecorationsByType('chat', dynamicVariableDecorationType, this._variables.map((r): IDecorationOptions => ({133range: r.range,134hoverMessage: this.getHoverForReference(r)135})));136137this.decorationData = [];138for (let i = 0; i < decorationIds.length; i++) {139this.decorationData.push({140id: decorationIds[i],141text: this.widget.inputEditor.getModel()!.getValueInRange(this._variables[i].range)142});143}144}145146private getHoverForReference(ref: IDynamicVariable): IMarkdownString | undefined {147const value = ref.data;148if (URI.isUri(value)) {149return new MarkdownString(this.labelService.getUriLabel(value, { relative: true }));150} else if (isLocation(value)) {151const prefix = ref.fullName ? ` ${ref.fullName}` : '';152const rangeString = `#${value.range.startLineNumber}-${value.range.endLineNumber}`;153return new MarkdownString(prefix + this.labelService.getUriLabel(value.uri, { relative: true }) + rangeString);154} else {155return undefined;156}157}158159/**160* Dispose all existing variables.161*/162private disposeVariables(): void {163for (const variable of this._variables) {164if (isDisposable(variable)) {165variable.dispose();166}167}168}169170public override dispose() {171this.disposeVariables();172super.dispose();173}174}175176/**177* Loose check to filter objects that are obviously missing data178*/179// eslint-disable-next-line @typescript-eslint/no-explicit-any180function isDynamicVariable(obj: any): obj is IDynamicVariable {181return obj &&182typeof obj.id === 'string' &&183Range.isIRange(obj.range) &&184'data' in obj;185}186187188189export interface IAddDynamicVariableContext {190id: string;191widget: IChatWidget;192range: IRange;193variableData: IChatRequestVariableValue;194command?: Command;195}196197// eslint-disable-next-line @typescript-eslint/no-explicit-any198function isAddDynamicVariableContext(context: any): context is IAddDynamicVariableContext {199return 'widget' in context &&200'range' in context &&201'variableData' in context;202}203204export class AddDynamicVariableAction extends Action2 {205static readonly ID = 'workbench.action.chat.addDynamicVariable';206207constructor() {208super({209id: AddDynamicVariableAction.ID,210title: '' // not displayed211});212}213214async run(accessor: ServicesAccessor, ...args: unknown[]) {215const context = args[0];216if (!isAddDynamicVariableContext(context)) {217return;218}219220let range = context.range;221const variableData = context.variableData;222223const doCleanup = () => {224// Failed, remove the dangling variable prefix225context.widget.inputEditor.executeEdits('chatInsertDynamicVariableWithArguments', [{ range: context.range, text: `` }]);226};227228// If this completion item has no command, return it directly229if (context.command) {230// Invoke the command on this completion item along with its args and return the result231const commandService = accessor.get(ICommandService);232const selection: string | undefined = await commandService.executeCommand(context.command.id, ...(context.command.arguments ?? []));233if (!selection) {234doCleanup();235return;236}237238// Compute new range and variableData239const insertText = ':' + selection;240const insertRange = new Range(range.startLineNumber, range.endColumn, range.endLineNumber, range.endColumn + insertText.length);241range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + insertText.length);242const editor = context.widget.inputEditor;243const success = editor.executeEdits('chatInsertDynamicVariableWithArguments', [{ range: insertRange, text: insertText + ' ' }]);244if (!success) {245doCleanup();246return;247}248}249250context.widget.getContrib<ChatDynamicVariableModel>(ChatDynamicVariableModel.ID)?.addReference({251id: context.id,252range: range,253isFile: true,254data: variableData255});256}257}258registerAction2(AddDynamicVariableAction);259260261