Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineEdits/common/nearbyCursorInlineEditProvider.ts
13399 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 { StatelessNextEditDocument } from '../../../platform/inlineEdits/common/statelessNextEditProvider';
7
import { ChoiceLogProbs } from '../../../platform/networking/common/openai';
8
import { BugIndicatingError } from '../../../util/vs/base/common/errors';
9
import { Range } from '../../../util/vs/editor/common/core/range';
10
import { OffsetRange, OffsetRangeSet } from '../../../util/vs/editor/common/core/ranges/offsetRange';
11
12
/**
13
* Read the selection from the document, otherwise deduce it from the last edit.
14
*/
15
export function getOrDeduceSelectionFromLastEdit(activeDoc: StatelessNextEditDocument): Range | null {
16
const origin = new OffsetRange(0, 0);
17
if (activeDoc.lastSelectionInAfterEdit && !activeDoc.lastSelectionInAfterEdit.equals(origin)) {
18
return activeDoc.documentAfterEdits.getTransformer().getRange(activeDoc.lastSelectionInAfterEdit);
19
}
20
21
const selectionRange = deduceSelectionFromLastEdit(activeDoc);
22
return selectionRange;
23
}
24
25
function deduceSelectionFromLastEdit(activeDoc: StatelessNextEditDocument): Range | null {
26
const mostRecentEdit = activeDoc.recentEdits.edits.at(-1);
27
if (mostRecentEdit === undefined) {
28
return null;
29
}
30
31
const mostRecentSingleEdit = mostRecentEdit.replacements.at(-1);
32
if (mostRecentSingleEdit === undefined) {
33
return null;
34
}
35
36
const offsetRange = mostRecentSingleEdit.replaceRange;
37
const newText = mostRecentSingleEdit.newText;
38
const change = newText.length - offsetRange.length;
39
const newOffset = offsetRange.endExclusive + change;
40
41
const selectionRange = activeDoc.documentAfterEdits.getTransformer().getRange(new OffsetRange(newOffset, newOffset));
42
43
return selectionRange;
44
}
45
46
type Tokens = Token<number>[];
47
48
export class Token<T> {
49
public readonly range: OffsetRange;
50
51
get id(): string {
52
return this.text + '_' + this.range.toString();
53
}
54
55
constructor(public readonly text: string, public readonly value: T, offset: number) {
56
this.range = new OffsetRange(offset, offset + text.length);
57
}
58
59
public equals(other: Token<T>): boolean {
60
return this.range.equals(other.range) && this.text === other.text;
61
}
62
63
public deltaOffset(offset: number): Token<T> {
64
return new Token(this.text, this.value, this.range.start + offset);
65
}
66
}
67
68
export function clipTokensToRange(tokens: Tokens, range: OffsetRange): Tokens {
69
return tokens.filter(token => range.intersects(token.range));
70
}
71
72
export function clipTokensToRangeAndAdjustOffsets(tokens: Tokens, range: OffsetRange): Tokens {
73
return clipTokensToRange(tokens, range).map(token => token.deltaOffset(-range.start));
74
}
75
76
export function removeTokensInRangeAndAdjustOffsets(tokens: Tokens, range: OffsetRange): Tokens {
77
const adjustedTokens: Tokens = [];
78
for (let token of tokens) {
79
// remove tokens inside the range
80
if (range.containsRange(token.range)) {
81
continue;
82
}
83
// adjust the token offset
84
if (token.range.start > range.start) {
85
token = token.deltaOffset(-range.length);
86
}
87
88
adjustedTokens.push(token);
89
}
90
91
return adjustedTokens;
92
}
93
94
export function getTokensFromLogProbs(logProbs: ChoiceLogProbs, offset: number): Tokens {
95
let acc = offset;
96
return logProbs.content.map(tokenContent => {
97
const token = new Token(tokenContent.token, tokenContent.logprob, acc);
98
acc += token.range.length;
99
return token;
100
});
101
}
102
103
export class LineWithTokens {
104
105
static stringEquals(a: LineWithTokens, b: LineWithTokens): boolean {
106
return a._text === b._text;
107
}
108
109
static fromText(text: string, tokens: Tokens | undefined): LineWithTokens[] {
110
tokens = tokens ?? [];
111
112
const lines: LineWithTokens[] = [];
113
while (true) {
114
const eolIdxWith = text.indexOf('\r\n');
115
const eolIdxWithout = text.indexOf('\n');
116
const eolIdx = (eolIdxWith === -1 ? eolIdxWithout : (eolIdxWithout === -1 ? eolIdxWith : Math.min(eolIdxWith, eolIdxWithout)));
117
const eol = (eolIdxWith !== -1 ? '\r\n' : (eolIdxWithout === -1 ? undefined : '\n'));
118
119
if (eol === undefined) {
120
lines.push(new LineWithTokens(text, tokens, '\n'));
121
break;
122
}
123
124
const lineLength = eolIdx + eol.length;
125
const line = text.substring(0, eolIdx);
126
const lineTokensWithBoundary = tokens.filter(t => t.range.start < lineLength && t.range.endExclusive > 0);
127
lines.push(new LineWithTokens(line, lineTokensWithBoundary, eol));
128
129
text = text.substring(lineLength);
130
tokens = tokens.map(t => t.deltaOffset(-lineLength)).filter(t => t.range.endExclusive > 0);
131
}
132
133
return lines;
134
}
135
136
get text(): string { return this._text; }
137
get tokens(): Tokens { return this._tokens; }
138
get length(): number { return this._text.length; }
139
get lengthWithEOL(): number { return this._text.length + this._eol.length; }
140
get eol(): '\n' | '\r\n' { return this._eol; }
141
142
constructor(
143
private readonly _text: string,
144
private readonly _tokens: Tokens,
145
private readonly _eol: '\n' | '\r\n'
146
) { }
147
148
trim() {
149
return this.trimStart().trimEnd();
150
}
151
152
trimStart() {
153
const lineStartTrimmed = this._text.trimStart();
154
const trimmedLength = this._text.length - lineStartTrimmed.length;
155
const tokensUpdated = this._tokens.map(t => t.deltaOffset(-trimmedLength)).filter(t => t.range.endExclusive > 0);
156
return new LineWithTokens(lineStartTrimmed, tokensUpdated, this._eol);
157
}
158
159
trimEnd() {
160
const lineEndTrimmed = this._text.trimEnd();
161
const tokensUpdated = this._tokens.filter(t => t.range.start < lineEndTrimmed.length);
162
return new LineWithTokens(lineEndTrimmed, tokensUpdated, this._eol);
163
}
164
165
substring(start: number, end: number): LineWithTokens {
166
const lineSubstring = this._text.substring(start, end);
167
const tokensUpdated = this._tokens.map(t => t.deltaOffset(-start)).filter(t => t.range.endExclusive > 0 && t.range.start < lineSubstring.length);
168
return new LineWithTokens(lineSubstring, tokensUpdated, this._eol);
169
}
170
171
stringEquals(other: LineWithTokens): boolean {
172
return LineWithTokens.stringEquals(this, other);
173
}
174
175
equals(other: LineWithTokens): boolean {
176
return this._text === other.text
177
&& this._tokens.length === other.tokens.length
178
&& this._tokens.every((t, i) => t.equals(other.tokens[i]));
179
}
180
181
dropTokens(tokens: Tokens): LineWithTokens {
182
return new LineWithTokens(this._text, this._tokens.filter(t => !tokens.some(token => t.equals(token))), this._eol);
183
}
184
185
findTokens(fn: (token: Token<number>) => boolean): Token<number>[] {
186
return this._tokens.filter(fn);
187
}
188
}
189
190
export function getTokensFromLinesWithTokens(lines: LineWithTokens[]): Tokens {
191
let offset = 0;
192
193
const tokens: Tokens = [];
194
for (const line of lines) {
195
const textLine = line.text + line.eol;
196
tokens.push(...line.tokens.map(t => t.deltaOffset(offset)));
197
offset += textLine.length;
198
}
199
200
const tokensDeduplicated: Tokens = [];
201
const tokensSeen = new Set<string>();
202
for (const token of tokens) {
203
if (!tokensSeen.has(token.id)) {
204
tokensSeen.add(token.id);
205
tokensDeduplicated.push(token);
206
}
207
}
208
209
return tokensDeduplicated;
210
}
211
212
export function mergeOffsetRangesAtDistance(ranges: OffsetRange[], distance: number): OffsetRange[] {
213
if (distance < 0) {
214
throw new BugIndicatingError('Distance must be positive');
215
}
216
217
const rangesGrown = ranges.map(r => new OffsetRange(r.start - distance, r.endExclusive + distance));
218
219
const set = new OffsetRangeSet();
220
for (const range of rangesGrown) {
221
set.addRange(range);
222
}
223
224
return set.ranges.map(r => new OffsetRange(r.start + distance, r.endExclusive - distance));
225
}
226
227