Skip to content

Commit

Permalink
Add incremental parsing support (#25)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
kerristrasz and PaddiM8 authored Jun 3, 2024
1 parent 5cb52b8 commit 7b3e890
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 27 deletions.
10 changes: 5 additions & 5 deletions server/src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ import * as fsSync from 'node:fs';
import * as url from 'node:url';

import { ModelicaDocument, ModelicaLibrary, ModelicaProject } from './project';
import { uriToPath } from "./util";
import { getAllDeclarationsInTree } from './util/declarations';
import { logger } from './util/logger';
import { uriToPath } from './util';

export default class Analyzer {
#project: ModelicaProject;
Expand Down Expand Up @@ -98,8 +98,8 @@ export default class Analyzer {
* @param uri uri to document to add
* @throws if the document does not belong to a library
*/
public addDocument(uri: LSP.DocumentUri): void {
this.#project.addDocument(uriToPath(uri));
public async addDocument(uri: LSP.DocumentUri): Promise<void> {
await this.#project.addDocument(uriToPath(uri));
}

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

/**
Expand Down
66 changes: 57 additions & 9 deletions server/src/project/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,24 @@ import Parser from 'web-tree-sitter';
import * as fs from 'node:fs/promises';
import * as TreeSitterUtil from '../util/tree-sitter';

import { pathToUri, uriToPath } from '../util';
import { logger } from '../util/logger';
import { ModelicaLibrary } from './library';
import { ModelicaProject } from './project';
import { positionToPoint } from '../util/tree-sitter';
import { pathToUri, uriToPath } from '../util';

export class ModelicaDocument implements TextDocument {
readonly #project: ModelicaProject;
readonly #library: ModelicaLibrary | null;
readonly #document: TextDocument;
#tree: Parser.Tree;

public constructor(project: ModelicaProject, library: ModelicaLibrary | null, document: TextDocument, tree: Parser.Tree) {
public constructor(
project: ModelicaProject,
library: ModelicaLibrary | null,
document: TextDocument,
tree: Parser.Tree,
) {
this.#project = project;
this.#library = library;
this.#document = document;
Expand Down Expand Up @@ -90,12 +96,54 @@ export class ModelicaDocument implements TextDocument {

/**
* Updates a document.
*
* @param text the modification
* @param range the range to update, or `undefined` to replace the whole file
*/
public async update(text: string): Promise<void> {
TextDocument.update(this.#document, [{ text }], this.version + 1);
this.#tree = this.project.parser.parse(text);
return;
public async update(text: string, range?: LSP.Range): Promise<void> {
if (range === undefined) {
TextDocument.update(this.#document, [{ text }], this.version + 1);
this.#tree = this.project.parser.parse(text);
return;
}

const startIndex = this.offsetAt(range.start);
const startPosition = positionToPoint(range.start);
const oldEndIndex = this.offsetAt(range.end);
const oldEndPosition = positionToPoint(range.end);
const newEndIndex = startIndex + text.length;

TextDocument.update(this.#document, [{ text, range }], this.version + 1);
const newEndPosition = positionToPoint(this.positionAt(newEndIndex));

this.#tree.edit({
startIndex,
startPosition,
oldEndIndex,
oldEndPosition,
newEndIndex,
newEndPosition,
});

this.#tree = this.project.parser.parse((index: number, position?: Parser.Point) => {
if (position) {
return this.getText({
start: {
character: position.column,
line: position.row,
},
end: {
character: position.column + 1,
line: position.row,
},
});
} else {
return this.getText({
start: this.positionAt(index),
end: this.positionAt(index + 1),
});
}
}, this.#tree);
}

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

// TODO: Use a helper function from TreeSitterUtil
const identifiers: string[] = [];
TreeSitterUtil.forEach(withinClause, (node) => {
if (node.type === "name") {
if (node.type === 'name') {
return true;
}

if (node.type === "IDENT") {
if (node.type === 'IDENT') {
identifiers.push(node.text);
}

Expand Down
19 changes: 9 additions & 10 deletions server/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@
*
*/

import Parser from "web-tree-sitter";
import * as LSP from "vscode-languageserver";
import url from "node:url";
import path from "node:path";
import Parser from 'web-tree-sitter';
import * as LSP from 'vscode-languageserver';
import url from 'node:url';
import path from 'node:path';

import { ModelicaLibrary } from "./library";
import { ModelicaLibrary } from './library';
import { ModelicaDocument } from './document';
import { logger } from "../util/logger";
import { logger } from '../util/logger';

/** Options for {@link ModelicaProject.getDocument} */
export interface GetDocumentOptions {
Expand Down Expand Up @@ -117,7 +117,7 @@ export class ModelicaProject {

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

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

const doc = await this.getDocument(documentPath, { load: true });
if (doc) {
doc.update(text);
doc.update(text, range);
logger.debug(`Updated document '${documentPath}'`);
return true;
} else {
Expand Down Expand Up @@ -191,5 +191,4 @@ export class ModelicaProject {
public get parser(): Parser {
return this.#parser;
}

}
40 changes: 40 additions & 0 deletions server/src/project/test/document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,46 @@ describe('ModelicaDocument', () => {
assert.equal(document.getText().trim(), UPDATED_TEST_PACKAGE_CONTENT.trim());
});

it('can update incrementally', () => {
const textDocument = createTextDocument('.', TEST_PACKAGE_CONTENT);
const tree = project.parser.parse(TEST_PACKAGE_CONTENT);
const document = new ModelicaDocument(project, library, textDocument, tree);
document.update(
'1.0.1',
{
start: {
line: 1,
character: 22,
},
end: {
line: 1,
character: 27,
},
}
);

assert.equal(document.getText().trim(), UPDATED_TEST_PACKAGE_CONTENT.trim());

document.update(
'\n model A\n end A;',
{
start: {
line: 1,
character: 30,
},
end: {
line: 1,
character: 30,
},
}
);

const model = document.tree.rootNode.descendantsOfType("class_definition")[1];
assert.equal(model.type, "class_definition");
assert.equal(model.descendantsOfType("IDENT")[0].text, "A");
assert.equal(document.tree.rootNode.descendantsOfType("annotation_clause").length, 1);
});

it('a file with no `within` clause has the correct package path', () => {
const textDocument = createTextDocument('./package.mo', TEST_PACKAGE_CONTENT);
const tree = project.parser.parse(TEST_PACKAGE_CONTENT);
Expand Down
7 changes: 4 additions & 3 deletions server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class ModelicaServer {
documentSymbolProvider: true,
colorProvider: false,
semanticTokensProvider: undefined,
textDocumentSync: LSP.TextDocumentSyncKind.Full,
textDocumentSync: LSP.TextDocumentSyncKind.Incremental,
workspace: {
workspaceFolders: {
supported: true,
Expand Down Expand Up @@ -150,7 +150,8 @@ export class ModelicaServer {
private async onDidChangeTextDocument(params: LSP.DidChangeTextDocumentParams): Promise<void> {
logger.debug('onDidChangeTextDocument');
for (const change of params.contentChanges) {
await this.#analyzer.updateDocument(params.textDocument.uri, change.text);
const range = 'range' in change ? change.range : undefined;
await this.#analyzer.updateDocument(params.textDocument.uri, change.text, range);
}
}

Expand All @@ -160,7 +161,7 @@ export class ModelicaServer {
for (const change of params.changes) {
switch (change.type) {
case LSP.FileChangeType.Created:
this.#analyzer.addDocument(change.uri);
await this.#analyzer.addDocument(change.uri);
break;
case LSP.FileChangeType.Changed: {
// TODO: incremental?
Expand Down
9 changes: 9 additions & 0 deletions server/src/util/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
* -----------------------------------------------------------------------------
*/

import Parser from 'web-tree-sitter';
import * as LSP from 'vscode-languageserver/node';
import { SyntaxNode } from 'web-tree-sitter';

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

return classPrefixNode.text;
}

export function positionToPoint(position: LSP.Position): Parser.Point {
return { row: position.line, column: position.character };
}

export function pointToPosition(point: Parser.Point): LSP.Position {
return { line: point.row, character: point.column };
}

0 comments on commit 7b3e890

Please sign in to comment.