Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export abstract class DiagnosticCompletionItem implements vscode.InlineCompletio
protected _getDisplayLocation(): INextEditDisplayLocation | undefined {
return undefined;
}

toString(): string {
return `DiagnosticCompletionItem(type=${this.type}, diagnostic=${this.diagnostic.toString()}, edit=${this._edit.toString()})`;
}
}

function displayLocationEquals(a: INextEditDisplayLocation | undefined, b: INextEditDisplayLocation | undefined): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { ThrottledDelayer } from '../../../../util/vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from '../../../../util/vs/base/common/cancellation';
import { BugIndicatingError } from '../../../../util/vs/base/common/errors';
import { Emitter } from '../../../../util/vs/base/common/event';
import { Disposable, DisposableStore } from '../../../../util/vs/base/common/lifecycle';
import { Disposable } from '../../../../util/vs/base/common/lifecycle';
import { autorun, derived, IObservable, runOnChange } from '../../../../util/vs/base/common/observableInternal';
import { isEqual } from '../../../../util/vs/base/common/resources';
import { StringEdit } from '../../../../util/vs/editor/common/core/edits/stringEdit';
Expand All @@ -37,7 +37,7 @@ import { IVSCodeObservableDocument, VSCodeWorkspace } from '../parts/vscodeWorks
import { toInternalPosition } from '../utils/translations';
import { AnyDiagnosticCompletionItem, AnyDiagnosticCompletionProvider } from './diagnosticsBasedCompletions/anyDiagnosticsCompletionProvider';
import { AsyncDiagnosticCompletionProvider } from './diagnosticsBasedCompletions/asyncDiagnosticsCompletionProvider';
import { Diagnostic, DiagnosticCompletionItem, DiagnosticInlineEditRequestLogContext, distanceToClosestDiagnostic, IDiagnosticCompletionProvider, log, logList, sortDiagnosticsByDistance } from './diagnosticsBasedCompletions/diagnosticsCompletions';
import { Diagnostic, DiagnosticCompletionItem, DiagnosticInlineEditRequestLogContext, IDiagnosticCompletionProvider, log, logList, sortDiagnosticsByDistance } from './diagnosticsBasedCompletions/diagnosticsCompletions';
import { ImportDiagnosticCompletionItem, ImportDiagnosticCompletionProvider } from './diagnosticsBasedCompletions/importDiagnosticsCompletionProvider';

interface IDiagnosticsCompletionState<T extends DiagnosticCompletionItem = DiagnosticCompletionItem> {
Expand Down Expand Up @@ -143,6 +143,7 @@ export type DiagnosticCompletionState = {
item: DiagnosticCompletionItem | undefined;
telemetry: IDiagnosticsCompletionTelemetry;
logContext: DiagnosticInlineEditRequestLogContext | undefined;
workInProgress?: boolean;
};

export class DiagnosticsCompletionProcessor extends Disposable {
Expand Down Expand Up @@ -275,7 +276,7 @@ export class DiagnosticsCompletionProcessor extends Disposable {
const cursor = toInternalPosition(selection.start);
const log = new DiagnosticInlineEditRequestLogContext();

const { availableDiagnostics, relevantDiagnostics } = this._getDiagnostics(workspaceDocument, cursor, log);
const relevantDiagnostics = this._getDiagnostics(workspaceDocument, cursor, log);
const diagnosticsSorted = sortDiagnosticsByDistance(workspaceDocument, relevantDiagnostics, cursor);

if (this._currentDiagnostics.isEqualAndUpdate(diagnosticsSorted)) {
Expand All @@ -284,14 +285,13 @@ export class DiagnosticsCompletionProcessor extends Disposable {

this._tracer.trace('Scheduled update for diagnostics inline completion');

await this._worker.schedule(async (token: CancellationToken) => this._runCompletionHandler(workspaceDocument, diagnosticsSorted, availableDiagnostics, cursor, log, token));
await this._worker.schedule(async (token: CancellationToken) => this._runCompletionHandler(workspaceDocument, diagnosticsSorted, cursor, log, token));
}

private _getDiagnostics(workspaceDocument: IVSCodeObservableDocument, cursor: Position, logContext: DiagnosticInlineEditRequestLogContext): { availableDiagnostics: Diagnostic[]; relevantDiagnostics: Diagnostic[] } {
private _getDiagnostics(workspaceDocument: IVSCodeObservableDocument, cursor: Position, logContext: DiagnosticInlineEditRequestLogContext): Diagnostic[] {
const availableDiagnostics = workspaceDocument.diagnostics.get().map(d => new Diagnostic(d));

if (availableDiagnostics.length === 0) {
return { availableDiagnostics: [], relevantDiagnostics: [] };
return [];
}

const filterDiagnosticsAndLog = (diagnostics: Diagnostic[], message: string, filterFn: (diagnostics: Diagnostic[]) => Diagnostic[]): Diagnostic[] => {
Expand All @@ -311,10 +311,10 @@ export class DiagnosticsCompletionProcessor extends Disposable {
relevantDiagnostics = filterDiagnosticsAndLog(relevantDiagnostics, 'Filtered by recent acceptance', ds => ds.filter(diagnostic => !this._hasDiagnosticRecentlyBeenAccepted(diagnostic)));
relevantDiagnostics = filterDiagnosticsAndLog(relevantDiagnostics, 'Filtered by no recent edit', ds => this._filterDiagnosticsByRecentEditNearby(ds, workspaceDocument));

return { availableDiagnostics, relevantDiagnostics };
return relevantDiagnostics;
}

private async _runCompletionHandler(workspaceDocument: IVSCodeObservableDocument, diagnosticsSorted: Diagnostic[], allDiagnostics: Diagnostic[], cursor: Position, log: DiagnosticInlineEditRequestLogContext, token: CancellationToken): Promise<IDiagnosticsCompletionState> {
private async _runCompletionHandler(workspaceDocument: IVSCodeObservableDocument, diagnosticsSorted: Diagnostic[], cursor: Position, log: DiagnosticInlineEditRequestLogContext, token: CancellationToken): Promise<IDiagnosticsCompletionState> {
const telemetryBuilder = new DiagnosticsCompletionHandlerTelemetry();

let completionItem = null;
Expand All @@ -327,20 +327,6 @@ export class DiagnosticsCompletionProcessor extends Disposable {

this._tracer.trace('Diagnostic Providers returned completion item: ' + (completionItem ? completionItem.toString() : 'null'));

// Distance to the closest diagnostic which is not supported by any provider
const allNoneSupportedDiagnostics = allDiagnostics.filter(diagnostic => !diagnosticsSorted.includes(diagnostic));
telemetryBuilder.setDistanceToUnknownDiagnostic(distanceToClosestDiagnostic(workspaceDocument, allNoneSupportedDiagnostics, cursor));

// Distance to the closest none result diagnostic
const allAlternativeDiagnostics = allDiagnostics.filter(diagnostic => !completionItem || !completionItem.diagnostic.equals(diagnostic));
telemetryBuilder.setDistanceToAlternativeDiagnostic(distanceToClosestDiagnostic(workspaceDocument, allAlternativeDiagnostics, cursor));

if (completionItem) {
const hasDiagnosticForSameRange = allAlternativeDiagnostics.some(diagnostic => completionItem.diagnostic.range.equals(diagnostic.range));
telemetryBuilder.setHasAlternativeDiagnosticForSameRange(hasDiagnosticForSameRange);
}

// Todo: this should be handled on a lower level
if (completionItem instanceof ImportDiagnosticCompletionItem) {
telemetryBuilder.setImportTelemetry(completionItem);
}
Expand All @@ -354,43 +340,28 @@ export class DiagnosticsCompletionProcessor extends Disposable {
const workspaceDocument = this._workspace.getDocument(docId);
if (!workspaceDocument) { return { item: undefined, telemetry: new DiagnosticsCompletionHandlerTelemetry().addDroppedReason('WorkspaceDocumentNotFound').build(), logContext: undefined }; }

if (currentState === NoResultReason.HasNotRunYet) {
if (currentState === undefined) {
return { item: undefined, telemetry: new DiagnosticsCompletionHandlerTelemetry().build(), logContext: undefined };
}
if (currentState === NoResultReason.WorkInProgress) {
return { item: undefined, telemetry: new DiagnosticsCompletionHandlerTelemetry().addDroppedReason(NoResultReason.WorkInProgress).build(), logContext: undefined };
}

const { telemetryBuilder, completionItem, logContext } = currentState;
const workInProgress = this._worker.workInProgress();
if (!completionItem) {
return { item: undefined, telemetry: telemetryBuilder.build(), logContext };
return { item: undefined, telemetry: telemetryBuilder.build(), logContext, workInProgress };
}

if (!this._isCompletionItemValid(completionItem, workspaceDocument, currentState.logContext, telemetryBuilder)) {
return { item: undefined, telemetry: telemetryBuilder.build(), logContext };
return { item: undefined, telemetry: telemetryBuilder.build(), logContext, workInProgress };
}

if (completionItem.documentId !== docId) {
logContext.addLog('Dropped: wrong-document');
return { item: undefined, telemetry: telemetryBuilder.addDroppedReason('wrong-document').build(), logContext };
return { item: undefined, telemetry: telemetryBuilder.addDroppedReason('wrong-document').build(), logContext, workInProgress };
}

log('following known diagnostics:\n' + this._currentDiagnostics.toString(), undefined, this._tracer);

return { item: completionItem, telemetry: telemetryBuilder.build(), logContext };
}

async getNextUpdatedState(docId: DocumentId, token: CancellationToken): Promise<DiagnosticCompletionState> {
const disposables = new DisposableStore();

await new Promise<void>((resolve) => {
disposables.add(token.onCancellationRequested(() => resolve()));
disposables.add(this._worker.onDidChange(() => resolve()));
});

disposables.dispose();

return this.getCurrentState(docId);
return { item: completionItem, telemetry: telemetryBuilder.build(), logContext, workInProgress };
}

private async _getCompletionFromDiagnostics(workspaceDocument: IVSCodeObservableDocument, diagnosticsSorted: Diagnostic[], pos: Position, logContext: DiagnosticInlineEditRequestLogContext, token: CancellationToken, tb: DiagnosticsCompletionHandlerTelemetry): Promise<DiagnosticCompletionItem | null> {
Expand Down Expand Up @@ -563,30 +534,25 @@ function isEditorFromEditorGrid(editor: vscode.TextEditor): boolean {
return editor.viewColumn !== undefined;
}

const enum NoResultReason {
WorkInProgress = 'work-in-progress',
HasNotRunYet = 'has-not-run-yet'
}

class AsyncWorker<T extends {}> extends Disposable {
private readonly _taskQueue: ThrottledDelayer<void>;

private readonly _onDidChange = this._register(new vscode.EventEmitter<T>());
readonly onDidChange = this._onDidChange.event;

private _currentTokenSource: CancellationTokenSource | undefined = undefined;
private _activeWorkPromise: Promise<void> | undefined = undefined;
private _activeWorkPromise: Promise<T | undefined> | undefined = undefined;

private __currentResult: T | undefined = undefined;
private get _currentResult(): T | undefined {
return this.__currentResult;
}
private set _currentResult(value: T) {
if (!this._taskQueue.isTriggered() && (this.__currentResult === undefined || !this._equals(value, this.__currentResult))) {
const changed = this.__currentResult === undefined || !this._equals(value, this.__currentResult);
this.__currentResult = value;
if (changed) {
this._onDidChange.fire(value);
}

this.__currentResult = value;
}

constructor(delay: number, private readonly _equals: (a: T, b: T) => boolean) {
Expand All @@ -599,47 +565,48 @@ class AsyncWorker<T extends {}> extends Disposable {
const activePromise = this._doSchedule(fn);
this._activeWorkPromise = activePromise;

await activePromise;
const result = await activePromise;

if (this._activeWorkPromise === activePromise) {
this._activeWorkPromise = undefined;
}

if (result !== undefined) {
this._currentResult = result;
}
}

private async _doSchedule(fn: (token: CancellationToken) => Promise<T>): Promise<void> {
private async _doSchedule(fn: (token: CancellationToken) => Promise<T>): Promise<T | undefined> {
this._currentTokenSource?.dispose(true);
this._currentTokenSource = new CancellationTokenSource();
const token = this._currentTokenSource.token;

let result;
await this._taskQueue.trigger(async () => {
if (token.isCancellationRequested) {
return;
}

const result = await fn(token);

if (token.isCancellationRequested) {
return;
}

this._currentResult = result;
result = await fn(token);
});

return result;
}

// Get the active result if there is one currently
// Return undefined if there is currently work being done
getCurrentResult(): T | NoResultReason {
getCurrentResult(): T | undefined {
if (this._currentResult === undefined) {
return NoResultReason.HasNotRunYet;
}

if (this._activeWorkPromise !== undefined) {
return NoResultReason.WorkInProgress;
return undefined;
}

return this._currentResult;
}

workInProgress(): boolean {
return this._activeWorkPromise !== undefined;
}

override dispose(): void {
if (this._currentTokenSource) {
this._currentTokenSource.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ShowNextEditPreference } from '../../../../platform/inlineEdits/common/
import { ILogService } from '../../../../platform/log/common/logService';
import * as errors from '../../../../util/common/errors';
import { createTracer, ITracer } from '../../../../util/common/tracing';
import { timeout } from '../../../../util/vs/base/common/async';
import { raceCancellation, timeout } from '../../../../util/vs/base/common/async';
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
import { BugIndicatingError } from '../../../../util/vs/base/common/errors';
import { Disposable } from '../../../../util/vs/base/common/lifecycle';
Expand All @@ -35,6 +35,7 @@ export class DiagnosticsNextEditResult implements INextEditResult {
showRangePreference?: ShowNextEditPreference;
action?: Command;
} | undefined,
public workInProgress: boolean = false
) { }
}

Expand Down Expand Up @@ -68,18 +69,7 @@ export class DiagnosticsNextEditProvider extends Disposable implements INextEdit

async getNextEdit(docId: DocumentId, context: NESInlineCompletionContext, logContext: InlineEditRequestLogContext, cancellationToken: CancellationToken, tb: DiagnosticsTelemetryBuilder): Promise<DiagnosticsNextEditResult> {
this._lastTriggerTime = Date.now();

if (cancellationToken.isCancellationRequested) {
this._tracer.trace('cancellationRequested before started');
return new DiagnosticsNextEditResult(logContext.requestId, undefined);
}

let diagnosticEditResult = this._diagnosticsCompletionHandler.getCurrentState(docId);
if (!diagnosticEditResult.item) {
diagnosticEditResult = await this._diagnosticsCompletionHandler.getNextUpdatedState(docId, cancellationToken);
}

return this._createNextEditResult(diagnosticEditResult, logContext, tb);
throw new BugIndicatingError('DiagnosticsNextEditProvider does not support getNextEdit, use runUntilNextEdit instead');
}

async runUntilNextEdit(docId: DocumentId, context: NESInlineCompletionContext, logContext: InlineEditRequestLogContext, delayStart: number, cancellationToken: CancellationToken, tb: DiagnosticsTelemetryBuilder): Promise<DiagnosticsNextEditResult> {
Expand All @@ -91,31 +81,40 @@ export class DiagnosticsNextEditProvider extends Disposable implements INextEdit
}

// Check if the last computed edit is still valid
let completionResult = this._diagnosticsCompletionHandler.getCurrentState(docId);
let telemetry = new DiagnosticsTelemetryBuilder();
let diagnosticEditResult = this._createNextEditResult(completionResult, logContext, telemetry);

// If the last computed edit is not valid, wait until the state is updated or the operation is cancelled
while (!diagnosticEditResult.result && !cancellationToken.isCancellationRequested) {
completionResult = await this._diagnosticsCompletionHandler.getNextUpdatedState(docId, cancellationToken);
telemetry = new DiagnosticsTelemetryBuilder();
diagnosticEditResult = this._createNextEditResult(completionResult, logContext, telemetry);
const initialResult = this._getResultForCurrentState(docId, logContext, tb);
if (initialResult.result) {
return initialResult;
}

telemetry.populate(tb);

// TODO: Better incorporate diagnostics logging
if (completionResult.logContext) {
completionResult.logContext.getLogs().forEach(log => logContext.addLog(log));
}

return diagnosticEditResult;
const asyncResult = await raceCancellation(new Promise<DiagnosticsNextEditResult>((resolve) => {
const onDidChangeDisposable = this._diagnosticsCompletionHandler.onDidChange((hasResult) => {
const completionResult = this._getResultForCurrentState(docId, logContext, tb);
if (completionResult.result || !completionResult.workInProgress) {
resolve(completionResult);
onDidChangeDisposable.dispose();
}
});
}), cancellationToken);

return asyncResult ?? initialResult;
} catch (error) {
const errorMessage = `Error occurred while waiting for diagnostic edit: ${errors.toString(errors.fromUnknown(error))}`;
logContext.addLog(errorMessage);
this._tracer.trace(errorMessage);
return new DiagnosticsNextEditResult(logContext.requestId, undefined);
} finally {
this._tracer.trace('DiagnosticsInlineCompletionProvider runUntilNextEdit complete' + (cancellationToken.isCancellationRequested ? ' (cancelled)' : ''));
}
}

private _getResultForCurrentState(docId: DocumentId, logContext: InlineEditRequestLogContext, tb: DiagnosticsTelemetryBuilder): DiagnosticsNextEditResult {
const completionResult = this._diagnosticsCompletionHandler.getCurrentState(docId);
const telemetry = new DiagnosticsTelemetryBuilder();
const diagnosticEditResult = this._createNextEditResult(completionResult, logContext, telemetry);
if (diagnosticEditResult.result) {
telemetry.populate(tb);
}
return diagnosticEditResult;
}

private _createNextEditResult(diagnosticEditResult: DiagnosticCompletionState, logContext: InlineEditRequestLogContext, tb: DiagnosticsTelemetryBuilder): DiagnosticsNextEditResult {
Expand All @@ -125,15 +124,15 @@ export class DiagnosticsNextEditProvider extends Disposable implements INextEdit
if (item && this._hasRecentlyBeenAccepted(item)) {
tb.addDroppedReason(`${item.type}:recently-accepted`);
this._tracer.trace('recently accepted');
return new DiagnosticsNextEditResult(logContext.requestId, undefined);
return new DiagnosticsNextEditResult(logContext.requestId, undefined, diagnosticEditResult.workInProgress);
}

telemetry.droppedReasons.forEach(reason => tb.addDroppedReason(reason));
tb.setDiagnosticRunTelemetry(telemetry);

if (!item) {
this._tracer.trace('no diagnostic edit result');
return new DiagnosticsNextEditResult(logContext.requestId, undefined);
return new DiagnosticsNextEditResult(logContext.requestId, undefined, diagnosticEditResult.workInProgress);
}

tb.setType(item.type);
Expand All @@ -145,7 +144,7 @@ export class DiagnosticsNextEditProvider extends Disposable implements INextEdit
edit: item.toOffsetEdit(),
displayLocation: item.nextEditDisplayLocation,
item
});
}, diagnosticEditResult.workInProgress);
}

handleShown(suggestion: DiagnosticsNextEditResult): void { }
Expand Down
Loading