Skip to content

Commit

Permalink
Add multi-file support
Browse files Browse the repository at this point in the history
Co-authored-by: Evan Hedbor <[email protected]>
  • Loading branch information
PaddiM8 and kerristrasz committed May 22, 2024
1 parent 08c0db8 commit d1eaec2
Show file tree
Hide file tree
Showing 9 changed files with 654 additions and 469 deletions.
432 changes: 21 additions & 411 deletions server/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"url": "https://github.com/OpenModelica/modelica-language-server"
},
"dependencies": {
"tree-sitter": "^0.20.6",
"tree-sitter": "^0.21.1",
"vscode-languageserver": "^9.0.1",
"vscode-languageserver-textdocument": "^1.0.11",
"web-tree-sitter": "^0.20.8"
Expand Down
98 changes: 71 additions & 27 deletions server/src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,47 +41,88 @@

import * as LSP from 'vscode-languageserver/node';
import { TextDocument } from 'vscode-languageserver-textdocument';

import Parser = require('web-tree-sitter');
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import * as fsSync from 'node:fs';
import * as url from 'node:url';

import { ModelicaDocument, ModelicaLibrary, ModelicaProject } from './project';

import { getAllDeclarationsInTree } from './util/declarations';
import { logger } from './util/logger';

type AnalyzedDocument = {
document: TextDocument;
declarations: LSP.SymbolInformation[];
tree: Parser.Tree;
};

export default class Analyzer {
#parser: Parser;
#uriToAnalyzedDocument: Record<string, AnalyzedDocument | undefined> = {};
#project: ModelicaProject;

public constructor(parser: Parser) {
this.#parser = parser;
this.#project = new ModelicaProject(parser);
}

public analyze(document: TextDocument): LSP.Diagnostic[] {
logger.debug('analyze:');
/**
* Adds a library (and all of its documents) to the analyzer.
*
* @param uri uri to the library root
* @param isWorkspace `true` if this is a user workspace/project, `false` if
* this is a library.
*/
public async loadLibrary(uri: LSP.URI, isWorkspace: boolean): Promise<void> {
const isLibrary = (folderPath: string) =>
fsSync.existsSync(path.join(folderPath, 'package.mo'));

const libraryPath = url.fileURLToPath(uri);
if (!isWorkspace || isLibrary(libraryPath)) {
const lib = await ModelicaLibrary.load(this.#project, libraryPath, isWorkspace);
this.#project.addLibrary(lib);
return;
}

const diagnostics: LSP.Diagnostic[] = [];
const fileContent = document.getText();
const uri = document.uri;
// TODO: go deeper... something like `TreeSitterUtil.forEach` but for files
// would be good here
for (const nestedRelative of await fs.readdir(libraryPath)) {
const nested = path.resolve(nestedRelative);
if (!isLibrary(nested)) {
continue;
}

const tree = this.#parser.parse(fileContent);
logger.debug(tree.rootNode.toString());
const library = await ModelicaLibrary.load(this.#project, nested, isWorkspace);
this.#project.addLibrary(library);
}
}

// Get declarations
const declarations = getAllDeclarationsInTree(tree, uri);
/**
* Adds a document to the analyzer.
*
* Note: {@link loadLibrary} already adds all discovered documents to the
* analyzer. It is only necessary to call this method on file creation.
*
* @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(url.fileURLToPath(uri));
}

// Update saved analysis for document uri
this.#uriToAnalyzedDocument[uri] = {
document,
declarations,
tree,
};
/**
* Submits a modification to a document. Ignores documents that have not been
* added with {@link addDocument} or {@link loadLibrary}.
*
* @param uri uri to document to update
* @param text the modification
* @param range range to update, or `undefined` to replace the whole file
*/
public updateDocument(uri: LSP.DocumentUri, text: string): void {
this.#project.updateDocument(url.fileURLToPath(uri), text);
}

return diagnostics;
/**
* Removes a document from the analyzer. Ignores documents that have not been
* added or have already been removed.
*
* @param uri uri to document to remove
*/
public removeDocument(uri: LSP.DocumentUri): void {
this.#project.removeDocument(url.fileURLToPath(uri));
}

/**
Expand All @@ -90,7 +131,10 @@ export default class Analyzer {
* TODO: convert to DocumentSymbol[] which is a hierarchy of symbols found in a given text document.
*/
public getDeclarationsForUri(uri: string): LSP.SymbolInformation[] {
const tree = this.#uriToAnalyzedDocument[uri]?.tree;
// TODO: convert to DocumentSymbol[] which is a hierarchy of symbols found
// in a given text document.
const path = url.fileURLToPath(uri);
const tree = this.#project.getDocument(path)?.tree;

if (!tree?.rootNode) {
return [];
Expand Down
166 changes: 166 additions & 0 deletions server/src/project/document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* This file is part of OpenModelica.
*
* Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC),
* c/o Linköpings universitet, Department of Computer and Information Science,
* SE-58183 Linköping, Sweden.
*
* All rights reserved.
*
* THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR
* THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8.
* ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES
* RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL
* VERSION 3, ACCORDING TO RECIPIENTS CHOICE.
*
* The OpenModelica software and the OSMC (Open Source Modelica Consortium)
* Public License (OSMC-PL) are obtained from OSMC, either from the above
* address, from the URLs:
* http://www.openmodelica.org or
* https://github.com/OpenModelica/ or
* http://www.ida.liu.se/projects/OpenModelica,
* and in the OpenModelica distribution.
*
* GNU AGPL version 3 is obtained from:
* https://www.gnu.org/licenses/licenses.html#GPL
*
* This program is distributed WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH
* IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL.
*
* See the full OSMC Public License conditions for more details.
*
*/

import { TextDocument } from "vscode-languageserver-textdocument";
import * as LSP from "vscode-languageserver/node";
import Parser from "web-tree-sitter";
import * as fs from "node:fs/promises";
import * as url from "node:url";
import * as path from "node:path";

import { logger } from "../util/logger";
import { ModelicaLibrary } from "./library";
import { ModelicaProject } from "./project";

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

public constructor(library: ModelicaLibrary, document: TextDocument, tree: Parser.Tree) {
this.#library = library;
this.#document = document;
this.#tree = tree;
}

/**
* Loads a document.
*
* @param library the containing {@link ModelicaLibrary}
* @param documentPath the path to the document
* @returns the document
*/
public static async load(
library: ModelicaLibrary,
documentPath: string,
): Promise<ModelicaDocument> {
logger.debug(`Loading document at '${documentPath}'...`);

const content = await fs.readFile(documentPath, "utf-8");
// On caching: see issue https://github.com/tree-sitter/tree-sitter/issues/824
// TL;DR: it's faster to re-parse the content than it is to deserialize the cached tree.
const tree = library.project.parser.parse(content);

return new ModelicaDocument(
library,
TextDocument.create(url.fileURLToPath(documentPath), "modelica", 0, content),
tree
);
}

/**
* Updates a document.
* @param text the modification
*/
public async update(text: string): Promise<void> {
TextDocument.update(this.#document, [{ text }], this.version + 1);
this.#tree = this.project.parser.parse(text);
return;
}

public getText(range?: LSP.Range | undefined): string {
return this.#document.getText(range);
}

public positionAt(offset: number): LSP.Position {
return this.#document.positionAt(offset);
}

public offsetAt(position: LSP.Position): number {
return this.#document.offsetAt(position);
}

public get uri(): LSP.DocumentUri {
return this.#document.uri;
}

public get path(): string {
return url.fileURLToPath(this.#document.uri);
}

public get languageId(): string {
return this.#document.languageId;
}

public get version(): number {
return this.#document.version;
}

public get lineCount(): number {
return this.#document.lineCount;
}

/**
* The fully-qualified name of the class declared by this file. For instance,
* for a file named `MyLibrary/MyPackage/MyClass.mo`, this would be
* `["MyLibrary", "MyPackage", "MyClass"]`.
*/
public get packagePath(): string[] {
const directories = path.relative(this.#library.path, this.path).split(path.sep);
const fileName = directories.pop()!;

const packagePath: string[] = [this.#library.name, ...directories];
if (fileName !== "package.mo") {
packagePath.push(fileName.slice(0, fileName.length - ".mo".length));
}

return packagePath;
}

/**
* The enclosing package of the class declared by this file. For instance, for
* a file named `MyLibrary/MyPackage/MyClass.mo`, this would be `["MyLibrary",
* "MyPackage"]`.
*
* Note: this property should be the same thing as the `within` clause
* declared in the document. However, we don't actually check the clause at
* all. The `within` clause is entirely redundant and completely ignored.
*/
public get within(): string[] {
return this.packagePath.slice(0, -1);
}

public get project(): ModelicaProject {
return this.#library.project;
}

public get library(): ModelicaLibrary {
return this.#library;
}

public get tree(): Parser.Tree {
return this.#tree;
}
}
3 changes: 3 additions & 0 deletions server/src/project/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { ModelicaDocument } from "./document";
export { ModelicaLibrary } from "./library";
export { ModelicaProject } from "./project";
Loading

0 comments on commit d1eaec2

Please sign in to comment.