From 06a1191206507779aa36664e8dd322ccc9a50ae4 Mon Sep 17 00:00:00 2001 From: osman362 <136453917+osman362@users.noreply.github.com> Date: Fri, 23 Feb 2024 21:20:15 +0100 Subject: [PATCH 01/12] onHover recognition of comments above a function - onHover method recognizes comments above a function and shows the content inside the hover information - output.md file is a reference on how the hover information should look like - step.mo is a test fixture for the hover method --- .vscode/tasks.json | 2 +- client/src/test/output.md | 19 +++ client/testFixture/step.mo | 20 +++ server/src/analyzer.ts | 229 +++++++++++++++++++++++++++++++- server/src/server.ts | 198 ++++++++++++++++++++++++--- server/src/util/array.ts | 38 ++++++ server/src/util/declarations.ts | 54 +++++++- 7 files changed, 532 insertions(+), 28 deletions(-) create mode 100644 client/src/test/output.md create mode 100644 client/testFixture/step.mo create mode 100644 server/src/util/array.ts diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f129682..f067c5a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -15,7 +15,7 @@ }, { "type": "npm", - "script": "watch", + "script": "esbuild-watch", "isBackground": true, "group": { "kind": "build", diff --git a/client/src/test/output.md b/client/src/test/output.md new file mode 100644 index 0000000..c6eaff0 --- /dev/null +++ b/client/src/test/output.md @@ -0,0 +1,19 @@ +# Modelica.Blocks.Sources.Step + +Generate step signal of type Real + +## Inputs + +## Outputs + +```modelica +Real y "Connector of Real output signal" +``` + +## Parameters + +```modelica +Real height = 1.0 "Height of step" +Real offset = 0.0 "Offset of output signal y" +Real startTime = 0.0 "Output y = offset for time < startTime" +``` \ No newline at end of file diff --git a/client/testFixture/step.mo b/client/testFixture/step.mo new file mode 100644 index 0000000..ded7120 --- /dev/null +++ b/client/testFixture/step.mo @@ -0,0 +1,20 @@ +class Modelica.Blocks.Sources.Step "Generate step signal of type Real" + parameter Real height = 1.0 "Height of step"; + output Real y "Connector of Real output signal"; + parameter Real offset = 0.0 "Offset of output signal y"; + parameter Real startTime(quantity = "Time", unit = "s") = 0.0 "Output y = offset for time < startTime"; +equation + y = offset + (if time < startTime then 0.0 else height); + annotation ( + Documentation(info=" +

+The Real output y is a step signal: +

+ +

+\"Step.png\" +

+ +")); +end Modelica.Blocks.Sources.Step; diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 0dd93cb..f8d1bc4 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -41,13 +41,15 @@ import * as LSP from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; - +import * as fs from 'fs'; import Parser = require('web-tree-sitter'); import { + getLocalDeclarations, getAllDeclarationsInTree } from './util/declarations'; import { logger } from './util/logger'; +import { log } from 'console'; type AnalyzedDocument = { document: TextDocument, @@ -59,11 +61,12 @@ export default class Analyzer { private parser: Parser; private uriToAnalyzedDocument: Record = {}; - constructor (parser: Parser) { + constructor(parser:Parser) + { this.parser = parser; - } + } - public analyze(document: TextDocument): LSP.Diagnostic[] { + public analyze({document}: {document: TextDocument}): LSP.Diagnostic[] { logger.debug('analyze:'); const diagnostics: LSP.Diagnostic[] = []; @@ -80,7 +83,7 @@ export default class Analyzer { this.uriToAnalyzedDocument[uri] = { document, declarations, - tree + tree, }; return diagnostics; @@ -100,4 +103,220 @@ export default class Analyzer { return getAllDeclarationsInTree(tree, uri); } + + /** + * Find declarations for the given word and position. + */ + public findDeclarationsMatchingWord({ + exactMatch, + position, + uri, + word, + }: { + exactMatch: boolean + position: LSP.Position + uri: string + word: string + }): LSP.SymbolInformation[] { + logger.debug('Finding Declarations Matching Word...'); + return this.getAllDeclarations({ uri, position }).filter((symbol) => { + if (exactMatch) { + logger.debug('name === word'); + return symbol.name === word + } else { + logger.debug('name.startsWith(word)'); + return symbol.name.startsWith(word) + } + }) + } + + private getAnalyzedReachableUris({ fromUri }: { fromUri?: string } = {}): string[] { + return this.ensureUrisAreAnalyzed(this.getReachableUris({ fromUri })) + } + + private ensureUrisAreAnalyzed(uris: string[]): string[] { + return uris.filter((uri) => { + if (!this.uriToAnalyzedDocument[uri]) { + // Either the background analysis didn't run or the file is outside + // the workspace. Let us try to analyze the file. + try { + logger.debug(`Analyzing file not covered by background analysis ${uri}`) + const fileContent = fs.readFileSync(new URL(uri), 'utf8') + this.analyze({ + document: TextDocument.create(uri, 'modelica', 1, fileContent), + }) + } catch (err) { + logger.warn(`Error while analyzing file ${uri}: ${err}`) + return false + } + } + + return true + }) + } + + private getReachableUris({ fromUri }: { fromUri?: string } = {}): string[] { + if (!fromUri) { + return Object.keys(this.uriToAnalyzedDocument) + } + return [fromUri] + } + + private getAllDeclarations({ + uri: fromUri, + position, + }: { uri?: string; position?: LSP.Position } = {}): LSP.SymbolInformation[] { + return this.getAnalyzedReachableUris({ fromUri }).reduce((symbols, uri) => { + logger.debug('getAnalyzedReachableUris Initialized'); + const analyzedDocument = this.uriToAnalyzedDocument[uri] + + if (analyzedDocument) { + if (uri !== fromUri || !position) { + // TODO: Use the global declarations for external files or if we do not have a position + } + + // For the current file we find declarations based on the current scope + if (uri === fromUri && position) { + const node = analyzedDocument.tree.rootNode?.descendantForPosition({ + row: position.line, + column: position.character, + }) + + const localDeclarations = getLocalDeclarations({ + node, + rootNode: analyzedDocument.tree.rootNode, + uri, + }) + logger.debug('localDeclarations: ', localDeclarations); + Object.keys(localDeclarations).map((name) => { + const symbolsMatchingWord = localDeclarations[name] + + // Find the latest definition + let closestSymbol: LSP.SymbolInformation | null = null + symbolsMatchingWord.forEach((symbol) => { + // Skip if the symbol is defined in the current file after the requested position + if (symbol.location.range.start.line > position.line) { + return + } + + if ( + closestSymbol === null || + symbol.location.range.start.line > closestSymbol.location.range.start.line + ) { + closestSymbol = symbol + } + }) + + if (closestSymbol) { + logger.debug('ClosestSymbol: ', closestSymbol); + symbols.push(closestSymbol) + } + }) + } + } + + return symbols + }, [] as LSP.SymbolInformation[]) + } + + /** + * Find a block of comments above a line position + */ + public commentsAbove(uri: string, line: number): string | null { + const doc = this.uriToAnalyzedDocument[uri]?.document; + if (!doc) { + return null; + } + + let commentBlock = []; + let inBlockComment = false; + + // start from the line above + let commentBlockIndex = line - 1; + + while (commentBlockIndex >= 0) { + let currentLineText = doc.getText({ + start: { line: commentBlockIndex, character: 0 }, + end: { line: commentBlockIndex + 1, character: 0 }, + }).trim(); + + if (inBlockComment) { + if (currentLineText.startsWith('/*')) { + inBlockComment = false; + // Remove the /* from the start + currentLineText = currentLineText.substring(2).trim(); + } else { + // Remove leading * from lines within the block comment + currentLineText = currentLineText.replace(/^\*\s?/, '').trim(); + } + if (currentLineText) { // Don't add empty lines + commentBlock.push(currentLineText); + } + } else { + if (currentLineText.startsWith('//')) { + // Strip the // and add to block + commentBlock.push(currentLineText.substring(2).trim()); + } else if (currentLineText.endsWith('*/')) { + inBlockComment = true; + // Remove the */ from the end + currentLineText = currentLineText.substring(0, currentLineText.length - 2).trim(); + if (currentLineText) { // Don't add empty lines + commentBlock.push(currentLineText); + } + } else { + break; // Stop if the current line is not part of a comment + } + } + + commentBlockIndex -= 1; + } + + if (commentBlock.length) { + commentBlock = ['```txt', ...commentBlock.reverse(), '```']; + return commentBlock.join('\n'); + } + + return null; + } + + /** + * Find the full word at the given point. + */ + public wordAtPoint(uri: string, line: number, column: number): string | null { + const node = this.nodeAtPoint(uri, line, column) + + if (!node || node.childCount > 0 || node.text.trim() === '') { + return null + } + + return node.text.trim() + } + + public wordAtPointFromTextPosition( + params: LSP.TextDocumentPositionParams, + ): string | null { + return this.wordAtPoint( + params.textDocument.uri, + params.position.line, + params.position.character, + ) + } + + /** + * Find the node at the given point. + */ + private nodeAtPoint( + uri: string, + line: number, + column: number, + ): Parser.SyntaxNode | null { + const tree = this.uriToAnalyzedDocument[uri]?.tree + + if (!tree?.rootNode) { + // Check for lacking rootNode (due to failed parse?) + return null + } + + return tree.rootNode.descendantForPosition({ row: line, column }) + } } diff --git a/server/src/server.ts b/server/src/server.ts index 4bc00e4..3598a46 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -36,15 +36,17 @@ /* ----------------------------------------------------------------------------- * Taken from bash-language-server and adapted to Modelica language server * https://github.com/bash-lsp/bash-language-server/blob/main/server/src/server.ts - * ----------------------------------------------------------------------------- + * ---------------------------------------------------------------------------- */ +import * as path from 'node:path'; import * as LSP from 'vscode-languageserver/node'; import { TextDocument} from 'vscode-languageserver-textdocument'; import { initializeParser } from './parser'; import Analyzer from './analyzer'; import { logger, setLogConnection, setLogLevel } from './util/logger'; +import { uniqueBasedOnHash } from './util/array'; /** * ModelicaServer collection all the important bits and bobs. @@ -57,11 +59,11 @@ export class ModelicaServer { private constructor( analyzer: Analyzer, - clientCapabilities: LSP.ClientCapabilities, + capabilities: LSP.ClientCapabilities, connection: LSP.Connection ) { this.analyzer = analyzer; - this.clientCapabilities = clientCapabilities; + this.clientCapabilities = capabilities; this.connection = connection; } @@ -91,7 +93,7 @@ export class ModelicaServer { return { textDocumentSync: LSP.TextDocumentSyncKind.Full, completionProvider: undefined, - hoverProvider: false, + hoverProvider: true, signatureHelpProvider: undefined, documentSymbolProvider: true, colorProvider: false, @@ -99,26 +101,19 @@ export class ModelicaServer { }; } + /** + * Register handlers for the events from the Language Server Protocol + * + * @param connection + */ public register(connection: LSP.Connection): void { - 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 () => { - 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); - } - }); - + // 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 }) => { @@ -132,13 +127,75 @@ export class ModelicaServer { this.analyzeDocument(document); } }); + + connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); + connection.onHover(this.onHover.bind(this)); + logger.debug('Event Handlers Registered'); + + connection.onInitialized(async () => { + 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); + } + }); } private async analyzeDocument(document: TextDocument) { - const diagnostics = this.analyzer.analyze(document); + const diagnostics = this.analyzer.analyze({document}); + } + + private logRequest({ + request, + params, + word, + }: { + request: string + params: LSP.ReferenceParams | LSP.TextDocumentPositionParams + word?: string | null + }) { + const wordLog = word ? `"${word}"` : 'null' + logger.debug( + `${request} ${params.position.line}:${params.position.character} word=${wordLog}`, + ) } +// getDocumentationForSymbol aus dem Bash LSP + private getDocumentationForSymbol({ + currentUri, + symbol, + }: { + symbol: LSP.SymbolInformation + currentUri: string + }): LSP.MarkupContent { + logger.debug( + `getDocumentationForSymbol: symbol=${symbol.name} uri=${symbol.location.uri}`, + ) + const symbolUri = symbol.location.uri + const symbolStartLine = symbol.location.range.start.line + + const commentAboveSymbol = this.analyzer.commentsAbove(symbolUri, symbolStartLine) + const symbolDocumentation = commentAboveSymbol ? `\n\n${commentAboveSymbol}` : '' + const hoverHeader = `${symbolKindToDescription(symbol.kind)}: **${symbol.name}**` + const symbolLocation = + symbolUri !== currentUri + ? `in ${path.relative(path.dirname(currentUri), symbolUri)}` + : `on line ${symbolStartLine + 1}` + + // TODO: An improvement could be to add show the symbol definition in the hover instead + // of the defined location – similar to how VSCode works for languages like TypeScript. + + return getMarkdownContent( + `${hoverHeader} - *defined ${symbolLocation}*${symbolDocumentation}`, + ) + } + + // ============================== + // Language server event handlers + // ============================== + /** * Provide symbols defined in document. * @@ -153,6 +210,111 @@ export class ModelicaServer { return this.analyzer.getDeclarationsForUri(params.textDocument.uri); } + private async onHover( + params: LSP.TextDocumentPositionParams, + ): Promise { + const word = this.analyzer.wordAtPointFromTextPosition(params) + const currentUri = params.textDocument.uri + logger.debug('------------'); + this.logRequest({ request: 'onHover init', params, word }) + + if (!word) { + return null + } + + const symbolsMatchingWord = this.analyzer.findDeclarationsMatchingWord({ + exactMatch: true, + uri: currentUri, + word, + position: params.position, + }) + logger.debug('symbolsMatchingWord: ', symbolsMatchingWord); + + const symbolDocumentation = deduplicateSymbols({ + symbols: symbolsMatchingWord, + currentUri, + }) + // do not return hover referencing for the current line + .filter( + (symbol) => + symbol.location.uri !== currentUri || + symbol.location.range.start.line !== params.position.line, + ) + .map((symbol: LSP.SymbolInformation) => + this.getDocumentationForSymbol({ currentUri, symbol }), + ) + + if (symbolDocumentation.length === 1) { + logger.debug('Symbol Documentation: ', symbolDocumentation[0]); + return { contents: symbolDocumentation[0] } + } + return null + } +} + +/* +return { contents: { kind: LSP.MarkupKind.Markdown, value: [ + '# Test', + 'Text text text', + '```modelica code```'].join('\n') + } +} +*/ +/** + * Deduplicate symbols by prioritizing the current file. + */ +function deduplicateSymbols({ + symbols, + currentUri, +}: { + symbols: LSP.SymbolInformation[] + currentUri: string +}) { + const isCurrentFile = ({ location: { uri } }: LSP.SymbolInformation) => + uri === currentUri + + const getSymbolId = ({ name, kind }: LSP.SymbolInformation) => `${name}${kind}` + + const symbolsCurrentFile = symbols.filter((s) => isCurrentFile(s)) + + const symbolsOtherFiles = symbols + .filter((s) => !isCurrentFile(s)) + // Remove identical symbols matching current file + .filter( + (symbolOtherFiles) => + !symbolsCurrentFile.some( + (symbolCurrentFile) => + getSymbolId(symbolCurrentFile) === getSymbolId(symbolOtherFiles), + ), + ) + + // NOTE: it might be that uniqueBasedOnHash is not needed anymore + return uniqueBasedOnHash([...symbolsCurrentFile, ...symbolsOtherFiles], getSymbolId) +} + +function symbolKindToDescription(kind: LSP.SymbolKind): string { + switch (kind) { + case LSP.SymbolKind.Class: + return 'Class'; + case LSP.SymbolKind.Function: + return 'Function'; + case LSP.SymbolKind.Package: + return 'Package'; + case LSP.SymbolKind.TypeParameter: + return 'Type'; + default: + return 'Modelica symbol'; + } +} + +function getMarkdownContent(documentation: string, language?: string): LSP.MarkupContent { + return { + value: language + ? // eslint-disable-next-line prefer-template + ['``` ' + language, documentation, '```'].join('\n') + : documentation, + kind: LSP.MarkupKind.Markdown, + } } // Create a connection for the server, using Node's IPC as a transport. diff --git a/server/src/util/array.ts b/server/src/util/array.ts new file mode 100644 index 0000000..292ce32 --- /dev/null +++ b/server/src/util/array.ts @@ -0,0 +1,38 @@ +/** + * Flatten a 2-dimensional array into a 1-dimensional one. + */ +export function flattenArray(nestedArray: T[][]): T[] { + return nestedArray.reduce((acc, array) => [...acc, ...array], []) +} + +/** + * Remove all duplicates from the list. + * Doesn't preserve ordering. + */ +export function uniq(a: A[]): A[] { + return Array.from(new Set(a)) +} + +/** + * Removed all duplicates from the list based on the hash function. + * First element matching the hash function wins. + */ +export function uniqueBasedOnHash>( + list: A[], + elementToHash: (a: A) => string, + __result: A[] = [], +): A[] { + const result: typeof list = [] + const hashSet = new Set() + + list.forEach((element) => { + const hash = elementToHash(element) + if (hashSet.has(hash)) { + return + } + hashSet.add(hash) + result.push(element) + }) + + return result +} diff --git a/server/src/util/declarations.ts b/server/src/util/declarations.ts index fbcface..7ca414d 100644 --- a/server/src/util/declarations.ts +++ b/server/src/util/declarations.ts @@ -54,6 +54,51 @@ const GLOBAL_DECLARATION_LEAF_NODE_TYPES = new Set([ 'function_definition', ]); +export function getLocalDeclarations({ + node, + rootNode, + uri, +}: { + node: Parser.SyntaxNode | null + rootNode: Parser.SyntaxNode + uri: string +}): Declarations { + const declarations: Declarations = {} + + // Bottom up traversal to capture all local and scoped declarations + const walk = (node: Parser.SyntaxNode | null) => { + if (node) { + for (const childNode of node.children) { + let symbol: LSP.SymbolInformation | null = null + + // local variables + if (childNode.type === 'component_reference') { + const identifierNode = childNode.children.filter( + (child) => child.type === 'IDENT', + )[0] + if (identifierNode) { + symbol = nodeToSymbolInformation({node:identifierNode, uri}); + } + } else { + symbol = getDeclarationSymbolFromNode({ node: childNode, uri}); + } + + if (symbol) { + if (!declarations[symbol.name]) { + declarations[symbol.name] = [] + } + declarations[symbol.name].push(symbol) + } + } + + walk(node.parent) + } + } + walk(node) + + return declarations +} + /** * Returns all declarations (functions or variables) from a given tree. * @@ -65,7 +110,7 @@ export function getAllDeclarationsInTree(tree: Parser.Tree, uri: string): LSP.Sy const symbols: LSP.SymbolInformation[] = []; TreeSitterUtil.forEach(tree.rootNode, (node) => { - const symbol = getDeclarationSymbolFromNode(node, uri); + const symbol = getDeclarationSymbolFromNode({node, uri}); if (symbol) { symbols.push(symbol); } @@ -81,7 +126,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, uri}: {node: Parser.SyntaxNode, uri: string}): LSP.SymbolInformation | null { const named = node.firstNamedChild; if (named === null) { @@ -115,9 +160,10 @@ 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, uri}: {node: Parser.SyntaxNode, uri: string}): LSP.SymbolInformation | null { + // hier dann andere nodes hinzufügen z.B. short_class_specifier if (TreeSitterUtil.isDefinition(node)) { - return nodeToSymbolInformation(node, uri); + return nodeToSymbolInformation({node, uri}); } return null; From 8d7c3235c87c5cf14653a54a0ea14d0f28926ec4 Mon Sep 17 00:00:00 2001 From: osman362 <136453917+osman362@users.noreply.github.com> Date: Mon, 26 Feb 2024 16:00:47 +0100 Subject: [PATCH 02/12] onHover returns content of description_string --- server/src/analyzer.ts | 9 +++++++++ server/src/server.ts | 19 +++++++++++++------ server/src/util/hoverUtil.ts | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 server/src/util/hoverUtil.ts diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index f8d1bc4..b3d8ac0 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -50,6 +50,7 @@ import { } from './util/declarations'; import { logger } from './util/logger'; import { log } from 'console'; +import { extractHoverInformation } from './util/hoverUtil'; type AnalyzedDocument = { document: TextDocument, @@ -319,4 +320,12 @@ export default class Analyzer { return tree.rootNode.descendantForPosition({ row: line, column }) } + + public descriptionInfo( + uri: string, + position: LSP.Position + ): string | null{ + const targetNode = this.nodeAtPoint(uri, position.line, position.character); + return extractHoverInformation(targetNode) + } } diff --git a/server/src/server.ts b/server/src/server.ts index 3598a46..55968b4 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -170,14 +170,14 @@ export class ModelicaServer { symbol: LSP.SymbolInformation currentUri: string }): LSP.MarkupContent { - logger.debug( - `getDocumentationForSymbol: symbol=${symbol.name} uri=${symbol.location.uri}`, - ) + + logger.debug(`getDocumentationForSymbol: symbol=${symbol.name} uri=${symbol.location.uri}`) + const symbolUri = symbol.location.uri const symbolStartLine = symbol.location.range.start.line const commentAboveSymbol = this.analyzer.commentsAbove(symbolUri, symbolStartLine) - const symbolDocumentation = commentAboveSymbol ? `\n\n${commentAboveSymbol}` : '' + const commentAboveDocumentation = commentAboveSymbol ? `\n\n${commentAboveSymbol}` : '' const hoverHeader = `${symbolKindToDescription(symbol.kind)}: **${symbol.name}**` const symbolLocation = symbolUri !== currentUri @@ -188,7 +188,7 @@ export class ModelicaServer { // of the defined location – similar to how VSCode works for languages like TypeScript. return getMarkdownContent( - `${hoverHeader} - *defined ${symbolLocation}*${symbolDocumentation}`, + `${hoverHeader} - *defined ${symbolLocation}*${commentAboveDocumentation}`, ) } @@ -246,7 +246,14 @@ export class ModelicaServer { if (symbolDocumentation.length === 1) { logger.debug('Symbol Documentation: ', symbolDocumentation[0]); - return { contents: symbolDocumentation[0] } + const position = params.position + const uri = currentUri + const description = this.analyzer.descriptionInfo(uri, position) + if (description) { + return {contents: getMarkdownContent(description)} + } + //return { contents: symbolDocumentation[0] } + return null } return null } diff --git a/server/src/util/hoverUtil.ts b/server/src/util/hoverUtil.ts new file mode 100644 index 0000000..fec9daa --- /dev/null +++ b/server/src/util/hoverUtil.ts @@ -0,0 +1,32 @@ +import { SyntaxNode } from 'web-tree-sitter'; +import * as TreeSitterUtil from './tree-sitter'; +import * as LSP from 'vscode-languageserver'; + +/** + * Extracts hover information for a Modelica class or package. + * + * @param rootNode The root node of the AST for the current document. + * @param position The position of the cursor in the document. + * @returns Markdown formatted string with hover information. + */ +export function extractHoverInformation(targetNode: SyntaxNode | null): string | null { + // Find the node at the cursor's position. + + if (!targetNode) { + return null; + } + + // Find the parent class_definition node. + const classDefNode = TreeSitterUtil.findParent(targetNode, n => n.type === 'class_definition'); + + if (!classDefNode) { + return null; + } + + // Extract the description_string if it exists. + const descriptionNode = TreeSitterUtil.findFirst(classDefNode, n => n.type === 'description_string'); + const descriptionText = descriptionNode ? descriptionNode.firstChild?.text : null; + + // Format as Markdown (simple example). + return descriptionText ? descriptionText : "No description available."; +} From 5faba65c786e446300c4bf575f7a96292b069d05 Mon Sep 17 00:00:00 2001 From: osman362 <136453917+osman362@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:28:05 +0100 Subject: [PATCH 03/12] description_string content inside hover pop-up --- server/src/analyzer.ts | 2 +- server/src/server.ts | 26 +++++++++++++------------- server/src/util/hoverUtil.ts | 30 +++++++++++++++++++++++------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index b3d8ac0..67de6e5 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -324,7 +324,7 @@ export default class Analyzer { public descriptionInfo( uri: string, position: LSP.Position - ): string | null{ + ): string{ const targetNode = this.nodeAtPoint(uri, position.line, position.character); return extractHoverInformation(targetNode) } diff --git a/server/src/server.ts b/server/src/server.ts index 55968b4..0aaa695 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -169,7 +169,7 @@ export class ModelicaServer { }: { symbol: LSP.SymbolInformation currentUri: string - }): LSP.MarkupContent { + }): string { logger.debug(`getDocumentationForSymbol: symbol=${symbol.name} uri=${symbol.location.uri}`) @@ -187,9 +187,8 @@ export class ModelicaServer { // TODO: An improvement could be to add show the symbol definition in the hover instead // of the defined location – similar to how VSCode works for languages like TypeScript. - return getMarkdownContent( - `${hoverHeader} - *defined ${symbolLocation}*${commentAboveDocumentation}`, - ) + return `${hoverHeader} - *defined ${symbolLocation}*${commentAboveDocumentation}` + } // ============================== @@ -243,18 +242,18 @@ export class ModelicaServer { .map((symbol: LSP.SymbolInformation) => this.getDocumentationForSymbol({ currentUri, symbol }), ) - + /* if (symbolDocumentation.length === 1) { logger.debug('Symbol Documentation: ', symbolDocumentation[0]); - const position = params.position - const uri = currentUri - const description = this.analyzer.descriptionInfo(uri, position) - if (description) { - return {contents: getMarkdownContent(description)} - } - //return { contents: symbolDocumentation[0] } - return null + return { contents: symbolDocumentation[0] }; + }*/ + const description = this.analyzer.descriptionInfo(currentUri, params.position) + + if (symbolDocumentation.length === 1 || description) { + logger.debug('Documentation: ', symbolDocumentation[0], description); + return { contents: getMarkdownContent(symbolDocumentation[0] + description) }; } + return null } } @@ -270,6 +269,7 @@ return { contents: { kind: LSP.MarkupKind.Markdown, value: [ /** * Deduplicate symbols by prioritizing the current file. */ + function deduplicateSymbols({ symbols, currentUri, diff --git a/server/src/util/hoverUtil.ts b/server/src/util/hoverUtil.ts index fec9daa..1c05c69 100644 --- a/server/src/util/hoverUtil.ts +++ b/server/src/util/hoverUtil.ts @@ -1,32 +1,48 @@ import { SyntaxNode } from 'web-tree-sitter'; import * as TreeSitterUtil from './tree-sitter'; import * as LSP from 'vscode-languageserver'; +import { logger } from './logger'; /** * Extracts hover information for a Modelica class or package. * * @param rootNode The root node of the AST for the current document. * @param position The position of the cursor in the document. - * @returns Markdown formatted string with hover information. + * @returns Text of the description_string or string saying there is no description. */ -export function extractHoverInformation(targetNode: SyntaxNode | null): string | null { - // Find the node at the cursor's position. +export function extractHoverInformation(targetNode: SyntaxNode | null): string { if (!targetNode) { - return null; + logger.debug('No target node found.'); + return ''; + } + + if (targetNode.type !== 'IDENT'){ + logger.debug('Target node is not an identifier.'); + return ''; } // Find the parent class_definition node. const classDefNode = TreeSitterUtil.findParent(targetNode, n => n.type === 'class_definition'); if (!classDefNode) { - return null; + logger.debug('No class definition found.'); + return ''; + } + + // Check if the targetNode is the first IDENT child of the class_definition, indicating it's the class name. + const isClassName = classDefNode.namedChildren.some((child, index) => + child.type === 'long_class_specifier' && + child.firstChild?.type === 'IDENT' && + child.firstChild?.text === targetNode.text); + + if (!isClassName) { + return ''; // The targetNode is not the class name identifier. } // Extract the description_string if it exists. const descriptionNode = TreeSitterUtil.findFirst(classDefNode, n => n.type === 'description_string'); const descriptionText = descriptionNode ? descriptionNode.firstChild?.text : null; - // Format as Markdown (simple example). - return descriptionText ? descriptionText : "No description available."; + return descriptionText ? `\n\n${descriptionText}` : `\n\n No description available.`; } From f331353f0294e68377be1686d79713bc6dec420c Mon Sep 17 00:00:00 2001 From: osman362 <136453917+osman362@users.noreply.github.com> Date: Fri, 1 Mar 2024 00:02:32 +0100 Subject: [PATCH 04/12] Hover shows Class Descriptions --- server/src/analyzer.ts | 8 +- server/src/server.ts | 19 ++-- server/src/util/hoverUtil.ts | 168 ++++++++++++++++++++++++++++++----- 3 files changed, 159 insertions(+), 36 deletions(-) diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 67de6e5..b688818 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -273,8 +273,8 @@ export default class Analyzer { } if (commentBlock.length) { - commentBlock = ['```txt', ...commentBlock.reverse(), '```']; - return commentBlock.join('\n'); + commentBlock = [...commentBlock.reverse()]; + return commentBlock.join('\n\n'); } return null; @@ -326,6 +326,10 @@ export default class Analyzer { position: LSP.Position ): string{ const targetNode = this.nodeAtPoint(uri, position.line, position.character); + if (!targetNode) { + logger.debug('No target node found.'); + return ''; + } return extractHoverInformation(targetNode) } } diff --git a/server/src/server.ts b/server/src/server.ts index 0aaa695..d3b7848 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -187,8 +187,10 @@ export class ModelicaServer { // TODO: An improvement could be to add show the symbol definition in the hover instead // of the defined location – similar to how VSCode works for languages like TypeScript. - return `${hoverHeader} - *defined ${symbolLocation}*${commentAboveDocumentation}` - + if (!undefined) {return `${hoverHeader} - *defined ${symbolLocation}* \n${commentAboveDocumentation}` + } else { + return `${hoverHeader}` + } } // ============================== @@ -232,28 +234,23 @@ export class ModelicaServer { const symbolDocumentation = deduplicateSymbols({ symbols: symbolsMatchingWord, currentUri, - }) + })/* // do not return hover referencing for the current line .filter( (symbol) => symbol.location.uri !== currentUri || symbol.location.range.start.line !== params.position.line, - ) + )*/ .map((symbol: LSP.SymbolInformation) => this.getDocumentationForSymbol({ currentUri, symbol }), ) - /* - if (symbolDocumentation.length === 1) { - logger.debug('Symbol Documentation: ', symbolDocumentation[0]); - return { contents: symbolDocumentation[0] }; - }*/ const description = this.analyzer.descriptionInfo(currentUri, params.position) if (symbolDocumentation.length === 1 || description) { logger.debug('Documentation: ', symbolDocumentation[0], description); - return { contents: getMarkdownContent(symbolDocumentation[0] + description) }; + return { contents: getMarkdownContent(symbolDocumentation[0], description) }; } - + return null } } diff --git a/server/src/util/hoverUtil.ts b/server/src/util/hoverUtil.ts index 1c05c69..5e69524 100644 --- a/server/src/util/hoverUtil.ts +++ b/server/src/util/hoverUtil.ts @@ -10,17 +10,7 @@ import { logger } from './logger'; * @param position The position of the cursor in the document. * @returns Text of the description_string or string saying there is no description. */ -export function extractHoverInformation(targetNode: SyntaxNode | null): string { - - if (!targetNode) { - logger.debug('No target node found.'); - return ''; - } - - if (targetNode.type !== 'IDENT'){ - logger.debug('Target node is not an identifier.'); - return ''; - } +export function extractHoverInformation(targetNode: SyntaxNode): string { // Find the parent class_definition node. const classDefNode = TreeSitterUtil.findParent(targetNode, n => n.type === 'class_definition'); @@ -30,19 +20,151 @@ export function extractHoverInformation(targetNode: SyntaxNode | null): string { return ''; } - // Check if the targetNode is the first IDENT child of the class_definition, indicating it's the class name. - const isClassName = classDefNode.namedChildren.some((child, index) => - child.type === 'long_class_specifier' && - child.firstChild?.type === 'IDENT' && - child.firstChild?.text === targetNode.text); + const getClassDescription = extractClassDescription(classDefNode, targetNode); + logger.debug(`Class description: ${getClassDescription}`); + return `${getClassDescription}`; +} - if (!isClassName) { - return ''; // The targetNode is not the class name identifier. - } +function extractClassDescription(classDefNode: SyntaxNode, targetNode: SyntaxNode): string { + + // Check if the targetNode is the first IDENT child of the class_definition, indicating it's the class name. + const isClassName = classDefNode.namedChildren.some((child, index) => + child.type === 'long_class_specifier' && + child.firstChild?.type === 'IDENT' && + child.firstChild?.text === targetNode.text); + + if (!isClassName) { + logger.debug('Target node is not the class name identifier.'); + return ''; // The targetNode is not the class name identifier. + } - // Extract the description_string if it exists. - const descriptionNode = TreeSitterUtil.findFirst(classDefNode, n => n.type === 'description_string'); - const descriptionText = descriptionNode ? descriptionNode.firstChild?.text : null; + // Extract the description_string if it exists. + const descriptionNode = TreeSitterUtil.findFirst(classDefNode, n => n.type === 'description_string'); + const descriptionText = descriptionNode ? descriptionNode.firstChild?.text : null; - return descriptionText ? `\n\n${descriptionText}` : `\n\n No description available.`; + return descriptionText ? `\n${descriptionText}` : '\nNo description available.'; } + +function extractInputs(classDefNode: SyntaxNode, targetNode: SyntaxNode): string { + // Placeholder for future extensions + return ''; +} + +/* +function extractHoverInformation(ast: Parser.SyntaxNode, position: LSP.Position): string { + let hoverMarkdown = ""; + + // Extract description + const description = getDescription(ast, position); + if (description) { + hoverMarkdown += `**Description:** ${description}\n\n`; + } + + // Placeholder for future extensions + // const inputs = getInputs(ast, position); + // const outputs = getOutputs(ast, position); + // const parameters = getParameters(ast, position); + + // Add to Markdown as these features are implemented + // if (inputs) { hoverMarkdown += `**Inputs:** ${inputs}\n\n`; } + // if (outputs) { hoverMarkdown += `**Outputs:** ${outputs}\n\n`; } + // if (parameters) { hoverMarkdown += `**Parameters:** ${parameters}\n\n`; } + + return hoverMarkdown; +} + +export function extractHoverInformation(classDefNode: SyntaxNode): string { + // Initialize Markdown content with the class description + let markdownContent = `**Description:** ${getClassDescription(classDefNode)}\n\n`; + + // Variables to store inputs, outputs, and parameters + let inputs: { identifier: string, description: string }[] = []; + let outputs: { identifier: string, description: string }[] = []; + let parameters: { identifier: string, description: string }[] = []; + + // Traverse the class definition's children to find relevant nodes + TreeSitterUtil.forEach(classDefNode, (node) => { + if (node.type === 'component_clause') { + const identifier = TreeSitterUtil.getIdentifier(node); + const description = getComponentDescription(node); + + if(!identifier) return false; // Skip if no identifier is found (e.g. for unnamed components + + if (node.text.includes('input')) { + inputs.push({ identifier, description: description ?? '' }); + } else if (node.text.includes('output')) { + outputs.push({ identifier, description: description ?? '' }); + } else if (node.text.includes('parameter')) { + parameters.push({ identifier, description: description ?? '' }); + } + } + return true; // Continue traversal + }); + + // Format and add inputs, outputs, and parameters to Markdown content + if (inputs.length > 0) { + markdownContent += formatComponentSection("Inputs", inputs); + } + if (outputs.length > 0) { + markdownContent += formatComponentSection("Outputs", outputs); + } + if (parameters.length > 0) { + markdownContent += formatComponentSection("Parameters", parameters); + } + + return markdownContent; +} + +// Helper function to format sections for inputs, outputs, and parameters +function formatComponentSection(title: string, components: Array<{identifier: string, description: string}>): string { + let section = `**${title}:**\n`; + components.forEach(comp => { + section += `- ${comp.identifier}: ${comp.description}\n`; + }); + return section + "\n"; // Add extra newline for spacing +} + +// Assume getClassDescription and getComponentDescription are implemented to extract descriptions + +/** + * Extracts the class description from a class_definition node. + * + * @param classDefNode The class definition syntax node. + * @returns The description text if available, otherwise a default message. + * +function getClassDescription(classDefNode: SyntaxNode): string { + // Find the description_string node directly under the class definition + const descriptionNode = classDefNode.namedChildren.find(child => child.type === 'description_string'); + + // If a description_string node is found, return its text content + if (descriptionNode && descriptionNode.firstChild) { + return descriptionNode.firstChild.text.trim(); + } + + // Default message if no description is available + return "No class description available."; +} + +/** + * Extracts the description for a component (input, output, parameter) from its node. + * + * @param componentNode The syntax node for the component. + * @returns The description text if available, otherwise a default message. + * +function getComponentDescription(componentNode: SyntaxNode): string { + // Assuming descriptions are in comments directly above the component declaration + let description = "No description available."; // Default message + + // Attempt to find a comment node immediately preceding the componentNode + let precedingNode = componentNode.previousSibling; + while (precedingNode) { + if (precedingNode.type === 'description_string') { + // If a comment is found, use its text as the description + description = precedingNode.text.trim(); + break; + } + precedingNode = precedingNode.previousSibling; + } + + return description; +}*/ From aa25db2142dd14b65f6be8b0318e7227bb14ebb8 Mon Sep 17 00:00:00 2001 From: osman362 <136453917+osman362@users.noreply.github.com> Date: Fri, 1 Mar 2024 14:57:24 +0100 Subject: [PATCH 05/12] Hover showing Inputs, Outputs & Parameters --- server/src/analyzer.ts | 4 +- server/src/server.ts | 34 +++--- server/src/util/hoverUtil.ts | 216 +++++++++++++++-------------------- 3 files changed, 110 insertions(+), 144 deletions(-) diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index b688818..ff54dfa 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -321,10 +321,10 @@ export default class Analyzer { return tree.rootNode.descendantForPosition({ row: line, column }) } - public descriptionInfo( + public hoverInformations( uri: string, position: LSP.Position - ): string{ + ): string { const targetNode = this.nodeAtPoint(uri, position.line, position.character); if (!targetNode) { logger.debug('No target node found.'); diff --git a/server/src/server.ts b/server/src/server.ts index d3b7848..fa92b89 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -163,7 +163,7 @@ export class ModelicaServer { } // getDocumentationForSymbol aus dem Bash LSP - private getDocumentationForSymbol({ + private getCommentForSymbol({ currentUri, symbol, }: { @@ -177,7 +177,7 @@ export class ModelicaServer { const symbolStartLine = symbol.location.range.start.line const commentAboveSymbol = this.analyzer.commentsAbove(symbolUri, symbolStartLine) - const commentAboveDocumentation = commentAboveSymbol ? `\n\n${commentAboveSymbol}` : '' + const commentAbove = commentAboveSymbol ? `\n\n${commentAboveSymbol}` : '' const hoverHeader = `${symbolKindToDescription(symbol.kind)}: **${symbol.name}**` const symbolLocation = symbolUri !== currentUri @@ -187,10 +187,11 @@ export class ModelicaServer { // TODO: An improvement could be to add show the symbol definition in the hover instead // of the defined location – similar to how VSCode works for languages like TypeScript. - if (!undefined) {return `${hoverHeader} - *defined ${symbolLocation}* \n${commentAboveDocumentation}` - } else { - return `${hoverHeader}` - } + return `\n${commentAbove}` + } + + private documentationForHover(hoverInfo: string): LSP.MarkupContent | null { + return null } // ============================== @@ -231,7 +232,7 @@ export class ModelicaServer { }) logger.debug('symbolsMatchingWord: ', symbolsMatchingWord); - const symbolDocumentation = deduplicateSymbols({ + const commentAboveDocumentation = deduplicateSymbols({ symbols: symbolsMatchingWord, currentUri, })/* @@ -242,31 +243,22 @@ export class ModelicaServer { symbol.location.range.start.line !== params.position.line, )*/ .map((symbol: LSP.SymbolInformation) => - this.getDocumentationForSymbol({ currentUri, symbol }), + this.getCommentForSymbol({ currentUri, symbol }), ) - const description = this.analyzer.descriptionInfo(currentUri, params.position) + const hoverInfo = this.analyzer.hoverInformations(currentUri, params.position) - if (symbolDocumentation.length === 1 || description) { - logger.debug('Documentation: ', symbolDocumentation[0], description); - return { contents: getMarkdownContent(symbolDocumentation[0], description) }; + if (hoverInfo) { + logger.debug('Documentation: ', hoverInfo); + return { contents: getMarkdownContent(hoverInfo) }; } return null } } -/* -return { contents: { kind: LSP.MarkupKind.Markdown, value: [ - '# Test', - 'Text text text', - '```modelica code```'].join('\n') - } -} -*/ /** * Deduplicate symbols by prioritizing the current file. */ - function deduplicateSymbols({ symbols, currentUri, diff --git a/server/src/util/hoverUtil.ts b/server/src/util/hoverUtil.ts index 5e69524..0573596 100644 --- a/server/src/util/hoverUtil.ts +++ b/server/src/util/hoverUtil.ts @@ -20,151 +20,125 @@ export function extractHoverInformation(targetNode: SyntaxNode): string { return ''; } - const getClassDescription = extractClassDescription(classDefNode, targetNode); - logger.debug(`Class description: ${getClassDescription}`); - return `${getClassDescription}`; -} - -function extractClassDescription(classDefNode: SyntaxNode, targetNode: SyntaxNode): string { + // Check if the targetNode is the first IDENT child of the class_definition, indicating it's the class name. + const isClassName = classDefNode.namedChildren.some((child, index) => + child.type === 'long_class_specifier' && + child.firstChild?.type === 'IDENT' && + child.firstChild?.text === targetNode.text); + + if (!isClassName) { + logger.debug('Target node is not the class name identifier.'); + return ''; + } - // Check if the targetNode is the first IDENT child of the class_definition, indicating it's the class name. - const isClassName = classDefNode.namedChildren.some((child, index) => - child.type === 'long_class_specifier' && - child.firstChild?.type === 'IDENT' && - child.firstChild?.text === targetNode.text); + const classDescription = extractClassDescription(classDefNode); + const inputInformation = extractInputInformation(classDefNode); + const outputInformation = extractOutputInformation(classDefNode); + const parameterInformation = extractParameterInformation(classDefNode); - if (!isClassName) { - logger.debug('Target node is not the class name identifier.'); - return ''; // The targetNode is not the class name identifier. - } + return `${classDescription} ${parameterInformation} ${inputInformation} ${outputInformation}`; +} - // Extract the description_string if it exists. +function extractClassDescription(classDefNode: SyntaxNode): string { const descriptionNode = TreeSitterUtil.findFirst(classDefNode, n => n.type === 'description_string'); const descriptionText = descriptionNode ? descriptionNode.firstChild?.text : null; - return descriptionText ? `\n${descriptionText}` : '\nNo description available.'; + return descriptionText ? `\n${descriptionText}` : ''; } -function extractInputs(classDefNode: SyntaxNode, targetNode: SyntaxNode): string { - // Placeholder for future extensions - return ''; -} +function extractInputInformation(classDefNode: SyntaxNode): string { + let inputsInfo: string[] = []; -/* -function extractHoverInformation(ast: Parser.SyntaxNode, position: LSP.Position): string { - let hoverMarkdown = ""; + TreeSitterUtil.forEach(classDefNode, (node) => { - // Extract description - const description = getDescription(ast, position); - if (description) { - hoverMarkdown += `**Description:** ${description}\n\n`; - } + if (node.type === 'component_clause' && node.text.includes('input')) { - // Placeholder for future extensions - // const inputs = getInputs(ast, position); - // const outputs = getOutputs(ast, position); - // const parameters = getParameters(ast, position); + const typeSpecifierNode = node.childForFieldName('typeSpecifier'); + logger.debug(`Type specifier node: ${typeSpecifierNode}`); + const typeSpecifier = typeSpecifierNode ? typeSpecifierNode.text : "Unknown Type"; - // Add to Markdown as these features are implemented - // if (inputs) { hoverMarkdown += `**Inputs:** ${inputs}\n\n`; } - // if (outputs) { hoverMarkdown += `**Outputs:** ${outputs}\n\n`; } - // if (parameters) { hoverMarkdown += `**Parameters:** ${parameters}\n\n`; } + const componentDeclarationNode = node.childForFieldName('componentDeclarations'); - return hoverMarkdown; -} + const declarationNode = componentDeclarationNode?.firstChild?.childForFieldName('declaration'); + logger.debug(`Declaration node: ${declarationNode}`); + const identifier = declarationNode ? declarationNode.text : "Unknown Identifier"; + + // Extracting description from description_string node + const descriptionNode = componentDeclarationNode?.firstChild?.childForFieldName('descriptionString'); + const description = descriptionNode ? descriptionNode.text : ''; -export function extractHoverInformation(classDefNode: SyntaxNode): string { - // Initialize Markdown content with the class description - let markdownContent = `**Description:** ${getClassDescription(classDefNode)}\n\n`; - - // Variables to store inputs, outputs, and parameters - let inputs: { identifier: string, description: string }[] = []; - let outputs: { identifier: string, description: string }[] = []; - let parameters: { identifier: string, description: string }[] = []; - - // Traverse the class definition's children to find relevant nodes - TreeSitterUtil.forEach(classDefNode, (node) => { - if (node.type === 'component_clause') { - const identifier = TreeSitterUtil.getIdentifier(node); - const description = getComponentDescription(node); - - if(!identifier) return false; // Skip if no identifier is found (e.g. for unnamed components - - if (node.text.includes('input')) { - inputs.push({ identifier, description: description ?? '' }); - } else if (node.text.includes('output')) { - outputs.push({ identifier, description: description ?? '' }); - } else if (node.text.includes('parameter')) { - parameters.push({ identifier, description: description ?? '' }); - } + inputsInfo.push(`${typeSpecifier} ${identifier} ${description}\n`); } - return true; // Continue traversal - }); + return true; + }); - // Format and add inputs, outputs, and parameters to Markdown content - if (inputs.length > 0) { - markdownContent += formatComponentSection("Inputs", inputs); - } - if (outputs.length > 0) { - markdownContent += formatComponentSection("Outputs", outputs); - } - if (parameters.length > 0) { - markdownContent += formatComponentSection("Parameters", parameters); + if (inputsInfo.length > 0) { + return "\n## Inputs:\n" + inputsInfo.join('\n'); } - return markdownContent; + return '' } -// Helper function to format sections for inputs, outputs, and parameters -function formatComponentSection(title: string, components: Array<{identifier: string, description: string}>): string { - let section = `**${title}:**\n`; - components.forEach(comp => { - section += `- ${comp.identifier}: ${comp.description}\n`; - }); - return section + "\n"; // Add extra newline for spacing -} +function extractOutputInformation(classDefNode: SyntaxNode): string { + let outputsInfo: string[] = []; -// Assume getClassDescription and getComponentDescription are implemented to extract descriptions + TreeSitterUtil.forEach(classDefNode, (node) => { -/** - * Extracts the class description from a class_definition node. - * - * @param classDefNode The class definition syntax node. - * @returns The description text if available, otherwise a default message. - * -function getClassDescription(classDefNode: SyntaxNode): string { - // Find the description_string node directly under the class definition - const descriptionNode = classDefNode.namedChildren.find(child => child.type === 'description_string'); - - // If a description_string node is found, return its text content - if (descriptionNode && descriptionNode.firstChild) { - return descriptionNode.firstChild.text.trim(); - } + if (node.type === 'component_clause' && node.text.includes('output')) { - // Default message if no description is available - return "No class description available."; -} + const typeSpecifierNode = node.childForFieldName('typeSpecifier'); + logger.debug(`Type specifier node: ${typeSpecifierNode}`); + const typeSpecifier = typeSpecifierNode ? typeSpecifierNode.text : "Unknown Type"; -/** - * Extracts the description for a component (input, output, parameter) from its node. - * - * @param componentNode The syntax node for the component. - * @returns The description text if available, otherwise a default message. - * -function getComponentDescription(componentNode: SyntaxNode): string { - // Assuming descriptions are in comments directly above the component declaration - let description = "No description available."; // Default message - - // Attempt to find a comment node immediately preceding the componentNode - let precedingNode = componentNode.previousSibling; - while (precedingNode) { - if (precedingNode.type === 'description_string') { - // If a comment is found, use its text as the description - description = precedingNode.text.trim(); - break; + const componentDeclarationNode = node.childForFieldName('componentDeclarations'); + + const declarationNode = componentDeclarationNode?.firstChild?.childForFieldName('declaration'); + logger.debug(`Declaration node: ${declarationNode}`); + const identifier = declarationNode ? declarationNode.text : "Unknown Identifier"; + + // Extracting description from description_string node + const descriptionNode = componentDeclarationNode?.firstChild?.childForFieldName('descriptionString'); + const description = descriptionNode ? descriptionNode.text : ''; + + outputsInfo.push(`${typeSpecifier} ${identifier} ${description}\n`); } - precedingNode = precedingNode.previousSibling; + return true; + }); + + if (outputsInfo.length > 0) { + return "\n## Outputs:\n" + outputsInfo.join('\n'); } + return ''; +} + +function extractParameterInformation(classDefNode: SyntaxNode): string { + let parametersInfo: string[] = []; + + TreeSitterUtil.forEach(classDefNode, (node) => { + + if (node.type === 'component_clause' && node.text.includes('parameter')) { + + const typeSpecifierNode = node.childForFieldName('typeSpecifier'); + logger.debug(`Type specifier node: ${typeSpecifierNode}`); + const typeSpecifier = typeSpecifierNode ? typeSpecifierNode.text : "Unknown Type"; + + const componentDeclarationNode = node.childForFieldName('componentDeclarations'); + + const declarationNode = componentDeclarationNode?.firstChild?.childForFieldName('declaration'); + logger.debug(`Declaration node: ${declarationNode}`); + const identifier = declarationNode ? declarationNode.text : "Unknown Identifier"; + + // Extracting description from description_string node + const descriptionNode = componentDeclarationNode?.firstChild?.childForFieldName('descriptionString'); + const description = descriptionNode ? descriptionNode.text : ''; + + parametersInfo.push(`${typeSpecifier} ${identifier} ${description}\n`); + }; + return true; + }); - return description; -}*/ + if (parametersInfo.length > 0) { + return "\n## Parameters:\n" + parametersInfo.join('\n'); + } + return ''; +} From 6dc77cc49dc15eb0994f41b81331226e8ce0737c Mon Sep 17 00:00:00 2001 From: osman362 <136453917+osman362@users.noreply.github.com> Date: Fri, 1 Mar 2024 15:12:33 +0100 Subject: [PATCH 06/12] Refactor Code --- server/src/server.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index fa92b89..c097a0e 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -162,7 +162,6 @@ export class ModelicaServer { ) } -// getDocumentationForSymbol aus dem Bash LSP private getCommentForSymbol({ currentUri, symbol, @@ -190,10 +189,6 @@ export class ModelicaServer { return `\n${commentAbove}` } - private documentationForHover(hoverInfo: string): LSP.MarkupContent | null { - return null - } - // ============================== // Language server event handlers // ============================== @@ -232,19 +227,6 @@ export class ModelicaServer { }) logger.debug('symbolsMatchingWord: ', symbolsMatchingWord); - const commentAboveDocumentation = deduplicateSymbols({ - symbols: symbolsMatchingWord, - currentUri, - })/* - // do not return hover referencing for the current line - .filter( - (symbol) => - symbol.location.uri !== currentUri || - symbol.location.range.start.line !== params.position.line, - )*/ - .map((symbol: LSP.SymbolInformation) => - this.getCommentForSymbol({ currentUri, symbol }), - ) const hoverInfo = this.analyzer.hoverInformations(currentUri, params.position) if (hoverInfo) { From ea2bc8ca8c53e1aeab2a7e7e12b33dd70cb6af84 Mon Sep 17 00:00:00 2001 From: AnHeuermann <38031952+AnHeuermann@users.noreply.github.com> Date: Thu, 7 Mar 2024 17:58:03 +0100 Subject: [PATCH 07/12] Adding E2E test for onHover, fixing issues - E2E test for onHover added. - Output not yet where we want it to be - Fixing compilation error in declarations.test.ts - Fixing lint errors --- client/src/test/onHover.test.ts | 110 ++++++++++++++++++ client/testFixture/MyLibrary/Examples/M.mo | 7 ++ .../testFixture/MyLibrary/Examples/package.mo | 4 + .../MyLibrary/Examples/package.order | 1 + client/testFixture/MyLibrary/package.mo | 2 + client/testFixture/MyLibrary/package.order | 1 + client/{src/test => testFixture}/output.md | 0 client/testFixture/step.mo | 4 +- server/src/analyzer.ts | 80 ++++++------- server/src/server.ts | 52 ++++----- server/src/util/array.ts | 20 ++-- server/src/util/declarations.ts | 18 +-- server/src/util/hoverUtil.ts | 24 ++-- server/src/util/test/declarations.test.ts | 2 +- 14 files changed, 225 insertions(+), 100 deletions(-) create mode 100644 client/src/test/onHover.test.ts create mode 100644 client/testFixture/MyLibrary/Examples/M.mo create mode 100644 client/testFixture/MyLibrary/Examples/package.mo create mode 100644 client/testFixture/MyLibrary/Examples/package.order create mode 100644 client/testFixture/MyLibrary/package.mo create mode 100644 client/testFixture/MyLibrary/package.order rename client/{src/test => testFixture}/output.md (100%) diff --git a/client/src/test/onHover.test.ts b/client/src/test/onHover.test.ts new file mode 100644 index 0000000..a4eed42 --- /dev/null +++ b/client/src/test/onHover.test.ts @@ -0,0 +1,110 @@ +/* + * 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 fs from 'fs'; +import * as vscode from 'vscode'; +import * as assert from 'assert'; +import { getDocUri, getDocPath, activate } from './helper'; + +suite('onHover information', async () => { + const docUri = getDocUri('step.mo'); + const position = new vscode.Position(19, 25); + const content = new vscode.MarkdownString( + fs.readFileSync(getDocPath('output.md'), 'utf-8')); + const expectedHoverInstances: vscode.Hover[] = [ + new vscode.Hover(content) + ]; + + test('onHover()', async () => { + await testOnHover(docUri, position, expectedHoverInstances); + }); +}); + +async function testOnHover( + uri: vscode.Uri, + position: vscode.Position, + expectedHoverInstances: vscode.Hover[] +) { + await activate(uri); + + // Execute `vscode.executeHoverProvider` to execute all hover providers + const actualHoverInstances = await vscode.commands.executeCommand("vscode.executeHoverProvider", uri, position); + + assertHoverInstancesEqual(expectedHoverInstances, actualHoverInstances); +} + +// Function to print the Hover class +function printHoverInstances( + hover: vscode.Hover +) { + for (const content of hover.contents) { + if (content instanceof vscode.MarkdownString) { + printMarkdownString(content); + } + } +} + +function printMarkdownString( + markdownString: vscode.MarkdownString +) { + console.log(markdownString.value); +} + +function assertHoverInstancesEqual(expected: vscode.Hover[], actual: vscode.Hover[]) { + assert.strictEqual(expected.length, actual.length, 'Array lengths do not match.'); + + for (let i = 0; i < expected.length; i++) { + const expectedHover = expected[i]; + const actualHover = actual[i]; + + let expectedContent = ""; + for (let j = 0; j < expectedHover.contents.length; j++) { + const content = expectedHover.contents[j]; + if (content instanceof vscode.MarkdownString) { + expectedContent += content.value; + } + } + + let actualContent = ""; + for (let j = 0; j < actualHover.contents.length; j++) { + const content = actualHover.contents[j]; + if (content instanceof vscode.MarkdownString) { + actualContent += content.value; + } + } + + assert.strictEqual(actualContent, expectedContent, `Content does not match expected content.`); + } +} diff --git a/client/testFixture/MyLibrary/Examples/M.mo b/client/testFixture/MyLibrary/Examples/M.mo new file mode 100644 index 0000000..bc51403 --- /dev/null +++ b/client/testFixture/MyLibrary/Examples/M.mo @@ -0,0 +1,7 @@ +within MyLibrary.Examples; + +model M "MWE Modelica Model" + Real x(start = 1.0, fixed = true); +equation + der(x) = -0.5*x; +end M; diff --git a/client/testFixture/MyLibrary/Examples/package.mo b/client/testFixture/MyLibrary/Examples/package.mo new file mode 100644 index 0000000..e938914 --- /dev/null +++ b/client/testFixture/MyLibrary/Examples/package.mo @@ -0,0 +1,4 @@ +within MyLibrary; + +package Examples +end Examples; diff --git a/client/testFixture/MyLibrary/Examples/package.order b/client/testFixture/MyLibrary/Examples/package.order new file mode 100644 index 0000000..ab77689 --- /dev/null +++ b/client/testFixture/MyLibrary/Examples/package.order @@ -0,0 +1 @@ +M diff --git a/client/testFixture/MyLibrary/package.mo b/client/testFixture/MyLibrary/package.mo new file mode 100644 index 0000000..7b8ef37 --- /dev/null +++ b/client/testFixture/MyLibrary/package.mo @@ -0,0 +1,2 @@ +package MyLibrary "My Modelica Library" +end MyLibrary; diff --git a/client/testFixture/MyLibrary/package.order b/client/testFixture/MyLibrary/package.order new file mode 100644 index 0000000..ad6b7fb --- /dev/null +++ b/client/testFixture/MyLibrary/package.order @@ -0,0 +1 @@ +Examples diff --git a/client/src/test/output.md b/client/testFixture/output.md similarity index 100% rename from client/src/test/output.md rename to client/testFixture/output.md diff --git a/client/testFixture/step.mo b/client/testFixture/step.mo index ded7120..e6988ad 100644 --- a/client/testFixture/step.mo +++ b/client/testFixture/step.mo @@ -1,4 +1,4 @@ -class Modelica.Blocks.Sources.Step "Generate step signal of type Real" +class Modelica_Blocks_Sources_Step "Generate step signal of type Real" parameter Real height = 1.0 "Height of step"; output Real y "Connector of Real output signal"; parameter Real offset = 0.0 "Offset of output signal y"; @@ -17,4 +17,4 @@ The Real output y is a step signal:

")); -end Modelica.Blocks.Sources.Step; +end Modelica_Blocks_Sources_Step; diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index ff54dfa..4d9531d 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -65,7 +65,7 @@ export default class Analyzer { constructor(parser:Parser) { this.parser = parser; - } + } public analyze({document}: {document: TextDocument}): LSP.Diagnostic[] { logger.debug('analyze:'); @@ -123,16 +123,16 @@ export default class Analyzer { return this.getAllDeclarations({ uri, position }).filter((symbol) => { if (exactMatch) { logger.debug('name === word'); - return symbol.name === word + return symbol.name === word; } else { logger.debug('name.startsWith(word)'); - return symbol.name.startsWith(word) + return symbol.name.startsWith(word); } - }) + }); } private getAnalyzedReachableUris({ fromUri }: { fromUri?: string } = {}): string[] { - return this.ensureUrisAreAnalyzed(this.getReachableUris({ fromUri })) + return this.ensureUrisAreAnalyzed(this.getReachableUris({ fromUri })); } private ensureUrisAreAnalyzed(uris: string[]): string[] { @@ -141,26 +141,26 @@ export default class Analyzer { // Either the background analysis didn't run or the file is outside // the workspace. Let us try to analyze the file. try { - logger.debug(`Analyzing file not covered by background analysis ${uri}`) - const fileContent = fs.readFileSync(new URL(uri), 'utf8') + logger.debug(`Analyzing file not covered by background analysis ${uri}`); + const fileContent = fs.readFileSync(new URL(uri), 'utf8'); this.analyze({ document: TextDocument.create(uri, 'modelica', 1, fileContent), - }) + }); } catch (err) { - logger.warn(`Error while analyzing file ${uri}: ${err}`) - return false + logger.warn(`Error while analyzing file ${uri}: ${err}`); + return false; } } - return true - }) + return true; + }); } private getReachableUris({ fromUri }: { fromUri?: string } = {}): string[] { if (!fromUri) { - return Object.keys(this.uriToAnalyzedDocument) + return Object.keys(this.uriToAnalyzedDocument); } - return [fromUri] + return [fromUri]; } private getAllDeclarations({ @@ -169,7 +169,7 @@ export default class Analyzer { }: { uri?: string; position?: LSP.Position } = {}): LSP.SymbolInformation[] { return this.getAnalyzedReachableUris({ fromUri }).reduce((symbols, uri) => { logger.debug('getAnalyzedReachableUris Initialized'); - const analyzedDocument = this.uriToAnalyzedDocument[uri] + const analyzedDocument = this.uriToAnalyzedDocument[uri]; if (analyzedDocument) { if (uri !== fromUri || !position) { @@ -181,43 +181,43 @@ export default class Analyzer { const node = analyzedDocument.tree.rootNode?.descendantForPosition({ row: position.line, column: position.character, - }) + }); const localDeclarations = getLocalDeclarations({ node, rootNode: analyzedDocument.tree.rootNode, uri, - }) + }); logger.debug('localDeclarations: ', localDeclarations); Object.keys(localDeclarations).map((name) => { - const symbolsMatchingWord = localDeclarations[name] + const symbolsMatchingWord = localDeclarations[name]; // Find the latest definition - let closestSymbol: LSP.SymbolInformation | null = null + let closestSymbol: LSP.SymbolInformation | null = null; symbolsMatchingWord.forEach((symbol) => { // Skip if the symbol is defined in the current file after the requested position if (symbol.location.range.start.line > position.line) { - return + return; } if ( closestSymbol === null || symbol.location.range.start.line > closestSymbol.location.range.start.line ) { - closestSymbol = symbol + closestSymbol = symbol; } - }) + }); if (closestSymbol) { logger.debug('ClosestSymbol: ', closestSymbol); - symbols.push(closestSymbol) + symbols.push(closestSymbol); } - }) + }); } } - return symbols - }, [] as LSP.SymbolInformation[]) + return symbols; + }, [] as LSP.SymbolInformation[]); } /** @@ -228,19 +228,19 @@ export default class Analyzer { if (!doc) { return null; } - + let commentBlock = []; let inBlockComment = false; // start from the line above let commentBlockIndex = line - 1; - + while (commentBlockIndex >= 0) { let currentLineText = doc.getText({ start: { line: commentBlockIndex, character: 0 }, end: { line: commentBlockIndex + 1, character: 0 }, }).trim(); - + if (inBlockComment) { if (currentLineText.startsWith('/*')) { inBlockComment = false; @@ -268,15 +268,15 @@ export default class Analyzer { break; // Stop if the current line is not part of a comment } } - + commentBlockIndex -= 1; } - + if (commentBlock.length) { commentBlock = [...commentBlock.reverse()]; return commentBlock.join('\n\n'); } - + return null; } @@ -284,13 +284,13 @@ export default class Analyzer { * Find the full word at the given point. */ public wordAtPoint(uri: string, line: number, column: number): string | null { - const node = this.nodeAtPoint(uri, line, column) + const node = this.nodeAtPoint(uri, line, column); if (!node || node.childCount > 0 || node.text.trim() === '') { - return null + return null; } - return node.text.trim() + return node.text.trim(); } public wordAtPointFromTextPosition( @@ -300,7 +300,7 @@ export default class Analyzer { params.textDocument.uri, params.position.line, params.position.character, - ) + ); } /** @@ -311,14 +311,14 @@ export default class Analyzer { line: number, column: number, ): Parser.SyntaxNode | null { - const tree = this.uriToAnalyzedDocument[uri]?.tree + const tree = this.uriToAnalyzedDocument[uri]?.tree; if (!tree?.rootNode) { // Check for lacking rootNode (due to failed parse?) - return null + return null; } - return tree.rootNode.descendantForPosition({ row: line, column }) + return tree.rootNode.descendantForPosition({ row: line, column }); } public hoverInformations( @@ -330,6 +330,6 @@ export default class Analyzer { logger.debug('No target node found.'); return ''; } - return extractHoverInformation(targetNode) + return extractHoverInformation(targetNode); } } diff --git a/server/src/server.ts b/server/src/server.ts index c097a0e..f9a840f 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -103,8 +103,8 @@ export class ModelicaServer { /** * Register handlers for the events from the Language Server Protocol - * - * @param connection + * + * @param connection */ public register(connection: LSP.Connection): void { let currentDocument: TextDocument | null = null; @@ -113,7 +113,7 @@ export class ModelicaServer { // Make the text document manager listen on the connection // for open, change and close text document events this.documents.listen(this.connection); - + // 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 }) => { @@ -156,10 +156,10 @@ export class ModelicaServer { params: LSP.ReferenceParams | LSP.TextDocumentPositionParams word?: string | null }) { - const wordLog = word ? `"${word}"` : 'null' + const wordLog = word ? `"${word}"` : 'null'; logger.debug( `${request} ${params.position.line}:${params.position.character} word=${wordLog}`, - ) + ); } private getCommentForSymbol({ @@ -170,23 +170,23 @@ export class ModelicaServer { currentUri: string }): string { - logger.debug(`getDocumentationForSymbol: symbol=${symbol.name} uri=${symbol.location.uri}`) + logger.debug(`getDocumentationForSymbol: symbol=${symbol.name} uri=${symbol.location.uri}`); - const symbolUri = symbol.location.uri - const symbolStartLine = symbol.location.range.start.line + const symbolUri = symbol.location.uri; + const symbolStartLine = symbol.location.range.start.line; - const commentAboveSymbol = this.analyzer.commentsAbove(symbolUri, symbolStartLine) - const commentAbove = commentAboveSymbol ? `\n\n${commentAboveSymbol}` : '' - const hoverHeader = `${symbolKindToDescription(symbol.kind)}: **${symbol.name}**` + const commentAboveSymbol = this.analyzer.commentsAbove(symbolUri, symbolStartLine); + const commentAbove = commentAboveSymbol ? `\n\n${commentAboveSymbol}` : ''; + const hoverHeader = `${symbolKindToDescription(symbol.kind)}: **${symbol.name}**`; const symbolLocation = symbolUri !== currentUri ? `in ${path.relative(path.dirname(currentUri), symbolUri)}` - : `on line ${symbolStartLine + 1}` + : `on line ${symbolStartLine + 1}`; // TODO: An improvement could be to add show the symbol definition in the hover instead // of the defined location – similar to how VSCode works for languages like TypeScript. - return `\n${commentAbove}` + return `\n${commentAbove}`; } // ============================== @@ -210,13 +210,13 @@ export class ModelicaServer { private async onHover( params: LSP.TextDocumentPositionParams, ): Promise { - const word = this.analyzer.wordAtPointFromTextPosition(params) - const currentUri = params.textDocument.uri + const word = this.analyzer.wordAtPointFromTextPosition(params); + const currentUri = params.textDocument.uri; logger.debug('------------'); - this.logRequest({ request: 'onHover init', params, word }) + this.logRequest({ request: 'onHover init', params, word }); if (!word) { - return null + return null; } const symbolsMatchingWord = this.analyzer.findDeclarationsMatchingWord({ @@ -224,17 +224,17 @@ export class ModelicaServer { uri: currentUri, word, position: params.position, - }) + }); logger.debug('symbolsMatchingWord: ', symbolsMatchingWord); - const hoverInfo = this.analyzer.hoverInformations(currentUri, params.position) + const hoverInfo = this.analyzer.hoverInformations(currentUri, params.position); if (hoverInfo) { logger.debug('Documentation: ', hoverInfo); return { contents: getMarkdownContent(hoverInfo) }; } - return null + return null; } } @@ -249,11 +249,11 @@ function deduplicateSymbols({ currentUri: string }) { const isCurrentFile = ({ location: { uri } }: LSP.SymbolInformation) => - uri === currentUri + uri === currentUri; - const getSymbolId = ({ name, kind }: LSP.SymbolInformation) => `${name}${kind}` + const getSymbolId = ({ name, kind }: LSP.SymbolInformation) => `${name}${kind}`; - const symbolsCurrentFile = symbols.filter((s) => isCurrentFile(s)) + const symbolsCurrentFile = symbols.filter((s) => isCurrentFile(s)); const symbolsOtherFiles = symbols .filter((s) => !isCurrentFile(s)) @@ -264,10 +264,10 @@ function deduplicateSymbols({ (symbolCurrentFile) => getSymbolId(symbolCurrentFile) === getSymbolId(symbolOtherFiles), ), - ) + ); // NOTE: it might be that uniqueBasedOnHash is not needed anymore - return uniqueBasedOnHash([...symbolsCurrentFile, ...symbolsOtherFiles], getSymbolId) + return uniqueBasedOnHash([...symbolsCurrentFile, ...symbolsOtherFiles], getSymbolId); } function symbolKindToDescription(kind: LSP.SymbolKind): string { @@ -292,7 +292,7 @@ function getMarkdownContent(documentation: string, language?: string): LSP.Marku ['``` ' + language, documentation, '```'].join('\n') : documentation, kind: LSP.MarkupKind.Markdown, - } + }; } // Create a connection for the server, using Node's IPC as a transport. diff --git a/server/src/util/array.ts b/server/src/util/array.ts index 292ce32..7e32d17 100644 --- a/server/src/util/array.ts +++ b/server/src/util/array.ts @@ -2,7 +2,7 @@ * Flatten a 2-dimensional array into a 1-dimensional one. */ export function flattenArray(nestedArray: T[][]): T[] { - return nestedArray.reduce((acc, array) => [...acc, ...array], []) + return nestedArray.reduce((acc, array) => [...acc, ...array], []); } /** @@ -10,7 +10,7 @@ export function flattenArray(nestedArray: T[][]): T[] { * Doesn't preserve ordering. */ export function uniq
(a: A[]): A[] { - return Array.from(new Set(a)) + return Array.from(new Set(a)); } /** @@ -22,17 +22,17 @@ export function uniqueBasedOnHash>( elementToHash: (a: A) => string, __result: A[] = [], ): A[] { - const result: typeof list = [] - const hashSet = new Set() + const result: typeof list = []; + const hashSet = new Set(); list.forEach((element) => { - const hash = elementToHash(element) + const hash = elementToHash(element); if (hashSet.has(hash)) { - return + return; } - hashSet.add(hash) - result.push(element) - }) + hashSet.add(hash); + result.push(element); + }); - return result + return result; } diff --git a/server/src/util/declarations.ts b/server/src/util/declarations.ts index 7ca414d..4f1a3a1 100644 --- a/server/src/util/declarations.ts +++ b/server/src/util/declarations.ts @@ -63,19 +63,19 @@ export function getLocalDeclarations({ rootNode: Parser.SyntaxNode uri: string }): Declarations { - const declarations: Declarations = {} + const declarations: Declarations = {}; // Bottom up traversal to capture all local and scoped declarations const walk = (node: Parser.SyntaxNode | null) => { if (node) { for (const childNode of node.children) { - let symbol: LSP.SymbolInformation | null = null + let symbol: LSP.SymbolInformation | null = null; // local variables if (childNode.type === 'component_reference') { const identifierNode = childNode.children.filter( (child) => child.type === 'IDENT', - )[0] + )[0]; if (identifierNode) { symbol = nodeToSymbolInformation({node:identifierNode, uri}); } @@ -85,18 +85,18 @@ export function getLocalDeclarations({ if (symbol) { if (!declarations[symbol.name]) { - declarations[symbol.name] = [] + declarations[symbol.name] = []; } - declarations[symbol.name].push(symbol) + declarations[symbol.name].push(symbol); } } - walk(node.parent) + walk(node.parent); } - } - walk(node) + }; + walk(node); - return declarations + return declarations; } /** diff --git a/server/src/util/hoverUtil.ts b/server/src/util/hoverUtil.ts index 0573596..83a0708 100644 --- a/server/src/util/hoverUtil.ts +++ b/server/src/util/hoverUtil.ts @@ -5,7 +5,7 @@ import { logger } from './logger'; /** * Extracts hover information for a Modelica class or package. - * + * * @param rootNode The root node of the AST for the current document. * @param position The position of the cursor in the document. * @returns Text of the description_string or string saying there is no description. @@ -21,9 +21,9 @@ export function extractHoverInformation(targetNode: SyntaxNode): string { } // Check if the targetNode is the first IDENT child of the class_definition, indicating it's the class name. - const isClassName = classDefNode.namedChildren.some((child, index) => - child.type === 'long_class_specifier' && - child.firstChild?.type === 'IDENT' && + const isClassName = classDefNode.namedChildren.some((child, index) => + child.type === 'long_class_specifier' && + child.firstChild?.type === 'IDENT' && child.firstChild?.text === targetNode.text); if (!isClassName) { @@ -47,7 +47,7 @@ function extractClassDescription(classDefNode: SyntaxNode): string { } function extractInputInformation(classDefNode: SyntaxNode): string { - let inputsInfo: string[] = []; + const inputsInfo: string[] = []; TreeSitterUtil.forEach(classDefNode, (node) => { @@ -76,11 +76,11 @@ function extractInputInformation(classDefNode: SyntaxNode): string { return "\n## Inputs:\n" + inputsInfo.join('\n'); } - return '' + return ''; } function extractOutputInformation(classDefNode: SyntaxNode): string { - let outputsInfo: string[] = []; + const outputsInfo: string[] = []; TreeSitterUtil.forEach(classDefNode, (node) => { @@ -112,7 +112,7 @@ function extractOutputInformation(classDefNode: SyntaxNode): string { } function extractParameterInformation(classDefNode: SyntaxNode): string { - let parametersInfo: string[] = []; + const parametersInfo: string[] = []; TreeSitterUtil.forEach(classDefNode, (node) => { @@ -121,19 +121,19 @@ function extractParameterInformation(classDefNode: SyntaxNode): string { const typeSpecifierNode = node.childForFieldName('typeSpecifier'); logger.debug(`Type specifier node: ${typeSpecifierNode}`); const typeSpecifier = typeSpecifierNode ? typeSpecifierNode.text : "Unknown Type"; - + const componentDeclarationNode = node.childForFieldName('componentDeclarations'); const declarationNode = componentDeclarationNode?.firstChild?.childForFieldName('declaration'); logger.debug(`Declaration node: ${declarationNode}`); const identifier = declarationNode ? declarationNode.text : "Unknown Identifier"; - + // Extracting description from description_string node const descriptionNode = componentDeclarationNode?.firstChild?.childForFieldName('descriptionString'); const description = descriptionNode ? descriptionNode.text : ''; - + parametersInfo.push(`${typeSpecifier} ${identifier} ${description}\n`); - }; + } return true; }); diff --git a/server/src/util/test/declarations.test.ts b/server/src/util/test/declarations.test.ts index 9e01d5b..90b60f7 100644 --- a/server/src/util/test/declarations.test.ts +++ b/server/src/util/test/declarations.test.ts @@ -58,7 +58,7 @@ describe('nodeToSymbolInformation', () => { const tree = parser.parse("type Temperature = Real(unit = \"K \");"); const classNode = tree.rootNode.childForFieldName('storedDefinitions')!.childForFieldName('classDefinition')!; - const symbol = nodeToSymbolInformation(classNode, "file.mo"); + const symbol = nodeToSymbolInformation({node: classNode, uri: "file.mo"}); assert.equal(symbol?.name, 'Temperature'); assert.equal(symbol?.kind, LSP.SymbolKind.TypeParameter); From 739d885ddadda52a364a61bb117dc89f311f0d02 Mon Sep 17 00:00:00 2001 From: AnHeuermann <38031952+AnHeuermann@users.noreply.github.com> Date: Fri, 8 Mar 2024 11:34:42 +0100 Subject: [PATCH 08/12] Only search for identifiers in onHover --- server/src/analyzer.ts | 73 +++++++++++++++++++----------------- server/src/server.ts | 54 ++++++++++---------------- server/src/util/hoverUtil.ts | 1 - 3 files changed, 58 insertions(+), 70 deletions(-) diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 4d9531d..3f7dfdf 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -67,7 +67,7 @@ export default class Analyzer { this.parser = parser; } - public analyze({document}: {document: TextDocument}): LSP.Diagnostic[] { + public analyze(document: TextDocument): LSP.Diagnostic[] { logger.debug('analyze:'); const diagnostics: LSP.Diagnostic[] = []; @@ -106,28 +106,21 @@ export default class Analyzer { } /** - * Find declarations for the given word and position. + * Find all declarations for given identifier reachable from position. */ - public findDeclarationsMatchingWord({ - exactMatch, - position, + public findDeclarationsMatchingIdent({ uri, - word, + position, + identifier }: { - exactMatch: boolean - position: LSP.Position uri: string - word: string + position: LSP.Position + identifier: string }): LSP.SymbolInformation[] { logger.debug('Finding Declarations Matching Word...'); return this.getAllDeclarations({ uri, position }).filter((symbol) => { - if (exactMatch) { - logger.debug('name === word'); - return symbol.name === word; - } else { - logger.debug('name.startsWith(word)'); - return symbol.name.startsWith(word); - } + logger.debug('name === word'); + return symbol.name === identifier; }); } @@ -143,9 +136,7 @@ export default class Analyzer { try { logger.debug(`Analyzing file not covered by background analysis ${uri}`); const fileContent = fs.readFileSync(new URL(uri), 'utf8'); - this.analyze({ - document: TextDocument.create(uri, 'modelica', 1, fileContent), - }); + this.analyze(TextDocument.create(uri, 'modelica', 1, fileContent)); } catch (err) { logger.warn(`Error while analyzing file ${uri}: ${err}`); return false; @@ -281,30 +272,42 @@ export default class Analyzer { } /** - * Find the full word at the given point. - */ - public wordAtPoint(uri: string, line: number, column: number): string | null { - const node = this.nodeAtPoint(uri, line, column); + * Return identifier at given text position. + * + * Checks if a node of type identifier exists at given position and return + * text. + * + * @param params Text document position. + * @returns String with identifier or null on failure. + */ + public identFromTextPosition( + params: LSP.TextDocumentPositionParams, + ): string | null { + + const node = this.nodeAtPoint( + params.textDocument.uri, + params.position.line, + params.position.character); if (!node || node.childCount > 0 || node.text.trim() === '') { return null; } - return node.text.trim(); - } + // Filter for identifier + if (node.type !== "IDENT") { + return null; + } - public wordAtPointFromTextPosition( - params: LSP.TextDocumentPositionParams, - ): string | null { - return this.wordAtPoint( - params.textDocument.uri, - params.position.line, - params.position.character, - ); + return node.text.trim(); } /** - * Find the node at the given point. + * Return abstract syntax tree node representing text position. + * + * @param uri + * @param line + * @param column + * @returns Node matching position. */ private nodeAtPoint( uri: string, @@ -321,7 +324,7 @@ export default class Analyzer { return tree.rootNode.descendantForPosition({ row: line, column }); } - public hoverInformations( + public hoverInformation( uri: string, position: LSP.Position ): string { diff --git a/server/src/server.ts b/server/src/server.ts index f9a840f..f5958fb 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -130,7 +130,6 @@ export class ModelicaServer { connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); connection.onHover(this.onHover.bind(this)); - logger.debug('Event Handlers Registered'); connection.onInitialized(async () => { initialized = true; @@ -142,24 +141,8 @@ export class ModelicaServer { }); } - private async analyzeDocument(document: TextDocument) { - const diagnostics = this.analyzer.analyze({document}); - } - - private logRequest({ - request, - params, - word, - }: { - request: string - params: LSP.ReferenceParams | LSP.TextDocumentPositionParams - word?: string | null - }) { - const wordLog = word ? `"${word}"` : 'null'; - logger.debug( - `${request} ${params.position.line}:${params.position.character} word=${wordLog}`, - ); + const diagnostics = this.analyzer.analyze(document); } private getCommentForSymbol({ @@ -196,38 +179,41 @@ export class ModelicaServer { /** * Provide symbols defined in document. * - * @param params Unused. - * @returns Symbol information. + * @param symbolParams Document symbols of given text document. + * @returns Symbol information. */ - private onDocumentSymbol(params: LSP.DocumentSymbolParams): LSP.SymbolInformation[] { + private onDocumentSymbol(symbolParams: 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); + return this.analyzer.getDeclarationsForUri(symbolParams.textDocument.uri); } + /** + * Provide hover information at given text document position. + * + * @param position Text document position. + * @returns Hover information. + */ private async onHover( - params: LSP.TextDocumentPositionParams, + position: LSP.TextDocumentPositionParams ): Promise { - const word = this.analyzer.wordAtPointFromTextPosition(params); - const currentUri = params.textDocument.uri; - logger.debug('------------'); - this.logRequest({ request: 'onHover init', params, word }); + logger.debug('onHover'); - if (!word) { + const identifier = this.analyzer.identFromTextPosition(position); + if (identifier === null) { return null; } - const symbolsMatchingWord = this.analyzer.findDeclarationsMatchingWord({ - exactMatch: true, - uri: currentUri, - word, - position: params.position, + const symbolsMatchingWord = this.analyzer.findDeclarationsMatchingIdent({ + uri: position.textDocument.uri, + position: position.position, + identifier, }); logger.debug('symbolsMatchingWord: ', symbolsMatchingWord); - const hoverInfo = this.analyzer.hoverInformations(currentUri, params.position); + const hoverInfo = this.analyzer.hoverInformation(position.textDocument.uri, position.position); if (hoverInfo) { logger.debug('Documentation: ', hoverInfo); diff --git a/server/src/util/hoverUtil.ts b/server/src/util/hoverUtil.ts index 83a0708..6b529ab 100644 --- a/server/src/util/hoverUtil.ts +++ b/server/src/util/hoverUtil.ts @@ -11,7 +11,6 @@ import { logger } from './logger'; * @returns Text of the description_string or string saying there is no description. */ export function extractHoverInformation(targetNode: SyntaxNode): string { - // Find the parent class_definition node. const classDefNode = TreeSitterUtil.findParent(targetNode, n => n.type === 'class_definition'); From e7df72ff090b5c01efb173a892b4baf06cc657da Mon Sep 17 00:00:00 2001 From: AnHeuermann <38031952+AnHeuermann@users.noreply.github.com> Date: Fri, 8 Mar 2024 12:20:39 +0100 Subject: [PATCH 09/12] Simplify "getAllDeclarations" function --- server/src/analyzer.ts | 125 +++++++++-------------------------------- server/src/server.ts | 6 +- 2 files changed, 28 insertions(+), 103 deletions(-) diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 3f7dfdf..2897c4b 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -106,109 +106,38 @@ export default class Analyzer { } /** - * Find all declarations for given identifier reachable from position. + * Get all reachable definitions matching identifier. + * + * TODO: All available analyzed documents are searched. Filter for reachable + * files and use scope of identifier. + * + * @param uri Text document. + * @param position Position of `identifier` in text document. + * @param identifier Identifier name. + * @returns Array of symbol information for `identifier. */ - public findDeclarationsMatchingIdent({ - uri, - position, - identifier - }: { - uri: string - position: LSP.Position - identifier: string - }): LSP.SymbolInformation[] { - logger.debug('Finding Declarations Matching Word...'); - return this.getAllDeclarations({ uri, position }).filter((symbol) => { - logger.debug('name === word'); - return symbol.name === identifier; - }); - } - - private getAnalyzedReachableUris({ fromUri }: { fromUri?: string } = {}): string[] { - return this.ensureUrisAreAnalyzed(this.getReachableUris({ fromUri })); - } - - private ensureUrisAreAnalyzed(uris: string[]): string[] { - return uris.filter((uri) => { - if (!this.uriToAnalyzedDocument[uri]) { - // Either the background analysis didn't run or the file is outside - // the workspace. Let us try to analyze the file. - try { - logger.debug(`Analyzing file not covered by background analysis ${uri}`); - const fileContent = fs.readFileSync(new URL(uri), 'utf8'); - this.analyze(TextDocument.create(uri, 'modelica', 1, fileContent)); - } catch (err) { - logger.warn(`Error while analyzing file ${uri}: ${err}`); - return false; + public getReachableDefinitions( + uri: string, + position: LSP.Position, + identifier: string): LSP.SymbolInformation[] { + + const declarations:LSP.SymbolInformation[] = []; + + // Find all declarations matching identifier. + for (const availableUri of Object.keys(this.uriToAnalyzedDocument)) { + // TODO: Filter reachable uri, e.g. because of an inclue + const decl = this.uriToAnalyzedDocument[availableUri]?.declarations; + if (decl) { + for (const d of decl) { + if (d.name === identifier) { + declarations.push(d); + } } } - - return true; - }); - } - - private getReachableUris({ fromUri }: { fromUri?: string } = {}): string[] { - if (!fromUri) { - return Object.keys(this.uriToAnalyzedDocument); } - return [fromUri]; - } - - private getAllDeclarations({ - uri: fromUri, - position, - }: { uri?: string; position?: LSP.Position } = {}): LSP.SymbolInformation[] { - return this.getAnalyzedReachableUris({ fromUri }).reduce((symbols, uri) => { - logger.debug('getAnalyzedReachableUris Initialized'); - const analyzedDocument = this.uriToAnalyzedDocument[uri]; - - if (analyzedDocument) { - if (uri !== fromUri || !position) { - // TODO: Use the global declarations for external files or if we do not have a position - } - - // For the current file we find declarations based on the current scope - if (uri === fromUri && position) { - const node = analyzedDocument.tree.rootNode?.descendantForPosition({ - row: position.line, - column: position.character, - }); - - const localDeclarations = getLocalDeclarations({ - node, - rootNode: analyzedDocument.tree.rootNode, - uri, - }); - logger.debug('localDeclarations: ', localDeclarations); - Object.keys(localDeclarations).map((name) => { - const symbolsMatchingWord = localDeclarations[name]; - - // Find the latest definition - let closestSymbol: LSP.SymbolInformation | null = null; - symbolsMatchingWord.forEach((symbol) => { - // Skip if the symbol is defined in the current file after the requested position - if (symbol.location.range.start.line > position.line) { - return; - } - - if ( - closestSymbol === null || - symbol.location.range.start.line > closestSymbol.location.range.start.line - ) { - closestSymbol = symbol; - } - }); - - if (closestSymbol) { - logger.debug('ClosestSymbol: ', closestSymbol); - symbols.push(closestSymbol); - } - }); - } - } - return symbols; - }, [] as LSP.SymbolInformation[]); + // TODO: Filter reachable declarations from scope. + return declarations; } /** diff --git a/server/src/server.ts b/server/src/server.ts index f5958fb..3816619 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -206,11 +206,7 @@ export class ModelicaServer { return null; } - const symbolsMatchingWord = this.analyzer.findDeclarationsMatchingIdent({ - uri: position.textDocument.uri, - position: position.position, - identifier, - }); + const symbolsMatchingWord = this.analyzer.getReachableDefinitions(position.textDocument.uri, position.position, identifier); logger.debug('symbolsMatchingWord: ', symbolsMatchingWord); const hoverInfo = this.analyzer.hoverInformation(position.textDocument.uri, position.position); From 6dc22e7b0edc3dfd923aa61d6d45833aa3d3f896 Mon Sep 17 00:00:00 2001 From: AnHeuermann <38031952+AnHeuermann@users.noreply.github.com> Date: Fri, 8 Mar 2024 16:35:35 +0100 Subject: [PATCH 10/12] Improving hover content display --- client/src/test/onHover.test.ts | 19 +-- client/testFixture/output.md | 29 +++-- client/testFixture/step.mo | 2 +- server/src/analyzer.ts | 26 +--- server/src/server.ts | 44 ++++--- server/src/util/hoverUtil.ts | 224 +++++++++++++++++++++----------- server/src/util/tree-sitter.ts | 43 ++++++ 7 files changed, 236 insertions(+), 151 deletions(-) diff --git a/client/src/test/onHover.test.ts b/client/src/test/onHover.test.ts index a4eed42..653e68d 100644 --- a/client/src/test/onHover.test.ts +++ b/client/src/test/onHover.test.ts @@ -65,23 +65,6 @@ async function testOnHover( assertHoverInstancesEqual(expectedHoverInstances, actualHoverInstances); } -// Function to print the Hover class -function printHoverInstances( - hover: vscode.Hover -) { - for (const content of hover.contents) { - if (content instanceof vscode.MarkdownString) { - printMarkdownString(content); - } - } -} - -function printMarkdownString( - markdownString: vscode.MarkdownString -) { - console.log(markdownString.value); -} - function assertHoverInstancesEqual(expected: vscode.Hover[], actual: vscode.Hover[]) { assert.strictEqual(expected.length, actual.length, 'Array lengths do not match.'); @@ -105,6 +88,6 @@ function assertHoverInstancesEqual(expected: vscode.Hover[], actual: vscode.Hove } } - assert.strictEqual(actualContent, expectedContent, `Content does not match expected content.`); + assert.strictEqual(actualContent.trim(), expectedContent.trim(), `Content does not match expected content.`); } } diff --git a/client/testFixture/output.md b/client/testFixture/output.md index c6eaff0..90f3cb9 100644 --- a/client/testFixture/output.md +++ b/client/testFixture/output.md @@ -1,19 +1,20 @@ -# Modelica.Blocks.Sources.Step - -Generate step signal of type Real - -## Inputs - -## Outputs - ```modelica -Real y "Connector of Real output signal" +class Modelica_Blocks_Sources_Step "Generate step signal of type Real" ``` +--- -## Parameters +**Parameter Inputs** +```modelica +parameter input Real height = 1.0 "Height of step"; +``` +**Outputs** +```modelica +output Real y "Connector of Real output signal"; +``` +**Parameter** ```modelica -Real height = 1.0 "Height of step" -Real offset = 0.0 "Offset of output signal y" -Real startTime = 0.0 "Output y = offset for time < startTime" -``` \ No newline at end of file +parameter input Real height = 1.0 "Height of step"; +parameter Real offset = 0.0 "Offset of output signal y"; +parameter Real startTime(quantity = "Time", unit = "s") = 0.0 "Output y = offset for time < startTime"; +``` diff --git a/client/testFixture/step.mo b/client/testFixture/step.mo index e6988ad..04262d1 100644 --- a/client/testFixture/step.mo +++ b/client/testFixture/step.mo @@ -1,5 +1,5 @@ class Modelica_Blocks_Sources_Step "Generate step signal of type Real" - parameter Real height = 1.0 "Height of step"; + parameter input Real height = 1.0 "Height of step"; output Real y "Connector of Real output signal"; parameter Real offset = 0.0 "Offset of output signal y"; parameter Real startTime(quantity = "Time", unit = "s") = 0.0 "Output y = offset for time < startTime"; diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 2897c4b..7074ea4 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -50,7 +50,6 @@ import { } from './util/declarations'; import { logger } from './util/logger'; import { log } from 'console'; -import { extractHoverInformation } from './util/hoverUtil'; type AnalyzedDocument = { document: TextDocument, @@ -201,17 +200,16 @@ export default class Analyzer { } /** - * Return identifier at given text position. + * Return IDENT node from given text position. * - * Checks if a node of type identifier exists at given position and return - * text. + * Check if a node of type identifier exists at given position and return it. * * @param params Text document position. - * @returns String with identifier or null on failure. + * @returns Identifier syntax node. */ - public identFromTextPosition( + public NodeFromTextPosition( params: LSP.TextDocumentPositionParams, - ): string | null { + ): Parser.SyntaxNode | null { const node = this.nodeAtPoint( params.textDocument.uri, @@ -227,7 +225,7 @@ export default class Analyzer { return null; } - return node.text.trim(); + return node; } /** @@ -252,16 +250,4 @@ export default class Analyzer { return tree.rootNode.descendantForPosition({ row: line, column }); } - - public hoverInformation( - uri: string, - position: LSP.Position - ): string { - const targetNode = this.nodeAtPoint(uri, position.line, position.character); - if (!targetNode) { - logger.debug('No target node found.'); - return ''; - } - return extractHoverInformation(targetNode); - } } diff --git a/server/src/server.ts b/server/src/server.ts index 3816619..497a3ca 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -45,8 +45,9 @@ import { TextDocument} from 'vscode-languageserver-textdocument'; import { initializeParser } from './parser'; import Analyzer from './analyzer'; -import { logger, setLogConnection, setLogLevel } from './util/logger'; import { uniqueBasedOnHash } from './util/array'; +import { extractHoverInformation } from './util/hoverUtil'; +import { logger, setLogConnection, setLogLevel } from './util/logger'; /** * ModelicaServer collection all the important bits and bobs. @@ -201,22 +202,35 @@ export class ModelicaServer { ): Promise { logger.debug('onHover'); - const identifier = this.analyzer.identFromTextPosition(position); - if (identifier === null) { + const node = this.analyzer.NodeFromTextPosition(position); + if (node === null) { return null; } - const symbolsMatchingWord = this.analyzer.getReachableDefinitions(position.textDocument.uri, position.position, identifier); + const identifier = node.text.trim(); + const symbolsMatchingWord = this.analyzer.getReachableDefinitions( + position.textDocument.uri, + position.position, + identifier); logger.debug('symbolsMatchingWord: ', symbolsMatchingWord); + if (symbolsMatchingWord.length == 0) { + return null; + } - const hoverInfo = this.analyzer.hoverInformation(position.textDocument.uri, position.position); - - if (hoverInfo) { - logger.debug('Documentation: ', hoverInfo); - return { contents: getMarkdownContent(hoverInfo) }; + const hoverInfo = extractHoverInformation(node); + if (hoverInfo == null) { + return null; } + logger.debug(hoverInfo); - return null; + const markdown : LSP.MarkupContent = { + kind: LSP.MarkupKind.Markdown, + value: hoverInfo + }; + + return { + contents: markdown + } as LSP.Hover; } } @@ -267,16 +281,6 @@ function symbolKindToDescription(kind: LSP.SymbolKind): string { } } -function getMarkdownContent(documentation: string, language?: string): LSP.MarkupContent { - return { - value: language - ? // eslint-disable-next-line prefer-template - ['``` ' + language, documentation, '```'].join('\n') - : documentation, - kind: LSP.MarkupKind.Markdown, - }; -} - // Create a connection for the server, using Node's IPC as a transport. // Also include all preview / proposed LSP features. const connection = LSP.createConnection(LSP.ProposedFeatures.all); diff --git a/server/src/util/hoverUtil.ts b/server/src/util/hoverUtil.ts index 6b529ab..5f23987 100644 --- a/server/src/util/hoverUtil.ts +++ b/server/src/util/hoverUtil.ts @@ -4,110 +4,178 @@ import * as LSP from 'vscode-languageserver'; import { logger } from './logger'; /** - * Extracts hover information for a Modelica class or package. + * Extracts hover information for given node. * - * @param rootNode The root node of the AST for the current document. - * @param position The position of the cursor in the document. - * @returns Text of the description_string or string saying there is no description. + * Documentation and information for class description, inputs, outputs and + * parameters. + * + * @param node Syntax Node. + * + * @returns Hover content or null if no information available. */ -export function extractHoverInformation(targetNode: SyntaxNode): string { +export function extractHoverInformation(node: SyntaxNode): string | null { // Find the parent class_definition node. - const classDefNode = TreeSitterUtil.findParent(targetNode, n => n.type === 'class_definition'); - + const classDefNode = TreeSitterUtil.findParent(node, n => n.type === 'class_definition'); if (!classDefNode) { - logger.debug('No class definition found.'); - return ''; + logger.debug('extractHoverInformation: No class definition found.'); + return null; } - // Check if the targetNode is the first IDENT child of the class_definition, indicating it's the class name. - const isClassName = classDefNode.namedChildren.some((child, index) => - child.type === 'long_class_specifier' && - child.firstChild?.type === 'IDENT' && - child.firstChild?.text === targetNode.text); + // Check if node is the first IDENT child of the class_definition, indicating it's the class name. + const isClassName = classDefNode.namedChildren.some((child, _) => + child.type === 'long_class_specifier' && + child.firstChild?.type === 'IDENT' && + child.firstChild?.text === node.text); if (!isClassName) { - logger.debug('Target node is not the class name identifier.'); - return ''; + logger.debug('extractHoverInformation: Target node is not the class name identifier.'); + return null; } const classDescription = extractClassDescription(classDefNode); - const inputInformation = extractInputInformation(classDefNode); - const outputInformation = extractOutputInformation(classDefNode); - const parameterInformation = extractParameterInformation(classDefNode); - - return `${classDescription} ${parameterInformation} ${inputInformation} ${outputInformation}`; + const {inputsInfo, outputsInfo, parameterInfo, parameterInputsInfo, parameterOutputsInfo} = extractComponentInformation(classDefNode); + + return [ + '```modelica', + TreeSitterUtil.getClassPrefixes(classDefNode) + ' ' + node.text + ' ' + classDescription, + '```', + '---', + inputsInfo, + parameterInputsInfo, + outputsInfo, + parameterOutputsInfo, + parameterInfo + ].join('\n'); } -function extractClassDescription(classDefNode: SyntaxNode): string { - const descriptionNode = TreeSitterUtil.findFirst(classDefNode, n => n.type === 'description_string'); - const descriptionText = descriptionNode ? descriptionNode.firstChild?.text : null; +/** + * Extract description string from class node. + * + * @param node Syntax node. + * @returns Description string or undefined. + */ +function extractClassDescription(node: SyntaxNode): string | undefined { + const descriptionNode = TreeSitterUtil.findFirst( + node, + n => n.type === 'description_string'); - return descriptionText ? `\n${descriptionText}` : ''; + return descriptionNode?.firstChild?.text; } -function extractInputInformation(classDefNode: SyntaxNode): string { +function extractComponentInformation( + classDefNode: SyntaxNode): { + inputsInfo: string | undefined; + outputsInfo: string | undefined; + parameterInfo: string | undefined; + parameterInputsInfo: string | undefined; + parameterOutputsInfo: string | undefined; + } { + const inputsInfo: string[] = []; + const outputsInfo: string[] = []; + const parameterInfo: string[] = []; + const parameterInputsInfo: string[] = []; + const parameterOutputsInfo: string[] = []; + let inputsString: string | undefined = undefined; + let outputsString: string | undefined = undefined; + let parameterString: string | undefined = undefined; + let parameterInputsString: string | undefined = undefined; + let parameterOutputsString: string | undefined = undefined; TreeSitterUtil.forEach(classDefNode, (node) => { - - if (node.type === 'component_clause' && node.text.includes('input')) { - - const typeSpecifierNode = node.childForFieldName('typeSpecifier'); - logger.debug(`Type specifier node: ${typeSpecifierNode}`); - const typeSpecifier = typeSpecifierNode ? typeSpecifierNode.text : "Unknown Type"; - - const componentDeclarationNode = node.childForFieldName('componentDeclarations'); - - const declarationNode = componentDeclarationNode?.firstChild?.childForFieldName('declaration'); - logger.debug(`Declaration node: ${declarationNode}`); - const identifier = declarationNode ? declarationNode.text : "Unknown Identifier"; - - // Extracting description from description_string node - const descriptionNode = componentDeclarationNode?.firstChild?.childForFieldName('descriptionString'); - const description = descriptionNode ? descriptionNode.text : ''; - - inputsInfo.push(`${typeSpecifier} ${identifier} ${description}\n`); + if (node.type === 'component_clause') { + const prefix = TreeSitterUtil.getPrefix(node); + const isParameter = TreeSitterUtil.isParameter(node); + if (prefix !== undefined || isParameter) { + const typeSpecifierNode = node.childForFieldName('typeSpecifier'); + const typeSpecifier = typeSpecifierNode ? typeSpecifierNode.text : "Unknown Type"; + + const componentDeclarationNode = node.childForFieldName('componentDeclarations'); + const declarationNode = componentDeclarationNode?.firstChild?.childForFieldName('declaration'); + const identifier = declarationNode ? declarationNode.text : "Unknown Identifier"; + + // Extracting description from description_string node + const descriptionNode = componentDeclarationNode?.firstChild?.childForFieldName('descriptionString'); + const description = descriptionNode ? descriptionNode.text : ''; + + const info = [ + isParameter ? 'parameter' : undefined, + prefix, + typeSpecifier, + identifier, + description + ].filter( (e) => e !== undefined ).join(' ') + ';'; + + if (prefix === 'input') { + if (isParameter) { + parameterInputsInfo.push(info); + } else { + inputsInfo.push(info); + } + } + if (prefix === 'output') { + if (isParameter) { + parameterOutputsInfo.push(info); + } else { + outputsInfo.push(info); + } + } + if (isParameter) { + parameterInfo.push(info); + } } - return true; + } + return true; }); if (inputsInfo.length > 0) { - return "\n## Inputs:\n" + inputsInfo.join('\n'); + inputsString = [ + '**Inputs**', + '```modelica', + inputsInfo.join('\n'), + '```' + ].join('\n'); + } + if (parameterInputsInfo.length > 0) { + parameterInputsString = [ + '**Parameter Inputs**', + '```modelica', + parameterInputsInfo.join('\n'), + '```' + ].join('\n'); } - - return ''; -} - -function extractOutputInformation(classDefNode: SyntaxNode): string { - const outputsInfo: string[] = []; - - TreeSitterUtil.forEach(classDefNode, (node) => { - - if (node.type === 'component_clause' && node.text.includes('output')) { - - const typeSpecifierNode = node.childForFieldName('typeSpecifier'); - logger.debug(`Type specifier node: ${typeSpecifierNode}`); - const typeSpecifier = typeSpecifierNode ? typeSpecifierNode.text : "Unknown Type"; - - const componentDeclarationNode = node.childForFieldName('componentDeclarations'); - - const declarationNode = componentDeclarationNode?.firstChild?.childForFieldName('declaration'); - logger.debug(`Declaration node: ${declarationNode}`); - const identifier = declarationNode ? declarationNode.text : "Unknown Identifier"; - - // Extracting description from description_string node - const descriptionNode = componentDeclarationNode?.firstChild?.childForFieldName('descriptionString'); - const description = descriptionNode ? descriptionNode.text : ''; - - outputsInfo.push(`${typeSpecifier} ${identifier} ${description}\n`); - } - return true; - }); - if (outputsInfo.length > 0) { - return "\n## Outputs:\n" + outputsInfo.join('\n'); + outputsString = [ + '**Outputs**', + '```modelica', + outputsInfo.join('\n'), + '```' + ].join('\n'); } - return ''; + if (parameterOutputsInfo.length > 0) { + parameterOutputsString = [ + '**Parameter Outputs**', + '```modelica', + parameterOutputsInfo.join('\n'), + '```' + ].join('\n'); + } + if (parameterInfo.length > 0) { + parameterString = [ + '**Parameter**', + '```modelica', + parameterInfo.join('\n'), + '```' + ].join('\n'); + } + + return { + inputsInfo: inputsString, + outputsInfo: outputsString, + parameterInfo: parameterString, + parameterInputsInfo: parameterInputsString, + parameterOutputsInfo: parameterOutputsString, + }; } function extractParameterInformation(classDefNode: SyntaxNode): string { diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index 90a37b2..dcda2c6 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -125,6 +125,49 @@ export function isDefinition(n: SyntaxNode): boolean { } } +/* + component_clause: $ => seq( + optional(choice(field("flow", "flow"), field("stream", "stream"))), + optional(choice(field("constant", "constant"), field("discrete", "discrete"), field("parameter", "parameter"))), + optional(choice(field("input", "input"), field("output", "output"))), + field("typeSpecifier", $.type_specifier), + optional(field("subscripts", $.array_subscripts)), + field("componentDeclarations", $.component_list) + ), +*/ + +/** + * Get input/output prefix from node. + * + * @param n Node of tree + * @returns Base prefix or undefined. + */ +export function getPrefix(n: SyntaxNode): string | undefined { + switch (n.type) { + case 'short_class_specifier': + return n.childForFieldName('basePrefix')?.text; + case 'component_clause': + return n.childForFieldName('input')?.text || n.childForFieldName('output')?.text; + default: + return undefined; + } +} + +/** + * Check if node is parameter. + * + * @param n Node of tree + * @returns True if node has parameter keyword. + */ +export function isParameter(n: SyntaxNode): boolean { + switch (n.type) { + case 'component_clause': + return n.childForFieldName('parameter') !== null; + default: + return false; + } +} + export function findParent( start: SyntaxNode, predicate: (n: SyntaxNode) => boolean, From 08de30b429fba37d620de42ae7af6999ffaabf99 Mon Sep 17 00:00:00 2001 From: AnHeuermann <38031952+AnHeuermann@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:20:15 +0100 Subject: [PATCH 11/12] Fixing doc string for no documentation --- README.md | 4 +++ client/src/test/onHover.test.ts | 28 +++++++++++++------ client/testFixture/{output.md => step.md} | 0 client/testFixture/velocityOfSound_ph.md | 16 +++++++++++ client/testFixture/velocityOfSound_ph.mo | 11 ++++++++ images/hover_demo.png | Bin 0 -> 40409 bytes server/src/util/hoverUtil.ts | 23 +++++----------- server/src/util/tree-sitter.ts | 32 ++++++++++++++-------- 8 files changed, 79 insertions(+), 35 deletions(-) rename client/testFixture/{output.md => step.md} (100%) create mode 100644 client/testFixture/velocityOfSound_ph.md create mode 100644 client/testFixture/velocityOfSound_ph.mo create mode 100644 images/hover_demo.png diff --git a/README.md b/README.md index 07debb0..31e3940 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ features: ![Outline](images/outline_demo.png) + - Hover provider for declared symbols. + + ![Hover](images/hover_demo.png) + ## Installation ### Via Marketplace diff --git a/client/src/test/onHover.test.ts b/client/src/test/onHover.test.ts index 653e68d..93089a4 100644 --- a/client/src/test/onHover.test.ts +++ b/client/src/test/onHover.test.ts @@ -39,15 +39,27 @@ import * as assert from 'assert'; import { getDocUri, getDocPath, activate } from './helper'; suite('onHover information', async () => { - const docUri = getDocUri('step.mo'); - const position = new vscode.Position(19, 25); - const content = new vscode.MarkdownString( - fs.readFileSync(getDocPath('output.md'), 'utf-8')); - const expectedHoverInstances: vscode.Hover[] = [ - new vscode.Hover(content) - ]; + test('Step', async () => { + const docUri = getDocUri('step.mo'); + const position = new vscode.Position(19, 25); + const content = new vscode.MarkdownString( + fs.readFileSync(getDocPath('step.md'), 'utf-8')); + const expectedHoverInstances: vscode.Hover[] = [ + new vscode.Hover(content) + ]; + + await testOnHover(docUri, position, expectedHoverInstances); + }); + + test('velocityOfSound_ph', async () => { + const docUri = getDocUri('velocityOfSound_ph.mo'); + const position = new vscode.Position(0, 20); + const content = new vscode.MarkdownString( + fs.readFileSync(getDocPath('velocityOfSound_ph.md'), 'utf-8')); + const expectedHoverInstances: vscode.Hover[] = [ + new vscode.Hover(content) + ]; - test('onHover()', async () => { await testOnHover(docUri, position, expectedHoverInstances); }); }); diff --git a/client/testFixture/output.md b/client/testFixture/step.md similarity index 100% rename from client/testFixture/output.md rename to client/testFixture/step.md diff --git a/client/testFixture/velocityOfSound_ph.md b/client/testFixture/velocityOfSound_ph.md new file mode 100644 index 0000000..5bfbcf4 --- /dev/null +++ b/client/testFixture/velocityOfSound_ph.md @@ -0,0 +1,16 @@ +```modelica +function velocityOfSound_ph +``` +--- +**Inputs** +```modelica +input SI.Pressure p "Pressure"; +input SI.SpecificEnthalpy h "Specific enthalpy"; +input Integer phase = 0 "2 for two-phase, 1 for one-phase, 0 if not known"; +input Integer region = 0 "If 0, region is unknown, otherwise known and this input"; +``` + +**Outputs** +```modelica +output SI.Velocity v_sound "Speed of sound"; +``` diff --git a/client/testFixture/velocityOfSound_ph.mo b/client/testFixture/velocityOfSound_ph.mo new file mode 100644 index 0000000..2bd5f0c --- /dev/null +++ b/client/testFixture/velocityOfSound_ph.mo @@ -0,0 +1,11 @@ +function velocityOfSound_ph + extends Modelica.Icons.Function; + input SI.Pressure p "Pressure"; + input SI.SpecificEnthalpy h "Specific enthalpy"; + input Integer phase = 0 "2 for two-phase, 1 for one-phase, 0 if not known"; + input Integer region = 0 "If 0, region is unknown, otherwise known and this input"; + output SI.Velocity v_sound "Speed of sound"; +algorithm + v_sound := velocityOfSound_props_ph(p, h, waterBaseProp_ph(p, h, phase, region)); + annotation(Inline = true); +end velocityOfSound_ph; diff --git a/images/hover_demo.png b/images/hover_demo.png new file mode 100644 index 0000000000000000000000000000000000000000..81518768ba18c9e18f36004a04e070b285c0074d GIT binary patch literal 40409 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
4+Gcmf0e+@$S6Nv@dji0cXA;!?COp0 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 n.type === 'description_string'); - - return descriptionNode?.firstChild?.text; -} - function extractComponentInformation( classDefNode: SyntaxNode): { inputsInfo: string | undefined; diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index dcda2c6..e4cfcc1 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -125,17 +125,6 @@ export function isDefinition(n: SyntaxNode): boolean { } } -/* - component_clause: $ => seq( - optional(choice(field("flow", "flow"), field("stream", "stream"))), - optional(choice(field("constant", "constant"), field("discrete", "discrete"), field("parameter", "parameter"))), - optional(choice(field("input", "input"), field("output", "output"))), - field("typeSpecifier", $.type_specifier), - optional(field("subscripts", $.array_subscripts)), - field("componentDeclarations", $.component_list) - ), -*/ - /** * Get input/output prefix from node. * @@ -212,3 +201,24 @@ export function getClassPrefixes(node: SyntaxNode): string | null { return classPrefixNode.text; } + +/** + * Get description string. + * + * @param node Syntax node + * @returns Description string of node. + */ +export function getDescriptionString(node: SyntaxNode): string | undefined { + let classNode: SyntaxNode | null; + + switch (node.type) { + case 'class_definition': + classNode = node.childForFieldName('classSpecifier'); + if (classNode !== null) { + return getDescriptionString(classNode); + } + return undefined; + default: + return node.childForFieldName('descriptionString')?.text; + } +} From 24f92c86858d0e4eee966e3960f2ed9d84533133 Mon Sep 17 00:00:00 2001 From: AnHeuermann <38031952+AnHeuermann@users.noreply.github.com> Date: Thu, 23 May 2024 11:42:20 +0200 Subject: [PATCH 12/12] Adding TODOs and linting --- .eslintrc.js | 3 +- .github/workflows/test.yml | 3 + client/src/extension.ts | 1 - client/src/test/symbolinformation.test.ts | 22 ------- server/src/analyzer.ts | 4 +- server/src/server.ts | 79 +---------------------- server/src/test/server.test.ts | 4 +- server/src/util/array.ts | 24 ------- server/src/util/declarations.ts | 5 -- server/src/util/hoverUtil.ts | 39 ++--------- server/src/util/tree-sitter.ts | 2 - 11 files changed, 14 insertions(+), 172 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 10793c5..8f2afe6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,7 +12,8 @@ module.exports = { ], rules: { 'semi': [2, "always"], - '@typescript-eslint/no-unused-vars': 0, + '@typescript-eslint/no-unused-vars': ["warn"], + '@typescript-eslint/no-unused-expressions': ["warn"], '@typescript-eslint/no-explicit-any': 0, '@typescript-eslint/explicit-module-boundary-types': 0, '@typescript-eslint/no-non-null-assertion': 0, diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d88a42d..0e91e93 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,9 @@ jobs: - name: Build package run: npm run esbuild + - name: ESLint + run: npm run lint + - name: Test language server run: npm run test:server diff --git a/client/src/extension.ts b/client/src/extension.ts index 4348345..fddba21 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -43,7 +43,6 @@ import { TransportKind, } from 'vscode-languageclient/node'; import { getFileExtension, getLanguage } from './getLanguage'; -import { fstat } from 'fs'; let client: LanguageClient; diff --git a/client/src/test/symbolinformation.test.ts b/client/src/test/symbolinformation.test.ts index c2ddfb6..8a7adfd 100644 --- a/client/src/test/symbolinformation.test.ts +++ b/client/src/test/symbolinformation.test.ts @@ -76,31 +76,9 @@ async function testSymbolInformation( docUri, ); - //printDocumentSymbols(actualSymbolInformation); assertDocumentSymbolsEqual(expectedDocumentSymbols, actualSymbolInformation); } -function printDocumentSymbols(documentSymbols: vscode.DocumentSymbol[]) { - documentSymbols.forEach((symbol, index) => { - console.log(`Document Symbol ${index + 1}:`); - console.log(`Name: ${symbol.name}`); - console.log(`Kind: ${vscode.SymbolKind[symbol.kind]}`); - console.log( - `Range: ${symbol.range.start.line}:${symbol.range.start.character}, ${symbol.range.end.line}:${symbol.range.end.character}`, - ); - console.log( - `SelectionRange: ${symbol.selectionRange.start.line}:${symbol.selectionRange.start.character}, ${symbol.selectionRange.end.line}:${symbol.selectionRange.end.character}`, - ); - console.log('Children:'); - - if (symbol.children && symbol.children.length > 0) { - printDocumentSymbols(symbol.children); - } - - console.log('---'); - }); -} - function assertDocumentSymbolsEqual( expected: vscode.DocumentSymbol[], actual: vscode.DocumentSymbol[], diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 9c75a46..d771911 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -41,12 +41,10 @@ import * as LSP from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import * as fs from 'fs'; import Parser = require('web-tree-sitter'); import { getAllDeclarationsInTree } from './util/declarations'; import { logger } from './util/logger'; -import { log } from 'console'; type AnalyzedDocument = { document: TextDocument; @@ -120,7 +118,7 @@ export default class Analyzer { // Find all declarations matching identifier. for (const availableUri of Object.keys(this.#uriToAnalyzedDocument)) { - // TODO: Filter reachable uri, e.g. because of an inclue + // TODO: Filter reachable uri, e.g. because of an include const decl = this.#uriToAnalyzedDocument[availableUri]?.declarations; if (decl) { for (const d of decl) { diff --git a/server/src/server.ts b/server/src/server.ts index 5f9f7ca..553572d 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -39,13 +39,11 @@ * ---------------------------------------------------------------------------- */ -import * as path from 'node:path'; import * as LSP from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { initializeParser } from './parser'; import Analyzer from './analyzer'; -import { uniqueBasedOnHash } from './util/array'; import { extractHoverInformation } from './util/hoverUtil'; import { logger, setLogConnection, setLogLevel } from './util/logger'; @@ -142,34 +140,7 @@ export class ModelicaServer { } private async analyzeDocument(document: TextDocument) { - const diagnostics = this.#analyzer.analyze(document); - } - - private getCommentForSymbol({ - currentUri, - symbol, - }: { - symbol: LSP.SymbolInformation - currentUri: string - }): string { - - logger.debug(`getDocumentationForSymbol: symbol=${symbol.name} uri=${symbol.location.uri}`); - - const symbolUri = symbol.location.uri; - const symbolStartLine = symbol.location.range.start.line; - - const commentAboveSymbol = this.#analyzer.commentsAbove(symbolUri, symbolStartLine); - const commentAbove = commentAboveSymbol ? `\n\n${commentAboveSymbol}` : ''; - const hoverHeader = `${symbolKindToDescription(symbol.kind)}: **${symbol.name}**`; - const symbolLocation = - symbolUri !== currentUri - ? `in ${path.relative(path.dirname(currentUri), symbolUri)}` - : `on line ${symbolStartLine + 1}`; - - // TODO: An improvement could be to add show the symbol definition in the hover instead - // of the defined location – similar to how VSCode works for languages like TypeScript. - - return `\n${commentAbove}`; + this.#analyzer.analyze(document); } // ============================== @@ -216,6 +187,7 @@ export class ModelicaServer { return null; } + // TODO: Get node defining symbol and extract hover information of that one. const hoverInfo = extractHoverInformation(node); if (hoverInfo == null) { return null; @@ -233,53 +205,6 @@ export class ModelicaServer { } } -/** - * Deduplicate symbols by prioritizing the current file. - */ -function deduplicateSymbols({ - symbols, - currentUri, -}: { - symbols: LSP.SymbolInformation[] - currentUri: string -}) { - const isCurrentFile = ({ location: { uri } }: LSP.SymbolInformation) => - uri === currentUri; - - const getSymbolId = ({ name, kind }: LSP.SymbolInformation) => `${name}${kind}`; - - const symbolsCurrentFile = symbols.filter((s) => isCurrentFile(s)); - - const symbolsOtherFiles = symbols - .filter((s) => !isCurrentFile(s)) - // Remove identical symbols matching current file - .filter( - (symbolOtherFiles) => - !symbolsCurrentFile.some( - (symbolCurrentFile) => - getSymbolId(symbolCurrentFile) === getSymbolId(symbolOtherFiles), - ), - ); - - // NOTE: it might be that uniqueBasedOnHash is not needed anymore - return uniqueBasedOnHash([...symbolsCurrentFile, ...symbolsOtherFiles], getSymbolId); -} - -function symbolKindToDescription(kind: LSP.SymbolKind): string { - switch (kind) { - case LSP.SymbolKind.Class: - return 'Class'; - case LSP.SymbolKind.Function: - return 'Function'; - case LSP.SymbolKind.Package: - return 'Package'; - case LSP.SymbolKind.TypeParameter: - return 'Type'; - default: - return 'Modelica symbol'; - } -} - // Create a connection for the server, using Node's IPC as a transport. // Also include all preview / proposed LSP features. const connection = LSP.createConnection(LSP.ProposedFeatures.all); diff --git a/server/src/test/server.test.ts b/server/src/test/server.test.ts index 8c982d2..8b97d1b 100644 --- a/server/src/test/server.test.ts +++ b/server/src/test/server.test.ts @@ -33,9 +33,7 @@ * */ -import * as Mocha from 'mocha'; import * as assert from 'assert'; -import * as Parser from 'web-tree-sitter'; import { initializeParser } from '../parser'; @@ -51,7 +49,7 @@ const parsedModelicaTestString = describe('Modelica tree-sitter parser', () => { it('Initialize parser', async () => { - const parser = await initializeParser(); + await initializeParser(); }); it('Parse string', async () => { diff --git a/server/src/util/array.ts b/server/src/util/array.ts index 7e32d17..360a997 100644 --- a/server/src/util/array.ts +++ b/server/src/util/array.ts @@ -12,27 +12,3 @@ export function flattenArray(nestedArray: T[][]): T[] { export function uniq(a: A[]): A[] { return Array.from(new Set(a)); } - -/** - * Removed all duplicates from the list based on the hash function. - * First element matching the hash function wins. - */ -export function uniqueBasedOnHash>( - list: A[], - elementToHash: (a: A) => string, - __result: A[] = [], -): A[] { - const result: typeof list = []; - const hashSet = new Set(); - - list.forEach((element) => { - const hash = elementToHash(element); - if (hashSet.has(hash)) { - return; - } - hashSet.add(hash); - result.push(element); - }); - - return result; -} diff --git a/server/src/util/declarations.ts b/server/src/util/declarations.ts index 3fe9fe5..549e736 100644 --- a/server/src/util/declarations.ts +++ b/server/src/util/declarations.ts @@ -42,22 +42,17 @@ 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']); - export function getLocalDeclarations({ node, - rootNode, uri, }: { node: Parser.SyntaxNode | null - rootNode: Parser.SyntaxNode uri: string }): Declarations { const declarations: Declarations = {}; diff --git a/server/src/util/hoverUtil.ts b/server/src/util/hoverUtil.ts index add17b7..e80cd73 100644 --- a/server/src/util/hoverUtil.ts +++ b/server/src/util/hoverUtil.ts @@ -1,6 +1,5 @@ import { SyntaxNode } from 'web-tree-sitter'; import * as TreeSitterUtil from './tree-sitter'; -import * as LSP from 'vscode-languageserver'; import { logger } from './logger'; /** @@ -9,6 +8,10 @@ import { logger } from './logger'; * Documentation and information for class description, inputs, outputs and * parameters. * + * TODO: Modify this function to accept class_definition and + * component_declaration and so on. + * TODO: Use querries instead of checking children. + * * @param node Syntax Node. * * @returns Hover content or null if no information available. @@ -22,7 +25,7 @@ export function extractHoverInformation(node: SyntaxNode): string | null { } // Check if node is the first IDENT child of the class_definition, indicating it's the class name. - const isClassName = classDefNode.namedChildren.some((child, _) => + const isClassName = classDefNode.namedChildren.some((child) => child.type === 'long_class_specifier' && child.firstChild?.type === 'IDENT' && child.firstChild?.text === node.text); @@ -168,35 +171,3 @@ function extractComponentInformation( parameterOutputsInfo: parameterOutputsString, }; } - -function extractParameterInformation(classDefNode: SyntaxNode): string { - const parametersInfo: string[] = []; - - TreeSitterUtil.forEach(classDefNode, (node) => { - - if (node.type === 'component_clause' && node.text.includes('parameter')) { - - const typeSpecifierNode = node.childForFieldName('typeSpecifier'); - logger.debug(`Type specifier node: ${typeSpecifierNode}`); - const typeSpecifier = typeSpecifierNode ? typeSpecifierNode.text : "Unknown Type"; - - const componentDeclarationNode = node.childForFieldName('componentDeclarations'); - - const declarationNode = componentDeclarationNode?.firstChild?.childForFieldName('declaration'); - logger.debug(`Declaration node: ${declarationNode}`); - const identifier = declarationNode ? declarationNode.text : "Unknown Identifier"; - - // Extracting description from description_string node - const descriptionNode = componentDeclarationNode?.firstChild?.childForFieldName('descriptionString'); - const description = descriptionNode ? descriptionNode.text : ''; - - parametersInfo.push(`${typeSpecifier} ${identifier} ${description}\n`); - } - return true; - }); - - if (parametersInfo.length > 0) { - return "\n## Parameters:\n" + parametersInfo.join('\n'); - } - return ''; -} diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index d9a21d1..67da965 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -42,8 +42,6 @@ import * as LSP from 'vscode-languageserver/node'; import { SyntaxNode } from 'web-tree-sitter'; -import { logger } from './logger'; - /** * Recursively iterate over all nodes in a tree. *