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
5272 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, DisposableMap, 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 { isLocation, 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.setValues([]);
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
let providerContext: StringChatContextValue | undefined;
180
if (model) {
181
languageId = model.getLanguageId();
182
if (selection && !selection.isEmpty()) {
183
newValue = { uri: model.uri, range: selection } satisfies Location;
184
isSelection = true;
185
} else {
186
if (this.configurationService.getValue('chat.implicitContext.suggestedContext')) {
187
newValue = model.uri;
188
} else {
189
const visibleRanges = codeEditor?.getVisibleRanges();
190
if (visibleRanges && visibleRanges.length > 0) {
191
// Merge visible ranges. Maybe the reference value could actually be an array of Locations?
192
// Something like a Location with an array of Ranges?
193
let range = visibleRanges[0];
194
visibleRanges.slice(1).forEach(r => {
195
range = range.plusRange(r);
196
});
197
newValue = { uri: model.uri, range } satisfies Location;
198
} else {
199
newValue = model.uri;
200
}
201
}
202
}
203
// Also check if a chat context provider can provide additional context for this text editor resource
204
providerContext = await this.chatContextService.contextForResource(model.uri, languageId);
205
}
206
207
const notebookEditor = this.findActiveNotebookEditor();
208
if (notebookEditor?.isReplHistory) {
209
// The chat APIs don't work well with Interactive Windows
210
newValue = undefined;
211
} else if (notebookEditor) {
212
const activeCell = notebookEditor.getActiveCell();
213
if (activeCell) {
214
const codeEditor = this.codeEditorService.getActiveCodeEditor();
215
const selection = codeEditor?.getSelection();
216
const visibleRanges = codeEditor?.getVisibleRanges() || [];
217
newValue = activeCell.uri;
218
const cellModel = codeEditor?.getModel();
219
if (cellModel && isEqual(cellModel.uri, activeCell.uri)) {
220
if (selection && !selection.isEmpty()) {
221
newValue = { uri: activeCell.uri, range: selection } satisfies Location;
222
isSelection = true;
223
} else if (visibleRanges.length > 0) {
224
// If the entire cell is visible, just use the cell URI, no need to specify range.
225
if (!isEntireCellVisible(cellModel, visibleRanges)) {
226
// Merge visible ranges. Maybe the reference value could actually be an array of Locations?
227
// Something like a Location with an array of Ranges?
228
let range = visibleRanges[0];
229
visibleRanges.slice(1).forEach(r => {
230
range = range.plusRange(r);
231
});
232
newValue = { uri: activeCell.uri, range } satisfies Location;
233
}
234
}
235
}
236
} else {
237
newValue = notebookEditor.textModel?.uri;
238
}
239
}
240
241
const webviewEditor = this.findActiveWebviewEditor();
242
if (webviewEditor?.input?.resource) {
243
const webviewContext = await this.chatContextService.contextForResource(webviewEditor.input.resource);
244
if (webviewContext) {
245
newValue = webviewContext;
246
}
247
}
248
249
const uri = newValue instanceof URI ? newValue : (isStringImplicitContextValue(newValue) ? undefined : newValue?.uri);
250
if (uri && (
251
await this.ignoredFilesService.fileIsIgnored(uri, cancelTokenSource.token) ||
252
uri.path.endsWith('.copilotmd'))
253
) {
254
newValue = undefined;
255
}
256
257
if (cancelTokenSource.token.isCancellationRequested) {
258
return;
259
}
260
261
const isPromptFile = languageId && getPromptsTypeForLanguageId(languageId) !== undefined;
262
263
const widgets = updateWidget ? [updateWidget] : [...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat), ...this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.EditorInline)];
264
for (const widget of widgets) {
265
if (!widget.input.implicitContext) {
266
continue;
267
}
268
const setting = this._implicitContextEnablement[widget.location];
269
const isFirstInteraction = widget.viewModel?.getItems().length === 0;
270
if ((setting === 'always' || setting === 'first' && isFirstInteraction) && !isPromptFile) { // disable implicit context for prompt files
271
widget.input.implicitContext.setValues([{ value: newValue, isSelection }, { value: providerContext, isSelection: false }]);
272
} else {
273
widget.input.implicitContext.setValues([]);
274
}
275
}
276
}
277
}
278
279
function isEntireCellVisible(cellModel: ITextModel, visibleRanges: IRange[]): boolean {
280
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)) {
281
return true;
282
}
283
return false;
284
}
285
286
interface ImplicitContextWithSelection {
287
value: Location | URI | StringChatContextValue | undefined;
288
isSelection: boolean;
289
}
290
291
export class ChatImplicitContexts extends Disposable {
292
private _onDidChangeValue = this._register(new Emitter<void>());
293
readonly onDidChangeValue = this._onDidChangeValue.event;
294
295
private _values: DisposableMap<ChatImplicitContext, DisposableStore> = this._register(new DisposableMap());
296
private readonly _valuesDisposables: DisposableStore = this._register(new DisposableStore());
297
298
setValues(values: ImplicitContextWithSelection[]): void {
299
this._valuesDisposables.clear();
300
this._values.clearAndDisposeAll();
301
302
if (!values || values.length === 0) {
303
this._onDidChangeValue.fire();
304
return;
305
}
306
307
const definedValues = values.filter(value => value.value !== undefined);
308
for (const value of definedValues) {
309
const implicitContext = new ChatImplicitContext();
310
implicitContext.setValue(value.value, value.isSelection);
311
const disposableStore = new DisposableStore();
312
disposableStore.add(implicitContext.onDidChangeValue(() => {
313
this._onDidChangeValue.fire();
314
}));
315
disposableStore.add(implicitContext);
316
this._values.set(implicitContext, disposableStore);
317
}
318
this._onDidChangeValue.fire();
319
}
320
321
get values(): ChatImplicitContext[] {
322
return Array.from(this._values.keys());
323
}
324
325
get hasEnabled(): boolean {
326
return Array.from(this._values.keys()).some(v => v.enabled);
327
}
328
329
setEnabled(enabled: boolean): void {
330
this.values.forEach((v) => v.enabled = enabled);
331
}
332
333
get hasValue(): boolean {
334
return this.values.some(v => v.value !== undefined);
335
}
336
337
get hasNonUri(): boolean {
338
return this.values.some(v => v.value !== undefined && !URI.isUri(v.value));
339
}
340
341
getLocations(): Location[] {
342
return this.values.filter(v => isLocation(v.value)).map(v => v.value as Location);
343
}
344
345
getUris(): URI[] {
346
return this.values.filter(v => URI.isUri(v.value)).map(v => v.value as URI);
347
}
348
349
get hasNonStringContext(): boolean {
350
return this.values.some(v => v.value !== undefined && !isStringImplicitContextValue(v.value));
351
}
352
353
enabledBaseEntries(includeAllLocations: boolean): IChatRequestVariableEntry[] {
354
return this.values.flatMap(v => {
355
if (v.enabled) {
356
return v.toBaseEntries();
357
} else if (includeAllLocations && isLocation(v.value)) {
358
return v.toBaseEntries();
359
}
360
return [];
361
});
362
}
363
}
364
365
export class ChatImplicitContext extends Disposable implements IChatRequestImplicitVariableEntry {
366
get id() {
367
if (URI.isUri(this.value)) {
368
return 'vscode.implicit.file';
369
} else if (isStringImplicitContextValue(this.value)) {
370
return 'vscode.implicit.string';
371
} else if (this.value) {
372
if (this._isSelection) {
373
return 'vscode.implicit.selection';
374
} else {
375
return 'vscode.implicit.viewport';
376
}
377
} else {
378
return 'vscode.implicit';
379
}
380
}
381
382
get name(): string {
383
if (URI.isUri(this.value)) {
384
return `file:${basename(this.value)}`;
385
}
386
if (isLocation(this.value)) {
387
return `file:${basename(this.value.uri)}`;
388
}
389
if (isStringImplicitContextValue(this.value)) {
390
if (this.value.name === undefined && this.value.resourceUri === undefined) {
391
throw new Error('ChatContextItem must have either a label or a resourceUri');
392
}
393
return this.value.name ?? basename(this.value.resourceUri!);
394
}
395
return 'implicit';
396
}
397
398
readonly kind = 'implicit';
399
400
get modelDescription(): string {
401
if (URI.isUri(this.value)) {
402
return `User's active file`;
403
} else if (isStringImplicitContextValue(this.value)) {
404
if (this.value.name === undefined && this.value.resourceUri === undefined) {
405
throw new Error('ChatContextItem must have either a label or a resourceUri');
406
}
407
const contextName = this.value.name ?? basename(this.value.resourceUri!);
408
return this.value.modelDescription ?? `User's active context from ${contextName}`;
409
} else if (this._isSelection) {
410
return `User's active selection`;
411
} else {
412
return `User's current visible code`;
413
}
414
}
415
416
readonly isFile = true;
417
418
private _isSelection = false;
419
public get isSelection(): boolean {
420
return this._isSelection;
421
}
422
423
private _onDidChangeValue = this._register(new Emitter<void>());
424
readonly onDidChangeValue = this._onDidChangeValue.event;
425
426
private _value: Location | URI | StringChatContextValue | undefined;
427
get value() {
428
return this._value;
429
}
430
431
private _enabled = false;
432
get enabled() {
433
return this._enabled;
434
}
435
436
set enabled(value: boolean) {
437
this._enabled = value;
438
this._onDidChangeValue.fire();
439
}
440
441
private _uri: URI | undefined;
442
get uri(): URI | undefined {
443
if (isStringImplicitContextValue(this.value)) {
444
return this.value.uri;
445
}
446
return this._uri;
447
}
448
449
get icon(): ThemeIcon | undefined {
450
if (isStringImplicitContextValue(this.value)) {
451
return this.value.icon;
452
}
453
return undefined;
454
}
455
456
setValue(value: Location | URI | StringChatContextValue | undefined, isSelection: boolean): void {
457
if (isStringImplicitContextValue(value)) {
458
this._value = value;
459
} else {
460
this._value = value;
461
this._uri = URI.isUri(value) ? value : value?.uri;
462
}
463
this._isSelection = isSelection;
464
this._onDidChangeValue.fire();
465
}
466
467
public toBaseEntries(): IChatRequestVariableEntry[] {
468
if (!this.value) {
469
return [];
470
}
471
472
if (isStringImplicitContextValue(this.value)) {
473
return [
474
{
475
kind: 'string',
476
id: this.id,
477
name: this.name,
478
value: this.value.value ?? this.name,
479
modelDescription: this.modelDescription,
480
icon: this.value.icon,
481
uri: this.value.uri,
482
resourceUri: this.value.resourceUri,
483
handle: this.value.handle,
484
commandId: this.value.commandId
485
}
486
];
487
}
488
489
return [{
490
kind: 'file',
491
id: this.id,
492
name: this.name,
493
value: this.value,
494
modelDescription: this.modelDescription,
495
}];
496
}
497
498
}
499
500