Path: blob/main/extensions/copilot/src/extension/inlineEdits/common/nearbyCursorInlineEditProvider.ts
13399 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 { StatelessNextEditDocument } from '../../../platform/inlineEdits/common/statelessNextEditProvider';6import { ChoiceLogProbs } from '../../../platform/networking/common/openai';7import { BugIndicatingError } from '../../../util/vs/base/common/errors';8import { Range } from '../../../util/vs/editor/common/core/range';9import { OffsetRange, OffsetRangeSet } from '../../../util/vs/editor/common/core/ranges/offsetRange';1011/**12* Read the selection from the document, otherwise deduce it from the last edit.13*/14export function getOrDeduceSelectionFromLastEdit(activeDoc: StatelessNextEditDocument): Range | null {15const origin = new OffsetRange(0, 0);16if (activeDoc.lastSelectionInAfterEdit && !activeDoc.lastSelectionInAfterEdit.equals(origin)) {17return activeDoc.documentAfterEdits.getTransformer().getRange(activeDoc.lastSelectionInAfterEdit);18}1920const selectionRange = deduceSelectionFromLastEdit(activeDoc);21return selectionRange;22}2324function deduceSelectionFromLastEdit(activeDoc: StatelessNextEditDocument): Range | null {25const mostRecentEdit = activeDoc.recentEdits.edits.at(-1);26if (mostRecentEdit === undefined) {27return null;28}2930const mostRecentSingleEdit = mostRecentEdit.replacements.at(-1);31if (mostRecentSingleEdit === undefined) {32return null;33}3435const offsetRange = mostRecentSingleEdit.replaceRange;36const newText = mostRecentSingleEdit.newText;37const change = newText.length - offsetRange.length;38const newOffset = offsetRange.endExclusive + change;3940const selectionRange = activeDoc.documentAfterEdits.getTransformer().getRange(new OffsetRange(newOffset, newOffset));4142return selectionRange;43}4445type Tokens = Token<number>[];4647export class Token<T> {48public readonly range: OffsetRange;4950get id(): string {51return this.text + '_' + this.range.toString();52}5354constructor(public readonly text: string, public readonly value: T, offset: number) {55this.range = new OffsetRange(offset, offset + text.length);56}5758public equals(other: Token<T>): boolean {59return this.range.equals(other.range) && this.text === other.text;60}6162public deltaOffset(offset: number): Token<T> {63return new Token(this.text, this.value, this.range.start + offset);64}65}6667export function clipTokensToRange(tokens: Tokens, range: OffsetRange): Tokens {68return tokens.filter(token => range.intersects(token.range));69}7071export function clipTokensToRangeAndAdjustOffsets(tokens: Tokens, range: OffsetRange): Tokens {72return clipTokensToRange(tokens, range).map(token => token.deltaOffset(-range.start));73}7475export function removeTokensInRangeAndAdjustOffsets(tokens: Tokens, range: OffsetRange): Tokens {76const adjustedTokens: Tokens = [];77for (let token of tokens) {78// remove tokens inside the range79if (range.containsRange(token.range)) {80continue;81}82// adjust the token offset83if (token.range.start > range.start) {84token = token.deltaOffset(-range.length);85}8687adjustedTokens.push(token);88}8990return adjustedTokens;91}9293export function getTokensFromLogProbs(logProbs: ChoiceLogProbs, offset: number): Tokens {94let acc = offset;95return logProbs.content.map(tokenContent => {96const token = new Token(tokenContent.token, tokenContent.logprob, acc);97acc += token.range.length;98return token;99});100}101102export class LineWithTokens {103104static stringEquals(a: LineWithTokens, b: LineWithTokens): boolean {105return a._text === b._text;106}107108static fromText(text: string, tokens: Tokens | undefined): LineWithTokens[] {109tokens = tokens ?? [];110111const lines: LineWithTokens[] = [];112while (true) {113const eolIdxWith = text.indexOf('\r\n');114const eolIdxWithout = text.indexOf('\n');115const eolIdx = (eolIdxWith === -1 ? eolIdxWithout : (eolIdxWithout === -1 ? eolIdxWith : Math.min(eolIdxWith, eolIdxWithout)));116const eol = (eolIdxWith !== -1 ? '\r\n' : (eolIdxWithout === -1 ? undefined : '\n'));117118if (eol === undefined) {119lines.push(new LineWithTokens(text, tokens, '\n'));120break;121}122123const lineLength = eolIdx + eol.length;124const line = text.substring(0, eolIdx);125const lineTokensWithBoundary = tokens.filter(t => t.range.start < lineLength && t.range.endExclusive > 0);126lines.push(new LineWithTokens(line, lineTokensWithBoundary, eol));127128text = text.substring(lineLength);129tokens = tokens.map(t => t.deltaOffset(-lineLength)).filter(t => t.range.endExclusive > 0);130}131132return lines;133}134135get text(): string { return this._text; }136get tokens(): Tokens { return this._tokens; }137get length(): number { return this._text.length; }138get lengthWithEOL(): number { return this._text.length + this._eol.length; }139get eol(): '\n' | '\r\n' { return this._eol; }140141constructor(142private readonly _text: string,143private readonly _tokens: Tokens,144private readonly _eol: '\n' | '\r\n'145) { }146147trim() {148return this.trimStart().trimEnd();149}150151trimStart() {152const lineStartTrimmed = this._text.trimStart();153const trimmedLength = this._text.length - lineStartTrimmed.length;154const tokensUpdated = this._tokens.map(t => t.deltaOffset(-trimmedLength)).filter(t => t.range.endExclusive > 0);155return new LineWithTokens(lineStartTrimmed, tokensUpdated, this._eol);156}157158trimEnd() {159const lineEndTrimmed = this._text.trimEnd();160const tokensUpdated = this._tokens.filter(t => t.range.start < lineEndTrimmed.length);161return new LineWithTokens(lineEndTrimmed, tokensUpdated, this._eol);162}163164substring(start: number, end: number): LineWithTokens {165const lineSubstring = this._text.substring(start, end);166const tokensUpdated = this._tokens.map(t => t.deltaOffset(-start)).filter(t => t.range.endExclusive > 0 && t.range.start < lineSubstring.length);167return new LineWithTokens(lineSubstring, tokensUpdated, this._eol);168}169170stringEquals(other: LineWithTokens): boolean {171return LineWithTokens.stringEquals(this, other);172}173174equals(other: LineWithTokens): boolean {175return this._text === other.text176&& this._tokens.length === other.tokens.length177&& this._tokens.every((t, i) => t.equals(other.tokens[i]));178}179180dropTokens(tokens: Tokens): LineWithTokens {181return new LineWithTokens(this._text, this._tokens.filter(t => !tokens.some(token => t.equals(token))), this._eol);182}183184findTokens(fn: (token: Token<number>) => boolean): Token<number>[] {185return this._tokens.filter(fn);186}187}188189export function getTokensFromLinesWithTokens(lines: LineWithTokens[]): Tokens {190let offset = 0;191192const tokens: Tokens = [];193for (const line of lines) {194const textLine = line.text + line.eol;195tokens.push(...line.tokens.map(t => t.deltaOffset(offset)));196offset += textLine.length;197}198199const tokensDeduplicated: Tokens = [];200const tokensSeen = new Set<string>();201for (const token of tokens) {202if (!tokensSeen.has(token.id)) {203tokensSeen.add(token.id);204tokensDeduplicated.push(token);205}206}207208return tokensDeduplicated;209}210211export function mergeOffsetRangesAtDistance(ranges: OffsetRange[], distance: number): OffsetRange[] {212if (distance < 0) {213throw new BugIndicatingError('Distance must be positive');214}215216const rangesGrown = ranges.map(r => new OffsetRange(r.start - distance, r.endExclusive + distance));217218const set = new OffsetRangeSet();219for (const range of rangesGrown) {220set.addRange(range);221}222223return set.ranges.map(r => new OffsetRange(r.start + distance, r.endExclusive - distance));224}225226227