Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/attachments/chatDynamicVariables.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { coalesce } from '../../../../../base/common/arrays.js';
7
import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';
8
import { Disposable, dispose, isDisposable } from '../../../../../base/common/lifecycle.js';
9
import { URI } from '../../../../../base/common/uri.js';
10
import { IRange, Range } from '../../../../../editor/common/core/range.js';
11
import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js';
12
import { Command, isLocation } from '../../../../../editor/common/languages.js';
13
import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';
14
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
15
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
16
import { ILabelService } from '../../../../../platform/label/common/label.js';
17
import { IChatRequestVariableValue, IDynamicVariable } from '../../common/attachments/chatVariables.js';
18
import { IChatWidget } from '../chat.js';
19
import { IChatWidgetContrib } from '../widget/chatWidget.js';
20
21
export const dynamicVariableDecorationType = 'chat-dynamic-variable';
22
23
24
25
export class ChatDynamicVariableModel extends Disposable implements IChatWidgetContrib {
26
public static readonly ID = 'chatDynamicVariableModel';
27
28
private _variables: IDynamicVariable[] = [];
29
30
get variables(): ReadonlyArray<IDynamicVariable> {
31
return [...this._variables];
32
}
33
34
get id() {
35
return ChatDynamicVariableModel.ID;
36
}
37
38
private decorationData: { id: string; text: string }[] = [];
39
40
constructor(
41
private readonly widget: IChatWidget,
42
@ILabelService private readonly labelService: ILabelService,
43
) {
44
super();
45
46
this._register(widget.inputEditor.onDidChangeModelContent(e => {
47
48
const removed: IDynamicVariable[] = [];
49
let didChange = false;
50
51
// Don't mutate entries in _variables, since they will be returned from the getter
52
this._variables = coalesce(this._variables.map((ref, idx): IDynamicVariable | null => {
53
const model = widget.inputEditor.getModel();
54
55
if (!model) {
56
removed.push(ref);
57
return null;
58
}
59
60
const data = this.decorationData[idx];
61
const newRange = model.getDecorationRange(data.id);
62
63
if (!newRange) {
64
// gone
65
removed.push(ref);
66
return null;
67
}
68
69
const newText = model.getValueInRange(newRange);
70
if (newText !== data.text) {
71
72
this.widget.inputEditor.executeEdits(this.id, [{
73
range: newRange,
74
text: '',
75
}]);
76
this.widget.refreshParsedInput();
77
78
removed.push(ref);
79
return null;
80
}
81
82
if (newRange.equalsRange(ref.range)) {
83
// all good
84
return ref;
85
}
86
87
didChange = true;
88
89
return { ...ref, range: newRange };
90
}));
91
92
// cleanup disposable variables
93
dispose(removed.filter(isDisposable));
94
95
if (didChange || removed.length > 0) {
96
this.widget.refreshParsedInput();
97
}
98
99
this.updateDecorations();
100
}));
101
}
102
103
getInputState(contrib: Record<string, unknown>): void {
104
contrib[ChatDynamicVariableModel.ID] = this.variables;
105
}
106
107
setInputState(contrib: Readonly<Record<string, unknown>>): void {
108
let s = contrib[ChatDynamicVariableModel.ID] as unknown[];
109
if (!Array.isArray(s)) {
110
s = [];
111
}
112
113
this.disposeVariables();
114
this._variables = [];
115
116
for (const variable of s) {
117
if (!isDynamicVariable(variable)) {
118
continue;
119
}
120
121
this.addReference(variable);
122
}
123
}
124
125
addReference(ref: IDynamicVariable): void {
126
this._variables.push(ref);
127
this.updateDecorations();
128
this.widget.refreshParsedInput();
129
}
130
131
private updateDecorations(): void {
132
133
const decorationIds = this.widget.inputEditor.setDecorationsByType('chat', dynamicVariableDecorationType, this._variables.map((r): IDecorationOptions => ({
134
range: r.range,
135
hoverMessage: this.getHoverForReference(r)
136
})));
137
138
this.decorationData = [];
139
for (let i = 0; i < decorationIds.length; i++) {
140
this.decorationData.push({
141
id: decorationIds[i],
142
text: this.widget.inputEditor.getModel()!.getValueInRange(this._variables[i].range)
143
});
144
}
145
}
146
147
private getHoverForReference(ref: IDynamicVariable): IMarkdownString | undefined {
148
const value = ref.data;
149
if (URI.isUri(value)) {
150
return new MarkdownString(this.labelService.getUriLabel(value, { relative: true }));
151
} else if (isLocation(value)) {
152
const prefix = ref.fullName ? ` ${ref.fullName}` : '';
153
const rangeString = `#${value.range.startLineNumber}-${value.range.endLineNumber}`;
154
return new MarkdownString(prefix + this.labelService.getUriLabel(value.uri, { relative: true }) + rangeString);
155
} else {
156
return undefined;
157
}
158
}
159
160
/**
161
* Dispose all existing variables.
162
*/
163
private disposeVariables(): void {
164
for (const variable of this._variables) {
165
if (isDisposable(variable)) {
166
variable.dispose();
167
}
168
}
169
}
170
171
public override dispose() {
172
this.disposeVariables();
173
super.dispose();
174
}
175
}
176
177
/**
178
* Loose check to filter objects that are obviously missing data
179
*/
180
// eslint-disable-next-line @typescript-eslint/no-explicit-any
181
function isDynamicVariable(obj: any): obj is IDynamicVariable {
182
return obj &&
183
typeof obj.id === 'string' &&
184
Range.isIRange(obj.range) &&
185
'data' in obj;
186
}
187
188
189
190
export interface IAddDynamicVariableContext {
191
id: string;
192
widget: IChatWidget;
193
range: IRange;
194
variableData: IChatRequestVariableValue;
195
command?: Command;
196
}
197
198
// eslint-disable-next-line @typescript-eslint/no-explicit-any
199
function isAddDynamicVariableContext(context: any): context is IAddDynamicVariableContext {
200
return 'widget' in context &&
201
'range' in context &&
202
'variableData' in context;
203
}
204
205
export class AddDynamicVariableAction extends Action2 {
206
static readonly ID = 'workbench.action.chat.addDynamicVariable';
207
208
constructor() {
209
super({
210
id: AddDynamicVariableAction.ID,
211
title: '' // not displayed
212
});
213
}
214
215
async run(accessor: ServicesAccessor, ...args: unknown[]) {
216
const context = args[0];
217
if (!isAddDynamicVariableContext(context)) {
218
return;
219
}
220
221
let range = context.range;
222
const variableData = context.variableData;
223
224
const doCleanup = () => {
225
// Failed, remove the dangling variable prefix
226
context.widget.inputEditor.executeEdits('chatInsertDynamicVariableWithArguments', [{ range: context.range, text: `` }]);
227
};
228
229
// If this completion item has no command, return it directly
230
if (context.command) {
231
// Invoke the command on this completion item along with its args and return the result
232
const commandService = accessor.get(ICommandService);
233
const selection: string | undefined = await commandService.executeCommand(context.command.id, ...(context.command.arguments ?? []));
234
if (!selection) {
235
doCleanup();
236
return;
237
}
238
239
// Compute new range and variableData
240
const insertText = ':' + selection;
241
const insertRange = new Range(range.startLineNumber, range.endColumn, range.endLineNumber, range.endColumn + insertText.length);
242
range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + insertText.length);
243
const editor = context.widget.inputEditor;
244
const success = editor.executeEdits('chatInsertDynamicVariableWithArguments', [{ range: insertRange, text: insertText + ' ' }]);
245
if (!success) {
246
doCleanup();
247
return;
248
}
249
}
250
251
context.widget.getContrib<ChatDynamicVariableModel>(ChatDynamicVariableModel.ID)?.addReference({
252
id: context.id,
253
range: range,
254
isFile: true,
255
data: variableData
256
});
257
}
258
}
259
registerAction2(AddDynamicVariableAction);
260
261