Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/prompts/node/inline/adjustSelection.ts
13405 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 { AbstractDocument } from '../../../../platform/editing/common/abstractText';
7
import { OverlayNode } from '../../../../platform/parser/node/nodes';
8
import { binarySearch2, equals } from '../../../../util/vs/base/common/arrays';
9
import { CharCode } from '../../../../util/vs/base/common/charCode';
10
import { BugIndicatingError } from '../../../../util/vs/base/common/errors';
11
import { OffsetRange } from '../../../../util/vs/editor/common/core/ranges/offsetRange';
12
import { Range } from '../../../../vscodeTypes';
13
14
export function getAdjustedSelection<TDocument extends AbstractDocument>(
15
ast: OverlayNode,
16
document: TDocument,
17
userSelection: Range,
18
): { adjusted: OffsetRange; original: OffsetRange } {
19
const documentText = document.getText();
20
const astWithoutWs = alignOverlayNodesToNonWsText(ast, documentText);
21
const root = LinkedOverlayNode.convertToLinkedTree(documentText, astWithoutWs ?? ast);
22
const start = document.getOffsetAtPosition(userSelection.start);
23
const end = document.getOffsetAtPosition(userSelection.end);
24
const adjustedSelection = markSelectedNodes(root, start, end);
25
return { adjusted: adjustedSelection, original: new OffsetRange(start, end) };
26
}
27
28
function alignOverlayNodesToNonWsText(ast: OverlayNode, text: string): OverlayNode | undefined {
29
const newStartIndex = alignToNonWsTextRight(ast.startIndex, text);
30
const newEndIndex = Math.max(newStartIndex, alignToNonWsTextLeft(ast.endIndex, text));
31
if (newStartIndex === newEndIndex) { // indentation-based structure can include nodes which can just contain newlines
32
return undefined;
33
}
34
const arr = ast.children.map(child => alignOverlayNodesToNonWsText(child, text)).filter(s => s !== undefined);
35
if (newStartIndex === ast.startIndex && newEndIndex === ast.endIndex && equals(arr, ast.children)) {
36
return ast;
37
}
38
return new OverlayNode(newStartIndex, newEndIndex, ast.kind, arr);
39
}
40
41
function alignToNonWsTextRight(idx: number, str: string): number {
42
while (idx < str.length) {
43
const ch = str.charCodeAt(idx);
44
if (ch !== CharCode.Space && ch !== CharCode.Tab && ch !== CharCode.LineFeed && ch !== CharCode.CarriageReturn) {
45
return idx;
46
}
47
idx++;
48
}
49
return idx;
50
}
51
52
function alignToNonWsTextLeft(idx: number, str: string): number {
53
while (idx > 0) {
54
const ch = str.charCodeAt(idx - 1);
55
if (ch !== CharCode.Space && ch !== CharCode.Tab && ch !== CharCode.LineFeed && ch !== CharCode.CarriageReturn) {
56
return idx;
57
}
58
idx--;
59
}
60
return idx;
61
}
62
63
function markSelectedNodes(root: LinkedOverlayNode, start: number, end: number) {
64
[start, end] = moveTowardsContent(root, start, end);
65
return adjustSelection(root, start, end);
66
}
67
68
/**
69
* If the selection sits on whitespace, move it towards the closest content
70
*/
71
function moveTowardsContent(root: LinkedOverlayNode, initialStart: number, initialEnd: number): [number, number] {
72
const selectedText = root.text.substring(initialStart, initialEnd);
73
const selectedTextIsEmptyOrWhitespace = /^\s*$/.test(selectedText);
74
if (!selectedTextIsEmptyOrWhitespace) {
75
return [initialStart, initialEnd];
76
}
77
let start = initialStart;
78
let end = initialEnd;
79
let goLeft = true;
80
let goRight = true;
81
do {
82
if (goRight && end >= root.text.length) {
83
// can't go right anymore
84
goRight = false;
85
}
86
if (goRight) {
87
const nextCharCode = root.text.charCodeAt(end);
88
if (nextCharCode === CharCode.CarriageReturn || nextCharCode === CharCode.LineFeed) {
89
// Hit the EOL
90
goRight = false;
91
} else if (nextCharCode !== CharCode.Space && nextCharCode !== CharCode.Tab) {
92
// Hit real content
93
return [end, end + 1];
94
} else {
95
end++;
96
}
97
}
98
if (goLeft && start === 0) {
99
// can't go left anymore
100
goLeft = false;
101
}
102
if (goLeft) {
103
const prevCharCode = root.text.charCodeAt(start - 1);
104
if (prevCharCode === CharCode.CarriageReturn || prevCharCode === CharCode.LineFeed) {
105
// Hit the EOL
106
goLeft = false;
107
} else if (prevCharCode !== CharCode.Space && prevCharCode !== CharCode.Tab) {
108
// Hit real content
109
return [start - 1, start];
110
} else {
111
start--;
112
}
113
}
114
} while (goLeft || goRight);
115
116
// Couldn't find real content
117
return [initialStart, initialEnd];
118
}
119
120
function adjustSelection(root: LinkedOverlayNode, start: number, end: number): OffsetRange {
121
// If the selection starts at the end of a line with content, move over the line feed
122
if (start > 0 && start < end && root.text.charCodeAt(start - 1) !== CharCode.LineFeed && root.text.charCodeAt(start) === CharCode.LineFeed) {
123
start++;
124
}
125
126
start = moveToStartOfLineOverWhitespace(root, start);
127
end = moveToEndOfLineOverWhitespace(root, end);
128
129
let startNode = root.findLeaf2(start);
130
let endNode = root.findLeaf2(end);
131
132
let hasChanged = false;
133
134
const extendStart = (newStart: number) => {
135
if (newStart < start) {
136
start = moveToStartOfLineOverWhitespace(root, newStart);
137
hasChanged = true;
138
startNode = root.findLeaf2(start);
139
}
140
};
141
142
const extendEnd = (newEnd: number) => {
143
if (newEnd > end) {
144
end = moveToEndOfLineOverWhitespace(root, newEnd);
145
hasChanged = true;
146
endNode = root.findLeaf2(end);
147
}
148
};
149
150
do {
151
hasChanged = false;
152
153
if (startNode instanceof LinkedOverlayNodeGap) {
154
const matchingGap = (startNode.isFirstGapInParent ? startNode.parent.lastGap : null);
155
const hasSelectedContentInGap = startNode.hasSelectedContent(start, end);
156
const hasSelectedContentInMatchingGap = (matchingGap && matchingGap.hasSelectedContent(start, end));
157
const extendSelection = (hasSelectedContentInGap || hasSelectedContentInMatchingGap);
158
if (extendSelection) {
159
extendStart(startNode.firstNonWhitespaceIndex);
160
}
161
if (extendSelection && matchingGap) {
162
extendEnd(matchingGap.lastNonWhitespaceIndex + 1);
163
}
164
}
165
166
if (endNode instanceof LinkedOverlayNodeGap) {
167
const matchingFirstGap = (endNode.isLastGapInParent ? endNode.parent.firstGap : null);
168
const hasSelectedContentInGap = endNode.hasSelectedContent(start, end);
169
const hasSelectedContentInFirstGap = (matchingFirstGap && matchingFirstGap.hasSelectedContent(start, end));
170
const extendSelection = (hasSelectedContentInGap || hasSelectedContentInFirstGap);
171
if (extendSelection && endNode.lastNonWhitespaceIndex + 1 > end) {
172
extendEnd(endNode.lastNonWhitespaceIndex + 1);
173
}
174
if (extendSelection && matchingFirstGap) {
175
extendStart(matchingFirstGap.firstNonWhitespaceIndex);
176
}
177
}
178
179
if (startNode instanceof LinkedOverlayNode && root.hasContentInRange(new OffsetRange(start, end))) {
180
// Hit a leaf!
181
if (startNode.startIndex < start) {
182
extendStart(startNode.startIndex);
183
}
184
}
185
186
if (endNode instanceof LinkedOverlayNode && root.hasContentInRange(new OffsetRange(start, end))) {
187
// Hit a leaf!
188
if (endNode.endIndex > end) {
189
extendEnd(endNode.endIndex);
190
}
191
}
192
193
} while (hasChanged);
194
195
return new OffsetRange(start, end);
196
}
197
198
function moveToStartOfLineOverWhitespace(root: LinkedOverlayNode, start: number): number {
199
// Move start to the start of the line if it only goes over whitespace
200
while (start > 0) {
201
const charCodeBeforeSelection = root.text.charCodeAt(start - 1);
202
if (charCodeBeforeSelection !== CharCode.Space && charCodeBeforeSelection !== CharCode.Tab) {
203
break;
204
}
205
start--;
206
}
207
return start;
208
}
209
210
function moveToEndOfLineOverWhitespace(root: LinkedOverlayNode, end: number): number {
211
const charCodeBefore = end > 0 ? root.text.charCodeAt(end - 1) : CharCode.Null;
212
if (charCodeBefore === CharCode.LineFeed) {
213
// Do not leave first character of the line
214
return end;
215
}
216
217
// Move end to the end of the line if it only goes over whitespace
218
while (end < root.text.length) {
219
const charCodeAfterSelection = root.text.charCodeAt(end);
220
if (charCodeAfterSelection !== CharCode.Space && charCodeAfterSelection !== CharCode.Tab) {
221
break;
222
}
223
end++;
224
}
225
return end;
226
}
227
228
function debugstr(str: string) {
229
return str.replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/\t/g, '\\t');
230
}
231
232
/**
233
* A tree datastructure which has parent pointers and markers if a node will survive or not.
234
*/
235
export class LinkedOverlayNode {
236
237
public static convertToLinkedTree(text: string, root: OverlayNode): LinkedOverlayNode {
238
const linkedRoot = new LinkedOverlayNode(text, null, root.startIndex, root.endIndex, root.kind, [], 0); // parentChildIndex
239
LinkedOverlayNode._convertChildrenToLinkedTree(text, root, linkedRoot); // Start with depth 1
240
return linkedRoot;
241
}
242
243
private static _convertChildrenToLinkedTree(text: string, overlayNode: OverlayNode, linkedNode: LinkedOverlayNode) {
244
for (let i = 0; i < overlayNode.children.length; i++) {
245
const child = overlayNode.children[i];
246
const linkedChild = new LinkedOverlayNode(text, linkedNode, child.startIndex, child.endIndex, child.kind, [], i);
247
linkedNode.children.push(linkedChild);
248
LinkedOverlayNode._convertChildrenToLinkedTree(text, child, linkedChild);
249
}
250
}
251
252
constructor(
253
private readonly _originalText: string,
254
public readonly parent: LinkedOverlayNode | null,
255
public readonly startIndex: number,
256
public readonly endIndex: number,
257
public readonly kind: string, // TODO@ulugbekna: come up with more generic kinds so that these aren't per-language, then use enum?
258
public readonly children: LinkedOverlayNode[],
259
private readonly myIndex: number, // Added parentChildIndex field
260
) { }
261
262
public get text(): string {
263
return this._originalText.substring(this.startIndex, this.endIndex);
264
}
265
266
public textAt(range: OffsetRange): string {
267
return range.substring(this._originalText);
268
}
269
270
/**
271
* Intersects the selection with this node's range to check if there is any non-whitespace text selected from this gap.
272
*/
273
public hasContentInRange(range: OffsetRange): boolean {
274
const content = this.textAt(range);
275
return !/^\s*$/s.test(content);
276
}
277
278
public toString(): string {
279
return `Node (${this.startIndex}-${this.endIndex}) ${debugstr(this._originalText.substring(this.startIndex, this.endIndex))}`;
280
}
281
282
gapBeforeChild(childIndex: number): LinkedOverlayNodeGap {
283
const startIndex = (childIndex === 0 ? this.startIndex : this.children[childIndex - 1].endIndex);
284
const endIndex = (childIndex === this.children.length ? this.endIndex : this.children[childIndex].startIndex);
285
return new LinkedOverlayNodeGap(this._originalText, this, startIndex, endIndex, childIndex);
286
}
287
288
childAt(childIndex: number): LinkedOverlayNode | null {
289
return this.children[childIndex] ?? null;
290
}
291
292
public get firstGap(): LinkedOverlayNodeGap | null {
293
if (this.children.length === 0) {
294
// there are no gaps
295
return null;
296
}
297
return this.gapBeforeChild(0);
298
}
299
300
public get lastGap(): LinkedOverlayNodeGap | null {
301
if (this.children.length === 0) {
302
// there are no gaps
303
return null;
304
}
305
return this.gapBeforeChild(this.children.length);
306
}
307
308
/**
309
* @return The deepest node which contains the offset. If the node is a leaf, the second pair result is 0.
310
* If the node is not a leaf, the second pair result will be -(n+1) (or ~n, using bitwise notation),
311
* where n is the index of the child before which the offset lies. (i.e. offset lies between children n-1 and n)
312
*/
313
public findLeaf(offset: number): [LinkedOverlayNode, number] {
314
if (this.children.length === 0) {
315
return [this, 0];
316
}
317
318
const index = binarySearch2(this.children.length, (index) => {
319
const child = this.children[index];
320
if (offset >= child.startIndex && offset <= child.endIndex) {
321
return 0;
322
} else if (offset < child.startIndex) {
323
return 1;
324
} else {
325
return -1;
326
}
327
});
328
329
if (index < 0) {
330
return [this, index];
331
}
332
333
return this.children[index].findLeaf(offset);
334
}
335
336
public findLeaf2(offset: number): LinkedOverlayNodeOrGap {
337
const [leaf, leafChildGapIndex] = this.findLeaf(offset);
338
if (leafChildGapIndex < 0) {
339
return leaf.gapBeforeChild(~leafChildGapIndex);
340
}
341
return leaf;
342
}
343
344
public get nextLeaf(): LinkedOverlayNode | null {
345
let currentNode: LinkedOverlayNode | null = this;
346
do {
347
const nextSibling = currentNode.nextSibling;
348
if (nextSibling) {
349
return nextSibling.leftMostLeafChild;
350
}
351
// go up until finding a next sibling
352
currentNode = currentNode.parent;
353
} while (currentNode);
354
return null;
355
}
356
357
public get leftMostLeafChild(): LinkedOverlayNode {
358
let currentNode: LinkedOverlayNode = this;
359
while (currentNode.children.length > 0) {
360
currentNode = currentNode.children[0];
361
}
362
return currentNode;
363
}
364
365
public get prevSibling(): LinkedOverlayNode | null {
366
const parent = this.parent;
367
const prevIndex = this.myIndex - 1;
368
return (parent && prevIndex >= 0) ? parent.children[prevIndex] : null;
369
}
370
371
public get nextSibling(): LinkedOverlayNode | null {
372
const parent = this.parent;
373
const nextIndex = this.myIndex + 1;
374
return (parent && nextIndex < parent.children.length) ? parent.children[nextIndex] : null;
375
}
376
}
377
378
/**
379
* Represents a gap (before the first child, between two children, or after the last child) in a `LinkedOverlayNode`.
380
*/
381
class LinkedOverlayNodeGap {
382
383
constructor(
384
private readonly _originalText: string,
385
public readonly parent: LinkedOverlayNode,
386
public readonly startIndex: number,
387
public readonly endIndex: number,
388
public readonly gapIndex: number,
389
) {
390
if (this.startIndex > this.endIndex) {
391
throw new BugIndicatingError('Invalid gap');
392
}
393
}
394
395
public get range(): OffsetRange {
396
return new OffsetRange(this.startIndex, this.endIndex);
397
}
398
399
public get isFirstGapInParent(): boolean {
400
return this.gapIndex === 0;
401
}
402
403
public get isLastGapInParent(): boolean {
404
return this.gapIndex === this.parent.children.length;
405
}
406
407
public toString(): string {
408
return `NodeGap (${this.startIndex}-${this.endIndex}) ${debugstr(this._originalText.substring(this.startIndex, this.endIndex))}`;
409
}
410
411
public get text(): string {
412
return this._originalText.substring(this.startIndex, this.endIndex);
413
}
414
415
/**
416
* Intersects the selection with this node's range to check if there is any non-whitespace text selected from this gap.
417
*/
418
public hasSelectedContent(start: number, end: number): boolean {
419
const selectedGapRange = this.range.intersect(new OffsetRange(start, end));
420
const selectedGapText = selectedGapRange ? this._originalText.substring(selectedGapRange.start, selectedGapRange.endExclusive) : '';
421
return !/^\s*$/s.test(selectedGapText);
422
}
423
424
public get firstNonWhitespaceIndex(): number {
425
let index = this.startIndex;
426
while (index < this.endIndex) {
427
const charCode = this._originalText.charCodeAt(index);
428
if (charCode !== CharCode.Tab && charCode !== CharCode.Space && charCode !== CharCode.LineFeed) {
429
return index;
430
}
431
index++;
432
}
433
return this.endIndex;
434
}
435
436
public get lastNonWhitespaceIndex(): number {
437
let index = this.endIndex - 1;
438
while (index >= this.startIndex) {
439
const charCode = this._originalText.charCodeAt(index);
440
if (charCode !== CharCode.Tab && charCode !== CharCode.Space && charCode !== CharCode.LineFeed) {
441
return index;
442
}
443
index--;
444
}
445
return this.startIndex - 1;
446
}
447
448
public get nextLeaf(): LinkedOverlayNode | null {
449
const nextSibling = this.parent.childAt(this.gapIndex);
450
return nextSibling ? nextSibling.leftMostLeafChild : this.parent.nextLeaf;
451
}
452
453
}
454
455
type LinkedOverlayNodeOrGap = LinkedOverlayNode | LinkedOverlayNodeGap;
456
457