Path: blob/main/extensions/copilot/src/extension/inlineEdits/vscode-node/features/diagnosticsBasedCompletions/diagnosticsCompletions.ts
13406 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 * as vscode from 'vscode';6import { DiagnosticData } from '../../../../../platform/inlineEdits/common/dataTypes/diagnosticData';7import { DocumentId } from '../../../../../platform/inlineEdits/common/dataTypes/documentId';8import { LanguageId } from '../../../../../platform/inlineEdits/common/dataTypes/languageId';9import { RootedLineEdit } from '../../../../../platform/inlineEdits/common/dataTypes/rootedLineEdit';10import { IObservableDocument } from '../../../../../platform/inlineEdits/common/observableWorkspace';11import { ILogger } from '../../../../../platform/log/common/logService';12import { min } from '../../../../../util/common/arrays';13import { ErrorUtils } from '../../../../../util/common/errors';14import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';15import { LineEdit } from '../../../../../util/vs/editor/common/core/edits/lineEdit';16import { StringReplacement } from '../../../../../util/vs/editor/common/core/edits/stringEdit';17import { TextEdit, TextReplacement } from '../../../../../util/vs/editor/common/core/edits/textEdit';18import { Position } from '../../../../../util/vs/editor/common/core/position';19import { Range } from '../../../../../util/vs/editor/common/core/range';20import { OffsetRange } from '../../../../../util/vs/editor/common/core/ranges/offsetRange';21import { INextEditDisplayLocation } from '../../../node/nextEditResult';22import { IVSCodeObservableDocument } from '../../parts/vscodeWorkspace';23import { toExternalRange, toInternalRange } from '../../utils/translations';2425export interface IDiagnosticCodeAction {26edit: TextReplacement;27}2829export abstract class DiagnosticCompletionItem implements vscode.InlineCompletionItem {3031static equals(a: DiagnosticCompletionItem, b: DiagnosticCompletionItem): boolean {32return a.documentId.toString() === b.documentId.toString() &&33Range.equalsRange(toInternalRange(a.range), toInternalRange(b.range)) &&34a.insertText === b.insertText &&35a.type === b.type &&36a.isInlineEdit === b.isInlineEdit &&37a.showInlineEditMenu === b.showInlineEditMenu &&38displayLocationEquals(a.nextEditDisplayLocation, b.nextEditDisplayLocation);39}4041public readonly isInlineEdit = true;42public readonly showInlineEditMenu = true;4344public readonly abstract providerName: string;4546private _range: vscode.Range | undefined;47get range(): vscode.Range {48if (!this._range) {49this._range = toExternalRange(this._edit.range);50}51return this._range;52}53get insertText(): string {54return this._edit.text;55}56get nextEditDisplayLocation(): INextEditDisplayLocation | undefined {57return this._getDisplayLocation();58}59get displayLocation(): vscode.InlineCompletionDisplayLocation | undefined {60const displayLocation = this.nextEditDisplayLocation;61return displayLocation ? {62range: toExternalRange(displayLocation.range),63label: displayLocation.label,64kind: vscode.InlineCompletionDisplayLocationKind.Code65} : undefined;66}67get documentId(): DocumentId {68return this._workspaceDocument.id;69}7071constructor(72public readonly type: string,73public readonly diagnostic: Diagnostic,74private readonly _edit: TextReplacement,75protected readonly _workspaceDocument: IVSCodeObservableDocument,76) { }7778toOffsetEdit() {79return StringReplacement.replace(this._toOffsetRange(this._edit.range), this._edit.text);80}8182toTextEdit() {83return new TextEdit([this._edit]);84}8586toLineEdit() {87return LineEdit.fromTextEdit(this.toTextEdit(), this._workspaceDocument.value.get());88}8990getDiagnosticOffsetRange() {91return this.diagnostic.range;92}9394getRootedLineEdit() {95return new RootedLineEdit(this._workspaceDocument.value.get(), this.toLineEdit());96}9798private _toOffsetRange(range: Range): OffsetRange {99const transformer = this._workspaceDocument.value.get().getTransformer();100return transformer.getOffsetRange(range);101}102103// TODO: rethink if this needs to be updatable104protected _getDisplayLocation(): INextEditDisplayLocation | undefined {105return undefined;106}107108toString(): string {109return `DiagnosticCompletionItem(type=${this.type}, diagnostic=${this.diagnostic.toString()}, edit=${this._edit.toString()})`;110}111}112113function displayLocationEquals(a: INextEditDisplayLocation | undefined, b: INextEditDisplayLocation | undefined): boolean {114return a === b || (a !== undefined && b !== undefined && a.label === b.label && Range.equalsRange(a.range, b.range));115}116117export interface IDiagnosticCompletionProvider<T extends DiagnosticCompletionItem = DiagnosticCompletionItem> {118readonly providerName: string;119providesCompletionsForDiagnostic(workspaceDocument: IVSCodeObservableDocument, diagnostic: Diagnostic, language: LanguageId, pos: Position): boolean;120provideDiagnosticCompletionItem(workspaceDocument: IVSCodeObservableDocument, sortedDiagnostics: Diagnostic[], pos: Position, logContext: DiagnosticInlineEditRequestLogContext, token: CancellationToken): Promise<T | null>;121completionItemRejected?(item: T): void;122isCompletionItemStillValid?(item: T, workspaceDocument: IObservableDocument): boolean;123}124125// TODO: Better incorporate diagnostics logging126export class DiagnosticInlineEditRequestLogContext {127128getLogs(): string[] {129if (!this._markedToBeLogged) {130return [];131}132133const lines = [];134135if (this._error) {136lines.push(`## Diagnostics Error`);137lines.push('```');138lines.push(ErrorUtils.toString(ErrorUtils.fromUnknown(this._error)));139lines.push('```');140}141142if (this._logs.length > 0) {143lines.push(`## Diagnostics Logs`);144lines.push(...this._logs);145}146147return lines;148}149150private _logs: string[] = [];151addLog(content: string): void {152this._logs.push(content.replace('\n', '\\n').replace('\t', '\\t').replace('`', '\`') + '\n');153}154155private _markedToBeLogged: boolean = false;156markToBeLogged() {157this._markedToBeLogged = true;158}159160private _error: unknown | undefined = undefined;161setError(e: unknown): void {162this._markedToBeLogged = true;163this._error = e;164}165166}167168export class Diagnostic {169170static equals(a: Diagnostic, b: Diagnostic): boolean {171return a.equals(b);172}173174private _updatedRange: OffsetRange;175get range(): OffsetRange {176return this._updatedRange;177}178179private _isValid: boolean = true;180isValid(): boolean {181return this._isValid;182}183184get message(): string {185return this.data.message;186}187188constructor(189public readonly data: DiagnosticData190) {191this._updatedRange = data.range;192}193194equals(other: Diagnostic): boolean {195return this.data.equals(other.data)196&& this._updatedRange.equals(other.range)197&& this._isValid === other._isValid;198}199200toString(): string {201if (this.data.range !== this._updatedRange) {202return `\`${this.data.toString()}\` (currently at \`${this._updatedRange.toString()}\`)`;203}204return `\`${this.data.toString()}\``;205}206207updateRange(range: OffsetRange): void {208this._updatedRange = range;209}210211invalidate(): void {212this._isValid = false;213}214}215216export function log(message: string, logContext?: DiagnosticInlineEditRequestLogContext, logger?: ILogger) {217if (logContext) {218const lines = message.split('\n');219lines.forEach(line => logContext.addLog(line));220}221222if (logger) {223logger.trace(message);224}225}226227export function logList(title: string, list: Array<string | { toString(): string }>, logContext?: DiagnosticInlineEditRequestLogContext, logger?: ILogger) {228const content = `${title}${list.map(item => `\n- ${typeof item === 'string' ? item : item.toString()}`).join('')}`;229log(content, logContext, logger);230}231232// TODO: there must be a utility for this somewhere? Otherwise make them available233234function diagnosticDistanceToPosition(workspaceDocument: IObservableDocument, diagnostic: Diagnostic, position: Position) {235function positionDistance(a: Position, b: Position) {236return { lineDelta: Math.abs(a.lineNumber - b.lineNumber), characterDelta: Math.abs(a.column - b.column) };237}238239const range = workspaceDocument.value.get().getTransformer().getRange(diagnostic.range);240const a = positionDistance(range.getStartPosition(), position);241const b = positionDistance(range.getEndPosition(), position);242243if (a.lineDelta === b.lineDelta) {244return a.characterDelta < b.characterDelta ? a : b;245}246247return a.lineDelta < b.lineDelta ? a : b;248}249250export function isDiagnosticWithinDistance(workspaceDocument: IObservableDocument, diagnostic: Diagnostic, position: Position, maxLineDistance: number): boolean {251return diagnosticDistanceToPosition(workspaceDocument, diagnostic, position).lineDelta <= maxLineDistance;252}253254export function sortDiagnosticsByDistance(workspaceDocument: IObservableDocument, diagnostics: Diagnostic[], position: Position): Diagnostic[] {255const transformer = workspaceDocument.value.get().getTransformer();256return diagnostics.sort((a, b) => {257const aDistance = diagnosticDistanceToPosition(workspaceDocument, a, position);258const bDistance = diagnosticDistanceToPosition(workspaceDocument, b, position);259260if (aDistance.lineDelta !== bDistance.lineDelta) {261return aDistance.lineDelta - bDistance.lineDelta;262}263264const aPosition = transformer.getPosition(a.range.start);265const bPosition = transformer.getPosition(b.range.start);266267if (aPosition.lineNumber !== bPosition.lineNumber) {268return aDistance.characterDelta - bDistance.characterDelta;269}270271if (aDistance.lineDelta < 2) {272return aDistance.characterDelta - bDistance.characterDelta;273}274275// If both diagnostics are on the same line and are more than 1 line away from the cursor276// always prefer the first diagnostic to minimize recomputation and flickering on cursor move277return -1;278});279}280281export function distanceToClosestDiagnostic(workspaceDocument: IObservableDocument, diagnostics: Diagnostic[], position: Position): number | undefined {282if (diagnostics.length === 0) {283return undefined;284}285286const distances = diagnostics.map(diagnostic => diagnosticDistanceToPosition(workspaceDocument, diagnostic, position).lineDelta);287288return min(distances);289}290291292