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/diagnosticsCompletionProcessor.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 * as vscode from 'vscode';
7
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
8
import { applyEditsToRanges } from '../../../../platform/editSurvivalTracking/common/editSurvivalTracker';
9
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
10
import { DocumentId } from '../../../../platform/inlineEdits/common/dataTypes/documentId';
11
import { Edits } from '../../../../platform/inlineEdits/common/dataTypes/edit';
12
import { ObservableGit } from '../../../../platform/inlineEdits/common/observableGit';
13
import { IObservableDocument } from '../../../../platform/inlineEdits/common/observableWorkspace';
14
import { autorunWithChanges } from '../../../../platform/inlineEdits/common/utils/observable';
15
import { WorkspaceDocumentEditHistory } from '../../../../platform/inlineEdits/common/workspaceEditTracker/workspaceDocumentEditTracker';
16
import { ILogger, ILogService } from '../../../../platform/log/common/logService';
17
import { ITabsAndEditorsService } from '../../../../platform/tabs/common/tabsAndEditorsService';
18
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
19
import { isNotebookCell } from '../../../../util/common/notebooks';
20
import { equals } from '../../../../util/vs/base/common/arrays';
21
import { findFirstMonotonous } from '../../../../util/vs/base/common/arraysFind';
22
import { ThrottledDelayer } from '../../../../util/vs/base/common/async';
23
import { CancellationToken, CancellationTokenSource } from '../../../../util/vs/base/common/cancellation';
24
import { BugIndicatingError } from '../../../../util/vs/base/common/errors';
25
import { Emitter } from '../../../../util/vs/base/common/event';
26
import { Disposable } from '../../../../util/vs/base/common/lifecycle';
27
import { autorun, derived, IObservable, runOnChange } from '../../../../util/vs/base/common/observableInternal';
28
import { isEqual } from '../../../../util/vs/base/common/resources';
29
import { StringEdit } from '../../../../util/vs/editor/common/core/edits/stringEdit';
30
import { Position } from '../../../../util/vs/editor/common/core/position';
31
import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';
32
import { StringText } from '../../../../util/vs/editor/common/core/text/abstractText';
33
import { getInformationDelta, InformationDelta } from '../../common/informationDelta';
34
import { RejectionCollector } from '../../common/rejectionCollector';
35
import { IVSCodeObservableDocument, VSCodeWorkspace } from '../parts/vscodeWorkspace';
36
import { toInternalPosition } from '../utils/translations';
37
import { AnyDiagnosticCompletionItem, AnyDiagnosticCompletionProvider } from './diagnosticsBasedCompletions/anyDiagnosticsCompletionProvider';
38
import { AsyncDiagnosticCompletionProvider } from './diagnosticsBasedCompletions/asyncDiagnosticsCompletionProvider';
39
import { Diagnostic, DiagnosticCompletionItem, DiagnosticInlineEditRequestLogContext, IDiagnosticCompletionProvider, log, logList, sortDiagnosticsByDistance } from './diagnosticsBasedCompletions/diagnosticsCompletions';
40
import { ImportDiagnosticCompletionItem, ImportDiagnosticCompletionProvider } from './diagnosticsBasedCompletions/importDiagnosticsCompletionProvider';
41
42
interface IDiagnosticsCompletionState<T extends DiagnosticCompletionItem = DiagnosticCompletionItem> {
43
completionItem: T | null;
44
logContext: DiagnosticInlineEditRequestLogContext;
45
telemetryBuilder: DiagnosticsCompletionHandlerTelemetry;
46
}
47
48
function diagnosticCompletionRunResultEquals(a: IDiagnosticsCompletionState, b: IDiagnosticsCompletionState): boolean {
49
if (!!a.completionItem && !!b.completionItem) {
50
return DiagnosticCompletionItem.equals(a.completionItem, b.completionItem);
51
}
52
return a.completionItem === b.completionItem;
53
}
54
55
// Only exported for testing
56
export class DiagnosticsCollection {
57
58
private _diagnostics: Diagnostic[] = [];
59
60
applyEdit(previous: StringText, edit: StringEdit, after: StringText): boolean {
61
62
let hasInvalidated = false;
63
for (const diagnostic of this._diagnostics) {
64
const oldRange = diagnostic.range;
65
const newRange = applyEditsToRanges([oldRange], edit)[0];
66
67
// If the range shrank then the diagnostic will have changed
68
if (!newRange || newRange.length < oldRange.length) {
69
diagnostic.invalidate();
70
hasInvalidated = true;
71
continue;
72
}
73
74
const contentAtOldRange = oldRange.substring(previous.value);
75
76
// If the range stays the same then the diagnostic is still valid if the content is the same
77
if (newRange.length === oldRange.length) {
78
const contentAtNewRange = newRange.substring(after.value);
79
if (contentAtOldRange === contentAtNewRange) {
80
diagnostic.updateRange(newRange);
81
} else {
82
diagnostic.invalidate();
83
hasInvalidated = true;
84
}
85
continue;
86
}
87
88
// If the range grew then we need to check what got added
89
const isSamePrefix = contentAtOldRange === new OffsetRange(newRange.start, newRange.start + oldRange.length).substring(after.value);
90
const isSameSuffix = contentAtOldRange === new OffsetRange(newRange.endExclusive - oldRange.length, newRange.endExclusive).substring(after.value);
91
if (!isSamePrefix && !isSameSuffix) {
92
// The content at the diagnostic range has changed
93
diagnostic.invalidate();
94
hasInvalidated = true;
95
continue;
96
}
97
98
let edgeCharacter;
99
if (isSamePrefix) {
100
const offsetAfterOldRange = newRange.start + oldRange.length;
101
edgeCharacter = new OffsetRange(offsetAfterOldRange, offsetAfterOldRange + 1).substring(after.value);
102
} else {
103
const offsetBeforeOldRange = newRange.endExclusive - oldRange.length - 1;
104
edgeCharacter = new OffsetRange(offsetBeforeOldRange, offsetBeforeOldRange + 1).substring(after.value);
105
}
106
107
if (edgeCharacter.length !== 1 || /^[a-zA-Z0-9_]$/.test(edgeCharacter)) {
108
// The content at the diagnostic range has changed
109
diagnostic.invalidate();
110
hasInvalidated = true;
111
continue;
112
}
113
114
// We need to update the range of the diagnostic after applying the edits
115
let updatedRange: OffsetRange;
116
if (isSamePrefix) {
117
updatedRange = new OffsetRange(newRange.start, newRange.start + oldRange.length);
118
} else {
119
updatedRange = new OffsetRange(newRange.endExclusive - oldRange.length, newRange.endExclusive);
120
}
121
122
diagnostic.updateRange(updatedRange);
123
}
124
125
return hasInvalidated;
126
}
127
128
isEqualAndUpdate(relevantDiagnostics: Diagnostic[]): boolean {
129
if (equals(this._diagnostics, relevantDiagnostics, Diagnostic.equals)) {
130
return true;
131
}
132
this._diagnostics = relevantDiagnostics;
133
return false;
134
}
135
136
toString(): string {
137
return this._diagnostics.map(d => d.toString()).join('\n');
138
}
139
}
140
141
export type DiagnosticCompletionState = {
142
item: DiagnosticCompletionItem | undefined;
143
telemetry: IDiagnosticsCompletionTelemetry;
144
logContext: DiagnosticInlineEditRequestLogContext | undefined;
145
workInProgress?: boolean;
146
};
147
148
export class DiagnosticsCompletionProcessor extends Disposable {
149
150
static get documentSelector(): vscode.DocumentSelector {
151
return Array.from(new Set([
152
...ImportDiagnosticCompletionProvider.SupportedLanguages,
153
...AsyncDiagnosticCompletionProvider.SupportedLanguages
154
]));
155
}
156
157
private readonly _onDidChange = this._register(new Emitter<boolean>());
158
readonly onDidChange = this._onDidChange.event;
159
160
private readonly _worker = new AsyncWorker<IDiagnosticsCompletionState>(20, diagnosticCompletionRunResultEquals);
161
162
private readonly _rejectionCollector: RejectionCollector;
163
private readonly _diagnosticsCompletionProviders: IObservable<IDiagnosticCompletionProvider[]>;
164
private readonly _workspaceDocumentEditHistory: WorkspaceDocumentEditHistory;
165
private readonly _currentDiagnostics = new DiagnosticsCollection();
166
167
private readonly _logger: ILogger;
168
169
constructor(
170
private readonly _workspace: VSCodeWorkspace,
171
git: ObservableGit,
172
@ILogService logService: ILogService,
173
@IConfigurationService configurationService: IConfigurationService,
174
@IWorkspaceService workspaceService: IWorkspaceService,
175
@IFileSystemService fileSystemService: IFileSystemService,
176
@ITabsAndEditorsService private readonly _tabsAndEditorsService: ITabsAndEditorsService,
177
) {
178
super();
179
180
this._workspaceDocumentEditHistory = this._register(new WorkspaceDocumentEditHistory(this._workspace, git, 100));
181
182
this._logger = logService.createSubLogger(['NES', 'DiagnosticsInlineCompletionProvider']);
183
184
const diagnosticsExplorationEnabled = configurationService.getConfigObservable(ConfigKey.TeamInternal.InlineEditsDiagnosticsExplorationEnabled);
185
186
const importProvider = new ImportDiagnosticCompletionProvider(this._logger.createSubLogger('Import'), workspaceService, fileSystemService);
187
const asyncProvider = new AsyncDiagnosticCompletionProvider(this._logger.createSubLogger('Async'));
188
189
this._diagnosticsCompletionProviders = derived(reader => {
190
const providers: IDiagnosticCompletionProvider[] = [
191
importProvider,
192
asyncProvider
193
];
194
195
if (diagnosticsExplorationEnabled.read(reader)) {
196
providers.push(new AnyDiagnosticCompletionProvider(this._logger.createSubLogger('All')));
197
}
198
199
return providers;
200
}).recomputeInitiallyAndOnChange(this._store);
201
202
this._rejectionCollector = this._register(new RejectionCollector(this._workspace, logService));
203
204
const isValidEditor = (editor: vscode.TextEditor | undefined): editor is vscode.TextEditor => {
205
return !!editor && (isNotebookCell(editor.document.uri) || isEditorFromEditorGrid(editor));
206
};
207
208
this._register(autorun(reader => {
209
const activeDocument = this._workspace.lastActiveDocument.read(reader);
210
if (!activeDocument) { return; }
211
212
const activeEditor = this._tabsAndEditorsService.activeTextEditor;
213
if (!activeEditor || !isEditorFromEditorGrid(activeEditor) || !isEqual(activeDocument.id.toUri(), activeEditor.document.uri)) {
214
return;
215
}
216
217
// update state because document changed
218
this._updateState();
219
220
// update state because diagnostics changed
221
reader.store.add(runOnChange(activeDocument.diagnostics, (diagnostics) => {
222
this._logger.trace(`Diagnostics changed received in processor: ${diagnostics.map(d => '\n- ' + d.message).join('')}`);
223
this._updateState();
224
}));
225
}));
226
227
this._register(vscode.window.onDidChangeTextEditorSelection(async e => {
228
const activeEditor = this._tabsAndEditorsService.activeTextEditor;
229
if (!isValidEditor(activeEditor)) {
230
return;
231
}
232
233
if (!isEqual(e.textEditor.document.uri, activeEditor.document.uri)) {
234
return;
235
}
236
237
this._updateState();
238
}));
239
240
this._register(this._worker.onDidChange(result => {
241
this._onDidChange.fire(!!result.completionItem);
242
}));
243
244
this._register(autorun(reader => {
245
const document = this._workspace.lastActiveDocument.read(reader);
246
if (!document) { return; }
247
248
reader.store.add(autorunWithChanges(this, {
249
value: document.value,
250
}, (data) => {
251
for (const edit of data.value.changes) {
252
if (!data.value.previous) { continue; }
253
const hasInvalidatedRange = this._currentDiagnostics.applyEdit(data.value.previous, edit, data.value.value);
254
if (hasInvalidatedRange) {
255
this._updateState();
256
}
257
}
258
}));
259
}));
260
}
261
262
private async _updateState(): Promise<void> {
263
const activeTextEditor = this._tabsAndEditorsService.activeTextEditor;
264
if (!activeTextEditor) { return; }
265
266
const workspaceDocument = this._workspace.getDocumentByTextDocument(activeTextEditor.document);
267
if (!workspaceDocument) { return; }
268
269
const range = new vscode.Range(activeTextEditor.selection.active, activeTextEditor.selection.active);
270
const selection = workspaceDocument.toRange(activeTextEditor.document, range);
271
if (!selection) {
272
return;
273
}
274
275
const cursor = toInternalPosition(selection.start);
276
const log = new DiagnosticInlineEditRequestLogContext();
277
278
const relevantDiagnostics = this._getDiagnostics(workspaceDocument, cursor, log);
279
const diagnosticsSorted = sortDiagnosticsByDistance(workspaceDocument, relevantDiagnostics, cursor);
280
281
if (this._currentDiagnostics.isEqualAndUpdate(diagnosticsSorted)) {
282
return;
283
}
284
285
this._logger.trace('Scheduled update for diagnostics inline completion');
286
287
await this._worker.schedule(async (token: CancellationToken) => this._runCompletionHandler(workspaceDocument, diagnosticsSorted, cursor, log, token));
288
}
289
290
private _getDiagnostics(workspaceDocument: IVSCodeObservableDocument, cursor: Position, logContext: DiagnosticInlineEditRequestLogContext): Diagnostic[] {
291
const availableDiagnostics = workspaceDocument.diagnostics.get().map(d => new Diagnostic(d));
292
if (availableDiagnostics.length === 0) {
293
return [];
294
}
295
296
const filterDiagnosticsAndLog = (diagnostics: Diagnostic[], message: string, filterFn: (diagnostics: Diagnostic[]) => Diagnostic[]): Diagnostic[] => {
297
const diagnosticsAfter = filterFn(diagnostics);
298
const diagnosticsDiff = diagnostics.filter(diagnostic => !diagnosticsAfter.includes(diagnostic));
299
if (diagnosticsDiff.length > 0) {
300
logList(message, diagnosticsDiff, logContext, this._logger);
301
}
302
return diagnosticsAfter;
303
};
304
305
const language = workspaceDocument.languageId.get();
306
const providers = this._diagnosticsCompletionProviders.get();
307
308
let relevantDiagnostics = [...availableDiagnostics];
309
relevantDiagnostics = filterDiagnosticsAndLog(relevantDiagnostics, 'Filtered by provider', ds => ds.filter(diagnostic => providers.some(provider => provider.providesCompletionsForDiagnostic(workspaceDocument, diagnostic, language, cursor))));
310
relevantDiagnostics = filterDiagnosticsAndLog(relevantDiagnostics, 'Filtered by recent acceptance', ds => ds.filter(diagnostic => !this._hasDiagnosticRecentlyBeenAccepted(diagnostic)));
311
relevantDiagnostics = filterDiagnosticsAndLog(relevantDiagnostics, 'Filtered by no recent edit', ds => this._filterDiagnosticsByRecentEditNearby(ds, workspaceDocument));
312
313
return relevantDiagnostics;
314
}
315
316
private async _runCompletionHandler(workspaceDocument: IVSCodeObservableDocument, diagnosticsSorted: Diagnostic[], cursor: Position, log: DiagnosticInlineEditRequestLogContext, token: CancellationToken): Promise<IDiagnosticsCompletionState> {
317
const telemetryBuilder = new DiagnosticsCompletionHandlerTelemetry();
318
319
let completionItem = null;
320
try {
321
this._logger.trace('Running diagnostics inline completion handler');
322
completionItem = await this._getCompletionFromDiagnostics(workspaceDocument, diagnosticsSorted, cursor, log, token, telemetryBuilder);
323
} catch (error) {
324
log.setError(error);
325
}
326
327
this._logger.trace('Diagnostic Providers returned completion item: ' + (completionItem ? completionItem.toString() : 'null'));
328
329
if (completionItem instanceof ImportDiagnosticCompletionItem) {
330
telemetryBuilder.setImportTelemetry(completionItem);
331
}
332
333
return { completionItem, logContext: log, telemetryBuilder: telemetryBuilder };
334
}
335
336
getCurrentState(docId: DocumentId): DiagnosticCompletionState {
337
const currentState = this._worker.getCurrentResult();
338
339
const workspaceDocument = this._workspace.getDocument(docId);
340
if (!workspaceDocument) { return { item: undefined, telemetry: new DiagnosticsCompletionHandlerTelemetry().addDroppedReason('WorkspaceDocumentNotFound').build(), logContext: undefined }; }
341
342
if (currentState === undefined) {
343
return { item: undefined, telemetry: new DiagnosticsCompletionHandlerTelemetry().build(), logContext: undefined };
344
}
345
346
const { telemetryBuilder, completionItem, logContext } = currentState;
347
const workInProgress = this._worker.workInProgress();
348
if (!completionItem) {
349
return { item: undefined, telemetry: telemetryBuilder.build(), logContext, workInProgress };
350
}
351
352
if (!this._isCompletionItemValid(completionItem, workspaceDocument, currentState.logContext, telemetryBuilder)) {
353
return { item: undefined, telemetry: telemetryBuilder.build(), logContext, workInProgress };
354
}
355
356
if (completionItem.documentId !== docId) {
357
logContext.addLog('Dropped: wrong-document');
358
return { item: undefined, telemetry: telemetryBuilder.addDroppedReason('wrong-document').build(), logContext, workInProgress };
359
}
360
361
log('following known diagnostics:\n' + this._currentDiagnostics.toString(), undefined, this._logger);
362
363
return { item: completionItem, telemetry: telemetryBuilder.build(), logContext, workInProgress };
364
}
365
366
private async _getCompletionFromDiagnostics(workspaceDocument: IVSCodeObservableDocument, diagnosticsSorted: Diagnostic[], pos: Position, logContext: DiagnosticInlineEditRequestLogContext, token: CancellationToken, tb: DiagnosticsCompletionHandlerTelemetry): Promise<DiagnosticCompletionItem | null> {
367
if (diagnosticsSorted.length === 0) {
368
log(`No diagnostics available for document ${workspaceDocument.id.toString()}`, logContext, this._logger);
369
return null;
370
}
371
372
const diagnosticsCompletionItems = await this._fetchDiagnosticsBasedCompletions(workspaceDocument, diagnosticsSorted, pos, logContext, token);
373
374
return diagnosticsCompletionItems.find(item => this._isCompletionItemValid(item, workspaceDocument, logContext, tb)) ?? null;
375
}
376
377
private async _fetchDiagnosticsBasedCompletions(workspaceDocument: IVSCodeObservableDocument, sortedDiagnostics: Diagnostic[], pos: Position, logContext: DiagnosticInlineEditRequestLogContext, token: CancellationToken): Promise<DiagnosticCompletionItem[]> {
378
const providers = this._diagnosticsCompletionProviders.get();
379
380
const providerTimings: Array<{ provider: string; duration: number }> = [];
381
382
const providerResults = await Promise.all(providers.map(async provider => {
383
const startTime = Date.now();
384
const result = await provider.provideDiagnosticCompletionItem(workspaceDocument, sortedDiagnostics, pos, logContext, token);
385
providerTimings.push({ provider: provider.providerName, duration: Date.now() - startTime });
386
return result;
387
}));
388
389
this._logger.trace(`Provider durations: ${providerTimings.map(timing => `\n- ${timing.provider}: ${timing.duration}ms`).join('')}`);
390
391
return providerResults.filter(item => !!item) as DiagnosticCompletionItem[];
392
}
393
394
// Handle Acceptance and rejection of diagnostics completion items
395
396
public handleEndOfLifetime(completionItem: DiagnosticCompletionItem, reason: vscode.InlineCompletionEndOfLifeReason): void {
397
const provider = this._diagnosticsCompletionProviders.get().find(p => p.providerName === completionItem.providerName);
398
if (!provider) {
399
throw new BugIndicatingError('No provider found for completion item');
400
}
401
402
if (reason.kind === vscode.InlineCompletionEndOfLifeReasonKind.Rejected) {
403
this._rejectDiagnosticCompletion(provider, completionItem);
404
} else if (reason.kind === vscode.InlineCompletionEndOfLifeReasonKind.Accepted) {
405
this._acceptDiagnosticCompletion(provider, completionItem);
406
}
407
}
408
409
private _lastAcceptedDiagnostic: { diagnostic: Diagnostic; time: number } | undefined = undefined;
410
private _acceptDiagnosticCompletion(provider: IDiagnosticCompletionProvider, item: DiagnosticCompletionItem): void {
411
this._lastAcceptedDiagnostic = { diagnostic: item.diagnostic, time: Date.now() };
412
}
413
414
private _rejectDiagnosticCompletion(provider: IDiagnosticCompletionProvider, item: DiagnosticCompletionItem): void {
415
this._rejectionCollector.reject(item.documentId, item.toOffsetEdit());
416
417
provider.completionItemRejected?.(item);
418
}
419
420
// Filters
421
422
private _isCompletionItemValid(item: DiagnosticCompletionItem, workspaceDocument: IObservableDocument, logContext: DiagnosticInlineEditRequestLogContext, tb: DiagnosticsCompletionHandlerTelemetry): boolean {
423
if (!item.diagnostic.isValid()) {
424
log('Diagnostic completion item is no longer valid', logContext, this._logger);
425
tb.addDroppedReason('no-longer-valid', item);
426
logContext.markToBeLogged();
427
return false;
428
}
429
430
if (this._isDiagnosticCompletionRejected(item)) {
431
log('Diagnostic completion item has been rejected before', logContext, this._logger);
432
tb.addDroppedReason('recently-rejected', item);
433
logContext.markToBeLogged();
434
return false;
435
}
436
437
if (this._isUndoRecentEdit(item)) {
438
log('Diagnostic completion item is an undo operation', logContext, this._logger);
439
tb.addDroppedReason('undo-operation', item);
440
logContext.markToBeLogged();
441
return false;
442
}
443
444
if (this._hasDiagnosticRecentlyBeenAccepted(item.diagnostic)) {
445
log('Completion item fixing the diagnostic has been accepted recently', logContext, this._logger);
446
tb.addDroppedReason('recently-accepted', item);
447
logContext.markToBeLogged();
448
return false;
449
}
450
451
if (this._hasRecentlyBeenAddedWithoutNES(item)) {
452
log('Diagnostic has been fixed without NES recently', logContext, this._logger);
453
tb.addDroppedReason('recently-added-without-nes', item);
454
logContext.markToBeLogged();
455
return false;
456
}
457
458
const provider = this._diagnosticsCompletionProviders.get().find(p => p.providerName === item.providerName);
459
if (provider && provider.isCompletionItemStillValid && !provider.isCompletionItemStillValid(item, workspaceDocument)) {
460
log(`${provider.providerName}: Completion item is no longer valid`, logContext, this._logger);
461
tb.addDroppedReason(`${provider.providerName}-no-longer-valid`, item);
462
logContext.markToBeLogged();
463
return false;
464
}
465
466
return true;
467
}
468
469
private _isDiagnosticCompletionRejected(diagnostic: DiagnosticCompletionItem): boolean {
470
return this._rejectionCollector.isRejected(diagnostic.documentId, diagnostic.toOffsetEdit());
471
}
472
473
private _hasRecentlyBeenAddedWithoutNES(item: DiagnosticCompletionItem): boolean {
474
const recentEdits = this._workspaceDocumentEditHistory.getNRecentEdits(item.documentId, 5)?.edits;
475
if (!recentEdits) {
476
return false;
477
}
478
479
const offsetEdit = item.toOffsetEdit();
480
return recentEdits.replacements.some(edit => edit.replaceRange.intersectsOrTouches(offsetEdit.replaceRange));
481
}
482
483
private _hasDiagnosticRecentlyBeenAccepted(diagnostic: Diagnostic): boolean {
484
if (!this._lastAcceptedDiagnostic || this._lastAcceptedDiagnostic.time + 1000 < Date.now()) {
485
return false;
486
}
487
return this._lastAcceptedDiagnostic.diagnostic.equals(diagnostic);
488
}
489
490
private _isUndoRecentEdit(diagnostic: DiagnosticCompletionItem): boolean {
491
const documentHistory = this._workspaceDocumentEditHistory.getRecentEdits(diagnostic.documentId);
492
if (!documentHistory) {
493
return false;
494
}
495
496
return diagnosticWouldUndoUserEdit(diagnostic, documentHistory.before, documentHistory.after, Edits.single(documentHistory.edits));
497
}
498
499
private _filterDiagnosticsByRecentEditNearby(diagnostics: Diagnostic[], document: IVSCodeObservableDocument): Diagnostic[] {
500
const recentEdits = this._workspaceDocumentEditHistory.getRecentEdits(document.id)?.edits;
501
if (!recentEdits) {
502
return [];
503
}
504
505
return diagnostics.filter(diagnostic => {
506
const newRanges = recentEdits.getNewRanges();
507
const potentialIntersection = findFirstMonotonous(newRanges, (r) => r.endExclusive >= diagnostic.range.start);
508
return potentialIntersection?.intersectsOrTouches(diagnostic.range);
509
});
510
}
511
}
512
513
function diagnosticWouldUndoUserEdit(diagnostic: DiagnosticCompletionItem, documentBefore: StringText, documentAfter: StringText, edits: Edits): boolean {
514
515
const currentEdit = diagnostic.toOffsetEdit().toEdit();
516
const ourInformationDelta = getInformationDelta(documentAfter.value, currentEdit);
517
518
let recentInformationDelta = new InformationDelta();
519
let doc = documentBefore.value;
520
for (const edit of edits.edits) {
521
recentInformationDelta = recentInformationDelta.combine(getInformationDelta(doc, edit));
522
doc = edit.apply(doc);
523
}
524
525
if (recentInformationDelta.isUndoneBy(ourInformationDelta)) {
526
return true;
527
}
528
529
return false;
530
}
531
532
function isEditorFromEditorGrid(editor: vscode.TextEditor): boolean {
533
return editor.viewColumn !== undefined;
534
}
535
536
class AsyncWorker<T extends {}> extends Disposable {
537
private readonly _taskQueue: ThrottledDelayer<void>;
538
539
private readonly _onDidChange = this._register(new vscode.EventEmitter<T>());
540
readonly onDidChange = this._onDidChange.event;
541
542
private _currentTokenSource: CancellationTokenSource | undefined = undefined;
543
private _activeWorkPromise: Promise<T | undefined> | undefined = undefined;
544
545
private __currentResult: T | undefined = undefined;
546
private get _currentResult(): T | undefined {
547
return this.__currentResult;
548
}
549
private set _currentResult(value: T) {
550
const changed = this.__currentResult === undefined || !this._equals(value, this.__currentResult);
551
this.__currentResult = value;
552
if (changed) {
553
this._onDidChange.fire(value);
554
}
555
}
556
557
constructor(delay: number, private readonly _equals: (a: T, b: T) => boolean) {
558
super();
559
560
this._taskQueue = new ThrottledDelayer<void>(delay);
561
}
562
563
async schedule(fn: (token: CancellationToken) => Promise<T>): Promise<void> {
564
const activePromise = this._doSchedule(fn);
565
this._activeWorkPromise = activePromise;
566
567
const result = await activePromise;
568
569
if (this._activeWorkPromise === activePromise) {
570
this._activeWorkPromise = undefined;
571
}
572
573
if (result !== undefined) {
574
this._currentResult = result;
575
}
576
}
577
578
private async _doSchedule(fn: (token: CancellationToken) => Promise<T>): Promise<T | undefined> {
579
this._currentTokenSource?.dispose(true);
580
this._currentTokenSource = new CancellationTokenSource();
581
const token = this._currentTokenSource.token;
582
583
let result;
584
await this._taskQueue.trigger(async () => {
585
if (token.isCancellationRequested) {
586
return;
587
}
588
589
result = await fn(token);
590
});
591
592
return result;
593
}
594
595
// Get the active result if there is one currently
596
// Return undefined if there is currently work being done
597
getCurrentResult(): T | undefined {
598
if (this._currentResult === undefined) {
599
return undefined;
600
}
601
602
return this._currentResult;
603
}
604
605
workInProgress(): boolean {
606
return this._activeWorkPromise !== undefined;
607
}
608
609
override dispose(): void {
610
if (this._currentTokenSource) {
611
this._currentTokenSource.dispose();
612
}
613
super.dispose();
614
}
615
}
616
617
interface IDiagnosticsCompletionTelemetry {
618
droppedReasons: string[];
619
alternativeImportsCount?: number;
620
hasExistingSameFileImport?: boolean;
621
isLocalImport?: boolean;
622
distanceToUnknownDiagnostic?: number;
623
distanceToAlternativeDiagnostic?: number;
624
hasAlternativeDiagnosticForSameRange?: boolean;
625
}
626
627
class DiagnosticsCompletionHandlerTelemetry {
628
private _droppedReasons: string[] = [];
629
630
addDroppedReason(reason: string, item?: DiagnosticCompletionItem): this {
631
if (item instanceof AnyDiagnosticCompletionItem) {
632
return this; // Do not track dropped reasons for "any" items
633
}
634
635
this._droppedReasons.push(item ? `${item.type}:${reason}` : reason);
636
return this;
637
}
638
639
private _distanceToAlternativeDiagnostic: number | undefined;
640
setDistanceToAlternativeDiagnostic(distance: number | undefined): this {
641
this._distanceToAlternativeDiagnostic = distance;
642
return this;
643
}
644
645
private _distanceToUnknownDiagnostic: number | undefined;
646
setDistanceToUnknownDiagnostic(distance: number | undefined): this {
647
this._distanceToUnknownDiagnostic = distance;
648
return this;
649
}
650
651
private _hasAlternativeDiagnosticForSameRange: boolean | undefined;
652
setHasAlternativeDiagnosticForSameRange(has: boolean | undefined): this {
653
this._hasAlternativeDiagnosticForSameRange = has;
654
return this;
655
}
656
657
private _alternativeImportsCount: number | undefined;
658
private _hasExistingSameFileImport: boolean | undefined;
659
private _isLocalImport: boolean | undefined;
660
661
setImportTelemetry(item: ImportDiagnosticCompletionItem): this {
662
this._alternativeImportsCount = item.alternativeImportsCount;
663
this._hasExistingSameFileImport = item.hasExistingSameFileImport;
664
this._isLocalImport = item.isLocalImport;
665
return this;
666
}
667
668
build(): IDiagnosticsCompletionTelemetry {
669
return {
670
droppedReasons: this._droppedReasons,
671
alternativeImportsCount: this._alternativeImportsCount,
672
hasExistingSameFileImport: this._hasExistingSameFileImport,
673
isLocalImport: this._isLocalImport,
674
distanceToUnknownDiagnostic: this._distanceToUnknownDiagnostic,
675
distanceToAlternativeDiagnostic: this._distanceToAlternativeDiagnostic,
676
hasAlternativeDiagnosticForSameRange: this._hasAlternativeDiagnosticForSameRange
677
};
678
}
679
}
680
681