Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentResolveService.ts
5263 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 { VSBuffer } from '../../../../../base/common/buffer.js';
7
import { Codicon } from '../../../../../base/common/codicons.js';
8
import { basename } from '../../../../../base/common/resources.js';
9
import { ThemeIcon } from '../../../../../base/common/themables.js';
10
import { URI } from '../../../../../base/common/uri.js';
11
import { IRange } from '../../../../../editor/common/core/range.js';
12
import { SymbolKinds } from '../../../../../editor/common/languages.js';
13
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
14
import { localize } from '../../../../../nls.js';
15
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
16
import { IDraggedResourceEditorInput, MarkerTransferData, DocumentSymbolTransferData, NotebookCellOutputTransferData } from '../../../../../platform/dnd/browser/dnd.js';
17
import { IFileService } from '../../../../../platform/files/common/files.js';
18
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
19
import { MarkerSeverity } from '../../../../../platform/markers/common/markers.js';
20
import { isUntitledResourceEditorInput } from '../../../../common/editor.js';
21
import { EditorInput } from '../../../../common/editor/editorInput.js';
22
import { IEditorService } from '../../../../services/editor/common/editorService.js';
23
import { IExtensionService, isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js';
24
import { UntitledTextEditorInput } from '../../../../services/untitled/common/untitledTextEditorInput.js';
25
import { createNotebookOutputVariableEntry, NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST } from '../../../notebook/browser/contrib/chat/notebookChatUtils.js';
26
import { getOutputViewModelFromId } from '../../../notebook/browser/controller/cellOutputActions.js';
27
import { getNotebookEditorFromEditorPane } from '../../../notebook/browser/notebookBrowser.js';
28
import { SCMHistoryItemTransferData } from '../../../scm/browser/scmHistoryChatContext.js';
29
import { CHAT_ATTACHABLE_IMAGE_MIME_TYPES, getAttachableImageExtension } from '../../common/model/chatModel.js';
30
import { IChatRequestVariableEntry, OmittedState, IDiagnosticVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry, toPromptFileVariableEntry, PromptFileVariableKind, ISCMHistoryItemVariableEntry } from '../../common/attachments/chatVariableEntries.js';
31
import { getPromptsTypeForLanguageId, PromptsType } from '../../common/promptSyntax/promptTypes.js';
32
import { imageToHash } from '../widget/input/editor/chatPasteProviders.js';
33
import { resizeImage } from '../chatImageUtils.js';
34
35
export const IChatAttachmentResolveService = createDecorator<IChatAttachmentResolveService>('IChatAttachmentResolveService');
36
37
export interface IChatAttachmentResolveService {
38
_serviceBrand: undefined;
39
40
resolveEditorAttachContext(editor: EditorInput | IDraggedResourceEditorInput): Promise<IChatRequestVariableEntry | undefined>;
41
resolveUntitledEditorAttachContext(editor: IDraggedResourceEditorInput): Promise<IChatRequestVariableEntry | undefined>;
42
resolveResourceAttachContext(resource: URI, isDirectory: boolean): Promise<IChatRequestVariableEntry | undefined>;
43
44
resolveImageEditorAttachContext(resource: URI, data?: VSBuffer, mimeType?: string): Promise<IChatRequestVariableEntry | undefined>;
45
resolveImageAttachContext(images: ImageTransferData[]): Promise<IChatRequestVariableEntry[]>;
46
resolveMarkerAttachContext(markers: MarkerTransferData[]): IDiagnosticVariableEntry[];
47
resolveSymbolsAttachContext(symbols: DocumentSymbolTransferData[]): ISymbolVariableEntry[];
48
resolveNotebookOutputAttachContext(data: NotebookCellOutputTransferData): IChatRequestVariableEntry[];
49
resolveSourceControlHistoryItemAttachContext(data: SCMHistoryItemTransferData[]): ISCMHistoryItemVariableEntry[];
50
resolveDirectoryImages(directoryUri: URI): Promise<IChatRequestVariableEntry[]>;
51
}
52
53
export class ChatAttachmentResolveService implements IChatAttachmentResolveService {
54
_serviceBrand: undefined;
55
56
constructor(
57
@IFileService private fileService: IFileService,
58
@IEditorService private editorService: IEditorService,
59
@ITextModelService private textModelService: ITextModelService,
60
@IExtensionService private extensionService: IExtensionService,
61
@IDialogService private dialogService: IDialogService
62
) { }
63
64
// --- EDITORS ---
65
66
public async resolveEditorAttachContext(editor: EditorInput | IDraggedResourceEditorInput): Promise<IChatRequestVariableEntry | undefined> {
67
// untitled editor
68
if (isUntitledResourceEditorInput(editor)) {
69
return await this.resolveUntitledEditorAttachContext(editor);
70
}
71
72
if (!editor.resource) {
73
return undefined;
74
}
75
76
let stat;
77
try {
78
stat = await this.fileService.stat(editor.resource);
79
} catch {
80
return undefined;
81
}
82
83
if (!stat.isDirectory && !stat.isFile) {
84
return undefined;
85
}
86
87
const imageContext = await this.resolveImageEditorAttachContext(editor.resource);
88
if (imageContext) {
89
return this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData')) ? imageContext : undefined;
90
}
91
92
return await this.resolveResourceAttachContext(editor.resource, stat.isDirectory);
93
}
94
95
public async resolveUntitledEditorAttachContext(editor: IDraggedResourceEditorInput): Promise<IChatRequestVariableEntry | undefined> {
96
// If the resource is known, we can use it directly
97
if (editor.resource) {
98
return await this.resolveResourceAttachContext(editor.resource, false);
99
}
100
101
// Otherwise, we need to check if the contents are already open in another editor
102
const openUntitledEditors = this.editorService.editors.filter(editor => editor instanceof UntitledTextEditorInput) as UntitledTextEditorInput[];
103
for (const canidate of openUntitledEditors) {
104
const model = await canidate.resolve();
105
const contents = model.textEditorModel?.getValue();
106
if (contents === editor.contents) {
107
return await this.resolveResourceAttachContext(canidate.resource, false);
108
}
109
}
110
111
return undefined;
112
}
113
114
public async resolveResourceAttachContext(resource: URI, isDirectory: boolean): Promise<IChatRequestVariableEntry | undefined> {
115
let omittedState = OmittedState.NotOmitted;
116
117
if (!isDirectory) {
118
119
let languageId: string | undefined;
120
try {
121
const createdModel = await this.textModelService.createModelReference(resource);
122
languageId = createdModel.object.getLanguageId();
123
createdModel.dispose();
124
} catch {
125
omittedState = OmittedState.Full;
126
}
127
128
if (/\.(svg)$/i.test(resource.path)) {
129
omittedState = OmittedState.Full;
130
}
131
if (languageId) {
132
const promptsType = getPromptsTypeForLanguageId(languageId);
133
if (promptsType === PromptsType.prompt) {
134
return toPromptFileVariableEntry(resource, PromptFileVariableKind.PromptFile);
135
} else if (promptsType === PromptsType.instructions) {
136
return toPromptFileVariableEntry(resource, PromptFileVariableKind.Instruction);
137
}
138
}
139
}
140
141
return {
142
kind: isDirectory ? 'directory' : 'file',
143
value: resource,
144
id: resource.toString(),
145
name: basename(resource),
146
omittedState
147
};
148
}
149
150
// --- IMAGES ---
151
152
public async resolveImageEditorAttachContext(resource: URI, data?: VSBuffer, mimeType?: string): Promise<IChatRequestVariableEntry | undefined> {
153
if (!resource) {
154
return undefined;
155
}
156
157
if (mimeType) {
158
if (!getAttachableImageExtension(mimeType)) {
159
return undefined;
160
}
161
} else {
162
const match = SUPPORTED_IMAGE_EXTENSIONS_REGEX.exec(resource.path);
163
if (!match) {
164
return undefined;
165
}
166
167
mimeType = getMimeTypeFromPath(match);
168
}
169
const fileName = basename(resource);
170
171
let dataBuffer: VSBuffer | undefined;
172
if (data) {
173
dataBuffer = data;
174
} else {
175
176
let stat;
177
try {
178
stat = await this.fileService.stat(resource);
179
} catch {
180
return undefined;
181
}
182
183
const readFile = await this.fileService.readFile(resource);
184
185
if (stat.size > 30 * 1024 * 1024) { // 30 MB
186
this.dialogService.error(localize('imageTooLarge', 'Image is too large'), localize('imageTooLargeMessage', 'The image {0} is too large to be attached.', fileName));
187
throw new Error('Image is too large');
188
}
189
190
dataBuffer = readFile.value;
191
}
192
193
const isPartiallyOmitted = /\.gif$/i.test(resource.path);
194
const imageFileContext = await this.resolveImageAttachContext([{
195
id: resource.toString(),
196
name: fileName,
197
data: dataBuffer.buffer,
198
icon: Codicon.fileMedia,
199
resource: resource,
200
mimeType: mimeType,
201
omittedState: isPartiallyOmitted ? OmittedState.Partial : OmittedState.NotOmitted
202
}]);
203
204
return imageFileContext[0];
205
}
206
207
public resolveImageAttachContext(images: ImageTransferData[]): Promise<IChatRequestVariableEntry[]> {
208
return Promise.all(images.map(async image => ({
209
id: image.id || await imageToHash(image.data),
210
name: image.name,
211
fullName: image.resource ? image.resource.path : undefined,
212
value: await resizeImage(image.data, image.mimeType),
213
icon: image.icon,
214
kind: 'image',
215
isFile: false,
216
isDirectory: false,
217
omittedState: image.omittedState || OmittedState.NotOmitted,
218
references: image.resource ? [{ reference: image.resource, kind: 'reference' }] : []
219
})));
220
}
221
222
// --- MARKERS ---
223
224
public resolveMarkerAttachContext(markers: MarkerTransferData[]): IDiagnosticVariableEntry[] {
225
return markers.map((marker): IDiagnosticVariableEntry => {
226
let filter: IDiagnosticVariableEntryFilterData;
227
if (!('severity' in marker)) {
228
filter = { filterUri: URI.revive(marker.uri), filterSeverity: MarkerSeverity.Warning };
229
} else {
230
filter = IDiagnosticVariableEntryFilterData.fromMarker(marker);
231
}
232
233
return IDiagnosticVariableEntryFilterData.toEntry(filter);
234
});
235
}
236
237
// --- SYMBOLS ---
238
239
public resolveSymbolsAttachContext(symbols: DocumentSymbolTransferData[]): ISymbolVariableEntry[] {
240
return symbols.map(symbol => {
241
const resource = URI.file(symbol.fsPath);
242
return {
243
kind: 'symbol',
244
id: symbolId(resource, symbol.range),
245
value: { uri: resource, range: symbol.range },
246
symbolKind: symbol.kind,
247
icon: SymbolKinds.toIcon(symbol.kind),
248
fullName: symbol.name,
249
name: symbol.name,
250
};
251
});
252
}
253
254
// --- NOTEBOOKS ---
255
256
public resolveNotebookOutputAttachContext(data: NotebookCellOutputTransferData): IChatRequestVariableEntry[] {
257
const notebookEditor = getNotebookEditorFromEditorPane(this.editorService.activeEditorPane);
258
if (!notebookEditor) {
259
return [];
260
}
261
262
const outputViewModel = getOutputViewModelFromId(data.outputId, notebookEditor);
263
if (!outputViewModel) {
264
return [];
265
}
266
267
const mimeType = outputViewModel.pickedMimeType?.mimeType;
268
if (mimeType && NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST.includes(mimeType)) {
269
270
const entry = createNotebookOutputVariableEntry(outputViewModel, mimeType, notebookEditor);
271
if (!entry) {
272
return [];
273
}
274
275
return [entry];
276
}
277
278
return [];
279
}
280
281
// --- DIRECTORIES ---
282
283
public async resolveDirectoryImages(directoryUri: URI): Promise<IChatRequestVariableEntry[]> {
284
const imageEntries: IChatRequestVariableEntry[] = [];
285
await this._collectDirectoryImages(directoryUri, imageEntries);
286
return imageEntries;
287
}
288
289
private async _collectDirectoryImages(directoryUri: URI, results: IChatRequestVariableEntry[]): Promise<void> {
290
let stat;
291
try {
292
stat = await this.fileService.resolve(directoryUri);
293
} catch {
294
return;
295
}
296
297
if (!stat.children) {
298
return;
299
}
300
301
const childPromises: Promise<void>[] = [];
302
303
for (const child of stat.children) {
304
if (child.isDirectory && !child.isSymbolicLink) {
305
childPromises.push(this._collectDirectoryImages(child.resource, results));
306
} else if (child.isFile && !child.isSymbolicLink && SUPPORTED_IMAGE_EXTENSIONS_REGEX.test(child.resource.path)) {
307
childPromises.push(
308
this.resolveImageEditorAttachContext(child.resource).then(entry => {
309
if (entry) {
310
results.push(entry);
311
}
312
}).catch(() => { /* skip unreadable images */ })
313
);
314
}
315
}
316
317
await Promise.all(childPromises);
318
}
319
320
// --- SOURCE CONTROL ---
321
322
public resolveSourceControlHistoryItemAttachContext(data: SCMHistoryItemTransferData[]): ISCMHistoryItemVariableEntry[] {
323
return data.map(d => ({
324
id: d.historyItem.id,
325
name: d.name,
326
value: URI.revive(d.resource),
327
historyItem: {
328
...d.historyItem,
329
references: []
330
},
331
kind: 'scmHistoryItem'
332
} satisfies ISCMHistoryItemVariableEntry));
333
}
334
}
335
336
function symbolId(resource: URI, range?: IRange): string {
337
let rangePart = '';
338
if (range) {
339
rangePart = `:${range.startLineNumber}`;
340
if (range.startLineNumber !== range.endLineNumber) {
341
rangePart += `-${range.endLineNumber}`;
342
}
343
}
344
return resource.fsPath + rangePart;
345
}
346
347
export type ImageTransferData = {
348
data: Uint8Array;
349
name: string;
350
icon?: ThemeIcon;
351
resource?: URI;
352
id?: string;
353
mimeType?: string;
354
omittedState?: OmittedState;
355
};
356
const SUPPORTED_IMAGE_EXTENSIONS_REGEX = new RegExp(`\\.(${Object.keys(CHAT_ATTACHABLE_IMAGE_MIME_TYPES).join('|')})$`, 'i');
357
358
function getMimeTypeFromPath(match: RegExpExecArray): string | undefined {
359
const ext = match[1].toLowerCase();
360
return CHAT_ATTACHABLE_IMAGE_MIME_TYPES[ext];
361
}
362
363