Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/ghostText.ts
4797 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 { equals } from '../../../../../base/common/arrays.js';6import { splitLines } from '../../../../../base/common/strings.js';7import { Position } from '../../../../common/core/position.js';8import { Range } from '../../../../common/core/range.js';9import { TextReplacement, TextEdit } from '../../../../common/core/edits/textEdit.js';10import { LineDecoration } from '../../../../common/viewLayout/lineDecorations.js';11import { ColumnRange } from '../../../../common/core/ranges/columnRange.js';12import { assertFn, checkAdjacentItems } from '../../../../../base/common/assert.js';13import { InlineDecoration } from '../../../../common/viewModel/inlineDecorations.js';1415export class GhostText {16constructor(17public readonly lineNumber: number,18public readonly parts: GhostTextPart[],19) {20assertFn(() => checkAdjacentItems(parts, (p1, p2) => p1.column <= p2.column));21}2223equals(other: GhostText): boolean {24return this.lineNumber === other.lineNumber &&25this.parts.length === other.parts.length &&26this.parts.every((part, index) => part.equals(other.parts[index]));27}2829/**30* Only used for testing/debugging.31*/32render(documentText: string, debug: boolean = false): string {33return new TextEdit([34...this.parts.map(p => new TextReplacement(35Range.fromPositions(new Position(this.lineNumber, p.column)),36debug ? `[${p.lines.map(line => line.line).join('\n')}]` : p.lines.map(line => line.line).join('\n')37)),38]).applyToString(documentText);39}4041renderForScreenReader(lineText: string): string {42if (this.parts.length === 0) {43return '';44}45const lastPart = this.parts[this.parts.length - 1];4647const cappedLineText = lineText.substr(0, lastPart.column - 1);48const text = new TextEdit([49...this.parts.map(p => new TextReplacement(50Range.fromPositions(new Position(1, p.column)),51p.lines.map(line => line.line).join('\n')52)),53]).applyToString(cappedLineText);5455return text.substring(this.parts[0].column - 1);56}5758isEmpty(): boolean {59return this.parts.every(p => p.lines.length === 0);60}6162get lineCount(): number {63return 1 + this.parts.reduce((r, p) => r + p.lines.length - 1, 0);64}65}6667export interface IGhostTextLine {68line: string;69lineDecorations: LineDecoration[];70}717273export class GhostTextPart {7475readonly lines: IGhostTextLine[];7677constructor(78readonly column: number,79readonly text: string,80/**81* Indicates if this part is a preview of an inline suggestion when a suggestion is previewed.82*/83readonly preview: boolean,84private _inlineDecorations: InlineDecoration[] = [],85) {86this.lines = splitLines(this.text).map((line, i) => ({87line,88lineDecorations: LineDecoration.filter(this._inlineDecorations, i + 1, 1, line.length + 1)89}));90}9192equals(other: GhostTextPart): boolean {93return this.column === other.column &&94this.lines.length === other.lines.length &&95this.lines.every((line, index) =>96line.line === other.lines[index].line &&97LineDecoration.equalsArr(line.lineDecorations, other.lines[index].lineDecorations)98);99}100}101102export class GhostTextReplacement {103public readonly parts: ReadonlyArray<GhostTextPart>;104readonly newLines: string[];105106constructor(107readonly lineNumber: number,108readonly columnRange: ColumnRange,109readonly text: string,110public readonly additionalReservedLineCount: number = 0,111) {112this.parts = [113new GhostTextPart(114this.columnRange.endColumnExclusive,115this.text,116false117),118];119this.newLines = splitLines(this.text);120}121122renderForScreenReader(_lineText: string): string {123return this.newLines.join('\n');124}125126render(documentText: string, debug: boolean = false): string {127const replaceRange = this.columnRange.toRange(this.lineNumber);128129if (debug) {130return new TextEdit([131new TextReplacement(Range.fromPositions(replaceRange.getStartPosition()), '('),132new TextReplacement(Range.fromPositions(replaceRange.getEndPosition()), `)[${this.newLines.join('\n')}]`),133]).applyToString(documentText);134} else {135return new TextEdit([136new TextReplacement(replaceRange, this.newLines.join('\n')),137]).applyToString(documentText);138}139}140141get lineCount(): number {142return this.newLines.length;143}144145isEmpty(): boolean {146return this.parts.every(p => p.lines.length === 0);147}148149equals(other: GhostTextReplacement): boolean {150return this.lineNumber === other.lineNumber &&151this.columnRange.equals(other.columnRange) &&152this.newLines.length === other.newLines.length &&153this.newLines.every((line, index) => line === other.newLines[index]) &&154this.additionalReservedLineCount === other.additionalReservedLineCount;155}156}157158export type GhostTextOrReplacement = GhostText | GhostTextReplacement;159160export function ghostTextsOrReplacementsEqual(a: readonly GhostTextOrReplacement[] | undefined, b: readonly GhostTextOrReplacement[] | undefined): boolean {161return equals(a, b, ghostTextOrReplacementEquals);162}163164export function ghostTextOrReplacementEquals(a: GhostTextOrReplacement | undefined, b: GhostTextOrReplacement | undefined): boolean {165if (a === b) {166return true;167}168if (!a || !b) {169return false;170}171if (a instanceof GhostText && b instanceof GhostText) {172return a.equals(b);173}174if (a instanceof GhostTextReplacement && b instanceof GhostTextReplacement) {175return a.equals(b);176}177return false;178}179180181