Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompts/node/codeMapper/codeMapperService.ts
13405 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 type * as vscode from 'vscode';
7
8
import { NotebookDocumentSnapshot } from '../../../../platform/editing/common/notebookDocumentSnapshot';
9
import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';
10
import { EditSurvivalResult } from '../../../../platform/editSurvivalTracking/common/editSurvivalReporter';
11
import { IEditSurvivalTrackerService, IEditSurvivalTrackingSession } from '../../../../platform/editSurvivalTracking/common/editSurvivalTrackerService';
12
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
13
import { inferAlternativeNotebookContentFormat } from '../../../../platform/notebook/common/alternativeContent';
14
import { IAlternativeNotebookContentEditGenerator, NotebookEditGenrationSource } from '../../../../platform/notebook/common/alternativeContentEditGenerator';
15
import { INotebookService } from '../../../../platform/notebook/common/notebookService';
16
import { emitEditSurvivalEvent } from '../../../../platform/otel/common/genAiEvents';
17
import { GenAiMetrics } from '../../../../platform/otel/common/genAiMetrics';
18
import { IOTelService } from '../../../../platform/otel/common/otelService';
19
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';
20
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
21
import { findNotebook } from '../../../../util/common/notebooks';
22
import { createServiceIdentifier } from '../../../../util/common/services';
23
import { Queue } from '../../../../util/vs/base/common/async';
24
import { Disposable, toDisposable } from '../../../../util/vs/base/common/lifecycle';
25
import { ResourceMap } from '../../../../util/vs/base/common/map';
26
import { isEqual } from '../../../../util/vs/base/common/resources';
27
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
28
import { Range, TextEdit } from '../../../../vscodeTypes';
29
import { OutcomeAnnotation } from '../../../inlineChat/node/promptCraftingTypes';
30
import { IWorkingSet } from '../../../prompt/common/intents';
31
import { EXISTING_CODE_MARKER } from '../panel/codeBlockFormattingRules';
32
import { CodeMapper, CodeMapperOutcomeTelemetry, ICodeMapperDocument, ICodeMapperRequestInput, processFullRewriteNewNotebook } from './codeMapper';
33
34
export type CodeBlock = { readonly code: string; readonly resource: vscode.Uri; readonly markdownBeforeBlock?: string };
35
export type ResourceTextEdits = { readonly target: vscode.Uri; readonly edits: TextEdit | TextEdit[] };
36
37
export interface ICodeMapperTelemetryInfo {
38
readonly isAgent?: boolean;
39
readonly chatRequestId?: string;
40
readonly chatSessionId?: string;
41
readonly chatRequestSource?: string;
42
readonly chatRequestModel?: string;
43
}
44
45
export const ICodeMapperService = createServiceIdentifier<ICodeMapperService>('ICodeMapperService');
46
47
export interface IMapCodeRequest {
48
readonly codeBlock: CodeBlock;
49
readonly workingSet?: IWorkingSet;
50
readonly location?: string;
51
}
52
53
export interface IMapCodeResult {
54
readonly errorDetails?: vscode.ChatErrorDetails;
55
readonly annotations?: OutcomeAnnotation[];
56
readonly telemetry?: CodeMapperOutcomeTelemetry;
57
}
58
59
export interface ICodeMapperService {
60
readonly _serviceBrand: undefined;
61
mapCode(request: IMapCodeRequest, responseStream: vscode.MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: vscode.CancellationToken): Promise<IMapCodeResult | undefined>;
62
}
63
64
export class CodeMapperService extends Disposable implements ICodeMapperService {
65
66
readonly _serviceBrand: undefined;
67
68
private readonly _queues: ResourceMap<Queue<IMapCodeResult | undefined>> = new ResourceMap();
69
70
constructor(
71
@IInstantiationService private readonly instantiationService: IInstantiationService,
72
@INotebookService private readonly notebookService: INotebookService,
73
) {
74
super();
75
this._register(toDisposable(() => this._queues.clear()));
76
}
77
78
async mapCode(request: IMapCodeRequest, responseStream: vscode.MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: vscode.CancellationToken): Promise<IMapCodeResult | undefined> {
79
let queue = this._queues.get(request.codeBlock.resource);
80
if (!queue) {
81
queue = new Queue<IMapCodeResult | undefined>();
82
this._queues.set(request.codeBlock.resource, queue);
83
}
84
85
return queue.queue(() => this._doMapCode(request, responseStream, telemetryInfo, token));
86
}
87
88
private async _doMapCode(request: IMapCodeRequest, responseStream: vscode.MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: vscode.CancellationToken): Promise<IMapCodeResult | undefined> {
89
const codeMapper = this.notebookService.hasSupportedNotebooks(request.codeBlock.resource) ?
90
this.instantiationService.createInstance(NotebookCodeMapper) :
91
this.instantiationService.createInstance(DocumentCodeMapper);
92
93
return codeMapper.mapCode(request, responseStream, telemetryInfo, token);
94
}
95
}
96
97
class DocumentCodeMapper extends Disposable implements ICodeMapperService {
98
99
readonly _serviceBrand: undefined;
100
private readonly codeMapper: CodeMapper;
101
constructor(
102
@IInstantiationService private readonly instantiationService: IInstantiationService,
103
@IWorkspaceService private readonly _workspaceService: IWorkspaceService,
104
@ITelemetryService private readonly _telemetryService: ITelemetryService,
105
@IEditSurvivalTrackerService private readonly _editSurvivalTrackerService: IEditSurvivalTrackerService,
106
@IFileSystemService private readonly _fileSystemService: IFileSystemService,
107
@IOTelService private readonly _otelService: IOTelService,
108
) {
109
super();
110
this.codeMapper = this.instantiationService.createInstance(CodeMapper);
111
}
112
113
async mapCode(request: IMapCodeRequest, responseStream: vscode.MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: vscode.CancellationToken): Promise<IMapCodeResult | undefined> {
114
const { codeBlock } = request;
115
const documentContext = await this._getDocumentContextForCodeBlock(codeBlock);
116
if (token.isCancellationRequested) {
117
return undefined;
118
}
119
120
if ((!documentContext || (documentContext.getText().length === 0)) && !codeBlock.code.includes(EXISTING_CODE_MARKER)) {
121
// for non existing, empty file and no '...existing code... content, we can emit the code block as is
122
// Fast path: the base request already gave us the content to apply in full, we can avoid going to the speculative decoding endpoint
123
responseStream.textEdit(codeBlock.resource, new TextEdit(new Range(0, 0, 0, 0), codeBlock.code));
124
/* __GDPR__
125
"codemapper.completeCodeBlock" : {
126
"owner": "aeschli",
127
"comment": "Sent when a codemapper request is received for a complete code block that contains no ...existing code... comments."
128
}
129
*/
130
this._telemetryService.sendMSFTTelemetryEvent('codemapper.completeCodeBlock');
131
return {};
132
}
133
134
135
let editSurvivalTracker: IEditSurvivalTrackingSession | undefined;
136
// set up edit survival tracking currently only when we are modifying an existing document
137
if (documentContext) {
138
const tracker = editSurvivalTracker = this._editSurvivalTrackerService.initialize(documentContext.document);
139
responseStream = spyResponseStream(responseStream, (_target, edits) => { tracker.collectAIEdits(edits); });
140
}
141
142
const result = await mapCode(request, responseStream, documentContext, this.codeMapper, this._telemetryService, telemetryInfo, token);
143
const telemetry = result?.telemetry;
144
if (telemetry) {
145
editSurvivalTracker?.startReporter(res => reportEditSurvivalEvent(res, telemetry, this._otelService));
146
}
147
return result;
148
}
149
150
private async _getDocumentContextForCodeBlock(codeblock: CodeBlock): Promise<TextDocumentSnapshot | undefined> {
151
try {
152
const existingDoc = this._workspaceService.textDocuments.find(doc => isEqual(doc.uri, codeblock.resource));
153
if (existingDoc) {
154
return TextDocumentSnapshot.create(existingDoc);
155
}
156
157
const existsOnDisk = await this._fileSystemService.stat(codeblock.resource).then(() => true, () => false);
158
if (!existsOnDisk) {
159
return undefined;
160
}
161
162
return await this._workspaceService.openTextDocumentAndSnapshot(codeblock.resource);
163
} catch (ex) {
164
// ignore, probably an invalid URI or the like.
165
console.error(`Failed to get document context for ${codeblock.resource.toString()}`, ex);
166
return undefined;
167
}
168
}
169
}
170
171
class NotebookCodeMapper extends Disposable implements ICodeMapperService {
172
173
readonly _serviceBrand: undefined;
174
175
private readonly codeMapper: CodeMapper;
176
177
constructor(
178
@IInstantiationService private readonly instantiationService: IInstantiationService,
179
@IWorkspaceService private readonly _workspaceService: IWorkspaceService,
180
@ITelemetryService private readonly _telemetryService: ITelemetryService,
181
@IFileSystemService private readonly _fileSystemService: IFileSystemService,
182
@IAlternativeNotebookContentEditGenerator private readonly alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator,
183
) {
184
super();
185
this.codeMapper = this.instantiationService.createInstance(CodeMapper);
186
}
187
188
async mapCode(request: IMapCodeRequest, responseStream: vscode.MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: vscode.CancellationToken): Promise<IMapCodeResult | undefined> {
189
const { codeBlock } = request;
190
const documentContext = await this._getDocumentContextForCodeBlock(codeBlock);
191
if (token.isCancellationRequested) {
192
return undefined;
193
}
194
195
if ((!documentContext || (documentContext.getText().length === 0)) && !codeBlock.code.includes(EXISTING_CODE_MARKER)) {
196
// for non existing, empty file and no '...existing code... content, we can emit the code block as is
197
// Fast path: the base request already gave us the content to apply in full, we can avoid going to the speculative decoding endpoint
198
await processFullRewriteNewNotebook(codeBlock.resource, codeBlock.code, responseStream, this.alternativeNotebookEditGenerator, { source: NotebookEditGenrationSource.newNotebookIntent, model: telemetryInfo?.chatRequestModel, requestId: telemetryInfo?.chatRequestId }, token);
199
/* __GDPR__
200
"codemapper.completeCodeBlock" : {
201
"owner": "aeschli",
202
"comment": "Sent when a codemapper request is received for a complete code block that contains no ...existing code... comments."
203
}
204
*/
205
this._telemetryService.sendMSFTTelemetryEvent('codemapper.completeCodeBlock');
206
return {};
207
}
208
209
return mapCode(request, responseStream, documentContext, this.codeMapper, this._telemetryService, telemetryInfo, token);
210
}
211
212
private async _getDocumentContextForCodeBlock(codeblock: CodeBlock): Promise<ICodeMapperDocument | undefined> {
213
try {
214
const format = inferAlternativeNotebookContentFormat(codeblock.code);
215
const notebookDocument = findNotebook(codeblock.resource, this._workspaceService.notebookDocuments);
216
if (notebookDocument) {
217
return NotebookDocumentSnapshot.create(notebookDocument, format);
218
}
219
220
const existsOnDisk = await this._fileSystemService.stat(codeblock.resource).then(() => true, () => false);
221
if (!existsOnDisk) {
222
return undefined;
223
}
224
return await this._workspaceService.openNotebookDocumentAndSnapshot(codeblock.resource, format);
225
} catch (ex) {
226
// ignore, probably an invalid URI or the like.
227
console.error(`Failed to get document context for ${codeblock.resource.toString()}`, ex);
228
return undefined;
229
}
230
231
}
232
233
}
234
235
async function mapCode(request: IMapCodeRequest, responseStream: vscode.MappedEditsResponseStream, documentContext: ICodeMapperDocument | undefined, codeMapper: CodeMapper, telemetryService: ITelemetryService, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: vscode.CancellationToken): Promise<IMapCodeResult | undefined> {
236
const { codeBlock, workingSet, location } = request;
237
const requestInput: ICodeMapperRequestInput = (documentContext && (documentContext.getText().length > 0)) ?
238
{
239
createNew: false,
240
codeBlock: codeBlock.code,
241
uri: codeBlock.resource,
242
markdownBeforeBlock: codeBlock.markdownBeforeBlock,
243
existingDocument: documentContext,
244
location
245
} : {
246
createNew: true,
247
codeBlock: codeBlock.code,
248
uri: codeBlock.resource,
249
markdownBeforeBlock: codeBlock.markdownBeforeBlock,
250
existingDocument: undefined,
251
workingSet: workingSet?.map(entry => entry.document) || []
252
};
253
254
255
const result = await codeMapper.mapCode(requestInput, responseStream, telemetryInfo, token);
256
if (result) {
257
reportTelemetry(telemetryService, result);
258
}
259
return result;
260
261
}
262
function reportTelemetry(telemetryService: ITelemetryService, { telemetry, annotations }: IMapCodeResult) {
263
if (!telemetry) {
264
return; // cancelled
265
}
266
267
/* __GDPR__
268
"codemapper.request" : {
269
"owner": "aeschli",
270
"comment": "Metadata about the code mapper request",
271
"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." },
272
"requestSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The source from where the request was made" },
273
"mapper": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The mapper used: One of 'fast', 'fast-lora', 'full' and 'patch'" },
274
"outcomeAnnotations": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Annotations about the outcome of the request." }
275
}
276
*/
277
telemetryService.sendMSFTTelemetryEvent('codemapper.request', {
278
requestId: telemetry.requestId,
279
requestSource: telemetry.requestSource,
280
mapper: telemetry.mapper,
281
outcomeAnnotations: annotations?.map(a => a.label).join(','),
282
}, {
283
});
284
}
285
286
function spyResponseStream(responseStream: vscode.MappedEditsResponseStream, callback: (target: vscode.Uri, edits: TextEdit | TextEdit[]) => void): vscode.MappedEditsResponseStream {
287
return {
288
textEdit: (target: vscode.Uri, edits: TextEdit | TextEdit[]) => {
289
callback(target, edits);
290
responseStream.textEdit(target, edits);
291
},
292
notebookEdit(target, edits) {
293
responseStream.notebookEdit(target, edits);
294
},
295
};
296
}
297
298
function reportEditSurvivalEvent(res: EditSurvivalResult, { requestId, speculationRequestId, requestSource, mapper, chatRequestModel }: CodeMapperOutcomeTelemetry, otelService: IOTelService) {
299
300
/* __GDPR__
301
"codeMapper.trackEditSurvival" : {
302
"owner": "aeschli",
303
"comment": "Tracks how much percent of the AI edits survived after 5 minutes of accepting",
304
"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." },
305
"speculationRequestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the speculation request." },
306
"requestSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The source from where the request was made" },
307
"chatRequestModel": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model used for the base chat request to generate the edit object." },
308
"mapper": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The code mapper used: One of 'fast', 'fast-lora', 'full' and 'patch'" },
309
"survivalRateFourGram": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The rate between 0 and 1 of how much of the AI edit is still present in the document." },
310
"survivalRateNoRevert": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The rate between 0 and 1 of how much of the ranges the AI touched ended up being reverted." },
311
"didBranchChange": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Indicates if the branch changed in the meantime. If the branch changed (value is 1), this event should probably be ignored." },
312
"timeDelayMs": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The time delay between the user accepting the edit and measuring the survival rate." }
313
}
314
*/
315
res.telemetryService.sendMSFTTelemetryEvent('codeMapper.trackEditSurvival', { requestId, speculationRequestId, requestSource, chatRequestModel, mapper }, {
316
survivalRateFourGram: res.fourGram,
317
survivalRateNoRevert: res.noRevert,
318
timeDelayMs: res.timeDelayMs,
319
didBranchChange: res.didBranchChange ? 1 : 0,
320
});
321
res.telemetryService.sendInternalMSFTTelemetryEvent('codeMapper.trackEditSurvival', {
322
requestId,
323
speculationRequestId,
324
requestSource,
325
chatRequestModel,
326
mapper,
327
currentFileContent: res.currentFileContent,
328
textBeforeAiEdits: res.textBeforeAiEdits ? JSON.stringify(res.textBeforeAiEdits) : undefined,
329
textAfterAiEdits: res.textAfterAiEdits ? JSON.stringify(res.textAfterAiEdits) : undefined,
330
textAfterUserEdits: res.textAfterUserEdits ? JSON.stringify(res.textAfterUserEdits) : undefined,
331
}, {
332
survivalRateFourGram: res.fourGram,
333
survivalRateNoRevert: res.noRevert,
334
timeDelayMs: res.timeDelayMs,
335
didBranchChange: res.didBranchChange ? 1 : 0,
336
});
337
res.telemetryService.sendEnhancedGHTelemetryEvent('fastApply/trackEditSurvival', {
338
providerId: mapper,
339
headerRequestId: speculationRequestId,
340
completionTextJson: res.currentFileContent,
341
chatRequestModel,
342
requestSource,
343
headBranchName: res.workspace?.headBranchName,
344
headCommitHash: res.workspace?.headCommitHash,
345
remoteUrl: res.workspace?.remoteUrl,
346
fileRelativePath: res.workspace?.fileRelativePath,
347
}, {
348
timeDelayMs: res.timeDelayMs,
349
survivalRateFourGram: res.fourGram,
350
survivalRateNoRevert: res.noRevert,
351
});
352
353
emitEditSurvivalEvent(otelService, 'code_mapper', res.fourGram, res.noRevert, res.timeDelayMs, res.didBranchChange, requestId ?? '', res.workspace);
354
GenAiMetrics.recordEditSurvivalFourGram(otelService, 'code_mapper', res.fourGram, res.timeDelayMs);
355
GenAiMetrics.recordEditSurvivalNoRevert(otelService, 'code_mapper', res.noRevert, res.timeDelayMs);
356
}
357