Skip to content

Commit 7b3e890

Browse files
ehedborPaddiM8
andauthored
Add incremental parsing support (#25)
* Add incremental parsing If LS already has a tree-sitter tree stored for document update tree with document changes instead of re-parsing the whole document. - Add test for incremental parsing - Test incremental insertion of a new model --------- Co-authored-by: PaddiM8 <hi@bakk.dev>
1 parent 5cb52b8 commit 7b3e890

6 files changed

Lines changed: 124 additions & 27 deletions

File tree

server/src/analyzer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ import * as fsSync from 'node:fs';
4747
import * as url from 'node:url';
4848

4949
import { ModelicaDocument, ModelicaLibrary, ModelicaProject } from './project';
50-
import { uriToPath } from "./util";
5150
import { getAllDeclarationsInTree } from './util/declarations';
5251
import { logger } from './util/logger';
52+
import { uriToPath } from './util';
5353

5454
export default class Analyzer {
5555
#project: ModelicaProject;
@@ -98,8 +98,8 @@ export default class Analyzer {
9898
* @param uri uri to document to add
9999
* @throws if the document does not belong to a library
100100
*/
101-
public addDocument(uri: LSP.DocumentUri): void {
102-
this.#project.addDocument(uriToPath(uri));
101+
public async addDocument(uri: LSP.DocumentUri): Promise<void> {
102+
await this.#project.addDocument(uriToPath(uri));
103103
}
104104

105105
/**
@@ -110,8 +110,8 @@ export default class Analyzer {
110110
* @param text the modification
111111
* @param range range to update, or `undefined` to replace the whole file
112112
*/
113-
public async updateDocument(uri: LSP.DocumentUri, text: string): Promise<void> {
114-
await this.#project.updateDocument(uriToPath(uri), text);
113+
public async updateDocument(uri: LSP.DocumentUri, text: string, range?: LSP.Range): Promise<void> {
114+
await this.#project.updateDocument(uriToPath(uri), text, range);
115115
}
116116

117117
/**

server/src/project/document.ts

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,24 @@ import Parser from 'web-tree-sitter';
3939
import * as fs from 'node:fs/promises';
4040
import * as TreeSitterUtil from '../util/tree-sitter';
4141

42-
import { pathToUri, uriToPath } from '../util';
4342
import { logger } from '../util/logger';
4443
import { ModelicaLibrary } from './library';
4544
import { ModelicaProject } from './project';
45+
import { positionToPoint } from '../util/tree-sitter';
46+
import { pathToUri, uriToPath } from '../util';
4647

4748
export class ModelicaDocument implements TextDocument {
4849
readonly #project: ModelicaProject;
4950
readonly #library: ModelicaLibrary | null;
5051
readonly #document: TextDocument;
5152
#tree: Parser.Tree;
5253

53-
public constructor(project: ModelicaProject, library: ModelicaLibrary | null, document: TextDocument, tree: Parser.Tree) {
54+
public constructor(
55+
project: ModelicaProject,
56+
library: ModelicaLibrary | null,
57+
document: TextDocument,
58+
tree: Parser.Tree,
59+
) {
5460
this.#project = project;
5561
this.#library = library;
5662
this.#document = document;
@@ -90,12 +96,54 @@ export class ModelicaDocument implements TextDocument {
9096

9197
/**
9298
* Updates a document.
99+
*
93100
* @param text the modification
101+
* @param range the range to update, or `undefined` to replace the whole file
94102
*/
95-
public async update(text: string): Promise<void> {
96-
TextDocument.update(this.#document, [{ text }], this.version + 1);
97-
this.#tree = this.project.parser.parse(text);
98-
return;
103+
public async update(text: string, range?: LSP.Range): Promise<void> {
104+
if (range === undefined) {
105+
TextDocument.update(this.#document, [{ text }], this.version + 1);
106+
this.#tree = this.project.parser.parse(text);
107+
return;
108+
}
109+
110+
const startIndex = this.offsetAt(range.start);
111+
const startPosition = positionToPoint(range.start);
112+
const oldEndIndex = this.offsetAt(range.end);
113+
const oldEndPosition = positionToPoint(range.end);
114+
const newEndIndex = startIndex + text.length;
115+
116+
TextDocument.update(this.#document, [{ text, range }], this.version + 1);
117+
const newEndPosition = positionToPoint(this.positionAt(newEndIndex));
118+
119+
this.#tree.edit({
120+
startIndex,
121+
startPosition,
122+
oldEndIndex,
123+
oldEndPosition,
124+
newEndIndex,
125+
newEndPosition,
126+
});
127+
128+
this.#tree = this.project.parser.parse((index: number, position?: Parser.Point) => {
129+
if (position) {
130+
return this.getText({
131+
start: {
132+
character: position.column,
133+
line: position.row,
134+
},
135+
end: {
136+
character: position.column + 1,
137+
line: position.row,
138+
},
139+
});
140+
} else {
141+
return this.getText({
142+
start: this.positionAt(index),
143+
end: this.positionAt(index + 1),
144+
});
145+
}
146+
}, this.#tree);
99147
}
100148

101149
public getText(range?: LSP.Range | undefined): string {
@@ -138,19 +186,19 @@ export class ModelicaDocument implements TextDocument {
138186
public get within(): string[] {
139187
const withinClause = this.#tree.rootNode.children
140188
.find((node) => node.type === 'within_clause')
141-
?.childForFieldName("name");
189+
?.childForFieldName('name');
142190
if (!withinClause) {
143191
return [];
144192
}
145193

146194
// TODO: Use a helper function from TreeSitterUtil
147195
const identifiers: string[] = [];
148196
TreeSitterUtil.forEach(withinClause, (node) => {
149-
if (node.type === "name") {
197+
if (node.type === 'name') {
150198
return true;
151199
}
152200

153-
if (node.type === "IDENT") {
201+
if (node.type === 'IDENT') {
154202
identifiers.push(node.text);
155203
}
156204

server/src/project/project.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@
3333
*
3434
*/
3535

36-
import Parser from "web-tree-sitter";
37-
import * as LSP from "vscode-languageserver";
38-
import url from "node:url";
39-
import path from "node:path";
36+
import Parser from 'web-tree-sitter';
37+
import * as LSP from 'vscode-languageserver';
38+
import url from 'node:url';
39+
import path from 'node:path';
4040

41-
import { ModelicaLibrary } from "./library";
41+
import { ModelicaLibrary } from './library';
4242
import { ModelicaDocument } from './document';
43-
import { logger } from "../util/logger";
43+
import { logger } from '../util/logger';
4444

4545
/** Options for {@link ModelicaProject.getDocument} */
4646
export interface GetDocumentOptions {
@@ -117,7 +117,7 @@ export class ModelicaProject {
117117

118118
for (const library of this.#libraries) {
119119
const relative = path.relative(library.path, documentPath);
120-
const isSubdirectory = relative && !relative.startsWith("..") && !path.isAbsolute(relative);
120+
const isSubdirectory = relative && !relative.startsWith('..') && !path.isAbsolute(relative);
121121

122122
// Assume that files can't be inside multiple libraries at the same time
123123
if (!isSubdirectory) {
@@ -155,12 +155,12 @@ export class ModelicaProject {
155155
* @param text the modification
156156
* @returns if the document was updated
157157
*/
158-
public async updateDocument(documentPath: string, text: string): Promise<boolean> {
158+
public async updateDocument(documentPath: string, text: string, range?: LSP.Range): Promise<boolean> {
159159
logger.debug(`Updating document at '${documentPath}'...`);
160160

161161
const doc = await this.getDocument(documentPath, { load: true });
162162
if (doc) {
163-
doc.update(text);
163+
doc.update(text, range);
164164
logger.debug(`Updated document '${documentPath}'`);
165165
return true;
166166
} else {
@@ -191,5 +191,4 @@ export class ModelicaProject {
191191
public get parser(): Parser {
192192
return this.#parser;
193193
}
194-
195194
}

server/src/project/test/document.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,46 @@ describe('ModelicaDocument', () => {
8282
assert.equal(document.getText().trim(), UPDATED_TEST_PACKAGE_CONTENT.trim());
8383
});
8484

85+
it('can update incrementally', () => {
86+
const textDocument = createTextDocument('.', TEST_PACKAGE_CONTENT);
87+
const tree = project.parser.parse(TEST_PACKAGE_CONTENT);
88+
const document = new ModelicaDocument(project, library, textDocument, tree);
89+
document.update(
90+
'1.0.1',
91+
{
92+
start: {
93+
line: 1,
94+
character: 22,
95+
},
96+
end: {
97+
line: 1,
98+
character: 27,
99+
},
100+
}
101+
);
102+
103+
assert.equal(document.getText().trim(), UPDATED_TEST_PACKAGE_CONTENT.trim());
104+
105+
document.update(
106+
'\n model A\n end A;',
107+
{
108+
start: {
109+
line: 1,
110+
character: 30,
111+
},
112+
end: {
113+
line: 1,
114+
character: 30,
115+
},
116+
}
117+
);
118+
119+
const model = document.tree.rootNode.descendantsOfType("class_definition")[1];
120+
assert.equal(model.type, "class_definition");
121+
assert.equal(model.descendantsOfType("IDENT")[0].text, "A");
122+
assert.equal(document.tree.rootNode.descendantsOfType("annotation_clause").length, 1);
123+
});
124+
85125
it('a file with no `within` clause has the correct package path', () => {
86126
const textDocument = createTextDocument('./package.mo', TEST_PACKAGE_CONTENT);
87127
const tree = project.parser.parse(TEST_PACKAGE_CONTENT);

server/src/server.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export class ModelicaServer {
102102
documentSymbolProvider: true,
103103
colorProvider: false,
104104
semanticTokensProvider: undefined,
105-
textDocumentSync: LSP.TextDocumentSyncKind.Full,
105+
textDocumentSync: LSP.TextDocumentSyncKind.Incremental,
106106
workspace: {
107107
workspaceFolders: {
108108
supported: true,
@@ -150,7 +150,8 @@ export class ModelicaServer {
150150
private async onDidChangeTextDocument(params: LSP.DidChangeTextDocumentParams): Promise<void> {
151151
logger.debug('onDidChangeTextDocument');
152152
for (const change of params.contentChanges) {
153-
await this.#analyzer.updateDocument(params.textDocument.uri, change.text);
153+
const range = 'range' in change ? change.range : undefined;
154+
await this.#analyzer.updateDocument(params.textDocument.uri, change.text, range);
154155
}
155156
}
156157

@@ -160,7 +161,7 @@ export class ModelicaServer {
160161
for (const change of params.changes) {
161162
switch (change.type) {
162163
case LSP.FileChangeType.Created:
163-
this.#analyzer.addDocument(change.uri);
164+
await this.#analyzer.addDocument(change.uri);
164165
break;
165166
case LSP.FileChangeType.Changed: {
166167
// TODO: incremental?

server/src/util/tree-sitter.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
* -----------------------------------------------------------------------------
4040
*/
4141

42+
import Parser from 'web-tree-sitter';
4243
import * as LSP from 'vscode-languageserver/node';
4344
import { SyntaxNode } from 'web-tree-sitter';
4445

@@ -169,3 +170,11 @@ export function getClassPrefixes(node: SyntaxNode): string | null {
169170

170171
return classPrefixNode.text;
171172
}
173+
174+
export function positionToPoint(position: LSP.Position): Parser.Point {
175+
return { row: position.line, column: position.character };
176+
}
177+
178+
export function pointToPosition(point: Parser.Point): LSP.Position {
179+
return { line: point.row, character: point.column };
180+
}

0 commit comments

Comments
 (0)