diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index e8096a2..27f7335 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -42,7 +42,7 @@ 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 @@ -51,16 +51,58 @@ import { logger } from './util/logger'; type AnalyzedDocument = { document: TextDocument, - declarations: LSP.SymbolInformation[], + declarations: LSP.DocumentSymbol[], tree: Parser.Tree } +export class MetaModelicaQueries { + public identifierQuery: Parser.Query; + public classTypeQuery: Parser.Query; + + constructor(language: Parser.Language) { + this.identifierQuery = language.query('(IDENT) @identifier'); + this.classTypeQuery = language.query('(class_type) @type'); + } + + /** + * Get identifier from node. + * + * @param node Node. + * @returns Identifier + */ + public getIdentifier(node: Parser.SyntaxNode): string | undefined { + const captures = this.identifierQuery.captures(node); + if (captures.length > 0) { + return captures[0].node.text; + } else { + return undefined; + } + } + + /** + * Get class type from class_definition node. + * + * @param node Node. + * @returns Class type + */ + public getClassType(node: Parser.SyntaxNode): string | undefined { + const captures = this.classTypeQuery.captures(node); + if (captures.length > 0) { + return captures[0].node.text; + } else { + return undefined; + } + } +} + export default class Analyzer { private parser: Parser; private uriToAnalyzedDocument: Record = {}; + private queries: MetaModelicaQueries; constructor (parser: Parser) { this.parser = parser; + this.queries = new MetaModelicaQueries(parser.getLanguage()); } public analyze(document: TextDocument): LSP.Diagnostic[] { @@ -74,7 +116,7 @@ export default class Analyzer { logger.debug(tree.rootNode.toString()); // Get declarations - const declarations = getAllDeclarationsInTree(tree, uri); + const declarations = getAllDeclarationsInTree(tree, this.queries); // Update saved analysis for document uri this.uriToAnalyzedDocument[uri] = { @@ -89,15 +131,14 @@ export default class Analyzer { /** * 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. */ - public getDeclarationsForUri(uri: string): LSP.SymbolInformation[] { + public getDeclarationsForUri(uri: string): LSP.DocumentSymbol[] { const tree = this.uriToAnalyzedDocument[uri]?.tree; if (!tree?.rootNode) { return []; } - return getAllDeclarationsInTree(tree, uri); + return getAllDeclarationsInTree(tree, this.queries,); } } diff --git a/server/src/server.ts b/server/src/server.ts index 395c49e..b8ce471 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -145,10 +145,7 @@ export class MetaModelicaServer { * @param params Unused. * @returns Symbol information. */ - 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 + private onDocumentSymbol(params: LSP.DocumentSymbolParams): LSP.DocumentSymbol[] { logger.debug(`onDocumentSymbol`); return this.analyzer.getDeclarationsForUri(params.textDocument.uri); } diff --git a/server/src/util/declarations.ts b/server/src/util/declarations.ts index 510f459..d0e4f27 100644 --- a/server/src/util/declarations.ts +++ b/server/src/util/declarations.ts @@ -43,100 +43,126 @@ import * as Parser from 'web-tree-sitter'; import * as TreeSitterUtil from './tree-sitter'; import { logger } from './logger'; +import { MetaModelicaQueries } from './../analyzer'; 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. + * Returns all class declarations from a given tree. * - * @param tree Tree-sitter tree. - * @param uri The document's uri. - * @returns Symbol information for all declarations. + * @param tree Tree-sitter tree. + * @param queries MetaModelica language queries. + * @returns Symbol information for all declarations. */ -export function getAllDeclarationsInTree(tree: Parser.Tree, uri: string): LSP.SymbolInformation[] { - const symbols: LSP.SymbolInformation[] = []; +export function getAllDeclarationsInTree(tree: Parser.Tree, queries: MetaModelicaQueries): LSP.DocumentSymbol[] { + const documentSymbols: LSP.DocumentSymbol[] = []; + const cursor = tree.walk(); + let reachedRoot = false; + + // Walk depth-first to each class_definition node. + // Update DocumentSymbol children when going back up. + while(!reachedRoot) { + const currentNode = cursor.currentNode(); + + if (cursor.nodeType === "class_definition") { + const symbol = nodeToDocumentSymbol(currentNode, queries, []); + if (symbol) { + documentSymbols.push(symbol); + } + } - TreeSitterUtil.forEach(tree.rootNode, (node) => { - const symbol = getDeclarationSymbolFromNode(node, uri); - if (symbol) { - symbols.push(symbol); + if (cursor.gotoFirstChild()) { + continue; } - }); - return symbols; + if (cursor.gotoNextSibling()) { + continue; + } + + let retracing = true; + while (retracing) { + // Try to go to parent + if (cursor.gotoParent()) { + if (cursor.nodeType === "class_definition") { + let tmp = undefined; + if (documentSymbols.length > 1) { + tmp = documentSymbols.pop(); + } + if (tmp) { + if (documentSymbols.length > 0) { + documentSymbols[documentSymbols.length - 1].children?.push(tmp); + } + } + } + } else { + retracing = false; + reachedRoot = true; + } + + if (cursor.gotoNextSibling()) { + retracing = false; + } + } + } + + return documentSymbols; } /** * Converts node to symbol information. * - * @param tree Tree-sitter tree. - * @param uri The document's uri. - * @returns Symbol information from node. + * @param tree Tree-sitter tree. + * @param queries MetaModelica language queries. + * @param children DocumentSymbol children. + * @returns Symbol information from node. */ -export function nodeToSymbolInformation(node: Parser.SyntaxNode, uri: string): LSP.SymbolInformation | null { - const named = node.firstNamedChild; - - if (named === null) { +export function nodeToDocumentSymbol(node: Parser.SyntaxNode, queries: MetaModelicaQueries, children: LSP.DocumentSymbol[] ): LSP.DocumentSymbol | null { + const name = queries.getIdentifier(node); + if (name === undefined || isEmpty(name)) { return null; } - const name = TreeSitterUtil.getIdentifier(node); - if (name === undefined || isEmpty(name)) { - return null; + const detail = []; + if ( node.childForFieldName("encapsulated") ) { + detail.push("encapsulated"); + } + if ( node.childForFieldName("partial") ) { + detail.push("partial"); } + detail.push(queries.getClassType(node)); + + const kind = getKind(node, queries) || LSP.SymbolKind.Variable; - const kind = getKind(node); + const range = TreeSitterUtil.range(node); + const selectionRange = TreeSitterUtil.range(queries.identifierQuery.captures(node)[0].node) || range; - const containerName = - TreeSitterUtil.findParent(node, (p) => p.type === 'function_definition') - ?.firstNamedChild?.text || ''; + // Walk tree to find next class_definition + const cursor = node.walk(); - return LSP.SymbolInformation.create( + return LSP.DocumentSymbol.create( name, - kind || LSP.SymbolKind.Variable, - TreeSitterUtil.range(node), - uri, - containerName, + detail.join(" "), + kind, + range, + selectionRange, + children ); } -/** - * Get declaration from node and convert to symbol information. - * - * @param node Root node of tree. - * @param uri The associated URI for this document. - * @returns LSP symbol information for definition. - */ -function getDeclarationSymbolFromNode(node: Parser.SyntaxNode, uri: string): LSP.SymbolInformation | null { - if (TreeSitterUtil.isDefinition(node)) { - return nodeToSymbolInformation(node, uri); - } - - return null; -} - /** * Returns symbol kind from class definition node. * * @param node Node containing class_definition * @returns Symbol kind or `undefined`. */ -function getKind(node: Parser.SyntaxNode): LSP.SymbolKind | undefined { +function getKind(node: Parser.SyntaxNode, queries: MetaModelicaQueries): LSP.SymbolKind | undefined { - const classTypes = TreeSitterUtil.getClassType(node)?.split(/\s+/); - if (classTypes === undefined) { + const classType = queries.getClassType(node); + if (classType === undefined) { return undefined; } - switch (classTypes[classTypes.length - 1]) { + switch (classType) { case 'class': case 'optimization': case 'model': @@ -150,6 +176,7 @@ function getKind(node: Parser.SyntaxNode): LSP.SymbolKind | undefined { case 'uniontype': return LSP.SymbolKind.Package; case 'record': + return LSP.SymbolKind.Struct; case 'type': return LSP.SymbolKind.TypeParameter; default: diff --git a/server/src/util/test/declarations.test.ts b/server/src/util/test/declarations.test.ts index 16efdc6..950ea37 100644 --- a/server/src/util/test/declarations.test.ts +++ b/server/src/util/test/declarations.test.ts @@ -36,28 +36,77 @@ import * as assert from 'assert'; import * as LSP from 'vscode-languageserver/node'; +import { MetaModelicaQueries } from '../../analyzer'; import { initializeParser } from '../../parser'; -import { getAllDeclarationsInTree, nodeToSymbolInformation } from '../declarations'; +import { getAllDeclarationsInTree, nodeToDocumentSymbol } from '../declarations'; const metaModelicaTestString = ` -model M "Description" -end M; - -function foo -end foo; - -type Temperature = Real(unit = "K"); +//Some comment +encapsulated package A + package B1 + partial function foo + end foo; + record R + end R; + end B1; + package B2 + end B2; +end A; `; -const expectedDefinitions = ["M", "foo", "Temperature"]; +const expectedSymbols = [ + LSP.DocumentSymbol.create( + "A", + "encapsulated package", + LSP.SymbolKind.Package, + LSP.Range.create(LSP.Position.create(2,0), LSP.Position.create(11,5)), + LSP.Range.create(LSP.Position.create(2,21), LSP.Position.create(2,22)), + [ + LSP.DocumentSymbol.create( + "B1", + "package", + LSP.SymbolKind.Package, + LSP.Range.create(LSP.Position.create(3,2), LSP.Position.create(8,8)), + LSP.Range.create(LSP.Position.create(3,10), LSP.Position.create(3,12)), + [ + LSP.DocumentSymbol.create( + "foo", + "partial function", + LSP.SymbolKind.Function, + LSP.Range.create(LSP.Position.create(4,4), LSP.Position.create(5,11)), + LSP.Range.create(LSP.Position.create(4,21), LSP.Position.create(4,24)), + [] + ), + LSP.DocumentSymbol.create( + "R", + "record", + LSP.SymbolKind.Struct, + LSP.Range.create(LSP.Position.create(6,4), LSP.Position.create(7,9)), + LSP.Range.create(LSP.Position.create(6,11), LSP.Position.create(6,12)), + [] + ), + ] + ), + LSP.DocumentSymbol.create( + "B2", + "package", + LSP.SymbolKind.Package, + LSP.Range.create(LSP.Position.create(9,2), LSP.Position.create(10,8)), + LSP.Range.create(LSP.Position.create(9,10), LSP.Position.create(9,12)), + [] + ), + ] + ) +]; + const expectedTypes = [LSP.SymbolKind.Class, LSP.SymbolKind.Function, LSP.SymbolKind.TypeParameter]; -describe('nodeToSymbolInformation', () => { +describe('nodeToDocumentSymbol', () => { it('type to TypeParameter', async () => { const parser = await initializeParser(); const tree = parser.parse("type Temperature = Real(unit = \"K \");"); - - const symbol = nodeToSymbolInformation(tree.rootNode.childForFieldName("classDefinitionList")!, "file.mo"); + const queries = new MetaModelicaQueries(parser.getLanguage()); + const symbol = nodeToDocumentSymbol(tree.rootNode.childForFieldName("classDefinitionList")!, queries, []); assert.equal(symbol?.name, 'Temperature'); assert.equal(symbol?.kind, LSP.SymbolKind.TypeParameter); @@ -68,16 +117,12 @@ describe('getAllDeclarationsInTree', () => { it('Definitions and types', async () => { const parser = await initializeParser(); const tree = parser.parse(metaModelicaTestString); - const symbols = getAllDeclarationsInTree(tree, "file.mo"); + const queries = new MetaModelicaQueries(parser.getLanguage()); + const symbols = getAllDeclarationsInTree(tree, queries); - const definitions: string[] = []; - const types: LSP.SymbolKind[] = []; - for (let i = 0; i < symbols.length; i++) { - definitions.push(symbols[i].name); - types.push(symbols[i].kind); - } + console.log(symbols![0].children![0].children![1].range); + console.log(symbols![0].children![0].children![1].selectionRange); - assert.deepEqual(definitions, expectedDefinitions); - assert.deepEqual(types, expectedTypes); + assert.deepEqual(symbols, expectedSymbols); }); }); diff --git a/server/src/util/test/util.test.ts b/server/src/util/test/util.test.ts index 957c737..21504db 100644 --- a/server/src/util/test/util.test.ts +++ b/server/src/util/test/util.test.ts @@ -35,6 +35,7 @@ import * as assert from 'assert'; +import { MetaModelicaQueries } from '../../analyzer'; import { initializeParser } from '../../parser'; import * as TreeSitterUtil from '../tree-sitter'; @@ -43,7 +44,8 @@ describe('getIdentifier', () => { const parser = await initializeParser(); const tree = parser.parse("type Temperature = Real(unit = \"K \");"); const classNode = tree.rootNode.childForFieldName("classDefinitionList")!; - const name = TreeSitterUtil.getIdentifier(classNode); + const queries = new MetaModelicaQueries(parser.getLanguage()); + const name = queries.getIdentifier(classNode); assert.equal(name, 'Temperature'); }); diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index ad193b4..9f0f7c8 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -139,17 +139,6 @@ export function findParent( return null; } -/** - * Get identifier from node. - * - * @param start Syntax tree node. - */ -export function getIdentifier(start: SyntaxNode): string | undefined { - - const node = findFirst(start, (n: SyntaxNode) => n.type == 'IDENT'); - return node?.text; -} - /** * Get class type from `class_definition` node. *