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:
+
+
+
+
+
+
+"));
+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: