Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineEdits/vscode-node/features/diagnosticsInlineEditProvider.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 { Command } from 'vscode';
7
import * as vscode from 'vscode';
8
import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/documentId';
9
import { InlineEditRequestLogContext } from '../../../../platform/inlineEdits/common/inlineEditLogContext';
10
import { ObservableGit } from '../../../../platform/inlineEdits/common/observableGit';
11
import { ILogService, ILogger } from '../../../../platform/log/common/logService';
12
import { ErrorUtils } from '../../../../util/common/errors';
13
import { raceCancellation, timeout } from '../../../../util/vs/base/common/async';
14
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
15
import { BugIndicatingError } from '../../../../util/vs/base/common/errors';
16
import { Disposable, DisposableStore } from '../../../../util/vs/base/common/lifecycle';
17
import { StringReplacement } from '../../../../util/vs/editor/common/core/edits/stringEdit';
18
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
19
import { INextEditProvider, NESInlineCompletionContext, NesOutcome } from '../../node/nextEditProvider';
20
import { DiagnosticsTelemetryBuilder } from '../../node/nextEditProviderTelemetry';
21
import { INextEditDisplayLocation, INextEditResult } from '../../node/nextEditResult';
22
import { VSCodeWorkspace } from '../parts/vscodeWorkspace';
23
import { DiagnosticCompletionItem } from './diagnosticsBasedCompletions/diagnosticsCompletions';
24
import { DiagnosticCompletionState, DiagnosticsCompletionProcessor } from './diagnosticsCompletionProcessor';
25
26
export class DiagnosticsNextEditResult implements INextEditResult {
27
constructor(
28
public readonly requestId: number,
29
public readonly result: {
30
edit: StringReplacement;
31
displayLocation?: INextEditDisplayLocation;
32
item: DiagnosticCompletionItem;
33
action?: Command;
34
} | undefined,
35
public workInProgress: boolean = false
36
) { }
37
}
38
39
export class DiagnosticsNextEditProvider extends Disposable implements INextEditProvider<DiagnosticsNextEditResult, DiagnosticsTelemetryBuilder, boolean> {
40
public readonly ID = 'DiagnosticsNextEditProvider';
41
42
private _lastRejectionTime: number = 0;
43
public get lastRejectionTime(): number {
44
return this._lastRejectionTime;
45
}
46
47
private _lastTriggerTime: number = 0;
48
public get lastTriggerTime(): number {
49
return this._lastTriggerTime;
50
}
51
52
private _lastOutcome: NesOutcome | undefined;
53
public get lastOutcome(): NesOutcome | undefined {
54
return this._lastOutcome;
55
}
56
57
private readonly _diagnosticsCompletionHandler: DiagnosticsCompletionProcessor;
58
private _logger: ILogger;
59
60
constructor(
61
workspace: VSCodeWorkspace,
62
git: ObservableGit,
63
@IInstantiationService instantiationService: IInstantiationService,
64
@ILogService logService: ILogService,
65
) {
66
super();
67
68
this._logger = logService.createSubLogger(['NES', 'DiagnosticsNextEditProvider']);
69
this._diagnosticsCompletionHandler = this._register(instantiationService.createInstance(DiagnosticsCompletionProcessor, workspace, git));
70
}
71
72
async getNextEdit(docId: DocumentId, context: NESInlineCompletionContext, logContext: InlineEditRequestLogContext, cancellationToken: CancellationToken, tb: DiagnosticsTelemetryBuilder): Promise<DiagnosticsNextEditResult> {
73
this._lastTriggerTime = Date.now();
74
throw new BugIndicatingError('DiagnosticsNextEditProvider does not support getNextEdit, use runUntilNextEdit instead');
75
}
76
77
async runUntilNextEdit(docId: DocumentId, context: NESInlineCompletionContext, logContext: InlineEditRequestLogContext, delayStart: number, cancellationToken: CancellationToken, tb: DiagnosticsTelemetryBuilder): Promise<DiagnosticsNextEditResult> {
78
try {
79
await timeout(delayStart);
80
if (cancellationToken.isCancellationRequested) {
81
this._logger.trace('cancellationRequested before started');
82
return new DiagnosticsNextEditResult(logContext.requestId, undefined);
83
}
84
85
// Check if the last computed edit is still valid
86
const initialResult = this._getResultForCurrentState(docId, logContext, tb);
87
if (initialResult.result) {
88
return initialResult;
89
}
90
91
const asyncResult = await raceCancellation(new Promise<DiagnosticsNextEditResult>((resolve) => {
92
const disposables = new DisposableStore();
93
const complete = (result: DiagnosticsNextEditResult) => {
94
resolve(result);
95
disposables.dispose();
96
};
97
98
disposables.add(this._diagnosticsCompletionHandler.onDidChange(() => {
99
const completionResult = this._getResultForCurrentState(docId, logContext, tb);
100
if (completionResult.result || !completionResult.workInProgress) {
101
complete(completionResult);
102
}
103
}));
104
105
disposables.add(cancellationToken.onCancellationRequested(() => {
106
disposables.dispose();
107
}));
108
}), cancellationToken);
109
110
return asyncResult ?? initialResult;
111
} catch (error) {
112
const errorMessage = `Error occurred while waiting for diagnostic edit: ${ErrorUtils.toString(ErrorUtils.fromUnknown(error))}`;
113
logContext.addLog(errorMessage);
114
this._logger.trace(errorMessage);
115
return new DiagnosticsNextEditResult(logContext.requestId, undefined);
116
} finally {
117
this._logger.trace('DiagnosticsInlineCompletionProvider runUntilNextEdit complete' + (cancellationToken.isCancellationRequested ? ' (cancelled)' : ''));
118
}
119
}
120
121
private _getResultForCurrentState(docId: DocumentId, logContext: InlineEditRequestLogContext, tb: DiagnosticsTelemetryBuilder): DiagnosticsNextEditResult {
122
const completionResult = this._diagnosticsCompletionHandler.getCurrentState(docId);
123
const telemetry = new DiagnosticsTelemetryBuilder();
124
const diagnosticEditResult = this._createNextEditResult(completionResult, logContext, telemetry);
125
if (diagnosticEditResult.result) {
126
telemetry.populate(tb);
127
}
128
return diagnosticEditResult;
129
}
130
131
private _createNextEditResult(diagnosticEditResult: DiagnosticCompletionState, logContext: InlineEditRequestLogContext, tb: DiagnosticsTelemetryBuilder): DiagnosticsNextEditResult {
132
const { item, telemetry } = diagnosticEditResult;
133
134
// Diagnostics might not have updated yet since accepting a diagnostics based NES
135
if (item && this._hasRecentlyBeenAccepted(item)) {
136
tb.addDroppedReason(`${item.type}:recently-accepted`);
137
this._logger.trace('recently accepted');
138
return new DiagnosticsNextEditResult(logContext.requestId, undefined, diagnosticEditResult.workInProgress);
139
}
140
141
telemetry.droppedReasons.forEach(reason => tb.addDroppedReason(reason));
142
tb.setDiagnosticRunTelemetry(telemetry);
143
144
if (!item) {
145
this._logger.trace('no diagnostic edit result');
146
return new DiagnosticsNextEditResult(logContext.requestId, undefined, diagnosticEditResult.workInProgress);
147
}
148
149
tb.setType(item.type);
150
logContext.setDiagnosticsResult(item.getRootedLineEdit());
151
152
this._logger.trace(`created next edit result`);
153
154
return new DiagnosticsNextEditResult(logContext.requestId, {
155
edit: item.toOffsetEdit(),
156
displayLocation: item.nextEditDisplayLocation,
157
item
158
}, diagnosticEditResult.workInProgress);
159
}
160
161
handleShown(suggestion: DiagnosticsNextEditResult): void { }
162
163
handleAcceptance(docId: DocumentId, suggestion: DiagnosticsNextEditResult): void {
164
const completionResult = suggestion.result;
165
if (!completionResult) {
166
throw new BugIndicatingError('Completion result is undefined when accepted');
167
}
168
169
this._lastAcceptedItem = { item: completionResult.item, time: Date.now() };
170
this._lastOutcome = NesOutcome.Accepted;
171
this._diagnosticsCompletionHandler.handleEndOfLifetime(completionResult.item, { kind: vscode.InlineCompletionEndOfLifeReasonKind.Accepted });
172
}
173
174
private _lastAcceptedItem: { item: DiagnosticCompletionItem; time: number } | undefined = undefined;
175
private _hasRecentlyBeenAccepted(item: DiagnosticCompletionItem): boolean {
176
if (!this._lastAcceptedItem) {
177
return false;
178
}
179
180
if (Date.now() - this._lastAcceptedItem.time >= 1000) {
181
return false;
182
}
183
184
return item.diagnostic.equals(this._lastAcceptedItem.item.diagnostic) || DiagnosticCompletionItem.equals(this._lastAcceptedItem.item, item);
185
}
186
187
handleRejection(docId: DocumentId, suggestion: DiagnosticsNextEditResult): void {
188
this._lastRejectionTime = Date.now();
189
this._lastOutcome = NesOutcome.Rejected;
190
191
const completionResult = suggestion.result;
192
if (!completionResult) {
193
throw new BugIndicatingError('Completion result is undefined when rejected');
194
}
195
196
this._diagnosticsCompletionHandler.handleEndOfLifetime(completionResult.item, { kind: vscode.InlineCompletionEndOfLifeReasonKind.Rejected });
197
}
198
199
handleIgnored(docId: DocumentId, suggestion: DiagnosticsNextEditResult, supersededBy: INextEditResult | undefined): void {
200
this._lastOutcome = NesOutcome.Ignored;
201
202
const completionResult = suggestion.result;
203
if (!completionResult) {
204
throw new BugIndicatingError('Completion result is undefined when accepted');
205
}
206
207
const supersededByItem = supersededBy instanceof DiagnosticsNextEditResult ? supersededBy?.result?.item : undefined;
208
209
this._diagnosticsCompletionHandler.handleEndOfLifetime(completionResult.item, {
210
kind: vscode.InlineCompletionEndOfLifeReasonKind.Ignored,
211
supersededBy: supersededByItem,
212
userTypingDisagreed: false /* TODO: Adopt this*/
213
});
214
}
215
216
}
217
218