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