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/diagnosticsBasedCompletions/diagnosticsCompletions.ts
13406 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 * as vscode from 'vscode';
7
import { DiagnosticData } from '../../../../../platform/inlineEdits/common/dataTypes/diagnosticData';
8
import { DocumentId } from '../../../../../platform/inlineEdits/common/dataTypes/documentId';
9
import { LanguageId } from '../../../../../platform/inlineEdits/common/dataTypes/languageId';
10
import { RootedLineEdit } from '../../../../../platform/inlineEdits/common/dataTypes/rootedLineEdit';
11
import { IObservableDocument } from '../../../../../platform/inlineEdits/common/observableWorkspace';
12
import { ILogger } from '../../../../../platform/log/common/logService';
13
import { min } from '../../../../../util/common/arrays';
14
import { ErrorUtils } from '../../../../../util/common/errors';
15
import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';
16
import { LineEdit } from '../../../../../util/vs/editor/common/core/edits/lineEdit';
17
import { StringReplacement } from '../../../../../util/vs/editor/common/core/edits/stringEdit';
18
import { TextEdit, TextReplacement } from '../../../../../util/vs/editor/common/core/edits/textEdit';
19
import { Position } from '../../../../../util/vs/editor/common/core/position';
20
import { Range } from '../../../../../util/vs/editor/common/core/range';
21
import { OffsetRange } from '../../../../../util/vs/editor/common/core/ranges/offsetRange';
22
import { INextEditDisplayLocation } from '../../../node/nextEditResult';
23
import { IVSCodeObservableDocument } from '../../parts/vscodeWorkspace';
24
import { toExternalRange, toInternalRange } from '../../utils/translations';
25
26
export interface IDiagnosticCodeAction {
27
edit: TextReplacement;
28
}
29
30
export abstract class DiagnosticCompletionItem implements vscode.InlineCompletionItem {
31
32
static equals(a: DiagnosticCompletionItem, b: DiagnosticCompletionItem): boolean {
33
return a.documentId.toString() === b.documentId.toString() &&
34
Range.equalsRange(toInternalRange(a.range), toInternalRange(b.range)) &&
35
a.insertText === b.insertText &&
36
a.type === b.type &&
37
a.isInlineEdit === b.isInlineEdit &&
38
a.showInlineEditMenu === b.showInlineEditMenu &&
39
displayLocationEquals(a.nextEditDisplayLocation, b.nextEditDisplayLocation);
40
}
41
42
public readonly isInlineEdit = true;
43
public readonly showInlineEditMenu = true;
44
45
public readonly abstract providerName: string;
46
47
private _range: vscode.Range | undefined;
48
get range(): vscode.Range {
49
if (!this._range) {
50
this._range = toExternalRange(this._edit.range);
51
}
52
return this._range;
53
}
54
get insertText(): string {
55
return this._edit.text;
56
}
57
get nextEditDisplayLocation(): INextEditDisplayLocation | undefined {
58
return this._getDisplayLocation();
59
}
60
get displayLocation(): vscode.InlineCompletionDisplayLocation | undefined {
61
const displayLocation = this.nextEditDisplayLocation;
62
return displayLocation ? {
63
range: toExternalRange(displayLocation.range),
64
label: displayLocation.label,
65
kind: vscode.InlineCompletionDisplayLocationKind.Code
66
} : undefined;
67
}
68
get documentId(): DocumentId {
69
return this._workspaceDocument.id;
70
}
71
72
constructor(
73
public readonly type: string,
74
public readonly diagnostic: Diagnostic,
75
private readonly _edit: TextReplacement,
76
protected readonly _workspaceDocument: IVSCodeObservableDocument,
77
) { }
78
79
toOffsetEdit() {
80
return StringReplacement.replace(this._toOffsetRange(this._edit.range), this._edit.text);
81
}
82
83
toTextEdit() {
84
return new TextEdit([this._edit]);
85
}
86
87
toLineEdit() {
88
return LineEdit.fromTextEdit(this.toTextEdit(), this._workspaceDocument.value.get());
89
}
90
91
getDiagnosticOffsetRange() {
92
return this.diagnostic.range;
93
}
94
95
getRootedLineEdit() {
96
return new RootedLineEdit(this._workspaceDocument.value.get(), this.toLineEdit());
97
}
98
99
private _toOffsetRange(range: Range): OffsetRange {
100
const transformer = this._workspaceDocument.value.get().getTransformer();
101
return transformer.getOffsetRange(range);
102
}
103
104
// TODO: rethink if this needs to be updatable
105
protected _getDisplayLocation(): INextEditDisplayLocation | undefined {
106
return undefined;
107
}
108
109
toString(): string {
110
return `DiagnosticCompletionItem(type=${this.type}, diagnostic=${this.diagnostic.toString()}, edit=${this._edit.toString()})`;
111
}
112
}
113
114
function displayLocationEquals(a: INextEditDisplayLocation | undefined, b: INextEditDisplayLocation | undefined): boolean {
115
return a === b || (a !== undefined && b !== undefined && a.label === b.label && Range.equalsRange(a.range, b.range));
116
}
117
118
export interface IDiagnosticCompletionProvider<T extends DiagnosticCompletionItem = DiagnosticCompletionItem> {
119
readonly providerName: string;
120
providesCompletionsForDiagnostic(workspaceDocument: IVSCodeObservableDocument, diagnostic: Diagnostic, language: LanguageId, pos: Position): boolean;
121
provideDiagnosticCompletionItem(workspaceDocument: IVSCodeObservableDocument, sortedDiagnostics: Diagnostic[], pos: Position, logContext: DiagnosticInlineEditRequestLogContext, token: CancellationToken): Promise<T | null>;
122
completionItemRejected?(item: T): void;
123
isCompletionItemStillValid?(item: T, workspaceDocument: IObservableDocument): boolean;
124
}
125
126
// TODO: Better incorporate diagnostics logging
127
export class DiagnosticInlineEditRequestLogContext {
128
129
getLogs(): string[] {
130
if (!this._markedToBeLogged) {
131
return [];
132
}
133
134
const lines = [];
135
136
if (this._error) {
137
lines.push(`## Diagnostics Error`);
138
lines.push('```');
139
lines.push(ErrorUtils.toString(ErrorUtils.fromUnknown(this._error)));
140
lines.push('```');
141
}
142
143
if (this._logs.length > 0) {
144
lines.push(`## Diagnostics Logs`);
145
lines.push(...this._logs);
146
}
147
148
return lines;
149
}
150
151
private _logs: string[] = [];
152
addLog(content: string): void {
153
this._logs.push(content.replace('\n', '\\n').replace('\t', '\\t').replace('`', '\`') + '\n');
154
}
155
156
private _markedToBeLogged: boolean = false;
157
markToBeLogged() {
158
this._markedToBeLogged = true;
159
}
160
161
private _error: unknown | undefined = undefined;
162
setError(e: unknown): void {
163
this._markedToBeLogged = true;
164
this._error = e;
165
}
166
167
}
168
169
export class Diagnostic {
170
171
static equals(a: Diagnostic, b: Diagnostic): boolean {
172
return a.equals(b);
173
}
174
175
private _updatedRange: OffsetRange;
176
get range(): OffsetRange {
177
return this._updatedRange;
178
}
179
180
private _isValid: boolean = true;
181
isValid(): boolean {
182
return this._isValid;
183
}
184
185
get message(): string {
186
return this.data.message;
187
}
188
189
constructor(
190
public readonly data: DiagnosticData
191
) {
192
this._updatedRange = data.range;
193
}
194
195
equals(other: Diagnostic): boolean {
196
return this.data.equals(other.data)
197
&& this._updatedRange.equals(other.range)
198
&& this._isValid === other._isValid;
199
}
200
201
toString(): string {
202
if (this.data.range !== this._updatedRange) {
203
return `\`${this.data.toString()}\` (currently at \`${this._updatedRange.toString()}\`)`;
204
}
205
return `\`${this.data.toString()}\``;
206
}
207
208
updateRange(range: OffsetRange): void {
209
this._updatedRange = range;
210
}
211
212
invalidate(): void {
213
this._isValid = false;
214
}
215
}
216
217
export function log(message: string, logContext?: DiagnosticInlineEditRequestLogContext, logger?: ILogger) {
218
if (logContext) {
219
const lines = message.split('\n');
220
lines.forEach(line => logContext.addLog(line));
221
}
222
223
if (logger) {
224
logger.trace(message);
225
}
226
}
227
228
export function logList(title: string, list: Array<string | { toString(): string }>, logContext?: DiagnosticInlineEditRequestLogContext, logger?: ILogger) {
229
const content = `${title}${list.map(item => `\n- ${typeof item === 'string' ? item : item.toString()}`).join('')}`;
230
log(content, logContext, logger);
231
}
232
233
// TODO: there must be a utility for this somewhere? Otherwise make them available
234
235
function diagnosticDistanceToPosition(workspaceDocument: IObservableDocument, diagnostic: Diagnostic, position: Position) {
236
function positionDistance(a: Position, b: Position) {
237
return { lineDelta: Math.abs(a.lineNumber - b.lineNumber), characterDelta: Math.abs(a.column - b.column) };
238
}
239
240
const range = workspaceDocument.value.get().getTransformer().getRange(diagnostic.range);
241
const a = positionDistance(range.getStartPosition(), position);
242
const b = positionDistance(range.getEndPosition(), position);
243
244
if (a.lineDelta === b.lineDelta) {
245
return a.characterDelta < b.characterDelta ? a : b;
246
}
247
248
return a.lineDelta < b.lineDelta ? a : b;
249
}
250
251
export function isDiagnosticWithinDistance(workspaceDocument: IObservableDocument, diagnostic: Diagnostic, position: Position, maxLineDistance: number): boolean {
252
return diagnosticDistanceToPosition(workspaceDocument, diagnostic, position).lineDelta <= maxLineDistance;
253
}
254
255
export function sortDiagnosticsByDistance(workspaceDocument: IObservableDocument, diagnostics: Diagnostic[], position: Position): Diagnostic[] {
256
const transformer = workspaceDocument.value.get().getTransformer();
257
return diagnostics.sort((a, b) => {
258
const aDistance = diagnosticDistanceToPosition(workspaceDocument, a, position);
259
const bDistance = diagnosticDistanceToPosition(workspaceDocument, b, position);
260
261
if (aDistance.lineDelta !== bDistance.lineDelta) {
262
return aDistance.lineDelta - bDistance.lineDelta;
263
}
264
265
const aPosition = transformer.getPosition(a.range.start);
266
const bPosition = transformer.getPosition(b.range.start);
267
268
if (aPosition.lineNumber !== bPosition.lineNumber) {
269
return aDistance.characterDelta - bDistance.characterDelta;
270
}
271
272
if (aDistance.lineDelta < 2) {
273
return aDistance.characterDelta - bDistance.characterDelta;
274
}
275
276
// If both diagnostics are on the same line and are more than 1 line away from the cursor
277
// always prefer the first diagnostic to minimize recomputation and flickering on cursor move
278
return -1;
279
});
280
}
281
282
export function distanceToClosestDiagnostic(workspaceDocument: IObservableDocument, diagnostics: Diagnostic[], position: Position): number | undefined {
283
if (diagnostics.length === 0) {
284
return undefined;
285
}
286
287
const distances = diagnostics.map(diagnostic => diagnosticDistanceToPosition(workspaceDocument, diagnostic, position).lineDelta);
288
289
return min(distances);
290
}
291
292