From e23da49d288ed5cc4b989632273223dc2f6237d4 Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Thu, 11 Apr 2024 15:38:26 +0200 Subject: [PATCH 01/24] Analyze entire project on opening it Co-authored-by: PaddiM8 --- server/package-lock.json | 91 ++++++++++++++++++++++++++++++++++++++++ server/package.json | 1 + server/src/analyzer.ts | 66 +++++++++++++++++++---------- server/src/server.ts | 91 ++++++++++++++++++++++++++++------------ 4 files changed, 201 insertions(+), 48 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index eee8282..fc6fce9 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "OSMC-PL-1-8", "dependencies": { + "@nodelib/fs.walk": "^2.0.0", "tree-sitter": "^0.20.6", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.11", @@ -18,6 +19,38 @@ "node": "20" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-3.0.0.tgz", + "integrity": "sha512-ktI9+PxfHYtKjF3cLTUAh2N+b8MijCRPNwKJNqTVdL0gB0QxLU2rIRaZ1t71oEa3YBDE6bukH1sR0+CDnpp/Mg==", + "dependencies": { + "@nodelib/fs.stat": "3.0.0", + "run-parallel": "^1.2.0" + }, + "engines": { + "node": ">=16.14.0" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-3.0.0.tgz", + "integrity": "sha512-2tQOI38s19P9i7X/Drt0v8iMA+KMsgdhB/dyPER+e+2Y8L1Z7QvnuRdW/uLuf5YRFUYmnj4bMA6qCuZHFI1GDQ==", + "engines": { + "node": ">=16.14.0" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-2.0.0.tgz", + "integrity": "sha512-54voNDBobGdMl3BUXSu7UaDh1P85PGHWlJ5e0XhPugo1JulOyCtp2I+5ri4wplGDJ8QGwPEQW7/x3yTLU7yF1A==", + "dependencies": { + "@nodelib/fs.scandir": "3.0.0", + "fastq": "^1.15.0" + }, + "engines": { + "node": ">=16.14.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -121,6 +154,14 @@ "node": ">=6" } }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -258,6 +299,25 @@ "once": "^1.3.1" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -285,6 +345,37 @@ "node": ">= 6" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", diff --git a/server/package.json b/server/package.json index afe8908..73c27d7 100644 --- a/server/package.json +++ b/server/package.json @@ -13,6 +13,7 @@ "url": "https://github.com/OpenModelica/modelica-language-server" }, "dependencies": { + "@nodelib/fs.walk": "^2.0.0", "tree-sitter": "^0.20.6", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.11", diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 0dd93cb..753269b 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -39,48 +39,51 @@ * ----------------------------------------------------------------------------- */ -import * as LSP from 'vscode-languageserver/node'; -import { TextDocument } from 'vscode-languageserver-textdocument'; +import * as LSP from "vscode-languageserver/node"; +import { TextDocument } from "vscode-languageserver-textdocument"; -import Parser = require('web-tree-sitter'); +import Parser from "web-tree-sitter"; -import { - getAllDeclarationsInTree -} from './util/declarations'; -import { logger } from './util/logger'; +import { getAllDeclarationsInTree } from "./util/declarations"; +import { logger } from "./util/logger"; +import * as TreeSitterUtil from "./util/tree-sitter"; type AnalyzedDocument = { - document: TextDocument, - declarations: LSP.SymbolInformation[], - tree: Parser.Tree -} + uri: string; + fileContent: string; + declarations: LSP.SymbolInformation[]; + tree: Parser.Tree; +}; export default class Analyzer { private parser: Parser; - private uriToAnalyzedDocument: Record = {}; + private uriToAnalyzedDocument: Record = + {}; - constructor (parser: Parser) { + constructor(parser: Parser) { this.parser = parser; } - public analyze(document: TextDocument): LSP.Diagnostic[] { - logger.debug('analyze:'); + public analyze(uri: string, fileContent: string): LSP.Diagnostic[] { + logger.debug(`analyze '${uri}':`); const diagnostics: LSP.Diagnostic[] = []; - const fileContent = document.getText(); - const uri = document.uri; - const tree = this.parser.parse(fileContent); - logger.debug(tree.rootNode.toString()); + //logger.debug(tree.rootNode.toString()); // Get declarations const declarations = getAllDeclarationsInTree(tree, uri); + // Analyze imported modules if necessary + this.resolveImports(tree); + // Update saved analysis for document uri + // TODO: do we even need fileContent? this.uriToAnalyzedDocument[uri] = { - document, + uri, + fileContent, declarations, - tree + tree, }; return diagnostics; @@ -100,4 +103,25 @@ export default class Analyzer { return getAllDeclarationsInTree(tree, uri); } + + private resolveImports(tree: Parser.Tree) { + // TODO: within statements + if (tree.rootNode.firstChild?.type == "within_clause") { + const packagePath = + tree.rootNode.firstChild.childForFieldName("name")?.text?.split(".") ?? + []; + logger.debug(`within package ${packagePath?.join(".")}`); + } + + // // TODO: Find all import statements + // TreeSitterUtil.forEach(tree.rootNode, (child) => { + // // TODO: return false if the node is not a class? or can imports be in any block? + // if (child.type == "import_clause") { + // const packagePath = child.childForFieldName("name")!.text.split("."); + // packagePath.pop(); + + // this.analyzePackage(packagePath); + // } + // }); + } } diff --git a/server/src/server.ts b/server/src/server.ts index 4bc00e4..03c483d 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -39,55 +39,68 @@ * ----------------------------------------------------------------------------- */ -import * as LSP from 'vscode-languageserver/node'; -import { TextDocument} from 'vscode-languageserver-textdocument'; +import * as LSP from "vscode-languageserver/node"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import * as fsWalk from "@nodelib/fs.walk"; +import * as fs from "node:fs/promises"; +import * as util from "node:util"; +import * as url from "node:url"; -import { initializeParser } from './parser'; -import Analyzer from './analyzer'; -import { logger, setLogConnection, setLogLevel } from './util/logger'; +import { initializeParser } from "./parser"; +import Analyzer from "./analyzer"; +import { logger, setLogConnection, setLogLevel } from "./util/logger"; /** * ModelicaServer collection all the important bits and bobs. */ export class ModelicaServer { - analyzer: Analyzer; + private analyzer: Analyzer; private clientCapabilities: LSP.ClientCapabilities; + private workspaceFolders: LSP.WorkspaceFolder[] | null | undefined; private connection: LSP.Connection; - private documents: LSP.TextDocuments = new LSP.TextDocuments(TextDocument); + private documents: LSP.TextDocuments = new LSP.TextDocuments( + TextDocument + ); private constructor( analyzer: Analyzer, clientCapabilities: LSP.ClientCapabilities, + workspaceFolders: LSP.WorkspaceFolder[] | null | undefined, connection: LSP.Connection ) { this.analyzer = analyzer; this.clientCapabilities = clientCapabilities; + this.workspaceFolders = workspaceFolders; this.connection = connection; } public static async initialize( connection: LSP.Connection, - { capabilities }: LSP.InitializeParams, + initializeParams: LSP.InitializeParams ): Promise { - // Initialize logger setLogConnection(connection); - setLogLevel('debug'); - logger.debug('Initializing...'); + setLogLevel("debug"); + logger.debug("Initializing..."); const parser = await initializeParser(); const analyzer = new Analyzer(parser); - const server = new ModelicaServer(analyzer, capabilities, connection); + const server = new ModelicaServer( + analyzer, + initializeParams.capabilities, + initializeParams.workspaceFolders, + connection + ); - logger.debug('Initialized'); + logger.debug("Initialized"); return server; } /** * Return what parts of the language server protocol are supported by ModelicaServer. */ - public capabilities(): LSP.ServerCapabilities { + public get capabilities(): LSP.ServerCapabilities { return { textDocumentSync: LSP.TextDocumentSyncKind.Full, completionProvider: undefined, @@ -95,12 +108,11 @@ export class ModelicaServer { signatureHelpProvider: undefined, documentSymbolProvider: true, colorProvider: false, - semanticTokensProvider: undefined + semanticTokensProvider: undefined, }; } public register(connection: LSP.Connection): void { - let currentDocument: TextDocument | null = null; let initialized = false; @@ -111,18 +123,23 @@ export class ModelicaServer { connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); connection.onInitialized(async () => { + logger.debug("onInitialized"); initialized = true; - if (currentDocument) { - // If we already have a document, analyze it now that we're initialized - // and the linter is ready. - this.analyzeDocument(currentDocument); - } + // if (currentDocument) { + // // If we already have a document, analyze it now that we're initialized + // // and the linter is ready. + // this.analyzeDocument(currentDocument); + // } + + // If we opened a project, analyze it now that we're initialized + // and the linter is ready. + this.analyzeWorkspaceFolders(); }); // The content of a text document has changed. This event is emitted // when the text document first opened or when its content has changed. this.documents.onDidChangeContent(({ document }) => { - logger.debug('onDidChangeContent'); + logger.debug("onDidChangeContent"); // We need to define some timing to wait some time or until whitespace is typed // to update the tree or we are doing this on every key stroke @@ -134,9 +151,28 @@ export class ModelicaServer { }); } + private async analyzeWorkspaceFolders(): Promise { + if (!this.workspaceFolders) { + return; + } + + for (const workspace of this.workspaceFolders) { + const walk = util.promisify(fsWalk.walk); + const entries = await walk(url.fileURLToPath(workspace.uri), { + entryFilter: (entry) => !!entry.name.match(/\.mos?$/), + }); + + for (const entry of entries) { + const diagnostics = this.analyzer.analyze( + entry.path, + await fs.readFile(entry.path, "utf-8") + ); + } + } + } - private async analyzeDocument(document: TextDocument) { - const diagnostics = this.analyzer.analyze(document); + private async analyzeDocument(document: TextDocument): Promise { + const diagnostics = this.analyzer.analyze(document.uri, document.getText()); } /** @@ -145,14 +181,15 @@ export class ModelicaServer { * @param params Unused. * @returns Symbol information. */ - private onDocumentSymbol(params: LSP.DocumentSymbolParams): LSP.SymbolInformation[] { + private onDocumentSymbol( + params: LSP.DocumentSymbolParams + ): LSP.SymbolInformation[] { // TODO: ideally this should return LSP.DocumentSymbol[] instead of LSP.SymbolInformation[] // which is a hierarchy of symbols. // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol logger.debug(`onDocumentSymbol`); return this.analyzer.getDeclarationsForUri(params.textDocument.uri); } - } // Create a connection for the server, using Node's IPC as a transport. @@ -164,7 +201,7 @@ connection.onInitialize( const server = await ModelicaServer.initialize(connection, params); server.register(connection); return { - capabilities: server.capabilities(), + capabilities: server.capabilities, }; } ); From e0cc38b03d8dc37f9c5ff87a9e60ac4ccc01b305 Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Thu, 11 Apr 2024 16:56:59 +0200 Subject: [PATCH 02/24] handle workspace events --- server/src/analyzer.ts | 8 ++++-- server/src/server.ts | 58 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 753269b..13c07cb 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -60,7 +60,7 @@ export default class Analyzer { private uriToAnalyzedDocument: Record = {}; - constructor(parser: Parser) { + public constructor(parser: Parser) { this.parser = parser; } @@ -89,6 +89,10 @@ export default class Analyzer { return diagnostics; } + public removeDocument(uri: string): void { + delete this.uriToAnalyzedDocument[uri]; + } + /** * Get all symbol declarations in the given file. This is used for generating an outline. * @@ -104,7 +108,7 @@ export default class Analyzer { return getAllDeclarationsInTree(tree, uri); } - private resolveImports(tree: Parser.Tree) { + private resolveImports(tree: Parser.Tree): void { // TODO: within statements if (tree.rootNode.firstChild?.type == "within_clause") { const packagePath = diff --git a/server/src/server.ts b/server/src/server.ts index 03c483d..7f2e910 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -101,6 +101,22 @@ export class ModelicaServer { * Return what parts of the language server protocol are supported by ModelicaServer. */ public get capabilities(): LSP.ServerCapabilities { + const modelicaFileFilter = { + scheme: "file", + pattern: { + glob: "*.{mo,mos}", + matches: LSP.FileOperationPatternKind.file, + }, + }; + + const folderFilter = { + scheme: "file", + pattern: { + glob: "*", + matches: LSP.FileOperationPatternKind.folder, + } + }; + return { textDocumentSync: LSP.TextDocumentSyncKind.Full, completionProvider: undefined, @@ -109,6 +125,23 @@ export class ModelicaServer { documentSymbolProvider: true, colorProvider: false, semanticTokensProvider: undefined, + workspace: { + workspaceFolders: { + supported: true, + changeNotifications: true, + }, + fileOperations: { + didCreate: { + filters: [modelicaFileFilter] + }, + didRename: { + filters: [modelicaFileFilter /*, folderFilter*/] + }, + didDelete: { + filters: [modelicaFileFilter /*, folderFilter*/], + }, + }, + }, }; } @@ -136,6 +169,29 @@ export class ModelicaServer { this.analyzeWorkspaceFolders(); }); + connection.workspace.onDidCreateFiles(async (params) => { + for (const file of params.files) { + this.analyzer.analyze(file.uri, await fs.readFile(file.uri, "utf-8")); + } + }); + + connection.workspace.onDidRenameFiles(async (params) => { + for (const file of params.files) { + // ...or maybe just analyzer.renameDocument, depending on uh if oldUri and newUri are always in the same folder? + this.analyzer.removeDocument(file.oldUri); + this.analyzer.analyze( + file.newUri, + await fs.readFile(file.newUri, "utf-8") + ); + } + }); + + connection.workspace.onDidDeleteFiles(async (params) => { + for (const file of params.files) { + this.analyzer.removeDocument(file.uri); + } + }); + // The content of a text document has changed. This event is emitted // when the text document first opened or when its content has changed. this.documents.onDidChangeContent(({ document }) => { @@ -163,7 +219,7 @@ export class ModelicaServer { }); for (const entry of entries) { - const diagnostics = this.analyzer.analyze( + const diagnostics = this.analyzer.analyze( entry.path, await fs.readFile(entry.path, "utf-8") ); From 07bd76ddb30a819abf98309f51a2584427b6b6ca Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Fri, 12 Apr 2024 11:52:41 +0200 Subject: [PATCH 03/24] Fix text document modification detection Co-authored-by: PaddiM8 --- server/src/analyzer.ts | 24 --------- server/src/server.ts | 120 ++++++++++++++++++----------------------- 2 files changed, 52 insertions(+), 92 deletions(-) diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 13c07cb..233fa27 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -74,9 +74,6 @@ export default class Analyzer { // Get declarations const declarations = getAllDeclarationsInTree(tree, uri); - // Analyze imported modules if necessary - this.resolveImports(tree); - // Update saved analysis for document uri // TODO: do we even need fileContent? this.uriToAnalyzedDocument[uri] = { @@ -107,25 +104,4 @@ export default class Analyzer { return getAllDeclarationsInTree(tree, uri); } - - private resolveImports(tree: Parser.Tree): void { - // TODO: within statements - if (tree.rootNode.firstChild?.type == "within_clause") { - const packagePath = - tree.rootNode.firstChild.childForFieldName("name")?.text?.split(".") ?? - []; - logger.debug(`within package ${packagePath?.join(".")}`); - } - - // // TODO: Find all import statements - // TreeSitterUtil.forEach(tree.rootNode, (child) => { - // // TODO: return false if the node is not a class? or can imports be in any block? - // if (child.type == "import_clause") { - // const packagePath = child.childForFieldName("name")!.text.split("."); - // packagePath.pop(); - - // this.analyzePackage(packagePath); - // } - // }); - } } diff --git a/server/src/server.ts b/server/src/server.ts index 7f2e910..d58633e 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -54,6 +54,7 @@ import { logger, setLogConnection, setLogLevel } from "./util/logger"; * ModelicaServer collection all the important bits and bobs. */ export class ModelicaServer { + private initialized = false; private analyzer: Analyzer; private clientCapabilities: LSP.ClientCapabilities; private workspaceFolders: LSP.WorkspaceFolder[] | null | undefined; @@ -101,22 +102,6 @@ export class ModelicaServer { * Return what parts of the language server protocol are supported by ModelicaServer. */ public get capabilities(): LSP.ServerCapabilities { - const modelicaFileFilter = { - scheme: "file", - pattern: { - glob: "*.{mo,mos}", - matches: LSP.FileOperationPatternKind.file, - }, - }; - - const folderFilter = { - scheme: "file", - pattern: { - glob: "*", - matches: LSP.FileOperationPatternKind.folder, - } - }; - return { textDocumentSync: LSP.TextDocumentSyncKind.Full, completionProvider: undefined, @@ -130,67 +115,20 @@ export class ModelicaServer { supported: true, changeNotifications: true, }, - fileOperations: { - didCreate: { - filters: [modelicaFileFilter] - }, - didRename: { - filters: [modelicaFileFilter /*, folderFilter*/] - }, - didDelete: { - filters: [modelicaFileFilter /*, folderFilter*/], - }, - }, }, }; } - public register(connection: LSP.Connection): void { + public async register(connection: LSP.Connection): Promise { let currentDocument: TextDocument | null = null; - let initialized = false; // Make the text document manager listen on the connection // for open, change and close text document events this.documents.listen(this.connection); connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); - - connection.onInitialized(async () => { - logger.debug("onInitialized"); - initialized = true; - // if (currentDocument) { - // // If we already have a document, analyze it now that we're initialized - // // and the linter is ready. - // this.analyzeDocument(currentDocument); - // } - - // If we opened a project, analyze it now that we're initialized - // and the linter is ready. - this.analyzeWorkspaceFolders(); - }); - - connection.workspace.onDidCreateFiles(async (params) => { - for (const file of params.files) { - this.analyzer.analyze(file.uri, await fs.readFile(file.uri, "utf-8")); - } - }); - - connection.workspace.onDidRenameFiles(async (params) => { - for (const file of params.files) { - // ...or maybe just analyzer.renameDocument, depending on uh if oldUri and newUri are always in the same folder? - this.analyzer.removeDocument(file.oldUri); - this.analyzer.analyze( - file.newUri, - await fs.readFile(file.newUri, "utf-8") - ); - } - }); - - connection.workspace.onDidDeleteFiles(async (params) => { - for (const file of params.files) { - this.analyzer.removeDocument(file.uri); - } - }); + connection.onInitialized(this.onInitialized.bind(this)); + connection.onDidChangeWatchedFiles(this.onDidChangeWatchedFiles.bind(this)); // The content of a text document has changed. This event is emitted // when the text document first opened or when its content has changed. @@ -201,12 +139,32 @@ export class ModelicaServer { // to update the tree or we are doing this on every key stroke currentDocument = document; - if (initialized) { + if (this.initialized) { this.analyzeDocument(document); } }); } + private async onInitialized(): Promise { + logger.debug("onInitialized"); + this.initialized = true; + + await connection.client.register( + new LSP.ProtocolNotificationType("workspace/didChangeWatchedFiles"), + { + watchers: [ + { + globPattern: "**/*.{mo,mos}", + }, + ], + } + ); + + // If we opened a project, analyze it now that we're initialized + // and the linter is ready. + this.analyzeWorkspaceFolders(); + } + private async analyzeWorkspaceFolders(): Promise { if (!this.workspaceFolders) { return; @@ -231,6 +189,32 @@ export class ModelicaServer { const diagnostics = this.analyzer.analyze(document.uri, document.getText()); } + private async onDidChangeWatchedFiles(params: LSP.DidChangeWatchedFilesParams): Promise { + logger.debug( + "onDidChangeWatchedFiles: " + JSON.stringify(params, undefined, 4) + ); + + for (const change of params.changes) { + switch (change.type) { + case LSP.FileChangeType.Created: { + const uri = url.fileURLToPath(change.uri); + this.analyzer.analyze(uri, await fs.readFile(uri, "utf-8")); + break; + } + case LSP.FileChangeType.Changed: { + const uri = url.fileURLToPath(change.uri); + this.analyzer.analyze(uri, await fs.readFile(uri, "utf-8")); + break; + } + case LSP.FileChangeType.Deleted: { + const uri = url.fileURLToPath(change.uri); + this.analyzer.removeDocument(uri); + break; + } + } + } + } + /** * Provide symbols defined in document. * @@ -255,7 +239,7 @@ const connection = LSP.createConnection(LSP.ProposedFeatures.all); connection.onInitialize( async (params: LSP.InitializeParams): Promise => { const server = await ModelicaServer.initialize(connection, params); - server.register(connection); + await server.register(connection); return { capabilities: server.capabilities, }; From 380e65cbd3f262ed6fcf8d9bf6869d8e62a9a1a9 Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Fri, 12 Apr 2024 16:44:33 +0200 Subject: [PATCH 04/24] Add caching for tree sitter analysis --- server/package-lock.json | 111 +++++++++++++++++++++++++++++++++++++++ server/package.json | 1 + server/src/analyzer.ts | 84 +++++++++++++++++++++++++++-- server/src/server.ts | 27 ++++++++-- 4 files changed, 216 insertions(+), 7 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index fc6fce9..7a84e0b 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,6 +10,7 @@ "license": "OSMC-PL-1-8", "dependencies": { "@nodelib/fs.walk": "^2.0.0", + "find-cache-dir": "^5.0.0", "tree-sitter": "^0.20.6", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.11", @@ -108,6 +109,11 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==" + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -162,6 +168,36 @@ "reusify": "^1.0.4" } }, + "node_modules/find-cache-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-5.0.0.tgz", + "integrity": "sha512-OuWNfjfP05JcpAP3JPgAKUhWefjMRfI5iAoSsvE24ANYWJaepAtlSgWECSVEuRgSXpyNEc9DJwG/TZpgcOqyig==", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -201,6 +237,20 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -265,6 +315,56 @@ "wrappy": "1" } }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prebuild-install": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", @@ -572,6 +672,17 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/server/package.json b/server/package.json index 73c27d7..e8fee6d 100644 --- a/server/package.json +++ b/server/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@nodelib/fs.walk": "^2.0.0", + "find-cache-dir": "^5.0.0", "tree-sitter": "^0.20.6", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.11", diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 233fa27..dca2950 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -41,8 +41,12 @@ import * as LSP from "vscode-languageserver/node"; import { TextDocument } from "vscode-languageserver-textdocument"; +import findCacheDirectory from "find-cache-dir"; import Parser from "web-tree-sitter"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import * as url from "node:url"; import { getAllDeclarationsInTree } from "./util/declarations"; import { logger } from "./util/logger"; @@ -50,21 +54,47 @@ import * as TreeSitterUtil from "./util/tree-sitter"; type AnalyzedDocument = { uri: string; - fileContent: string; + lastAnalyzed: Date, declarations: LSP.SymbolInformation[]; tree: Parser.Tree; }; +const cacheBaseDir = findCacheDirectory({ + name: "modelica-language-server", + create: true +}); + export default class Analyzer { private parser: Parser; + private workspaceFolders: LSP.WorkspaceFolder[] | null | undefined; private uriToAnalyzedDocument: Record = {}; - public constructor(parser: Parser) { + public constructor(parser: Parser, workspaceFolders?: LSP.WorkspaceFolder[] | null) { this.parser = parser; + this.workspaceFolders = workspaceFolders; } - public analyze(uri: string, fileContent: string): LSP.Diagnostic[] { + /** + * Analyzes a file. + * + * @param uri uri to file to analyze + * @param fileContent the updated content of the file + * @param lastModified the last time the file was changed. undefined == now. + * @returns diagnostics for the file + */ + public analyze(uri: string, fileContent: string, lastModified?: Date): LSP.Diagnostic[] { + // TODO: determine if the file needs to be reanalyzed or not + // (it might have been cached) + // We will need the lastModified time for the file. + const oldDocument = this.uriToAnalyzedDocument[uri]; + if (oldDocument && lastModified && oldDocument.lastAnalyzed >= lastModified) { + logger.debug(`skipping: ${uri}`); + + // TODO: return same diagnostics + return []; + } + logger.debug(`analyze '${uri}':`); const diagnostics: LSP.Diagnostic[] = []; @@ -78,7 +108,7 @@ export default class Analyzer { // TODO: do we even need fileContent? this.uriToAnalyzedDocument[uri] = { uri, - fileContent, + lastAnalyzed: new Date(), declarations, tree, }; @@ -104,4 +134,50 @@ export default class Analyzer { return getAllDeclarationsInTree(tree, uri); } + + public async loadCache(cacheDir: string): Promise { + for (const absolutePath in fs.readdir(cacheDir)) { + const originalFilePath = decodeURIComponent(path.basename(absolutePath)); + logger.debug(`loading from cache: ${originalFilePath}`); + + const documentContent = await fs.readFile(originalFilePath); + const originalFileUri = url.pathToFileURL(originalFilePath).href; + this.uriToAnalyzedDocument[originalFileUri] = JSON.parse(documentContent.toString()); + } + } + + public async saveCache(): Promise { + for (const [uri, document] of Object.entries(this.uriToAnalyzedDocument)) { + const cacheDir = this.getWorkspaceCacheDir(uri); + if (!document || !cacheDir) { + continue; + } + + const fileStats = await fs.stat(uri); + if (document.lastAnalyzed > fileStats.mtime) { + logger.debug(`writing to cache: ${uri}`); + const cacheFile = path.join(cacheDir, encodeURIComponent(uri)); + // TODO: is there a faster serialization method? + fs.writeFile(cacheFile, JSON.stringify(document)); + } + } + } + + private getWorkspaceCacheDir(fileUri: string): string | undefined { + if (!this.workspaceFolders) { + return undefined; + } + + const workspaceFolder = this.workspaceFolders + .map(folder => folder.uri) + .filter(fileUri.startsWith) + .sort((a, b) => b.length - a.length) + .at(0); + + if (!cacheBaseDir || !workspaceFolder) { + return undefined; + } + + return path.join(cacheBaseDir, encodeURIComponent(workspaceFolder)); + } } diff --git a/server/src/server.ts b/server/src/server.ts index d58633e..97b79e3 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -41,6 +41,7 @@ import * as LSP from "vscode-languageserver/node"; import { TextDocument } from "vscode-languageserver-textdocument"; +import findCacheDirectory from "find-cache-dir"; import * as fsWalk from "@nodelib/fs.walk"; import * as fs from "node:fs/promises"; import * as util from "node:util"; @@ -85,7 +86,7 @@ export class ModelicaServer { logger.debug("Initializing..."); const parser = await initializeParser(); - const analyzer = new Analyzer(parser); + const analyzer = new Analyzer(parser, initializeParams.workspaceFolders); const server = new ModelicaServer( analyzer, @@ -126,6 +127,7 @@ export class ModelicaServer { // for open, change and close text document events this.documents.listen(this.connection); + connection.onShutdown(this.onShutdown.bind(this)); connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); connection.onInitialized(this.onInitialized.bind(this)); connection.onDidChangeWatchedFiles(this.onDidChangeWatchedFiles.bind(this)); @@ -165,6 +167,21 @@ export class ModelicaServer { this.analyzeWorkspaceFolders(); } + private async onShutdown(): Promise { + logger.debug("close"); + + const cacheDir = findCacheDirectory({ + name: "modelica-language-server", + create: true + }); + + if (cacheDir) { + // TODO: open the file and read it + // TODO: determine what needs to be saved + await this.analyzer.saveCache(); + } + } + private async analyzeWorkspaceFolders(): Promise { if (!this.workspaceFolders) { return; @@ -177,12 +194,16 @@ export class ModelicaServer { }); for (const entry of entries) { + const stats = await fs.stat(entry.path); const diagnostics = this.analyzer.analyze( - entry.path, - await fs.readFile(entry.path, "utf-8") + url.pathToFileURL(entry.path).href, + await fs.readFile(entry.path, "utf-8"), + stats.mtime ); } } + + await this.analyzer.saveCache(); } private async analyzeDocument(document: TextDocument): Promise { From 683af7778a0360540bba8e2c8070d6c7c64e8775 Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Fri, 19 Apr 2024 16:49:07 +0200 Subject: [PATCH 05/24] Add prettier config Co-authored-by: PaddiM8 --- .prettierrc.js | 13 +++++++++++++ .vscode/settings.json | 6 +++++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 .prettierrc.js diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..4edfb95 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,13 @@ +module.exports = { + printWidth: 100, + tabWidth: 2, + useTabs: false, + semi: true, + singleQuote: false, + quoteProps: "as-needed", + trailingComma: "all", + bracketSpacing: true, + arrowParens: "always", + proseWrap: "preserve", + endOfLine: "lf", +}; diff --git a/.vscode/settings.json b/.vscode/settings.json index 2fb4fd9..d2fc35c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,9 @@ "typescript.preferences.quoteStyle": "single", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" - } + }, + "prettier.configPath": "./.prettierrc.js", + "cSpell.words": [ + "Modelica" + ] } From e6f5b3b8af553d034bd586ecb1cef1140b8d8251 Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Fri, 19 Apr 2024 16:50:36 +0200 Subject: [PATCH 06/24] Steal representation of Modelica projects from OMFrontend --- server/src/analyzer.ts | 63 ++++++++++- server/src/project/document.ts | 188 +++++++++++++++++++++++++++++++++ server/src/project/library.ts | 124 ++++++++++++++++++++++ server/src/project/project.ts | 100 ++++++++++++++++++ server/src/project/scope.ts | 53 ++++++++++ server/src/server.ts | 16 ++- server/src/util/tree-sitter.ts | 5 + 7 files changed, 541 insertions(+), 8 deletions(-) create mode 100644 server/src/project/document.ts create mode 100644 server/src/project/library.ts create mode 100644 server/src/project/project.ts create mode 100644 server/src/project/scope.ts diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index dca2950..009f179 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -53,7 +53,7 @@ import { logger } from "./util/logger"; import * as TreeSitterUtil from "./util/tree-sitter"; type AnalyzedDocument = { - uri: string; + uri: LSP.DocumentUri; lastAnalyzed: Date, declarations: LSP.SymbolInformation[]; tree: Parser.Tree; @@ -83,7 +83,7 @@ export default class Analyzer { * @param lastModified the last time the file was changed. undefined == now. * @returns diagnostics for the file */ - public analyze(uri: string, fileContent: string, lastModified?: Date): LSP.Diagnostic[] { + public analyze(uri: LSP.DocumentUri, fileContent: string, lastModified?: Date): LSP.Diagnostic[] { // TODO: determine if the file needs to be reanalyzed or not // (it might have been cached) // We will need the lastModified time for the file. @@ -116,7 +116,7 @@ export default class Analyzer { return diagnostics; } - public removeDocument(uri: string): void { + public removeDocument(uri: LSP.DocumentUri): void { delete this.uriToAnalyzedDocument[uri]; } @@ -125,7 +125,7 @@ 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[] { + public getDeclarationsForUri(uri: LSP.DocumentUri): LSP.SymbolInformation[] { const tree = this.uriToAnalyzedDocument[uri]?.tree; if (!tree?.rootNode) { @@ -135,6 +135,59 @@ export default class Analyzer { return getAllDeclarationsInTree(tree, uri); } + public findDeclarationFromPosition(uri: LSP.DocumentUri, line: number, character: number): LSP.Location | undefined { + const tree = this.uriToAnalyzedDocument[uri]?.tree; + if (!tree?.rootNode) { + return undefined; + } + + const hoveredNode = this.findSymbol(tree, line, character); + if (!hoveredNode) { + return undefined; + } + + const foundDeclaration = this.findDeclaration(hoveredNode); + return { + uri, + range: { + start: { + line: foundDeclaration.startPosition.row, + character: foundDeclaration.startPosition.column, + }, + end: { + line: foundDeclaration.endPosition.row, + character: foundDeclaration.endPosition.column, + }, + } + }; + } + + private findDeclaration(symbol: Parser.SyntaxNode): Parser.SyntaxNode { + return undefined as any; + } + + private findSymbol(tree: Parser.Tree, line: number, character: number): Parser.SyntaxNode | undefined { + let hoveredNode: Parser.SyntaxNode | undefined = undefined; + TreeSitterUtil.forEach(tree.rootNode, node => { + if (hoveredNode) { + return false; + } + + const isInNode = line >= node.startPosition.row && + line <= node.endPosition.row && + character >= node.startPosition.column && + character <= node.endPosition.column; + + if (node.type == "symbol...?") { + hoveredNode = node; + } + + return isInNode; + }); + + return hoveredNode; + } + public async loadCache(cacheDir: string): Promise { for (const absolutePath in fs.readdir(cacheDir)) { const originalFilePath = decodeURIComponent(path.basename(absolutePath)); @@ -163,7 +216,7 @@ export default class Analyzer { } } - private getWorkspaceCacheDir(fileUri: string): string | undefined { + private getWorkspaceCacheDir(fileUri: LSP.DocumentUri): string | undefined { if (!this.workspaceFolders) { return undefined; } diff --git a/server/src/project/document.ts b/server/src/project/document.ts new file mode 100644 index 0000000..a3f4c2c --- /dev/null +++ b/server/src/project/document.ts @@ -0,0 +1,188 @@ +/* + * 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 { Position, Range, 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 TreeSitterUtil from "../util/tree-sitter"; +import { positionToPoint } from "../util/tree-sitter"; +import { ModelicaProject } from "./project"; +import { ModelicaScope } from "./scope"; +import { publicDecrypt } from 'node:crypto'; +import path from 'node:path'; + +export class ModelicaDocument implements ModelicaScope, TextDocument { + readonly #project: ModelicaProject; + readonly #path: string; + readonly #document: TextDocument; + #tree: Parser.Tree; + + private constructor(project: ModelicaProject, path: string, document: TextDocument, tree: Parser.Tree) { + this.#project = project; + this.#path = path; + this.#document = document; + this.#tree = tree; + } + + public static async load( + project: ModelicaProject, + path: string, + ): Promise { + const content = await fs.readFile(path, "utf-8"); + const uri = url.pathToFileURL(path).href; + return new ModelicaDocument( + project, + path, + TextDocument.create(uri, "modelica", 0, content), + project.parser.parse(content), + ); + } + + public async update(text: string, range?: Range): Promise { + 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 !== undefined) { + 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 async resolve(reference: string[]): Promise { + let foundSymbol: Parser.SyntaxNode | null = null; + TreeSitterUtil.forEach(this.#tree.rootNode, (node: Parser.SyntaxNode) => { + if (foundSymbol) { + return false; + } + + // TODO: Is this right? + const className = node + .childForFieldName("classSpecifier") + ?.childForFieldName("IDENT") + ?.toString(); + if (node.type == "class_definition" && className == reference[0]) { + reference = reference.slice(1); + if (reference.length == 0) { + foundSymbol = node; + } + + return true; + } + + return false; + }); + + return foundSymbol; + } + + public getText(range?: Range | undefined): string { + return this.#document.getText(range); + } + + public positionAt(offset: number): Position { + return this.#document.positionAt(offset); + } + + public offsetAt(position: Position): number { + return this.#document.offsetAt(position); + } + + public get uri(): string { + return this.#document.uri; + } + + public get path(): string { + return this.#path; + } + + public get languageId(): string { + return this.#document.languageId; + } + + public get version(): number { + return this.#document.version; + } + + public get lineCount(): number { + return this.#document.lineCount; + } + + public get project(): ModelicaProject { + return this.#project; + } + + // public get tree(): Parser.Tree { + // return this.#tree; + // } +} diff --git a/server/src/project/library.ts b/server/src/project/library.ts new file mode 100644 index 0000000..11f7a86 --- /dev/null +++ b/server/src/project/library.ts @@ -0,0 +1,124 @@ +/* + * 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 * as LSP from "vscode-languageserver"; +import * as fsWalk from "@nodelib/fs.walk"; +import * as path from "node:path"; +import * as util from "node:util"; +import * as url from "node:url"; + +import { ModelicaDocument } from "./document"; +import { ModelicaProject } from "./project"; +import { ModelicaScope } from "./scope"; + +export class ModelicaLibrary implements ModelicaScope { + readonly #project: ModelicaProject; + readonly #path: string; + readonly #documents: ModelicaDocument[]; + + private constructor(project: ModelicaProject, basePath: string, documents: ModelicaDocument[]) { + this.#project = project; + this.#path = basePath; + this.#documents = documents; + } + + public static async load(project: ModelicaProject, basePath: string): Promise { + const walk = util.promisify(fsWalk.walk); + const entries = await walk(basePath, { + entryFilter: (entry) => !!entry.name.match(/.*\.mo/) && !entry.dirent.isDirectory(), + }); + + const documents = []; + for (const entry of entries) { + documents.push(await ModelicaDocument.load(project, entry.path)); + } + + return new ModelicaLibrary(project, basePath, documents); + } + + public get project(): ModelicaProject { + return this.#project; + } + + public async resolve(reference: string[]): Promise { + if (this.#documents.length === 0) { + return null; + } + + const getPathLength = (parent: T[], child: T[]): number => { + let matchedLength = 0; + for (let i = 0; i < child.length; i++) { + if (parent[i] !== child[i]) { + break; + } + matchedLength++; + } + + return matchedLength; + }; + + let bestDocument: ModelicaDocument; + let bestPathLength = -1; + for (const document of this.#documents) { + const directories = path.relative(this.path, document.path).split(path.sep); + const fileName = directories.pop()!; + + let packagePath: string[]; + if (fileName === "package.mo") { + packagePath = directories; + } else if (fileName.endsWith(".mo")) { + packagePath = [...directories, fileName.slice(0, fileName.length - ".mo".length)]; + } else { + continue; + } + + const pathLength = getPathLength(packagePath, reference); + if (pathLength > bestPathLength) { + bestDocument = document; + bestPathLength = pathLength; + } + } + + return await bestDocument!.resolve(reference); + } + + public get name(): string { + return path.basename(this.#path); + } + + public get path(): string { + return this.#path; + } +} diff --git a/server/src/project/project.ts b/server/src/project/project.ts new file mode 100644 index 0000000..6d0b1f1 --- /dev/null +++ b/server/src/project/project.ts @@ -0,0 +1,100 @@ +/* + * 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 Parser from "web-tree-sitter"; +import * as LSP from "vscode-languageserver"; + +import { ModelicaScope } from "./scope"; +import { ModelicaLibrary } from "./library"; + +export class ModelicaProject implements ModelicaScope { + readonly #parser: Parser; + #workspace: ModelicaLibrary | undefined; + #libraries: ModelicaLibrary[]; + + public constructor(parser: Parser) { + this.#parser = parser; + this.#workspace = undefined; + this.#libraries = []; + + } + + public get workspace(): ModelicaLibrary { + if (this.#workspace === undefined) { + throw new Error("Tried to access workspace before setting it"); + } + return this.#workspace; + } + + public set workspace(workspace: ModelicaLibrary) { + this.#workspace = workspace; + } + + public get libraries(): ModelicaLibrary[] { + return this.libraries; + } + + public addLibrary(library: ModelicaLibrary) { + this.#libraries.push(library); + } + + public async resolve(reference: string[]): Promise { + if (reference[0] === this.workspace.name) { + return await this.workspace.resolve(reference.slice(1)); + } + + for (const library of this.libraries) { + if (reference[0] === library.name) { + return await library.resolve(reference.slice(1)); + } + } + + // TODO: check annotations + // We don't need to resolve builtins like Boolean because they aren't + // declared anywhere. + + // TODO: check... array subscripts? can probably skip that + + return null; + } + + public get parser(): Parser { + return this.#parser; + } + + public get project(): ModelicaProject { + return this; + } +} diff --git a/server/src/project/scope.ts b/server/src/project/scope.ts new file mode 100644 index 0000000..0351ebc --- /dev/null +++ b/server/src/project/scope.ts @@ -0,0 +1,53 @@ +/* + * 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 * as LSP from "vscode-languageserver/node"; + +import { ModelicaProject } from "./project"; + +export interface ModelicaScope { + /** + * The project that this scope belongs to. + */ + project: ModelicaProject; + + /** + * Resolves a symbol reference. + * + * @param reference a symbol name, relative to the scope + * @returns the symbol, or null if not found in this scope. + */ + resolve(reference: string[]): Promise; +} diff --git a/server/src/server.ts b/server/src/server.ts index 97b79e3..8abac07 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -104,13 +104,14 @@ export class ModelicaServer { */ public get capabilities(): LSP.ServerCapabilities { return { - textDocumentSync: LSP.TextDocumentSyncKind.Full, + colorProvider: false, completionProvider: undefined, + declarationProvider: true, + documentSymbolProvider: true, hoverProvider: false, signatureHelpProvider: undefined, - documentSymbolProvider: true, - colorProvider: false, semanticTokensProvider: undefined, + textDocumentSync: LSP.TextDocumentSyncKind.Full, workspace: { workspaceFolders: { supported: true, @@ -131,6 +132,7 @@ export class ModelicaServer { connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); connection.onInitialized(this.onInitialized.bind(this)); connection.onDidChangeWatchedFiles(this.onDidChangeWatchedFiles.bind(this)); + connection.onDeclaration(this.onDeclaration.bind(this)); // The content of a text document has changed. This event is emitted // when the text document first opened or when its content has changed. @@ -236,6 +238,14 @@ export class ModelicaServer { } } + private onDeclaration(params: LSP.DeclarationParams): LSP.Location | undefined { + return this.analyzer.findDeclarationFromPosition( + params.textDocument.uri, + params.position.line, + params.position.character + ); + } + /** * Provide symbols defined in document. * diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index 90a37b2..f5b1a45 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -40,6 +40,7 @@ */ import * as LSP from 'vscode-languageserver/node'; +import Parser from 'web-tree-sitter'; import { SyntaxNode } from 'web-tree-sitter'; import { logger } from './logger'; @@ -169,3 +170,7 @@ 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 }; +} \ No newline at end of file From 38da1aaeac9e35bceb7b60693888ba1e7451fe78 Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Mon, 22 Apr 2024 17:11:24 +0200 Subject: [PATCH 07/24] Finish resolve algorithm; integrate with analyzer and server Co-authored-by: PaddiM8 --- server/src/analyzer.ts | 198 +++++++++++---------------------- server/src/project/document.ts | 177 +++++++++++++++++++++++------ server/src/project/library.ts | 7 +- server/src/project/project.ts | 57 ++++++++-- server/src/project/scope.ts | 6 +- server/src/server.ts | 164 +++++++++------------------ server/src/util/tree-sitter.ts | 80 +++++++++---- 7 files changed, 378 insertions(+), 311 deletions(-) diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 009f179..6a28ff2 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -41,7 +41,6 @@ import * as LSP from "vscode-languageserver/node"; import { TextDocument } from "vscode-languageserver-textdocument"; -import findCacheDirectory from "find-cache-dir"; import Parser from "web-tree-sitter"; import * as fs from "node:fs/promises"; @@ -51,73 +50,34 @@ import * as url from "node:url"; import { getAllDeclarationsInTree } from "./util/declarations"; import { logger } from "./util/logger"; import * as TreeSitterUtil from "./util/tree-sitter"; - -type AnalyzedDocument = { - uri: LSP.DocumentUri; - lastAnalyzed: Date, - declarations: LSP.SymbolInformation[]; - tree: Parser.Tree; -}; - -const cacheBaseDir = findCacheDirectory({ - name: "modelica-language-server", - create: true -}); +import { ModelicaProject } from "./project/project"; +import { ModelicaLibrary } from "./project/library"; export default class Analyzer { - private parser: Parser; - private workspaceFolders: LSP.WorkspaceFolder[] | null | undefined; - private uriToAnalyzedDocument: Record = - {}; - - public constructor(parser: Parser, workspaceFolders?: LSP.WorkspaceFolder[] | null) { - this.parser = parser; - this.workspaceFolders = workspaceFolders; - } - - /** - * Analyzes a file. - * - * @param uri uri to file to analyze - * @param fileContent the updated content of the file - * @param lastModified the last time the file was changed. undefined == now. - * @returns diagnostics for the file - */ - public analyze(uri: LSP.DocumentUri, fileContent: string, lastModified?: Date): LSP.Diagnostic[] { - // TODO: determine if the file needs to be reanalyzed or not - // (it might have been cached) - // We will need the lastModified time for the file. - const oldDocument = this.uriToAnalyzedDocument[uri]; - if (oldDocument && lastModified && oldDocument.lastAnalyzed >= lastModified) { - logger.debug(`skipping: ${uri}`); - - // TODO: return same diagnostics - return []; - } + #project: ModelicaProject; - logger.debug(`analyze '${uri}':`); - - const diagnostics: LSP.Diagnostic[] = []; - const tree = this.parser.parse(fileContent); - //logger.debug(tree.rootNode.toString()); + public constructor(parser: Parser) { + this.#project = new ModelicaProject(parser); + } - // Get declarations - const declarations = getAllDeclarationsInTree(tree, uri); + public async loadWorkspace(workspaceFolder: LSP.WorkspaceFolder): Promise { + const workspace = await ModelicaLibrary.load( + this.#project, + url.fileURLToPath(workspaceFolder.uri), + ); + this.#project.addWorkspace(workspace); + } - // Update saved analysis for document uri - // TODO: do we even need fileContent? - this.uriToAnalyzedDocument[uri] = { - uri, - lastAnalyzed: new Date(), - declarations, - tree, - }; + public addDocument(uri: LSP.DocumentUri): void { + this.#project.addDocument(uri); + } - return diagnostics; + public updateDocument(uri: LSP.DocumentUri, text: string, range?: LSP.Range): void { + this.#project.updateDocument(uri, text, range); } public removeDocument(uri: LSP.DocumentUri): void { - delete this.uriToAnalyzedDocument[uri]; + this.#project.removeDocument(uri); } /** @@ -126,7 +86,7 @@ export default class Analyzer { * TODO: convert to DocumentSymbol[] which is a hierarchy of symbols found in a given text document. */ public getDeclarationsForUri(uri: LSP.DocumentUri): LSP.SymbolInformation[] { - const tree = this.uriToAnalyzedDocument[uri]?.tree; + const tree = this.#project.getDocumentForUri(uri)?.tree; if (!tree?.rootNode) { return []; @@ -135,50 +95,70 @@ export default class Analyzer { return getAllDeclarationsInTree(tree, uri); } - public findDeclarationFromPosition(uri: LSP.DocumentUri, line: number, character: number): LSP.Location | undefined { - const tree = this.uriToAnalyzedDocument[uri]?.tree; + public async findDeclarationFromPosition( + uri: LSP.DocumentUri, + line: number, + character: number, + ): Promise { + const tree = this.#project.getDocumentForUri(uri)?.tree; if (!tree?.rootNode) { - return undefined; + return null; } - const hoveredNode = this.findSymbol(tree, line, character); - if (!hoveredNode) { - return undefined; + const hoveredName = this.findNodeAtPosition( + tree.rootNode, + line, + character, + node => node.type == "name" + ); + if (!hoveredName) { + return null; } - const foundDeclaration = this.findDeclaration(hoveredNode); - return { - uri, - range: { - start: { - line: foundDeclaration.startPosition.row, - character: foundDeclaration.startPosition.column, - }, - end: { - line: foundDeclaration.endPosition.row, - character: foundDeclaration.endPosition.column, - }, + const hoveredOffset = character - hoveredName.startPosition.column; + let symbols = TreeSitterUtil.getName(hoveredName); + + // Find out which symbol in `symbols` is the hovered one + // and remove the ones after it, since they are not relevant + let currentOffset = 0; + for (let i = 0; i < symbols.length; i++) { + if (currentOffset > hoveredOffset) { + symbols = symbols.slice(0, i + 1); + break; } - }; - } - private findDeclaration(symbol: Parser.SyntaxNode): Parser.SyntaxNode { - return undefined as any; + currentOffset += symbols[i].length; + } + + const hoveredIdentifier = this.findNodeAtPosition( + hoveredName, + line, + character, + node => node.type == "IDENT" + ); + + return await this.#project.getDocumentForUri(uri)?.resolveLocally(symbols, hoveredName) ?? null; } - private findSymbol(tree: Parser.Tree, line: number, character: number): Parser.SyntaxNode | undefined { + private findNodeAtPosition( + rootNode: Parser.SyntaxNode, + line: number, + character: number, + condition: (node: Parser.SyntaxNode) => boolean, + ): Parser.SyntaxNode | undefined { let hoveredNode: Parser.SyntaxNode | undefined = undefined; - TreeSitterUtil.forEach(tree.rootNode, node => { + TreeSitterUtil.forEach(rootNode, (node) => { if (hoveredNode) { return false; } - const isInNode = line >= node.startPosition.row && + const isInNode = + line >= node.startPosition.row && line <= node.endPosition.row && character >= node.startPosition.column && character <= node.endPosition.column; - if (node.type == "symbol...?") { + if (condition(node)) { hoveredNode = node; } @@ -187,50 +167,4 @@ export default class Analyzer { return hoveredNode; } - - public async loadCache(cacheDir: string): Promise { - for (const absolutePath in fs.readdir(cacheDir)) { - const originalFilePath = decodeURIComponent(path.basename(absolutePath)); - logger.debug(`loading from cache: ${originalFilePath}`); - - const documentContent = await fs.readFile(originalFilePath); - const originalFileUri = url.pathToFileURL(originalFilePath).href; - this.uriToAnalyzedDocument[originalFileUri] = JSON.parse(documentContent.toString()); - } - } - - public async saveCache(): Promise { - for (const [uri, document] of Object.entries(this.uriToAnalyzedDocument)) { - const cacheDir = this.getWorkspaceCacheDir(uri); - if (!document || !cacheDir) { - continue; - } - - const fileStats = await fs.stat(uri); - if (document.lastAnalyzed > fileStats.mtime) { - logger.debug(`writing to cache: ${uri}`); - const cacheFile = path.join(cacheDir, encodeURIComponent(uri)); - // TODO: is there a faster serialization method? - fs.writeFile(cacheFile, JSON.stringify(document)); - } - } - } - - private getWorkspaceCacheDir(fileUri: LSP.DocumentUri): string | undefined { - if (!this.workspaceFolders) { - return undefined; - } - - const workspaceFolder = this.workspaceFolders - .map(folder => folder.uri) - .filter(fileUri.startsWith) - .sort((a, b) => b.length - a.length) - .at(0); - - if (!cacheBaseDir || !workspaceFolder) { - return undefined; - } - - return path.join(cacheBaseDir, encodeURIComponent(workspaceFolder)); - } } diff --git a/server/src/project/document.ts b/server/src/project/document.ts index a3f4c2c..a1c8f51 100644 --- a/server/src/project/document.ts +++ b/server/src/project/document.ts @@ -33,50 +33,56 @@ * */ -import { Position, Range, TextDocument } from "vscode-languageserver-textdocument"; +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 { logger } from "../util/logger"; import * as TreeSitterUtil from "../util/tree-sitter"; import { positionToPoint } from "../util/tree-sitter"; +import { ModelicaLibrary } from "./library"; import { ModelicaProject } from "./project"; import { ModelicaScope } from "./scope"; -import { publicDecrypt } from 'node:crypto'; -import path from 'node:path'; export class ModelicaDocument implements ModelicaScope, TextDocument { - readonly #project: ModelicaProject; + readonly #library: ModelicaLibrary; readonly #path: string; readonly #document: TextDocument; #tree: Parser.Tree; - private constructor(project: ModelicaProject, path: string, document: TextDocument, tree: Parser.Tree) { - this.#project = project; + private constructor( + library: ModelicaLibrary, + path: string, + document: TextDocument, + tree: Parser.Tree, + ) { + this.#library = library; this.#path = path; this.#document = document; this.#tree = tree; } - public static async load( - project: ModelicaProject, - path: string, - ): Promise { - const content = await fs.readFile(path, "utf-8"); - const uri = url.pathToFileURL(path).href; + public static async load(library: ModelicaLibrary, filePath: string): Promise { + const content = await fs.readFile(filePath, "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); + + const uri = url.pathToFileURL(filePath).href; return new ModelicaDocument( - project, - path, + library, + filePath, TextDocument.create(uri, "modelica", 0, content), - project.parser.parse(content), + tree, ); } - public async update(text: string, range?: Range): Promise { + public async update(text: string, range?: LSP.Range): Promise { if (range === undefined) { TextDocument.update(this.#document, [{ text }], this.version + 1); - this.#tree = this.#project.parser.parse(text); + this.#tree = this.project.parser.parse(text); return; } @@ -98,7 +104,7 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { newEndPosition, }); - this.#tree = this.#project.parser.parse((index: number, position?: Parser.Point) => { + this.#tree = this.project.parser.parse((index: number, position?: Parser.Point) => { if (position !== undefined) { return this.getText({ start: { @@ -126,11 +132,7 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { return false; } - // TODO: Is this right? - const className = node - .childForFieldName("classSpecifier") - ?.childForFieldName("IDENT") - ?.toString(); + const className = TreeSitterUtil.getIdentifier(node); if (node.type == "class_definition" && className == reference[0]) { reference = reference.slice(1); if (reference.length == 0) { @@ -143,18 +145,127 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { return false; }); - return foundSymbol; + if (!foundSymbol) { + return null; + } + + return TreeSitterUtil.getSymbolInformation(this.#path, LSP.SymbolKind.Class, foundSymbol); + } + + public async resolveLocally( + reference: string[], + node: Parser.SyntaxNode, + ): Promise { + // Bottom up traversal: + // if there is an import statement in the current scope: + // call `resolve` on the current ModelicaProject (or library?), + // `resolve([..importSymbols, ..reference])` + // else if there is a variable declaration with the name reference[0]: + // if reference.length is just 1: + // return the position of the variable declaration + // else: + // find the type of the local. + // return the position of the field declaration in that type + // else if there is a local class with the name reference[0]: + // if reference.length is just 1: + // return the position of the class declaration + // else: + // return the position of the declaration in that class + // else: + // if node.parentNode == null: + // call `resolve` on the current ModelicaProject (or library?) + // else: + // node = node.parentNode + const importClauses = node.parent?.children.filter( + (sibling) => sibling.type == "import_clause", + ); + if (importClauses && importClauses.length > 0) { + for (const importClause of importClauses) { + const importedSymbol = await this.resolveImportClause(reference, importClause); + if (importedSymbol !== undefined) { + logger.debug( + `resolved ${reference} to import: ${JSON.stringify(importedSymbol, undefined, 4)}`, + ); + return importedSymbol; + } + } + } + + const local = node.parent?.children + .filter((sibling) => sibling.type == "component_clause") + .find((decl) => TreeSitterUtil.getIdentifier(decl) === reference[0]); + if (local) { + logger.debug(`found local: ${JSON.stringify(local, undefined, 4)}`); + + return TreeSitterUtil.getSymbolInformation(this.#path, LSP.SymbolKind.Variable, local); + } + + const classDefinition = node.parent?.children + .filter((sibling) => sibling.type == "class_definition") + .find((def) => TreeSitterUtil.getIdentifier(def) === reference[0]); + if (classDefinition) { + logger.debug(`found class: ${JSON.stringify(classDefinition, undefined, 4)}`); + + return TreeSitterUtil.getSymbolInformation(this.#path, LSP.SymbolKind.Class, classDefinition); + } + + if (!node.parent) { + // call `resolve` on the current ModelicaProject (or library?) + return await this.project.resolve(reference); + } + + return await this.resolveLocally(reference, node.parent!); + } + + private async resolveImportClause( + reference: string[], + importClause: Parser.SyntaxNode, + ): Promise { + const importedSymbol = TreeSitterUtil.getName(importClause.childForFieldName("name")!); + + // wildcard import: import a.b.*; + const isWildcard = importClause.childForFieldName("wildcard") != null; + if (isWildcard) { + const result = await this.project.resolve([...importedSymbol, ...reference]); + return result ?? undefined; + } + + // import alias: import z = a.b.c; + const alias = importClause.childForFieldName("alias")?.text; + if (alias && alias === reference[0]) { + return this.project.resolve([...importedSymbol, ...reference.slice(1)]); + } + + // multi-import: import a.b.{c, d, e}; + const childImports = importClause.childForFieldName("imports"); + if (childImports) { + const symbolWasImported = childImports.namedChildren + .filter((node) => node.type === "IDENT") + .map((node) => node.text) + .some((name) => name === reference[0]); + + if (symbolWasImported) { + return this.project.resolve([...importedSymbol, ...reference]); + } + } + + // normal import: import a.b.c; + if (importedSymbol.at(-1) === reference[0]) { + return this.project.resolve([...importedSymbol, ...reference.slice(1)]); + } + + return undefined; } - public getText(range?: Range | undefined): string { + public getText(range?: LSP.Range | undefined): string { return this.#document.getText(range); } - public positionAt(offset: number): Position { + public positionAt(offset: number): LSP.Position { return this.#document.positionAt(offset); } - public offsetAt(position: Position): number { + public offsetAt(position: LSP.Position): number { return this.#document.offsetAt(position); } @@ -179,10 +290,14 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { } public get project(): ModelicaProject { - return this.#project; + return this.#library.project; + } + + public get library(): ModelicaLibrary { + return this.#library; } - // public get tree(): Parser.Tree { - // return this.#tree; - // } + public get tree(): Parser.Tree { + return this.#tree; + } } diff --git a/server/src/project/library.ts b/server/src/project/library.ts index 11f7a86..4c90fc9 100644 --- a/server/src/project/library.ts +++ b/server/src/project/library.ts @@ -60,12 +60,13 @@ export class ModelicaLibrary implements ModelicaScope { entryFilter: (entry) => !!entry.name.match(/.*\.mo/) && !entry.dirent.isDirectory(), }); - const documents = []; + const documents: ModelicaDocument[] = []; + const library = new ModelicaLibrary(project, basePath, documents); for (const entry of entries) { - documents.push(await ModelicaDocument.load(project, entry.path)); + documents.push(await ModelicaDocument.load(library, entry.path)); } - return new ModelicaLibrary(project, basePath, documents); + return library; } public get project(): ModelicaProject { diff --git a/server/src/project/project.ts b/server/src/project/project.ts index 6d0b1f1..5fa6e2c 100644 --- a/server/src/project/project.ts +++ b/server/src/project/project.ts @@ -38,28 +38,25 @@ import * as LSP from "vscode-languageserver"; import { ModelicaScope } from "./scope"; import { ModelicaLibrary } from "./library"; +import { ModelicaDocument } from './document'; export class ModelicaProject implements ModelicaScope { readonly #parser: Parser; - #workspace: ModelicaLibrary | undefined; + #workspaces: ModelicaLibrary[]; #libraries: ModelicaLibrary[]; public constructor(parser: Parser) { this.#parser = parser; - this.#workspace = undefined; + this.#workspaces = []; this.#libraries = []; - } - public get workspace(): ModelicaLibrary { - if (this.#workspace === undefined) { - throw new Error("Tried to access workspace before setting it"); - } - return this.#workspace; + public get workspaces(): ModelicaLibrary[] { + return this.#workspaces; } - public set workspace(workspace: ModelicaLibrary) { - this.#workspace = workspace; + public addWorkspace(workspace: ModelicaLibrary) { + this.#workspaces.push(workspace); } public get libraries(): ModelicaLibrary[] { @@ -70,9 +67,45 @@ export class ModelicaProject implements ModelicaScope { this.#libraries.push(library); } + /** + * Finds the document identified by the given uri. + * + * @param uri file:// uri pointing to the document + * @returns the document, or `null` if no such document exists + */ + public getDocumentForUri(uri: LSP.DocumentUri): ModelicaDocument | null { + return null; + } + + /** + * Adds a new document to the LSP. + */ + public addDocument(uri: LSP.DocumentUri): void { + throw new Error("Not implemented!"); + } + + /** + * Updates the content and tree of the given document. + * + * @param text the modification + * @param range range to update, or undefined to replace the whole file + */ + public updateDocument(uri: LSP.DocumentUri, text: string, range?: LSP.Range): void { + throw new Error("Not implemented!"); + } + + /** + * Removes a document from the cache. + */ + public removeDocument(uri: LSP.DocumentUri): void { + throw new Error("Not implemented!"); + } + public async resolve(reference: string[]): Promise { - if (reference[0] === this.workspace.name) { - return await this.workspace.resolve(reference.slice(1)); + for (const workspace of this.workspaces) { + if (reference[0] === workspace.name) { + return await workspace.resolve(reference.slice(1)); + } } for (const library of this.libraries) { diff --git a/server/src/project/scope.ts b/server/src/project/scope.ts index 0351ebc..145d563 100644 --- a/server/src/project/scope.ts +++ b/server/src/project/scope.ts @@ -39,15 +39,15 @@ import { ModelicaProject } from "./project"; export interface ModelicaScope { /** - * The project that this scope belongs to. + * The project that this scope belongs to. */ project: ModelicaProject; /** * Resolves a symbol reference. - * + * * @param reference a symbol name, relative to the scope - * @returns the symbol, or null if not found in this scope. + * @returns the symbol declaration, or null if not found in this scope. */ resolve(reference: string[]): Promise; } diff --git a/server/src/server.ts b/server/src/server.ts index 8abac07..69cc87c 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -58,27 +58,22 @@ export class ModelicaServer { private initialized = false; private analyzer: Analyzer; private clientCapabilities: LSP.ClientCapabilities; - private workspaceFolders: LSP.WorkspaceFolder[] | null | undefined; private connection: LSP.Connection; - private documents: LSP.TextDocuments = new LSP.TextDocuments( - TextDocument - ); + private documents: LSP.TextDocuments = new LSP.TextDocuments(TextDocument); private constructor( analyzer: Analyzer, clientCapabilities: LSP.ClientCapabilities, - workspaceFolders: LSP.WorkspaceFolder[] | null | undefined, - connection: LSP.Connection + connection: LSP.Connection, ) { this.analyzer = analyzer; this.clientCapabilities = clientCapabilities; - this.workspaceFolders = workspaceFolders; this.connection = connection; } public static async initialize( connection: LSP.Connection, - initializeParams: LSP.InitializeParams + { capabilities, workspaceFolders }: LSP.InitializeParams, ): Promise { // Initialize logger setLogConnection(connection); @@ -86,17 +81,16 @@ export class ModelicaServer { logger.debug("Initializing..."); const parser = await initializeParser(); - const analyzer = new Analyzer(parser, initializeParams.workspaceFolders); - - const server = new ModelicaServer( - analyzer, - initializeParams.capabilities, - initializeParams.workspaceFolders, - connection - ); + const analyzer = new Analyzer(parser); + if (workspaceFolders != null) { + for (const workspace of workspaceFolders) { + await analyzer.loadWorkspace(workspace); + } + } + // TODO: add libraries as well logger.debug("Initialized"); - return server; + return new ModelicaServer(analyzer, capabilities, connection); } /** @@ -122,8 +116,6 @@ export class ModelicaServer { } public async register(connection: LSP.Connection): Promise { - let currentDocument: TextDocument | null = null; - // Make the text document manager listen on the connection // for open, change and close text document events this.documents.listen(this.connection); @@ -136,125 +128,79 @@ export class ModelicaServer { // The content of a text document has changed. This event is emitted // when the text document first opened or when its content has changed. - this.documents.onDidChangeContent(({ document }) => { + this.documents.onDidChangeContent((params) => { logger.debug("onDidChangeContent"); // We need to define some timing to wait some time or until whitespace is typed // to update the tree or we are doing this on every key stroke - currentDocument = document; - if (this.initialized) { - this.analyzeDocument(document); - } + // TODO: this gives us a document instance managed by this.document + // However, we make our documents ourselves. How do we get that to work? + // Do we just not use the TextDocuments class? + + // TODO: actually reanalyze }); } private async onInitialized(): Promise { - logger.debug("onInitialized"); - this.initialized = true; + logger.debug("onInitialized"); + this.initialized = true; + + await connection.client.register( + new LSP.ProtocolNotificationType("workspace/didChangeWatchedFiles"), + { + watchers: [ + { + globPattern: "**/*.{mo,mos}", + }, + ], + }, + ); - await connection.client.register( - new LSP.ProtocolNotificationType("workspace/didChangeWatchedFiles"), - { - watchers: [ - { - globPattern: "**/*.{mo,mos}", - }, - ], - } - ); + // If we opened a project, analyze it now that we're initialized + // and the linter is ready. - // If we opened a project, analyze it now that we're initialized - // and the linter is ready. - this.analyzeWorkspaceFolders(); + // TODO: analysis } private async onShutdown(): Promise { logger.debug("close"); - - const cacheDir = findCacheDirectory({ - name: "modelica-language-server", - create: true - }); - - if (cacheDir) { - // TODO: open the file and read it - // TODO: determine what needs to be saved - await this.analyzer.saveCache(); - } - } - - private async analyzeWorkspaceFolders(): Promise { - if (!this.workspaceFolders) { - return; - } - - for (const workspace of this.workspaceFolders) { - const walk = util.promisify(fsWalk.walk); - const entries = await walk(url.fileURLToPath(workspace.uri), { - entryFilter: (entry) => !!entry.name.match(/\.mos?$/), - }); - - for (const entry of entries) { - const stats = await fs.stat(entry.path); - const diagnostics = this.analyzer.analyze( - url.pathToFileURL(entry.path).href, - await fs.readFile(entry.path, "utf-8"), - stats.mtime - ); - } - } - - await this.analyzer.saveCache(); - } - - private async analyzeDocument(document: TextDocument): Promise { - const diagnostics = this.analyzer.analyze(document.uri, document.getText()); } private async onDidChangeWatchedFiles(params: LSP.DidChangeWatchedFilesParams): Promise { - logger.debug( - "onDidChangeWatchedFiles: " + JSON.stringify(params, undefined, 4) - ); + logger.debug("onDidChangeWatchedFiles: " + JSON.stringify(params, undefined, 4)); for (const change of params.changes) { switch (change.type) { - case LSP.FileChangeType.Created: { - const uri = url.fileURLToPath(change.uri); - this.analyzer.analyze(uri, await fs.readFile(uri, "utf-8")); + case LSP.FileChangeType.Created: + this.analyzer.addDocument(change.uri); break; - } case LSP.FileChangeType.Changed: { - const uri = url.fileURLToPath(change.uri); - this.analyzer.analyze(uri, await fs.readFile(uri, "utf-8")); + // TODO: incremental? + const path = url.fileURLToPath(change.uri); + const content = await fs.readFile(path, 'utf-8'); + this.analyzer.updateDocument(change.uri, content); break; } case LSP.FileChangeType.Deleted: { - const uri = url.fileURLToPath(change.uri); - this.analyzer.removeDocument(uri); + this.analyzer.removeDocument(change.uri); break; } } } } - private onDeclaration(params: LSP.DeclarationParams): LSP.Location | undefined { - return this.analyzer.findDeclarationFromPosition( + private async onDeclaration(params: LSP.DeclarationParams): Promise { + const symbolInformation = await this.analyzer.findDeclarationFromPosition( params.textDocument.uri, params.position.line, - params.position.character + params.position.character, ); + + return symbolInformation?.location ?? undefined; } - /** - * Provide symbols defined in document. - * - * @param params Unused. - * @returns Symbol information. - */ - private onDocumentSymbol( - params: LSP.DocumentSymbolParams - ): LSP.SymbolInformation[] { + private onDocumentSymbol(params: LSP.DocumentSymbolParams): LSP.SymbolInformation[] { // TODO: ideally this should return LSP.DocumentSymbol[] instead of LSP.SymbolInformation[] // which is a hierarchy of symbols. // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol @@ -267,15 +213,13 @@ export class ModelicaServer { // Also include all preview / proposed LSP features. const connection = LSP.createConnection(LSP.ProposedFeatures.all); -connection.onInitialize( - async (params: LSP.InitializeParams): Promise => { - const server = await ModelicaServer.initialize(connection, params); - await server.register(connection); - return { - capabilities: server.capabilities, - }; - } -); +connection.onInitialize(async (params: LSP.InitializeParams): Promise => { + const server = await ModelicaServer.initialize(connection, params); + await server.register(connection); + return { + capabilities: server.capabilities, + }; +}); // Listen on the connection connection.listen(); diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index f5b1a45..c673dcd 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -39,11 +39,11 @@ * ----------------------------------------------------------------------------- */ -import * as LSP from 'vscode-languageserver/node'; -import Parser from 'web-tree-sitter'; -import { SyntaxNode } from 'web-tree-sitter'; +import * as LSP from "vscode-languageserver/node"; +import Parser from "web-tree-sitter"; +import { SyntaxNode } from "web-tree-sitter"; -import { logger } from './logger'; +import { logger } from "./logger"; /** * Recursively iterate over all nodes in a tree. @@ -66,15 +66,17 @@ export function forEach(node: SyntaxNode, callback: (n: SyntaxNode) => void | bo * @param start The node to start iterating from * @param callback Callback returning true if node is searched node. */ -export function findFirst(start: SyntaxNode, callback: (n: SyntaxNode) => boolean): SyntaxNode | null { - +export function findFirst( + start: SyntaxNode, + callback: (n: SyntaxNode) => boolean, +): SyntaxNode | null { const cursor = start.walk(); let reachedRoot = false; let retracing = false; while (!reachedRoot) { const node = cursor.currentNode(); - if (callback(node) === true ) { + if (callback(node) === true) { return node; } @@ -88,14 +90,14 @@ export function findFirst(start: SyntaxNode, callback: (n: SyntaxNode) => boolea retracing = true; while (retracing) { - if (!cursor.gotoParent()) { - retracing = false; - reachedRoot = true; - } + if (!cursor.gotoParent()) { + retracing = false; + reachedRoot = true; + } - if (cursor.gotoNextSibling()) { + if (cursor.gotoNextSibling()) { retracing = false; - } + } } } @@ -119,7 +121,7 @@ export function range(n: SyntaxNode): LSP.Range { */ export function isDefinition(n: SyntaxNode): boolean { switch (n.type) { - case 'class_definition': + case "class_definition": return true; default: return false; @@ -146,11 +148,30 @@ export function findParent( * @param start Syntax tree node. */ export function getIdentifier(start: SyntaxNode): string | undefined { - - const node = findFirst(start, (n: SyntaxNode) => n.type == 'IDENT'); + const node = findFirst(start, (n: SyntaxNode) => n.type == "IDENT"); return node?.text; } +/** + * + * @param nameNode + * @returns + */ +export function getName(nameNode: SyntaxNode): string[] { + if (nameNode.type !== "name") { + throw new Error(`Expected a 'name' node; got '${nameNode.type}'`); + } + + const ident = nameNode.childForFieldName("identifier")!.text; + const qualifierNode = nameNode.childForFieldName("qualifier"); + if (qualifierNode) { + const qualifier = getName(qualifierNode); + return [...qualifier, ident]; + } else { + return [ident]; + } +} + /** * Get class prefixes from `class_definition` node. * @@ -158,13 +179,12 @@ export function getIdentifier(start: SyntaxNode): string | undefined { * @returns String with class prefixes or `null` if no `class_prefixes` can be found. */ export function getClassPrefixes(node: SyntaxNode): string | null { - - if (node.type !== 'class_definition') { + if (node.type !== "class_definition") { return null; } - const classPrefixNode = node.childForFieldName('classPrefixes'); - if (classPrefixNode == null || classPrefixNode.type !== 'class_prefixes') { + const classPrefixNode = node.childForFieldName("classPrefixes"); + if (classPrefixNode == null || classPrefixNode.type !== "class_prefixes") { return null; } @@ -173,4 +193,24 @@ export function getClassPrefixes(node: SyntaxNode): string | null { export function positionToPoint(position: LSP.Position): Parser.Point { return { row: position.line, column: position.character }; +} + +export function getSymbolInformation(documentUri: string, kind: LSP.SymbolKind, node: Parser.SyntaxNode): LSP.SymbolInformation { + return { + name: getIdentifier(node) ?? "", + kind, + location: { + uri: documentUri, + range: { + start: { + line: node.startPosition.row, + character: node.startPosition.column, + }, + end: { + line: node.endPosition.row, + character: node.endPosition.column, + }, + } + } + }; } \ No newline at end of file From 3646148fa89b0ec9069c27d53341affb629380c2 Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Tue, 23 Apr 2024 15:30:40 +0200 Subject: [PATCH 08/24] Get outline working again; changes to uri handling Co-authored-by: PaddiM8 --- .vscode/settings.json | 4 +- server/src/analyzer.ts | 17 ++++--- server/src/project/document.ts | 19 +++----- server/src/project/library.ts | 86 +++++++++++++++++++++------------ server/src/project/project.ts | 69 +++++++++++++++++--------- server/src/server.ts | 10 ++-- server/src/util/declarations.ts | 6 +-- server/src/util/index.ts | 15 ++++++ server/src/util/logger.ts | 4 +- server/src/util/tree-sitter.ts | 2 +- 10 files changed, 147 insertions(+), 85 deletions(-) create mode 100644 server/src/util/index.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index d2fc35c..732ac4f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,8 @@ }, "prettier.configPath": "./.prettierrc.js", "cSpell.words": [ - "Modelica" + "metamodelica", + "Modelica", + "OSMC" ] } diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 6a28ff2..dbfc9c0 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -60,12 +60,13 @@ export default class Analyzer { this.#project = new ModelicaProject(parser); } - public async loadWorkspace(workspaceFolder: LSP.WorkspaceFolder): Promise { + public async loadLibrary(uri: LSP.URI, isWorkspace: boolean): Promise { const workspace = await ModelicaLibrary.load( this.#project, - url.fileURLToPath(workspaceFolder.uri), + uri, + isWorkspace, ); - this.#project.addWorkspace(workspace); + this.#project.addLibrary(workspace); } public addDocument(uri: LSP.DocumentUri): void { @@ -86,7 +87,7 @@ export default class Analyzer { * TODO: convert to DocumentSymbol[] which is a hierarchy of symbols found in a given text document. */ public getDeclarationsForUri(uri: LSP.DocumentUri): LSP.SymbolInformation[] { - const tree = this.#project.getDocumentForUri(uri)?.tree; + const tree = this.#project.getDocument(uri)?.tree; if (!tree?.rootNode) { return []; @@ -100,7 +101,7 @@ export default class Analyzer { line: number, character: number, ): Promise { - const tree = this.#project.getDocumentForUri(uri)?.tree; + const tree = this.#project.getDocument(uri)?.tree; if (!tree?.rootNode) { return null; } @@ -137,14 +138,14 @@ export default class Analyzer { node => node.type == "IDENT" ); - return await this.#project.getDocumentForUri(uri)?.resolveLocally(symbols, hoveredName) ?? null; + return await this.#project.getDocument(uri)?.resolveLocally(symbols, hoveredName) ?? null; } private findNodeAtPosition( rootNode: Parser.SyntaxNode, line: number, character: number, - condition: (node: Parser.SyntaxNode) => boolean, + condition?: (node: Parser.SyntaxNode) => boolean, ): Parser.SyntaxNode | undefined { let hoveredNode: Parser.SyntaxNode | undefined = undefined; TreeSitterUtil.forEach(rootNode, (node) => { @@ -158,7 +159,7 @@ export default class Analyzer { character >= node.startPosition.column && character <= node.endPosition.column; - if (condition(node)) { + if (!condition || condition(node)) { hoveredNode = node; } diff --git a/server/src/project/document.ts b/server/src/project/document.ts index a1c8f51..a44119d 100644 --- a/server/src/project/document.ts +++ b/server/src/project/document.ts @@ -48,32 +48,29 @@ import { ModelicaScope } from "./scope"; export class ModelicaDocument implements ModelicaScope, TextDocument { readonly #library: ModelicaLibrary; - readonly #path: string; readonly #document: TextDocument; #tree: Parser.Tree; private constructor( library: ModelicaLibrary, - path: string, document: TextDocument, tree: Parser.Tree, ) { this.#library = library; - this.#path = path; this.#document = document; this.#tree = tree; } - public static async load(library: ModelicaLibrary, filePath: string): Promise { - const content = await fs.readFile(filePath, "utf-8"); + public static async load(library: ModelicaLibrary, uri: LSP.DocumentUri): Promise { + logger.debug(`Loading document at '${uri}'...`); + + const content = await fs.readFile(url.fileURLToPath(uri), "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); - const uri = url.pathToFileURL(filePath).href; return new ModelicaDocument( library, - filePath, TextDocument.create(uri, "modelica", 0, content), tree, ); @@ -149,7 +146,7 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { return null; } - return TreeSitterUtil.getSymbolInformation(this.#path, LSP.SymbolKind.Class, foundSymbol); + return TreeSitterUtil.getSymbolInformation(this.uri, LSP.SymbolKind.Class, foundSymbol); } public async resolveLocally( @@ -197,7 +194,7 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { if (local) { logger.debug(`found local: ${JSON.stringify(local, undefined, 4)}`); - return TreeSitterUtil.getSymbolInformation(this.#path, LSP.SymbolKind.Variable, local); + return TreeSitterUtil.getSymbolInformation(this.uri, LSP.SymbolKind.Variable, local); } const classDefinition = node.parent?.children @@ -206,7 +203,7 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { if (classDefinition) { logger.debug(`found class: ${JSON.stringify(classDefinition, undefined, 4)}`); - return TreeSitterUtil.getSymbolInformation(this.#path, LSP.SymbolKind.Class, classDefinition); + return TreeSitterUtil.getSymbolInformation(this.uri, LSP.SymbolKind.Class, classDefinition); } if (!node.parent) { @@ -274,7 +271,7 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { } public get path(): string { - return this.#path; + return url.fileURLToPath(this.uri); } public get languageId(): string { diff --git a/server/src/project/library.ts b/server/src/project/library.ts index 4c90fc9..cc4a248 100644 --- a/server/src/project/library.ts +++ b/server/src/project/library.ts @@ -39,60 +39,63 @@ import * as path from "node:path"; import * as util from "node:util"; import * as url from "node:url"; +import * as miscUtil from "../util"; +import logger from '../util/logger'; import { ModelicaDocument } from "./document"; import { ModelicaProject } from "./project"; import { ModelicaScope } from "./scope"; export class ModelicaLibrary implements ModelicaScope { readonly #project: ModelicaProject; - readonly #path: string; - readonly #documents: ModelicaDocument[]; + readonly #uri: string; + readonly #documents: Map; + readonly #isWorkspace: boolean; - private constructor(project: ModelicaProject, basePath: string, documents: ModelicaDocument[]) { + private constructor(project: ModelicaProject, uri: LSP.URI, isWorkspace: boolean) { this.#project = project; - this.#path = basePath; - this.#documents = documents; + this.#uri = uri; + this.#documents = new Map(); + this.#isWorkspace = isWorkspace; } - public static async load(project: ModelicaProject, basePath: string): Promise { + public static async load( + project: ModelicaProject, + uri: LSP.URI, + isWorkspace: boolean, + ): Promise { + logger.info(`Loading ${isWorkspace ? 'workspace' : 'library'} at '${uri}'...`); + const walk = util.promisify(fsWalk.walk); - const entries = await walk(basePath, { + const entries = await walk(url.fileURLToPath(uri), { entryFilter: (entry) => !!entry.name.match(/.*\.mo/) && !entry.dirent.isDirectory(), }); - const documents: ModelicaDocument[] = []; - const library = new ModelicaLibrary(project, basePath, documents); + const library = new ModelicaLibrary(project, uri, isWorkspace); for (const entry of entries) { - documents.push(await ModelicaDocument.load(library, entry.path)); + let documentUri = url.pathToFileURL(entry.path).href; + // Note: LSP sends us file uris containing '%3A' instead of ':', but + // the node pathToFileURL uses ':' anyways. Manually fix this here. + // This is a bit hacky but we should ideally only be working with the URIs from LSP anyways. + documentUri = documentUri.slice(0, 5) + documentUri.slice(5).replace(":", "%3A"); + logger.info(`uri`, documentUri); + + const document = await ModelicaDocument.load(library, documentUri); + library.#documents.set(documentUri, document); } + logger.debug(`Loaded ${library.#documents.size} documents`); return library; } - public get project(): ModelicaProject { - return this.#project; - } - public async resolve(reference: string[]): Promise { - if (this.#documents.length === 0) { + if (this.#documents.size === 0) { return null; } - const getPathLength = (parent: T[], child: T[]): number => { - let matchedLength = 0; - for (let i = 0; i < child.length; i++) { - if (parent[i] !== child[i]) { - break; - } - matchedLength++; - } - - return matchedLength; - }; - let bestDocument: ModelicaDocument; let bestPathLength = -1; - for (const document of this.#documents) { + for (const entry of this.#documents) { + const [_uri, document] = entry; const directories = path.relative(this.path, document.path).split(path.sep); const fileName = directories.pop()!; @@ -105,7 +108,10 @@ export class ModelicaLibrary implements ModelicaScope { continue; } - const pathLength = getPathLength(packagePath, reference); + // TODO: this won't work because in the case of workspaces, the actual package might be in a subdirectory + // Perhaps we should just add the concept of a "library root" that is searched for libraries. + // That might be unnecessary though. + const pathLength = miscUtil.getOverlappingLength(packagePath, reference); if (pathLength > bestPathLength) { bestDocument = document; bestPathLength = pathLength; @@ -116,10 +122,26 @@ export class ModelicaLibrary implements ModelicaScope { } public get name(): string { - return path.basename(this.#path); + return path.basename(this.path); } - + public get path(): string { - return this.#path; + return url.fileURLToPath(this.#uri); + } + + public get uri(): string { + return this.#uri; + } + + public get project(): ModelicaProject { + return this.#project; + } + + public get documents(): Map { + return this.#documents; + } + + public get isWorkspace(): boolean { + return this.#isWorkspace; } } diff --git a/server/src/project/project.ts b/server/src/project/project.ts index 5fa6e2c..a704bd2 100644 --- a/server/src/project/project.ts +++ b/server/src/project/project.ts @@ -35,30 +35,24 @@ import Parser from "web-tree-sitter"; import * as LSP from "vscode-languageserver"; +import url from "node:url"; +import path from "node:path"; import { ModelicaScope } from "./scope"; import { ModelicaLibrary } from "./library"; import { ModelicaDocument } from './document'; +import * as util from '../util'; +import logger from "../util/logger"; export class ModelicaProject implements ModelicaScope { readonly #parser: Parser; - #workspaces: ModelicaLibrary[]; - #libraries: ModelicaLibrary[]; + readonly #libraries: ModelicaLibrary[]; public constructor(parser: Parser) { this.#parser = parser; - this.#workspaces = []; this.#libraries = []; } - public get workspaces(): ModelicaLibrary[] { - return this.#workspaces; - } - - public addWorkspace(workspace: ModelicaLibrary) { - this.#workspaces.push(workspace); - } - public get libraries(): ModelicaLibrary[] { return this.libraries; } @@ -71,17 +65,43 @@ export class ModelicaProject implements ModelicaScope { * Finds the document identified by the given uri. * * @param uri file:// uri pointing to the document - * @returns the document, or `null` if no such document exists + * @returns the document, or `undefined` if no such document exists */ - public getDocumentForUri(uri: LSP.DocumentUri): ModelicaDocument | null { - return null; + public getDocument(uri: LSP.DocumentUri): ModelicaDocument | undefined { + for (const library of this.#libraries) { + const doc = library.documents.get(uri); + if (doc) { + logger.debug(`Found document: ${doc.path}`); + return doc; + } + } + + logger.debug(`Couldn't find document: ${uri}`); + + return undefined; } /** * Adds a new document to the LSP. */ - public addDocument(uri: LSP.DocumentUri): void { - throw new Error("Not implemented!"); + public async addDocument(uri: LSP.DocumentUri): Promise { + logger.info(`Adding document at '${uri}'...`); + + const documentPath = url.fileURLToPath(uri); + for (const library of this.#libraries) { + const relative = path.relative(library.path, documentPath); + const isSubdirectory = relative && !relative.startsWith("..") && !path.isAbsolute(relative); + + // Assume that files can't be inside multiple libraries at the same time + if (isSubdirectory) { + const document = await ModelicaDocument.load(library, documentPath); + library.documents.set(uri, document); + logger.debug(`Added document: ${uri}`); + return; + } + } + + throw Error(`Failed to add document '${uri}': not a part of any libraries.`); } /** @@ -91,23 +111,24 @@ export class ModelicaProject implements ModelicaScope { * @param range range to update, or undefined to replace the whole file */ public updateDocument(uri: LSP.DocumentUri, text: string, range?: LSP.Range): void { - throw new Error("Not implemented!"); + logger.debug(`Updating document at '${uri}'...`); + + const doc = this.getDocument(uri); + doc?.update(text, range); + logger.debug(`Updated document: ${uri}`); } /** * Removes a document from the cache. */ public removeDocument(uri: LSP.DocumentUri): void { - throw new Error("Not implemented!"); + logger.info(`Removing document at '${uri}'...`); + + const doc = this.getDocument(uri); + doc?.library.documents.delete(uri); } public async resolve(reference: string[]): Promise { - for (const workspace of this.workspaces) { - if (reference[0] === workspace.name) { - return await workspace.resolve(reference.slice(1)); - } - } - for (const library of this.libraries) { if (reference[0] === library.name) { return await library.resolve(reference.slice(1)); diff --git a/server/src/server.ts b/server/src/server.ts index 69cc87c..d84c58b 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -84,7 +84,7 @@ export class ModelicaServer { const analyzer = new Analyzer(parser); if (workspaceFolders != null) { for (const workspace of workspaceFolders) { - await analyzer.loadWorkspace(workspace); + await analyzer.loadLibrary(workspace.uri, true); } } // TODO: add libraries as well @@ -164,7 +164,7 @@ export class ModelicaServer { } private async onShutdown(): Promise { - logger.debug("close"); + logger.debug("onShutdown"); } private async onDidChangeWatchedFiles(params: LSP.DidChangeWatchedFilesParams): Promise { @@ -191,6 +191,8 @@ export class ModelicaServer { } private async onDeclaration(params: LSP.DeclarationParams): Promise { + logger.debug("onDeclaration"); + const symbolInformation = await this.analyzer.findDeclarationFromPosition( params.textDocument.uri, params.position.line, @@ -201,10 +203,10 @@ export class ModelicaServer { } private onDocumentSymbol(params: LSP.DocumentSymbolParams): LSP.SymbolInformation[] { + logger.debug(`onDocumentSymbol`); // TODO: ideally this should return LSP.DocumentSymbol[] instead of LSP.SymbolInformation[] // which is a hierarchy of symbols. // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol - logger.debug(`onDocumentSymbol`); return this.analyzer.getDeclarationsForUri(params.textDocument.uri); } } @@ -213,7 +215,7 @@ export class ModelicaServer { // Also include all preview / proposed LSP features. const connection = LSP.createConnection(LSP.ProposedFeatures.all); -connection.onInitialize(async (params: LSP.InitializeParams): Promise => { +connection.onInitialize(async (params) => { const server = await ModelicaServer.initialize(connection, params); await server.register(connection); return { diff --git a/server/src/util/declarations.ts b/server/src/util/declarations.ts index fbcface..45ea489 100644 --- a/server/src/util/declarations.ts +++ b/server/src/util/declarations.ts @@ -61,7 +61,7 @@ const GLOBAL_DECLARATION_LEAF_NODE_TYPES = new Set([ * @param uri The document's uri. * @returns Symbol information for all declarations. */ -export function getAllDeclarationsInTree(tree: Parser.Tree, uri: string): LSP.SymbolInformation[] { +export function getAllDeclarationsInTree(tree: Parser.Tree, uri: LSP.DocumentUri): LSP.SymbolInformation[] { const symbols: LSP.SymbolInformation[] = []; TreeSitterUtil.forEach(tree.rootNode, (node) => { @@ -81,7 +81,7 @@ export function getAllDeclarationsInTree(tree: Parser.Tree, uri: string): LSP.Sy * @param uri The document's uri. * @returns Symbol information from node. */ -export function nodeToSymbolInformation(node: Parser.SyntaxNode, uri: string): LSP.SymbolInformation | null { +export function nodeToSymbolInformation(node: Parser.SyntaxNode, uri: LSP.DocumentUri): LSP.SymbolInformation | null { const named = node.firstNamedChild; if (named === null) { @@ -115,7 +115,7 @@ export function nodeToSymbolInformation(node: Parser.SyntaxNode, uri: string): L * @param uri The associated URI for this document. * @returns LSP symbol information for definition. */ -function getDeclarationSymbolFromNode(node: Parser.SyntaxNode, uri: string): LSP.SymbolInformation | null { +function getDeclarationSymbolFromNode(node: Parser.SyntaxNode, uri: LSP.DocumentUri): LSP.SymbolInformation | null { if (TreeSitterUtil.isDefinition(node)) { return nodeToSymbolInformation(node, uri); } diff --git a/server/src/util/index.ts b/server/src/util/index.ts new file mode 100644 index 0000000..2cdc6f6 --- /dev/null +++ b/server/src/util/index.ts @@ -0,0 +1,15 @@ +export function getOverlappingLength(parent: T[], child: T[]): number; +export function getOverlappingLength(parent: string, child: string): number; +export function getOverlappingLength(parent: Record, child: Record & { length: number }): number { + let matchedLength = 0; + for (let i = 0; i < child.length; i++) { + if (parent[i] !== child[i]) { + break; + } + matchedLength++; + } + + return matchedLength; +} + + diff --git a/server/src/util/logger.ts b/server/src/util/logger.ts index be3df8d..883d166 100644 --- a/server/src/util/logger.ts +++ b/server/src/util/logger.ts @@ -165,4 +165,6 @@ export function getLogLevelFromEnvironment(): LSP.MessageType { } return LOG_LEVELS_TO_MESSAGE_TYPES[DEFAULT_LOG_LEVEL]; -} \ No newline at end of file +} + +export default logger; \ No newline at end of file diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index c673dcd..4536443 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -195,7 +195,7 @@ export function positionToPoint(position: LSP.Position): Parser.Point { return { row: position.line, column: position.character }; } -export function getSymbolInformation(documentUri: string, kind: LSP.SymbolKind, node: Parser.SyntaxNode): LSP.SymbolInformation { +export function getSymbolInformation(documentUri: LSP.DocumentUri, kind: LSP.SymbolKind, node: Parser.SyntaxNode): LSP.SymbolInformation { return { name: getIdentifier(node) ?? "", kind, From 7eb37279f8b1d7862286adc2d2e9976b6a984265 Mon Sep 17 00:00:00 2001 From: PaddiM8 Date: Wed, 24 Apr 2024 12:05:13 +0200 Subject: [PATCH 09/24] Fix infinite recursion and declaration symbols not being found --- server/src/analyzer.ts | 15 +++++++++------ server/src/project/document.ts | 1 + server/src/project/library.ts | 2 +- server/src/project/project.ts | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index dbfc9c0..a787f9c 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -137,8 +137,11 @@ export default class Analyzer { character, node => node.type == "IDENT" ); + if (!hoveredIdentifier) { + return null; + } - return await this.#project.getDocument(uri)?.resolveLocally(symbols, hoveredName) ?? null; + return await this.#project.getDocument(uri)?.resolveLocally(symbols, hoveredIdentifier) ?? null; } private findNodeAtPosition( @@ -153,11 +156,11 @@ export default class Analyzer { return false; } - const isInNode = - line >= node.startPosition.row && - line <= node.endPosition.row && - character >= node.startPosition.column && - character <= node.endPosition.column; + const startPos = node.startPosition; + const endPos = node.endPosition; + const isInNode = (line > startPos.row && line < endPos.row) + || (line >= startPos.row && character >= startPos.column && character <= startPos.column) + || (line >= endPos.row && character >= endPos.column && character <= endPos.column); if (!condition || condition(node)) { hoveredNode = node; diff --git a/server/src/project/document.ts b/server/src/project/document.ts index a44119d..8b93c3e 100644 --- a/server/src/project/document.ts +++ b/server/src/project/document.ts @@ -211,6 +211,7 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { return await this.project.resolve(reference); } + return await this.resolveLocally(reference, node.parent!); } diff --git a/server/src/project/library.ts b/server/src/project/library.ts index cc4a248..6ac0499 100644 --- a/server/src/project/library.ts +++ b/server/src/project/library.ts @@ -124,7 +124,7 @@ export class ModelicaLibrary implements ModelicaScope { public get name(): string { return path.basename(this.path); } - + public get path(): string { return url.fileURLToPath(this.#uri); } diff --git a/server/src/project/project.ts b/server/src/project/project.ts index a704bd2..8a6fc5d 100644 --- a/server/src/project/project.ts +++ b/server/src/project/project.ts @@ -54,7 +54,7 @@ export class ModelicaProject implements ModelicaScope { } public get libraries(): ModelicaLibrary[] { - return this.libraries; + return this.#libraries; } public addLibrary(library: ModelicaLibrary) { From 9dae5cbb6e2f9decadd9f50d1e7b933928a737f2 Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Fri, 26 Apr 2024 16:14:22 +0200 Subject: [PATCH 10/24] Implement goto declaration/definition for locals Co-authored-by: PaddiM8 --- .vscode/settings.json | 4 +- server/src/analyzer.ts | 131 +++++++++++++++----------- server/src/project/document.ts | 164 ++++++++++++++++++++------------- server/src/project/library.ts | 56 +++++++---- server/src/project/project.ts | 23 +++-- server/src/project/scope.ts | 2 +- server/src/server.ts | 61 +++++++++++- server/src/util/tree-sitter.ts | 101 ++++++++++++++++---- 8 files changed, 373 insertions(+), 169 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 732ac4f..d1f152c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,8 @@ "cSpell.words": [ "metamodelica", "Modelica", - "OSMC" + "nodelib", + "OSMC", + "redeclaration" ] } diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index a787f9c..a12dfd9 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -61,11 +61,7 @@ export default class Analyzer { } public async loadLibrary(uri: LSP.URI, isWorkspace: boolean): Promise { - const workspace = await ModelicaLibrary.load( - this.#project, - uri, - isWorkspace, - ); + const workspace = await ModelicaLibrary.load(this.#project, uri, isWorkspace); this.#project.addLibrary(workspace); } @@ -100,75 +96,102 @@ export default class Analyzer { uri: LSP.DocumentUri, line: number, character: number, - ): Promise { - const tree = this.#project.getDocument(uri)?.tree; - if (!tree?.rootNode) { + ): Promise { + logger.debug(`Searching for declaration of symbol at ${line + 1}:${character + 1} in '${uri}'`); + + const document = this.#project.getDocument(uri); + if (!document) { + logger.warn(`Couldn't find declaration: document not loaded.`); return null; } - const hoveredName = this.findNodeAtPosition( - tree.rootNode, - line, - character, - node => node.type == "name" - ); - if (!hoveredName) { + if (!document.tree.rootNode) { + logger.info(`Couldn't find declaration: document has no nodes.`); return null; } - const hoveredOffset = character - hoveredName.startPosition.column; - let symbols = TreeSitterUtil.getName(hoveredName); + const documentOffset = document.offsetAt({ line, character }); - // Find out which symbol in `symbols` is the hovered one - // and remove the ones after it, since they are not relevant - let currentOffset = 0; - for (let i = 0; i < symbols.length; i++) { - if (currentOffset > hoveredOffset) { - symbols = symbols.slice(0, i + 1); - break; + const hoveredName = this.findNodeAtPosition( + document.tree.rootNode, + documentOffset, + (node) => node.type === "name", + ); + + let symbols: string[] | undefined; + let startNode: Parser.SyntaxNode | undefined; + if (hoveredName) { + symbols = TreeSitterUtil.getName(hoveredName); + + // Find out which symbol in `symbols` is the hovered one + // and remove the ones after it, since they are not relevant + // TODO: this doesn't actually do anything at the moment. + const hoveredOffset = character - hoveredName.startPosition.column; + let currentOffset = 0; + for (let i = 0; i < symbols.length; i++) { + if (currentOffset > hoveredOffset) { + symbols = symbols.slice(0, i); + break; + } + + currentOffset += symbols[i].length; } - currentOffset += symbols[i].length; + startNode = this.findNodeAtPosition( + hoveredName, + documentOffset, + (node) => node.type === "IDENT", + ); + } else { + startNode = this.findNodeAtPosition( + document.tree.rootNode, + documentOffset, + (node) => node.type === "IDENT", + ); + symbols = startNode ? [startNode.text] : undefined; } - const hoveredIdentifier = this.findNodeAtPosition( - hoveredName, - line, - character, - node => node.type == "IDENT" - ); - if (!hoveredIdentifier) { + if (!startNode || !symbols) { + logger.info(`Tried to find declaration in '${uri}', but not hovering on any identifiers`); return null; } - return await this.#project.getDocument(uri)?.resolveLocally(symbols, hoveredIdentifier) ?? null; + logger.debug( + `Searching for declaration '${symbols.join(".")} at ${line + 1}:${character + 1} in '${uri}'`, + ); + const result = await document.resolveLocally(symbols, startNode); + if (result) { + logger.debug(`Found declaration: `, result); + } else { + logger.debug("Didn't find declaration"); + } + return result; } + /** + * Locates the first node at the given text position that matches the given + * `condition`, starting from the `rootNode`. + * + * Note: it is very important to have some kind of condition. If one tries to + * just accept the first node at that position, this function will always + * return the `rootNode` (or `undefined` if outside the node.) + * + * @param rootNode node to start searching from. parents/siblings of this node will be ignored + * @param offset the offset of the symbol from the start of the document + * @param condition the condition to check if a node is good + * @returns the node at the position, or `undefined` if none was found + */ private findNodeAtPosition( rootNode: Parser.SyntaxNode, - line: number, - character: number, - condition?: (node: Parser.SyntaxNode) => boolean, + offset: number, + condition: (node: Parser.SyntaxNode) => boolean, ): Parser.SyntaxNode | undefined { - let hoveredNode: Parser.SyntaxNode | undefined = undefined; - TreeSitterUtil.forEach(rootNode, (node) => { - if (hoveredNode) { - return false; - } - - const startPos = node.startPosition; - const endPos = node.endPosition; - const isInNode = (line > startPos.row && line < endPos.row) - || (line >= startPos.row && character >= startPos.column && character <= startPos.column) - || (line >= endPos.row && character >= endPos.column && character <= endPos.column); - - if (!condition || condition(node)) { - hoveredNode = node; - } - - return isInNode; + // TODO: find the deepest node. findFirst doesn't work (maybe?) + const hoveredNode = TreeSitterUtil.findFirst(rootNode, (node) => { + const isInNode = offset >= node.startIndex && offset <= node.endIndex; + return isInNode && condition(node); }); - return hoveredNode; + return hoveredNode ?? undefined; } } diff --git a/server/src/project/document.ts b/server/src/project/document.ts index 8b93c3e..86d2947 100644 --- a/server/src/project/document.ts +++ b/server/src/project/document.ts @@ -38,6 +38,7 @@ 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 * as TreeSitterUtil from "../util/tree-sitter"; @@ -51,17 +52,16 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { readonly #document: TextDocument; #tree: Parser.Tree; - private constructor( - library: ModelicaLibrary, - document: TextDocument, - tree: Parser.Tree, - ) { + private constructor(library: ModelicaLibrary, document: TextDocument, tree: Parser.Tree) { this.#library = library; this.#document = document; this.#tree = tree; } - public static async load(library: ModelicaLibrary, uri: LSP.DocumentUri): Promise { + public static async load( + library: ModelicaLibrary, + uri: LSP.DocumentUri, + ): Promise { logger.debug(`Loading document at '${uri}'...`); const content = await fs.readFile(url.fileURLToPath(uri), "utf-8"); @@ -69,11 +69,7 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { // 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(uri, "modelica", 0, content), - tree, - ); + return new ModelicaDocument(library, TextDocument.create(uri, "modelica", 0, content), tree); } public async update(text: string, range?: LSP.Range): Promise { @@ -122,15 +118,22 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { }, this.#tree); } - public async resolve(reference: string[]): Promise { + public async resolve(reference: string[]): Promise { + logger.debug(`Searching for reference '${reference.join(".")}' in document '${this.uri}'`); + + // make the reference relative to the root of this file. + reference = reference.slice(this.packagePath.length - 1); let foundSymbol: Parser.SyntaxNode | null = null; TreeSitterUtil.forEach(this.#tree.rootNode, (node: Parser.SyntaxNode) => { if (foundSymbol) { return false; } - const className = TreeSitterUtil.getIdentifier(node); - if (node.type == "class_definition" && className == reference[0]) { + if (node.type !== "class_definition") { + return false; + } + + if (TreeSitterUtil.getDeclaredIdentifiers(node).includes(reference[0])) { reference = reference.slice(1); if (reference.length == 0) { foundSymbol = node; @@ -146,79 +149,102 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { return null; } - return TreeSitterUtil.getSymbolInformation(this.uri, LSP.SymbolKind.Class, foundSymbol); + const info = TreeSitterUtil.createLocationLink(this.uri, foundSymbol); + logger.debug("Resolved reference:", info); + return info; } public async resolveLocally( reference: string[], node: Parser.SyntaxNode, - ): Promise { - // Bottom up traversal: - // if there is an import statement in the current scope: - // call `resolve` on the current ModelicaProject (or library?), - // `resolve([..importSymbols, ..reference])` - // else if there is a variable declaration with the name reference[0]: - // if reference.length is just 1: - // return the position of the variable declaration - // else: - // find the type of the local. - // return the position of the field declaration in that type - // else if there is a local class with the name reference[0]: - // if reference.length is just 1: - // return the position of the class declaration - // else: - // return the position of the declaration in that class - // else: - // if node.parentNode == null: - // call `resolve` on the current ModelicaProject (or library?) - // else: - // node = node.parentNode + classDepth: number = 0, + ): Promise { + const symbol = reference.length > 1 ? reference[reference.length - classDepth] : reference[0]; + const variableTypes = ["component_clause", "component_redeclaration", "named_element"]; + const local = node.children + .filter((child) => variableTypes.includes(child.type)) + .map((decl) => [decl, TreeSitterUtil.getDeclaredIdentifiers(decl)] as const) + .find(([_decl, idents]) => idents.includes(symbol)); + if (local) { + logger.debug(`Resolved ${reference.join(".")} to local: ${local[1]}`); + + return TreeSitterUtil.createLocationLink(this.uri, local[0]); + } + + const classDefinition = node.children + .filter((child) => child.type === "class_definition") + .map((classDef) => [classDef, TreeSitterUtil.getDeclaredIdentifiers(classDef)] as const) + .find(([_def, idents]) => idents.includes(symbol)); + if (classDefinition) { + logger.debug(`Resolved ${reference.join(".")} to class: ${classDefinition[1]}`); + + return TreeSitterUtil.createLocationLink(this.uri, classDefinition[0]); + } + + // Check for any elements declared by a class. + if (node.type === "class_definition") { + const elementListTypes = ["element_list", "public_element_list", "protected_element_list"]; + const element = node + .childForFieldName("classSpecifier") + ?.children?.filter((child) => elementListTypes.includes(child.type)) + ?.flatMap((element_list) => element_list.namedChildren) + ?.map((element) => [element, TreeSitterUtil.getDeclaredIdentifiers(element)] as const) + ?.find(([_element, idents]) => idents.includes(symbol)); + + if (element) { + logger.debug(`Resolved ${reference.join(".")} to element: ${element[1]}`); + + return TreeSitterUtil.createLocationLink(this.uri, element[0]); + } + } + const importClauses = node.parent?.children.filter( - (sibling) => sibling.type == "import_clause", + (sibling) => sibling.type === "import_clause", ); if (importClauses && importClauses.length > 0) { for (const importClause of importClauses) { const importedSymbol = await this.resolveImportClause(reference, importClause); if (importedSymbol !== undefined) { - logger.debug( - `resolved ${reference} to import: ${JSON.stringify(importedSymbol, undefined, 4)}`, - ); + logger.debug(`Resolved ${reference.join(".")} to import: ${importClause}`); return importedSymbol; } } } - const local = node.parent?.children - .filter((sibling) => sibling.type == "component_clause") - .find((decl) => TreeSitterUtil.getIdentifier(decl) === reference[0]); - if (local) { - logger.debug(`found local: ${JSON.stringify(local, undefined, 4)}`); - - return TreeSitterUtil.getSymbolInformation(this.uri, LSP.SymbolKind.Variable, local); - } - - const classDefinition = node.parent?.children - .filter((sibling) => sibling.type == "class_definition") - .find((def) => TreeSitterUtil.getIdentifier(def) === reference[0]); - if (classDefinition) { - logger.debug(`found class: ${JSON.stringify(classDefinition, undefined, 4)}`); - - return TreeSitterUtil.getSymbolInformation(this.uri, LSP.SymbolKind.Class, classDefinition); - } + if (node.parent) { + if (node.type === "class_definition") { + classDepth++; + } - if (!node.parent) { - // call `resolve` on the current ModelicaProject (or library?) - return await this.project.resolve(reference); + //logger.debug(`Reference ${reference.join(".")} not at current node; checking parent node`); + return await this.resolveLocally(reference, node.parent, classDepth); } - - return await this.resolveLocally(reference, node.parent!); + // TODO: check for relative symbols. Example: + // + // within Foo; + // + // package Bar + // class Baz + // end Baz; + // end Bar; + // + // class Test + // Foo.Bar.Baz baz1; // absolute symbol + // Bar.Baz baz2; // relative symbol -- still valid! + // end Test; + // + // TODO: also make sure to handle encapsulated packages correctly. + + // call `resolve` on the current ModelicaProject (or library?) + logger.debug(`Reference '${reference.join(".")}' not in document; is this a global?`); + return await this.project.resolve(reference); } private async resolveImportClause( reference: string[], importClause: Parser.SyntaxNode, - ): Promise { + ): Promise { const importedSymbol = TreeSitterUtil.getName(importClause.childForFieldName("name")!); // wildcard import: import a.b.*; @@ -287,6 +313,18 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { return this.#document.lineCount; } + 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; + } + public get project(): ModelicaProject { return this.#library.project; } diff --git a/server/src/project/library.ts b/server/src/project/library.ts index 6ac0499..6026fb0 100644 --- a/server/src/project/library.ts +++ b/server/src/project/library.ts @@ -77,7 +77,6 @@ export class ModelicaLibrary implements ModelicaScope { // the node pathToFileURL uses ':' anyways. Manually fix this here. // This is a bit hacky but we should ideally only be working with the URIs from LSP anyways. documentUri = documentUri.slice(0, 5) + documentUri.slice(5).replace(":", "%3A"); - logger.info(`uri`, documentUri); const document = await ModelicaDocument.load(library, documentUri); library.#documents.set(documentUri, document); @@ -87,38 +86,55 @@ export class ModelicaLibrary implements ModelicaScope { return library; } - public async resolve(reference: string[]): Promise { + public async resolve(reference: string[]): Promise { + logger.debug(`searching for reference '${reference.join('.')}' in library '${this.name}'.`); + logger.debug(`Base dir: ${this.path}`); + if (this.#documents.size === 0) { + logger.debug(`No documents in library; giving up`); return null; } - let bestDocument: ModelicaDocument; + let bestDocuments: ModelicaDocument[] = []; let bestPathLength = -1; for (const entry of this.#documents) { const [_uri, document] = entry; - const directories = path.relative(this.path, document.path).split(path.sep); - const fileName = directories.pop()!; - - let packagePath: string[]; - if (fileName === "package.mo") { - packagePath = directories; - } else if (fileName.endsWith(".mo")) { - packagePath = [...directories, fileName.slice(0, fileName.length - ".mo".length)]; - } else { - continue; - } - - // TODO: this won't work because in the case of workspaces, the actual package might be in a subdirectory - // Perhaps we should just add the concept of a "library root" that is searched for libraries. - // That might be unnecessary though. + const packagePath = document.packagePath; + + // TODO: the package path should be relative to the root package.mo file + // but in the case of workspaces, it doesn't have to be. This ruins the + // algorithm we use here. + // + // Since a workspace can technically store many libraries, we need to treat + // them differently. A workspace should be considered to be a "library root" + // that can contain multiple libraries. We should scan the workspace for + // libraries upon creating it, and when adding files to the workspace, + // we should figure out which library it belongs to. + // TODO: how do we handle the case in which a file belongs to no libraries? + + logger.debug(`package: ${packagePath}\t\treference: ${reference}`); const pathLength = miscUtil.getOverlappingLength(packagePath, reference); if (pathLength > bestPathLength) { - bestDocument = document; + bestDocuments = [document]; bestPathLength = pathLength; + } else if (pathLength === bestPathLength) { + bestDocuments.push(document); + } + } + + // logger.debug(`Chose these documents as the best matches:`); + // for (const document of bestDocuments) { + // logger.debug(` - ${document.uri}`); + // } + + for (const document of bestDocuments) { + const result = await document.resolve(reference); + if (result) { + return result; } } - return await bestDocument!.resolve(reference); + return null; } public get name(): string { diff --git a/server/src/project/project.ts b/server/src/project/project.ts index 8a6fc5d..09b39fd 100644 --- a/server/src/project/project.ts +++ b/server/src/project/project.ts @@ -101,7 +101,7 @@ export class ModelicaProject implements ModelicaScope { } } - throw Error(`Failed to add document '${uri}': not a part of any libraries.`); + throw new Error(`Failed to add document '${uri}': not a part of any libraries.`); } /** @@ -114,8 +114,12 @@ export class ModelicaProject implements ModelicaScope { logger.debug(`Updating document at '${uri}'...`); const doc = this.getDocument(uri); - doc?.update(text, range); - logger.debug(`Updated document: ${uri}`); + if (doc) { + doc.update(text, range); + logger.debug(`Updated document '${uri}'`); + } else { + logger.warn(`Failed to update document '${uri}': not loaded`); + } } /** @@ -125,13 +129,19 @@ export class ModelicaProject implements ModelicaScope { logger.info(`Removing document at '${uri}'...`); const doc = this.getDocument(uri); - doc?.library.documents.delete(uri); + if (doc) { + doc.library.documents.delete(uri); + } else { + logger.warn(`Failed to remove document '${uri}': not loaded`); + } } - public async resolve(reference: string[]): Promise { + public async resolve(reference: string[]): Promise { + logger.debug(`searching for reference '${reference.join('.')}' globally.`); + for (const library of this.libraries) { if (reference[0] === library.name) { - return await library.resolve(reference.slice(1)); + return await library.resolve(reference); } } @@ -141,6 +151,7 @@ export class ModelicaProject implements ModelicaScope { // TODO: check... array subscripts? can probably skip that + logger.debug(`Reference '${reference.join('.')}' not found in project.`); return null; } diff --git a/server/src/project/scope.ts b/server/src/project/scope.ts index 145d563..2b6c7d0 100644 --- a/server/src/project/scope.ts +++ b/server/src/project/scope.ts @@ -49,5 +49,5 @@ export interface ModelicaScope { * @param reference a symbol name, relative to the scope * @returns the symbol declaration, or null if not found in this scope. */ - resolve(reference: string[]): Promise; + resolve(reference: string[]): Promise; } diff --git a/server/src/server.ts b/server/src/server.ts index d84c58b..1cf33dd 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -101,6 +101,7 @@ export class ModelicaServer { colorProvider: false, completionProvider: undefined, declarationProvider: true, + definitionProvider: true, documentSymbolProvider: true, hoverProvider: false, signatureHelpProvider: undefined, @@ -125,6 +126,7 @@ export class ModelicaServer { connection.onInitialized(this.onInitialized.bind(this)); connection.onDidChangeWatchedFiles(this.onDidChangeWatchedFiles.bind(this)); connection.onDeclaration(this.onDeclaration.bind(this)); + connection.onDefinition(this.onDefinition.bind(this)); // The content of a text document has changed. This event is emitted // when the text document first opened or when its content has changed. @@ -178,7 +180,7 @@ export class ModelicaServer { case LSP.FileChangeType.Changed: { // TODO: incremental? const path = url.fileURLToPath(change.uri); - const content = await fs.readFile(path, 'utf-8'); + const content = await fs.readFile(path, "utf-8"); this.analyzer.updateDocument(change.uri, content); break; } @@ -190,16 +192,67 @@ export class ModelicaServer { } } - private async onDeclaration(params: LSP.DeclarationParams): Promise { + // TODO: We currently treat goto declaration and goto definition the same, + // but there are probably some differences we need to handle. + // + // 1. inner/outer variables. Modelica allows the user to redeclare variables + // from enclosing classes to use them in inner classes. Goto Declaration + // should go to whichever declaration is in scope, while Goto Definition + // should go to the `outer` declaration. In the following example: + // + // model Outer + // model Inner + // inner Real shared; + // equation + // shared = ...; (A) + // end Inner; + // outer Real shared = 0; + // equation + // shared = ...; (B) + // end Outer; + // + // +-----+-------------+------------+ + // | Ref | Declaration | Definition | + // +-----+-------------+------------+ + // | A | inner | outer | + // | B | outer | outer | + // +-----+-------------+------------+ + // + // 2. extends_clause is weird. This is a valid class: + // + // class extends Foo; + // end Foo; + // + // Is this a definition of Foo or a redeclaration of Foo? + + private async onDeclaration(params: LSP.DeclarationParams): Promise { logger.debug("onDeclaration"); - const symbolInformation = await this.analyzer.findDeclarationFromPosition( + const locationLink = await this.analyzer.findDeclarationFromPosition( params.textDocument.uri, params.position.line, params.position.character, ); + if (locationLink == null) { + return []; + } + + return [locationLink]; + } + + private async onDefinition(params: LSP.DefinitionParams): Promise { + logger.debug("onDefinition"); + + const locationLink = await this.analyzer.findDeclarationFromPosition( + params.textDocument.uri, + params.position.line, + params.position.character, + ); + if (locationLink == null) { + return []; + } - return symbolInformation?.location ?? undefined; + return [locationLink]; } private onDocumentSymbol(params: LSP.DocumentSymbolParams): LSP.SymbolInformation[] { diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index 4536443..66ac46e 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -153,9 +153,68 @@ export function getIdentifier(start: SyntaxNode): string | undefined { } /** - * - * @param nameNode - * @returns + * Returns the identifier(s) declared by the given node, or `[]` if no + * identifiers are declared. + * + * Note: this does not return any identifiers that are declared "inside" of the + * node. For instance, calling `getDeclaredIdentifiers` on a class_definition + * will only return the name of the class. + * + * @param node The node to check. Must be a declaration. + * @returns The identifiers. + */ +export function getDeclaredIdentifiers(node: SyntaxNode): string[] { + if (node == null) { + throw new Error("getDeclaredIdentifiers called with null/undefined node"); + } + + // TODO: does this support all desired node types? Are we considering too many nodes? + switch (node.type) { + case "declaration": + case "derivative_class_specifier": + case "enumeration_class_specifier": + case "extends_class_specifier": + case "long_class_specifier": + case "short_class_specifier": + case "enumeration_literal": + case "for_index": + return [node.childForFieldName("identifier")!.text]; + case "stored_definitions": + case "component_list": + case "enum_list": + case "element_list": + case "public_element_list": + case "protected_element_list": + case "for_indices": + return node.namedChildren.flatMap(getDeclaredIdentifiers); + case "component_clause": + return getDeclaredIdentifiers(node.childForFieldName("componentDeclarations")!); + case "component_declaration": + return getDeclaredIdentifiers(node.childForFieldName("declaration")!); + case "component_redeclaration": + return getDeclaredIdentifiers(node.childForFieldName("componentClause")!); + case "stored_definition": + return getDeclaredIdentifiers(node.childForFieldName("classDefinition")!); + case "class_definition": + return getDeclaredIdentifiers(node.childForFieldName("classSpecifier")!); + case "for_equation": + case "for_statement": + return getDeclaredIdentifiers(node.childForFieldName("indices")!); + case "named_element": { + const definition = + node.childForFieldName("classDefinition") ?? node.childForFieldName("componentClause")!; + return getDeclaredIdentifiers(definition); + } + default: + logger.warn(`getDeclaredIdentifiers: unknown node type ${node.type}`); + return []; + } +} + +/** + * + * @param nameNode + * @returns */ export function getName(nameNode: SyntaxNode): string[] { if (nameNode.type !== "name") { @@ -195,22 +254,24 @@ export function positionToPoint(position: LSP.Position): Parser.Point { return { row: position.line, column: position.character }; } -export function getSymbolInformation(documentUri: LSP.DocumentUri, kind: LSP.SymbolKind, node: Parser.SyntaxNode): LSP.SymbolInformation { +export function pointToPosition(point: Parser.Point): LSP.Position { + return { line: point.row, character: point.column }; +} + +export function createLocationLink( + documentUri: LSP.DocumentUri, + node: Parser.SyntaxNode, +): LSP.LocationLink { + // TODO: properly set targetSelectionRange (e.g. the name of a function or variable). return { - name: getIdentifier(node) ?? "", - kind, - location: { - uri: documentUri, - range: { - start: { - line: node.startPosition.row, - character: node.startPosition.column, - }, - end: { - line: node.endPosition.row, - character: node.endPosition.column, - }, - } - } + targetUri: documentUri, + targetRange: { + start: pointToPosition(node.startPosition), + end: pointToPosition(node.endPosition), + }, + targetSelectionRange: { + start: pointToPosition(node.startPosition), + end: pointToPosition(node.endPosition), + }, }; -} \ No newline at end of file +} From 8795ed11e512430cd85369b78c335cb9ae4873d4 Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Tue, 30 Apr 2024 10:27:05 +0200 Subject: [PATCH 11/24] Minor tweaks to resolve algorithm Co-authored-by: PaddiM8 --- server/src/analyzer.ts | 34 +++----- server/src/project/document.ts | 153 +++++++++++++++++++++++++-------- server/src/project/library.ts | 4 +- server/src/project/project.ts | 4 +- server/src/project/scope.ts | 29 ++++++- server/src/server.ts | 7 +- server/src/util/tree-sitter.ts | 51 ++++++++--- 7 files changed, 208 insertions(+), 74 deletions(-) diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index a12dfd9..39b771c 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -121,21 +121,13 @@ export default class Analyzer { let symbols: string[] | undefined; let startNode: Parser.SyntaxNode | undefined; if (hoveredName) { - symbols = TreeSitterUtil.getName(hoveredName); - - // Find out which symbol in `symbols` is the hovered one - // and remove the ones after it, since they are not relevant - // TODO: this doesn't actually do anything at the moment. - const hoveredOffset = character - hoveredName.startPosition.column; - let currentOffset = 0; - for (let i = 0; i < symbols.length; i++) { - if (currentOffset > hoveredOffset) { - symbols = symbols.slice(0, i); - break; - } - - currentOffset += symbols[i].length; - } + symbols = TreeSitterUtil.getNameIdentifiers(hoveredName) + .filter( + (node) => + node.startPosition.row < line || + (node.startPosition.row === line && node.startPosition.column <= character), + ) + .map((node) => node.text); startNode = this.findNodeAtPosition( hoveredName, @@ -160,12 +152,14 @@ export default class Analyzer { `Searching for declaration '${symbols.join(".")} at ${line + 1}:${character + 1} in '${uri}'`, ); const result = await document.resolveLocally(symbols, startNode); - if (result) { - logger.debug(`Found declaration: `, result); - } else { - logger.debug("Didn't find declaration"); + if (!result) { + logger.debug(`Didn't find declaration of ${symbols.join(".")}`); + return null; } - return result; + + const link = TreeSitterUtil.createLocationLink(result.documentUri, result.node); + logger.debug(`Found declaration of ${symbols.join(".")}: `, link); + return link; } /** diff --git a/server/src/project/document.ts b/server/src/project/document.ts index 86d2947..a2da47c 100644 --- a/server/src/project/document.ts +++ b/server/src/project/document.ts @@ -45,7 +45,7 @@ import * as TreeSitterUtil from "../util/tree-sitter"; import { positionToPoint } from "../util/tree-sitter"; import { ModelicaLibrary } from "./library"; import { ModelicaProject } from "./project"; -import { ModelicaScope } from "./scope"; +import { ModelicaScope, ResolvedSymbol } from "./scope"; export class ModelicaDocument implements ModelicaScope, TextDocument { readonly #library: ModelicaLibrary; @@ -118,13 +118,14 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { }, this.#tree); } - public async resolve(reference: string[]): Promise { + public async resolve(reference: string[]): Promise { logger.debug(`Searching for reference '${reference.join(".")}' in document '${this.uri}'`); // make the reference relative to the root of this file. + // TODO: this won't handle constants (+other static fields?) properly reference = reference.slice(this.packagePath.length - 1); let foundSymbol: Parser.SyntaxNode | null = null; - TreeSitterUtil.forEach(this.#tree.rootNode, (node: Parser.SyntaxNode) => { + await TreeSitterUtil.forEach(this.#tree.rootNode, async (node: Parser.SyntaxNode) => { if (foundSymbol) { return false; } @@ -142,6 +143,12 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { return true; } + const decl = await this.findDeclarationInClass(node, reference[0]); + if (decl) { + foundSymbol = decl.node; + return true; + } + return false; }); @@ -149,55 +156,79 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { return null; } - const info = TreeSitterUtil.createLocationLink(this.uri, foundSymbol); - logger.debug("Resolved reference:", info); - return info; + logger.debug("Resolved reference:", foundSymbol); + return { + node: foundSymbol, + documentUri: this.uri, + symbol: TreeSitterUtil.getIdentifier(foundSymbol)!, + }; } public async resolveLocally( reference: string[], node: Parser.SyntaxNode, - classDepth: number = 0, - ): Promise { - const symbol = reference.length > 1 ? reference[reference.length - classDepth] : reference[0]; + ): Promise { + // Check if the referenced symbol is relative to a class that is in scope. + // If so, we need to resolve it top-down instead. + // const localClass = node.children + // .find((child) => + // child.type === "class_definition" && + // TreeSitterUtil.getIdentifier(child) == reference[0] + // ); + // if (reference.length > 1 && localClass) { + // return await this.resolve([...this.packagePath, ...reference]); + // } + + // TODO: what's the point of this? If reference.at(1 - classDepth) isn't the last symbol + // then we'll return early and won't fully resolve the reference. + //const symbol = reference.length > 1 ? reference[reference.length - classDepth] : reference[0]; + const symbol = reference[0]; + + // Check if this symbol refers to a local variable. const variableTypes = ["component_clause", "component_redeclaration", "named_element"]; const local = node.children .filter((child) => variableTypes.includes(child.type)) .map((decl) => [decl, TreeSitterUtil.getDeclaredIdentifiers(decl)] as const) .find(([_decl, idents]) => idents.includes(symbol)); - if (local) { + if (reference.length === 1 && local) { logger.debug(`Resolved ${reference.join(".")} to local: ${local[1]}`); - return TreeSitterUtil.createLocationLink(this.uri, local[0]); + return { + node: local[0], + documentUri: this.uri, + symbol, + }; } + // Check if the symbol refers to a local class. const classDefinition = node.children .filter((child) => child.type === "class_definition") .map((classDef) => [classDef, TreeSitterUtil.getDeclaredIdentifiers(classDef)] as const) .find(([_def, idents]) => idents.includes(symbol)); if (classDefinition) { logger.debug(`Resolved ${reference.join(".")} to class: ${classDefinition[1]}`); + if (reference.length === 1) { + return { + node: classDefinition[0], + documentUri: this.uri, + symbol, + }; + } - return TreeSitterUtil.createLocationLink(this.uri, classDefinition[0]); + // TODO: Use an absolute reference [...this.packagePath, ...localAncestors, ...reference] + // or use some other function + return await this.resolve(reference); } // Check for any elements declared by a class. if (node.type === "class_definition") { - const elementListTypes = ["element_list", "public_element_list", "protected_element_list"]; - const element = node - .childForFieldName("classSpecifier") - ?.children?.filter((child) => elementListTypes.includes(child.type)) - ?.flatMap((element_list) => element_list.namedChildren) - ?.map((element) => [element, TreeSitterUtil.getDeclaredIdentifiers(element)] as const) - ?.find(([_element, idents]) => idents.includes(symbol)); - - if (element) { - logger.debug(`Resolved ${reference.join(".")} to element: ${element[1]}`); - - return TreeSitterUtil.createLocationLink(this.uri, element[0]); + const decl = await this.findDeclarationInClass(node, symbol); + if (decl) { + return decl; } } + // Check if this symbol refers to an import---either a direct import or a wildcard import. const importClauses = node.parent?.children.filter( (sibling) => sibling.type === "import_clause", ); @@ -211,40 +242,88 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { } } - if (node.parent) { - if (node.type === "class_definition") { - classDepth++; - } - //logger.debug(`Reference ${reference.join(".")} not at current node; checking parent node`); - return await this.resolveLocally(reference, node.parent, classDepth); + return await this.resolveLocally(reference, node.parent); } // TODO: check for relative symbols. Example: // // within Foo; - // + // // package Bar // class Baz // end Baz; // end Bar; // - // class Test + // class Test // Foo.Bar.Baz baz1; // absolute symbol // Bar.Baz baz2; // relative symbol -- still valid! // end Test; // - // TODO: also make sure to handle encapsulated packages correctly. + // TODO: also make sure to handle encapsulated packages correctly. + + // This must be some kind of relative reference. Check all possible relative imports + the absolute import. + logger.debug(`Reference '${reference.join(".")}' not in document; checking references from other packages`); + + // TODO: packagePath isn't correct due to the resolveLocally(node.parent, ...) stuff above. + // We need the class path at the location specified by the original node. + const packagePath = this.packagePath; + for (let i = packagePath.length; i >= 0; i--) { + + const fullPath = packagePath.slice(i).concat(reference); + const result = await this.project.resolve(fullPath); + if (result) { + return result; + } + } - // call `resolve` on the current ModelicaProject (or library?) - logger.debug(`Reference '${reference.join(".")}' not in document; is this a global?`); - return await this.project.resolve(reference); + // We couldn't resolve the reference. + return null; + } + + private async findDeclarationInClass(classNode: Parser.SyntaxNode, symbol: string): Promise { + const elementListTypes = ["element_list", "public_element_list", "protected_element_list"]; + const elements = classNode + .childForFieldName("classSpecifier") + ?.children?.filter((child) => elementListTypes.includes(child.type)) + ?.flatMap((element_list) => element_list.namedChildren) + ?.map((element) => [element, TreeSitterUtil.getDeclaredIdentifiers(element)] as const); + + const field = elements?.find(([element, idents]) => element.type === "named_element" && idents.includes(symbol)); + if (field) { + logger.debug(`Resolved ${symbol} to field: ${field[1]}`); + + // TODO: this handles named_elements but what if it's an import clause? + return { + symbol, + node: field[0], + documentUri: this.uri, + }; + } + + const inheritedClassPromises = elements + ?.map((element) => element[0]) + ?.filter((element) => element.type === "extends") + .map((node) => this.library.resolve(TreeSitterUtil.getName(node))); + const inheritedClasses = await Promise.all(inheritedClassPromises ?? []); + for (const inheritedClass of inheritedClasses) { + if (!inheritedClass) { + continue; + } + + const decl = this.findDeclarationInClass(inheritedClass.node, symbol); + if (decl) { + return decl; + } + } + + return undefined; } private async resolveImportClause( reference: string[], importClause: Parser.SyntaxNode, - ): Promise { + ): Promise { const importedSymbol = TreeSitterUtil.getName(importClause.childForFieldName("name")!); // wildcard import: import a.b.*; diff --git a/server/src/project/library.ts b/server/src/project/library.ts index 6026fb0..4ce60a0 100644 --- a/server/src/project/library.ts +++ b/server/src/project/library.ts @@ -43,7 +43,7 @@ import * as miscUtil from "../util"; import logger from '../util/logger'; import { ModelicaDocument } from "./document"; import { ModelicaProject } from "./project"; -import { ModelicaScope } from "./scope"; +import { ModelicaScope, ResolvedSymbol } from "./scope"; export class ModelicaLibrary implements ModelicaScope { readonly #project: ModelicaProject; @@ -86,7 +86,7 @@ export class ModelicaLibrary implements ModelicaScope { return library; } - public async resolve(reference: string[]): Promise { + public async resolve(reference: string[]): Promise { logger.debug(`searching for reference '${reference.join('.')}' in library '${this.name}'.`); logger.debug(`Base dir: ${this.path}`); diff --git a/server/src/project/project.ts b/server/src/project/project.ts index 09b39fd..3c7d287 100644 --- a/server/src/project/project.ts +++ b/server/src/project/project.ts @@ -38,7 +38,7 @@ import * as LSP from "vscode-languageserver"; import url from "node:url"; import path from "node:path"; -import { ModelicaScope } from "./scope"; +import { ModelicaScope, ResolvedSymbol } from "./scope"; import { ModelicaLibrary } from "./library"; import { ModelicaDocument } from './document'; import * as util from '../util'; @@ -136,7 +136,7 @@ export class ModelicaProject implements ModelicaScope { } } - public async resolve(reference: string[]): Promise { + public async resolve(reference: string[]): Promise { logger.debug(`searching for reference '${reference.join('.')}' globally.`); for (const library of this.libraries) { diff --git a/server/src/project/scope.ts b/server/src/project/scope.ts index 2b6c7d0..94fb8c2 100644 --- a/server/src/project/scope.ts +++ b/server/src/project/scope.ts @@ -36,6 +36,33 @@ import * as LSP from "vscode-languageserver/node"; import { ModelicaProject } from "./project"; +import Parser from "web-tree-sitter"; + +export interface ResolvedSymbol { + /** + * The symbol being declared. + */ + symbol: string; + + /** + * The node that defines the symbol. + * + * For instance, in a declaration like: + * + * ```modelica + * Real a, b; + * ``` + * + * the `node` for the `ResolvedSymbol` for `a` would be the one + * representing the entire declaration of `a` and `b`. + */ + node: Parser.SyntaxNode; + + /** + * The document in which the symbol was defined. + */ + documentUri: LSP.DocumentUri; +} export interface ModelicaScope { /** @@ -49,5 +76,5 @@ export interface ModelicaScope { * @param reference a symbol name, relative to the scope * @returns the symbol declaration, or null if not found in this scope. */ - resolve(reference: string[]): Promise; + resolve(reference: string[]): Promise; } diff --git a/server/src/server.ts b/server/src/server.ts index 1cf33dd..bb84807 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -223,7 +223,12 @@ export class ModelicaServer { // class extends Foo; // end Foo; // - // Is this a definition of Foo or a redeclaration of Foo? + // What does this even mean? Is this a definition of Foo or a redeclaration of Foo? + // + // 3. Import aliases. Should this be considered to be a declaration of `Frobnicator`? + // + // import Frobnicator = Foo.Bar.Baz; + // private async onDeclaration(params: LSP.DeclarationParams): Promise { logger.debug("onDeclaration"); diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index 66ac46e..0acb5cc 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -45,16 +45,40 @@ import { SyntaxNode } from "web-tree-sitter"; import { logger } from "./logger"; +type MaybePromise = T | Promise; + /** * Recursively iterate over all nodes in a tree. * * @param node The node to start iterating from * @param callback The callback to call for each node. Return false to stop following children. */ -export function forEach(node: SyntaxNode, callback: (n: SyntaxNode) => void | boolean) { - const followChildren = callback(node) !== false; - if (followChildren && node.children.length) { - node.children.forEach((n) => forEach(n, callback)); +export function forEach(start: SyntaxNode, callback: (n: SyntaxNode) => void | boolean): void; +export function forEach( + start: SyntaxNode, + callback: (n: SyntaxNode) => Promise, +): Promise; +export function forEach( + start: SyntaxNode, + callback: (n: SyntaxNode) => MaybePromise, +): MaybePromise; +export function forEach( + start: SyntaxNode, + callback: (n: SyntaxNode) => MaybePromise, +): MaybePromise { + const callbackResult = callback(start); + if (typeof callbackResult === "object") { + return callbackResult.then(async (callbackResult) => { + const followChildren = callbackResult !== false; + if (followChildren && start.children.length) { + await Promise.all(start.children.map((n) => forEach(n, callback))); + } + }); + } + + const followChildren = callbackResult !== false; + if (followChildren && start.children.length) { + start.children.forEach((n) => forEach(n, callback)); } } @@ -212,22 +236,27 @@ export function getDeclaredIdentifiers(node: SyntaxNode): string[] { } /** - * - * @param nameNode - * @returns + * Converts a name `SyntaxNode` into an array of the `IDENT`s in that node. */ export function getName(nameNode: SyntaxNode): string[] { + return getNameIdentifiers(nameNode).map((identNode) => identNode.text); +} + +/** + * Converts a name `SyntaxNode` into an array of the `IDENT`s in that node. + */ +export function getNameIdentifiers(nameNode: SyntaxNode): Parser.SyntaxNode[] { if (nameNode.type !== "name") { throw new Error(`Expected a 'name' node; got '${nameNode.type}'`); } - const ident = nameNode.childForFieldName("identifier")!.text; + const identNode = nameNode.childForFieldName("identifier")!; const qualifierNode = nameNode.childForFieldName("qualifier"); if (qualifierNode) { - const qualifier = getName(qualifierNode); - return [...qualifier, ident]; + const qualifier = getNameIdentifiers(qualifierNode); + return [...qualifier, identNode]; } else { - return [ident]; + return [identNode]; } } From 30cbf1d7248a4d5ad0ea2eafad39ed8af67caca9 Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Wed, 1 May 2024 12:09:33 +0200 Subject: [PATCH 12/24] Add new resolve algorithm Co-authored-by: PaddiM8 --- server/src/analysis/reference.ts | 95 ++++++ server/src/analysis/resolveReference.ts | 410 ++++++++++++++++++++++++ server/src/analyzer.ts | 24 +- server/src/project/document.ts | 246 +------------- server/src/project/index.ts | 3 + server/src/project/library.ts | 55 +--- server/src/project/project.ts | 23 +- server/src/project/scope.ts | 80 ----- server/src/util/tree-sitter.ts | 61 +++- 9 files changed, 584 insertions(+), 413 deletions(-) create mode 100644 server/src/analysis/reference.ts create mode 100644 server/src/analysis/resolveReference.ts create mode 100644 server/src/project/index.ts delete mode 100644 server/src/project/scope.ts diff --git a/server/src/analysis/reference.ts b/server/src/analysis/reference.ts new file mode 100644 index 0000000..1a5b107 --- /dev/null +++ b/server/src/analysis/reference.ts @@ -0,0 +1,95 @@ +import { ModelicaDocument } from "../project/document"; +import Parser from "web-tree-sitter"; + +export abstract class BaseUnresolvedReference { + /** + * The path to the symbol reference. + */ + public readonly symbols: string[]; + + public constructor(symbols: string[]) { + if (symbols.length === 0) { + throw new Error("Symbols length must be greater tham 0"); + } + + this.symbols = symbols; + } + + public abstract isAbsolute(): this is UnresolvedAbsoluteReference; +} + +export class UnresolvedRelativeReference extends BaseUnresolvedReference { + /** + * The document that contains the `node`. + */ + public readonly document: ModelicaDocument; + + /** + * A `SyntaxNode` in which the symbol is in scope. + */ + public readonly node: Parser.SyntaxNode; + + public constructor(document: ModelicaDocument, node: Parser.SyntaxNode, symbols: string[]) { + super(symbols); + this.document = document; + this.node = node; + } + + public isAbsolute(): this is UnresolvedAbsoluteReference { + return false; + } + + public get [Symbol.toStringTag](): string { + const start = this.node.startPosition; + const pos = `${start.row + 1}:${start.column + 1}`; + + return `${this.symbols.join(".")} at ${pos} in "${this.document.uri}"`; + } +} + +export class UnresolvedAbsoluteReference extends BaseUnresolvedReference { + public constructor(symbols: string[]) { + super(symbols); + } + + public isAbsolute(): this is UnresolvedAbsoluteReference { + return true; + } + + public get [Symbol.toStringTag](): string { + return `.${this.symbols.join(".")}`; + } +} + +export type UnresolvedReference = UnresolvedRelativeReference | UnresolvedAbsoluteReference; + +export class ResolvedReference { + /** + * The document that contains the `node`. + */ + readonly document: ModelicaDocument; + + /** + * The node that declares/defines this symbol. + */ + readonly node: Parser.SyntaxNode; + + /** + * The full, absolute path to the symbol. + */ + readonly symbols: string[]; + + public constructor(document: ModelicaDocument, node: Parser.SyntaxNode, symbols: string[]) { + if (symbols.length === 0) { + throw new Error("Symbols length must be greater than 0."); + } + + this.document = document; + this.node = node; + this.symbols = symbols; + } + + public get [Symbol.toStringTag](): string { + return `.${this.symbols.join(".")}`; + } +} diff --git a/server/src/analysis/resolveReference.ts b/server/src/analysis/resolveReference.ts new file mode 100644 index 0000000..f0ee59e --- /dev/null +++ b/server/src/analysis/resolveReference.ts @@ -0,0 +1,410 @@ +import Parser from "web-tree-sitter"; +import * as path from "node:path"; +import * as fs from "node:fs"; + +import * as TreeSitterUtil from "../util/tree-sitter"; +import { + ResolvedReference, + UnresolvedAbsoluteReference, + UnresolvedReference, + UnresolvedRelativeReference, +} from "./reference"; +import logger from "../util/logger"; +import { ModelicaProject, ModelicaLibrary, ModelicaDocument } from "../project"; + +export type Resolution = "declaration" | "definition"; + +/** + * Locates the declaration or definition of a symbol reference. + * + * @param project the project + * @param reference a reference + * @param resolution the kind of symbol to search for + */ +export default function resolveReference( + project: ModelicaProject, + reference: UnresolvedReference, + resolution: Resolution, +): ResolvedReference | null { + logger.debug(`Resolving ${resolution} ${reference}`); + + if (resolution === "definition") { + throw new Error("Resolving definitions not yet supported!"); + } + + const absoluteReference = reference.isAbsolute() ? reference : absolutize(reference); + if (absoluteReference === null) { + return null; + } else if (absoluteReference instanceof ResolvedReference) { + return absoluteReference; + } + + return resolveAbsoluteReference(project, absoluteReference); +} + +/** + * Converts a relative reference to an absolute reference. + * + * @param reference a relative reference to a symbol declaration/definition + * @returns an absolute reference to that symbol, or `null` if no such symbol exists. + */ +function absolutize( + reference: UnresolvedRelativeReference, +): UnresolvedAbsoluteReference | ResolvedReference | null { + logger.debug(`Absolutize: ${reference}`); + const local = findReferenceInDocument(reference); + if (local === null) { + logger.debug(`Didn't find symbol ${reference}`); + return null; + } else if (local instanceof ResolvedReference) { + logger.debug(`Resolved reference ${local}`); + return local; + } else if (local instanceof UnresolvedAbsoluteReference) { + logger.debug(`Found absolute reference ${local}`); + return local; + } + + const ancestors: string[] = []; + let currentNode = local.node; + while (currentNode.parent) { + if (currentNode.type === "class_definition") { + const identifier = TreeSitterUtil.getDeclaredIdentifiers(currentNode).at(0); + if (identifier) { + ancestors.unshift(identifier); + } + } + + currentNode = currentNode.parent; + } + + logger.debug(`Found local: ${local} with ancestors: ${ancestors}`); + + return new UnresolvedAbsoluteReference([ + ...local.document.packagePath, + ...ancestors, + ...local.symbols, + ]); +} + +/** + * Locates the declaration/definition of a reference in its document, or finds a suitable absolute reference. + * + * @param reference a reference to a local in which the `document` and `node` properties reference + * the usage of the symbol. + * @returns either + * (1) a relative reference in which the `document` and `node` properties reference + * the symbol's declaration/definition, + * (2) an absolute reference + */ +function* findReferenceInDocument( + reference: UnresolvedRelativeReference, +): Generator { + // TODO: Function declarations + logger.warn("NOT checking for functions!"); + + logger.debug("Checking for local class or variable..."); + const decl = reference.node.children.find((child) => { + return ( + (TreeSitterUtil.isDefinition(child) || TreeSitterUtil.isVariableDeclaration(child)) && + TreeSitterUtil.hasIdentifier(child, reference.symbols[0]) + ); + }); + if (decl) { + logger.debug("Found local"); + return new UnresolvedRelativeReference(reference.document, decl, reference.symbols); + } + + logger.debug("Checking for declaration in class..."); + const declInClass = findDeclarationInClass(reference.document, reference.node, reference.symbols); + if (declInClass) { + return declInClass; + } + + const importClauses = reference.node.parent?.children.filter( + (child) => child.type === "import_clause", + ); + if (importClauses && importClauses.length > 0) { + logger.debug("Checking imports..."); + for (const importClause of importClauses) { + yield* resolveImportClause(reference.symbols, importClause); + } + } + + if (reference.node.parent) { + logger.debug("Checking parent node..."); + return findReferenceInDocument( + new UnresolvedRelativeReference(reference.document, reference.node.parent, reference.symbols), + ); + } + + // TODO: check subpackages + logger.warn("NOT checking subpackages!"); + + logger.debug("Not found in documment. This reference is either global or undefined."); + return new UnresolvedAbsoluteReference(reference.symbols); +} + +function findDeclarationInClass( + document: ModelicaDocument, + classNode: Parser.SyntaxNode, + symbols: string[], +): UnresolvedRelativeReference | undefined { + if (classNode.type !== "class_definition") { + return undefined; + } + + const elements = classNode + .childForFieldName("classSpecifier") + ?.children?.filter(TreeSitterUtil.isElementList) + ?.flatMap((element_list) => element_list.namedChildren) + ?.map((element) => [element, TreeSitterUtil.getDeclaredIdentifiers(element)] as const); + + if (!elements) { + return undefined; + } + + const field = elements.find( + ([element, idents]) => element.type === "named_element" && idents.includes(symbols[0]), + ); + if (field) { + logger.debug(`Resolved ${symbols[0]} to field: ${field[1]}`); + + // TODO: this handles named_elements but what if it's an import clause? + return new UnresolvedRelativeReference(document, field[0], symbols); + } + + const superclasses = elements + .map(([element, _idents]) => element) + .filter((element) => element.type === "extends") + .map((node) => { + const superclassRef = new UnresolvedAbsoluteReference(TreeSitterUtil.getName(node)); + logger.debug(`Resolving superclass ${superclassRef} (of ${TreeSitterUtil.getDeclaredIdentifiers(classNode)[0]})`); + + const resolvedSuperclassRef = resolveAbsoluteReference(document.project, superclassRef); + if (!resolvedSuperclassRef) { + logger.warn(`Could not find superclass ${superclassRef}`); + } + + return resolvedSuperclassRef; + }) + .filter((superclass) => superclass != null) as ResolvedReference[]; + + for (const superclass of superclasses) { + logger.debug(`Checking superclass ${superclass}`); + const decl = findDeclarationInClass(document, superclass.node, symbols); + if (decl) { + logger.debug(`Declaration ${decl} found in superclass ${superclasses}`); + return decl; + } + } + + return undefined; +} + +function* resolveImportClause( + symbols: string[], + importClause: Parser.SyntaxNode, +): Generator { + const importPath = TreeSitterUtil.getName(importClause.childForFieldName("name")!); + + // wildcard import: import a.b.*; + const isWildcard = importClause.childForFieldName("wildcard") != null; + if (isWildcard) { + const importCandidate = new UnresolvedAbsoluteReference([...importPath, ...symbols]); + logger.debug(`Candidate: ${importCandidate} (from import ${importPath.join(".")}.*)`); + + yield importCandidate; + } + + // import alias: import z = a.b.c; + // TODO: Determine if import aliases should be counted as "declarations". + // If so, then we should stop here for decls when symbols.length == 1. + const alias = importClause.childForFieldName("alias")?.text; + if (alias && alias === symbols[0]) { + const importCandidate = new UnresolvedAbsoluteReference([...importPath, ...symbols.slice(1)]); + logger.debug(`Candidate: ${importCandidate} (from import ${alias} = ${importPath.join(".")})`); + + return importCandidate; + } + + // multi-import: import a.b.{c, d, e}; + const childImports = importClause + .childForFieldName("imports") + ?.namedChildren?.filter((node) => node.type === "IDENT") + ?.map((node) => node.text); + + if (childImports?.some((name) => name === symbols[0])) { + const importCandidate = new UnresolvedAbsoluteReference([...importPath, ...symbols]); + const importString = `import ${importPath.join(".")}.{ ${childImports.join(", ")} }`; + logger.debug(`Candidate: ${importCandidate} (from ${importString})`); + + return importCandidate; + } + + // normal import: import a.b.c; + if (importPath.at(-1) === symbols[0]) { + const importCandidate = new UnresolvedAbsoluteReference([...importPath, ...symbols.slice(1)]); + logger.debug(`Candidate: ${importCandidate} (from import ${importPath.join(".")})`); + + return importCandidate; + } +} + +/** + * Locates the declaration/definition of an absolute symbol reference. + * + * @param reference an absolute reference + * @returns a resolved reference, or `null` if no such symbol exists + */ +function resolveAbsoluteReference( + project: ModelicaProject, + reference: UnresolvedAbsoluteReference, +): ResolvedReference | null { + logger.debug(`Resolving absolute reference ${reference}`); + + const library = project.libraries.find((lib) => lib.name === reference.symbols[0]); + if (library == null) { + logger.debug(`Couldn't find library: ${reference.symbols[0]}`); + return null; + } + + logger.debug(`Found library ${library.name}; performing resolution: `); + + let alreadyResolved: ResolvedReference | null = null; + for (let i = 0; i < reference.symbols.length; i++) { + alreadyResolved = resolveNext(library, reference, alreadyResolved); + if (alreadyResolved == null) { + return null; + } + + console.log(`Step ${i + 1}: ${alreadyResolved}`); + + // If we're not done with the reference chain, we need to make sure that we know + // the type of the variable in order to check its child variables + if ( + i < reference.symbols.length - 1 && + TreeSitterUtil.isVariableDeclaration(alreadyResolved.node) + ) { + const classRef = variableRefToClassRef(alreadyResolved); + if (classRef == null) { + logger.debug(`Failed to find type of var ${alreadyResolved}`); + return null; + } + + logger.debug(` => class: ${classRef}`); + alreadyResolved = classRef; + } + } + + logger.debug(`Resolved symbol ${alreadyResolved?.symbols} in ${alreadyResolved?.document.path}`); + + return alreadyResolved; +} + +/** + * Performs a single iteration of the resolution algorithm. + * + * @param reference the entire reference + * @param alreadyResolved a resolved reference (to a class) + * @returns the next resolved reference + */ +function resolveNext( + library: ModelicaLibrary, + reference: UnresolvedAbsoluteReference, + alreadyResolved: ResolvedReference | null, +): ResolvedReference | null { + // If at the root level, find the root package + if (!alreadyResolved) { + const documentPath = path.join(library.path, "package.mo"); + const [document, packageClass] = getPackageClassFromFilePath( + library, + documentPath, + reference.symbols[0], + ); + if (!document || !packageClass) { + return null; + } + + return new ResolvedReference(document, packageClass, reference.symbols); + } + + const nextSymbolIndex = alreadyResolved.symbols.length; + const nextSymbol = reference.symbols[nextSymbolIndex]; + + // If nextSymbol is in alreadyResolved.node: + // return the declaration + // TODO: Variable declarations may be nested inside an element list + const child = findDeclarationInClass( + alreadyResolved.document, + alreadyResolved.node, + reference.symbols, + ); + if (child) { + return new ResolvedReference(alreadyResolved!.document, child.node, reference.symbols); + } + + // If there is a document for nextSymbol blablabla + const potentialPaths = [ + path.join(alreadyResolved.document.path, `${nextSymbol}.mo`), + path.join(alreadyResolved.document.path, `${nextSymbol}/package.mo`), + ]; + for (const documentPath of potentialPaths) { + if (!fs.existsSync(documentPath)) { + continue; + } + + const [document, packageClass] = getPackageClassFromFilePath(library, documentPath, nextSymbol); + if (!document || !packageClass) { + return null; + } + + return new ResolvedReference(document, packageClass, reference.symbols); + } + + return null; +} + +function getPackageClassFromFilePath( + library: ModelicaLibrary, + filePath: string, + symbol: string, +): [ModelicaDocument | undefined, Parser.SyntaxNode | undefined] { + const document = library.documents.get(filePath); + if (!document) { + return [undefined, undefined]; + } + + const node = document.tree.rootNode.children.find((child) => + TreeSitterUtil.hasIdentifier(child, symbol), + ); + if (!node) { + return [document, undefined]; + } + + return [document, node]; +} + +/** + * Finds the type of a variable declaration and returns a reference to that type. + * + * @param varRef a reference to a variable declaration/definition + * @returns a reference to the class definition, or `null` if the type is not a class (e.g. a builtin like `Real`) + */ +function variableRefToClassRef(varRef: ResolvedReference): ResolvedReference | null { + const type = TreeSitterUtil.getDeclarationType(varRef.node); + + const absoluteReference = (() => { + if (type.global) { + return new UnresolvedAbsoluteReference(type.symbols); + } else { + const typeRef = new UnresolvedRelativeReference(varRef.document, varRef.node, type.symbols); + return absolutize(typeRef); + } + })(); + + if (absoluteReference === null) { + return null; + } + + return resolveAbsoluteReference(varRef.document.project, absoluteReference); +} diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 39b771c..7d6c754 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -40,18 +40,15 @@ */ import * as LSP from "vscode-languageserver/node"; -import { TextDocument } from "vscode-languageserver-textdocument"; - import Parser from "web-tree-sitter"; -import * as fs from "node:fs/promises"; -import * as path from "node:path"; -import * as url from "node:url"; -import { getAllDeclarationsInTree } from "./util/declarations"; -import { logger } from "./util/logger"; -import * as TreeSitterUtil from "./util/tree-sitter"; +import { UnresolvedRelativeReference } from "./analysis/reference"; +import resolveReference from "./analysis/resolveReference"; import { ModelicaProject } from "./project/project"; import { ModelicaLibrary } from "./project/library"; +import { getAllDeclarationsInTree } from "./util/declarations"; +import logger from "./util/logger"; +import * as TreeSitterUtil from "./util/tree-sitter"; export default class Analyzer { #project: ModelicaProject; @@ -149,15 +146,20 @@ export default class Analyzer { } logger.debug( - `Searching for declaration '${symbols.join(".")} at ${line + 1}:${character + 1} in '${uri}'`, + `Searching for declaration '${symbols.join(".")}' at ${line + 1}:${character + 1} in '${uri}'`, + ); + + const result = resolveReference( + document.project, + new UnresolvedRelativeReference(document, startNode, symbols), + "declaration", ); - const result = await document.resolveLocally(symbols, startNode); if (!result) { logger.debug(`Didn't find declaration of ${symbols.join(".")}`); return null; } - const link = TreeSitterUtil.createLocationLink(result.documentUri, result.node); + const link = TreeSitterUtil.createLocationLink(result.document, result.node); logger.debug(`Found declaration of ${symbols.join(".")}: `, link); return link; } diff --git a/server/src/project/document.ts b/server/src/project/document.ts index a2da47c..bcd9e5d 100644 --- a/server/src/project/document.ts +++ b/server/src/project/document.ts @@ -41,13 +41,11 @@ import * as url from "node:url"; import * as path from "node:path"; import { logger } from "../util/logger"; -import * as TreeSitterUtil from "../util/tree-sitter"; import { positionToPoint } from "../util/tree-sitter"; import { ModelicaLibrary } from "./library"; import { ModelicaProject } from "./project"; -import { ModelicaScope, ResolvedSymbol } from "./scope"; -export class ModelicaDocument implements ModelicaScope, TextDocument { +export class ModelicaDocument implements TextDocument { readonly #library: ModelicaLibrary; readonly #document: TextDocument; #tree: Parser.Tree; @@ -118,248 +116,6 @@ export class ModelicaDocument implements ModelicaScope, TextDocument { }, this.#tree); } - public async resolve(reference: string[]): Promise { - logger.debug(`Searching for reference '${reference.join(".")}' in document '${this.uri}'`); - - // make the reference relative to the root of this file. - // TODO: this won't handle constants (+other static fields?) properly - reference = reference.slice(this.packagePath.length - 1); - let foundSymbol: Parser.SyntaxNode | null = null; - await TreeSitterUtil.forEach(this.#tree.rootNode, async (node: Parser.SyntaxNode) => { - if (foundSymbol) { - return false; - } - - if (node.type !== "class_definition") { - return false; - } - - if (TreeSitterUtil.getDeclaredIdentifiers(node).includes(reference[0])) { - reference = reference.slice(1); - if (reference.length == 0) { - foundSymbol = node; - } - - return true; - } - - const decl = await this.findDeclarationInClass(node, reference[0]); - if (decl) { - foundSymbol = decl.node; - return true; - } - - return false; - }); - - if (!foundSymbol) { - return null; - } - - logger.debug("Resolved reference:", foundSymbol); - return { - node: foundSymbol, - documentUri: this.uri, - symbol: TreeSitterUtil.getIdentifier(foundSymbol)!, - }; - } - - public async resolveLocally( - reference: string[], - node: Parser.SyntaxNode, - ): Promise { - // Check if the referenced symbol is relative to a class that is in scope. - // If so, we need to resolve it top-down instead. - // const localClass = node.children - // .find((child) => - // child.type === "class_definition" && - // TreeSitterUtil.getIdentifier(child) == reference[0] - // ); - // if (reference.length > 1 && localClass) { - // return await this.resolve([...this.packagePath, ...reference]); - // } - - // TODO: what's the point of this? If reference.at(1 - classDepth) isn't the last symbol - // then we'll return early and won't fully resolve the reference. - //const symbol = reference.length > 1 ? reference[reference.length - classDepth] : reference[0]; - const symbol = reference[0]; - - // Check if this symbol refers to a local variable. - const variableTypes = ["component_clause", "component_redeclaration", "named_element"]; - const local = node.children - .filter((child) => variableTypes.includes(child.type)) - .map((decl) => [decl, TreeSitterUtil.getDeclaredIdentifiers(decl)] as const) - .find(([_decl, idents]) => idents.includes(symbol)); - if (reference.length === 1 && local) { - logger.debug(`Resolved ${reference.join(".")} to local: ${local[1]}`); - - return { - node: local[0], - documentUri: this.uri, - symbol, - }; - } - - // Check if the symbol refers to a local class. - const classDefinition = node.children - .filter((child) => child.type === "class_definition") - .map((classDef) => [classDef, TreeSitterUtil.getDeclaredIdentifiers(classDef)] as const) - .find(([_def, idents]) => idents.includes(symbol)); - if (classDefinition) { - logger.debug(`Resolved ${reference.join(".")} to class: ${classDefinition[1]}`); - if (reference.length === 1) { - return { - node: classDefinition[0], - documentUri: this.uri, - symbol, - }; - } - - // TODO: Use an absolute reference [...this.packagePath, ...localAncestors, ...reference] - // or use some other function - return await this.resolve(reference); - } - - // Check for any elements declared by a class. - if (node.type === "class_definition") { - const decl = await this.findDeclarationInClass(node, symbol); - if (decl) { - return decl; - } - } - - // Check if this symbol refers to an import---either a direct import or a wildcard import. - const importClauses = node.parent?.children.filter( - (sibling) => sibling.type === "import_clause", - ); - if (importClauses && importClauses.length > 0) { - for (const importClause of importClauses) { - const importedSymbol = await this.resolveImportClause(reference, importClause); - if (importedSymbol !== undefined) { - logger.debug(`Resolved ${reference.join(".")} to import: ${importClause}`); - return importedSymbol; - } - } - } - - //logger.debug(`Reference ${reference.join(".")} not at current node; checking parent node`); - return await this.resolveLocally(reference, node.parent); - } - - // TODO: check for relative symbols. Example: - // - // within Foo; - // - // package Bar - // class Baz - // end Baz; - // end Bar; - // - // class Test - // Foo.Bar.Baz baz1; // absolute symbol - // Bar.Baz baz2; // relative symbol -- still valid! - // end Test; - // - // TODO: also make sure to handle encapsulated packages correctly. - - // This must be some kind of relative reference. Check all possible relative imports + the absolute import. - logger.debug(`Reference '${reference.join(".")}' not in document; checking references from other packages`); - - // TODO: packagePath isn't correct due to the resolveLocally(node.parent, ...) stuff above. - // We need the class path at the location specified by the original node. - const packagePath = this.packagePath; - for (let i = packagePath.length; i >= 0; i--) { - - const fullPath = packagePath.slice(i).concat(reference); - const result = await this.project.resolve(fullPath); - if (result) { - return result; - } - } - - // We couldn't resolve the reference. - return null; - } - - private async findDeclarationInClass(classNode: Parser.SyntaxNode, symbol: string): Promise { - const elementListTypes = ["element_list", "public_element_list", "protected_element_list"]; - const elements = classNode - .childForFieldName("classSpecifier") - ?.children?.filter((child) => elementListTypes.includes(child.type)) - ?.flatMap((element_list) => element_list.namedChildren) - ?.map((element) => [element, TreeSitterUtil.getDeclaredIdentifiers(element)] as const); - - const field = elements?.find(([element, idents]) => element.type === "named_element" && idents.includes(symbol)); - if (field) { - logger.debug(`Resolved ${symbol} to field: ${field[1]}`); - - // TODO: this handles named_elements but what if it's an import clause? - return { - symbol, - node: field[0], - documentUri: this.uri, - }; - } - - const inheritedClassPromises = elements - ?.map((element) => element[0]) - ?.filter((element) => element.type === "extends") - .map((node) => this.library.resolve(TreeSitterUtil.getName(node))); - const inheritedClasses = await Promise.all(inheritedClassPromises ?? []); - for (const inheritedClass of inheritedClasses) { - if (!inheritedClass) { - continue; - } - - const decl = this.findDeclarationInClass(inheritedClass.node, symbol); - if (decl) { - return decl; - } - } - - return undefined; - } - - private async resolveImportClause( - reference: string[], - importClause: Parser.SyntaxNode, - ): Promise { - const importedSymbol = TreeSitterUtil.getName(importClause.childForFieldName("name")!); - - // wildcard import: import a.b.*; - const isWildcard = importClause.childForFieldName("wildcard") != null; - if (isWildcard) { - const result = await this.project.resolve([...importedSymbol, ...reference]); - return result ?? undefined; - } - - // import alias: import z = a.b.c; - const alias = importClause.childForFieldName("alias")?.text; - if (alias && alias === reference[0]) { - return this.project.resolve([...importedSymbol, ...reference.slice(1)]); - } - - // multi-import: import a.b.{c, d, e}; - const childImports = importClause.childForFieldName("imports"); - if (childImports) { - const symbolWasImported = childImports.namedChildren - .filter((node) => node.type === "IDENT") - .map((node) => node.text) - .some((name) => name === reference[0]); - - if (symbolWasImported) { - return this.project.resolve([...importedSymbol, ...reference]); - } - } - - // normal import: import a.b.c; - if (importedSymbol.at(-1) === reference[0]) { - return this.project.resolve([...importedSymbol, ...reference.slice(1)]); - } - - return undefined; - } - public getText(range?: LSP.Range | undefined): string { return this.#document.getText(range); } diff --git a/server/src/project/index.ts b/server/src/project/index.ts new file mode 100644 index 0000000..a80780b --- /dev/null +++ b/server/src/project/index.ts @@ -0,0 +1,3 @@ +export { ModelicaDocument } from "./document"; +export { ModelicaLibrary } from "./library"; +export { ModelicaProject } from "./project"; diff --git a/server/src/project/library.ts b/server/src/project/library.ts index 4ce60a0..fcc33f7 100644 --- a/server/src/project/library.ts +++ b/server/src/project/library.ts @@ -39,13 +39,11 @@ import * as path from "node:path"; import * as util from "node:util"; import * as url from "node:url"; -import * as miscUtil from "../util"; import logger from '../util/logger'; import { ModelicaDocument } from "./document"; import { ModelicaProject } from "./project"; -import { ModelicaScope, ResolvedSymbol } from "./scope"; -export class ModelicaLibrary implements ModelicaScope { +export class ModelicaLibrary { readonly #project: ModelicaProject; readonly #uri: string; readonly #documents: Map; @@ -86,57 +84,6 @@ export class ModelicaLibrary implements ModelicaScope { return library; } - public async resolve(reference: string[]): Promise { - logger.debug(`searching for reference '${reference.join('.')}' in library '${this.name}'.`); - logger.debug(`Base dir: ${this.path}`); - - if (this.#documents.size === 0) { - logger.debug(`No documents in library; giving up`); - return null; - } - - let bestDocuments: ModelicaDocument[] = []; - let bestPathLength = -1; - for (const entry of this.#documents) { - const [_uri, document] = entry; - const packagePath = document.packagePath; - - // TODO: the package path should be relative to the root package.mo file - // but in the case of workspaces, it doesn't have to be. This ruins the - // algorithm we use here. - // - // Since a workspace can technically store many libraries, we need to treat - // them differently. A workspace should be considered to be a "library root" - // that can contain multiple libraries. We should scan the workspace for - // libraries upon creating it, and when adding files to the workspace, - // we should figure out which library it belongs to. - // TODO: how do we handle the case in which a file belongs to no libraries? - - logger.debug(`package: ${packagePath}\t\treference: ${reference}`); - const pathLength = miscUtil.getOverlappingLength(packagePath, reference); - if (pathLength > bestPathLength) { - bestDocuments = [document]; - bestPathLength = pathLength; - } else if (pathLength === bestPathLength) { - bestDocuments.push(document); - } - } - - // logger.debug(`Chose these documents as the best matches:`); - // for (const document of bestDocuments) { - // logger.debug(` - ${document.uri}`); - // } - - for (const document of bestDocuments) { - const result = await document.resolve(reference); - if (result) { - return result; - } - } - - return null; - } - public get name(): string { return path.basename(this.path); } diff --git a/server/src/project/project.ts b/server/src/project/project.ts index 3c7d287..a864f8f 100644 --- a/server/src/project/project.ts +++ b/server/src/project/project.ts @@ -38,13 +38,11 @@ import * as LSP from "vscode-languageserver"; import url from "node:url"; import path from "node:path"; -import { ModelicaScope, ResolvedSymbol } from "./scope"; import { ModelicaLibrary } from "./library"; import { ModelicaDocument } from './document'; -import * as util from '../util'; import logger from "../util/logger"; -export class ModelicaProject implements ModelicaScope { +export class ModelicaProject { readonly #parser: Parser; readonly #libraries: ModelicaLibrary[]; @@ -136,25 +134,6 @@ export class ModelicaProject implements ModelicaScope { } } - public async resolve(reference: string[]): Promise { - logger.debug(`searching for reference '${reference.join('.')}' globally.`); - - for (const library of this.libraries) { - if (reference[0] === library.name) { - return await library.resolve(reference); - } - } - - // TODO: check annotations - // We don't need to resolve builtins like Boolean because they aren't - // declared anywhere. - - // TODO: check... array subscripts? can probably skip that - - logger.debug(`Reference '${reference.join('.')}' not found in project.`); - return null; - } - public get parser(): Parser { return this.#parser; } diff --git a/server/src/project/scope.ts b/server/src/project/scope.ts deleted file mode 100644 index 94fb8c2..0000000 --- a/server/src/project/scope.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 * as LSP from "vscode-languageserver/node"; - -import { ModelicaProject } from "./project"; -import Parser from "web-tree-sitter"; - -export interface ResolvedSymbol { - /** - * The symbol being declared. - */ - symbol: string; - - /** - * The node that defines the symbol. - * - * For instance, in a declaration like: - * - * ```modelica - * Real a, b; - * ``` - * - * the `node` for the `ResolvedSymbol` for `a` would be the one - * representing the entire declaration of `a` and `b`. - */ - node: Parser.SyntaxNode; - - /** - * The document in which the symbol was defined. - */ - documentUri: LSP.DocumentUri; -} - -export interface ModelicaScope { - /** - * The project that this scope belongs to. - */ - project: ModelicaProject; - - /** - * Resolves a symbol reference. - * - * @param reference a symbol name, relative to the scope - * @returns the symbol declaration, or null if not found in this scope. - */ - resolve(reference: string[]): Promise; -} diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index 0acb5cc..ac4a01c 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -44,6 +44,7 @@ import Parser from "web-tree-sitter"; import { SyntaxNode } from "web-tree-sitter"; import { logger } from "./logger"; +import { TextDocument } from "vscode-languageserver-textdocument"; type MaybePromise = T | Promise; @@ -152,6 +153,40 @@ export function isDefinition(n: SyntaxNode): boolean { } } +/** + * Tell if a node is a variable declaration. + * + * @param n Node of tree + * @returns `true` if node is a variable declaration, `false` otherwise. + */ +export function isVariableDeclaration(n: SyntaxNode): boolean { + switch (n.type) { + case "component_clause": + case "component_redeclaration": + case "named_element": + return true; + default: + return false; + } +} + +/** + * Tell if a node is an element list. + * + * @param n Node of tree + * @returns `true` if node is an element list, `false` otherwise. + */ +export function isElementList(n: SyntaxNode): boolean { + switch (n.type) { + case "element_list": + case "public_element_list": + case "protected_element_list": + return true; + default: + return false; + } +} + export function findParent( start: SyntaxNode, predicate: (n: SyntaxNode) => boolean, @@ -235,6 +270,22 @@ export function getDeclaredIdentifiers(node: SyntaxNode): string[] { } } +export function getDeclarationType(node: SyntaxNode): { symbols: string[]; global: boolean } { + const typeSpecifier = findFirst(node, (child) => child.type === "type_specifier"); + if (!typeSpecifier) { + throw new Error("Node does not contain a type_specifier"); + } + + return { + symbols: getName(typeSpecifier), + global: typeSpecifier.childForFieldName("global") !== null, + }; +} + +export function hasIdentifier(node: SyntaxNode, identifier: string): boolean { + return getDeclaredIdentifiers(node).includes(identifier); +} + /** * Converts a name `SyntaxNode` into an array of the `IDENT`s in that node. */ @@ -287,13 +338,21 @@ export function pointToPosition(point: Parser.Point): LSP.Position { return { line: point.row, character: point.column }; } +export function createLocationLink( + document: TextDocument, + node: Parser.SyntaxNode, +): LSP.LocationLink; export function createLocationLink( documentUri: LSP.DocumentUri, node: Parser.SyntaxNode, +): LSP.LocationLink; +export function createLocationLink( + document: TextDocument | LSP.DocumentUri, + node: Parser.SyntaxNode, ): LSP.LocationLink { // TODO: properly set targetSelectionRange (e.g. the name of a function or variable). return { - targetUri: documentUri, + targetUri: typeof document === "string" ? document : document.uri, targetRange: { start: pointToPosition(node.startPosition), end: pointToPosition(node.endPosition), From df86c404535e947e8cfa898e27c9a92956c8d7c5 Mon Sep 17 00:00:00 2001 From: PaddiM8 Date: Wed, 1 May 2024 14:39:25 +0200 Subject: [PATCH 13/24] Fix resolution of types and locals --- server/src/analysis/resolveReference.ts | 87 ++++++++++++++++++++----- server/src/analyzer.ts | 28 ++++---- server/src/util/tree-sitter.ts | 13 ++-- 3 files changed, 94 insertions(+), 34 deletions(-) diff --git a/server/src/analysis/resolveReference.ts b/server/src/analysis/resolveReference.ts index f0ee59e..24d5cd4 100644 --- a/server/src/analysis/resolveReference.ts +++ b/server/src/analysis/resolveReference.ts @@ -1,6 +1,7 @@ import Parser from "web-tree-sitter"; -import * as path from "node:path"; import * as fs from "node:fs"; +import * as path from "node:path"; +import * as url from "node:url"; import * as TreeSitterUtil from "../util/tree-sitter"; import { @@ -77,6 +78,12 @@ function absolutize( currentNode = currentNode.parent; } + // If the starting point is a class, it will be the last element + // in ancestors at this point, so it needs to be removed + if (local.node.type === "class_definition") { + ancestors.pop(); + } + logger.debug(`Found local: ${local} with ancestors: ${ancestors}`); return new UnresolvedAbsoluteReference([ @@ -84,6 +91,8 @@ function absolutize( ...ancestors, ...local.symbols, ]); + + return null; } /** @@ -96,9 +105,9 @@ function absolutize( * the symbol's declaration/definition, * (2) an absolute reference */ -function* findReferenceInDocument( +function findReferenceInDocument( reference: UnresolvedRelativeReference, -): Generator { +): UnresolvedReference | null { // TODO: Function declarations logger.warn("NOT checking for functions!"); @@ -114,7 +123,6 @@ function* findReferenceInDocument( return new UnresolvedRelativeReference(reference.document, decl, reference.symbols); } - logger.debug("Checking for declaration in class..."); const declInClass = findDeclarationInClass(reference.document, reference.node, reference.symbols); if (declInClass) { return declInClass; @@ -126,7 +134,13 @@ function* findReferenceInDocument( if (importClauses && importClauses.length > 0) { logger.debug("Checking imports..."); for (const importClause of importClauses) { - yield* resolveImportClause(reference.symbols, importClause); + const importResult = resolveImportClause(reference.document.project, reference.symbols, importClause); + if (importResult) { + logger.debug("-------------- found import"); + return importResult; + } + + logger.debug("--------------"); } } @@ -153,6 +167,8 @@ function findDeclarationInClass( return undefined; } + logger.debug(`findDeclarationInClass: ${TreeSitterUtil.getIdentifier(classNode)} with symbols ${symbols}`); + logger.debug(`Checking for declaration in class: ${TreeSitterUtil.getDeclaredIdentifiers(classNode)}`); const elements = classNode .childForFieldName("classSpecifier") ?.children?.filter(TreeSitterUtil.isElementList) @@ -160,6 +176,7 @@ function findDeclarationInClass( ?.map((element) => [element, TreeSitterUtil.getDeclaredIdentifiers(element)] as const); if (!elements) { + logger.debug("Didn't find declaration in class"); return undefined; } @@ -169,6 +186,11 @@ function findDeclarationInClass( if (field) { logger.debug(`Resolved ${symbols[0]} to field: ${field[1]}`); + const classDef = field[0].childForFieldName("classDefinition"); + if (classDef) { + return new UnresolvedRelativeReference(document, classDef, symbols); + } + // TODO: this handles named_elements but what if it's an import clause? return new UnresolvedRelativeReference(document, field[0], symbols); } @@ -201,10 +223,11 @@ function findDeclarationInClass( return undefined; } -function* resolveImportClause( +function resolveImportClause( + project: ModelicaProject, symbols: string[], importClause: Parser.SyntaxNode, -): Generator { +): UnresolvedAbsoluteReference | null { const importPath = TreeSitterUtil.getName(importClause.childForFieldName("name")!); // wildcard import: import a.b.*; @@ -213,7 +236,9 @@ function* resolveImportClause( const importCandidate = new UnresolvedAbsoluteReference([...importPath, ...symbols]); logger.debug(`Candidate: ${importCandidate} (from import ${importPath.join(".")}.*)`); - yield importCandidate; + if (resolveAbsoluteReference(project, importCandidate)) { + return importCandidate; + } } // import alias: import z = a.b.c; @@ -248,6 +273,8 @@ function* resolveImportClause( return importCandidate; } + + return null; } /** @@ -273,14 +300,16 @@ function resolveAbsoluteReference( let alreadyResolved: ResolvedReference | null = null; for (let i = 0; i < reference.symbols.length; i++) { alreadyResolved = resolveNext(library, reference, alreadyResolved); + logger.debug(`resolveNext found symbol: ${alreadyResolved != null}`); if (alreadyResolved == null) { return null; } - console.log(`Step ${i + 1}: ${alreadyResolved}`); + logger.debug(`Step ${i + 1}: ${alreadyResolved}`); // If we're not done with the reference chain, we need to make sure that we know // the type of the variable in order to check its child variables + logger.debug(alreadyResolved.node.type); if ( i < reference.symbols.length - 1 && TreeSitterUtil.isVariableDeclaration(alreadyResolved.node) @@ -315,6 +344,7 @@ function resolveNext( ): ResolvedReference | null { // If at the root level, find the root package if (!alreadyResolved) { + logger.debug(`Resolve next: ${reference.symbols[0]}`); const documentPath = path.join(library.path, "package.mo"); const [document, packageClass] = getPackageClassFromFilePath( library, @@ -322,14 +352,16 @@ function resolveNext( reference.symbols[0], ); if (!document || !packageClass) { + logger.debug(`Couldn't find package class: ${reference.symbols[0]} in ${documentPath}`); return null; } - return new ResolvedReference(document, packageClass, reference.symbols); + return new ResolvedReference(document, packageClass, reference.symbols.slice(0, 1)); } const nextSymbolIndex = alreadyResolved.symbols.length; const nextSymbol = reference.symbols[nextSymbolIndex]; + logger.debug(`Resolve next: ${nextSymbol} (alreadyResolved: ${alreadyResolved.node.type} with ident ${TreeSitterUtil.getIdentifier(alreadyResolved.node)})`); // If nextSymbol is in alreadyResolved.node: // return the declaration @@ -337,17 +369,23 @@ function resolveNext( const child = findDeclarationInClass( alreadyResolved.document, alreadyResolved.node, - reference.symbols, + reference.symbols.slice(nextSymbolIndex), ); if (child) { - return new ResolvedReference(alreadyResolved!.document, child.node, reference.symbols); + return new ResolvedReference( + alreadyResolved!.document, + child.node, + reference.symbols.slice(0, nextSymbolIndex + 1) + ); } // If there is a document for nextSymbol blablabla + const dirName = path.dirname(alreadyResolved.document.path); const potentialPaths = [ - path.join(alreadyResolved.document.path, `${nextSymbol}.mo`), - path.join(alreadyResolved.document.path, `${nextSymbol}/package.mo`), + path.join(dirName, `${nextSymbol}.mo`), + path.join(dirName, `${nextSymbol}/package.mo`), ]; + logger.debug(`resolveNext potentialPaths: ${potentialPaths}`); for (const documentPath of potentialPaths) { if (!fs.existsSync(documentPath)) { continue; @@ -355,12 +393,19 @@ function resolveNext( const [document, packageClass] = getPackageClassFromFilePath(library, documentPath, nextSymbol); if (!document || !packageClass) { + logger.debug(`Couldn't find package class: ${nextSymbol} in ${documentPath}`); return null; } - return new ResolvedReference(document, packageClass, reference.symbols); + return new ResolvedReference( + document, + packageClass, + reference.symbols.slice(0, nextSymbolIndex + 1) + ); } + logger.debug(`Couldn't find document for ${nextSymbol}`); + return null; } @@ -369,15 +414,17 @@ function getPackageClassFromFilePath( filePath: string, symbol: string, ): [ModelicaDocument | undefined, Parser.SyntaxNode | undefined] { - const document = library.documents.get(filePath); + const document = library.documents.get(url.pathToFileURL(filePath).href); if (!document) { + logger.debug(`getPackageClassFromFilePath: Couldn't find document ${filePath}`); return [undefined, undefined]; } - const node = document.tree.rootNode.children.find((child) => - TreeSitterUtil.hasIdentifier(child, symbol), + const node = TreeSitterUtil.findFirst(document.tree.rootNode, (child) => + child.type === "class_definition" && TreeSitterUtil.hasIdentifier(child, symbol) ); if (!node) { + logger.debug(`getPackageClassFromFilePath: Couldn't find package class node ${symbol} in ${filePath}`); return [document, undefined]; } @@ -406,5 +453,9 @@ function variableRefToClassRef(varRef: ResolvedReference): ResolvedReference | n return null; } + if (absoluteReference instanceof ResolvedReference) { + return absoluteReference; + } + return resolveAbsoluteReference(varRef.document.project, absoluteReference); } diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 7d6c754..17e1fdf 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -108,7 +108,6 @@ export default class Analyzer { } const documentOffset = document.offsetAt({ line, character }); - const hoveredName = this.findNodeAtPosition( document.tree.rootNode, documentOffset, @@ -149,19 +148,24 @@ export default class Analyzer { `Searching for declaration '${symbols.join(".")}' at ${line + 1}:${character + 1} in '${uri}'`, ); - const result = resolveReference( - document.project, - new UnresolvedRelativeReference(document, startNode, symbols), - "declaration", - ); - if (!result) { - logger.debug(`Didn't find declaration of ${symbols.join(".")}`); + try { + const result = resolveReference( + document.project, + new UnresolvedRelativeReference(document, startNode, symbols), + "declaration", + ); + if (!result) { + logger.debug(`Didn't find declaration of ${symbols.join(".")}`); + return null; + } + + const link = TreeSitterUtil.createLocationLink(result.document, result.node); + logger.debug(`Found declaration of ${symbols.join(".")}: `, link); + return link; + } catch (ex) { + logger.debug("Caught exception: " + JSON.stringify((ex as Error).stack)); return null; } - - const link = TreeSitterUtil.createLocationLink(result.document, result.node); - logger.debug(`Found declaration of ${symbols.join(".")}: `, link); - return link; } /** diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index ac4a01c..a100eb0 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -163,8 +163,9 @@ export function isVariableDeclaration(n: SyntaxNode): boolean { switch (n.type) { case "component_clause": case "component_redeclaration": - case "named_element": return true; + case "named_element": + return n.childForFieldName("classDefinition") == null; default: return false; } @@ -277,12 +278,16 @@ export function getDeclarationType(node: SyntaxNode): { symbols: string[]; globa } return { - symbols: getName(typeSpecifier), + symbols: getName(typeSpecifier.childForFieldName("name")!), global: typeSpecifier.childForFieldName("global") !== null, }; } -export function hasIdentifier(node: SyntaxNode, identifier: string): boolean { +export function hasIdentifier(node: SyntaxNode | null, identifier: string): boolean { + if (!node) { + return false; + } + return getDeclaredIdentifiers(node).includes(identifier); } @@ -298,7 +303,7 @@ export function getName(nameNode: SyntaxNode): string[] { */ export function getNameIdentifiers(nameNode: SyntaxNode): Parser.SyntaxNode[] { if (nameNode.type !== "name") { - throw new Error(`Expected a 'name' node; got '${nameNode.type}'`); + throw new Error(`Expected a 'name' node; got '${nameNode.type}' (${nameNode.text})`); } const identNode = nameNode.childForFieldName("identifier")!; From c15d93f8762b129731dc4fc24da862b20300a062 Mon Sep 17 00:00:00 2001 From: PaddiM8 Date: Wed, 1 May 2024 15:35:44 +0200 Subject: [PATCH 14/24] Fix resolution of inherited members --- server/src/analysis/resolveReference.ts | 66 +++++++++++-------------- server/src/util/tree-sitter.ts | 2 + 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/server/src/analysis/resolveReference.ts b/server/src/analysis/resolveReference.ts index 24d5cd4..5fe4981 100644 --- a/server/src/analysis/resolveReference.ts +++ b/server/src/analysis/resolveReference.ts @@ -91,8 +91,6 @@ function absolutize( ...ancestors, ...local.symbols, ]); - - return null; } /** @@ -108,7 +106,7 @@ function absolutize( function findReferenceInDocument( reference: UnresolvedRelativeReference, ): UnresolvedReference | null { - // TODO: Function declarations + // TODO: Function declarations (do they even exist?) logger.warn("NOT checking for functions!"); logger.debug("Checking for local class or variable..."); @@ -195,27 +193,23 @@ function findDeclarationInClass( return new UnresolvedRelativeReference(document, field[0], symbols); } - const superclasses = elements + const extendClauses = elements .map(([element, _idents]) => element) - .filter((element) => element.type === "extends") - .map((node) => { - const superclassRef = new UnresolvedAbsoluteReference(TreeSitterUtil.getName(node)); - logger.debug(`Resolving superclass ${superclassRef} (of ${TreeSitterUtil.getDeclaredIdentifiers(classNode)[0]})`); - - const resolvedSuperclassRef = resolveAbsoluteReference(document.project, superclassRef); - if (!resolvedSuperclassRef) { - logger.warn(`Could not find superclass ${superclassRef}`); - } - - return resolvedSuperclassRef; - }) - .filter((superclass) => superclass != null) as ResolvedReference[]; + .filter((element) => element.type === "extends_clause"); + for (const extendClause of extendClauses) { + const unresolvedSuperclass = new UnresolvedAbsoluteReference(TreeSitterUtil.getDeclaredIdentifiers(extendClause)); + logger.debug(`Resolving superclass ${unresolvedSuperclass} (of ${TreeSitterUtil.getDeclaredIdentifiers(classNode)[0]})`); + + const superclass = resolveAbsoluteReference(document.project, unresolvedSuperclass); + if (!superclass) { + logger.warn(`Could not find superclass ${unresolvedSuperclass}`); + continue; + } - for (const superclass of superclasses) { logger.debug(`Checking superclass ${superclass}`); - const decl = findDeclarationInClass(document, superclass.node, symbols); + const decl = findDeclarationInClass(superclass.document, superclass.node, symbols); if (decl) { - logger.debug(`Declaration ${decl} found in superclass ${superclasses}`); + logger.debug(`Declaration ${decl} found in superclass ${superclass}`); return decl; } } @@ -363,22 +357,6 @@ function resolveNext( const nextSymbol = reference.symbols[nextSymbolIndex]; logger.debug(`Resolve next: ${nextSymbol} (alreadyResolved: ${alreadyResolved.node.type} with ident ${TreeSitterUtil.getIdentifier(alreadyResolved.node)})`); - // If nextSymbol is in alreadyResolved.node: - // return the declaration - // TODO: Variable declarations may be nested inside an element list - const child = findDeclarationInClass( - alreadyResolved.document, - alreadyResolved.node, - reference.symbols.slice(nextSymbolIndex), - ); - if (child) { - return new ResolvedReference( - alreadyResolved!.document, - child.node, - reference.symbols.slice(0, nextSymbolIndex + 1) - ); - } - // If there is a document for nextSymbol blablabla const dirName = path.dirname(alreadyResolved.document.path); const potentialPaths = [ @@ -404,6 +382,22 @@ function resolveNext( ); } + // If nextSymbol is in alreadyResolved.node: + // return the declaration + // TODO: Variable declarations may be nested inside an element list + const child = findDeclarationInClass( + alreadyResolved.document, + alreadyResolved.node, + reference.symbols.slice(nextSymbolIndex), + ); + if (child) { + return new ResolvedReference( + child.document, + child.node, + reference.symbols.slice(0, nextSymbolIndex + 1) + ); + } + logger.debug(`Couldn't find document for ${nextSymbol}`); return null; diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index a100eb0..85d4ff8 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -230,6 +230,8 @@ export function getDeclaredIdentifiers(node: SyntaxNode): string[] { // TODO: does this support all desired node types? Are we considering too many nodes? switch (node.type) { + case "extends_clause": + return getName(node.childForFieldName("typeSpecifier")!.childForFieldName("name")!); case "declaration": case "derivative_class_specifier": case "enumeration_class_specifier": From aaa6dbcf8ce3a77d334ec7297ddf74ee972cfa52 Mon Sep 17 00:00:00 2001 From: PaddiM8 Date: Wed, 1 May 2024 15:45:58 +0200 Subject: [PATCH 15/24] Allow relative references in extends clauses --- server/src/analysis/resolveReference.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/analysis/resolveReference.ts b/server/src/analysis/resolveReference.ts index 5fe4981..7eaa70e 100644 --- a/server/src/analysis/resolveReference.ts +++ b/server/src/analysis/resolveReference.ts @@ -152,6 +152,13 @@ function findReferenceInDocument( // TODO: check subpackages logger.warn("NOT checking subpackages!"); + const referenceWithPackagePath = new UnresolvedAbsoluteReference( + [...reference.document.packagePath, ...reference.symbols] + ); + if (resolveAbsoluteReference(reference.document.project, referenceWithPackagePath)) { + return referenceWithPackagePath; + } + logger.debug("Not found in documment. This reference is either global or undefined."); return new UnresolvedAbsoluteReference(reference.symbols); } From 6101adaa9622e48e28e11060e2e4b557c349de1e Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Thu, 2 May 2024 17:12:27 +0200 Subject: [PATCH 16/24] Bugfixes; use paths internally Co-authored-by: PaddiM8 --- server/src/analysis/reference.ts | 12 +-- server/src/analysis/resolveReference.ts | 112 +++++++++++++++--------- server/src/analyzer.ts | 44 ++++++++-- server/src/project/document.ts | 21 +++-- server/src/project/library.ts | 45 +++++----- server/src/project/project.ts | 37 ++++---- server/src/server.ts | 3 +- server/src/util/index.ts | 23 +++-- server/src/util/tree-sitter.ts | 67 +++++++++----- 9 files changed, 230 insertions(+), 134 deletions(-) diff --git a/server/src/analysis/reference.ts b/server/src/analysis/reference.ts index 1a5b107..b87c475 100644 --- a/server/src/analysis/reference.ts +++ b/server/src/analysis/reference.ts @@ -39,11 +39,11 @@ export class UnresolvedRelativeReference extends BaseUnresolvedReference { return false; } - public get [Symbol.toStringTag](): string { + public toString(): string { const start = this.node.startPosition; const pos = `${start.row + 1}:${start.column + 1}`; - return `${this.symbols.join(".")} at ${pos} in "${this.document.uri}"`; + return `?${this.symbols.join(".")} at ${pos} in "${this.document.path}"`; } } @@ -56,8 +56,8 @@ export class UnresolvedAbsoluteReference extends BaseUnresolvedReference { return true; } - public get [Symbol.toStringTag](): string { - return `.${this.symbols.join(".")}`; + public toString(): string { + return `?.${this.symbols.join(".")}`; } } @@ -89,7 +89,7 @@ export class ResolvedReference { this.symbols = symbols; } - public get [Symbol.toStringTag](): string { - return `.${this.symbols.join(".")}`; + public toString(): string { + return `.${this.symbols.join(".")}`; } } diff --git a/server/src/analysis/resolveReference.ts b/server/src/analysis/resolveReference.ts index 7eaa70e..a8a155e 100644 --- a/server/src/analysis/resolveReference.ts +++ b/server/src/analysis/resolveReference.ts @@ -49,17 +49,12 @@ export default function resolveReference( * @param reference a relative reference to a symbol declaration/definition * @returns an absolute reference to that symbol, or `null` if no such symbol exists. */ -function absolutize( - reference: UnresolvedRelativeReference, -): UnresolvedAbsoluteReference | ResolvedReference | null { +function absolutize(reference: UnresolvedRelativeReference): UnresolvedAbsoluteReference | null { logger.debug(`Absolutize: ${reference}`); const local = findReferenceInDocument(reference); if (local === null) { logger.debug(`Didn't find symbol ${reference}`); return null; - } else if (local instanceof ResolvedReference) { - logger.debug(`Resolved reference ${local}`); - return local; } else if (local instanceof UnresolvedAbsoluteReference) { logger.debug(`Found absolute reference ${local}`); return local; @@ -78,8 +73,6 @@ function absolutize( currentNode = currentNode.parent; } - // If the starting point is a class, it will be the last element - // in ancestors at this point, so it needs to be removed if (local.node.type === "class_definition") { ancestors.pop(); } @@ -87,7 +80,7 @@ function absolutize( logger.debug(`Found local: ${local} with ancestors: ${ancestors}`); return new UnresolvedAbsoluteReference([ - ...local.document.packagePath, + ...local.document.within, ...ancestors, ...local.symbols, ]); @@ -106,10 +99,14 @@ function absolutize( function findReferenceInDocument( reference: UnresolvedRelativeReference, ): UnresolvedReference | null { - // TODO: Function declarations (do they even exist?) - logger.warn("NOT checking for functions!"); + if ( + TreeSitterUtil.isDefinition(reference.node) && + TreeSitterUtil.hasIdentifier(reference.node, reference.symbols[0]) + ) { + return reference; + } - logger.debug("Checking for local class or variable..."); + logger.debug("findReferenceInDocument: Checking for local class or variable..."); const decl = reference.node.children.find((child) => { return ( (TreeSitterUtil.isDefinition(child) || TreeSitterUtil.isVariableDeclaration(child)) && @@ -121,6 +118,7 @@ function findReferenceInDocument( return new UnresolvedRelativeReference(reference.document, decl, reference.symbols); } + logger.debug("findReferenceInDocument: Checking for declarations in class..."); const declInClass = findDeclarationInClass(reference.document, reference.node, reference.symbols); if (declInClass) { return declInClass; @@ -130,20 +128,22 @@ function findReferenceInDocument( (child) => child.type === "import_clause", ); if (importClauses && importClauses.length > 0) { - logger.debug("Checking imports..."); + logger.debug("findReferenceInDocument: Checking imports..."); for (const importClause of importClauses) { - const importResult = resolveImportClause(reference.document.project, reference.symbols, importClause); + const importResult = resolveImportClause( + reference.document.project, + reference.symbols, + importClause, + ); if (importResult) { - logger.debug("-------------- found import"); + logger.debug("findReferenceInDocument: found import!"); return importResult; } - - logger.debug("--------------"); } } if (reference.node.parent) { - logger.debug("Checking parent node..."); + logger.debug("findReferenceInDocument: Checking parent node..."); return findReferenceInDocument( new UnresolvedRelativeReference(reference.document, reference.node.parent, reference.symbols), ); @@ -152,14 +152,15 @@ function findReferenceInDocument( // TODO: check subpackages logger.warn("NOT checking subpackages!"); - const referenceWithPackagePath = new UnresolvedAbsoluteReference( - [...reference.document.packagePath, ...reference.symbols] - ); + const referenceWithPackagePath = new UnresolvedAbsoluteReference([ + ...reference.document.packagePath, + ...reference.symbols, + ]); if (resolveAbsoluteReference(reference.document.project, referenceWithPackagePath)) { return referenceWithPackagePath; } - logger.debug("Not found in documment. This reference is either global or undefined."); + logger.debug("Not found in document. This reference is either global or undefined."); return new UnresolvedAbsoluteReference(reference.symbols); } @@ -172,8 +173,11 @@ function findDeclarationInClass( return undefined; } - logger.debug(`findDeclarationInClass: ${TreeSitterUtil.getIdentifier(classNode)} with symbols ${symbols}`); - logger.debug(`Checking for declaration in class: ${TreeSitterUtil.getDeclaredIdentifiers(classNode)}`); + logger.debug( + `findDeclarationInClass: Checking for declaration ${symbols.join(".")} ` + + `in class: ${TreeSitterUtil.getDeclaredIdentifiers(classNode)}`, + ); + const elements = classNode .childForFieldName("classSpecifier") ?.children?.filter(TreeSitterUtil.isElementList) @@ -200,12 +204,27 @@ function findDeclarationInClass( return new UnresolvedRelativeReference(document, field[0], symbols); } - const extendClauses = elements + const extendsClauses = elements .map(([element, _idents]) => element) .filter((element) => element.type === "extends_clause"); - for (const extendClause of extendClauses) { - const unresolvedSuperclass = new UnresolvedAbsoluteReference(TreeSitterUtil.getDeclaredIdentifiers(extendClause)); - logger.debug(`Resolving superclass ${unresolvedSuperclass} (of ${TreeSitterUtil.getDeclaredIdentifiers(classNode)[0]})`); + for (const extendsClause of extendsClauses) { + const superclassType = TreeSitterUtil.getDeclaredType(extendsClause); + const unresolvedSuperclass = superclassType.isGlobal + ? new UnresolvedAbsoluteReference(superclassType.symbols) + : absolutize( + new UnresolvedRelativeReference(document, extendsClause, superclassType.symbols), + ); + + if (unresolvedSuperclass == null) { + logger.warn(`Superclass ${superclassType.symbols} not found`); + continue; + } + + logger.debug( + `Resolving superclass ${unresolvedSuperclass} (of ${ + TreeSitterUtil.getDeclaredIdentifiers(classNode)[0] + })`, + ); const superclass = resolveAbsoluteReference(document.project, unresolvedSuperclass); if (!superclass) { @@ -229,7 +248,10 @@ function resolveImportClause( symbols: string[], importClause: Parser.SyntaxNode, ): UnresolvedAbsoluteReference | null { - const importPath = TreeSitterUtil.getName(importClause.childForFieldName("name")!); + // imports are always relative according to the grammar + const importPath = TreeSitterUtil.getDeclaredType( + importClause.childForFieldName("name")!, + ).symbols; // wildcard import: import a.b.*; const isWildcard = importClause.childForFieldName("wildcard") != null; @@ -237,6 +259,8 @@ function resolveImportClause( const importCandidate = new UnresolvedAbsoluteReference([...importPath, ...symbols]); logger.debug(`Candidate: ${importCandidate} (from import ${importPath.join(".")}.*)`); + // TODO: this should probably not resolve the reference fully, then immediately + // discard it so it can do so again. if (resolveAbsoluteReference(project, importCandidate)) { return importCandidate; } @@ -288,6 +312,10 @@ function resolveAbsoluteReference( project: ModelicaProject, reference: UnresolvedAbsoluteReference, ): ResolvedReference | null { + if (!(reference instanceof UnresolvedAbsoluteReference)) { + throw new Error(`Reference is not an UnresolvedAbsoluteReference: ${reference}`); + } + logger.debug(`Resolving absolute reference ${reference}`); const library = project.libraries.find((lib) => lib.name === reference.symbols[0]); @@ -310,7 +338,6 @@ function resolveAbsoluteReference( // If we're not done with the reference chain, we need to make sure that we know // the type of the variable in order to check its child variables - logger.debug(alreadyResolved.node.type); if ( i < reference.symbols.length - 1 && TreeSitterUtil.isVariableDeclaration(alreadyResolved.node) @@ -362,7 +389,11 @@ function resolveNext( const nextSymbolIndex = alreadyResolved.symbols.length; const nextSymbol = reference.symbols[nextSymbolIndex]; - logger.debug(`Resolve next: ${nextSymbol} (alreadyResolved: ${alreadyResolved.node.type} with ident ${TreeSitterUtil.getIdentifier(alreadyResolved.node)})`); + logger.debug( + `Resolve next: ${nextSymbol} (alreadyResolved: ${ + alreadyResolved.node.type + } with ident ${TreeSitterUtil.getIdentifier(alreadyResolved.node)})`, + ); // If there is a document for nextSymbol blablabla const dirName = path.dirname(alreadyResolved.document.path); @@ -385,7 +416,7 @@ function resolveNext( return new ResolvedReference( document, packageClass, - reference.symbols.slice(0, nextSymbolIndex + 1) + reference.symbols.slice(0, nextSymbolIndex + 1), ); } @@ -401,7 +432,7 @@ function resolveNext( return new ResolvedReference( child.document, child.node, - reference.symbols.slice(0, nextSymbolIndex + 1) + reference.symbols.slice(0, nextSymbolIndex + 1), ); } @@ -415,17 +446,20 @@ function getPackageClassFromFilePath( filePath: string, symbol: string, ): [ModelicaDocument | undefined, Parser.SyntaxNode | undefined] { - const document = library.documents.get(url.pathToFileURL(filePath).href); + const document = library.documents.get(filePath); if (!document) { logger.debug(`getPackageClassFromFilePath: Couldn't find document ${filePath}`); return [undefined, undefined]; } - const node = TreeSitterUtil.findFirst(document.tree.rootNode, (child) => - child.type === "class_definition" && TreeSitterUtil.hasIdentifier(child, symbol) + const node = TreeSitterUtil.findFirst( + document.tree.rootNode, + (child) => child.type === "class_definition" && TreeSitterUtil.hasIdentifier(child, symbol), ); if (!node) { - logger.debug(`getPackageClassFromFilePath: Couldn't find package class node ${symbol} in ${filePath}`); + logger.debug( + `getPackageClassFromFilePath: Couldn't find package class node ${symbol} in ${filePath}`, + ); return [document, undefined]; } @@ -439,10 +473,10 @@ function getPackageClassFromFilePath( * @returns a reference to the class definition, or `null` if the type is not a class (e.g. a builtin like `Real`) */ function variableRefToClassRef(varRef: ResolvedReference): ResolvedReference | null { - const type = TreeSitterUtil.getDeclarationType(varRef.node); + const type = TreeSitterUtil.getDeclaredType(varRef.node); const absoluteReference = (() => { - if (type.global) { + if (type.isGlobal) { return new UnresolvedAbsoluteReference(type.symbols); } else { const typeRef = new UnresolvedRelativeReference(varRef.document, varRef.node, type.symbols); diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 17e1fdf..d8ad79f 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -41,11 +41,16 @@ import * as LSP from "vscode-languageserver/node"; import Parser from "web-tree-sitter"; +import * as fs from "node:fs/promises"; +import * as fsSync from "node:fs"; +import * as path from "node:path"; +import * as url from "node:url"; import { UnresolvedRelativeReference } from "./analysis/reference"; import resolveReference from "./analysis/resolveReference"; import { ModelicaProject } from "./project/project"; import { ModelicaLibrary } from "./project/library"; +import { uriToPath } from "./util"; import { getAllDeclarationsInTree } from "./util/declarations"; import logger from "./util/logger"; import * as TreeSitterUtil from "./util/tree-sitter"; @@ -58,8 +63,27 @@ export default class Analyzer { } public async loadLibrary(uri: LSP.URI, isWorkspace: boolean): Promise { - const workspace = await ModelicaLibrary.load(this.#project, uri, isWorkspace); - this.#project.addLibrary(workspace); + const isLibrary = (folderPath: string) => + fsSync.existsSync(path.join(folderPath, "package.mo")); + + const libraryPath = uriToPath(uri); + if (!isWorkspace || isLibrary(libraryPath)) { + const lib = await ModelicaLibrary.load(this.#project, libraryPath, isWorkspace); + this.#project.addLibrary(lib); + return; + } + + // 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 library = await ModelicaLibrary.load(this.#project, nested, isWorkspace); + this.#project.addLibrary(library); + } } public addDocument(uri: LSP.DocumentUri): void { @@ -80,7 +104,8 @@ export default class Analyzer { * TODO: convert to DocumentSymbol[] which is a hierarchy of symbols found in a given text document. */ public getDeclarationsForUri(uri: LSP.DocumentUri): LSP.SymbolInformation[] { - const tree = this.#project.getDocument(uri)?.tree; + const path = uriToPath(uri); + const tree = this.#project.getDocument(path)?.tree; if (!tree?.rootNode) { return []; @@ -94,9 +119,10 @@ export default class Analyzer { line: number, character: number, ): Promise { - logger.debug(`Searching for declaration of symbol at ${line + 1}:${character + 1} in '${uri}'`); + const path = uriToPath(uri); + logger.debug(`Searching for declaration of symbol at ${line + 1}:${character + 1} in '${path}'`); - const document = this.#project.getDocument(uri); + const document = this.#project.getDocument(path); if (!document) { logger.warn(`Couldn't find declaration: document not loaded.`); return null; @@ -108,6 +134,8 @@ export default class Analyzer { } const documentOffset = document.offsetAt({ line, character }); + + // TODO: we should check for a `type_specifier` first, then a `name`, then an `ident` const hoveredName = this.findNodeAtPosition( document.tree.rootNode, documentOffset, @@ -117,7 +145,7 @@ export default class Analyzer { let symbols: string[] | undefined; let startNode: Parser.SyntaxNode | undefined; if (hoveredName) { - symbols = TreeSitterUtil.getNameIdentifiers(hoveredName) + symbols = TreeSitterUtil.getDeclaredType(hoveredName).symbolNodes .filter( (node) => node.startPosition.row < line || @@ -140,12 +168,12 @@ export default class Analyzer { } if (!startNode || !symbols) { - logger.info(`Tried to find declaration in '${uri}', but not hovering on any identifiers`); + logger.info(`Tried to find declaration in '${path}', but not hovering on any identifiers`); return null; } logger.debug( - `Searching for declaration '${symbols.join(".")}' at ${line + 1}:${character + 1} in '${uri}'`, + `Searching for declaration '${symbols.join(".")}' at ${line + 1}:${character + 1} in '${path}'`, ); try { diff --git a/server/src/project/document.ts b/server/src/project/document.ts index bcd9e5d..6344d5d 100644 --- a/server/src/project/document.ts +++ b/server/src/project/document.ts @@ -44,6 +44,7 @@ import { logger } from "../util/logger"; import { positionToPoint } from "../util/tree-sitter"; import { ModelicaLibrary } from "./library"; import { ModelicaProject } from "./project"; +import { pathToUri, uriToPath } from "../util"; export class ModelicaDocument implements TextDocument { readonly #library: ModelicaLibrary; @@ -58,16 +59,20 @@ export class ModelicaDocument implements TextDocument { public static async load( library: ModelicaLibrary, - uri: LSP.DocumentUri, + documentPath: string, ): Promise { - logger.debug(`Loading document at '${uri}'...`); + logger.debug(`Loading document at '${documentPath}'...`); - const content = await fs.readFile(url.fileURLToPath(uri), "utf-8"); + 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(uri, "modelica", 0, content), tree); + return new ModelicaDocument( + library, + TextDocument.create(pathToUri(documentPath), "modelica", 0, content), + tree + ); } public async update(text: string, range?: LSP.Range): Promise { @@ -128,12 +133,12 @@ export class ModelicaDocument implements TextDocument { return this.#document.offsetAt(position); } - public get uri(): string { + public get uri(): LSP.DocumentUri { return this.#document.uri; } public get path(): string { - return url.fileURLToPath(this.uri); + return uriToPath(this.#document.uri); } public get languageId(): string { @@ -160,6 +165,10 @@ export class ModelicaDocument implements TextDocument { return packagePath; } + public get within(): string[] { + return this.packagePath.slice(0, -1); + } + public get project(): ModelicaProject { return this.#library.project; } diff --git a/server/src/project/library.ts b/server/src/project/library.ts index fcc33f7..6a77d64 100644 --- a/server/src/project/library.ts +++ b/server/src/project/library.ts @@ -37,7 +37,6 @@ import * as LSP from "vscode-languageserver"; import * as fsWalk from "@nodelib/fs.walk"; import * as path from "node:path"; import * as util from "node:util"; -import * as url from "node:url"; import logger from '../util/logger'; import { ModelicaDocument } from "./document"; @@ -45,39 +44,43 @@ import { ModelicaProject } from "./project"; export class ModelicaLibrary { readonly #project: ModelicaProject; - readonly #uri: string; - readonly #documents: Map; + readonly #documents: Map; readonly #isWorkspace: boolean; + #path: string; - private constructor(project: ModelicaProject, uri: LSP.URI, isWorkspace: boolean) { + private constructor(project: ModelicaProject, libraryPath: string, isWorkspace: boolean) { this.#project = project; - this.#uri = uri; + this.#path = libraryPath, this.#documents = new Map(); this.#isWorkspace = isWorkspace; } public static async load( project: ModelicaProject, - uri: LSP.URI, + libraryPath: string, isWorkspace: boolean, ): Promise { - logger.info(`Loading ${isWorkspace ? 'workspace' : 'library'} at '${uri}'...`); + logger.info(`Loading ${isWorkspace ? 'workspace' : 'library'} at '${libraryPath}'...`); + + const library = new ModelicaLibrary(project, libraryPath, isWorkspace); + const workspaceRootDocument = await ModelicaDocument.load(library, path.join(libraryPath, "package.mo")); + + // Find the root path of the library and update library.#path. + // It might have been set incorrectly if we opened a child folder. + for (let i = 0; i < workspaceRootDocument.packagePath.length - 1; i++) { + library.#path = path.dirname(library.#path); + } + + logger.debug(`Set library path to ${library.path}`); const walk = util.promisify(fsWalk.walk); - const entries = await walk(url.fileURLToPath(uri), { + const entries = await walk(library.#path, { entryFilter: (entry) => !!entry.name.match(/.*\.mo/) && !entry.dirent.isDirectory(), }); - const library = new ModelicaLibrary(project, uri, isWorkspace); for (const entry of entries) { - let documentUri = url.pathToFileURL(entry.path).href; - // Note: LSP sends us file uris containing '%3A' instead of ':', but - // the node pathToFileURL uses ':' anyways. Manually fix this here. - // This is a bit hacky but we should ideally only be working with the URIs from LSP anyways. - documentUri = documentUri.slice(0, 5) + documentUri.slice(5).replace(":", "%3A"); - - const document = await ModelicaDocument.load(library, documentUri); - library.#documents.set(documentUri, document); + const document = await ModelicaDocument.load(library, entry.path); + library.#documents.set(entry.path, document); } logger.debug(`Loaded ${library.#documents.size} documents`); @@ -89,18 +92,14 @@ export class ModelicaLibrary { } public get path(): string { - return url.fileURLToPath(this.#uri); - } - - public get uri(): string { - return this.#uri; + return this.#path; } public get project(): ModelicaProject { return this.#project; } - public get documents(): Map { + public get documents(): Map { return this.#documents; } diff --git a/server/src/project/project.ts b/server/src/project/project.ts index a864f8f..f7509cc 100644 --- a/server/src/project/project.ts +++ b/server/src/project/project.ts @@ -65,16 +65,16 @@ export class ModelicaProject { * @param uri file:// uri pointing to the document * @returns the document, or `undefined` if no such document exists */ - public getDocument(uri: LSP.DocumentUri): ModelicaDocument | undefined { + public getDocument(documentPath: string): ModelicaDocument | undefined { for (const library of this.#libraries) { - const doc = library.documents.get(uri); + const doc = library.documents.get(documentPath); if (doc) { logger.debug(`Found document: ${doc.path}`); return doc; } } - logger.debug(`Couldn't find document: ${uri}`); + logger.debug(`Couldn't find document: ${documentPath}`); return undefined; } @@ -82,10 +82,9 @@ export class ModelicaProject { /** * Adds a new document to the LSP. */ - public async addDocument(uri: LSP.DocumentUri): Promise { - logger.info(`Adding document at '${uri}'...`); + public async addDocument(documentPath: string): Promise { + logger.info(`Adding document at '${documentPath}'...`); - const documentPath = url.fileURLToPath(uri); for (const library of this.#libraries) { const relative = path.relative(library.path, documentPath); const isSubdirectory = relative && !relative.startsWith("..") && !path.isAbsolute(relative); @@ -93,13 +92,13 @@ export class ModelicaProject { // Assume that files can't be inside multiple libraries at the same time if (isSubdirectory) { const document = await ModelicaDocument.load(library, documentPath); - library.documents.set(uri, document); - logger.debug(`Added document: ${uri}`); + library.documents.set(documentPath, document); + logger.debug(`Added document: ${documentPath}`); return; } } - throw new Error(`Failed to add document '${uri}': not a part of any libraries.`); + throw new Error(`Failed to add document '${documentPath}': not a part of any libraries.`); } /** @@ -108,29 +107,29 @@ export class ModelicaProject { * @param text the modification * @param range range to update, or undefined to replace the whole file */ - public updateDocument(uri: LSP.DocumentUri, text: string, range?: LSP.Range): void { - logger.debug(`Updating document at '${uri}'...`); + public updateDocument(documentPath: string, text: string, range?: LSP.Range): void { + logger.debug(`Updating document at '${documentPath}'...`); - const doc = this.getDocument(uri); + const doc = this.getDocument(documentPath); if (doc) { doc.update(text, range); - logger.debug(`Updated document '${uri}'`); + logger.debug(`Updated document '${documentPath}'`); } else { - logger.warn(`Failed to update document '${uri}': not loaded`); + logger.warn(`Failed to update document '${documentPath}': not loaded`); } } /** * Removes a document from the cache. */ - public removeDocument(uri: LSP.DocumentUri): void { - logger.info(`Removing document at '${uri}'...`); + public removeDocument(documentPath: string): void { + logger.info(`Removing document at '${documentPath}'...`); - const doc = this.getDocument(uri); + const doc = this.getDocument(documentPath); if (doc) { - doc.library.documents.delete(uri); + doc.library.documents.delete(documentPath); } else { - logger.warn(`Failed to remove document '${uri}': not loaded`); + logger.warn(`Failed to remove document '${documentPath}': not loaded`); } } diff --git a/server/src/server.ts b/server/src/server.ts index bb84807..5a91f08 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -50,6 +50,7 @@ import * as url from "node:url"; import { initializeParser } from "./parser"; import Analyzer from "./analyzer"; import { logger, setLogConnection, setLogLevel } from "./util/logger"; +import { uriToPath } from './util'; /** * ModelicaServer collection all the important bits and bobs. @@ -179,7 +180,7 @@ export class ModelicaServer { break; case LSP.FileChangeType.Changed: { // TODO: incremental? - const path = url.fileURLToPath(change.uri); + const path = uriToPath(change.uri); const content = await fs.readFile(path, "utf-8"); this.analyzer.updateDocument(change.uri, content); break; diff --git a/server/src/util/index.ts b/server/src/util/index.ts index 2cdc6f6..c7ea4ca 100644 --- a/server/src/util/index.ts +++ b/server/src/util/index.ts @@ -1,15 +1,14 @@ -export function getOverlappingLength(parent: T[], child: T[]): number; -export function getOverlappingLength(parent: string, child: string): number; -export function getOverlappingLength(parent: Record, child: Record & { length: number }): number { - let matchedLength = 0; - for (let i = 0; i < child.length; i++) { - if (parent[i] !== child[i]) { - break; - } - matchedLength++; - } +import * as url from "node:url"; +import * as LSP from "vscode-languageserver"; - return matchedLength; -} +export const uriToPath = url.fileURLToPath; +export function pathToUri(filePath: string): LSP.URI { + const uri = url.pathToFileURL(filePath).href; + + // Note: LSP sends us file uris containing '%3A' instead of ':', but + // the node pathToFileURL uses ':' anyways. Manually fix this here. + // This is a bit hacky but we should ideally only be working with the URIs from LSP anyways. + return uri.slice(0, 5) + uri.slice(5).replace(":", "%3A"); +} \ No newline at end of file diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index 85d4ff8..66cfd4f 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -230,8 +230,6 @@ export function getDeclaredIdentifiers(node: SyntaxNode): string[] { // TODO: does this support all desired node types? Are we considering too many nodes? switch (node.type) { - case "extends_clause": - return getName(node.childForFieldName("typeSpecifier")!.childForFieldName("name")!); case "declaration": case "derivative_class_specifier": case "enumeration_class_specifier": @@ -273,18 +271,6 @@ export function getDeclaredIdentifiers(node: SyntaxNode): string[] { } } -export function getDeclarationType(node: SyntaxNode): { symbols: string[]; global: boolean } { - const typeSpecifier = findFirst(node, (child) => child.type === "type_specifier"); - if (!typeSpecifier) { - throw new Error("Node does not contain a type_specifier"); - } - - return { - symbols: getName(typeSpecifier.childForFieldName("name")!), - global: typeSpecifier.childForFieldName("global") !== null, - }; -} - export function hasIdentifier(node: SyntaxNode | null, identifier: string): boolean { if (!node) { return false; @@ -293,17 +279,58 @@ export function hasIdentifier(node: SyntaxNode | null, identifier: string): bool return getDeclaredIdentifiers(node).includes(identifier); } -/** - * Converts a name `SyntaxNode` into an array of the `IDENT`s in that node. - */ -export function getName(nameNode: SyntaxNode): string[] { - return getNameIdentifiers(nameNode).map((identNode) => identNode.text); +export interface TypeSpecifier { + isGlobal: boolean; + symbols: string[]; + symbolNodes: SyntaxNode[]; +} + +export function getDeclaredType(node: SyntaxNode): TypeSpecifier { + switch (node.type) { + case "type_specifier": { + const isGlobal = node.childForFieldName("global") !== null; + const name = node.childForFieldName("name")!; + const symbolNodes = getNameIdentifiers(name); + return { + isGlobal, + symbols: symbolNodes.map((id) => id.text), + symbolNodes, + }; + } + case "name": { + const symbolNodes = getNameIdentifiers(node); + return { + isGlobal: false, + symbols: symbolNodes.map((id) => id.text), + symbolNodes, + }; + } + case "IDENT": + return { + isGlobal: false, + symbols: [node.text], + symbolNodes: [node], + }; + default: { + const typeSpecifier = findFirst(node, (child) => child.type === "type_specifier"); + if (typeSpecifier) { + return getDeclaredType(typeSpecifier); + } + + const name = findFirst(node, (child) => child.type === "name"); + if (name) { + return getDeclaredType(name); + } + + throw new Error("Syntax node does not contain a type_specifier or name"); + } + } } /** * Converts a name `SyntaxNode` into an array of the `IDENT`s in that node. */ -export function getNameIdentifiers(nameNode: SyntaxNode): Parser.SyntaxNode[] { +function getNameIdentifiers(nameNode: SyntaxNode): Parser.SyntaxNode[] { if (nameNode.type !== "name") { throw new Error(`Expected a 'name' node; got '${nameNode.type}' (${nameNode.text})`); } From e01576647c7aeccd9412791213bc1af3b604a546 Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Fri, 3 May 2024 13:55:31 +0200 Subject: [PATCH 17/24] Fix endless recursion bug in resolveReference Co-authored-by: PaddiM8 --- server/src/analysis/reference.ts | 50 +++++-- server/src/analysis/resolveReference.ts | 187 ++++++++++++++++-------- 2 files changed, 161 insertions(+), 76 deletions(-) diff --git a/server/src/analysis/reference.ts b/server/src/analysis/reference.ts index b87c475..e20b0a3 100644 --- a/server/src/analysis/reference.ts +++ b/server/src/analysis/reference.ts @@ -1,18 +1,23 @@ import { ModelicaDocument } from "../project/document"; import Parser from "web-tree-sitter"; +export type ReferenceKind = "class" | "variable"; + export abstract class BaseUnresolvedReference { /** * The path to the symbol reference. */ public readonly symbols: string[]; - public constructor(symbols: string[]) { + public readonly kind: ReferenceKind | undefined; + + public constructor(symbols: string[], kind?: ReferenceKind) { if (symbols.length === 0) { throw new Error("Symbols length must be greater tham 0"); } this.symbols = symbols; + this.kind = kind; } public abstract isAbsolute(): this is UnresolvedAbsoluteReference; @@ -29,8 +34,13 @@ export class UnresolvedRelativeReference extends BaseUnresolvedReference { */ public readonly node: Parser.SyntaxNode; - public constructor(document: ModelicaDocument, node: Parser.SyntaxNode, symbols: string[]) { - super(symbols); + public constructor( + document: ModelicaDocument, + node: Parser.SyntaxNode, + symbols: string[], + kind?: ReferenceKind, + ) { + super(symbols, kind); this.document = document; this.node = node; } @@ -41,15 +51,20 @@ export class UnresolvedRelativeReference extends BaseUnresolvedReference { public toString(): string { const start = this.node.startPosition; - const pos = `${start.row + 1}:${start.column + 1}`; - - return `?${this.symbols.join(".")} at ${pos} in "${this.document.path}"`; + return ( + `UnresolvedReference { ` + + `symbols: ${this.symbols.join(".")}, ` + + `kind: ${this.kind}, ` + + `position: ${start.row + 1}:${start.column + 1}, ` + + `document: "${this.document.path}" ` + + `}` + ); } } export class UnresolvedAbsoluteReference extends BaseUnresolvedReference { - public constructor(symbols: string[]) { - super(symbols); + public constructor(symbols: string[], kind?: ReferenceKind) { + super(symbols, kind); } public isAbsolute(): this is UnresolvedAbsoluteReference { @@ -57,7 +72,12 @@ export class UnresolvedAbsoluteReference extends BaseUnresolvedReference { } public toString(): string { - return `?.${this.symbols.join(".")}`; + return ( + `UnresolvedReference { ` + + `symbols: .${this.symbols.join(".")}, ` + + `kind: ${this.kind} ` + + `}` + ); } } @@ -79,7 +99,14 @@ export class ResolvedReference { */ readonly symbols: string[]; - public constructor(document: ModelicaDocument, node: Parser.SyntaxNode, symbols: string[]) { + readonly kind: ReferenceKind; + + public constructor( + document: ModelicaDocument, + node: Parser.SyntaxNode, + symbols: string[], + kind: ReferenceKind, + ) { if (symbols.length === 0) { throw new Error("Symbols length must be greater than 0."); } @@ -87,9 +114,10 @@ export class ResolvedReference { this.document = document; this.node = node; this.symbols = symbols; + this.kind = kind; } public toString(): string { - return `.${this.symbols.join(".")}`; + return `Reference { symbols: .${this.symbols.join(".")}, kind: ${this.kind} }`; } } diff --git a/server/src/analysis/resolveReference.ts b/server/src/analysis/resolveReference.ts index a8a155e..0d99c9f 100644 --- a/server/src/analysis/resolveReference.ts +++ b/server/src/analysis/resolveReference.ts @@ -1,10 +1,10 @@ import Parser from "web-tree-sitter"; import * as fs from "node:fs"; import * as path from "node:path"; -import * as url from "node:url"; import * as TreeSitterUtil from "../util/tree-sitter"; import { + ReferenceKind, ResolvedReference, UnresolvedAbsoluteReference, UnresolvedReference, @@ -79,11 +79,10 @@ function absolutize(reference: UnresolvedRelativeReference): UnresolvedAbsoluteR logger.debug(`Found local: ${local} with ancestors: ${ancestors}`); - return new UnresolvedAbsoluteReference([ - ...local.document.within, - ...ancestors, - ...local.symbols, - ]); + return new UnresolvedAbsoluteReference( + [...local.document.within, ...ancestors, ...local.symbols], + local.kind, + ); } /** @@ -99,27 +98,60 @@ function absolutize(reference: UnresolvedRelativeReference): UnresolvedAbsoluteR function findReferenceInDocument( reference: UnresolvedRelativeReference, ): UnresolvedReference | null { - if ( - TreeSitterUtil.isDefinition(reference.node) && - TreeSitterUtil.hasIdentifier(reference.node, reference.symbols[0]) - ) { - return reference; + const maybeClass = reference.kind === "class" || reference.kind === undefined; + const maybeVariable = reference.kind === "variable" || reference.kind === undefined; + + if (maybeClass) { + logger.debug("findReferenceInDocument: Checking if this node is a class..."); + if ( + TreeSitterUtil.isDefinition(reference.node) && + TreeSitterUtil.hasIdentifier(reference.node, reference.symbols[0]) + ) { + return new UnresolvedRelativeReference( + reference.document, + reference.node, + reference.symbols, + "class", + ); + } + + logger.debug("findReferenceInDocument: Checking for child class..."); + const classDecl = reference.node.children.find( + (child) => + TreeSitterUtil.isDefinition(child) && + TreeSitterUtil.hasIdentifier(child, reference.symbols[0]), + ); + if (classDecl) { + logger.debug("Found local class"); + return new UnresolvedRelativeReference(reference.document, classDecl, reference.symbols, "class"); + } } - logger.debug("findReferenceInDocument: Checking for local class or variable..."); - const decl = reference.node.children.find((child) => { - return ( - (TreeSitterUtil.isDefinition(child) || TreeSitterUtil.isVariableDeclaration(child)) && - TreeSitterUtil.hasIdentifier(child, reference.symbols[0]) + if (maybeVariable) { + logger.debug("findReferenceInDocument: Checking for child variable..."); + const varDecl = reference.node.children.find( + (child) => + TreeSitterUtil.isVariableDeclaration(child) && + TreeSitterUtil.hasIdentifier(child, reference.symbols[0]), ); - }); - if (decl) { - logger.debug("Found local"); - return new UnresolvedRelativeReference(reference.document, decl, reference.symbols); + if (varDecl) { + logger.debug("Found local variable"); + return new UnresolvedRelativeReference( + reference.document, + varDecl, + reference.symbols, + "variable", + ); + } } logger.debug("findReferenceInDocument: Checking for declarations in class..."); - const declInClass = findDeclarationInClass(reference.document, reference.node, reference.symbols); + const declInClass = findDeclarationInClass( + reference.document, + reference.node, + reference.symbols, + reference.kind, + ); if (declInClass) { return declInClass; } @@ -145,30 +177,31 @@ function findReferenceInDocument( if (reference.node.parent) { logger.debug("findReferenceInDocument: Checking parent node..."); return findReferenceInDocument( - new UnresolvedRelativeReference(reference.document, reference.node.parent, reference.symbols), + new UnresolvedRelativeReference(reference.document, reference.node.parent, reference.symbols, reference.kind), ); } // TODO: check subpackages - logger.warn("NOT checking subpackages!"); + logger.warn("NOT checking subpackages! (TODO)"); const referenceWithPackagePath = new UnresolvedAbsoluteReference([ ...reference.document.packagePath, ...reference.symbols, - ]); + ], reference.kind); if (resolveAbsoluteReference(reference.document.project, referenceWithPackagePath)) { return referenceWithPackagePath; } logger.debug("Not found in document. This reference is either global or undefined."); - return new UnresolvedAbsoluteReference(reference.symbols); + return new UnresolvedAbsoluteReference(reference.symbols, reference.kind); } function findDeclarationInClass( document: ModelicaDocument, classNode: Parser.SyntaxNode, symbols: string[], -): UnresolvedRelativeReference | undefined { + referenceKind: ReferenceKind | undefined, +): (UnresolvedRelativeReference & { kind: ReferenceKind }) | undefined { if (classNode.type !== "class_definition") { return undefined; } @@ -189,60 +222,81 @@ function findDeclarationInClass( return undefined; } - const field = elements.find( + const namedElement = elements.find( ([element, idents]) => element.type === "named_element" && idents.includes(symbols[0]), ); - if (field) { - logger.debug(`Resolved ${symbols[0]} to field: ${field[1]}`); + if (namedElement) { + logger.debug(`Resolved ${symbols[0]} to field: ${namedElement[1]}`); - const classDef = field[0].childForFieldName("classDefinition"); + const classDef = namedElement[0].childForFieldName("classDefinition"); if (classDef) { - return new UnresolvedRelativeReference(document, classDef, symbols); + return new UnresolvedRelativeReference( + document, + classDef, + symbols, + "class", + ) as UnresolvedRelativeReference & { kind: ReferenceKind }; } + const componentDef = namedElement[0].childForFieldName("componentClause")!; + // TODO: this handles named_elements but what if it's an import clause? - return new UnresolvedRelativeReference(document, field[0], symbols); + return new UnresolvedRelativeReference( + document, + componentDef, + symbols, + "variable", + ) as UnresolvedRelativeReference & { kind: ReferenceKind }; } - const extendsClauses = elements - .map(([element, _idents]) => element) - .filter((element) => element.type === "extends_clause"); - for (const extendsClause of extendsClauses) { - const superclassType = TreeSitterUtil.getDeclaredType(extendsClause); - const unresolvedSuperclass = superclassType.isGlobal - ? new UnresolvedAbsoluteReference(superclassType.symbols) - : absolutize( - new UnresolvedRelativeReference(document, extendsClause, superclassType.symbols), - ); - - if (unresolvedSuperclass == null) { - logger.warn(`Superclass ${superclassType.symbols} not found`); - continue; - } + // only check superclasses if we know we're not looking for a class + if (referenceKind !== "class") { + const extendsClauses = elements + .map(([element, _idents]) => element) + .filter((element) => element.type === "extends_clause"); + for (const extendsClause of extendsClauses) { + const superclassType = TreeSitterUtil.getDeclaredType(extendsClause); + const unresolvedSuperclass = superclassType.isGlobal + ? new UnresolvedAbsoluteReference(superclassType.symbols, "class") + : absolutize( + new UnresolvedRelativeReference( + document, + extendsClause, + superclassType.symbols, + "class", + ), + ); + + if (unresolvedSuperclass == null) { + logger.warn(`Superclass ${superclassType.symbols} not found`); + continue; + } - logger.debug( - `Resolving superclass ${unresolvedSuperclass} (of ${ - TreeSitterUtil.getDeclaredIdentifiers(classNode)[0] - })`, - ); + logger.debug( + `Resolving superclass ${unresolvedSuperclass} (of ${ + TreeSitterUtil.getDeclaredIdentifiers(classNode)[0] + })`, + ); - const superclass = resolveAbsoluteReference(document.project, unresolvedSuperclass); - if (!superclass) { - logger.warn(`Could not find superclass ${unresolvedSuperclass}`); - continue; - } + const superclass = resolveAbsoluteReference(document.project, unresolvedSuperclass); + if (!superclass) { + logger.warn(`Could not find superclass ${unresolvedSuperclass}`); + continue; + } - logger.debug(`Checking superclass ${superclass}`); - const decl = findDeclarationInClass(superclass.document, superclass.node, symbols); - if (decl) { - logger.debug(`Declaration ${decl} found in superclass ${superclass}`); - return decl; + logger.debug(`Checking superclass ${superclass}`); + const decl = findDeclarationInClass(superclass.document, superclass.node, symbols, "class"); + if (decl) { + logger.debug(`Declaration ${decl} found in superclass ${superclass}`); + return decl; + } } } return undefined; } + function resolveImportClause( project: ModelicaProject, symbols: string[], @@ -384,7 +438,7 @@ function resolveNext( return null; } - return new ResolvedReference(document, packageClass, reference.symbols.slice(0, 1)); + return new ResolvedReference(document, packageClass, reference.symbols.slice(0, 1), "class"); } const nextSymbolIndex = alreadyResolved.symbols.length; @@ -417,6 +471,7 @@ function resolveNext( document, packageClass, reference.symbols.slice(0, nextSymbolIndex + 1), + "class", ); } @@ -427,12 +482,14 @@ function resolveNext( alreadyResolved.document, alreadyResolved.node, reference.symbols.slice(nextSymbolIndex), + alreadyResolved.kind, ); if (child) { return new ResolvedReference( child.document, child.node, reference.symbols.slice(0, nextSymbolIndex + 1), + child.kind, ); } @@ -477,9 +534,9 @@ function variableRefToClassRef(varRef: ResolvedReference): ResolvedReference | n const absoluteReference = (() => { if (type.isGlobal) { - return new UnresolvedAbsoluteReference(type.symbols); + return new UnresolvedAbsoluteReference(type.symbols, "class"); } else { - const typeRef = new UnresolvedRelativeReference(varRef.document, varRef.node, type.symbols); + const typeRef = new UnresolvedRelativeReference(varRef.document, varRef.node, type.symbols, "class"); return absolutize(typeRef); } })(); From 2d0cc19b8f107cef96fd9680d905d78f47feec6f Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Fri, 3 May 2024 16:48:05 +0200 Subject: [PATCH 18/24] Fix resolution algorithm --- server/src/analysis/resolveReference.ts | 252 +++++++++++++----------- server/src/analyzer.ts | 6 +- 2 files changed, 136 insertions(+), 122 deletions(-) diff --git a/server/src/analysis/resolveReference.ts b/server/src/analysis/resolveReference.ts index 0d99c9f..0f56d80 100644 --- a/server/src/analysis/resolveReference.ts +++ b/server/src/analysis/resolveReference.ts @@ -33,14 +33,18 @@ export default function resolveReference( throw new Error("Resolving definitions not yet supported!"); } - const absoluteReference = reference.isAbsolute() ? reference : absolutize(reference); - if (absoluteReference === null) { - return null; - } else if (absoluteReference instanceof ResolvedReference) { - return absoluteReference; + if (reference instanceof UnresolvedAbsoluteReference) { + return resolveAbsoluteReference(project, reference); + } + + for (const ref of getAbsoluteReferenceCandidates(reference)) { + const resolved = resolveAbsoluteReference(project, ref); + if (resolved) { + return resolved; + } } - return resolveAbsoluteReference(project, absoluteReference); + return null; } /** @@ -49,40 +53,50 @@ export default function resolveReference( * @param reference a relative reference to a symbol declaration/definition * @returns an absolute reference to that symbol, or `null` if no such symbol exists. */ -function absolutize(reference: UnresolvedRelativeReference): UnresolvedAbsoluteReference | null { - logger.debug(`Absolutize: ${reference}`); - const local = findReferenceInDocument(reference); - if (local === null) { - logger.debug(`Didn't find symbol ${reference}`); - return null; - } else if (local instanceof UnresolvedAbsoluteReference) { - logger.debug(`Found absolute reference ${local}`); - return local; - } +function* getAbsoluteReferenceCandidates( + reference: UnresolvedRelativeReference, +): Generator { + logger.debug(`Checking candidates for ${reference}`); + + for (const local of findReferenceInDocument(reference)) { + if (local instanceof UnresolvedAbsoluteReference) { + logger.debug(`Found ${local}`); + yield local; + continue; + } + + const relativeReference = local ?? reference; - const ancestors: string[] = []; - let currentNode = local.node; - while (currentNode.parent) { - if (currentNode.type === "class_definition") { - const identifier = TreeSitterUtil.getDeclaredIdentifiers(currentNode).at(0); - if (identifier) { - ancestors.unshift(identifier); + const ancestors: string[] = []; + let currentNode: Parser.SyntaxNode | null = relativeReference.node; + while (currentNode) { + if (currentNode.type === "class_definition") { + const identifier = TreeSitterUtil.getDeclaredIdentifiers(currentNode).at(0); + if (identifier) { + ancestors.unshift(identifier); + } } + + currentNode = currentNode.parent; } - currentNode = currentNode.parent; - } + if (relativeReference.node.type === "class_definition") { + ancestors.pop(); + } - if (local.node.type === "class_definition") { - ancestors.pop(); - } + logger.debug(`Found ${relativeReference} with ancestors: [${ancestors}]`); - logger.debug(`Found local: ${local} with ancestors: ${ancestors}`); + const classPath = [...relativeReference.document.within, ...ancestors]; + while (true) { + yield new UnresolvedAbsoluteReference([...classPath, ...relativeReference.symbols], relativeReference.kind); + if (classPath.length === 0) { + break; + } + classPath.pop(); + } + } - return new UnresolvedAbsoluteReference( - [...local.document.within, ...ancestors, ...local.symbols], - local.kind, - ); + logger.debug(`Didn't find ${reference}`); } /** @@ -94,28 +108,30 @@ function absolutize(reference: UnresolvedRelativeReference): UnresolvedAbsoluteR * (1) a relative reference in which the `document` and `node` properties reference * the symbol's declaration/definition, * (2) an absolute reference + * (3) `undefined` (not in the document) */ -function findReferenceInDocument( +function* findReferenceInDocument( reference: UnresolvedRelativeReference, -): UnresolvedReference | null { +): Generator { const maybeClass = reference.kind === "class" || reference.kind === undefined; const maybeVariable = reference.kind === "variable" || reference.kind === undefined; if (maybeClass) { - logger.debug("findReferenceInDocument: Checking if this node is a class..."); + // logger.debug("findReferenceInDocument: Checking if this node is a class..."); if ( TreeSitterUtil.isDefinition(reference.node) && TreeSitterUtil.hasIdentifier(reference.node, reference.symbols[0]) ) { - return new UnresolvedRelativeReference( + yield new UnresolvedRelativeReference( reference.document, reference.node, reference.symbols, "class", ); + return; } - logger.debug("findReferenceInDocument: Checking for child class..."); + // logger.debug("findReferenceInDocument: Checking for child class..."); const classDecl = reference.node.children.find( (child) => TreeSitterUtil.isDefinition(child) && @@ -123,12 +139,18 @@ function findReferenceInDocument( ); if (classDecl) { logger.debug("Found local class"); - return new UnresolvedRelativeReference(reference.document, classDecl, reference.symbols, "class"); + yield new UnresolvedRelativeReference( + reference.document, + classDecl, + reference.symbols, + "class", + ); + return; } } if (maybeVariable) { - logger.debug("findReferenceInDocument: Checking for child variable..."); + // logger.debug("findReferenceInDocument: Checking for child variable..."); const varDecl = reference.node.children.find( (child) => TreeSitterUtil.isVariableDeclaration(child) && @@ -136,16 +158,17 @@ function findReferenceInDocument( ); if (varDecl) { logger.debug("Found local variable"); - return new UnresolvedRelativeReference( + yield new UnresolvedRelativeReference( reference.document, varDecl, reference.symbols, "variable", ); + return; } } - logger.debug("findReferenceInDocument: Checking for declarations in class..."); + // logger.debug("findReferenceInDocument: Checking for declarations in class..."); const declInClass = findDeclarationInClass( reference.document, reference.node, @@ -153,47 +176,46 @@ function findReferenceInDocument( reference.kind, ); if (declInClass) { - return declInClass; + yield declInClass; + return; } const importClauses = reference.node.parent?.children.filter( (child) => child.type === "import_clause", ); if (importClauses && importClauses.length > 0) { - logger.debug("findReferenceInDocument: Checking imports..."); + // logger.debug("findReferenceInDocument: Checking imports..."); for (const importClause of importClauses) { - const importResult = resolveImportClause( - reference.document.project, - reference.symbols, - importClause, - ); - if (importResult) { - logger.debug("findReferenceInDocument: found import!"); - return importResult; + const { importCandidate, wildcard } = resolveImportClause(reference.symbols, importClause); + if (importCandidate) { + // logger.debug("findReferenceInDocument: found import!"); + + if (wildcard) { + yield importCandidate; + } else { + yield importCandidate; + return; + } } } } if (reference.node.parent) { - logger.debug("findReferenceInDocument: Checking parent node..."); - return findReferenceInDocument( - new UnresolvedRelativeReference(reference.document, reference.node.parent, reference.symbols, reference.kind), + // logger.debug("findReferenceInDocument: Checking parent node..."); + yield* findReferenceInDocument( + new UnresolvedRelativeReference( + reference.document, + reference.node.parent, + reference.symbols, + reference.kind, + ), ); + return; } - // TODO: check subpackages - logger.warn("NOT checking subpackages! (TODO)"); - - const referenceWithPackagePath = new UnresolvedAbsoluteReference([ - ...reference.document.packagePath, - ...reference.symbols, - ], reference.kind); - if (resolveAbsoluteReference(reference.document.project, referenceWithPackagePath)) { - return referenceWithPackagePath; - } - - logger.debug("Not found in document. This reference is either global or undefined."); - return new UnresolvedAbsoluteReference(reference.symbols, reference.kind); + logger.debug("Not found in document. May be a global?"); + yield undefined; + return; } function findDeclarationInClass( @@ -206,10 +228,10 @@ function findDeclarationInClass( return undefined; } - logger.debug( - `findDeclarationInClass: Checking for declaration ${symbols.join(".")} ` + - `in class: ${TreeSitterUtil.getDeclaredIdentifiers(classNode)}`, - ); + // logger.debug( + // `findDeclarationInClass: Checking for declaration ${symbols.join(".")} ` + + // `in class: ${TreeSitterUtil.getDeclaredIdentifiers(classNode)}`, + // ); const elements = classNode .childForFieldName("classSpecifier") @@ -258,19 +280,7 @@ function findDeclarationInClass( const superclassType = TreeSitterUtil.getDeclaredType(extendsClause); const unresolvedSuperclass = superclassType.isGlobal ? new UnresolvedAbsoluteReference(superclassType.symbols, "class") - : absolutize( - new UnresolvedRelativeReference( - document, - extendsClause, - superclassType.symbols, - "class", - ), - ); - - if (unresolvedSuperclass == null) { - logger.warn(`Superclass ${superclassType.symbols} not found`); - continue; - } + : new UnresolvedRelativeReference(document, extendsClause, superclassType.symbols, "class"); logger.debug( `Resolving superclass ${unresolvedSuperclass} (of ${ @@ -278,7 +288,8 @@ function findDeclarationInClass( })`, ); - const superclass = resolveAbsoluteReference(document.project, unresolvedSuperclass); + // TODO: support "definition" resolution + const superclass = resolveReference(document.project, unresolvedSuperclass, "declaration"); if (!superclass) { logger.warn(`Could not find superclass ${unresolvedSuperclass}`); continue; @@ -296,12 +307,30 @@ function findDeclarationInClass( return undefined; } +interface ResolveImportClauseResult { + /** + * The resolved import candidate, or `undefined` if none was found. + */ + importCandidate?: UnresolvedAbsoluteReference; + /** + * `true` if this was a wildcard import, and we are not sure if this import even exists. + * `false` if this was not a wildcard import. + */ + wildcard: boolean; +} +/** + * Given an import clause and a potentially-imported symbol, returns an + * unresolved reference to check. + * + * @param symbols a symbol that may have been imported + * @param importClause an import clause + * @returns the resolved import + */ function resolveImportClause( - project: ModelicaProject, symbols: string[], importClause: Parser.SyntaxNode, -): UnresolvedAbsoluteReference | null { +): ResolveImportClauseResult { // imports are always relative according to the grammar const importPath = TreeSitterUtil.getDeclaredType( importClause.childForFieldName("name")!, @@ -315,9 +344,7 @@ function resolveImportClause( // TODO: this should probably not resolve the reference fully, then immediately // discard it so it can do so again. - if (resolveAbsoluteReference(project, importCandidate)) { - return importCandidate; - } + return { importCandidate, wildcard: true }; } // import alias: import z = a.b.c; @@ -328,7 +355,7 @@ function resolveImportClause( const importCandidate = new UnresolvedAbsoluteReference([...importPath, ...symbols.slice(1)]); logger.debug(`Candidate: ${importCandidate} (from import ${alias} = ${importPath.join(".")})`); - return importCandidate; + return { importCandidate, wildcard: false }; } // multi-import: import a.b.{c, d, e}; @@ -342,7 +369,7 @@ function resolveImportClause( const importString = `import ${importPath.join(".")}.{ ${childImports.join(", ")} }`; logger.debug(`Candidate: ${importCandidate} (from ${importString})`); - return importCandidate; + return { importCandidate, wildcard: false }; } // normal import: import a.b.c; @@ -350,10 +377,10 @@ function resolveImportClause( const importCandidate = new UnresolvedAbsoluteReference([...importPath, ...symbols.slice(1)]); logger.debug(`Candidate: ${importCandidate} (from import ${importPath.join(".")})`); - return importCandidate; + return { importCandidate, wildcard: false }; } - return null; + return { wildcard: false }; } /** @@ -383,7 +410,7 @@ function resolveAbsoluteReference( let alreadyResolved: ResolvedReference | null = null; for (let i = 0; i < reference.symbols.length; i++) { alreadyResolved = resolveNext(library, reference, alreadyResolved); - logger.debug(`resolveNext found symbol: ${alreadyResolved != null}`); + // logger.debug(`resolveNext found symbol: ${alreadyResolved != null}`); if (alreadyResolved == null) { return null; } @@ -443,11 +470,11 @@ function resolveNext( const nextSymbolIndex = alreadyResolved.symbols.length; const nextSymbol = reference.symbols[nextSymbolIndex]; - logger.debug( - `Resolve next: ${nextSymbol} (alreadyResolved: ${ - alreadyResolved.node.type - } with ident ${TreeSitterUtil.getIdentifier(alreadyResolved.node)})`, - ); + // logger.debug( + // `Resolve next: ${nextSymbol} (alreadyResolved: ${ + // alreadyResolved.node.type + // } with ident ${TreeSitterUtil.getIdentifier(alreadyResolved.node)})`, + // ); // If there is a document for nextSymbol blablabla const dirName = path.dirname(alreadyResolved.document.path); @@ -455,7 +482,7 @@ function resolveNext( path.join(dirName, `${nextSymbol}.mo`), path.join(dirName, `${nextSymbol}/package.mo`), ]; - logger.debug(`resolveNext potentialPaths: ${potentialPaths}`); + //logger.debug(`resolveNext potentialPaths: ${potentialPaths}`); for (const documentPath of potentialPaths) { if (!fs.existsSync(documentPath)) { continue; @@ -532,22 +559,9 @@ function getPackageClassFromFilePath( function variableRefToClassRef(varRef: ResolvedReference): ResolvedReference | null { const type = TreeSitterUtil.getDeclaredType(varRef.node); - const absoluteReference = (() => { - if (type.isGlobal) { - return new UnresolvedAbsoluteReference(type.symbols, "class"); - } else { - const typeRef = new UnresolvedRelativeReference(varRef.document, varRef.node, type.symbols, "class"); - return absolutize(typeRef); - } - })(); - - if (absoluteReference === null) { - return null; - } - - if (absoluteReference instanceof ResolvedReference) { - return absoluteReference; - } + const typeRef = type.isGlobal + ? new UnresolvedAbsoluteReference(type.symbols, "class") + : new UnresolvedRelativeReference(varRef.document, varRef.node, type.symbols, "class"); - return resolveAbsoluteReference(varRef.document.project, absoluteReference); + return resolveReference(varRef.document.project, typeRef, "declaration"); } diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index d8ad79f..7e646b0 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -87,15 +87,15 @@ export default class Analyzer { } public addDocument(uri: LSP.DocumentUri): void { - this.#project.addDocument(uri); + this.#project.addDocument(uriToPath(uri)); } public updateDocument(uri: LSP.DocumentUri, text: string, range?: LSP.Range): void { - this.#project.updateDocument(uri, text, range); + this.#project.updateDocument(uriToPath(uri), text, range); } public removeDocument(uri: LSP.DocumentUri): void { - this.#project.removeDocument(uri); + this.#project.removeDocument(uriToPath(uri)); } /** From b597f9a579a6346135d68baf7485575393fe0faa Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Tue, 7 May 2024 11:06:56 +0200 Subject: [PATCH 19/24] Tweak resolution algorithm Co-authored-by: PaddiM8 --- server/package.json | 2 - server/src/analysis/resolveReference.ts | 87 ++++++------ server/src/analyzer.ts | 168 ++++++++++++++++-------- server/src/server.ts | 22 +--- server/src/util/tree-sitter.ts | 49 ++++++- 5 files changed, 203 insertions(+), 125 deletions(-) diff --git a/server/package.json b/server/package.json index e8fee6d..afe8908 100644 --- a/server/package.json +++ b/server/package.json @@ -13,8 +13,6 @@ "url": "https://github.com/OpenModelica/modelica-language-server" }, "dependencies": { - "@nodelib/fs.walk": "^2.0.0", - "find-cache-dir": "^5.0.0", "tree-sitter": "^0.20.6", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.11", diff --git a/server/src/analysis/resolveReference.ts b/server/src/analysis/resolveReference.ts index 0f56d80..2c7c1ae 100644 --- a/server/src/analysis/resolveReference.ts +++ b/server/src/analysis/resolveReference.ts @@ -88,7 +88,10 @@ function* getAbsoluteReferenceCandidates( const classPath = [...relativeReference.document.within, ...ancestors]; while (true) { - yield new UnresolvedAbsoluteReference([...classPath, ...relativeReference.symbols], relativeReference.kind); + yield new UnresolvedAbsoluteReference( + [...classPath, ...relativeReference.symbols], + relativeReference.kind, + ); if (classPath.length === 0) { break; } @@ -213,7 +216,7 @@ function* findReferenceInDocument( return; } - logger.debug("Not found in document. May be a global?"); + logger.debug(`Not found in document. May be a global? ${reference}`); yield undefined; return; } @@ -277,7 +280,7 @@ function findDeclarationInClass( .map(([element, _idents]) => element) .filter((element) => element.type === "extends_clause"); for (const extendsClause of extendsClauses) { - const superclassType = TreeSitterUtil.getDeclaredType(extendsClause); + const superclassType = TreeSitterUtil.getTypeSpecifier(extendsClause); const unresolvedSuperclass = superclassType.isGlobal ? new UnresolvedAbsoluteReference(superclassType.symbols, "class") : new UnresolvedRelativeReference(document, extendsClause, superclassType.symbols, "class"); @@ -296,7 +299,12 @@ function findDeclarationInClass( } logger.debug(`Checking superclass ${superclass}`); - const decl = findDeclarationInClass(superclass.document, superclass.node, symbols, "class"); + const decl = findDeclarationInClass( + superclass.document, + superclass.node, + symbols, + "variable", + ); if (decl) { logger.debug(`Declaration ${decl} found in superclass ${superclass}`); return decl; @@ -332,7 +340,7 @@ function resolveImportClause( importClause: Parser.SyntaxNode, ): ResolveImportClauseResult { // imports are always relative according to the grammar - const importPath = TreeSitterUtil.getDeclaredType( + const importPath = TreeSitterUtil.getTypeSpecifier( importClause.childForFieldName("name")!, ).symbols; @@ -397,7 +405,7 @@ function resolveAbsoluteReference( throw new Error(`Reference is not an UnresolvedAbsoluteReference: ${reference}`); } - logger.debug(`Resolving absolute reference ${reference}`); + logger.debug(`Resolving ${reference}`); const library = project.libraries.find((lib) => lib.name === reference.symbols[0]); if (library == null) { @@ -405,18 +413,13 @@ function resolveAbsoluteReference( return null; } - logger.debug(`Found library ${library.name}; performing resolution: `); - let alreadyResolved: ResolvedReference | null = null; for (let i = 0; i < reference.symbols.length; i++) { - alreadyResolved = resolveNext(library, reference, alreadyResolved); - // logger.debug(`resolveNext found symbol: ${alreadyResolved != null}`); + alreadyResolved = resolveNext(library, reference.symbols[i], alreadyResolved); if (alreadyResolved == null) { return null; } - logger.debug(`Step ${i + 1}: ${alreadyResolved}`); - // If we're not done with the reference chain, we need to make sure that we know // the type of the variable in order to check its child variables if ( @@ -429,12 +432,11 @@ function resolveAbsoluteReference( return null; } - logger.debug(` => class: ${classRef}`); alreadyResolved = classRef; } } - logger.debug(`Resolved symbol ${alreadyResolved?.symbols} in ${alreadyResolved?.document.path}`); + logger.debug(`Resolved symbol ${alreadyResolved}`); return alreadyResolved; } @@ -442,47 +444,37 @@ function resolveAbsoluteReference( /** * Performs a single iteration of the resolution algorithm. * - * @param reference the entire reference - * @param alreadyResolved a resolved reference (to a class) + * @param nextSymbol the next symbol to resolve + * @param parentReference a resolved reference (to a class) * @returns the next resolved reference */ function resolveNext( library: ModelicaLibrary, - reference: UnresolvedAbsoluteReference, - alreadyResolved: ResolvedReference | null, + nextSymbol: string, + parentReference: ResolvedReference | null, ): ResolvedReference | null { // If at the root level, find the root package - if (!alreadyResolved) { - logger.debug(`Resolve next: ${reference.symbols[0]}`); + if (!parentReference) { const documentPath = path.join(library.path, "package.mo"); const [document, packageClass] = getPackageClassFromFilePath( library, documentPath, - reference.symbols[0], + nextSymbol, ); if (!document || !packageClass) { - logger.debug(`Couldn't find package class: ${reference.symbols[0]} in ${documentPath}`); + logger.debug(`Couldn't find package class: ${nextSymbol} in ${documentPath}`); return null; } - return new ResolvedReference(document, packageClass, reference.symbols.slice(0, 1), "class"); + return new ResolvedReference(document, packageClass, [nextSymbol], "class"); } - const nextSymbolIndex = alreadyResolved.symbols.length; - const nextSymbol = reference.symbols[nextSymbolIndex]; - // logger.debug( - // `Resolve next: ${nextSymbol} (alreadyResolved: ${ - // alreadyResolved.node.type - // } with ident ${TreeSitterUtil.getIdentifier(alreadyResolved.node)})`, - // ); - - // If there is a document for nextSymbol blablabla - const dirName = path.dirname(alreadyResolved.document.path); + const dirName = path.dirname(parentReference.document.path); const potentialPaths = [ path.join(dirName, `${nextSymbol}.mo`), path.join(dirName, `${nextSymbol}/package.mo`), ]; - //logger.debug(`resolveNext potentialPaths: ${potentialPaths}`); + for (const documentPath of potentialPaths) { if (!fs.existsSync(documentPath)) { continue; @@ -497,30 +489,35 @@ function resolveNext( return new ResolvedReference( document, packageClass, - reference.symbols.slice(0, nextSymbolIndex + 1), + [...parentReference.symbols, nextSymbol], "class", ); } - // If nextSymbol is in alreadyResolved.node: - // return the declaration - // TODO: Variable declarations may be nested inside an element list + // TODO: The `kind` parameter here should be `undefined` unless + // `resolveReference` was called with kind = "class" by + // the superclass handling section in findDeclarationInClass. + // ...or something like that + // As it is now, we don't know if `child` is a class or variable. We can't use + // `undefined` to indicate this because this results in infinite recursion. + // This issue causes us to be unable to look up variables declared in a superclass + // of a member variable. A redesign might be necessary to resolve this. const child = findDeclarationInClass( - alreadyResolved.document, - alreadyResolved.node, - reference.symbols.slice(nextSymbolIndex), - alreadyResolved.kind, + parentReference.document, + parentReference.node, + [nextSymbol], + parentReference.kind, ); if (child) { return new ResolvedReference( child.document, child.node, - reference.symbols.slice(0, nextSymbolIndex + 1), + [...parentReference.symbols, nextSymbol], child.kind, ); } - logger.debug(`Couldn't find document for ${nextSymbol}`); + logger.debug(`Couldn't find: .${parentReference.symbols.join(".")}.${nextSymbol}`); return null; } @@ -557,7 +554,7 @@ function getPackageClassFromFilePath( * @returns a reference to the class definition, or `null` if the type is not a class (e.g. a builtin like `Real`) */ function variableRefToClassRef(varRef: ResolvedReference): ResolvedReference | null { - const type = TreeSitterUtil.getDeclaredType(varRef.node); + const type = TreeSitterUtil.getTypeSpecifier(varRef.node); const typeRef = type.isGlobal ? new UnresolvedAbsoluteReference(type.symbols, "class") diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 7e646b0..8c62490 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -44,16 +44,18 @@ import Parser from "web-tree-sitter"; import * as fs from "node:fs/promises"; import * as fsSync from "node:fs"; import * as path from "node:path"; -import * as url from "node:url"; -import { UnresolvedRelativeReference } from "./analysis/reference"; +import { + UnresolvedAbsoluteReference, + UnresolvedReference, + UnresolvedRelativeReference, +} from "./analysis/reference"; import resolveReference from "./analysis/resolveReference"; -import { ModelicaProject } from "./project/project"; -import { ModelicaLibrary } from "./project/library"; +import { ModelicaDocument, ModelicaLibrary, ModelicaProject } from "./project"; import { uriToPath } from "./util"; +import * as TreeSitterUtil from "./util/tree-sitter"; import { getAllDeclarationsInTree } from "./util/declarations"; import logger from "./util/logger"; -import * as TreeSitterUtil from "./util/tree-sitter"; export default class Analyzer { #project: ModelicaProject; @@ -114,13 +116,16 @@ export default class Analyzer { return getAllDeclarationsInTree(tree, uri); } - public async findDeclarationFromPosition( + public findDeclaration( uri: LSP.DocumentUri, - line: number, - character: number, - ): Promise { + position: LSP.Position, + ): LSP.LocationLink | null { const path = uriToPath(uri); - logger.debug(`Searching for declaration of symbol at ${line + 1}:${character + 1} in '${path}'`); + logger.debug( + `Searching for declaration of symbol at ${position.line + 1}:${ + position.character + 1 + } in '${path}'`, + ); const document = this.#project.getDocument(path); if (!document) { @@ -133,69 +138,122 @@ export default class Analyzer { return null; } - const documentOffset = document.offsetAt({ line, character }); - - // TODO: we should check for a `type_specifier` first, then a `name`, then an `ident` - const hoveredName = this.findNodeAtPosition( - document.tree.rootNode, - documentOffset, - (node) => node.type === "name", - ); - - let symbols: string[] | undefined; - let startNode: Parser.SyntaxNode | undefined; - if (hoveredName) { - symbols = TreeSitterUtil.getDeclaredType(hoveredName).symbolNodes - .filter( - (node) => - node.startPosition.row < line || - (node.startPosition.row === line && node.startPosition.column <= character), - ) - .map((node) => node.text); - - startNode = this.findNodeAtPosition( - hoveredName, - documentOffset, - (node) => node.type === "IDENT", - ); - } else { - startNode = this.findNodeAtPosition( - document.tree.rootNode, - documentOffset, - (node) => node.type === "IDENT", - ); - symbols = startNode ? [startNode.text] : undefined; - } - - if (!startNode || !symbols) { + const reference = this.getReferenceAt(document, position); + if (!reference) { logger.info(`Tried to find declaration in '${path}', but not hovering on any identifiers`); return null; } logger.debug( - `Searching for declaration '${symbols.join(".")}' at ${line + 1}:${character + 1} in '${path}'`, + `Searching for '${reference}' at ${position.line + 1}:${position.character + 1} in '${path}'`, ); try { - const result = resolveReference( - document.project, - new UnresolvedRelativeReference(document, startNode, symbols), - "declaration", - ); + const result = resolveReference(document.project, reference, "declaration"); if (!result) { - logger.debug(`Didn't find declaration of ${symbols.join(".")}`); + logger.debug(`Didn't find declaration of ${reference.symbols.join(".")}`); return null; } const link = TreeSitterUtil.createLocationLink(result.document, result.node); - logger.debug(`Found declaration of ${symbols.join(".")}: `, link); + logger.debug(`Found declaration of ${reference.symbols.join(".")}: `, link); return link; - } catch (ex) { - logger.debug("Caught exception: " + JSON.stringify((ex as Error).stack)); + } catch (e: unknown) { + if (e instanceof Error) { + logger.debug("Caught exception: ", e.stack); + } else { + logger.debug(`Caught:`, e); + } return null; } } + /** + * Returns the reference at the document position, or `null` if no reference exists. + */ + private getReferenceAt( + document: ModelicaDocument, + position: LSP.Position, + ): UnresolvedReference | null { + function checkBeforeCursor(node: Parser.SyntaxNode): boolean { + if (node.startPosition.row < position.line) { + return true; + } + return ( + node.startPosition.row === position.line && node.startPosition.column <= position.character + ); + } + + const documentOffset = document.offsetAt(position); + + // First, check if this is a `type_specifier` or a `name`. + let hoveredType = this.findNodeAtPosition( + document.tree.rootNode, + documentOffset, + (node) => node.type === "name", + ); + + if (hoveredType) { + if (hoveredType.parent?.type === "type_specifier") { + hoveredType = hoveredType.parent; + } + + const declaredType = TreeSitterUtil.getTypeSpecifier(hoveredType); + const symbols = declaredType.symbolNodes.filter(checkBeforeCursor).map((node) => node.text); + + if (declaredType.isGlobal) { + return new UnresolvedAbsoluteReference(symbols, "class"); + } else { + const startNode = this.findNodeAtPosition( + hoveredType, + documentOffset, + (node) => node.type === "IDENT", + )!; + + return new UnresolvedRelativeReference(document, startNode, symbols, "class"); + } + } + + // Next, check if this is a `component_reference`. + const hoveredComponentReference = this.findNodeAtPosition( + document.tree.rootNode, + documentOffset, + (node) => node.type === "component_reference", + ); + if (hoveredComponentReference) { + // TODO: handle array indices + const componentReference = TreeSitterUtil.getComponentReference(hoveredComponentReference); + const symbols = componentReference.componentNodes + .filter(checkBeforeCursor) + .map((node) => node.text); + + if (componentReference.isGlobal) { + return new UnresolvedAbsoluteReference(symbols, "variable"); + } else { + const startNode = this.findNodeAtPosition( + hoveredComponentReference, + documentOffset, + (node) => node.type === "IDENT", + )!; + + return new UnresolvedRelativeReference(document, startNode, symbols, "variable"); + } + } + + // Finally, give up and check if this is just an ident. + const startNode = this.findNodeAtPosition( + document.tree.rootNode, + documentOffset, + (node) => node.type === "IDENT", + ); + if (startNode) { + return new UnresolvedRelativeReference(document, startNode, [startNode.text]); + } + + // We're not hovering over an identifier. + return null; + } + /** * Locates the first node at the given text position that matches the given * `condition`, starting from the `rootNode`. diff --git a/server/src/server.ts b/server/src/server.ts index 5a91f08..4c8dde2 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -41,16 +41,12 @@ import * as LSP from "vscode-languageserver/node"; import { TextDocument } from "vscode-languageserver-textdocument"; -import findCacheDirectory from "find-cache-dir"; -import * as fsWalk from "@nodelib/fs.walk"; import * as fs from "node:fs/promises"; -import * as util from "node:util"; -import * as url from "node:url"; import { initializeParser } from "./parser"; import Analyzer from "./analyzer"; +import { uriToPath } from "./util"; import { logger, setLogConnection, setLogLevel } from "./util/logger"; -import { uriToPath } from './util'; /** * ModelicaServer collection all the important bits and bobs. @@ -226,19 +222,15 @@ export class ModelicaServer { // // What does this even mean? Is this a definition of Foo or a redeclaration of Foo? // - // 3. Import aliases. Should this be considered to be a declaration of `Frobnicator`? + // 3. Import aliases. Should this be considered to be a declaration of `Frobnicator`? // - // import Frobnicator = Foo.Bar.Baz; + // import Frobnicator = Foo.Bar.Baz; // private async onDeclaration(params: LSP.DeclarationParams): Promise { logger.debug("onDeclaration"); - const locationLink = await this.analyzer.findDeclarationFromPosition( - params.textDocument.uri, - params.position.line, - params.position.character, - ); + const locationLink = this.analyzer.findDeclaration(params.textDocument.uri, params.position); if (locationLink == null) { return []; } @@ -249,11 +241,7 @@ export class ModelicaServer { private async onDefinition(params: LSP.DefinitionParams): Promise { logger.debug("onDefinition"); - const locationLink = await this.analyzer.findDeclarationFromPosition( - params.textDocument.uri, - params.position.line, - params.position.character, - ); + const locationLink = this.analyzer.findDeclaration(params.textDocument.uri, params.position); if (locationLink == null) { return []; } diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index 66cfd4f..29f7ab0 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -266,7 +266,6 @@ export function getDeclaredIdentifiers(node: SyntaxNode): string[] { return getDeclaredIdentifiers(definition); } default: - logger.warn(`getDeclaredIdentifiers: unknown node type ${node.type}`); return []; } } @@ -285,7 +284,7 @@ export interface TypeSpecifier { symbolNodes: SyntaxNode[]; } -export function getDeclaredType(node: SyntaxNode): TypeSpecifier { +export function getTypeSpecifier(node: SyntaxNode): TypeSpecifier { switch (node.type) { case "type_specifier": { const isGlobal = node.childForFieldName("global") !== null; @@ -314,12 +313,12 @@ export function getDeclaredType(node: SyntaxNode): TypeSpecifier { default: { const typeSpecifier = findFirst(node, (child) => child.type === "type_specifier"); if (typeSpecifier) { - return getDeclaredType(typeSpecifier); + return getTypeSpecifier(typeSpecifier); } const name = findFirst(node, (child) => child.type === "name"); if (name) { - return getDeclaredType(name); + return getTypeSpecifier(name); } throw new Error("Syntax node does not contain a type_specifier or name"); @@ -327,12 +326,50 @@ export function getDeclaredType(node: SyntaxNode): TypeSpecifier { } } +// TODO: this does not handle indexing arrays +export interface ComponentReference { + isGlobal: boolean; + components: string[]; + componentNodes: SyntaxNode[]; +} + +export function getComponentReference(node: SyntaxNode): ComponentReference { + switch (node.type) { + case "component_reference": { + const isGlobal = node.childForFieldName("global") !== null; + const componentNodes = getNameIdentifiers(node); + + return { + isGlobal, + components: componentNodes.map((id) => id.text), + componentNodes, + }; + } + case "IDENT": + return { + isGlobal: false, + components: [node.text], + componentNodes: [node], + }; + default: { + const componentRef = findFirst(node, (child) => child.type === "component_reference"); + if (componentRef) { + return getComponentReference(componentRef); + } + + throw new Error("Syntax node does not contain a component_reference"); + } + } +} + /** * Converts a name `SyntaxNode` into an array of the `IDENT`s in that node. */ function getNameIdentifiers(nameNode: SyntaxNode): Parser.SyntaxNode[] { - if (nameNode.type !== "name") { - throw new Error(`Expected a 'name' node; got '${nameNode.type}' (${nameNode.text})`); + if (nameNode.type !== "name" && nameNode.type !== "component_reference") { + throw new Error( + `Expected a 'name' or 'component_reference' node; got '${nameNode.type}' (${nameNode.text})`, + ); } const identNode = nameNode.childForFieldName("identifier")!; From de6798c71519862fef0af5f050f8ed88cc4c3c9d Mon Sep 17 00:00:00 2001 From: PaddiM8 Date: Tue, 7 May 2024 14:14:04 +0200 Subject: [PATCH 20/24] Reanalyse when a file is modified, before it's saved --- server/src/server.ts | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index 4c8dde2..1c576ab 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -121,24 +121,10 @@ export class ModelicaServer { connection.onShutdown(this.onShutdown.bind(this)); connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); connection.onInitialized(this.onInitialized.bind(this)); + connection.onDidChangeTextDocument(this.onDidChangeTextDocument.bind(this)); connection.onDidChangeWatchedFiles(this.onDidChangeWatchedFiles.bind(this)); connection.onDeclaration(this.onDeclaration.bind(this)); connection.onDefinition(this.onDefinition.bind(this)); - - // The content of a text document has changed. This event is emitted - // when the text document first opened or when its content has changed. - this.documents.onDidChangeContent((params) => { - logger.debug("onDidChangeContent"); - - // We need to define some timing to wait some time or until whitespace is typed - // to update the tree or we are doing this on every key stroke - - // TODO: this gives us a document instance managed by this.document - // However, we make our documents ourselves. How do we get that to work? - // Do we just not use the TextDocuments class? - - // TODO: actually reanalyze - }); } private async onInitialized(): Promise { @@ -166,6 +152,13 @@ export class ModelicaServer { logger.debug("onShutdown"); } + private async onDidChangeTextDocument(params: LSP.DidChangeTextDocumentParams): Promise { + logger.debug("onDidChangeTextDocument"); + for (const change of params.contentChanges) { + this.analyzer.updateDocument(params.textDocument.uri, change.text); + } + } + private async onDidChangeWatchedFiles(params: LSP.DidChangeWatchedFilesParams): Promise { logger.debug("onDidChangeWatchedFiles: " + JSON.stringify(params, undefined, 4)); From 634fb3c9a8b9d4bc6d0d7b75e2e1b7322320df74 Mon Sep 17 00:00:00 2001 From: PaddiM8 Date: Fri, 10 May 2024 12:06:12 +0200 Subject: [PATCH 21/24] Incremental parsing --- server/src/project/document.ts | 2 +- server/src/server.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/project/document.ts b/server/src/project/document.ts index 6344d5d..31310e4 100644 --- a/server/src/project/document.ts +++ b/server/src/project/document.ts @@ -101,7 +101,7 @@ export class ModelicaDocument implements TextDocument { }); this.#tree = this.project.parser.parse((index: number, position?: Parser.Point) => { - if (position !== undefined) { + if (position) { return this.getText({ start: { character: position.column, diff --git a/server/src/server.ts b/server/src/server.ts index 1c576ab..2ec07d2 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -103,7 +103,7 @@ export class ModelicaServer { hoverProvider: false, signatureHelpProvider: undefined, semanticTokensProvider: undefined, - textDocumentSync: LSP.TextDocumentSyncKind.Full, + textDocumentSync: LSP.TextDocumentSyncKind.Incremental, workspace: { workspaceFolders: { supported: true, @@ -155,7 +155,8 @@ export class ModelicaServer { private async onDidChangeTextDocument(params: LSP.DidChangeTextDocumentParams): Promise { logger.debug("onDidChangeTextDocument"); for (const change of params.contentChanges) { - this.analyzer.updateDocument(params.textDocument.uri, change.text); + const range = "range" in change ? change.range : undefined; + this.analyzer.updateDocument(params.textDocument.uri, change.text, range); } } From cf6d053b9903955b28cf43e64a26e4064c4b9a85 Mon Sep 17 00:00:00 2001 From: AnHeuermann <38031952+AnHeuermann@users.noreply.github.com> Date: Tue, 14 May 2024 12:06:25 +0200 Subject: [PATCH 22/24] Whitespace changes --- server/src/analysis/resolveReference.ts | 4 ++-- server/src/util/index.ts | 2 +- server/src/util/logger.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/analysis/resolveReference.ts b/server/src/analysis/resolveReference.ts index 2c7c1ae..945ec60 100644 --- a/server/src/analysis/resolveReference.ts +++ b/server/src/analysis/resolveReference.ts @@ -499,8 +499,8 @@ function resolveNext( // the superclass handling section in findDeclarationInClass. // ...or something like that // As it is now, we don't know if `child` is a class or variable. We can't use - // `undefined` to indicate this because this results in infinite recursion. - // This issue causes us to be unable to look up variables declared in a superclass + // `undefined` to indicate this because this results in infinite recursion. + // This issue causes us to be unable to look up variables declared in a superclass // of a member variable. A redesign might be necessary to resolve this. const child = findDeclarationInClass( parentReference.document, diff --git a/server/src/util/index.ts b/server/src/util/index.ts index c7ea4ca..05fc8db 100644 --- a/server/src/util/index.ts +++ b/server/src/util/index.ts @@ -11,4 +11,4 @@ export function pathToUri(filePath: string): LSP.URI { // the node pathToFileURL uses ':' anyways. Manually fix this here. // This is a bit hacky but we should ideally only be working with the URIs from LSP anyways. return uri.slice(0, 5) + uri.slice(5).replace(":", "%3A"); -} \ No newline at end of file +} diff --git a/server/src/util/logger.ts b/server/src/util/logger.ts index 883d166..068a3fb 100644 --- a/server/src/util/logger.ts +++ b/server/src/util/logger.ts @@ -167,4 +167,4 @@ export function getLogLevelFromEnvironment(): LSP.MessageType { return LOG_LEVELS_TO_MESSAGE_TYPES[DEFAULT_LOG_LEVEL]; } -export default logger; \ No newline at end of file +export default logger; From 6a727b323776580e3da0197169921d4cfc989641 Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Tue, 21 May 2024 14:41:59 +0200 Subject: [PATCH 23/24] Add additional documentation. Co-authored-by: PaddiM8 --- server/src/analysis/reference.ts | 6 +++ server/src/analysis/resolveReference.ts | 68 +++++++++++++------------ server/src/analyzer.ts | 53 +++++++++++++++++-- server/src/project/document.ts | 27 ++++++++++ server/src/project/library.ts | 8 +++ server/src/project/project.ts | 16 ++++-- server/src/util/declarations.ts | 6 --- server/src/util/index.ts | 6 +-- 8 files changed, 140 insertions(+), 50 deletions(-) diff --git a/server/src/analysis/reference.ts b/server/src/analysis/reference.ts index e20b0a3..f05c63d 100644 --- a/server/src/analysis/reference.ts +++ b/server/src/analysis/reference.ts @@ -81,8 +81,14 @@ export class UnresolvedAbsoluteReference extends BaseUnresolvedReference { } } +/** + * A possibly-valid reference to a symbol that must be resolved before use. + */ export type UnresolvedReference = UnresolvedRelativeReference | UnresolvedAbsoluteReference; +/** + * A valid, absolute reference to a symbol. + */ export class ResolvedReference { /** * The document that contains the `node`. diff --git a/server/src/analysis/resolveReference.ts b/server/src/analysis/resolveReference.ts index 945ec60..4cc0a1a 100644 --- a/server/src/analysis/resolveReference.ts +++ b/server/src/analysis/resolveReference.ts @@ -103,15 +103,17 @@ function* getAbsoluteReferenceCandidates( } /** - * Locates the declaration/definition of a reference in its document, or finds a suitable absolute reference. + * Attempts to locate the definition of an `UnresolvedRelativeReference` within + * the referenced document. * - * @param reference a reference to a local in which the `document` and `node` properties reference - * the usage of the symbol. - * @returns either - * (1) a relative reference in which the `document` and `node` properties reference - * the symbol's declaration/definition, - * (2) an absolute reference - * (3) `undefined` (not in the document) + * If the reference is present in the document, an `UnresolvedRelativeReference` + * pointing to the definition will be returned. If the reference may refer to an + * import, an `UnresolvedAbsoluteReference` will be returned. If the reference + * was not present at all, `undefined` will be returned. + * + * @param reference a reference to a local in which the `document` and `node` + * properties reference the usage of the symbol. + * @returns the reference candidates */ function* findReferenceInDocument( reference: UnresolvedRelativeReference, @@ -120,7 +122,6 @@ function* findReferenceInDocument( const maybeVariable = reference.kind === "variable" || reference.kind === undefined; if (maybeClass) { - // logger.debug("findReferenceInDocument: Checking if this node is a class..."); if ( TreeSitterUtil.isDefinition(reference.node) && TreeSitterUtil.hasIdentifier(reference.node, reference.symbols[0]) @@ -134,7 +135,6 @@ function* findReferenceInDocument( return; } - // logger.debug("findReferenceInDocument: Checking for child class..."); const classDecl = reference.node.children.find( (child) => TreeSitterUtil.isDefinition(child) && @@ -153,7 +153,6 @@ function* findReferenceInDocument( } if (maybeVariable) { - // logger.debug("findReferenceInDocument: Checking for child variable..."); const varDecl = reference.node.children.find( (child) => TreeSitterUtil.isVariableDeclaration(child) && @@ -171,7 +170,6 @@ function* findReferenceInDocument( } } - // logger.debug("findReferenceInDocument: Checking for declarations in class..."); const declInClass = findDeclarationInClass( reference.document, reference.node, @@ -187,11 +185,9 @@ function* findReferenceInDocument( (child) => child.type === "import_clause", ); if (importClauses && importClauses.length > 0) { - // logger.debug("findReferenceInDocument: Checking imports..."); for (const importClause of importClauses) { const { importCandidate, wildcard } = resolveImportClause(reference.symbols, importClause); if (importCandidate) { - // logger.debug("findReferenceInDocument: found import!"); if (wildcard) { yield importCandidate; @@ -204,7 +200,6 @@ function* findReferenceInDocument( } if (reference.node.parent) { - // logger.debug("findReferenceInDocument: Checking parent node..."); yield* findReferenceInDocument( new UnresolvedRelativeReference( reference.document, @@ -221,6 +216,15 @@ function* findReferenceInDocument( return; } +/** + * Searches for a declaration within a class (or a superclass). + * + * @param document the class' document + * @param classNode the `class_definition` syntax node referencing the class + * @param symbols the symbol to search for + * @param referenceKind the type of reference + * @returns an unresolved reference to the symbol, or `undefined` if not present + */ function findDeclarationInClass( document: ModelicaDocument, classNode: Parser.SyntaxNode, @@ -231,11 +235,6 @@ function findDeclarationInClass( return undefined; } - // logger.debug( - // `findDeclarationInClass: Checking for declaration ${symbols.join(".")} ` + - // `in class: ${TreeSitterUtil.getDeclaredIdentifiers(classNode)}`, - // ); - const elements = classNode .childForFieldName("classSpecifier") ?.children?.filter(TreeSitterUtil.isElementList) @@ -260,7 +259,7 @@ function findDeclarationInClass( classDef, symbols, "class", - ) as UnresolvedRelativeReference & { kind: ReferenceKind }; + ) as UnresolvedRelativeReference & { kind: "class" }; } const componentDef = namedElement[0].childForFieldName("componentClause")!; @@ -271,7 +270,7 @@ function findDeclarationInClass( componentDef, symbols, "variable", - ) as UnresolvedRelativeReference & { kind: ReferenceKind }; + ) as UnresolvedRelativeReference & { kind: "variable" }; } // only check superclasses if we know we're not looking for a class @@ -321,8 +320,8 @@ interface ResolveImportClauseResult { */ importCandidate?: UnresolvedAbsoluteReference; /** - * `true` if this was a wildcard import, and we are not sure if this import even exists. - * `false` if this was not a wildcard import. + * `true` if this was a wildcard import, and we are not sure if this import + * even exists. `false` if this was not a wildcard import. */ wildcard: boolean; } @@ -420,8 +419,8 @@ function resolveAbsoluteReference( return null; } - // If we're not done with the reference chain, we need to make sure that we know - // the type of the variable in order to check its child variables + // If we're not done with the reference chain, we need to make sure that we + // know the type of the variable in order to check its child variables if ( i < reference.symbols.length - 1 && TreeSitterUtil.isVariableDeclaration(alreadyResolved.node) @@ -495,13 +494,14 @@ function resolveNext( } // TODO: The `kind` parameter here should be `undefined` unless - // `resolveReference` was called with kind = "class" by - // the superclass handling section in findDeclarationInClass. - // ...or something like that + // `resolveReference` was called with kind = "class" by the superclass + // handling section in findDeclarationInClass. ...or something like that // As it is now, we don't know if `child` is a class or variable. We can't use // `undefined` to indicate this because this results in infinite recursion. - // This issue causes us to be unable to look up variables declared in a superclass - // of a member variable. A redesign might be necessary to resolve this. + // This issue causes us to be unable to look up variables declared in a + // superclass of a member variable. A redesign might be necessary to resolve + // this. Perhaps if we could keep track of which classes we already visited, + // we wouldn't need the whole "class"/"variable" trick at all! const child = findDeclarationInClass( parentReference.document, parentReference.node, @@ -548,10 +548,12 @@ function getPackageClassFromFilePath( } /** - * Finds the type of a variable declaration and returns a reference to that type. + * Finds the type of a variable declaration and returns a reference to that + * type. * * @param varRef a reference to a variable declaration/definition - * @returns a reference to the class definition, or `null` if the type is not a class (e.g. a builtin like `Real`) + * @returns a reference to the class definition, or `null` if the type is not a + * class (e.g. a builtin like `Real`) */ function variableRefToClassRef(varRef: ResolvedReference): ResolvedReference | null { const type = TreeSitterUtil.getTypeSpecifier(varRef.node); diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 8c62490..ce1ca7f 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -64,6 +64,13 @@ export default class Analyzer { this.#project = new ModelicaProject(parser); } + /** + * 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 { const isLibrary = (folderPath: string) => fsSync.existsSync(path.join(folderPath, "package.mo")); @@ -88,24 +95,51 @@ export default class Analyzer { } } + /** + * 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(uriToPath(uri)); } + /** + * 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, range?: LSP.Range): void { this.#project.updateDocument(uriToPath(uri), text, range); } + /** + * 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(uriToPath(uri)); } /** - * Get all symbol declarations in the given file. This is used for generating an outline. - * - * TODO: convert to DocumentSymbol[] which is a hierarchy of symbols found in a given text document. + * Gets all symbol declarations in the given file. This is used for generating + * an outline. + * + * @param uri uri to document + * @returns the symbols */ public getDeclarationsForUri(uri: LSP.DocumentUri): LSP.SymbolInformation[] { + // TODO: convert to DocumentSymbol[] which is a hierarchy of symbols found + // in a given text document. const path = uriToPath(uri); const tree = this.#project.getDocument(path)?.tree; @@ -116,6 +150,14 @@ export default class Analyzer { return getAllDeclarationsInTree(tree, uri); } + /** + * Finds the position of the declaration of the symbol at the given position. + * + * @param uri the opened document + * @param position the cursor position + * @returns a {@link LSP.LocationLink} to the symbol's declaration, or `null` + * if not found. + */ public findDeclaration( uri: LSP.DocumentUri, position: LSP.Position, @@ -169,7 +211,8 @@ export default class Analyzer { } /** - * Returns the reference at the document position, or `null` if no reference exists. + * Returns the reference at the document position, or `null` if no reference + * exists. */ private getReferenceAt( document: ModelicaDocument, @@ -264,7 +307,7 @@ export default class Analyzer { * * @param rootNode node to start searching from. parents/siblings of this node will be ignored * @param offset the offset of the symbol from the start of the document - * @param condition the condition to check if a node is good + * @param condition the condition to check if a node is "good" * @returns the node at the position, or `undefined` if none was found */ private findNodeAtPosition( diff --git a/server/src/project/document.ts b/server/src/project/document.ts index 31310e4..a40fb82 100644 --- a/server/src/project/document.ts +++ b/server/src/project/document.ts @@ -57,6 +57,13 @@ export class ModelicaDocument implements TextDocument { 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, @@ -75,6 +82,12 @@ export class ModelicaDocument implements TextDocument { ); } + /** + * Incrementally 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, range?: LSP.Range): Promise { if (range === undefined) { TextDocument.update(this.#document, [{ text }], this.version + 1); @@ -153,6 +166,11 @@ export class ModelicaDocument implements TextDocument { 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()!; @@ -165,6 +183,15 @@ export class ModelicaDocument implements TextDocument { 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); } diff --git a/server/src/project/library.ts b/server/src/project/library.ts index 6a77d64..a4e26d1 100644 --- a/server/src/project/library.ts +++ b/server/src/project/library.ts @@ -55,6 +55,14 @@ export class ModelicaLibrary { this.#isWorkspace = isWorkspace; } + /** + * Loads a library and all of its {@link ModelicaDocument}s. + * + * @param project the containing project + * @param libraryPath the path to the library + * @param isWorkspace `true` if this is a user workspace + * @returns the loaded library + */ public static async load( project: ModelicaProject, libraryPath: string, diff --git a/server/src/project/project.ts b/server/src/project/project.ts index f7509cc..7531b95 100644 --- a/server/src/project/project.ts +++ b/server/src/project/project.ts @@ -80,7 +80,11 @@ export class ModelicaProject { } /** - * Adds a new document to the LSP. + * Adds a new document to the LSP. Calling this method multiple times for the + * same document has no effect. + * + * @param documentPath path to the document + * @throws if the document does not belong to a library */ public async addDocument(documentPath: string): Promise { logger.info(`Adding document at '${documentPath}'...`); @@ -91,6 +95,11 @@ export class ModelicaProject { // Assume that files can't be inside multiple libraries at the same time if (isSubdirectory) { + if (library.documents.get(documentPath) !== undefined) { + logger.warn(`Document '${documentPath}' already in library '${library.name}'; ignoring...`); + return; + } + const document = await ModelicaDocument.load(library, documentPath); library.documents.set(documentPath, document); logger.debug(`Added document: ${documentPath}`); @@ -102,10 +111,11 @@ export class ModelicaProject { } /** - * Updates the content and tree of the given document. + * Incrementally updates the content and tree of the given document. Ignores + * documents that have not been loaded. * * @param text the modification - * @param range range to update, or undefined to replace the whole file + * @param range range to update, or `undefined` to replace the whole file */ public updateDocument(documentPath: string, text: string, range?: LSP.Range): void { logger.debug(`Updating document at '${documentPath}'...`); diff --git a/server/src/util/declarations.ts b/server/src/util/declarations.ts index 45ea489..3249de9 100644 --- a/server/src/util/declarations.ts +++ b/server/src/util/declarations.ts @@ -42,18 +42,12 @@ import * as LSP from 'vscode-languageserver/node'; import * as Parser from 'web-tree-sitter'; import * as TreeSitterUtil from './tree-sitter'; -import { logger } from './logger'; const isEmpty = (data: string): boolean => typeof data === "string" && data.trim().length == 0; export type GlobalDeclarations = { [word: string]: LSP.SymbolInformation } export type Declarations = { [word: string]: LSP.SymbolInformation[] } -const GLOBAL_DECLARATION_LEAF_NODE_TYPES = new Set([ - 'if_statement', - 'function_definition', -]); - /** * Returns all declarations (functions or variables) from a given tree. * diff --git a/server/src/util/index.ts b/server/src/util/index.ts index 05fc8db..0017b94 100644 --- a/server/src/util/index.ts +++ b/server/src/util/index.ts @@ -7,8 +7,8 @@ export const uriToPath = url.fileURLToPath; export function pathToUri(filePath: string): LSP.URI { const uri = url.pathToFileURL(filePath).href; - // Note: LSP sends us file uris containing '%3A' instead of ':', but - // the node pathToFileURL uses ':' anyways. Manually fix this here. - // This is a bit hacky but we should ideally only be working with the URIs from LSP anyways. + // Note: LSP sends us file uris containing '%3A' instead of ':', but the + // node pathToFileURL uses ':' anyways. We manually fix this here. This is a + // bit hacky but it works. return uri.slice(0, 5) + uri.slice(5).replace(":", "%3A"); } From 38dc70e51481a8e6e8c353b1de5cff8446cb234a Mon Sep 17 00:00:00 2001 From: Evan Hedbor Date: Tue, 21 May 2024 15:30:35 +0200 Subject: [PATCH 24/24] update settings --- .vscode/settings.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d1f152c..bded9c2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,5 +12,7 @@ "nodelib", "OSMC", "redeclaration" - ] + ], + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, }