Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/attachments/chatImplicitContext.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 { CancellationTokenSource } from '../../../../../base/common/cancellation.js';
7
import { Emitter, Event } from '../../../../../base/common/event.js';
8
import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js';
9
import { Schemas } from '../../../../../base/common/network.js';
10
import { autorun } from '../../../../../base/common/observable.js';
11
import { basename, isEqual } from '../../../../../base/common/resources.js';
12
import { ThemeIcon } from '../../../../../base/common/themables.js';
13
import { URI } from '../../../../../base/common/uri.js';
14
import { getCodeEditor, ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
15
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
16
import { Location } from '../../../../../editor/common/languages.js';
17
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
18
import { IWorkbenchContribution } from '../../../../common/contributions.js';
19
import { EditorsOrder } from '../../../../common/editor.js';
20
import { IEditorService } from '../../../../services/editor/common/editorService.js';
21
import { getNotebookEditorFromEditorPane, INotebookEditor } from '../../../notebook/browser/notebookBrowser.js';
22
import { WebviewEditor } from '../../../webviewPanel/browser/webviewEditor.js';
23
import { WebviewInput } from '../../../webviewPanel/browser/webviewEditorInput.js';
24
import { IChatEditingService } from '../../common/editing/chatEditingService.js';
25
import { IChatService } from '../../common/chatService/chatService.js';
26
import { IChatRequestImplicitVariableEntry, IChatRequestVariableEntry, isStringImplicitContextValue, StringChatContextValue } from '../../common/attachments/chatVariableEntries.js';
27
import { ChatAgentLocation } from '../../common/constants.js';
28
import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js';
29
import { getPromptsTypeForLanguageId } from '../../common/promptSyntax/promptTypes.js';
30
import { IChatWidget, IChatWidgetService } from '../chat.js';
31
import { IChatContextService } from '../contextContrib/chatContextService.js';
32
import { ITextModel } from '../../../../../editor/common/model.js';
33
import { IRange } from '../../../../../editor/common/core/range.js';
34
35
export class ChatImplicitContextContribution extends Disposable implements IWorkbenchContribution {
36
static readonly ID = 'chat.implicitContext';
37
38
private readonly _currentCancelTokenSource: MutableDisposable<CancellationTokenSource>;
39
40
private _implicitContextEnablement: { [mode: string]: string };
41
42
constructor(
43
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
44
@IEditorService private readonly editorService: IEditorService,
45
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
46
@IChatService private readonly chatService: IChatService,
47
@IChatEditingService private readonly chatEditingService: IChatEditingService,
48
@IConfigurationService private readonly configurationService: IConfigurationService,
49
@ILanguageModelIgnoredFilesService private readonly ignoredFilesService: ILanguageModelIgnoredFilesService,
50
@IChatContextService private readonly chatContextService: IChatContextService
51
) {
52
super();
53
this._currentCancelTokenSource = this._register(new MutableDisposable<CancellationTokenSource>());
54
this._implicitContextEnablement = this.configurationService.getValue<{ [mode: string]: string }>('chat.implicitContext.enabled');
55
56
const activeEditorDisposables = this._register(new DisposableStore());
57
58
this._register(Event.runAndSubscribe(
59
editorService.onDidActiveEditorChange,
60
(() => {
61
activeEditorDisposables.clear();
62
const codeEditor = this.findActiveCodeEditor();
63
if (codeEditor) {
64
activeEditorDisposables.add(Event.debounce(
65
Event.any(
66
codeEditor.onDidChangeModel,
67
codeEditor.onDidChangeModelLanguage,
68
codeEditor.onDidChangeCursorSelection,
69
codeEditor.onDidScrollChange),
70
() => undefined,
71
500)(() => this.updateImplicitContext()));
72
}
73
74
const notebookEditor = this.findActiveNotebookEditor();
75
if (notebookEditor) {
76
const activeCellDisposables = activeEditorDisposables.add(new DisposableStore());
77
activeEditorDisposables.add(notebookEditor.onDidChangeActiveCell(() => {
78
activeCellDisposables.clear();
79
const codeEditor = this.codeEditorService.getActiveCodeEditor();
80
if (codeEditor && codeEditor.getModel()?.uri.scheme === Schemas.vscodeNotebookCell) {
81
activeCellDisposables.add(Event.debounce(
82
Event.any(
83
codeEditor.onDidChangeModel,
84
codeEditor.onDidChangeCursorSelection,
85
codeEditor.onDidScrollChange),
86
() => undefined,
87
500)(() => this.updateImplicitContext()));
88
}
89
}));
90
91
activeEditorDisposables.add(Event.debounce(
92
Event.any(
93
notebookEditor.onDidChangeModel,
94
notebookEditor.onDidChangeActiveCell
95
),
96
() => undefined,
97
500)(() => this.updateImplicitContext()));
98
}
99
const webviewEditor = this.findActiveWebviewEditor();
100
if (webviewEditor) {
101
activeEditorDisposables.add(Event.debounce((webviewEditor.input as WebviewInput).webview.onMessage, () => undefined, 500)(() => {
102
this.updateImplicitContext();
103
}));
104
}
105
106
this.updateImplicitContext();
107
})));
108
this._register(autorun((reader) => {
109
this.chatEditingService.editingSessionsObs.read(reader);
110
this.updateImplicitContext();
111
}));
112
this._register(this.configurationService.onDidChangeConfiguration(e => {
113
if (e.affectsConfiguration('chat.implicitContext.enabled')) {
114
this._implicitContextEnablement = this.configurationService.getValue<{ [mode: string]: string }>('chat.implicitContext.enabled');
115
this.updateImplicitContext();
116
}
117
}));
118
this._register(this.chatService.onDidSubmitRequest(({ chatSessionResource }) => {
119
const widget = this.chatWidgetService.getWidgetBySessionResource(chatSessionResource);
120
if (!widget?.input.implicitContext) {
121
return;
122
}
123
if (this._implicitContextEnablement[widget.location] === 'first' && widget.viewModel?.getItems().length !== 0) {
124
widget.input.implicitContext.setValue(undefined, false, undefined);
125
}
126
}));
127
this._register(this.chatWidgetService.onDidAddWidget(async (widget) => {
128
await this.updateImplicitContext(widget);
129
}));
130
}
131
132
private findActiveCodeEditor(): ICodeEditor | undefined {
133
const codeEditor = this.codeEditorService.getActiveCodeEditor();
134
if (codeEditor) {
135
const model = codeEditor.getModel();
136
if (model?.uri.scheme === Schemas.vscodeNotebookCell) {
137
return undefined;
138
}
139
140
if (model) {
141
return codeEditor;
142
}
143
}
144
for (const codeOrDiffEditor of this.editorService.getVisibleTextEditorControls(EditorsOrder.MOST_RECENTLY_ACTIVE)) {
145
const codeEditor = getCodeEditor(codeOrDiffEditor);
146
if (!codeEditor) {
147
continue;
148
}
149
150
const model = codeEditor.getModel();
151
if (model) {
152
return codeEditor;
153
}
154
}
155
return undefined;
156
}
157
158
private findActiveWebviewEditor(): WebviewEditor | undefined {
159
const activeEditorPane = this.editorService.activeEditorPane;
160
if (activeEditorPane?.input instanceof WebviewInput) {
161
return activeEditorPane as WebviewEditor;
162
}
163
return undefined;
164
}
165
166
private findActiveNotebookEditor(): INotebookEditor | undefined {
167
return getNotebookEditorFromEditorPane(this.editorService.activeEditorPane);
168
}
169
170
private async updateImplicitContext(updateWidget?: IChatWidget): Promise<void> {
171
const cancelTokenSource = this._currentCancelTokenSource.value = new CancellationTokenSource();
172
const codeEditor = this.findActiveCodeEditor();
173
const model = codeEditor?.getModel();
174
const selection = codeEditor?.getSelection();
175
let newValue: Location | URI | StringChatContextValue | undefined;
176
let isSelection = false;
177
178
let languageId: string | undefined;
179
if (model) {
180
languageId = model.getLanguageId();
181
if (selection && !selection.isEmpty()) {
182
newValue = { uri: model.uri, range: selection } satisfies Location;
183
isSelection = true;
184
} else {
185
if (this.configurationService.getValue('chat.implicitContext.suggestedContext')) {
186
newValue = model.uri;
187
} else {
188
const visibleRanges = codeEditor?.getVisibleRanges();
189
if (visibleRanges && visibleRanges.length > 0) {
190
// Merge visible ranges. Maybe the reference value could actually be an array of Locations?
191
// Something like a Location with an array of Ranges?
192
let range = visibleRanges[0];
193
visibleRanges.slice(1).forEach(r => {
194
range = range.plusRange(r);
195
});
196
newValue = { uri: model.uri, range } satisfies Location;
197
} else {
198
newValue = model.uri;
199
}
200
}
201
}
202
}
203
204
const notebookEditor = this.findActiveNotebookEditor();
205
if (notebookEditor?.isReplHistory) {
206
// The chat APIs don't work well with Interactive Windows
207
newValue = undefined;
208
} else if (notebookEditor) {
209
const activeCell = notebookEditor.getActiveCell();
210
if (activeCell) {
211
const codeEditor = this.codeEditorService.getActiveCodeEditor();
212
const selection = codeEditor?.getSelection();
213
const visibleRanges = codeEditor?.getVisibleRanges() || [];
214
newValue = activeCell.uri;
215
const cellModel = codeEditor?.getModel();
216
if (cellModel && isEqual(cellModel.uri, activeCell.uri)) {
217
if (selection && !selection.isEmpty()) {
218
newValue = { uri: activeCell.uri, range: selection } satisfies Location;
219
isSelection = true;
220
} else if (visibleRanges.length > 0) {
221
// If the entire cell is visible, just use the cell URI, no need to specify range.
222
if (!isEntireCellVisible(cellModel, visibleRanges)) {
223
// Merge visible ranges. Maybe the reference value could actually be an array of Locations?
224
// Something like a Location with an array of Ranges?
225
let range = visibleRanges[0];
226
visibleRanges.slice(1).forEach(r => {
227
range = range.plusRange(r);
228
});
229
newValue = { uri: activeCell.uri, range } satisfies Location;
230
}
231
}
232
}
233
} else {
234
newValue = notebookEditor.textModel?.uri;
235
}
236
}
237
238
const webviewEditor = this.findActiveWebviewEditor();
239
if (webviewEditor?.input?.resource) {
240
const webviewContext = await this.chatContextService.contextForResource(webviewEditor.input.resource);
241
if (webviewContext) {
242
newValue = webviewContext;
243
}
244
}
245
246
const uri = newValue instanceof URI ? newValue : (isStringImplicitContextValue(newValue) ? undefined : newValue?.uri);
247
if (uri && (
248
await this.ignoredFilesService.fileIsIgnored(uri, cancelTokenSource.token) ||
249
uri.path.endsWith('.copilotmd'))
250
) {
251
newValue = undefined;
252
}
253
254
if (cancelTokenSource.token.isCancellationRequested) {
255
return;
256
}
257
258
const isPromptFile = languageId && getPromptsTypeForLanguageId(languageId) !== undefined;
259
260
const widgets = updateWidget ? [updateWidget] : [...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat), ...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.EditorInline)];
261
for (const widget of widgets) {
262
if (!widget.input.implicitContext) {
263
continue;
264
}
265
const setting = this._implicitContextEnablement[widget.location];
266
const isFirstInteraction = widget.viewModel?.getItems().length === 0;
267
if ((setting === 'always' || setting === 'first' && isFirstInteraction) && !isPromptFile) { // disable implicit context for prompt files
268
widget.input.implicitContext.setValue(newValue, isSelection, languageId);
269
} else {
270
widget.input.implicitContext.setValue(undefined, false, undefined);
271
}
272
}
273
}
274
}
275
276
function isEntireCellVisible(cellModel: ITextModel, visibleRanges: IRange[]): boolean {
277
if (visibleRanges.length === 1 && visibleRanges[0].startLineNumber === 1 && visibleRanges[0].startColumn === 1 && visibleRanges[0].endLineNumber === cellModel.getLineCount() && visibleRanges[0].endColumn === cellModel.getLineMaxColumn(visibleRanges[0].endLineNumber)) {
278
return true;
279
}
280
return false;
281
}
282
283
export class ChatImplicitContext extends Disposable implements IChatRequestImplicitVariableEntry {
284
get id() {
285
if (URI.isUri(this.value)) {
286
return 'vscode.implicit.file';
287
} else if (isStringImplicitContextValue(this.value)) {
288
return 'vscode.implicit.string';
289
} else if (this.value) {
290
if (this._isSelection) {
291
return 'vscode.implicit.selection';
292
} else {
293
return 'vscode.implicit.viewport';
294
}
295
} else {
296
return 'vscode.implicit';
297
}
298
}
299
300
get name(): string {
301
if (URI.isUri(this.value)) {
302
return `file:${basename(this.value)}`;
303
} else if (isStringImplicitContextValue(this.value)) {
304
return this.value.name;
305
} else if (this.value) {
306
return `file:${basename(this.value.uri)}`;
307
} else {
308
return 'implicit';
309
}
310
}
311
312
readonly kind = 'implicit';
313
314
get modelDescription(): string {
315
if (URI.isUri(this.value)) {
316
return `User's active file`;
317
} else if (isStringImplicitContextValue(this.value)) {
318
return this.value.modelDescription ?? `User's active context from ${this.value.name}`;
319
} else if (this._isSelection) {
320
return `User's active selection`;
321
} else {
322
return `User's current visible code`;
323
}
324
}
325
326
readonly isFile = true;
327
328
private _isSelection = false;
329
public get isSelection(): boolean {
330
return this._isSelection;
331
}
332
333
private _onDidChangeValue = this._register(new Emitter<void>());
334
readonly onDidChangeValue = this._onDidChangeValue.event;
335
336
private _value: Location | URI | StringChatContextValue | undefined;
337
get value() {
338
return this._value;
339
}
340
341
private _enabled = true;
342
get enabled() {
343
return this._enabled;
344
}
345
346
set enabled(value: boolean) {
347
this._enabled = value;
348
this._onDidChangeValue.fire();
349
}
350
351
private _uri: URI | undefined;
352
get uri(): URI | undefined {
353
if (isStringImplicitContextValue(this.value)) {
354
return this.value.uri;
355
}
356
return this._uri;
357
}
358
359
get icon(): ThemeIcon | undefined {
360
if (isStringImplicitContextValue(this.value)) {
361
return this.value.icon;
362
}
363
return undefined;
364
}
365
366
setValue(value: Location | URI | StringChatContextValue | undefined, isSelection: boolean, languageId?: string): void {
367
if (isStringImplicitContextValue(value)) {
368
this._value = value;
369
} else {
370
this._value = value;
371
this._uri = URI.isUri(value) ? value : value?.uri;
372
}
373
this._isSelection = isSelection;
374
this._onDidChangeValue.fire();
375
}
376
377
public toBaseEntries(): IChatRequestVariableEntry[] {
378
if (!this.value) {
379
return [];
380
}
381
382
if (isStringImplicitContextValue(this.value)) {
383
return [
384
{
385
kind: 'string',
386
id: this.id,
387
name: this.name,
388
value: this.value.value ?? this.name,
389
modelDescription: this.modelDescription,
390
icon: this.value.icon,
391
uri: this.value.uri
392
}
393
];
394
}
395
396
return [{
397
kind: 'file',
398
id: this.id,
399
name: this.name,
400
value: this.value,
401
modelDescription: this.modelDescription,
402
}];
403
}
404
405
}
406
407