Path: blob/main/extensions/copilot/src/extension/prompts/node/codeMapper/codeMapperService.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type * as vscode from 'vscode';67import { NotebookDocumentSnapshot } from '../../../../platform/editing/common/notebookDocumentSnapshot';8import { TextDocumentSnapshot } from '../../../../platform/editing/common/textDocumentSnapshot';9import { EditSurvivalResult } from '../../../../platform/editSurvivalTracking/common/editSurvivalReporter';10import { IEditSurvivalTrackerService, IEditSurvivalTrackingSession } from '../../../../platform/editSurvivalTracking/common/editSurvivalTrackerService';11import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';12import { inferAlternativeNotebookContentFormat } from '../../../../platform/notebook/common/alternativeContent';13import { IAlternativeNotebookContentEditGenerator, NotebookEditGenrationSource } from '../../../../platform/notebook/common/alternativeContentEditGenerator';14import { INotebookService } from '../../../../platform/notebook/common/notebookService';15import { emitEditSurvivalEvent } from '../../../../platform/otel/common/genAiEvents';16import { GenAiMetrics } from '../../../../platform/otel/common/genAiMetrics';17import { IOTelService } from '../../../../platform/otel/common/otelService';18import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry';19import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';20import { findNotebook } from '../../../../util/common/notebooks';21import { createServiceIdentifier } from '../../../../util/common/services';22import { Queue } from '../../../../util/vs/base/common/async';23import { Disposable, toDisposable } from '../../../../util/vs/base/common/lifecycle';24import { ResourceMap } from '../../../../util/vs/base/common/map';25import { isEqual } from '../../../../util/vs/base/common/resources';26import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';27import { Range, TextEdit } from '../../../../vscodeTypes';28import { OutcomeAnnotation } from '../../../inlineChat/node/promptCraftingTypes';29import { IWorkingSet } from '../../../prompt/common/intents';30import { EXISTING_CODE_MARKER } from '../panel/codeBlockFormattingRules';31import { CodeMapper, CodeMapperOutcomeTelemetry, ICodeMapperDocument, ICodeMapperRequestInput, processFullRewriteNewNotebook } from './codeMapper';3233export type CodeBlock = { readonly code: string; readonly resource: vscode.Uri; readonly markdownBeforeBlock?: string };34export type ResourceTextEdits = { readonly target: vscode.Uri; readonly edits: TextEdit | TextEdit[] };3536export interface ICodeMapperTelemetryInfo {37readonly isAgent?: boolean;38readonly chatRequestId?: string;39readonly chatSessionId?: string;40readonly chatRequestSource?: string;41readonly chatRequestModel?: string;42}4344export const ICodeMapperService = createServiceIdentifier<ICodeMapperService>('ICodeMapperService');4546export interface IMapCodeRequest {47readonly codeBlock: CodeBlock;48readonly workingSet?: IWorkingSet;49readonly location?: string;50}5152export interface IMapCodeResult {53readonly errorDetails?: vscode.ChatErrorDetails;54readonly annotations?: OutcomeAnnotation[];55readonly telemetry?: CodeMapperOutcomeTelemetry;56}5758export interface ICodeMapperService {59readonly _serviceBrand: undefined;60mapCode(request: IMapCodeRequest, responseStream: vscode.MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: vscode.CancellationToken): Promise<IMapCodeResult | undefined>;61}6263export class CodeMapperService extends Disposable implements ICodeMapperService {6465readonly _serviceBrand: undefined;6667private readonly _queues: ResourceMap<Queue<IMapCodeResult | undefined>> = new ResourceMap();6869constructor(70@IInstantiationService private readonly instantiationService: IInstantiationService,71@INotebookService private readonly notebookService: INotebookService,72) {73super();74this._register(toDisposable(() => this._queues.clear()));75}7677async mapCode(request: IMapCodeRequest, responseStream: vscode.MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: vscode.CancellationToken): Promise<IMapCodeResult | undefined> {78let queue = this._queues.get(request.codeBlock.resource);79if (!queue) {80queue = new Queue<IMapCodeResult | undefined>();81this._queues.set(request.codeBlock.resource, queue);82}8384return queue.queue(() => this._doMapCode(request, responseStream, telemetryInfo, token));85}8687private async _doMapCode(request: IMapCodeRequest, responseStream: vscode.MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: vscode.CancellationToken): Promise<IMapCodeResult | undefined> {88const codeMapper = this.notebookService.hasSupportedNotebooks(request.codeBlock.resource) ?89this.instantiationService.createInstance(NotebookCodeMapper) :90this.instantiationService.createInstance(DocumentCodeMapper);9192return codeMapper.mapCode(request, responseStream, telemetryInfo, token);93}94}9596class DocumentCodeMapper extends Disposable implements ICodeMapperService {9798readonly _serviceBrand: undefined;99private readonly codeMapper: CodeMapper;100constructor(101@IInstantiationService private readonly instantiationService: IInstantiationService,102@IWorkspaceService private readonly _workspaceService: IWorkspaceService,103@ITelemetryService private readonly _telemetryService: ITelemetryService,104@IEditSurvivalTrackerService private readonly _editSurvivalTrackerService: IEditSurvivalTrackerService,105@IFileSystemService private readonly _fileSystemService: IFileSystemService,106@IOTelService private readonly _otelService: IOTelService,107) {108super();109this.codeMapper = this.instantiationService.createInstance(CodeMapper);110}111112async mapCode(request: IMapCodeRequest, responseStream: vscode.MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: vscode.CancellationToken): Promise<IMapCodeResult | undefined> {113const { codeBlock } = request;114const documentContext = await this._getDocumentContextForCodeBlock(codeBlock);115if (token.isCancellationRequested) {116return undefined;117}118119if ((!documentContext || (documentContext.getText().length === 0)) && !codeBlock.code.includes(EXISTING_CODE_MARKER)) {120// for non existing, empty file and no '...existing code... content, we can emit the code block as is121// Fast path: the base request already gave us the content to apply in full, we can avoid going to the speculative decoding endpoint122responseStream.textEdit(codeBlock.resource, new TextEdit(new Range(0, 0, 0, 0), codeBlock.code));123/* __GDPR__124"codemapper.completeCodeBlock" : {125"owner": "aeschli",126"comment": "Sent when a codemapper request is received for a complete code block that contains no ...existing code... comments."127}128*/129this._telemetryService.sendMSFTTelemetryEvent('codemapper.completeCodeBlock');130return {};131}132133134let editSurvivalTracker: IEditSurvivalTrackingSession | undefined;135// set up edit survival tracking currently only when we are modifying an existing document136if (documentContext) {137const tracker = editSurvivalTracker = this._editSurvivalTrackerService.initialize(documentContext.document);138responseStream = spyResponseStream(responseStream, (_target, edits) => { tracker.collectAIEdits(edits); });139}140141const result = await mapCode(request, responseStream, documentContext, this.codeMapper, this._telemetryService, telemetryInfo, token);142const telemetry = result?.telemetry;143if (telemetry) {144editSurvivalTracker?.startReporter(res => reportEditSurvivalEvent(res, telemetry, this._otelService));145}146return result;147}148149private async _getDocumentContextForCodeBlock(codeblock: CodeBlock): Promise<TextDocumentSnapshot | undefined> {150try {151const existingDoc = this._workspaceService.textDocuments.find(doc => isEqual(doc.uri, codeblock.resource));152if (existingDoc) {153return TextDocumentSnapshot.create(existingDoc);154}155156const existsOnDisk = await this._fileSystemService.stat(codeblock.resource).then(() => true, () => false);157if (!existsOnDisk) {158return undefined;159}160161return await this._workspaceService.openTextDocumentAndSnapshot(codeblock.resource);162} catch (ex) {163// ignore, probably an invalid URI or the like.164console.error(`Failed to get document context for ${codeblock.resource.toString()}`, ex);165return undefined;166}167}168}169170class NotebookCodeMapper extends Disposable implements ICodeMapperService {171172readonly _serviceBrand: undefined;173174private readonly codeMapper: CodeMapper;175176constructor(177@IInstantiationService private readonly instantiationService: IInstantiationService,178@IWorkspaceService private readonly _workspaceService: IWorkspaceService,179@ITelemetryService private readonly _telemetryService: ITelemetryService,180@IFileSystemService private readonly _fileSystemService: IFileSystemService,181@IAlternativeNotebookContentEditGenerator private readonly alternativeNotebookEditGenerator: IAlternativeNotebookContentEditGenerator,182) {183super();184this.codeMapper = this.instantiationService.createInstance(CodeMapper);185}186187async mapCode(request: IMapCodeRequest, responseStream: vscode.MappedEditsResponseStream, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: vscode.CancellationToken): Promise<IMapCodeResult | undefined> {188const { codeBlock } = request;189const documentContext = await this._getDocumentContextForCodeBlock(codeBlock);190if (token.isCancellationRequested) {191return undefined;192}193194if ((!documentContext || (documentContext.getText().length === 0)) && !codeBlock.code.includes(EXISTING_CODE_MARKER)) {195// for non existing, empty file and no '...existing code... content, we can emit the code block as is196// Fast path: the base request already gave us the content to apply in full, we can avoid going to the speculative decoding endpoint197await processFullRewriteNewNotebook(codeBlock.resource, codeBlock.code, responseStream, this.alternativeNotebookEditGenerator, { source: NotebookEditGenrationSource.newNotebookIntent, model: telemetryInfo?.chatRequestModel, requestId: telemetryInfo?.chatRequestId }, token);198/* __GDPR__199"codemapper.completeCodeBlock" : {200"owner": "aeschli",201"comment": "Sent when a codemapper request is received for a complete code block that contains no ...existing code... comments."202}203*/204this._telemetryService.sendMSFTTelemetryEvent('codemapper.completeCodeBlock');205return {};206}207208return mapCode(request, responseStream, documentContext, this.codeMapper, this._telemetryService, telemetryInfo, token);209}210211private async _getDocumentContextForCodeBlock(codeblock: CodeBlock): Promise<ICodeMapperDocument | undefined> {212try {213const format = inferAlternativeNotebookContentFormat(codeblock.code);214const notebookDocument = findNotebook(codeblock.resource, this._workspaceService.notebookDocuments);215if (notebookDocument) {216return NotebookDocumentSnapshot.create(notebookDocument, format);217}218219const existsOnDisk = await this._fileSystemService.stat(codeblock.resource).then(() => true, () => false);220if (!existsOnDisk) {221return undefined;222}223return await this._workspaceService.openNotebookDocumentAndSnapshot(codeblock.resource, format);224} catch (ex) {225// ignore, probably an invalid URI or the like.226console.error(`Failed to get document context for ${codeblock.resource.toString()}`, ex);227return undefined;228}229230}231232}233234async function mapCode(request: IMapCodeRequest, responseStream: vscode.MappedEditsResponseStream, documentContext: ICodeMapperDocument | undefined, codeMapper: CodeMapper, telemetryService: ITelemetryService, telemetryInfo: ICodeMapperTelemetryInfo | undefined, token: vscode.CancellationToken): Promise<IMapCodeResult | undefined> {235const { codeBlock, workingSet, location } = request;236const requestInput: ICodeMapperRequestInput = (documentContext && (documentContext.getText().length > 0)) ?237{238createNew: false,239codeBlock: codeBlock.code,240uri: codeBlock.resource,241markdownBeforeBlock: codeBlock.markdownBeforeBlock,242existingDocument: documentContext,243location244} : {245createNew: true,246codeBlock: codeBlock.code,247uri: codeBlock.resource,248markdownBeforeBlock: codeBlock.markdownBeforeBlock,249existingDocument: undefined,250workingSet: workingSet?.map(entry => entry.document) || []251};252253254const result = await codeMapper.mapCode(requestInput, responseStream, telemetryInfo, token);255if (result) {256reportTelemetry(telemetryService, result);257}258return result;259260}261function reportTelemetry(telemetryService: ITelemetryService, { telemetry, annotations }: IMapCodeResult) {262if (!telemetry) {263return; // cancelled264}265266/* __GDPR__267"codemapper.request" : {268"owner": "aeschli",269"comment": "Metadata about the code mapper request",270"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." },271"requestSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The source from where the request was made" },272"mapper": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The mapper used: One of 'fast', 'fast-lora', 'full' and 'patch'" },273"outcomeAnnotations": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Annotations about the outcome of the request." }274}275*/276telemetryService.sendMSFTTelemetryEvent('codemapper.request', {277requestId: telemetry.requestId,278requestSource: telemetry.requestSource,279mapper: telemetry.mapper,280outcomeAnnotations: annotations?.map(a => a.label).join(','),281}, {282});283}284285function spyResponseStream(responseStream: vscode.MappedEditsResponseStream, callback: (target: vscode.Uri, edits: TextEdit | TextEdit[]) => void): vscode.MappedEditsResponseStream {286return {287textEdit: (target: vscode.Uri, edits: TextEdit | TextEdit[]) => {288callback(target, edits);289responseStream.textEdit(target, edits);290},291notebookEdit(target, edits) {292responseStream.notebookEdit(target, edits);293},294};295}296297function reportEditSurvivalEvent(res: EditSurvivalResult, { requestId, speculationRequestId, requestSource, mapper, chatRequestModel }: CodeMapperOutcomeTelemetry, otelService: IOTelService) {298299/* __GDPR__300"codeMapper.trackEditSurvival" : {301"owner": "aeschli",302"comment": "Tracks how much percent of the AI edits survived after 5 minutes of accepting",303"requestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the current request turn." },304"speculationRequestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The id of the speculation request." },305"requestSource": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The source from where the request was made" },306"chatRequestModel": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The model used for the base chat request to generate the edit object." },307"mapper": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The code mapper used: One of 'fast', 'fast-lora', 'full' and 'patch'" },308"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." },309"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." },310"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." },311"timeDelayMs": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "The time delay between the user accepting the edit and measuring the survival rate." }312}313*/314res.telemetryService.sendMSFTTelemetryEvent('codeMapper.trackEditSurvival', { requestId, speculationRequestId, requestSource, chatRequestModel, mapper }, {315survivalRateFourGram: res.fourGram,316survivalRateNoRevert: res.noRevert,317timeDelayMs: res.timeDelayMs,318didBranchChange: res.didBranchChange ? 1 : 0,319});320res.telemetryService.sendInternalMSFTTelemetryEvent('codeMapper.trackEditSurvival', {321requestId,322speculationRequestId,323requestSource,324chatRequestModel,325mapper,326currentFileContent: res.currentFileContent,327textBeforeAiEdits: res.textBeforeAiEdits ? JSON.stringify(res.textBeforeAiEdits) : undefined,328textAfterAiEdits: res.textAfterAiEdits ? JSON.stringify(res.textAfterAiEdits) : undefined,329textAfterUserEdits: res.textAfterUserEdits ? JSON.stringify(res.textAfterUserEdits) : undefined,330}, {331survivalRateFourGram: res.fourGram,332survivalRateNoRevert: res.noRevert,333timeDelayMs: res.timeDelayMs,334didBranchChange: res.didBranchChange ? 1 : 0,335});336res.telemetryService.sendEnhancedGHTelemetryEvent('fastApply/trackEditSurvival', {337providerId: mapper,338headerRequestId: speculationRequestId,339completionTextJson: res.currentFileContent,340chatRequestModel,341requestSource,342headBranchName: res.workspace?.headBranchName,343headCommitHash: res.workspace?.headCommitHash,344remoteUrl: res.workspace?.remoteUrl,345fileRelativePath: res.workspace?.fileRelativePath,346}, {347timeDelayMs: res.timeDelayMs,348survivalRateFourGram: res.fourGram,349survivalRateNoRevert: res.noRevert,350});351352emitEditSurvivalEvent(otelService, 'code_mapper', res.fourGram, res.noRevert, res.timeDelayMs, res.didBranchChange, requestId ?? '', res.workspace);353GenAiMetrics.recordEditSurvivalFourGram(otelService, 'code_mapper', res.fourGram, res.timeDelayMs);354GenAiMetrics.recordEditSurvivalNoRevert(otelService, 'code_mapper', res.noRevert, res.timeDelayMs);355}356357