diff --git a/README.md b/README.md index d36cede..e1c0a43 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ features: ![Goto Declaration](images/goto_declaration_demo.png) +- Hover provider for declared symbols. + + ![Hover](images/hover_demo.png) + ## Configuration ### Loading external Modelica libraries diff --git a/client/package-lock.json b/client/package-lock.json index 839298a..198da3b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "modelica-language-server-client", - "version": "0.2.3", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "modelica-language-server-client", - "version": "0.2.3", + "version": "0.3.0", "license": "OSMC-PL-1-8", "dependencies": { "vscode-languageclient": "^10.0.0" @@ -21,9 +21,9 @@ } }, "node_modules/@types/vscode": { - "version": "1.120.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.120.0.tgz", - "integrity": "sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==", + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", "dev": true, "license": "MIT" }, @@ -45,13 +45,11 @@ } }, "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, + "license": "MIT", "engines": { "node": ">= 14" } @@ -136,15 +134,17 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -180,6 +180,7 @@ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -189,12 +190,13 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "^7.0.2", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { @@ -205,13 +207,15 @@ "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/is-interactive": { "version": "2.0.0", @@ -243,13 +247,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", @@ -262,6 +268,7 @@ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "dev": true, + "license": "MIT", "dependencies": { "immediate": "~3.0.5" } @@ -325,10 +332,11 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/onetime": { "version": "7.0.0", @@ -374,19 +382,22 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "dev": true, + "license": "(MIT AND Zlib)" }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -418,12 +429,13 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/semver": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", - "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -436,7 +448,8 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/signal-exit": { "version": "4.1.0", @@ -469,6 +482,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } @@ -511,7 +525,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/vscode-jsonrpc": { "version": "9.0.0", diff --git a/client/package.json b/client/package.json index 57432d8..5516785 100644 --- a/client/package.json +++ b/client/package.json @@ -3,7 +3,7 @@ "description": "VSCode part of a language server", "author": "Andreas Heuermann, Osman Karabel, Evan Hedbor, PaddiM8", "license": "OSMC-PL-1-8", - "version": "0.2.3", + "version": "0.3.0", "publisher": "vscode", "repository": { "type": "git", diff --git a/client/src/test/onHover.test.ts b/client/src/test/onHover.test.ts new file mode 100644 index 0000000..b0c71cd --- /dev/null +++ b/client/src/test/onHover.test.ts @@ -0,0 +1,108 @@ +/* + * This file is part of OpenModelica. + * + * Copyright (c) 1998-2026, 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, executeProviderUntilResult } from './helper'; + +suite('onHover information', async () => { + 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) + ]; + + 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 executeProviderUntilResult( + "vscode.executeHoverProvider", + [uri, position], + ); + + assertHoverInstancesEqual(expectedHoverInstances, actualHoverInstances); +} + +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.trim(), expectedContent.trim(), `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/testFixture/step.md b/client/testFixture/step.md new file mode 100644 index 0000000..90f3cb9 --- /dev/null +++ b/client/testFixture/step.md @@ -0,0 +1,20 @@ +```modelica +class Modelica_Blocks_Sources_Step "Generate step signal of type Real" +``` +--- + +**Parameter Inputs** +```modelica +parameter input Real height = 1.0 "Height of step"; +``` +**Outputs** +```modelica +output Real y "Connector of Real output signal"; +``` + +**Parameter** +```modelica +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 new file mode 100644 index 0000000..04262d1 --- /dev/null +++ b/client/testFixture/step.mo @@ -0,0 +1,20 @@ +class Modelica_Blocks_Sources_Step "Generate step signal of type Real" + 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"; +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/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 0000000..8151876 Binary files /dev/null and b/images/hover_demo.png differ diff --git a/server/README.md b/server/README.md index 67b05bd..10af6b9 100644 --- a/server/README.md +++ b/server/README.md @@ -16,7 +16,7 @@ This package is the standalone LSP server. It communicates over stdio and works | Go to declaration | ✓ | | Go to definition | ✓ | | Incremental document sync | ✓ | -| Hover | planned | +| Hover | ✓ | | Completion | planned | ## Installation diff --git a/server/package-lock.json b/server/package-lock.json index 5c7ef57..cf3c797 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@openmodelica/modelica-language-server", - "version": "0.2.3", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@openmodelica/modelica-language-server", - "version": "0.2.3", + "version": "0.3.0", "license": "(AGPL-3.0-only OR LicenseRef-OSMC-PL-1-8)", "dependencies": { "vscode-languageserver": "^10.0.0", @@ -27,9 +27,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -40,13 +40,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -61,9 +61,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -78,9 +78,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -95,9 +95,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -112,9 +112,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -129,9 +129,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -146,9 +146,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -163,9 +163,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -180,9 +180,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -197,9 +197,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -214,9 +214,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -231,9 +231,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -248,9 +248,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -265,9 +265,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -282,9 +282,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -299,9 +299,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -316,9 +316,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -333,9 +333,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -350,9 +350,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -367,9 +367,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -384,9 +384,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -401,9 +401,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -418,9 +418,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -435,9 +435,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -452,9 +452,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -469,9 +469,9 @@ } }, "node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "version": "25.9.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz", + "integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==", "dev": true, "license": "MIT", "dependencies": { @@ -479,9 +479,9 @@ } }, "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -492,32 +492,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/fsevents": { diff --git a/server/package.json b/server/package.json index 038693f..b269728 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,7 @@ "lsp", "openmodelica" ], - "version": "0.2.3", + "version": "0.3.0", "author": "Andreas Heuermann, Osman Karabel, Evan Hedbor, PaddiM8", "license": "(AGPL-3.0-only OR LicenseRef-OSMC-PL-1-8)", "engines": { diff --git a/server/src/analysis/resolveReference.ts b/server/src/analysis/resolveReference.ts index 0cf702b..9e044a9 100644 --- a/server/src/analysis/resolveReference.ts +++ b/server/src/analysis/resolveReference.ts @@ -474,7 +474,6 @@ function resolveReferenceInLibrary( library: ModelicaLibrary, reference: UnresolvedAbsoluteReference, ): ResolvedReference | null { - let alreadyResolved: ResolvedReference | null = null; for (let i = 0; i < reference.symbols.length; i++) { alreadyResolved = resolveNext(library, reference.symbols[i], alreadyResolved); diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index 0bcec75..a974a36 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -57,6 +57,7 @@ import { uriToPath } from './util'; import * as TreeSitterUtil from './util/tree-sitter'; import { getAllDeclarationsInTree } from './util/declarations'; import { logger } from './util/logger'; +import { extractHoverInformation } from './util/hoverUtil'; export default class Analyzer { #project: ModelicaProject; @@ -230,6 +231,74 @@ export default class Analyzer { } } + /** + * Returns hover information for the symbol at the given position. + * + * @param uri the opened document + * @param position the cursor position + * @returns Markdown hover content, or `null` if not found. + */ + public async findHoverInfo( + uri: LSP.DocumentUri, + position: LSP.Position, + ): Promise { + const path = uriToPath(uri); + const document = await this.#project.getDocument(path); + if (!document || !document.tree.rootNode) { + return null; + } + + const reference = this.getReferenceAt(document, position); + if (!reference) { + return null; + } + + // Try to resolve via the full resolution system (handles external classes, qualified names). + try { + const result = resolveReference(document.project, reference, 'declaration'); + if (result?.node.type === 'class_definition') { + return this.hoverFromClassDef(result.node); + } + } catch (e: unknown) { + if (e instanceof Error) { + logger.debug('Caught exception in findHoverInfo: ', e.stack); + } + } + + // Fallback: search the current document for a matching class definition. + // This handles standalone files where the file name differs from the class name. + const lastName = reference.symbols[reference.symbols.length - 1]; + const localClassDef = TreeSitterUtil.findFirst( + document.tree.rootNode, + (n) => n.type === 'class_definition' && TreeSitterUtil.hasIdentifier(n, lastName), + ); + if (localClassDef) { + return this.hoverFromClassDef(localClassDef); + } + + return null; + } + + /** + * Builds a Markdown hover from a class definition node. + * + * @param classDefNode a `class_definition` syntax node + * @returns a Markdown {@link LSP.Hover}, or `null` if no hover information + * could be extracted from the node. + */ + private hoverFromClassDef(classDefNode: SyntaxNode): LSP.Hover | null { + const hoverInfo = extractHoverInformation(classDefNode); + if (!hoverInfo) { + return null; + } + return { + contents: { + kind: LSP.MarkupKind.Markdown, + value: hoverInfo, + } as LSP.MarkupContent, + }; + } + /** * Returns the reference at the document position, or `null` if no reference * exists. diff --git a/server/src/server.ts b/server/src/server.ts index a65a389..6bfe6a8 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -133,7 +133,7 @@ export class ModelicaServer { completionProvider: undefined, declarationProvider: true, definitionProvider: true, - hoverProvider: false, + hoverProvider: true, signatureHelpProvider: undefined, documentSymbolProvider: true, colorProvider: false, @@ -148,6 +148,11 @@ export class ModelicaServer { }; } + /** + * Register handlers for the events from the Language Server Protocol + * + * @param connection + */ public register(connection: LSP.Connection): void { // Make the text document manager listen on the connection // for open, change and close text document events @@ -160,6 +165,7 @@ export class ModelicaServer { connection.onDeclaration(this.onDeclaration.bind(this)); connection.onDefinition(this.onDefinition.bind(this)); connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); + connection.onHover(this.onHover.bind(this)); } private async onInitialized(): Promise { @@ -282,11 +288,15 @@ export class ModelicaServer { return [locationLink]; } + // ============================== + // Language server event handlers + // ============================== + /** * Provide symbols defined in document. * - * @param params Unused. - * @returns Symbol information. + * @param symbolParams Document symbols of given text document. + * @returns Symbol information. */ private async onDocumentSymbol( params: LSP.DocumentSymbolParams, @@ -297,6 +307,17 @@ export class ModelicaServer { logger.debug(`onDocumentSymbol`); return this.#analyzer.getDeclarationsForUri(params.textDocument.uri); } + + /** + * Provide hover information at given text document position. + * + * @param params Text document position. + * @returns Hover information. + */ + private async onHover(params: LSP.HoverParams): Promise { + logger.debug('onHover'); + return this.#analyzer.findHoverInfo(params.textDocument.uri, params.position); + } } // Create a connection for the server, using Node's IPC as a transport. diff --git a/server/src/util/hoverUtil.ts b/server/src/util/hoverUtil.ts new file mode 100644 index 0000000..c097de2 --- /dev/null +++ b/server/src/util/hoverUtil.ts @@ -0,0 +1,160 @@ +/* + * This file is part of OpenModelica. + * + * Copyright (c) 1998-2026, 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 { Node as SyntaxNode } from 'web-tree-sitter'; +import * as TreeSitterUtil from './tree-sitter'; +import { logger } from './logger'; + +/** + * Extracts hover information for a `class_definition` syntax node. + * + * Returns Markdown-formatted documentation including the class description, + * inputs, outputs, and parameters. + * + * @param classDefNode A `class_definition` syntax node. + * @returns Hover content in Markdown, or null if unavailable. + */ +export function extractHoverInformation(classDefNode: SyntaxNode): string | null { + if (classDefNode.type !== 'class_definition') { + logger.debug('extractHoverInformation: Node is not a class_definition.'); + return null; + } + + const classSpecifier = classDefNode.childForFieldName('classSpecifier'); + const nameNode = classSpecifier?.childForFieldName('identifier'); + if (!nameNode) { + logger.debug('extractHoverInformation: No identifier found in class_definition.'); + return null; + } + + const className = nameNode.text; + const classDescription = TreeSitterUtil.getDescriptionString(classDefNode); + const { inputsInfo, outputsInfo, parameterInfo, parameterInputsInfo, parameterOutputsInfo } = + extractComponentInformation(classDefNode); + + const classDefinition = [ + TreeSitterUtil.getClassPrefixes(classDefNode), + className, + classDescription, + ] + .filter((e) => e) + .join(' ') + .trim(); + + return [ + '```modelica', + classDefinition, + '```', + '---', + inputsInfo, + parameterInputsInfo, + outputsInfo, + parameterOutputsInfo, + parameterInfo, + ].join('\n'); +} + +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[] = []; + + TreeSitterUtil.forEach(classDefNode, (node) => { + 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'; + + 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 { + inputsInfo: formatSection('**Inputs**', inputsInfo), + parameterInputsInfo: formatSection('**Parameter Inputs**', parameterInputsInfo), + outputsInfo: formatSection('**Outputs**', outputsInfo), + parameterOutputsInfo: formatSection('**Parameter Outputs**', parameterOutputsInfo), + parameterInfo: formatSection('**Parameter**', parameterInfo), + }; +} + +function formatSection(header: string, lines: string[]): string | undefined { + if (lines.length === 0) { + return undefined; + } + return [header, '```modelica', lines.join('\n'), '```'].join('\n'); +} diff --git a/server/src/util/test/hover.test.ts b/server/src/util/test/hover.test.ts new file mode 100644 index 0000000..53a31c6 --- /dev/null +++ b/server/src/util/test/hover.test.ts @@ -0,0 +1,135 @@ +/* + * This file is part of OpenModelica. + * + * Copyright (c) 1998-2026, 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 assert from 'node:assert/strict'; +import path from 'node:path'; +import * as LSP from 'vscode-languageserver/node'; +import { initializeParser } from '../../parser'; +import Analyzer from '../../analyzer'; +import { ModelicaLibrary, ModelicaProject } from '../../project'; +import { extractHoverInformation } from '../hoverUtil'; +import * as TreeSitterUtil from '../tree-sitter'; +import { pathToUri } from '../index'; + +const TEST_LIBRARY_PATH = path.join( + __dirname, + '../../analysis/test/TestLibrary', +); +const TEST_CLASS_PATH = path.join( + TEST_LIBRARY_PATH, + 'TestPackage', + 'TestClass.mo', +); + +describe('extractHoverInformation', () => { + it('returns null for non-class_definition nodes', async () => { + const parser = await initializeParser(); + const tree = parser.parse('function Foo input Real x; end Foo;'); + assert.ok(tree); + // Pass the root node (stored_definitions), not a class_definition + const result = extractHoverInformation(tree.rootNode); + assert.equal(result, null); + }); + + it('returns hover content for a function with inputs', async () => { + const parser = await initializeParser(); + const project = new ModelicaProject(parser); + project.addLibrary(await ModelicaLibrary.load(project, TEST_LIBRARY_PATH, true)); + + const document = await project.getDocument(TEST_CLASS_PATH); + assert.ok(document); + + const classDefNode = TreeSitterUtil.findFirst( + document.tree.rootNode, + (n) => n.type === 'class_definition' && TreeSitterUtil.hasIdentifier(n, 'TestClass'), + ); + assert.ok(classDefNode, 'TestClass class_definition not found'); + + const result = extractHoverInformation(classDefNode); + assert.ok(result, 'Expected non-null hover content'); + assert.ok(result.includes('TestClass'), 'Expected class name in hover'); + assert.ok(result.includes('**Inputs**'), 'Expected inputs section in hover'); + assert.ok(result.includes('twoE'), 'Expected input twoE in hover'); + }); +}); + +describe('Analyzer.findHoverInfo', () => { + let analyzer: Analyzer; + + beforeEach(async () => { + const parser = await initializeParser(); + analyzer = new Analyzer(parser); + await analyzer.loadLibrary(pathToUri(TEST_LIBRARY_PATH), true); + }); + + it('returns hover info when hovering on a class name in its definition', async () => { + // `function TestClass` — position (4, 9) is on 'T' of TestClass + const uri = pathToUri(TEST_CLASS_PATH); + const position: LSP.Position = { line: 4, character: 9 }; + + const hover = await analyzer.findHoverInfo(uri, position); + assert.ok(hover, 'Expected non-null hover for class definition'); + + const content = (hover.contents as LSP.MarkupContent).value; + assert.ok(content.includes('TestClass'), 'Expected class name in hover content'); + assert.ok(content.includes('**Inputs**'), 'Expected inputs section'); + }); + + it('returns hover info for a fully qualified class name reference', async () => { + // In Constants.mo: `package Constants` on line 2 (0-indexed) + // hover on 'C' at (2, 8) + const constantsPath = path.join(TEST_LIBRARY_PATH, 'Constants.mo'); + const uri = pathToUri(constantsPath); + const position: LSP.Position = { line: 2, character: 8 }; + + const hover = await analyzer.findHoverInfo(uri, position); + assert.ok(hover, 'Expected non-null hover for package class'); + const content = (hover.contents as LSP.MarkupContent).value; + assert.ok(content.includes('Constants'), 'Expected class name in hover'); + }); + + it('resolves a qualified within-clause reference to the top-level library', async () => { + // In TestClass.mo line 0: `within TestLibrary.TestPackage;` + // Hovering on 'T' (col 7) resolves 'TestLibrary' via the qualified name. + const uri = pathToUri(TEST_CLASS_PATH); + // col 7 = start of 'TestLibrary' in the within clause + const position: LSP.Position = { line: 0, character: 7 }; + + const hover = await analyzer.findHoverInfo(uri, position); + assert.ok(hover, 'Expected hover for qualified name in within clause'); + const content = (hover.contents as LSP.MarkupContent).value; + assert.ok(content.includes('TestLibrary'), 'Expected library name in hover content'); + }); +}); diff --git a/server/src/util/tree-sitter.ts b/server/src/util/tree-sitter.ts index 76da9ae..16f9547 100644 --- a/server/src/util/tree-sitter.ts +++ b/server/src/util/tree-sitter.ts @@ -41,7 +41,6 @@ import { Node as SyntaxNode, Point } from 'web-tree-sitter'; import * as LSP from 'vscode-languageserver/node'; - import { TextDocument } from 'vscode-languageserver-textdocument'; /** @@ -124,6 +123,38 @@ export function isDefinition(n: SyntaxNode): boolean { } } +/** + * 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; + } +} + /** * Tell if a node is a variable declaration. * @@ -391,6 +422,27 @@ 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; + } +} + export function positionToPoint(position: LSP.Position): Point { return { row: position.line, column: position.character }; }