Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/common/model/tokens/treeSitter/treeSitterTree.ts
3296 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
import type * as TreeSitter from '@vscode/tree-sitter-wasm';
6
import { TaskQueue } from '../../../../../base/common/async.js';
7
import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js';
8
import { IObservable, observableValue, transaction, IObservableWithChange } from '../../../../../base/common/observable.js';
9
import { setTimeout0 } from '../../../../../base/common/platform.js';
10
import { ILogService } from '../../../../../platform/log/common/log.js';
11
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
12
import { TextLength } from '../../../core/text/textLength.js';
13
import { IModelContentChangedEvent } from '../../../textModelEvents.js';
14
import { IModelContentChange } from '../../mirrorTextModel.js';
15
import { TextModel } from '../../textModel.js';
16
import { gotoParent, getClosestPreviousNodes, nextSiblingOrParentSibling, gotoNthChild } from './cursorUtils.js';
17
import { Range } from '../../../core/range.js';
18
19
export class TreeSitterTree extends Disposable {
20
21
private readonly _tree = observableValue<TreeSitter.Tree | undefined, TreeParseUpdateEvent>(this, undefined);
22
public readonly tree: IObservableWithChange<TreeSitter.Tree | undefined, TreeParseUpdateEvent> = this._tree;
23
24
private readonly _treeLastParsedVersion = observableValue(this, -1);
25
public readonly treeLastParsedVersion: IObservable<number> = this._treeLastParsedVersion;
26
27
private _lastFullyParsed: TreeSitter.Tree | undefined;
28
private _lastFullyParsedWithEdits: TreeSitter.Tree | undefined;
29
30
private _onDidChangeContentQueue: TaskQueue = new TaskQueue();
31
32
constructor(
33
public readonly languageId: string,
34
private _ranges: TreeSitter.Range[] | undefined,
35
// readonly treeSitterLanguage: Language,
36
/** Must have the language set! */
37
private readonly _parser: TreeSitter.Parser,
38
private readonly _parserClass: typeof TreeSitter.Parser,
39
// private readonly _injectionQuery: TreeSitter.Query,
40
public readonly textModel: TextModel,
41
@ILogService private readonly _logService: ILogService,
42
@ITelemetryService private readonly _telemetryService: ITelemetryService
43
) {
44
super();
45
46
this._tree = observableValue(this, undefined);
47
this.tree = this._tree;
48
49
this._register(toDisposable(() => {
50
this._tree.get()?.delete();
51
this._lastFullyParsed?.delete();
52
this._lastFullyParsedWithEdits?.delete();
53
this._parser.delete();
54
}));
55
this.handleContentChange(undefined, this._ranges);
56
}
57
58
public handleContentChange(e: IModelContentChangedEvent | undefined, ranges?: TreeSitter.Range[]): void {
59
const version = this.textModel.getVersionId();
60
let newRanges: TreeSitter.Range[] = [];
61
if (ranges) {
62
newRanges = this._setRanges(ranges);
63
}
64
if (e) {
65
this._applyEdits(e.changes);
66
}
67
68
this._onDidChangeContentQueue.clearPending();
69
this._onDidChangeContentQueue.schedule(async () => {
70
if (this._store.isDisposed) {
71
// No need to continue the queue if we are disposed
72
return;
73
}
74
75
const oldTree = this._lastFullyParsed;
76
let changedNodes: TreeSitter.Range[] | undefined;
77
if (this._lastFullyParsedWithEdits && this._lastFullyParsed) {
78
changedNodes = this._findChangedNodes(this._lastFullyParsedWithEdits, this._lastFullyParsed);
79
}
80
81
const completed = await this._parseAndUpdateTree(version);
82
if (completed) {
83
let ranges: RangeChange[] | undefined;
84
if (!changedNodes) {
85
if (this._ranges) {
86
ranges = this._ranges.map(r => ({ newRange: new Range(r.startPosition.row + 1, r.startPosition.column + 1, r.endPosition.row + 1, r.endPosition.column + 1), oldRangeLength: r.endIndex - r.startIndex, newRangeStartOffset: r.startIndex, newRangeEndOffset: r.endIndex }));
87
}
88
} else if (oldTree && changedNodes) {
89
ranges = this._findTreeChanges(completed, changedNodes, newRanges);
90
}
91
if (!ranges) {
92
ranges = [{ newRange: this.textModel.getFullModelRange(), newRangeStartOffset: 0, newRangeEndOffset: this.textModel.getValueLength() }];
93
}
94
95
const previousTree = this._tree.get();
96
transaction(tx => {
97
this._tree.set(completed, tx, { ranges, versionId: version });
98
this._treeLastParsedVersion.set(version, tx);
99
});
100
previousTree?.delete();
101
}
102
});
103
}
104
105
get ranges(): TreeSitter.Range[] | undefined {
106
return this._ranges;
107
}
108
109
public getInjectionTrees(startIndex: number, languageId: string): TreeSitterTree | undefined {
110
// TODO
111
return undefined;
112
}
113
114
private _applyEdits(changes: IModelContentChange[]) {
115
for (const change of changes) {
116
const originalTextLength = TextLength.ofRange(Range.lift(change.range));
117
const newTextLength = TextLength.ofText(change.text);
118
const summedTextLengths = change.text.length === 0 ? newTextLength : originalTextLength.add(newTextLength);
119
const edit = {
120
startIndex: change.rangeOffset,
121
oldEndIndex: change.rangeOffset + change.rangeLength,
122
newEndIndex: change.rangeOffset + change.text.length,
123
startPosition: { row: change.range.startLineNumber - 1, column: change.range.startColumn - 1 },
124
oldEndPosition: { row: change.range.endLineNumber - 1, column: change.range.endColumn - 1 },
125
newEndPosition: { row: change.range.startLineNumber + summedTextLengths.lineCount - 1, column: summedTextLengths.lineCount ? summedTextLengths.columnCount : (change.range.endColumn + summedTextLengths.columnCount) }
126
};
127
this._tree.get()?.edit(edit);
128
this._lastFullyParsedWithEdits?.edit(edit);
129
}
130
}
131
132
private _findChangedNodes(newTree: TreeSitter.Tree, oldTree: TreeSitter.Tree): TreeSitter.Range[] | undefined {
133
if ((this._ranges && this._ranges.every(range => range.startPosition.row !== newTree.rootNode.startPosition.row)) || newTree.rootNode.startPosition.row !== 0) {
134
return [];
135
}
136
const newCursor = newTree.walk();
137
const oldCursor = oldTree.walk();
138
139
const nodes: TreeSitter.Range[] = [];
140
let next = true;
141
142
do {
143
if (newCursor.currentNode.hasChanges) {
144
// Check if only one of the children has changes.
145
// If it's only one, then we go to that child.
146
// If it's more then, we need to go to each child
147
// If it's none, then we've found one of our ranges
148
const newChildren = newCursor.currentNode.children;
149
const indexChangedChildren: number[] = [];
150
const changedChildren = newChildren.filter((c, index) => {
151
if (c?.hasChanges || (oldCursor.currentNode.children.length <= index)) {
152
indexChangedChildren.push(index);
153
return true;
154
}
155
return false;
156
});
157
// If we have changes and we *had* an error, the whole node should be refreshed.
158
if ((changedChildren.length === 0) || (newCursor.currentNode.hasError !== oldCursor.currentNode.hasError)) {
159
// walk up again until we get to the first one that's named as unnamed nodes can be too granular
160
while (newCursor.currentNode.parent && next && !newCursor.currentNode.isNamed) {
161
next = gotoParent(newCursor, oldCursor);
162
}
163
// Use the end position of the previous node and the start position of the current node
164
const newNode = newCursor.currentNode;
165
const closestPreviousNode = getClosestPreviousNodes(newCursor, newTree) ?? newNode;
166
nodes.push({
167
startIndex: closestPreviousNode.startIndex,
168
endIndex: newNode.endIndex,
169
startPosition: closestPreviousNode.startPosition,
170
endPosition: newNode.endPosition
171
});
172
next = nextSiblingOrParentSibling(newCursor, oldCursor);
173
} else if (changedChildren.length >= 1) {
174
next = gotoNthChild(newCursor, oldCursor, indexChangedChildren[0]);
175
}
176
} else {
177
next = nextSiblingOrParentSibling(newCursor, oldCursor);
178
}
179
} while (next);
180
181
newCursor.delete();
182
oldCursor.delete();
183
return nodes;
184
}
185
186
private _findTreeChanges(newTree: TreeSitter.Tree, changedNodes: TreeSitter.Range[], newRanges: TreeSitter.Range[]): RangeChange[] {
187
let newRangeIndex = 0;
188
const mergedChanges: RangeChange[] = [];
189
190
// Find the parent in the new tree of the changed node
191
for (let nodeIndex = 0; nodeIndex < changedNodes.length; nodeIndex++) {
192
const node = changedNodes[nodeIndex];
193
194
if (mergedChanges.length > 0) {
195
if ((node.startIndex >= mergedChanges[mergedChanges.length - 1].newRangeStartOffset) && (node.endIndex <= mergedChanges[mergedChanges.length - 1].newRangeEndOffset)) {
196
// This node is within the previous range, skip it
197
continue;
198
}
199
}
200
201
const cursor = newTree.walk();
202
const cursorContainersNode = () => cursor.startIndex < node.startIndex && cursor.endIndex > node.endIndex;
203
204
while (cursorContainersNode()) {
205
// See if we can go to a child
206
let child = cursor.gotoFirstChild();
207
let foundChild = false;
208
while (child) {
209
if (cursorContainersNode() && cursor.currentNode.isNamed) {
210
foundChild = true;
211
break;
212
} else {
213
child = cursor.gotoNextSibling();
214
}
215
}
216
if (!foundChild) {
217
cursor.gotoParent();
218
break;
219
}
220
if (cursor.currentNode.childCount === 0) {
221
break;
222
}
223
}
224
225
const startPosition = cursor.currentNode.startPosition;
226
const endPosition = cursor.currentNode.endPosition;
227
const startIndex = cursor.currentNode.startIndex;
228
const endIndex = cursor.currentNode.endIndex;
229
230
const newChange = { newRange: new Range(startPosition.row + 1, startPosition.column + 1, endPosition.row + 1, endPosition.column + 1), newRangeStartOffset: startIndex, newRangeEndOffset: endIndex };
231
if ((newRangeIndex < newRanges.length) && rangesIntersect(newRanges[newRangeIndex], { startIndex, endIndex, startPosition, endPosition })) {
232
// combine the new change with the range
233
if (newRanges[newRangeIndex].startIndex < newChange.newRangeStartOffset) {
234
newChange.newRange = newChange.newRange.setStartPosition(newRanges[newRangeIndex].startPosition.row + 1, newRanges[newRangeIndex].startPosition.column + 1);
235
newChange.newRangeStartOffset = newRanges[newRangeIndex].startIndex;
236
}
237
if (newRanges[newRangeIndex].endIndex > newChange.newRangeEndOffset) {
238
newChange.newRange = newChange.newRange.setEndPosition(newRanges[newRangeIndex].endPosition.row + 1, newRanges[newRangeIndex].endPosition.column + 1);
239
newChange.newRangeEndOffset = newRanges[newRangeIndex].endIndex;
240
}
241
newRangeIndex++;
242
} else if (newRangeIndex < newRanges.length && newRanges[newRangeIndex].endIndex < newChange.newRangeStartOffset) {
243
// add the full range to the merged changes
244
mergedChanges.push({
245
newRange: new Range(newRanges[newRangeIndex].startPosition.row + 1, newRanges[newRangeIndex].startPosition.column + 1, newRanges[newRangeIndex].endPosition.row + 1, newRanges[newRangeIndex].endPosition.column + 1),
246
newRangeStartOffset: newRanges[newRangeIndex].startIndex,
247
newRangeEndOffset: newRanges[newRangeIndex].endIndex
248
});
249
}
250
251
if ((mergedChanges.length > 0) && (mergedChanges[mergedChanges.length - 1].newRangeEndOffset >= newChange.newRangeStartOffset)) {
252
// Merge the changes
253
mergedChanges[mergedChanges.length - 1].newRange = Range.fromPositions(mergedChanges[mergedChanges.length - 1].newRange.getStartPosition(), newChange.newRange.getEndPosition());
254
mergedChanges[mergedChanges.length - 1].newRangeEndOffset = newChange.newRangeEndOffset;
255
} else {
256
mergedChanges.push(newChange);
257
}
258
}
259
return this._constrainRanges(mergedChanges);
260
}
261
262
private _constrainRanges(changes: RangeChange[]): RangeChange[] {
263
if (!this._ranges) {
264
return changes;
265
}
266
267
const constrainedChanges: RangeChange[] = [];
268
let changesIndex = 0;
269
let rangesIndex = 0;
270
while (changesIndex < changes.length && rangesIndex < this._ranges.length) {
271
const change = changes[changesIndex];
272
const range = this._ranges[rangesIndex];
273
if (change.newRangeEndOffset < range.startIndex) {
274
// Change is before the range, move to the next change
275
changesIndex++;
276
} else if (change.newRangeStartOffset > range.endIndex) {
277
// Change is after the range, move to the next range
278
rangesIndex++;
279
} else {
280
// Change is within the range, constrain it
281
const newRangeStartOffset = Math.max(change.newRangeStartOffset, range.startIndex);
282
const newRangeEndOffset = Math.min(change.newRangeEndOffset, range.endIndex);
283
const newRange = change.newRange.intersectRanges(new Range(range.startPosition.row + 1, range.startPosition.column + 1, range.endPosition.row + 1, range.endPosition.column + 1))!;
284
constrainedChanges.push({
285
newRange,
286
newRangeEndOffset,
287
newRangeStartOffset
288
});
289
// Remove the intersected range from the current change
290
if (newRangeEndOffset < change.newRangeEndOffset) {
291
change.newRange = Range.fromPositions(newRange.getEndPosition(), change.newRange.getEndPosition());
292
change.newRangeStartOffset = newRangeEndOffset + 1;
293
} else {
294
// Move to the next change
295
changesIndex++;
296
}
297
}
298
}
299
300
return constrainedChanges;
301
}
302
303
private async _parseAndUpdateTree(version: number): Promise<TreeSitter.Tree | undefined> {
304
const tree = await this._parse();
305
if (tree) {
306
this._lastFullyParsed?.delete();
307
this._lastFullyParsed = tree.copy();
308
this._lastFullyParsedWithEdits?.delete();
309
this._lastFullyParsedWithEdits = tree.copy();
310
311
return tree;
312
} else if (!this._tree.get()) {
313
// No tree means this is the initial parse and there were edits
314
// parse function doesn't handle this well and we can end up with an incorrect tree, so we reset
315
this._parser.reset();
316
}
317
return undefined;
318
}
319
320
private _parse(): Promise<TreeSitter.Tree | undefined> {
321
let parseType: TelemetryParseType = TelemetryParseType.Full;
322
if (this._tree.get()) {
323
parseType = TelemetryParseType.Incremental;
324
}
325
return this._parseAndYield(parseType);
326
}
327
328
private async _parseAndYield(parseType: TelemetryParseType): Promise<TreeSitter.Tree | undefined> {
329
let time: number = 0;
330
let passes: number = 0;
331
const inProgressVersion = this.textModel.getVersionId();
332
let newTree: TreeSitter.Tree | null | undefined;
333
334
const progressCallback = newTimeOutProgressCallback();
335
336
do {
337
const timer = performance.now();
338
339
newTree = this._parser.parse((index: number, position?: TreeSitter.Point) => this._parseCallback(index), this._tree.get(), { progressCallback, includedRanges: this._ranges });
340
341
time += performance.now() - timer;
342
passes++;
343
344
// So long as this isn't the initial parse, even if the model changes and edits are applied, the tree parsing will continue correctly after the await.
345
await new Promise<void>(resolve => setTimeout0(resolve));
346
347
} while (!this._store.isDisposed && !newTree && inProgressVersion === this.textModel.getVersionId());
348
this._sendParseTimeTelemetry(parseType, time, passes);
349
return (newTree && (inProgressVersion === this.textModel.getVersionId())) ? newTree : undefined;
350
}
351
352
private _parseCallback(index: number): string | undefined {
353
try {
354
return this.textModel.getTextBuffer().getNearestChunk(index);
355
} catch (e) {
356
this._logService.debug('Error getting chunk for tree-sitter parsing', e);
357
}
358
return undefined;
359
}
360
361
private _setRanges(newRanges: TreeSitter.Range[]): TreeSitter.Range[] {
362
const unKnownRanges: TreeSitter.Range[] = [];
363
// If we have existing ranges, find the parts of the new ranges that are not included in the existing ones
364
if (this._ranges) {
365
for (const newRange of newRanges) {
366
let isFullyIncluded = false;
367
368
for (let i = 0; i < this._ranges.length; i++) {
369
const existingRange = this._ranges[i];
370
371
if (rangesEqual(existingRange, newRange) || rangesIntersect(existingRange, newRange)) {
372
isFullyIncluded = true;
373
break;
374
}
375
}
376
377
if (!isFullyIncluded) {
378
unKnownRanges.push(newRange);
379
}
380
}
381
} else {
382
// No existing ranges, all new ranges are unknown
383
unKnownRanges.push(...newRanges);
384
}
385
386
this._ranges = newRanges;
387
return unKnownRanges;
388
}
389
390
private _sendParseTimeTelemetry(parseType: TelemetryParseType, time: number, passes: number): void {
391
this._logService.debug(`Tree parsing (${parseType}) took ${time} ms and ${passes} passes.`);
392
type ParseTimeClassification = {
393
owner: 'alexr00';
394
comment: 'Used to understand how long it takes to parse a tree-sitter tree';
395
languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The programming language ID.' };
396
time: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The ms it took to parse' };
397
passes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of passes it took to parse' };
398
};
399
if (parseType === TelemetryParseType.Full) {
400
this._telemetryService.publicLog2<{ languageId: string; time: number; passes: number }, ParseTimeClassification>(`treeSitter.fullParse`, { languageId: this.languageId, time, passes });
401
} else {
402
this._telemetryService.publicLog2<{ languageId: string; time: number; passes: number }, ParseTimeClassification>(`treeSitter.incrementalParse`, { languageId: this.languageId, time, passes });
403
}
404
}
405
406
public createParsedTreeSync(src: string): TreeSitter.Tree | undefined {
407
const parser = new this._parserClass();
408
parser.setLanguage(this._parser.language!);
409
const tree = parser.parse(src);
410
parser.delete();
411
return tree ?? undefined;
412
}
413
}
414
415
const enum TelemetryParseType {
416
Full = 'fullParse',
417
Incremental = 'incrementalParse'
418
}
419
420
export interface TreeParseUpdateEvent {
421
ranges: RangeChange[];
422
versionId: number;
423
}
424
425
export interface RangeWithOffsets {
426
range: Range;
427
startOffset: number;
428
endOffset: number;
429
}
430
431
export interface RangeChange {
432
newRange: Range;
433
newRangeStartOffset: number;
434
newRangeEndOffset: number;
435
}
436
437
function newTimeOutProgressCallback(): (state: TreeSitter.ParseState) => void {
438
let lastYieldTime: number = performance.now();
439
return function parseProgressCallback(_state: TreeSitter.ParseState) {
440
const now = performance.now();
441
if (now - lastYieldTime > 50) {
442
lastYieldTime = now;
443
return true;
444
}
445
return false;
446
};
447
}
448
export function rangesEqual(a: TreeSitter.Range, b: TreeSitter.Range) {
449
return (a.startPosition.row === b.startPosition.row)
450
&& (a.startPosition.column === b.startPosition.column)
451
&& (a.endPosition.row === b.endPosition.row)
452
&& (a.endPosition.column === b.endPosition.column)
453
&& (a.startIndex === b.startIndex)
454
&& (a.endIndex === b.endIndex);
455
}
456
457
export function rangesIntersect(a: TreeSitter.Range, b: TreeSitter.Range) {
458
return (a.startIndex <= b.startIndex && a.endIndex >= b.startIndex) ||
459
(b.startIndex <= a.startIndex && b.endIndex >= a.startIndex);
460
}
461
462