├── .gitignore ├── .eslintignore ├── images ├── outline_demo.png ├── goto_declaration_demo.png └── Modelica_Language_margin.jpg ├── server ├── src │ ├── analysis │ │ ├── test │ │ │ ├── TestLibrary │ │ │ │ ├── TestPackage │ │ │ │ │ ├── package.mo │ │ │ │ │ └── TestClass.mo │ │ │ │ ├── package.mo │ │ │ │ └── Constants.mo │ │ │ └── resolveReference.test.ts │ │ ├── reference.ts │ │ └── resolveReference.ts │ ├── project │ │ ├── test │ │ │ ├── TestLibrary 1.0.0 │ │ │ │ ├── package.mo │ │ │ │ └── HalfAdder.mo │ │ │ ├── project.test.ts │ │ │ └── document.test.ts │ │ ├── index.ts │ │ ├── library.ts │ │ ├── document.ts │ │ └── project.ts │ ├── tree-sitter-modelica.wasm │ ├── util │ │ ├── index.ts │ │ ├── test │ │ │ ├── util.test.ts │ │ │ └── declarations.test.ts │ │ ├── declarations.ts │ │ ├── logger.ts │ │ └── tree-sitter.ts │ ├── parser.ts │ ├── test │ │ └── server.test.ts │ ├── server.ts │ └── analyzer.ts ├── tsconfig.json ├── package.json └── package-lock.json ├── scripts ├── e2e.sh └── e2e.ps1 ├── client ├── testFixture │ └── MyLibrary.mo ├── tsconfig.json ├── package.json ├── src │ ├── test │ │ ├── runTest.ts │ │ ├── index.ts │ │ ├── gotoDeclaration.test.ts │ │ ├── helper.ts │ │ └── symbolinformation.test.ts │ ├── getLanguage.ts │ └── extension.ts └── package-lock.json ├── .github ├── dependabot └── workflows │ └── test.yml ├── .vscodeignore ├── .prettierrc.js ├── .vscode ├── extensions.json ├── settings.json ├── tasks.json └── launch.json ├── tsconfig.json ├── .eslintrc.js ├── esbuild.config.js ├── package.json ├── README.md └── OSMC-License.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode-test 2 | modelica-language-server*.vsix 3 | node_modules/ 4 | out/ 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | client/node_modules/** 3 | client/out/** 4 | server/node_modules/** 5 | server/out/** 6 | -------------------------------------------------------------------------------- /images/outline_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenModelica/modelica-language-server/HEAD/images/outline_demo.png -------------------------------------------------------------------------------- /server/src/analysis/test/TestLibrary/TestPackage/package.mo: -------------------------------------------------------------------------------- 1 | within TestLibrary; 2 | 3 | package TestPackage 4 | end TestPackage; 5 | -------------------------------------------------------------------------------- /server/src/analysis/test/TestLibrary/package.mo: -------------------------------------------------------------------------------- 1 | package TestLibrary 2 | annotation(version="1.0.0"); 3 | end TestLibrary; 4 | -------------------------------------------------------------------------------- /images/goto_declaration_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenModelica/modelica-language-server/HEAD/images/goto_declaration_demo.png -------------------------------------------------------------------------------- /server/src/project/test/TestLibrary 1.0.0/package.mo: -------------------------------------------------------------------------------- 1 | package TestLibrary 2 | annotation(version="1.0.0"); 3 | end TestLibrary; 4 | -------------------------------------------------------------------------------- /images/Modelica_Language_margin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenModelica/modelica-language-server/HEAD/images/Modelica_Language_margin.jpg -------------------------------------------------------------------------------- /server/src/tree-sitter-modelica.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenModelica/modelica-language-server/HEAD/server/src/tree-sitter-modelica.wasm -------------------------------------------------------------------------------- /server/src/project/index.ts: -------------------------------------------------------------------------------- 1 | export { ModelicaDocument } from "./document"; 2 | export { ModelicaLibrary } from "./library"; 3 | export { ModelicaProject } from "./project"; 4 | -------------------------------------------------------------------------------- /scripts/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export CODE_TESTS_PATH="$(pwd)/client/out/test" 4 | export CODE_TESTS_WORKSPACE="$(pwd)/client/testFixture" 5 | 6 | node "$(pwd)/client/out/test/runTest" 7 | -------------------------------------------------------------------------------- /scripts/e2e.ps1: -------------------------------------------------------------------------------- 1 | $env:CODE_TESTS_PATH = "$(Get-Location)\client\out\test" 2 | $env:CODE_TESTS_WORKSPACE = "$(Get-Location)\client\testFixture" 3 | 4 | node "$(Get-Location)\client\out\test\runTest" 5 | -------------------------------------------------------------------------------- /server/src/analysis/test/TestLibrary/Constants.mo: -------------------------------------------------------------------------------- 1 | within TestLibrary; 2 | 3 | package Constants 4 | constant Real e = Modelica.Math.exp(1.0); 5 | constant Real pi = 2 * Modelica.Math.asin(1.0); 6 | end Constants; 7 | -------------------------------------------------------------------------------- /client/testFixture/MyLibrary.mo: -------------------------------------------------------------------------------- 1 | package MyLibrary "My Modelica Library" 2 | model M "MWE Modelica Model" 3 | Real x(start = 1.0, fixed = true); 4 | equation 5 | der(x) = -0.5*x; 6 | end M; 7 | end MyLibrary; 8 | -------------------------------------------------------------------------------- /.github/dependabot: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: monthly 12 | -------------------------------------------------------------------------------- /server/src/analysis/test/TestLibrary/TestPackage/TestClass.mo: -------------------------------------------------------------------------------- 1 | within TestLibrary.TestPackage; 2 | 3 | import TestLibrary.Constants.pi; 4 | 5 | function TestClass 6 | input Real twoE = 2 * Constants.e; 7 | input Real tau = 2 * pi; 8 | input Real notTau = tau / twoE; 9 | end TestClass; 10 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | !server/OSMC-License.txt 2 | .eslintignore 3 | .github 4 | .gitignore 5 | .travis.yml 6 | .vscode/** 7 | **/*.map 8 | **/*.ts 9 | **/tsconfig.base.json 10 | **/tsconfig.json 11 | client/ 12 | contributing.md 13 | esbuild.config.js 14 | node_modules/ 15 | scripts/ 16 | server/ 17 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | quoteProps: 'as-needed', 8 | trailingComma: 'all', 9 | bracketSpacing: true, 10 | arrowParens: 'always', 11 | proseWrap: 'preserve', 12 | endOfLine: 'lf', 13 | }; 14 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2020", 5 | "lib": [ 6 | "es2020" 7 | ], 8 | "outDir": "out", 9 | "rootDir": "src", 10 | "sourceMap": true 11 | }, 12 | "include": [ 13 | "src", 14 | "src/test" 15 | ], 16 | "exclude": [ 17 | "node_modules", 18 | ".vscode-test" 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "dbaeumer.vscode-eslint", 7 | "AnHeuermann.metamodelica" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2020", 4 | "target": "es2020", 5 | "lib": [ 6 | "es2020" 7 | ], 8 | "outDir": "out", 9 | "rootDir": "src", 10 | "sourceMap": true 11 | }, 12 | "include": [ 13 | "src" 14 | ], 15 | "exclude": [ 16 | "node_modules", 17 | ".vscode-test" 18 | ], 19 | "references": [ 20 | { 21 | "path": "./client" 22 | }, 23 | { 24 | "path": "./server" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false, 3 | "typescript.tsc.autoDetect": "off", 4 | "typescript.preferences.quoteStyle": "single", 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "explicit" 7 | }, 8 | "prettier.configPath": "./.prettierrc.js", 9 | "cSpell.words": [ 10 | "metamodelica", 11 | "Modelica", 12 | "nodelib", 13 | "OSMC", 14 | "redeclaration" 15 | ], 16 | "files.insertFinalNewline": true, 17 | "files.trimTrailingWhitespace": true, 18 | } 19 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2020", 5 | "lib": [ 6 | "es2020" 7 | ], 8 | "moduleResolution": "node", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "outDir": "out", 13 | "rootDir": "src", 14 | "esModuleInterop": true 15 | }, 16 | "include": [ 17 | "src", 18 | "src/util" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | ".vscode-test", 23 | "src/test", 24 | "src/util/test" 25 | ] 26 | } -------------------------------------------------------------------------------- /server/src/util/index.ts: -------------------------------------------------------------------------------- 1 | import * as url from "node:url"; 2 | import * as LSP from "vscode-languageserver"; 3 | 4 | 5 | export const uriToPath = url.fileURLToPath; 6 | 7 | export function pathToUri(filePath: string): LSP.URI { 8 | const uri = url.pathToFileURL(filePath).href; 9 | 10 | // Note: LSP sends us file uris containing '%3A' instead of ':', but the 11 | // node pathToFileURL uses ':' anyways. We manually fix this here. This is a 12 | // bit hacky but it works. 13 | return uri.slice(0, 5) + uri.slice(5).replace(":", "%3A"); 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /**@type {import('eslint').Linter.Config} */ 2 | // eslint-disable-next-line no-undef 3 | module.exports = { 4 | root: true, 5 | parser: '@typescript-eslint/parser', 6 | plugins: [ 7 | '@typescript-eslint', 8 | ], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | ], 13 | rules: { 14 | 'semi': [2, "always"], 15 | '@typescript-eslint/no-unused-vars': 0, 16 | '@typescript-eslint/no-explicit-any': 0, 17 | '@typescript-eslint/explicit-module-boundary-types': 0, 18 | '@typescript-eslint/no-non-null-assertion': 0, 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /server/src/project/test/TestLibrary 1.0.0/HalfAdder.mo: -------------------------------------------------------------------------------- 1 | within TestLibrary; 2 | 3 | import Modelica.Electrical.Digital.Interfaces.{DigitalInput, DigitalOutput}; 4 | import Modelica.Electrical.Digital.Gates.{AndGate, XorGate}; 5 | 6 | model HalfAdder 7 | DigitalInput a; 8 | DigitalInput b; 9 | DigitalOutput s; 10 | DigitalOutput c; 11 | protected 12 | AndGate andGate; 13 | XorGate xorGate; 14 | equation 15 | connect(andGate,y, c); 16 | connect(xorGate.y, s); 17 | connect(b, andGate.x[1]); 18 | connect(b, xorGate.x[1]); 19 | connect(a, xorGate.x[2]); 20 | connect(a, andGate.x[2]); 21 | end HalfAdder; 22 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modelica-language-server-client", 3 | "description": "VSCode part of a language server", 4 | "author": "Andreas Heuermann, Osman Karabel, Evan Hedbor, PaddiM8", 5 | "license": "OSMC-PL-1-8", 6 | "version": "0.2.0", 7 | "publisher": "vscode", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/OpenModelica/modelica-language-server" 11 | }, 12 | "engines": { 13 | "vscode": "^1.75.0", 14 | "node": "20" 15 | }, 16 | "dependencies": { 17 | "vscode-languageclient": "^8.1.0" 18 | }, 19 | "devDependencies": { 20 | "@types/vscode": "^1.75.1", 21 | "@vscode/test-electron": "^2.3.8" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modelica-language-server-server", 3 | "displayName": "Modelica Language Server", 4 | "description": "[Experimental] Modelica language server.", 5 | "version": "0.2.0", 6 | "author": "Andreas Heuermann, Osman Karabel, Evan Hedbor, PaddiM8", 7 | "license": "OSMC-PL-1-8", 8 | "engines": { 9 | "node": "20" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/OpenModelica/modelica-language-server" 14 | }, 15 | "dependencies": { 16 | "tree-sitter": "^0.21.1", 17 | "vscode-languageserver": "^9.0.1", 18 | "vscode-languageserver-textdocument": "^1.0.11", 19 | "web-tree-sitter": "^0.20.8" 20 | }, 21 | "scripts": {} 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "esbuild", 7 | "group": "build", 8 | "presentation": { 9 | "panel": "dedicated", 10 | "reveal": "never" 11 | }, 12 | "problemMatcher": [ 13 | "$tsc" 14 | ] 15 | }, 16 | { 17 | "type": "npm", 18 | "script": "esbuild-watch", 19 | "isBackground": true, 20 | "group": { 21 | "kind": "build", 22 | "isDefault": true 23 | }, 24 | "presentation": { 25 | "panel": "dedicated", 26 | "reveal": "never" 27 | }, 28 | "problemMatcher": [ 29 | "$tsc-watch" 30 | ] 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /esbuild.config.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | const fs = require('fs'); 3 | 4 | // Build client 5 | esbuild.build({ 6 | entryPoints: [ 7 | './client/src/extension.ts' 8 | ], 9 | bundle: true, 10 | outfile: './out/client.js', 11 | platform: 'node', 12 | external: [ 13 | 'vscode' 14 | ], 15 | format: 'cjs', 16 | tsconfig: './client/tsconfig.json', 17 | }).catch(() => process.exit(1)); 18 | 19 | // Build server 20 | esbuild.build({ 21 | entryPoints: [ 22 | './server/src/server.ts' 23 | ], 24 | bundle: true, 25 | outfile: './out/server.js', 26 | platform: 'node', 27 | external: [ 28 | 'vscode', 29 | ], 30 | format: 'cjs', 31 | tsconfig: './server/tsconfig.json', 32 | }).catch(() => process.exit(1)); 33 | 34 | // Copy tree-sitter.wasm and tree-sitter-modelica.wasm to the output directory 35 | if (!fs.existsSync('out')) { 36 | fs.mkdirSync('out'); 37 | } 38 | fs.copyFileSync('./server/src/tree-sitter-modelica.wasm', './out/tree-sitter-modelica.wasm'); 39 | fs.copyFileSync('./server/node_modules/web-tree-sitter/tree-sitter.wasm', './out/tree-sitter.wasm'); 40 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a 2 | // new window 3 | { 4 | "version": "0.2.0", 5 | "configurations": [ 6 | { 7 | "name": "Launch Client", 8 | "type": "extensionHost", 9 | "request": "launch", 10 | "runtimeExecutable": "${execPath}", 11 | "args": [ 12 | "--extensionDevelopmentPath=${workspaceRoot}" 13 | ], 14 | "outFiles": [ 15 | "${workspaceRoot}/out/**/*.js" 16 | ], 17 | "preLaunchTask": { 18 | "type": "npm", 19 | "script": "esbuild-watch" 20 | } 21 | }, 22 | { 23 | "name": "Language Server E2E Test", 24 | "type": "extensionHost", 25 | "request": "launch", 26 | "runtimeExecutable": "${execPath}", 27 | "args": [ 28 | "--extensionDevelopmentPath=${workspaceRoot}", 29 | "--extensionTestsPath=${workspaceRoot}/client/out/test/index", 30 | "${workspaceRoot}/client/testFixture" 31 | ], 32 | "outFiles": [ 33 | "${workspaceRoot}/client/out/test/**/*.js" 34 | ], 35 | "preLaunchTask": { 36 | "type": "npm", 37 | "script": "test-compile" 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /server/src/util/test/util.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import * as assert from 'assert'; 37 | 38 | import { initializeParser } from '../../parser'; 39 | import * as TreeSitterUtil from '../tree-sitter'; 40 | 41 | describe('getIdentifier', () => { 42 | it('Identifier of type class', async () => { 43 | const parser = await initializeParser(); 44 | const tree = parser.parse('type Temperature = Real(unit = "K ");'); 45 | const classNode = tree.rootNode 46 | .childForFieldName('storedDefinitions')! 47 | .childForFieldName('classDefinition')!; 48 | const name = TreeSitterUtil.getIdentifier(classNode); 49 | 50 | assert.equal(name, 'Temperature'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /client/src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import * as path from 'path'; 37 | 38 | import { runTests } from '@vscode/test-electron'; 39 | 40 | async function main() { 41 | try { 42 | // The folder containing the Extension Manifest package.json 43 | // Passed to `--extensionDevelopmentPath` 44 | const extensionDevelopmentPath = path.resolve(__dirname, '../../../'); 45 | 46 | // The path to test runner 47 | // Passed to --extensionTestsPath 48 | const extensionTestsPath = path.resolve(__dirname, './index'); 49 | 50 | // Download VS Code, unzip it and run the integration test 51 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 52 | } catch (err) { 53 | console.error('Failed to run tests'); 54 | process.exit(1); 55 | } 56 | } 57 | 58 | main(); 59 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*.*.*" 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup npm 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | registry-url: https://registry.npmjs.org/ 25 | 26 | - name: Install X server 27 | run: | 28 | sudo apt-get update 29 | sudo apt-get install -y xvfb 30 | 31 | - name: Install the dependencies 32 | run: npm clean-install && npm run postinstall 33 | 34 | - name: Build package 35 | run: npm run esbuild 36 | 37 | - name: Test language server 38 | run: npm run test:server 39 | 40 | - name: Test language server client 41 | run: | 42 | Xvfb -ac :99 -screen 0 1280x1024x16 & 43 | export DISPLAY=:99 44 | npm run test:e2e 45 | 46 | - name: Package Extension 47 | run: npx vsce package 48 | 49 | - name: Archive vsix package 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: modelica-language-server.vsix 53 | path: modelica-language-server-*.vsix 54 | 55 | release: 56 | if: startsWith(github.ref, 'refs/tags/') 57 | needs: build 58 | runs-on: ubuntu-latest 59 | permissions: 60 | contents: write 61 | steps: 62 | - uses: actions/download-artifact@v4 63 | with: 64 | name: modelica-language-server.vsix 65 | 66 | - name: Release 67 | uses: softprops/action-gh-release@v2 68 | with: 69 | files: | 70 | modelica-language-server-*.vsix 71 | fail_on_unmatched_files: true 72 | generate_release_notes: true 73 | append_body: true 74 | 75 | - name: Publish to Visual Studio Marketplace 76 | if: always() 77 | run: | 78 | npx vsce publish -i $(ls modelica-language-server-*.vsix) -p ${{ secrets.VSCE_PAT }} 79 | 80 | - name: Publish to Open VSX 81 | if: always() 82 | run: | 83 | npx ovsx publish $(ls modelica-language-server-*.vsix) -p ${{ secrets.OPEN_VSX_TOKEN }} 84 | -------------------------------------------------------------------------------- /client/src/getLanguage.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import * as path from 'path'; 37 | import { TextDocument } from 'vscode'; 38 | 39 | type LanguageTypes = 'modelica' | 'metamodelica' | 'unknown'; 40 | 41 | export function getFileExtension(document: TextDocument): string | undefined { 42 | const uri = document.uri; 43 | const filePath = uri.fsPath; 44 | return path.extname(filePath); 45 | } 46 | 47 | function hasMetaModelicaKeywords(content: string): boolean { 48 | const unionRegex = new RegExp('\\b(uniontype)\\s+(\\w+)\\s*(".*")*'); 49 | 50 | return unionRegex.test(content); 51 | } 52 | 53 | /** 54 | * Check if the text document is a Modelica files, MetaModelica file or other. 55 | * @param document Text document. 56 | */ 57 | export function getLanguage(document: TextDocument): LanguageTypes { 58 | // Check 59 | if (hasMetaModelicaKeywords(document.getText())) { 60 | return 'metamodelica'; 61 | } 62 | 63 | return 'modelica'; 64 | } 65 | -------------------------------------------------------------------------------- /client/src/test/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import * as path from 'path'; 37 | import * as Mocha from 'mocha'; 38 | import * as glob from 'glob'; 39 | 40 | export function run(): Promise { 41 | // Create the mocha test 42 | const mocha = new Mocha({ 43 | ui: 'tdd', 44 | color: true, 45 | }); 46 | mocha.timeout(100000); 47 | 48 | const testsRoot = __dirname; 49 | 50 | return new Promise((resolve, reject) => { 51 | glob('**.test.js', { cwd: testsRoot }, (err, files) => { 52 | if (err) { 53 | return reject(err); 54 | } 55 | 56 | // Add files to the test suite 57 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 58 | 59 | try { 60 | // Run the mocha test 61 | mocha.run((failures) => { 62 | if (failures > 0) { 63 | reject(new Error(`${failures} tests failed.`)); 64 | } else { 65 | resolve(); 66 | } 67 | }); 68 | } catch (err) { 69 | console.error(err); 70 | reject(err); 71 | } 72 | }); 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /client/src/test/gotoDeclaration.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import * as vscode from 'vscode'; 37 | import * as assert from 'assert'; 38 | import { getDocUri, activate } from './helper'; 39 | 40 | suite('Goto Declaration', () => { 41 | test('onDeclaration()', async () => { 42 | const docUri = getDocUri('MyLibrary.mo'); 43 | await activate(docUri); 44 | 45 | const position = new vscode.Position(4, 18); 46 | const actualLocations = await vscode.commands.executeCommand( 47 | 'vscode.executeDeclarationProvider', 48 | docUri, 49 | position, 50 | ); 51 | 52 | assert.strictEqual(actualLocations.length, 1); 53 | 54 | const actualLocation = actualLocations[0]; 55 | assert.strictEqual(actualLocation.targetUri.toString(), docUri.toString()); 56 | assert.strictEqual(actualLocation.targetRange.start.line, 2); 57 | assert.strictEqual(actualLocation.targetRange.start.character, 4); 58 | assert.strictEqual(actualLocation.targetRange.end.line, 2); 59 | assert.strictEqual(actualLocation.targetRange.end.character, 37); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /server/src/parser.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | /* ----------------------------------------------------------------------------- 37 | * Taken from bash-language-server and adapted to Modelica language server 38 | * https://github.com/bash-lsp/bash-language-server/blob/main/server/src/parser.ts 39 | * ----------------------------------------------------------------------------- 40 | */ 41 | 42 | import Parser from 'web-tree-sitter'; 43 | import * as fs from 'fs'; 44 | import * as path from 'path'; 45 | 46 | /** 47 | * Initialize tree-sitter parser and load Modelica language. 48 | * 49 | * @returns tree-sitter-modelica parser 50 | */ 51 | export async function initializeParser(): Promise { 52 | await Parser.init(); 53 | const parser = new Parser(); 54 | 55 | const modelicaWasmFile = path.join(__dirname, 'tree-sitter-modelica.wasm'); 56 | if (!fs.existsSync(modelicaWasmFile)) { 57 | throw new Error(`Can't find 'tree-sitter-modelica.wasm' at ${modelicaWasmFile}`); 58 | } 59 | 60 | const Modelica = await Parser.Language.load(modelicaWasmFile); 61 | parser.setLanguage(Modelica); 62 | 63 | return parser; 64 | } 65 | -------------------------------------------------------------------------------- /client/src/test/helper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import * as vscode from 'vscode'; 37 | import * as path from 'path'; 38 | 39 | export let doc: vscode.TextDocument; 40 | export let editor: vscode.TextEditor; 41 | export let documentEol: string; 42 | export let platformEol: string; 43 | 44 | /** 45 | * Activates the OpenModelica.modelica-language-server extension 46 | */ 47 | export async function activate(docUri: vscode.Uri) { 48 | // The extensionId is `publisher.name` from package.json 49 | const ext = vscode.extensions.getExtension('OpenModelica.modelica-language-server')!; 50 | await ext.activate(); 51 | try { 52 | doc = await vscode.workspace.openTextDocument(docUri); 53 | editor = await vscode.window.showTextDocument(doc); 54 | await sleep(5000); // Wait for server activation 55 | } catch (e) { 56 | console.error(e); 57 | } 58 | } 59 | 60 | async function sleep(ms: number) { 61 | return new Promise((resolve) => setTimeout(resolve, ms)); 62 | } 63 | 64 | export const getDocPath = (p: string) => { 65 | return path.resolve(__dirname, '../../testFixture', p); 66 | }; 67 | 68 | export const getDocUri = (p: string) => { 69 | return vscode.Uri.file(getDocPath(p)); 70 | }; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modelica-language-server", 3 | "displayName": "Modelica Language Server", 4 | "description": "[Experimental] Modelica language server", 5 | "version": "0.2.0", 6 | "author": "Andreas Heuermann, Osman Karabel, Evan Hedbor, PaddiM8", 7 | "license": "OSMC-PL-1-8", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/OpenModelica/modelica-language-server" 11 | }, 12 | "publisher": "OpenModelica", 13 | "categories": [ 14 | "Programming Languages" 15 | ], 16 | "keywords": [ 17 | "modelica", 18 | "language", 19 | "language-server" 20 | ], 21 | "homepage": "https://github.com/OpenModelica/modelica-language-server", 22 | "icon": "images/Modelica_Language_margin.jpg", 23 | "bugs": "https://github.com/OpenModelica/modelica-language-server/issues", 24 | "engines": { 25 | "vscode": "^1.75.0" 26 | }, 27 | "activationEvents": [ 28 | "onLanguage:modelica" 29 | ], 30 | "main": "./out/client", 31 | "contributes": { 32 | "languages": [ 33 | { 34 | "id": "modelica", 35 | "aliases": [ 36 | "Modelica", 37 | "modelica" 38 | ], 39 | "extensions": [ 40 | ".mo" 41 | ] 42 | } 43 | ] 44 | }, 45 | "scripts": { 46 | "vscode:prepublish": "npm run esbuild-base -- --minify", 47 | "esbuild-base": "node esbuild.config.js", 48 | "esbuild": "npm run esbuild-base -- --sourcemap", 49 | "esbuild-watch": "npm run esbuild-base -- --sourcemap --watch", 50 | "test-compile": "tsc -b ./", 51 | "lint": "eslint ./client/src ./server/src --ext .ts,.tsx", 52 | "postinstall": "cd client && npm install && cd ../server && npm install && cd ..", 53 | "test:e2e": "run-script-os", 54 | "test:e2e:win32": "npm run test-compile && powershell -File ./scripts/e2e.ps1", 55 | "test:e2e:default": "npm run test-compile && sh ./scripts/e2e.sh", 56 | "test:server": "cd server && npx mocha -r ts-node/register src/test/**/*.test.ts src/project/test/**/*.test.ts src/util/test/**/*.test.ts src/analysis/test/**/*.test.ts", 57 | "all": "npm run postinstall && npm run esbuild && npm run lint && npm run test:server && npm run test:e2e && npm run vscode:prepublish" 58 | }, 59 | "devDependencies": { 60 | "@types/mocha": "^10.0.6", 61 | "@types/node": "^20.10.4", 62 | "@typescript-eslint/eslint-plugin": "^6.13.2", 63 | "@typescript-eslint/parser": "^6.13.2", 64 | "esbuild": "^0.20.0", 65 | "eslint": "^8.55.0", 66 | "mocha": "^10.2.0", 67 | "run-script-os": "^1.1.6", 68 | "ts-node": "^10.9.1", 69 | "typescript": "^5.3.3" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /server/src/util/test/declarations.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import * as assert from 'assert'; 37 | import * as LSP from 'vscode-languageserver/node'; 38 | 39 | import { initializeParser } from '../../parser'; 40 | import { getAllDeclarationsInTree, nodeToSymbolInformation } from '../declarations'; 41 | 42 | const modelicaTestString = ` 43 | model M "Description" 44 | end M; 45 | 46 | function foo 47 | end foo; 48 | 49 | type Temperature = Real(unit = "K"); 50 | `; 51 | 52 | const expectedDefinitions = ['M', 'foo', 'Temperature']; 53 | const expectedTypes = [LSP.SymbolKind.Class, LSP.SymbolKind.Function, LSP.SymbolKind.TypeParameter]; 54 | 55 | describe('nodeToSymbolInformation', () => { 56 | it('type to TypeParameter', async () => { 57 | const parser = await initializeParser(); 58 | const tree = parser.parse('type Temperature = Real(unit = "K ");'); 59 | 60 | const classNode = tree.rootNode 61 | .childForFieldName('storedDefinitions')! 62 | .childForFieldName('classDefinition')!; 63 | const symbol = nodeToSymbolInformation(classNode, 'file.mo'); 64 | 65 | assert.equal(symbol?.name, 'Temperature'); 66 | assert.equal(symbol?.kind, LSP.SymbolKind.TypeParameter); 67 | }); 68 | }); 69 | 70 | describe('getAllDeclarationsInTree', () => { 71 | it('Definitions and types', async () => { 72 | const parser = await initializeParser(); 73 | const tree = parser.parse(modelicaTestString); 74 | const symbols = getAllDeclarationsInTree(tree, 'file.mo'); 75 | 76 | const definitions: string[] = []; 77 | const types: LSP.SymbolKind[] = []; 78 | for (let i = 0; i < symbols.length; i++) { 79 | definitions.push(symbols[i].name); 80 | types.push(symbols[i].kind); 81 | } 82 | 83 | assert.deepEqual(definitions, expectedDefinitions); 84 | assert.deepEqual(types, expectedTypes); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /server/src/test/server.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import * as Mocha from 'mocha'; 37 | import * as assert from 'assert'; 38 | import * as Parser from 'web-tree-sitter'; 39 | 40 | import { initializeParser } from '../parser'; 41 | 42 | const modelicaTestString = ` 43 | model M "Hello World Modelica" 44 | Real x(start=1,fixed=true) "state"; 45 | equations 46 | der(x) = -0.5*x; 47 | end M; 48 | `; 49 | const parsedModelicaTestString = 50 | '(stored_definitions storedDefinitions: (stored_definition classDefinition: (class_definition classPrefixes: (class_prefixes) classSpecifier: (long_class_specifier identifier: (IDENT) descriptionString: (description_string value: (STRING)) (element_list element: (named_element componentClause: (component_clause typeSpecifier: (type_specifier name: (name identifier: (IDENT))) componentDeclarations: (component_list componentDeclaration: (component_declaration declaration: (declaration identifier: (IDENT) modification: (modification classModification: (class_modification arguments: (argument_list argument: (element_modification name: (name identifier: (IDENT)) modification: (modification expression: (expression (simple_expression (primary_expression (literal_expression (unsigned_integer_literal_expression (UNSIGNED_INTEGER)))))))) argument: (element_modification name: (name identifier: (IDENT)) modification: (modification expression: (expression (simple_expression (primary_expression (literal_expression (logical_literal_expression))))))))))) descriptionString: (description_string value: (STRING)))))) element: (named_element componentClause: (component_clause typeSpecifier: (type_specifier name: (name identifier: (IDENT))) componentDeclarations: (component_list componentDeclaration: (component_declaration declaration: (declaration identifier: (IDENT) modification: (modification classModification: (class_modification arguments: (argument_list argument: (element_modification name: (name identifier: (IDENT))))) expression: (expression (simple_expression (binary_expression operand1: (simple_expression (unary_expression operand: (simple_expression (primary_expression (literal_expression (unsigned_real_literal_expression (UNSIGNED_REAL))))))) operand2: (simple_expression (primary_expression (component_reference identifier: (IDENT)))))))))))))) endIdentifier: (IDENT)))))'; 51 | 52 | describe('Modelica tree-sitter parser', () => { 53 | it('Initialize parser', async () => { 54 | const parser = await initializeParser(); 55 | }); 56 | 57 | it('Parse string', async () => { 58 | const parser = await initializeParser(); 59 | const tree = parser.parse(modelicaTestString); 60 | const parsedString = tree.rootNode.toString(); 61 | assert.equal(parsedString, parsedModelicaTestString); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modelica-language-server-server", 3 | "version": "0.2.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "modelica-language-server-server", 9 | "version": "0.2.0", 10 | "license": "OSMC-PL-1-8", 11 | "dependencies": { 12 | "tree-sitter": "^0.21.1", 13 | "vscode-languageserver": "^9.0.1", 14 | "vscode-languageserver-textdocument": "^1.0.11", 15 | "web-tree-sitter": "^0.20.8" 16 | }, 17 | "engines": { 18 | "node": "20" 19 | } 20 | }, 21 | "node_modules/node-addon-api": { 22 | "version": "8.0.0", 23 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.0.0.tgz", 24 | "integrity": "sha512-ipO7rsHEBqa9STO5C5T10fj732ml+5kLN1cAG8/jdHd56ldQeGj3Q7+scUS+VHK/qy1zLEwC4wMK5+yM0btPvw==", 25 | "engines": { 26 | "node": "^18 || ^20 || >= 21" 27 | } 28 | }, 29 | "node_modules/node-gyp-build": { 30 | "version": "4.8.1", 31 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", 32 | "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", 33 | "bin": { 34 | "node-gyp-build": "bin.js", 35 | "node-gyp-build-optional": "optional.js", 36 | "node-gyp-build-test": "build-test.js" 37 | } 38 | }, 39 | "node_modules/tree-sitter": { 40 | "version": "0.21.1", 41 | "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", 42 | "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", 43 | "hasInstallScript": true, 44 | "dependencies": { 45 | "node-addon-api": "^8.0.0", 46 | "node-gyp-build": "^4.8.0" 47 | } 48 | }, 49 | "node_modules/vscode-jsonrpc": { 50 | "version": "8.2.0", 51 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", 52 | "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", 53 | "engines": { 54 | "node": ">=14.0.0" 55 | } 56 | }, 57 | "node_modules/vscode-languageserver": { 58 | "version": "9.0.1", 59 | "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", 60 | "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", 61 | "dependencies": { 62 | "vscode-languageserver-protocol": "3.17.5" 63 | }, 64 | "bin": { 65 | "installServerIntoExtension": "bin/installServerIntoExtension" 66 | } 67 | }, 68 | "node_modules/vscode-languageserver-protocol": { 69 | "version": "3.17.5", 70 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", 71 | "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", 72 | "dependencies": { 73 | "vscode-jsonrpc": "8.2.0", 74 | "vscode-languageserver-types": "3.17.5" 75 | } 76 | }, 77 | "node_modules/vscode-languageserver-textdocument": { 78 | "version": "1.0.11", 79 | "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz", 80 | "integrity": "sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==" 81 | }, 82 | "node_modules/vscode-languageserver-types": { 83 | "version": "3.17.5", 84 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", 85 | "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" 86 | }, 87 | "node_modules/web-tree-sitter": { 88 | "version": "0.20.8", 89 | "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz", 90 | "integrity": "sha512-weOVgZ3aAARgdnb220GqYuh7+rZU0Ka9k9yfKtGAzEYMa6GgiCzW9JjQRJyCJakvibQW+dfjJdihjInKuuCAUQ==" 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /client/src/extension.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import * as path from 'path'; 37 | import * as fs from 'fs'; 38 | import { languages, workspace, ExtensionContext, TextDocument } from 'vscode'; 39 | import { 40 | LanguageClient, 41 | LanguageClientOptions, 42 | ServerOptions, 43 | TransportKind, 44 | } from 'vscode-languageclient/node'; 45 | import { getFileExtension, getLanguage } from './getLanguage'; 46 | import { fstat } from 'fs'; 47 | 48 | let client: LanguageClient; 49 | 50 | export function activate(context: ExtensionContext) { 51 | // Register event listener to set language for '.mo' files. 52 | const checkedFiles: { [id: string]: boolean } = {}; 53 | workspace.onDidOpenTextDocument((document: TextDocument) => { 54 | if (checkedFiles[document.fileName]) { 55 | return; 56 | } 57 | 58 | checkedFiles[document.fileName] = true; 59 | if (getFileExtension(document) == '.mo') { 60 | const lang = getLanguage(document); 61 | 62 | switch (lang) { 63 | case 'modelica': 64 | languages.setTextDocumentLanguage(document, 'modelica'); 65 | break; 66 | case 'metamodelica': 67 | languages.setTextDocumentLanguage(document, 'metamodelica'); 68 | break; 69 | default: 70 | break; 71 | } 72 | } 73 | }); 74 | 75 | // The server is implemented in node, point to packed module 76 | const serverModule = context.asAbsolutePath(path.join('out', 'server.js')); 77 | if (!fs.existsSync(serverModule)) { 78 | throw new Error(`Can't find server module in ${serverModule}`); 79 | } 80 | 81 | // If the extension is launched in debug mode then the debug server options are used 82 | // Otherwise the run options are used 83 | const serverOptions: ServerOptions = { 84 | run: { module: serverModule, transport: TransportKind.ipc }, 85 | debug: { 86 | module: serverModule, 87 | transport: TransportKind.ipc, 88 | }, 89 | }; 90 | 91 | // Options to control the language client 92 | const clientOptions: LanguageClientOptions = { 93 | // Register the server for modelica text documents 94 | documentSelector: [ 95 | { 96 | language: 'modelica', 97 | scheme: 'file', 98 | }, 99 | ], 100 | synchronize: { 101 | // Notify the server about file changes to '.clientrc files contained in the workspace 102 | fileEvents: workspace.createFileSystemWatcher('**/.clientrc'), 103 | }, 104 | }; 105 | 106 | // Create the language client and start the client. 107 | client = new LanguageClient( 108 | 'modelicaLanguageServer', 109 | 'Modelica Language Server', 110 | serverOptions, 111 | clientOptions, 112 | ); 113 | 114 | // Start the client. This will also launch the server 115 | client.start(); 116 | } 117 | 118 | export function deactivate(): Thenable | undefined { 119 | if (!client) { 120 | return undefined; 121 | } 122 | return client.stop(); 123 | } 124 | -------------------------------------------------------------------------------- /server/src/project/library.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import * as LSP from 'vscode-languageserver'; 37 | import * as fsWalk from '@nodelib/fs.walk'; 38 | import * as path from 'node:path'; 39 | import * as util from 'node:util'; 40 | 41 | import { logger } from '../util/logger'; 42 | import { ModelicaDocument } from './document'; 43 | import { ModelicaProject } from './project'; 44 | 45 | export class ModelicaLibrary { 46 | readonly #project: ModelicaProject; 47 | readonly #documents: Map; 48 | readonly #isWorkspace: boolean; 49 | readonly #name: string; 50 | #path: string; 51 | 52 | public constructor( 53 | project: ModelicaProject, 54 | libraryPath: string, 55 | isWorkspace: boolean, 56 | name?: string, 57 | ) { 58 | this.#project = project; 59 | (this.#path = libraryPath), (this.#documents = new Map()); 60 | this.#isWorkspace = isWorkspace; 61 | // Path basename could contain version seperated by whitespace 62 | this.#name = name ?? path.basename(this.path).split(/\s/)[0]; 63 | } 64 | 65 | /** 66 | * Loads a library and all of its {@link ModelicaDocument}s. 67 | * 68 | * @param project the containing project 69 | * @param libraryPath the path to the library 70 | * @param isWorkspace `true` if this is a user workspace 71 | * @returns the loaded library 72 | */ 73 | public static async load( 74 | project: ModelicaProject, 75 | libraryPath: string, 76 | isWorkspace: boolean, 77 | ): Promise { 78 | logger.info(`Loading ${isWorkspace ? 'workspace' : 'library'} at '${libraryPath}'...`); 79 | 80 | const library = new ModelicaLibrary(project, libraryPath, isWorkspace); 81 | const workspaceRootDocument = await ModelicaDocument.load( 82 | project, 83 | library, 84 | path.join(libraryPath, 'package.mo'), 85 | ); 86 | 87 | // Find the root path of the library and update library.#path. 88 | // It might have been set incorrectly if we opened a child folder. 89 | for (let i = 0; i < workspaceRootDocument.within.length; i++) { 90 | library.#path = path.dirname(library.#path); 91 | } 92 | 93 | logger.debug(`Set library path to ${library.path}`); 94 | 95 | const walk = util.promisify(fsWalk.walk); 96 | const entries = await walk(library.#path, { 97 | entryFilter: (entry) => !!entry.name.match(/.*\.mo/) && !entry.dirent.isDirectory(), 98 | }); 99 | 100 | for (const entry of entries) { 101 | const document = await ModelicaDocument.load(project, library, entry.path); 102 | library.#documents.set(entry.path, document); 103 | } 104 | 105 | logger.debug(`Loaded ${library.#documents.size} documents`); 106 | return library; 107 | } 108 | 109 | public get name(): string { 110 | return this.#name; 111 | } 112 | 113 | public get path(): string { 114 | return this.#path; 115 | } 116 | 117 | public get project(): ModelicaProject { 118 | return this.#project; 119 | } 120 | 121 | public get documents(): Map { 122 | return this.#documents; 123 | } 124 | 125 | public get isWorkspace(): boolean { 126 | return this.#isWorkspace; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modelica Language Server 2 | 3 | [![Build](https://github.com/OpenModelica/modelica-language-server/actions/workflows/test.yml/badge.svg)](https://github.com/OpenModelica/modelica-language-server/actions/workflows/test.yml) 4 | 5 | A very early version of a Modelica Language Server based on 6 | [OpenModelica/tree-sitter-modelica](https://github.com/OpenModelica/tree-sitter-modelica). 7 | 8 | For syntax highlighting install enxtension 9 | [AnHeuermann.metamodelica](https://marketplace.visualstudio.com/items?itemName=AnHeuermann.metamodelica) 10 | in addition. 11 | 12 | ## Functionality 13 | 14 | This Language Server works for Modelica files. It has the following language 15 | features: 16 | 17 | - Provide Outline of Modelica files. 18 | 19 | ![Outline](images/outline_demo.png) 20 | 21 | - Goto declarations. 22 | 23 | ![Goto Declaration](images/goto_declaration_demo.png) 24 | 25 | ## Installation 26 | 27 | ### Via Marketplace 28 | 29 | - [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=OpenModelica.modelica-language-server) 30 | - [Open VSX Registry](https://open-vsx.org/extension/OpenModelica/modelica-language-server) 31 | 32 | ### Via VSIX File 33 | 34 | Download the latest 35 | [modelica-language-server-0.2.0.vsix](https://github.com/OpenModelica/modelica-language-server/releases/download/v0.2.0/modelica-language-server-0.2.0.vsix) 36 | from the 37 | [releases](https://github.com/OpenModelica/modelica-language-server/releases) 38 | page. 39 | 40 | Check the [VS Code documentation](https://code.visualstudio.com/docs/editor/extension-marketplace#_install-from-a-vsix) 41 | on how to install a .vsix file. 42 | Use the `Install from VSIX` command or run 43 | 44 | ```bash 45 | code --install-extension modelica-language-server-0.2.0.vsix 46 | ``` 47 | 48 | ## Contributing ❤️ 49 | 50 | Contributions are very welcome! 51 | 52 | We made the first tiny step but need help to add more features and refine the 53 | language server. 54 | 55 | If you are searching for a good point to start 56 | check the 57 | [good first issue](https://github.com/OpenModelica/modelica-language-server/labels/good%20first%20issue). 58 | To see where the development is heading to check the 59 | [Projects section](https://github.com/OpenModelica/modelica-language-server/projects?query=is%3Aopen). 60 | If you need more information start a discussion over at 61 | [OpenModelica/OpenModelica](https://github.com/OpenModelica/OpenModelica). 62 | 63 | Found a bug or having issues? Open a 64 | [new issue](https://github.com/OpenModelica/modelica-language-server/issues/new/choose). 65 | 66 | ## Structure 67 | 68 | ``` 69 | . 70 | ├── client // Language Client 71 | │ ├── src 72 | │ │ ├── test // End to End tests for Language Client / Server 73 | │ │ └── extension.ts // Language Client entry point 74 | ├── package.json // The extension manifest. 75 | └── server // Modelica Language Server 76 | └── src 77 | └── server.ts // Language Server entry point 78 | ``` 79 | 80 | ## Building the Language Server 81 | 82 | - Run `npm install` and `npm run postinstall` in this folder.This installs all 83 | necessary npm modules in both the client and server folder 84 | - Open VS Code on this folder. 85 | - Press Ctrl+Shift+B to start compiling the client and server in [watch 86 | mode](https://code.visualstudio.com/docs/editor/tasks#:~:text=The%20first%20entry%20executes,the%20HelloWorld.js%20file.). 87 | - Switch to the Run and Debug View in the Sidebar (Ctrl+Shift+D). 88 | - Select `Launch Client` from the drop down (if it is not already). 89 | - Press ▷ to run the launch config (F5). 90 | - In the [Extension Development 91 | Host](https://code.visualstudio.com/api/get-started/your-first-extension#:~:text=Then%2C%20inside%20the%20editor%2C%20press%20F5.%20This%20will%20compile%20and%20run%20the%20extension%20in%20a%20new%20Extension%20Development%20Host%20window.) 92 | instance of VSCode, open a document in 'modelica' language mode. 93 | - Check the console output of `Language Server Modelica` to see the parsed 94 | tree of the opened file. 95 | 96 | ## Build and Install Extension 97 | 98 | ``` 99 | npx vsce package 100 | ``` 101 | 102 | ## License 103 | 104 | modelica-language-server is licensed under the OSMC Public License v1.8, see 105 | [OSMC-License.txt](./OSMC-License.txt). 106 | 107 | ### 3rd Party Licenses 108 | 109 | This extension is based on 110 | [https://github.com/microsoft/vscode-extension-samples/tree/main/lsp-sample](https://github.com/microsoft/vscode-extension-samples/tree/main/lsp-sample), 111 | licensed under MIT license. 112 | 113 | Some parts of the source code are taken from 114 | [bash-lsp/bash-language-server](https://github.com/bash-lsp/bash-language-server), 115 | licensed under the MIT license and adapted to the Modelica language server. 116 | 117 | [OpenModelica/tree-sitter-modelica](https://github.com/OpenModelica/tree-sitter-modelica) 118 | v0.2.0 is included in this extension and is licensed under the [OSMC-PL 119 | v1.8](./server/OSMC-License.txt). 120 | 121 | ## Acknowledgments 122 | 123 | This package was initially developed by 124 | [Hochschule Bielefeld - University of Applied Sciences and Arts](hsbi.de). 125 | -------------------------------------------------------------------------------- /server/src/project/test/project.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import Parser from 'web-tree-sitter'; 37 | import { ModelicaProject, ModelicaLibrary } from '..'; 38 | import assert from 'node:assert/strict'; 39 | import path from 'node:path'; 40 | import { initializeParser } from '../../parser'; 41 | 42 | const TEST_LIBRARY_PATH = path.join(__dirname, 'TestLibrary 1.0.0'); 43 | const TEST_PACKAGE_PATH = path.join(TEST_LIBRARY_PATH, 'package.mo'); 44 | const TEST_CLASS_PATH = path.join(TEST_LIBRARY_PATH, 'HalfAdder.mo'); 45 | 46 | const TEST_PACKAGE_CONTENT = `package TestLibrary 47 | annotation(version="1.0.0"); 48 | end TestLibrary; 49 | `; 50 | 51 | describe('ModelicaProject', () => { 52 | describe('an empty project', () => { 53 | let project: ModelicaProject; 54 | 55 | beforeEach(async () => { 56 | const parser = await initializeParser(); 57 | project = new ModelicaProject(parser); 58 | }); 59 | 60 | it('should have no libraries', () => { 61 | assert.equal(project.libraries.length, 0); 62 | }); 63 | 64 | it('updating and deleting documents does nothing', async () => { 65 | assert(!await project.updateDocument(TEST_CLASS_PATH, 'file content')); 66 | assert(!await project.removeDocument(TEST_CLASS_PATH)); 67 | }); 68 | }); 69 | 70 | describe('when adding a library', async () => { 71 | let project: ModelicaProject; 72 | let library: ModelicaLibrary; 73 | 74 | beforeEach(async () => { 75 | const parser = await initializeParser(); 76 | project = new ModelicaProject(parser); 77 | library = await ModelicaLibrary.load(project, TEST_LIBRARY_PATH, false); 78 | project.addLibrary(library); 79 | }); 80 | 81 | it('should add the library', () => { 82 | assert.equal(project.libraries.length, 1); 83 | assert.equal(project.libraries[0], library); 84 | assert.equal(project.libraries[0].name, "TestLibrary"); 85 | }); 86 | 87 | it('should add all the documents in the library', async () => { 88 | assert.notEqual(await project.getDocument(TEST_PACKAGE_PATH), undefined); 89 | assert.notEqual(await project.getDocument(TEST_CLASS_PATH), undefined); 90 | 91 | assert.equal( 92 | library.documents.get(TEST_PACKAGE_PATH), 93 | await project.getDocument(TEST_PACKAGE_PATH), 94 | ); 95 | assert.equal(library.documents.get(TEST_CLASS_PATH), await project.getDocument(TEST_CLASS_PATH)); 96 | }); 97 | 98 | it('repeatedly adding documents has no effect', async () => { 99 | for (let i = 0; i < 5; i++) { 100 | assert(!(await project.addDocument(TEST_PACKAGE_PATH))); 101 | assert(!(await project.addDocument(TEST_CLASS_PATH))); 102 | } 103 | }); 104 | 105 | it('documents can be updated', async () => { 106 | const document = (await project.getDocument(TEST_PACKAGE_PATH))!; 107 | assert.equal( 108 | document.getText().replace(/\r\n/g, '\n'), 109 | TEST_PACKAGE_CONTENT.replace(/\r\n/g, '\n'), 110 | ); 111 | 112 | const newContent = `within; 113 | 114 | package TestLibrary 115 | annotation(version="1.0.1"); 116 | end TestLibrary; 117 | `; 118 | assert(await project.updateDocument(document.path, newContent)); 119 | assert.equal(document.getText(), newContent); 120 | }); 121 | 122 | it('documents can be removed (and re-added)', async () => { 123 | assert.notEqual(await project.getDocument(TEST_CLASS_PATH), undefined); 124 | 125 | assert(await project.removeDocument(TEST_CLASS_PATH)); 126 | 127 | // no effect -- already removed 128 | assert(!await project.removeDocument(TEST_CLASS_PATH)); 129 | 130 | // can re-add document without issues 131 | assert(await project.addDocument(TEST_CLASS_PATH)); 132 | assert.notEqual(await project.getDocument(TEST_CLASS_PATH), undefined); 133 | }); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /server/src/util/declarations.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | /* -------------------------------------------------------------------------------------------- 37 | * Taken from bash-language-server and adapted to Modelica language server 38 | * https://github.com/bash-lsp/bash-language-server/blob/main/server/src/util/declarations.ts 39 | * ------------------------------------------------------------------------------------------ */ 40 | 41 | import * as LSP from 'vscode-languageserver/node'; 42 | import * as Parser from 'web-tree-sitter'; 43 | 44 | import * as TreeSitterUtil from './tree-sitter'; 45 | import { logger } from './logger'; 46 | 47 | const isEmpty = (data: string): boolean => typeof data === 'string' && data.trim().length == 0; 48 | 49 | export type GlobalDeclarations = { [word: string]: LSP.SymbolInformation }; 50 | export type Declarations = { [word: string]: LSP.SymbolInformation[] }; 51 | 52 | const GLOBAL_DECLARATION_LEAF_NODE_TYPES = new Set(['if_statement', 'function_definition']); 53 | 54 | /** 55 | * Returns all declarations (functions or variables) from a given tree. 56 | * 57 | * @param tree Tree-sitter tree. 58 | * @param uri The document's uri. 59 | * @returns Symbol information for all declarations. 60 | */ 61 | export function getAllDeclarationsInTree(tree: Parser.Tree, uri: string): LSP.SymbolInformation[] { 62 | const symbols: LSP.SymbolInformation[] = []; 63 | 64 | TreeSitterUtil.forEach(tree.rootNode, (node) => { 65 | const symbol = getDeclarationSymbolFromNode(node, uri); 66 | if (symbol) { 67 | symbols.push(symbol); 68 | } 69 | }); 70 | 71 | return symbols; 72 | } 73 | 74 | /** 75 | * Converts node to symbol information. 76 | * 77 | * @param tree Tree-sitter tree. 78 | * @param uri The document's uri. 79 | * @returns Symbol information from node. 80 | */ 81 | export function nodeToSymbolInformation( 82 | node: Parser.SyntaxNode, 83 | uri: string, 84 | ): LSP.SymbolInformation | null { 85 | const named = node.firstNamedChild; 86 | 87 | if (named === null) { 88 | return null; 89 | } 90 | 91 | const name = TreeSitterUtil.getIdentifier(node); 92 | if (name === undefined || isEmpty(name)) { 93 | return null; 94 | } 95 | 96 | const kind = getKind(node); 97 | 98 | const containerName = 99 | TreeSitterUtil.findParent(node, (p) => p.type === 'function_definition')?.firstNamedChild 100 | ?.text || ''; 101 | 102 | return LSP.SymbolInformation.create( 103 | name, 104 | kind || LSP.SymbolKind.Variable, 105 | TreeSitterUtil.range(node), 106 | uri, 107 | containerName, 108 | ); 109 | } 110 | 111 | /** 112 | * Get declaration from node and convert to symbol information. 113 | * 114 | * @param node Root node of tree. 115 | * @param uri The associated URI for this document. 116 | * @returns LSP symbol information for definition. 117 | */ 118 | function getDeclarationSymbolFromNode( 119 | node: Parser.SyntaxNode, 120 | uri: string, 121 | ): LSP.SymbolInformation | null { 122 | if (TreeSitterUtil.isDefinition(node)) { 123 | return nodeToSymbolInformation(node, uri); 124 | } 125 | 126 | return null; 127 | } 128 | 129 | /** 130 | * Returns symbol kind from class definition node. 131 | * 132 | * @param node Node containing class_definition 133 | * @returns Symbol kind or `undefined`. 134 | */ 135 | function getKind(node: Parser.SyntaxNode): LSP.SymbolKind | undefined { 136 | const classPrefixes = TreeSitterUtil.getClassPrefixes(node)?.split(/\s+/); 137 | if (classPrefixes === undefined) { 138 | return undefined; 139 | } 140 | 141 | switch (classPrefixes[classPrefixes.length - 1]) { 142 | case 'block': 143 | case 'class': 144 | case 'connector': 145 | case 'model': 146 | return LSP.SymbolKind.Class; 147 | case 'function': 148 | case 'operator': 149 | return LSP.SymbolKind.Function; 150 | case 'package': 151 | case 'record': 152 | return LSP.SymbolKind.Package; 153 | case 'type': 154 | return LSP.SymbolKind.TypeParameter; 155 | default: 156 | return undefined; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /server/src/project/test/document.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import * as assert from 'node:assert/strict'; 37 | import * as path from 'node:path'; 38 | import { ModelicaProject, ModelicaLibrary, ModelicaDocument } from '..'; 39 | import { TextDocument } from 'vscode-languageserver-textdocument'; 40 | import { initializeParser } from '../../parser'; 41 | import { pathToUri } from '../../util'; 42 | 43 | // Fake directory path 44 | const TEST_PACKAGE_ROOT = path.join(__dirname, 'TestPackage'); 45 | const TEST_PACKAGE_CONTENT = `package TestPackage 46 | annotation(version="1.0.0"); 47 | end Test; 48 | `; 49 | const UPDATED_TEST_PACKAGE_CONTENT = `package TestPackage 50 | annotation(version="1.0.1"); 51 | end Test; 52 | `; 53 | const TEST_CLASS_CONTENT = `within TestPackage.Foo.Bar; 54 | 55 | class Frobnicator 56 | end Frobnicator; 57 | `; 58 | 59 | function createTextDocument(filePath: string, content: string): TextDocument { 60 | const absolutePath = path.join(TEST_PACKAGE_ROOT, filePath); 61 | const uri = pathToUri(absolutePath); 62 | return TextDocument.create(uri, 'modelica', 0, content); 63 | } 64 | 65 | describe('ModelicaDocument', () => { 66 | let project: ModelicaProject; 67 | let library: ModelicaLibrary; 68 | 69 | beforeEach(async () => { 70 | const parser = await initializeParser(); 71 | project = new ModelicaProject(parser); 72 | project.addLibrary(library); 73 | library = new ModelicaLibrary(project, TEST_PACKAGE_ROOT, true); 74 | }); 75 | 76 | it('can update the entire document', () => { 77 | const textDocument = createTextDocument('.', TEST_PACKAGE_CONTENT); 78 | const tree = project.parser.parse(TEST_PACKAGE_CONTENT); 79 | const document = new ModelicaDocument(project, library, textDocument, tree); 80 | document.update(UPDATED_TEST_PACKAGE_CONTENT); 81 | 82 | assert.equal(document.getText().trim(), UPDATED_TEST_PACKAGE_CONTENT.trim()); 83 | }); 84 | 85 | it('can update incrementally', () => { 86 | const textDocument = createTextDocument('.', TEST_PACKAGE_CONTENT); 87 | const tree = project.parser.parse(TEST_PACKAGE_CONTENT); 88 | const document = new ModelicaDocument(project, library, textDocument, tree); 89 | document.update( 90 | '1.0.1', 91 | { 92 | start: { 93 | line: 1, 94 | character: 22, 95 | }, 96 | end: { 97 | line: 1, 98 | character: 27, 99 | }, 100 | } 101 | ); 102 | 103 | assert.equal(document.getText().trim(), UPDATED_TEST_PACKAGE_CONTENT.trim()); 104 | 105 | document.update( 106 | '\n model A\n end A;', 107 | { 108 | start: { 109 | line: 1, 110 | character: 30, 111 | }, 112 | end: { 113 | line: 1, 114 | character: 30, 115 | }, 116 | } 117 | ); 118 | 119 | const model = document.tree.rootNode.descendantsOfType("class_definition")[1]; 120 | assert.equal(model.type, "class_definition"); 121 | assert.equal(model.descendantsOfType("IDENT")[0].text, "A"); 122 | assert.equal(document.tree.rootNode.descendantsOfType("annotation_clause").length, 1); 123 | }); 124 | 125 | it('a file with no `within` clause has the correct package path', () => { 126 | const textDocument = createTextDocument('./package.mo', TEST_PACKAGE_CONTENT); 127 | const tree = project.parser.parse(TEST_PACKAGE_CONTENT); 128 | const document = new ModelicaDocument(project, library, textDocument, tree); 129 | 130 | assert.deepEqual(document.within, []); 131 | }); 132 | 133 | it('a file with a `within` clause has the correct package path', () => { 134 | const textDocument = createTextDocument('./Foo/Bar/Frobnicator.mo', TEST_CLASS_CONTENT); 135 | const tree = project.parser.parse(TEST_CLASS_CONTENT); 136 | const document = new ModelicaDocument(project, library, textDocument, tree); 137 | 138 | assert.deepEqual(document.within, ['TestPackage', 'Foo', 'Bar']); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /client/src/test/symbolinformation.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import * as vscode from 'vscode'; 37 | import * as assert from 'assert'; 38 | import { getDocUri, activate } from './helper'; 39 | 40 | suite('Symbol Information', () => { 41 | const docUri = getDocUri('MyLibrary.mo'); 42 | 43 | test('onDocumentSymbol()', async () => { 44 | const documentSymbols: vscode.DocumentSymbol[] = [ 45 | new vscode.DocumentSymbol( 46 | 'MyLibrary', 47 | '', 48 | vscode.SymbolKind.Package, 49 | new vscode.Range(new vscode.Position(0, 0), new vscode.Position(6, 13)), 50 | new vscode.Range(new vscode.Position(0, 0), new vscode.Position(6, 13)), 51 | ), 52 | ]; 53 | documentSymbols[0].children.push( 54 | new vscode.DocumentSymbol( 55 | 'M', 56 | '', 57 | vscode.SymbolKind.Class, 58 | new vscode.Range(new vscode.Position(1, 2), new vscode.Position(5, 7)), 59 | new vscode.Range(new vscode.Position(1, 2), new vscode.Position(5, 7)), 60 | ), 61 | ); 62 | 63 | await testSymbolInformation(docUri, documentSymbols); 64 | }); 65 | }); 66 | 67 | async function testSymbolInformation( 68 | docUri: vscode.Uri, 69 | expectedDocumentSymbols: vscode.DocumentSymbol[], 70 | ) { 71 | await activate(docUri); 72 | 73 | // Execute `vscode.executeDocumentSymbolProvider` to get file outline 74 | const actualSymbolInformation = await vscode.commands.executeCommand( 75 | 'vscode.executeDocumentSymbolProvider', 76 | docUri, 77 | ); 78 | 79 | //printDocumentSymbols(actualSymbolInformation); 80 | assertDocumentSymbolsEqual(expectedDocumentSymbols, actualSymbolInformation); 81 | } 82 | 83 | function printDocumentSymbols(documentSymbols: vscode.DocumentSymbol[]) { 84 | documentSymbols.forEach((symbol, index) => { 85 | console.log(`Document Symbol ${index + 1}:`); 86 | console.log(`Name: ${symbol.name}`); 87 | console.log(`Kind: ${vscode.SymbolKind[symbol.kind]}`); 88 | console.log( 89 | `Range: ${symbol.range.start.line}:${symbol.range.start.character}, ${symbol.range.end.line}:${symbol.range.end.character}`, 90 | ); 91 | console.log( 92 | `SelectionRange: ${symbol.selectionRange.start.line}:${symbol.selectionRange.start.character}, ${symbol.selectionRange.end.line}:${symbol.selectionRange.end.character}`, 93 | ); 94 | console.log('Children:'); 95 | 96 | if (symbol.children && symbol.children.length > 0) { 97 | printDocumentSymbols(symbol.children); 98 | } 99 | 100 | console.log('---'); 101 | }); 102 | } 103 | 104 | function assertDocumentSymbolsEqual( 105 | expected: vscode.DocumentSymbol[], 106 | actual: vscode.DocumentSymbol[], 107 | ) { 108 | assert.strictEqual(expected.length, actual.length, 'Array lengths do not match.'); 109 | 110 | for (let i = 0; i < expected.length; i++) { 111 | const expectedSymbol = expected[i]; 112 | const actualSymbol = actual[i]; 113 | 114 | assert.strictEqual( 115 | expectedSymbol.name, 116 | actualSymbol.name, 117 | `Symbol names do not match at index ${i}.`, 118 | ); 119 | assert.strictEqual( 120 | expectedSymbol.kind, 121 | actualSymbol.kind, 122 | `Symbol kinds do not match at index ${i}.`, 123 | ); 124 | 125 | assert.strictEqual( 126 | expectedSymbol.range.start.line, 127 | actualSymbol.range.start.line, 128 | `Symbol start line does not match at index ${i}.`, 129 | ); 130 | assert.strictEqual( 131 | expectedSymbol.range.start.character, 132 | actualSymbol.range.start.character, 133 | `Symbol start character does not match at index ${i}.`, 134 | ); 135 | 136 | assert.strictEqual( 137 | expectedSymbol.range.end.line, 138 | actualSymbol.range.end.line, 139 | `Symbol end line does not match at index ${i}.`, 140 | ); 141 | assert.strictEqual( 142 | expectedSymbol.range.end.character, 143 | actualSymbol.range.end.character, 144 | `Symbol end character does not match at index ${i}.`, 145 | ); 146 | 147 | // Recursive check for children symbols 148 | assertDocumentSymbolsEqual(expectedSymbol.children || [], actualSymbol.children || []); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /server/src/analysis/reference.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import { ModelicaDocument } from '../project/document'; 37 | import Parser from 'web-tree-sitter'; 38 | 39 | export type ReferenceKind = 'class' | 'variable'; 40 | 41 | export abstract class BaseUnresolvedReference { 42 | /** 43 | * The path to the symbol reference. 44 | */ 45 | public readonly symbols: string[]; 46 | 47 | public readonly kind: ReferenceKind | undefined; 48 | 49 | public constructor(symbols: string[], kind?: ReferenceKind) { 50 | if (symbols.length === 0) { 51 | throw new Error('Symbols length must be greater tham 0'); 52 | } 53 | 54 | this.symbols = symbols; 55 | this.kind = kind; 56 | } 57 | 58 | public abstract isAbsolute(): this is UnresolvedAbsoluteReference; 59 | 60 | public abstract equals(other: unknown): boolean; 61 | } 62 | 63 | export class UnresolvedRelativeReference extends BaseUnresolvedReference { 64 | /** 65 | * The document that contains the `node`. 66 | */ 67 | public readonly document: ModelicaDocument; 68 | 69 | /** 70 | * A `SyntaxNode` in which the symbol is in scope. 71 | */ 72 | public readonly node: Parser.SyntaxNode; 73 | 74 | public constructor( 75 | document: ModelicaDocument, 76 | node: Parser.SyntaxNode, 77 | symbols: string[], 78 | kind?: ReferenceKind, 79 | ) { 80 | super(symbols, kind); 81 | this.document = document; 82 | this.node = node; 83 | } 84 | 85 | public isAbsolute(): this is UnresolvedAbsoluteReference { 86 | return false; 87 | } 88 | 89 | public equals(other: unknown): boolean { 90 | if (!(other instanceof UnresolvedRelativeReference)) { 91 | return false; 92 | } 93 | 94 | return ( 95 | this.document.uri === other.document.uri && 96 | this.node.equals(other.node) && 97 | this.symbols.length === other.symbols.length && 98 | this.symbols.every((s, i) => s === other.symbols[i]) && 99 | this.kind === other.kind 100 | ); 101 | } 102 | 103 | public toString(): string { 104 | const start = this.node.startPosition; 105 | return ( 106 | `UnresolvedReference { ` + 107 | `symbols: ${this.symbols.join('.')}, ` + 108 | `kind: ${this.kind}, ` + 109 | `position: ${start.row + 1}:${start.column + 1}, ` + 110 | `document: "${this.document.path}" ` + 111 | `}` 112 | ); 113 | } 114 | } 115 | 116 | export class UnresolvedAbsoluteReference extends BaseUnresolvedReference { 117 | public constructor(symbols: string[], kind?: ReferenceKind) { 118 | super(symbols, kind); 119 | } 120 | 121 | public isAbsolute(): this is UnresolvedAbsoluteReference { 122 | return true; 123 | } 124 | 125 | public equals(other: unknown): boolean { 126 | if (!(other instanceof UnresolvedAbsoluteReference)) { 127 | return false; 128 | } 129 | 130 | return ( 131 | this.symbols.length === other.symbols.length && 132 | this.symbols.every((s, i) => s === other.symbols[i]) && 133 | this.kind === other.kind 134 | ); 135 | } 136 | 137 | public toString(): string { 138 | return ( 139 | `UnresolvedReference { ` + 140 | `symbols: .${this.symbols.join('.')}, ` + 141 | `kind: ${this.kind} ` + 142 | `}` 143 | ); 144 | } 145 | } 146 | 147 | /** 148 | * A possibly-valid reference to a symbol that must be resolved before use. 149 | */ 150 | export type UnresolvedReference = UnresolvedRelativeReference | UnresolvedAbsoluteReference; 151 | 152 | /** 153 | * A valid, absolute reference to a symbol. 154 | */ 155 | export class ResolvedReference { 156 | /** 157 | * The document that contains the `node`. 158 | */ 159 | readonly document: ModelicaDocument; 160 | 161 | /** 162 | * The node that declares/defines this symbol. 163 | */ 164 | readonly node: Parser.SyntaxNode; 165 | 166 | /** 167 | * The full, absolute path to the symbol. 168 | */ 169 | readonly symbols: string[]; 170 | 171 | readonly kind: ReferenceKind; 172 | 173 | public constructor( 174 | document: ModelicaDocument, 175 | node: Parser.SyntaxNode, 176 | symbols: string[], 177 | kind: ReferenceKind, 178 | ) { 179 | if (symbols.length === 0) { 180 | throw new Error('Symbols length must be greater than 0.'); 181 | } 182 | 183 | this.document = document; 184 | this.node = node; 185 | this.symbols = symbols; 186 | this.kind = kind; 187 | } 188 | 189 | public equals(other: unknown): boolean { 190 | if (!(other instanceof ResolvedReference)) { 191 | return false; 192 | } 193 | 194 | return ( 195 | this.document.uri === other.document.uri && 196 | this.node.equals(other.node) && 197 | this.symbols.length === other.symbols.length && 198 | this.symbols.every((s, i) => s === other.symbols[i]) && 199 | this.kind === other.kind 200 | ); 201 | } 202 | 203 | public toString(): string { 204 | return `Reference { symbols: .${this.symbols.join('.')}, kind: ${this.kind} }`; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /server/src/util/logger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | /* ----------------------------------------------------------------------------- 37 | * Taken from bash-language-server and adapted to Modelica language server 38 | * https://github.com/bash-lsp/bash-language-server/blob/main/server/src/parser.ts 39 | * ----------------------------------------------------------------------------- 40 | */ 41 | 42 | import * as LSP from 'vscode-languageserver'; 43 | 44 | export const LOG_LEVEL_ENV_VAR = 'MODELICA_IDE_LOG_LEVEL'; 45 | export const LOG_LEVELS = ['debug', 'log', 'info', 'warning', 'error'] as const; 46 | export const DEFAULT_LOG_LEVEL: LogLevel = 'info'; 47 | 48 | export type LogLevel = (typeof LOG_LEVELS)[number]; 49 | 50 | const LOG_LEVELS_TO_MESSAGE_TYPES: { 51 | [logLevel in LogLevel]: LSP.MessageType; 52 | } = { 53 | debug: LSP.MessageType.Debug, 54 | log: LSP.MessageType.Log, 55 | info: LSP.MessageType.Info, 56 | warning: LSP.MessageType.Warning, 57 | error: LSP.MessageType.Error, 58 | } as const; 59 | 60 | export interface LoggerOptions { 61 | /** 62 | * The connection to the LSP client. If unset, will not log to the client. 63 | * 64 | * Default: `null` 65 | */ 66 | connection?: LSP.Connection | null; 67 | /** 68 | * The minimum log level. 69 | * 70 | * Default: use the environment variable {@link LOG_LEVEL_ENV_VAR}, or {@link DEFAULT_LOG_LEVEL} if unset. 71 | */ 72 | logLevel?: LogLevel; 73 | /** 74 | * `true` to log locally as well as to the LSP client. 75 | * 76 | * Default: `false` 77 | */ 78 | useLocalLogging?: boolean; 79 | } 80 | 81 | // Singleton madness to allow for logging from anywhere in the codebase 82 | let _options: LoggerOptions = {}; 83 | 84 | /** 85 | * Sets the logger options. Should be done at startup. 86 | */ 87 | export function setLoggerOptions(options: LoggerOptions) { 88 | _options = options; 89 | } 90 | 91 | export class Logger { 92 | private prefix: string; 93 | 94 | constructor({ prefix = '' }: { prefix?: string } = {}) { 95 | this.prefix = prefix; 96 | } 97 | 98 | static MESSAGE_TYPE_TO_LOG_LEVEL_MSG: Record = { 99 | [LSP.MessageType.Error]: 'ERROR ⛔️', 100 | [LSP.MessageType.Warning]: 'WARNING ⛔️', 101 | [LSP.MessageType.Info]: 'INFO', 102 | [LSP.MessageType.Log]: 'LOG', 103 | [LSP.MessageType.Debug]: 'DEBUG', 104 | }; 105 | 106 | static MESSAGE_TYPE_TO_LOG_FUNCTION: Record void> = { 107 | [LSP.MessageType.Error]: console.error, 108 | [LSP.MessageType.Warning]: console.warn, 109 | [LSP.MessageType.Info]: console.info, 110 | [LSP.MessageType.Log]: console.log, 111 | [LSP.MessageType.Debug]: console.debug, 112 | }; 113 | 114 | public log(severity: LSP.MessageType, messageObjects: any[]) { 115 | const logLevelString = _options.logLevel ?? getLogLevelFromEnvironment(); 116 | const logLevel = LOG_LEVELS_TO_MESSAGE_TYPES[logLevelString]; 117 | if (logLevel < severity) { 118 | return; 119 | } 120 | 121 | const formattedMessage = messageObjects 122 | .map((p) => { 123 | if (p instanceof Error) { 124 | return p.stack || p.message; 125 | } 126 | 127 | if (typeof p === 'object') { 128 | return JSON.stringify(p, null, 2); 129 | } 130 | 131 | return p; 132 | }) 133 | .join(' '); 134 | 135 | const level = Logger.MESSAGE_TYPE_TO_LOG_LEVEL_MSG[severity]; 136 | const prefix = this.prefix ? `${this.prefix} - ` : ''; 137 | const time = new Date().toISOString().substring(11, 23); 138 | const message = `${time} ${level} ${prefix}${formattedMessage}`; 139 | 140 | if (_options.connection) { 141 | _options.connection.sendNotification(LSP.LogMessageNotification.type, { 142 | type: severity, 143 | message, 144 | }); 145 | } 146 | 147 | if (_options.useLocalLogging) { 148 | const log = Logger.MESSAGE_TYPE_TO_LOG_FUNCTION[logLevel]; 149 | log(message); 150 | } 151 | } 152 | 153 | public debug(message: string, ...additionalArgs: any[]) { 154 | this.log(LSP.MessageType.Debug, [message, ...additionalArgs]); 155 | } 156 | public info(message: string, ...additionalArgs: any[]) { 157 | this.log(LSP.MessageType.Info, [message, ...additionalArgs]); 158 | } 159 | public warn(message: string, ...additionalArgs: any[]) { 160 | this.log(LSP.MessageType.Warning, [message, ...additionalArgs]); 161 | } 162 | public error(message: string, ...additionalArgs: any[]) { 163 | this.log(LSP.MessageType.Error, [message, ...additionalArgs]); 164 | } 165 | } 166 | 167 | /** 168 | * Default logger. 169 | */ 170 | export const logger = new Logger(); 171 | 172 | /** 173 | * Get the log level from the environment, before the server initializes. 174 | * Should only be used internally. 175 | */ 176 | function getLogLevelFromEnvironment(): LogLevel { 177 | const logLevel = process.env[LOG_LEVEL_ENV_VAR]; 178 | if (logLevel) { 179 | if (logLevel in LOG_LEVELS_TO_MESSAGE_TYPES) { 180 | return logLevel as LogLevel; 181 | } 182 | // eslint-disable-next-line no-console 183 | console.warn( 184 | `Invalid ${LOG_LEVEL_ENV_VAR} "${logLevel}", expected one of: ${Object.keys( 185 | LOG_LEVELS_TO_MESSAGE_TYPES, 186 | ).join(', ')}`, 187 | ); 188 | } 189 | 190 | return DEFAULT_LOG_LEVEL; 191 | } 192 | -------------------------------------------------------------------------------- /server/src/analysis/test/resolveReference.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import Parser from 'web-tree-sitter'; 37 | import assert from 'node:assert/strict'; 38 | import path from 'node:path'; 39 | import { ModelicaProject, ModelicaLibrary, ModelicaDocument } from '../../project'; 40 | import { initializeParser } from '../../parser'; 41 | import resolveReference from '../resolveReference'; 42 | import { 43 | UnresolvedAbsoluteReference, 44 | UnresolvedRelativeReference, 45 | ResolvedReference, 46 | } from '../reference'; 47 | import * as TreeSitterUtil from '../../util/tree-sitter'; 48 | 49 | const TEST_LIBRARY_PATH = path.join(__dirname, 'TestLibrary'); 50 | const TEST_CLASS_PATH = path.join(TEST_LIBRARY_PATH, 'TestPackage', 'TestClass.mo'); 51 | const CONSTANTS_PATH = path.join(TEST_LIBRARY_PATH, 'Constants.mo'); 52 | 53 | describe('resolveReference', () => { 54 | let project: ModelicaProject; 55 | 56 | beforeEach(async () => { 57 | const parser = await initializeParser(); 58 | project = new ModelicaProject(parser); 59 | project.addLibrary(await ModelicaLibrary.load(project, TEST_LIBRARY_PATH, true)); 60 | }); 61 | 62 | it('should resolve absolute references to classes', async () => { 63 | const unresolved = new UnresolvedAbsoluteReference(['TestLibrary', 'TestPackage', 'TestClass']); 64 | const resolved = resolveReference(project, unresolved, 'declaration'); 65 | 66 | const resolvedDocument = await project.getDocument(TEST_CLASS_PATH); 67 | assert(resolvedDocument !== undefined); 68 | 69 | // Get node declaring `TestClass` 70 | const resolvedNode = TreeSitterUtil.findFirst( 71 | resolvedDocument.tree.rootNode, 72 | (node) => 73 | node.type === 'class_definition' && TreeSitterUtil.getIdentifier(node) === 'TestClass', 74 | )!; 75 | const resolvedSymbols = ['TestLibrary', 'TestPackage', 'TestClass']; 76 | 77 | assert( 78 | resolved?.equals( 79 | new ResolvedReference(resolvedDocument, resolvedNode, resolvedSymbols, 'class'), 80 | ), 81 | ); 82 | }); 83 | 84 | it('should resolve absolute references to variables', async () => { 85 | const unresolved = new UnresolvedAbsoluteReference(['TestLibrary', 'Constants', 'e']); 86 | const resolved = resolveReference(project, unresolved, 'declaration'); 87 | 88 | const resolvedDocument = (await project.getDocument(CONSTANTS_PATH))!; 89 | 90 | // Get the node declaring `e` 91 | const resolvedNode = TreeSitterUtil.findFirst( 92 | resolvedDocument.tree.rootNode, 93 | (node) => 94 | node.type === 'component_clause' && TreeSitterUtil.getDeclaredIdentifiers(node)[0] === 'e', 95 | )!; 96 | const resolvedSymbols = ['TestLibrary', 'Constants', 'e']; 97 | 98 | assert( 99 | resolved?.equals( 100 | new ResolvedReference(resolvedDocument, resolvedNode, resolvedSymbols, 'variable'), 101 | ), 102 | ); 103 | }); 104 | 105 | it('should resolve relative references to locals', async () => { 106 | const document = (await project.getDocument(TEST_CLASS_PATH))!; 107 | const unresolvedNode = TreeSitterUtil.findFirst( 108 | document.tree.rootNode, 109 | (node) => node.startPosition.row === 7 && node.startPosition.column === 21, 110 | )!; 111 | const unresolved = new UnresolvedRelativeReference(document, unresolvedNode, ['tau']); 112 | const resolved = resolveReference(project, unresolved, 'declaration'); 113 | 114 | // the resolved node is the declaration of tau 115 | // `input Real tau = 2 * pi;` 116 | const resolvedNode = TreeSitterUtil.findFirst( 117 | document.tree.rootNode, 118 | (node) => 119 | node.type === 'component_clause' && 120 | TreeSitterUtil.getDeclaredIdentifiers(node)[0] === 'tau', 121 | )!; 122 | 123 | assert( 124 | resolved?.equals( 125 | new ResolvedReference( 126 | document, 127 | resolvedNode, 128 | ['TestLibrary', 'TestPackage', 'TestClass', 'tau'], 129 | 'variable', 130 | ), 131 | ), 132 | ); 133 | }); 134 | 135 | it('should resolve relative references to globals', async () => { 136 | // input Real twoE = 2 * Constants.e; 137 | // ^ 5:33 138 | const unresolvedDocument = (await project.getDocument(TEST_CLASS_PATH))!; 139 | const unresolvedNode = TreeSitterUtil.findFirst( 140 | unresolvedDocument.tree.rootNode, 141 | (node) => node.startPosition.row === 5 && node.startPosition.column === 33, 142 | )!; 143 | const unresolved = new UnresolvedRelativeReference(unresolvedDocument, unresolvedNode, [ 144 | 'Constants', 145 | 'e', 146 | ]); 147 | const resolved = resolveReference(project, unresolved, 'declaration'); 148 | 149 | const resolvedDocument = (await project.getDocument(CONSTANTS_PATH))!; 150 | // Get the node declaring `e` 151 | const resolvedNode = TreeSitterUtil.findFirst( 152 | resolvedDocument.tree.rootNode, 153 | (node) => 154 | node.type === 'component_clause' && TreeSitterUtil.getDeclaredIdentifiers(node)[0] === 'e', 155 | )!; 156 | 157 | assert( 158 | resolved?.equals( 159 | new ResolvedReference( 160 | resolvedDocument, 161 | resolvedNode, 162 | ['TestLibrary', 'Constants', 'e'], 163 | 'variable', 164 | ), 165 | ), 166 | ); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /server/src/project/document.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import { TextDocument } from 'vscode-languageserver-textdocument'; 37 | import * as LSP from 'vscode-languageserver/node'; 38 | import Parser from 'web-tree-sitter'; 39 | import * as fs from 'node:fs/promises'; 40 | import * as TreeSitterUtil from '../util/tree-sitter'; 41 | 42 | import { logger } from '../util/logger'; 43 | import { ModelicaLibrary } from './library'; 44 | import { ModelicaProject } from './project'; 45 | import { positionToPoint } from '../util/tree-sitter'; 46 | import { pathToUri, uriToPath } from '../util'; 47 | 48 | export class ModelicaDocument implements TextDocument { 49 | readonly #project: ModelicaProject; 50 | readonly #library: ModelicaLibrary | null; 51 | readonly #document: TextDocument; 52 | #tree: Parser.Tree; 53 | 54 | public constructor( 55 | project: ModelicaProject, 56 | library: ModelicaLibrary | null, 57 | document: TextDocument, 58 | tree: Parser.Tree, 59 | ) { 60 | this.#project = project; 61 | this.#library = library; 62 | this.#document = document; 63 | this.#tree = tree; 64 | } 65 | 66 | /** 67 | * Loads a document. 68 | * 69 | * @param project the {@link ModelicaProject} 70 | * @param library the containing {@link ModelicaLibrary} (or `null` if not a part of one) 71 | * @param documentPath the path to the document 72 | * @returns the document 73 | */ 74 | public static async load( 75 | project: ModelicaProject, 76 | library: ModelicaLibrary | null, 77 | documentPath: string, 78 | ): Promise { 79 | logger.debug(`Loading document at '${documentPath}'...`); 80 | 81 | try { 82 | const content = await fs.readFile(documentPath, 'utf-8'); 83 | 84 | const uri = pathToUri(documentPath); 85 | const document = TextDocument.create(uri, 'modelica', 0, content); 86 | 87 | const tree = project.parser.parse(content); 88 | 89 | return new ModelicaDocument(project, library, document, tree); 90 | } catch (err) { 91 | throw new Error( 92 | `Failed to load document at '${documentPath}': ${err instanceof Error ? err.message : err}`, 93 | ); 94 | } 95 | } 96 | 97 | /** 98 | * Updates a document. 99 | * 100 | * @param text the modification 101 | * @param range the range to update, or `undefined` to replace the whole file 102 | */ 103 | public async update(text: string, range?: LSP.Range): Promise { 104 | if (range === undefined) { 105 | TextDocument.update(this.#document, [{ text }], this.version + 1); 106 | this.#tree = this.project.parser.parse(text); 107 | return; 108 | } 109 | 110 | const startIndex = this.offsetAt(range.start); 111 | const startPosition = positionToPoint(range.start); 112 | const oldEndIndex = this.offsetAt(range.end); 113 | const oldEndPosition = positionToPoint(range.end); 114 | const newEndIndex = startIndex + text.length; 115 | 116 | TextDocument.update(this.#document, [{ text, range }], this.version + 1); 117 | const newEndPosition = positionToPoint(this.positionAt(newEndIndex)); 118 | 119 | this.#tree.edit({ 120 | startIndex, 121 | startPosition, 122 | oldEndIndex, 123 | oldEndPosition, 124 | newEndIndex, 125 | newEndPosition, 126 | }); 127 | 128 | this.#tree = this.project.parser.parse((index: number, position?: Parser.Point) => { 129 | if (position) { 130 | return this.getText({ 131 | start: { 132 | character: position.column, 133 | line: position.row, 134 | }, 135 | end: { 136 | character: position.column + 1, 137 | line: position.row, 138 | }, 139 | }); 140 | } else { 141 | return this.getText({ 142 | start: this.positionAt(index), 143 | end: this.positionAt(index + 1), 144 | }); 145 | } 146 | }, this.#tree); 147 | } 148 | 149 | public getText(range?: LSP.Range | undefined): string { 150 | return this.#document.getText(range); 151 | } 152 | 153 | public positionAt(offset: number): LSP.Position { 154 | return this.#document.positionAt(offset); 155 | } 156 | 157 | public offsetAt(position: LSP.Position): number { 158 | return this.#document.offsetAt(position); 159 | } 160 | 161 | public get uri(): LSP.DocumentUri { 162 | return this.#document.uri; 163 | } 164 | 165 | public get path(): string { 166 | return uriToPath(this.#document.uri); 167 | } 168 | 169 | public get languageId(): string { 170 | return this.#document.languageId; 171 | } 172 | 173 | public get version(): number { 174 | return this.#document.version; 175 | } 176 | 177 | public get lineCount(): number { 178 | return this.#document.lineCount; 179 | } 180 | 181 | /** 182 | * The enclosing package of the class declared by this file. For instance, for 183 | * a file named `MyLibrary/MyPackage/MyClass.mo`, this should be `["MyLibrary", 184 | * "MyPackage"]`. 185 | */ 186 | public get within(): string[] { 187 | const withinClause = this.#tree.rootNode.children 188 | .find((node) => node.type === 'within_clause') 189 | ?.childForFieldName('name'); 190 | if (!withinClause) { 191 | return []; 192 | } 193 | 194 | // TODO: Use a helper function from TreeSitterUtil 195 | const identifiers: string[] = []; 196 | TreeSitterUtil.forEach(withinClause, (node) => { 197 | if (node.type === 'name') { 198 | return true; 199 | } 200 | 201 | if (node.type === 'IDENT') { 202 | identifiers.push(node.text); 203 | } 204 | 205 | return false; 206 | }); 207 | 208 | return identifiers; 209 | } 210 | 211 | public get project(): ModelicaProject { 212 | return this.#project; 213 | } 214 | 215 | public get library(): ModelicaLibrary | null { 216 | return this.#library; 217 | } 218 | 219 | public get tree(): Parser.Tree { 220 | return this.#tree; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /server/src/project/project.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import Parser from 'web-tree-sitter'; 37 | import * as LSP from 'vscode-languageserver'; 38 | import url from 'node:url'; 39 | import path from 'node:path'; 40 | 41 | import { ModelicaLibrary } from './library'; 42 | import { ModelicaDocument } from './document'; 43 | import { logger } from '../util/logger'; 44 | 45 | /** Options for {@link ModelicaProject.getDocument} */ 46 | export interface GetDocumentOptions { 47 | /** 48 | * `true` to try loading the document from disk if it is not already loaded. 49 | * 50 | * Default value: `true`. 51 | */ 52 | load?: boolean; 53 | } 54 | 55 | export class ModelicaProject { 56 | readonly #parser: Parser; 57 | readonly #libraries: ModelicaLibrary[]; 58 | 59 | public constructor(parser: Parser) { 60 | this.#parser = parser; 61 | this.#libraries = []; 62 | } 63 | 64 | public get libraries(): readonly ModelicaLibrary[] { 65 | return this.#libraries; 66 | } 67 | 68 | public addLibrary(library: ModelicaLibrary) { 69 | this.#libraries.push(library); 70 | } 71 | 72 | /** 73 | * Finds the document identified by the given path. 74 | * 75 | * Will load the document from disk if unloaded and `options.load` is `true` or `undefined`. 76 | * 77 | * @param documentPath file path pointing to the document 78 | * @param options 79 | * @returns the document, or `undefined` if no such document exists 80 | */ 81 | public async getDocument( 82 | documentPath: string, 83 | options?: GetDocumentOptions, 84 | ): Promise { 85 | let loadedDocument: ModelicaDocument | undefined = undefined; 86 | for (const library of this.#libraries) { 87 | loadedDocument = library.documents.get(documentPath); 88 | if (loadedDocument) { 89 | logger.debug(`Found document: ${documentPath}`); 90 | break; 91 | } 92 | } 93 | 94 | if (loadedDocument) { 95 | return loadedDocument; 96 | } 97 | 98 | if (options?.load !== false) { 99 | const newDocument = await this.addDocument(documentPath); 100 | if (newDocument) { 101 | return newDocument; 102 | } 103 | } 104 | 105 | logger.debug(`Couldn't find document: ${documentPath}`); 106 | 107 | return undefined; 108 | } 109 | 110 | /** 111 | * Adds a new document to the LSP. Calling this method multiple times for the 112 | * same document has no effect. 113 | * 114 | * @param documentPath path to the document 115 | * @returns the document, or undefined if it wasn't added 116 | * @throws if the document does not belong to a library 117 | */ 118 | public async addDocument(documentPath: string): Promise { 119 | logger.info(`Adding document at '${documentPath}'...`); 120 | 121 | for (const library of this.#libraries) { 122 | const relative = path.relative(library.path, documentPath); 123 | const isSubdirectory = relative && !relative.startsWith('..') && !path.isAbsolute(relative); 124 | 125 | // Assume that files can't be inside multiple libraries at the same time 126 | if (!isSubdirectory) { 127 | continue; 128 | } 129 | 130 | if (library.documents.get(documentPath) !== undefined) { 131 | logger.warn(`Document '${documentPath}' already in library '${library.name}'; ignoring...`); 132 | return undefined; 133 | } 134 | 135 | const document = await ModelicaDocument.load(this, library, documentPath); 136 | library.documents.set(documentPath, document); 137 | logger.debug(`Added document: ${documentPath}`); 138 | return document; 139 | } 140 | 141 | // If the document doesn't belong to a library, it could still be loaded 142 | // as a standalone document if it has an empty or non-existent within clause 143 | const standaloneName = path.basename(documentPath).split('.')[0]; 144 | const standaloneLibrary = new ModelicaLibrary( 145 | this, 146 | path.dirname(documentPath), 147 | false, 148 | standaloneName, 149 | ); 150 | const document = await ModelicaDocument.load(this, standaloneLibrary, documentPath); 151 | if (document.within.length === 0) { 152 | this.addLibrary(standaloneLibrary); 153 | logger.debug(`Added document: ${documentPath}`); 154 | return document; 155 | } 156 | 157 | logger.debug(`Failed to add document '${documentPath}': not a part of any libraries.`); 158 | return undefined; 159 | } 160 | 161 | /** 162 | * Updates the content and tree of the given document. Does nothing and 163 | * returns `false` if the document was not found. 164 | * 165 | * @param documentPath path to the document 166 | * @param text the modification 167 | * @returns if the document was updated 168 | */ 169 | public async updateDocument( 170 | documentPath: string, 171 | text: string, 172 | range?: LSP.Range, 173 | ): Promise { 174 | logger.debug(`Updating document at '${documentPath}'...`); 175 | 176 | const doc = await this.getDocument(documentPath, { load: true }); 177 | if (doc) { 178 | doc.update(text, range); 179 | logger.debug(`Updated document '${documentPath}'`); 180 | return true; 181 | } else { 182 | logger.warn(`Failed to update document '${documentPath}': not found`); 183 | return false; 184 | } 185 | } 186 | 187 | /** 188 | * Removes a document from the cache. 189 | * 190 | * @param documentPath path to the document 191 | * @returns if the document was removed 192 | */ 193 | public async removeDocument(documentPath: string): Promise { 194 | logger.info(`Removing document at '${documentPath}'...`); 195 | 196 | const doc = await this.getDocument(documentPath, { load: false }); 197 | if (doc) { 198 | doc.library?.documents.delete(documentPath); 199 | return true; 200 | } else { 201 | logger.warn(`Failed to remove document '${documentPath}': not found`); 202 | return false; 203 | } 204 | } 205 | 206 | public get parser(): Parser { 207 | return this.#parser; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | /* ----------------------------------------------------------------------------- 37 | * Taken from bash-language-server and adapted to Modelica language server 38 | * https://github.com/bash-lsp/bash-language-server/blob/main/server/src/server.ts 39 | * ----------------------------------------------------------------------------- 40 | */ 41 | 42 | import * as LSP from 'vscode-languageserver/node'; 43 | import { TextDocument } from 'vscode-languageserver-textdocument'; 44 | import url from 'node:url'; 45 | import fs from 'node:fs/promises'; 46 | 47 | import { initializeParser } from './parser'; 48 | import Analyzer from './analyzer'; 49 | import { logger, setLoggerOptions } from './util/logger'; 50 | 51 | /** 52 | * ModelicaServer collection all the important bits and bobs. 53 | */ 54 | export class ModelicaServer { 55 | #analyzer: Analyzer; 56 | #clientCapabilities: LSP.ClientCapabilities; 57 | #connection: LSP.Connection; 58 | #documents: LSP.TextDocuments = new LSP.TextDocuments(TextDocument); 59 | 60 | private constructor( 61 | analyzer: Analyzer, 62 | clientCapabilities: LSP.ClientCapabilities, 63 | connection: LSP.Connection, 64 | ) { 65 | this.#analyzer = analyzer; 66 | this.#clientCapabilities = clientCapabilities; 67 | this.#connection = connection; 68 | } 69 | 70 | public static async initialize( 71 | connection: LSP.Connection, 72 | { capabilities, workspaceFolders }: LSP.InitializeParams, 73 | ): Promise { 74 | // Initialize logger 75 | setLoggerOptions({ 76 | connection, 77 | logLevel: 'debug', 78 | }); 79 | logger.debug('Initializing...'); 80 | 81 | const parser = await initializeParser(); 82 | const analyzer = new Analyzer(parser); 83 | if (workspaceFolders != null) { 84 | for (const workspace of workspaceFolders) { 85 | await analyzer.loadLibrary(workspace.uri, true); 86 | } 87 | } 88 | // TODO: add libraries as well 89 | 90 | logger.debug('Initialized'); 91 | return new ModelicaServer(analyzer, capabilities, connection); 92 | } 93 | 94 | /** 95 | * Return what parts of the language server protocol are supported by ModelicaServer. 96 | */ 97 | public capabilities(): LSP.ServerCapabilities { 98 | return { 99 | completionProvider: undefined, 100 | declarationProvider: true, 101 | definitionProvider: true, 102 | hoverProvider: false, 103 | signatureHelpProvider: undefined, 104 | documentSymbolProvider: true, 105 | colorProvider: false, 106 | semanticTokensProvider: undefined, 107 | textDocumentSync: LSP.TextDocumentSyncKind.Incremental, 108 | workspace: { 109 | workspaceFolders: { 110 | supported: true, 111 | changeNotifications: true, 112 | }, 113 | }, 114 | }; 115 | } 116 | 117 | public register(connection: LSP.Connection): void { 118 | // Make the text document manager listen on the connection 119 | // for open, change and close text document events 120 | this.#documents.listen(this.#connection); 121 | 122 | connection.onInitialized(this.onInitialized.bind(this)); 123 | connection.onShutdown(this.onShutdown.bind(this)); 124 | connection.onDidChangeTextDocument(this.onDidChangeTextDocument.bind(this)); 125 | connection.onDidChangeWatchedFiles(this.onDidChangeWatchedFiles.bind(this)); 126 | connection.onDeclaration(this.onDeclaration.bind(this)); 127 | connection.onDefinition(this.onDefinition.bind(this)); 128 | connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); 129 | } 130 | 131 | private async onInitialized(): Promise { 132 | logger.debug('onInitialized'); 133 | await connection.client.register( 134 | new LSP.ProtocolNotificationType('workspace/didChangeWatchedFiles'), 135 | { 136 | watchers: [ 137 | { 138 | globPattern: '**/*.{mo,mos}', 139 | }, 140 | ], 141 | }, 142 | ); 143 | 144 | // If we opened a project, analyze it now that we're initialized 145 | // and the linter is ready. 146 | 147 | // TODO: analysis 148 | } 149 | 150 | private async onShutdown(): Promise { 151 | logger.debug('onShutdown'); 152 | } 153 | 154 | private async onDidChangeTextDocument(params: LSP.DidChangeTextDocumentParams): Promise { 155 | logger.debug('onDidChangeTextDocument'); 156 | for (const change of params.contentChanges) { 157 | const range = 'range' in change ? change.range : undefined; 158 | await this.#analyzer.updateDocument(params.textDocument.uri, change.text, range); 159 | } 160 | } 161 | 162 | private async onDidChangeWatchedFiles(params: LSP.DidChangeWatchedFilesParams): Promise { 163 | logger.debug('onDidChangeWatchedFiles: ' + JSON.stringify(params, undefined, 4)); 164 | 165 | for (const change of params.changes) { 166 | switch (change.type) { 167 | case LSP.FileChangeType.Created: 168 | await this.#analyzer.addDocument(change.uri); 169 | break; 170 | case LSP.FileChangeType.Changed: { 171 | // TODO: incremental? 172 | const path = url.fileURLToPath(change.uri); 173 | const content = await fs.readFile(path, 'utf-8'); 174 | await this.#analyzer.updateDocument(change.uri, content); 175 | break; 176 | } 177 | case LSP.FileChangeType.Deleted: { 178 | this.#analyzer.removeDocument(change.uri); 179 | break; 180 | } 181 | } 182 | } 183 | } 184 | 185 | // TODO: We currently treat goto declaration and goto definition the same, 186 | // but there are probably some differences we need to handle. 187 | // 188 | // 1. inner/outer variables. Modelica allows the user to redeclare variables 189 | // from enclosing classes to use them in inner classes. Goto Declaration 190 | // should go to whichever declaration is in scope, while Goto Definition 191 | // should go to the `outer` declaration. In the following example: 192 | // 193 | // model Outer 194 | // model Inner 195 | // inner Real shared; 196 | // equation 197 | // shared = ...; (A) 198 | // end Inner; 199 | // outer Real shared = 0; 200 | // equation 201 | // shared = ...; (B) 202 | // end Outer; 203 | // 204 | // +-----+-------------+------------+ 205 | // | Ref | Declaration | Definition | 206 | // +-----+-------------+------------+ 207 | // | A | inner | outer | 208 | // | B | outer | outer | 209 | // +-----+-------------+------------+ 210 | // 211 | // 2. extends_clause is weird. This is a valid class: 212 | // 213 | // class extends Foo; 214 | // end Foo; 215 | // 216 | // What does this even mean? Is this a definition of Foo or a redeclaration of Foo? 217 | // 218 | // 3. Import aliases. Should this be considered to be a declaration of `Frobnicator`? 219 | // 220 | // import Frobnicator = Foo.Bar.Baz; 221 | // 222 | 223 | private async onDeclaration(params: LSP.DeclarationParams): Promise { 224 | logger.debug('onDeclaration'); 225 | 226 | const locationLink = await this.#analyzer.findDeclaration( 227 | params.textDocument.uri, 228 | params.position, 229 | ); 230 | if (locationLink == null) { 231 | return []; 232 | } 233 | 234 | return [locationLink]; 235 | } 236 | 237 | private async onDefinition(params: LSP.DefinitionParams): Promise { 238 | logger.debug('onDefinition'); 239 | 240 | const locationLink = await this.#analyzer.findDeclaration( 241 | params.textDocument.uri, 242 | params.position, 243 | ); 244 | if (locationLink == null) { 245 | return []; 246 | } 247 | 248 | return [locationLink]; 249 | } 250 | 251 | /** 252 | * Provide symbols defined in document. 253 | * 254 | * @param params Unused. 255 | * @returns Symbol information. 256 | */ 257 | private async onDocumentSymbol( 258 | params: LSP.DocumentSymbolParams, 259 | ): Promise { 260 | // TODO: ideally this should return LSP.DocumentSymbol[] instead of LSP.SymbolInformation[] 261 | // which is a hierarchy of symbols. 262 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol 263 | logger.debug(`onDocumentSymbol`); 264 | return this.#analyzer.getDeclarationsForUri(params.textDocument.uri); 265 | } 266 | } 267 | 268 | // Create a connection for the server, using Node's IPC as a transport. 269 | // Also include all preview / proposed LSP features. 270 | const connection = LSP.createConnection(LSP.ProposedFeatures.all); 271 | 272 | connection.onInitialize(async (params: LSP.InitializeParams): Promise => { 273 | const server = await ModelicaServer.initialize(connection, params); 274 | server.register(connection); 275 | return { 276 | capabilities: server.capabilities(), 277 | }; 278 | }); 279 | 280 | // Listen on the connection 281 | connection.listen(); 282 | -------------------------------------------------------------------------------- /server/src/analyzer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | /* ----------------------------------------------------------------------------- 37 | * Taken from bash-language-server and adapted to Modelica language server 38 | * https://github.com/bash-lsp/bash-language-server/blob/main/server/src/analyser.ts 39 | * ----------------------------------------------------------------------------- 40 | */ 41 | 42 | import * as LSP from 'vscode-languageserver/node'; 43 | import Parser from 'web-tree-sitter'; 44 | import * as path from 'node:path'; 45 | import * as fs from 'node:fs/promises'; 46 | import * as fsSync from 'node:fs'; 47 | import * as url from 'node:url'; 48 | 49 | import { 50 | UnresolvedAbsoluteReference, 51 | UnresolvedReference, 52 | UnresolvedRelativeReference, 53 | } from './analysis/reference'; 54 | import resolveReference from './analysis/resolveReference'; 55 | import { ModelicaDocument, ModelicaLibrary, ModelicaProject } from './project'; 56 | import { uriToPath } from './util'; 57 | import * as TreeSitterUtil from './util/tree-sitter'; 58 | import { getAllDeclarationsInTree } from './util/declarations'; 59 | import { logger } from './util/logger'; 60 | 61 | export default class Analyzer { 62 | #project: ModelicaProject; 63 | 64 | public constructor(parser: Parser) { 65 | this.#project = new ModelicaProject(parser); 66 | } 67 | 68 | /** 69 | * Adds a library (and all of its documents) to the analyzer. 70 | * 71 | * @param uri uri to the library root 72 | * @param isWorkspace `true` if this is a user workspace/project, `false` if 73 | * this is a library. 74 | */ 75 | public async loadLibrary(uri: LSP.URI, isWorkspace: boolean): Promise { 76 | const isLibrary = (folderPath: string) => 77 | fsSync.existsSync(path.join(folderPath, 'package.mo')); 78 | 79 | const libraryPath = url.fileURLToPath(uri); 80 | if (!isWorkspace || isLibrary(libraryPath)) { 81 | const lib = await ModelicaLibrary.load(this.#project, libraryPath, isWorkspace); 82 | this.#project.addLibrary(lib); 83 | return; 84 | } 85 | 86 | // TODO: go deeper... something like `TreeSitterUtil.forEach` but for files 87 | // would be good here 88 | for (const nestedRelative of await fs.readdir(libraryPath)) { 89 | const nested = path.resolve(nestedRelative); 90 | if (!isLibrary(nested)) { 91 | continue; 92 | } 93 | 94 | const library = await ModelicaLibrary.load(this.#project, nested, isWorkspace); 95 | this.#project.addLibrary(library); 96 | } 97 | } 98 | 99 | /** 100 | * Adds a document to the analyzer. 101 | * 102 | * Note: {@link loadLibrary} already adds all discovered documents to the 103 | * analyzer. It is only necessary to call this method on file creation. 104 | * 105 | * @param uri uri to document to add 106 | * @throws if the document does not belong to a library 107 | */ 108 | public async addDocument(uri: LSP.DocumentUri): Promise { 109 | await this.#project.addDocument(uriToPath(uri)); 110 | } 111 | 112 | /** 113 | * Submits a modification to a document. Ignores documents that have not been 114 | * added with {@link addDocument} or {@link loadLibrary}. 115 | * 116 | * @param uri uri to document to update 117 | * @param text the modification 118 | * @param range range to update, or `undefined` to replace the whole file 119 | */ 120 | public async updateDocument( 121 | uri: LSP.DocumentUri, 122 | text: string, 123 | range?: LSP.Range, 124 | ): Promise { 125 | await this.#project.updateDocument(uriToPath(uri), text, range); 126 | } 127 | 128 | /** 129 | * Removes a document from the analyzer. Ignores documents that have not been 130 | * added or have already been removed. 131 | * 132 | * @param uri uri to document to remove 133 | */ 134 | public removeDocument(uri: LSP.DocumentUri): void { 135 | this.#project.removeDocument(uriToPath(uri)); 136 | } 137 | 138 | /** 139 | * Get all symbol declarations in the given file. This is used for generating an outline. 140 | * 141 | * TODO: convert to DocumentSymbol[] which is a hierarchy of symbols found in a given text document. 142 | */ 143 | public async getDeclarationsForUri(uri: string): Promise { 144 | // TODO: convert to DocumentSymbol[] which is a hierarchy of symbols found 145 | // in a given text document. 146 | const path = uriToPath(uri); 147 | const document = await this.#project.getDocument(path); 148 | const tree = document?.tree; 149 | 150 | if (!tree?.rootNode) { 151 | return []; 152 | } 153 | 154 | return getAllDeclarationsInTree(tree, uri); 155 | } 156 | 157 | /** 158 | * Finds the position of the declaration of the symbol at the given position. 159 | * 160 | * @param uri the opened document 161 | * @param position the cursor position 162 | * @returns a {@link LSP.LocationLink} to the symbol's declaration, or `null` 163 | * if not found. 164 | */ 165 | public async findDeclaration( 166 | uri: LSP.DocumentUri, 167 | position: LSP.Position, 168 | ): Promise { 169 | const path = uriToPath(uri); 170 | logger.debug( 171 | `Searching for declaration of symbol at ${position.line + 1}:${ 172 | position.character + 1 173 | } in '${path}'`, 174 | ); 175 | 176 | const document = await this.#project.getDocument(path); 177 | if (!document) { 178 | logger.warn(`Couldn't find declaration: document not loaded.`); 179 | return null; 180 | } 181 | 182 | if (!document.tree.rootNode) { 183 | logger.info(`Couldn't find declaration: document has no nodes.`); 184 | return null; 185 | } 186 | 187 | const reference = this.getReferenceAt(document, position); 188 | if (!reference) { 189 | logger.info(`Tried to find declaration in '${path}', but not hovering on any identifiers`); 190 | return null; 191 | } 192 | 193 | logger.debug( 194 | `Searching for '${reference}' at ${position.line + 1}:${position.character + 1} in '${path}'`, 195 | ); 196 | 197 | try { 198 | const result = resolveReference(document.project, reference, 'declaration'); 199 | if (!result) { 200 | logger.debug(`Didn't find declaration of ${reference.symbols.join('.')}`); 201 | return null; 202 | } 203 | 204 | const link = TreeSitterUtil.createLocationLink(result.document, result.node); 205 | logger.debug(`Found declaration of ${reference.symbols.join('.')}: `, link); 206 | return link; 207 | } catch (e: unknown) { 208 | if (e instanceof Error) { 209 | logger.debug('Caught exception: ', e.stack); 210 | } else { 211 | logger.debug(`Caught:`, e); 212 | } 213 | return null; 214 | } 215 | } 216 | 217 | /** 218 | * Returns the reference at the document position, or `null` if no reference 219 | * exists. 220 | */ 221 | private getReferenceAt( 222 | document: ModelicaDocument, 223 | position: LSP.Position, 224 | ): UnresolvedReference | null { 225 | function checkBeforeCursor(node: Parser.SyntaxNode): boolean { 226 | if (node.startPosition.row < position.line) { 227 | return true; 228 | } 229 | return ( 230 | node.startPosition.row === position.line && node.startPosition.column <= position.character 231 | ); 232 | } 233 | 234 | const documentOffset = document.offsetAt(position); 235 | 236 | // First, check if this is a `type_specifier` or a `name`. 237 | let hoveredType = this.findNodeAtPosition( 238 | document.tree.rootNode, 239 | documentOffset, 240 | (node) => node.type === 'name', 241 | ); 242 | 243 | if (hoveredType) { 244 | if (hoveredType.parent?.type === 'type_specifier') { 245 | hoveredType = hoveredType.parent; 246 | } 247 | 248 | const declaredType = TreeSitterUtil.getTypeSpecifier(hoveredType); 249 | const symbols = declaredType.symbolNodes.filter(checkBeforeCursor).map((node) => node.text); 250 | 251 | if (declaredType.isGlobal) { 252 | return new UnresolvedAbsoluteReference(symbols, 'class'); 253 | } else { 254 | const startNode = this.findNodeAtPosition( 255 | hoveredType, 256 | documentOffset, 257 | (node) => node.type === 'IDENT', 258 | )!; 259 | 260 | return new UnresolvedRelativeReference(document, startNode, symbols, 'class'); 261 | } 262 | } 263 | 264 | // Next, check if this is a `component_reference`. 265 | const hoveredComponentReference = this.findNodeAtPosition( 266 | document.tree.rootNode, 267 | documentOffset, 268 | (node) => node.type === 'component_reference', 269 | ); 270 | if (hoveredComponentReference) { 271 | // TODO: handle array indices 272 | const componentReference = TreeSitterUtil.getComponentReference(hoveredComponentReference); 273 | const symbols = componentReference.componentNodes 274 | .filter(checkBeforeCursor) 275 | .map((node) => node.text); 276 | 277 | if (componentReference.isGlobal) { 278 | return new UnresolvedAbsoluteReference(symbols, 'variable'); 279 | } else { 280 | const startNode = this.findNodeAtPosition( 281 | hoveredComponentReference, 282 | documentOffset, 283 | (node) => node.type === 'IDENT', 284 | )!; 285 | 286 | return new UnresolvedRelativeReference(document, startNode, symbols, 'variable'); 287 | } 288 | } 289 | 290 | // Finally, give up and check if this is just an ident. 291 | const startNode = this.findNodeAtPosition( 292 | document.tree.rootNode, 293 | documentOffset, 294 | (node) => node.type === 'IDENT', 295 | ); 296 | if (startNode) { 297 | return new UnresolvedRelativeReference(document, startNode, [startNode.text]); 298 | } 299 | 300 | // We're not hovering over an identifier. 301 | return null; 302 | } 303 | 304 | /** 305 | * Locates the first node at the given text position that matches the given 306 | * `condition`, starting from the `rootNode`. 307 | * 308 | * Note: it is very important to have some kind of condition. If one tries to 309 | * just accept the first node at that position, this function will always 310 | * return the `rootNode` (or `undefined` if outside the node.) 311 | * 312 | * @param rootNode node to start searching from. parents/siblings of this node will be ignored 313 | * @param offset the offset of the symbol from the start of the document 314 | * @param condition the condition to check if a node is "good" 315 | * @returns the node at the position, or `undefined` if none was found 316 | */ 317 | private findNodeAtPosition( 318 | rootNode: Parser.SyntaxNode, 319 | offset: number, 320 | condition: (node: Parser.SyntaxNode) => boolean, 321 | ): Parser.SyntaxNode | undefined { 322 | // TODO: find the deepest node. findFirst doesn't work (maybe?) 323 | const hoveredNode = TreeSitterUtil.findFirst(rootNode, (node) => { 324 | const isInNode = offset >= node.startIndex && offset <= node.endIndex; 325 | return isInNode && condition(node); 326 | }); 327 | 328 | return hoveredNode ?? undefined; 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /server/src/util/tree-sitter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | /* ----------------------------------------------------------------------------- 37 | * Taken from bash-language-server and adapted to Modelica language server 38 | * https://github.com/bash-lsp/bash-language-server/blob/main/server/src/util/tree-sitter.ts 39 | * ----------------------------------------------------------------------------- 40 | */ 41 | 42 | import Parser from 'web-tree-sitter'; 43 | import * as LSP from 'vscode-languageserver/node'; 44 | import { SyntaxNode } from 'web-tree-sitter'; 45 | 46 | import { logger } from './logger'; 47 | import { TextDocument } from 'vscode-languageserver-textdocument'; 48 | 49 | /** 50 | * Recursively iterate over all nodes in a tree. 51 | * 52 | * @param node The node to start iterating from 53 | * @param callback The callback to call for each node. Return false to stop following children. 54 | */ 55 | export function forEach(node: SyntaxNode, callback: (n: SyntaxNode) => void | boolean) { 56 | const followChildren = callback(node) !== false; 57 | if (followChildren && node.children.length) { 58 | node.children.forEach((n) => forEach(n, callback)); 59 | } 60 | } 61 | 62 | /** 63 | * Find first node where callback returns true. 64 | * 65 | * Traverse tree depth first, left to right. 66 | * 67 | * @param start The node to start iterating from 68 | * @param callback Callback returning true if node is searched node. 69 | */ 70 | export function findFirst( 71 | start: SyntaxNode, 72 | callback: (n: SyntaxNode) => boolean, 73 | ): SyntaxNode | null { 74 | const cursor = start.walk(); 75 | let reachedRoot = false; 76 | let retracing = false; 77 | 78 | while (!reachedRoot) { 79 | const node = cursor.currentNode(); 80 | if (callback(node) === true) { 81 | return node; 82 | } 83 | 84 | if (cursor.gotoFirstChild()) { 85 | continue; 86 | } 87 | 88 | if (cursor.gotoNextSibling()) { 89 | continue; 90 | } 91 | 92 | retracing = true; 93 | while (retracing) { 94 | if (!cursor.gotoParent()) { 95 | retracing = false; 96 | reachedRoot = true; 97 | } 98 | 99 | if (cursor.gotoNextSibling()) { 100 | retracing = false; 101 | } 102 | } 103 | } 104 | 105 | return null; 106 | } 107 | 108 | export function range(n: SyntaxNode): LSP.Range { 109 | return LSP.Range.create( 110 | n.startPosition.row, 111 | n.startPosition.column, 112 | n.endPosition.row, 113 | n.endPosition.column, 114 | ); 115 | } 116 | 117 | /** 118 | * Tell if a node is a definition. 119 | * 120 | * @param n Node of tree 121 | * @returns `true` if node is a definition, `false` otherwise. 122 | */ 123 | export function isDefinition(n: SyntaxNode): boolean { 124 | switch (n.type) { 125 | case 'class_definition': 126 | return true; 127 | default: 128 | return false; 129 | } 130 | } 131 | 132 | /** 133 | * Tell if a node is a variable declaration. 134 | * 135 | * @param n Node of tree 136 | * @returns `true` if node is a variable declaration, `false` otherwise. 137 | */ 138 | export function isVariableDeclaration(n: SyntaxNode): boolean { 139 | switch (n.type) { 140 | case 'component_clause': 141 | case 'component_redeclaration': 142 | return true; 143 | case 'named_element': 144 | return n.childForFieldName('classDefinition') == null; 145 | default: 146 | return false; 147 | } 148 | } 149 | 150 | /** 151 | * Tell if a node is an element list. 152 | * 153 | * @param n Node of tree 154 | * @returns `true` if node is an element list, `false` otherwise. 155 | */ 156 | export function isElementList(n: SyntaxNode): boolean { 157 | switch (n.type) { 158 | case 'element_list': 159 | case 'public_element_list': 160 | case 'protected_element_list': 161 | return true; 162 | default: 163 | return false; 164 | } 165 | } 166 | 167 | export function findParent( 168 | start: SyntaxNode, 169 | predicate: (n: SyntaxNode) => boolean, 170 | ): SyntaxNode | null { 171 | let node = start.parent; 172 | while (node !== null) { 173 | if (predicate(node)) { 174 | return node; 175 | } 176 | node = node.parent; 177 | } 178 | return null; 179 | } 180 | 181 | /** 182 | * Get identifier from node. 183 | * 184 | * @param start Syntax tree node. 185 | */ 186 | export function getIdentifier(start: SyntaxNode): string | undefined { 187 | const node = findFirst(start, (n: SyntaxNode) => n.type == 'IDENT'); 188 | return node?.text; 189 | } 190 | 191 | /** 192 | * Returns the identifier(s) declared by the given node, or `[]` if no 193 | * identifiers are declared. 194 | * 195 | * Note: this does not return any identifiers that are declared "inside" of the 196 | * node. For instance, calling `getDeclaredIdentifiers` on a class_definition 197 | * will only return the name of the class. 198 | * 199 | * @param node The node to check. Must be a declaration. 200 | * @returns The identifiers. 201 | */ 202 | export function getDeclaredIdentifiers(node: SyntaxNode): string[] { 203 | if (node == null) { 204 | throw new Error('getDeclaredIdentifiers called with null/undefined node'); 205 | } 206 | 207 | // TODO: does this support all desired node types? Are we considering too many nodes? 208 | switch (node.type) { 209 | case 'declaration': 210 | case 'derivative_class_specifier': 211 | case 'enumeration_class_specifier': 212 | case 'extends_class_specifier': 213 | case 'long_class_specifier': 214 | case 'short_class_specifier': 215 | case 'enumeration_literal': 216 | case 'for_index': 217 | return [node.childForFieldName('identifier')!.text]; 218 | case 'stored_definitions': 219 | case 'component_list': 220 | case 'enum_list': 221 | case 'element_list': 222 | case 'public_element_list': 223 | case 'protected_element_list': 224 | case 'for_indices': 225 | return node.namedChildren.flatMap(getDeclaredIdentifiers); 226 | case 'component_clause': 227 | return getDeclaredIdentifiers(node.childForFieldName('componentDeclarations')!); 228 | case 'component_declaration': 229 | return getDeclaredIdentifiers(node.childForFieldName('declaration')!); 230 | case 'component_redeclaration': 231 | return getDeclaredIdentifiers(node.childForFieldName('componentClause')!); 232 | case 'stored_definition': 233 | return getDeclaredIdentifiers(node.childForFieldName('classDefinition')!); 234 | case 'class_definition': 235 | return getDeclaredIdentifiers(node.childForFieldName('classSpecifier')!); 236 | case 'for_equation': 237 | case 'for_statement': 238 | return getDeclaredIdentifiers(node.childForFieldName('indices')!); 239 | case 'named_element': { 240 | const definition = 241 | node.childForFieldName('classDefinition') ?? node.childForFieldName('componentClause')!; 242 | return getDeclaredIdentifiers(definition); 243 | } 244 | default: 245 | return []; 246 | } 247 | } 248 | 249 | export function hasIdentifier(node: SyntaxNode | null, identifier: string): boolean { 250 | if (!node) { 251 | return false; 252 | } 253 | 254 | return getDeclaredIdentifiers(node).includes(identifier); 255 | } 256 | 257 | export interface TypeSpecifier { 258 | isGlobal: boolean; 259 | symbols: string[]; 260 | symbolNodes: SyntaxNode[]; 261 | } 262 | 263 | export function getTypeSpecifier(node: SyntaxNode): TypeSpecifier { 264 | switch (node.type) { 265 | case 'type_specifier': { 266 | const isGlobal = node.childForFieldName('global') !== null; 267 | const name = node.childForFieldName('name')!; 268 | const symbolNodes = getNameIdentifiers(name); 269 | return { 270 | isGlobal, 271 | symbols: symbolNodes.map((id) => id.text), 272 | symbolNodes, 273 | }; 274 | } 275 | case 'name': { 276 | const symbolNodes = getNameIdentifiers(node); 277 | return { 278 | isGlobal: false, 279 | symbols: symbolNodes.map((id) => id.text), 280 | symbolNodes, 281 | }; 282 | } 283 | case 'IDENT': 284 | return { 285 | isGlobal: false, 286 | symbols: [node.text], 287 | symbolNodes: [node], 288 | }; 289 | default: { 290 | const typeSpecifier = findFirst(node, (child) => child.type === 'type_specifier'); 291 | if (typeSpecifier) { 292 | return getTypeSpecifier(typeSpecifier); 293 | } 294 | 295 | const name = findFirst(node, (child) => child.type === 'name'); 296 | if (name) { 297 | return getTypeSpecifier(name); 298 | } 299 | 300 | throw new Error('Syntax node does not contain a type_specifier or name'); 301 | } 302 | } 303 | } 304 | 305 | // TODO: this does not handle indexing arrays 306 | export interface ComponentReference { 307 | isGlobal: boolean; 308 | components: string[]; 309 | componentNodes: SyntaxNode[]; 310 | } 311 | 312 | export function getComponentReference(node: SyntaxNode): ComponentReference { 313 | switch (node.type) { 314 | case 'component_reference': { 315 | const isGlobal = node.childForFieldName('global') !== null; 316 | const componentNodes = getNameIdentifiers(node); 317 | 318 | return { 319 | isGlobal, 320 | components: componentNodes.map((id) => id.text), 321 | componentNodes, 322 | }; 323 | } 324 | case 'IDENT': 325 | return { 326 | isGlobal: false, 327 | components: [node.text], 328 | componentNodes: [node], 329 | }; 330 | default: { 331 | const componentRef = findFirst(node, (child) => child.type === 'component_reference'); 332 | if (componentRef) { 333 | return getComponentReference(componentRef); 334 | } 335 | 336 | throw new Error('Syntax node does not contain a component_reference'); 337 | } 338 | } 339 | } 340 | 341 | /** 342 | * Converts a name `SyntaxNode` into an array of the `IDENT`s in that node. 343 | */ 344 | function getNameIdentifiers(nameNode: SyntaxNode): Parser.SyntaxNode[] { 345 | if (nameNode.type !== 'name' && nameNode.type !== 'component_reference') { 346 | throw new Error( 347 | `Expected a 'name' or 'component_reference' node; got '${nameNode.type}' (${nameNode.text})`, 348 | ); 349 | } 350 | 351 | const identNode = nameNode.childForFieldName('identifier')!; 352 | const qualifierNode = nameNode.childForFieldName('qualifier'); 353 | if (qualifierNode) { 354 | const qualifier = getNameIdentifiers(qualifierNode); 355 | return [...qualifier, identNode]; 356 | } else { 357 | return [identNode]; 358 | } 359 | } 360 | 361 | /** 362 | * Get class prefixes from `class_definition` node. 363 | * 364 | * @param node Class definition node. 365 | * @returns String with class prefixes or `null` if no `class_prefixes` can be found. 366 | */ 367 | export function getClassPrefixes(node: SyntaxNode): string | null { 368 | if (node.type !== 'class_definition') { 369 | return null; 370 | } 371 | 372 | const classPrefixNode = node.childForFieldName('classPrefixes'); 373 | if (classPrefixNode == null || classPrefixNode.type !== 'class_prefixes') { 374 | return null; 375 | } 376 | 377 | return classPrefixNode.text; 378 | } 379 | 380 | export function positionToPoint(position: LSP.Position): Parser.Point { 381 | return { row: position.line, column: position.character }; 382 | } 383 | 384 | export function pointToPosition(point: Parser.Point): LSP.Position { 385 | return { line: point.row, character: point.column }; 386 | } 387 | 388 | export function createLocationLink( 389 | document: TextDocument, 390 | node: Parser.SyntaxNode, 391 | ): LSP.LocationLink; 392 | export function createLocationLink( 393 | documentUri: LSP.DocumentUri, 394 | node: Parser.SyntaxNode, 395 | ): LSP.LocationLink; 396 | export function createLocationLink( 397 | document: TextDocument | LSP.DocumentUri, 398 | node: Parser.SyntaxNode, 399 | ): LSP.LocationLink { 400 | // TODO: properly set targetSelectionRange (e.g. the name of a function or variable). 401 | return { 402 | targetUri: typeof document === 'string' ? document : document.uri, 403 | targetRange: { 404 | start: pointToPosition(node.startPosition), 405 | end: pointToPosition(node.endPosition), 406 | }, 407 | targetSelectionRange: { 408 | start: pointToPosition(node.startPosition), 409 | end: pointToPosition(node.endPosition), 410 | }, 411 | }; 412 | } 413 | -------------------------------------------------------------------------------- /OSMC-License.txt: -------------------------------------------------------------------------------- 1 | --- Start of Definition of OSMC Public License --- 2 | 3 | /* 4 | * This file is part of OpenModelica. 5 | * 6 | * Copyright (c) 1998-CurrentYear, Open Source Modelica Consortium (OSMC), 7 | * c/o Linköpings universitet, Department of Computer and Information Science, 8 | * SE-58183 Linköping, Sweden. 9 | * 10 | * All rights reserved. 11 | * 12 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 13 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 14 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 15 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 16 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 17 | * 18 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 19 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 20 | * address, from the URLs: 21 | * http://www.openmodelica.org or 22 | * https://github.com/OpenModelica/ or 23 | * http://www.ida.liu.se/projects/OpenModelica, 24 | * and in the OpenModelica distribution. 25 | * 26 | * GNU AGPL version 3 is obtained from: 27 | * https://www.gnu.org/licenses/licenses.html#GPL 28 | * 29 | * This program is distributed WITHOUT ANY WARRANTY; without 30 | * even the implied warranty of MERCHANTABILITY or FITNESS 31 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 32 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 33 | * 34 | * See the full OSMC Public License conditions for more details. 35 | * 36 | */ 37 | 38 | --- End of OSMC Public License Header --- 39 | 40 | The OSMC-PL is a public license for OpenModelica with three modes/alternatives 41 | (AGPL, OSMC-Internal-EPL, OSMC-External-EPL) for use and redistribution, 42 | in source and/or binary/object-code form: 43 | 44 | * AGPL. Any party (member or non-member of OSMC) may use and redistribute 45 | OpenModelica under GNU AGPL version 3. 46 | 47 | * Level 1 members of OSMC may also use and redistribute OpenModelica under 48 | OSMC-Internal-EPL conditions. 49 | 50 | * Level 2 members of OSMC may also use and redistribute OpenModelica under 51 | OSMC-Internal-EPL or OSMC-External-EPL conditions. 52 | Definitions of OSMC Public license modes: 53 | 54 | * AGPL = GNU AGPL version 3. 55 | 56 | * OSMC-Internal-EPL = These OSMC Public license conditions together with 57 | Internally restricted EPL, i.e., EPL version 1.0 with the Additional 58 | Condition that use and redistribution by an OSMC member is only allowed 59 | within the OSMC member's own organization (i.e., its own legal entity), 60 | or for an OSMC member paying an annual fee corresponding to the size 61 | of the organization including all its affiliates, use and redistribution 62 | is allowed within/between its affiliates. 63 | 64 | * OSMC-External-EPL = These OSMC Public license conditions together with 65 | Externally restricted EPL, i.e., EPL version 1.0 with the Additional 66 | Condition that use and redistribution by an OSMC member, or by a Licensed 67 | Third Party Distributor having a redistribution agreement with that member, 68 | to parties external to the OSMC member’s own organization (i.e., its own 69 | legal entity) is only allowed in binary/object-code form, except the case of 70 | redistribution to other OSMC members to which source is also allowed to be 71 | distributed. 72 | 73 | [This has the consequence that an external party who wishes to use 74 | OpenModelica in source form together with its own proprietary software in all 75 | cases must be a member of OSMC]. 76 | 77 | In all cases of usage and redistribution by recipients, the following 78 | conditions also apply: 79 | 80 | a) Redistributions of source code must retain the above copyright notice, 81 | all definitions, and conditions. It is sufficient if the OSMC-PL Header 82 | is present in each source file, if the full OSMC-PL is available in a 83 | prominent and easily located place in the redistribution. 84 | 85 | b) Redistributions in binary/object-code form must reproduce the above 86 | copyright notice, all definitions, and conditions. It is sufficient if the 87 | OSMC-PL Header and the location in the redistribution of the full OSMC-PL 88 | are present in the documentation and/or other materials provided with the 89 | redistribution, if the full OSMC-PL is available in a prominent and easily 90 | located place in the redistribution. 91 | 92 | c) A recipient must clearly indicate its chosen usage mode of OSMC-PL, 93 | in accompanying documentation and in a text file OSMC-USAGE-MODE.txt, 94 | provided with the distribution. 95 | 96 | d) Contributor(s) making a Contribution to OpenModelica thereby also makes a 97 | Transfer of Contribution Copyright. In return, upon the effective date of 98 | the transfer, OSMC grants the Contributor(s) a Contribution License of the 99 | Contribution. OSMC has the right to accept or refuse Contributions. 100 | 101 | Definitions: 102 | 103 | "Subsidiary license conditions" means: 104 | 105 | The additional license conditions depending on the by the recipient chosen 106 | mode of OSMC-PL, defined by GNU AGPL version 3.0 for AGPL, and by EPL for 107 | OSMC-Internal-EPL and OSMC-External-EPL. 108 | "OSMC-PL" means: 109 | 110 | Open Source Modelica Consortium Public License version 1.8, i.e., the license 111 | defined here (the text between 112 | "--- Start of Definition of OSMC Public License ---" and 113 | "--- End of Definition of OSMC Public License ---", or later versions thereof. 114 | 115 | "OSMC-PL Header" means: 116 | 117 | Open Source Modelica Consortium Public License Header version 1.8, i.e., the 118 | text between "--- Start of Definition of OSMC Public License ---" and 119 | "--- End of OSMC Public License Header ---", or later versions thereof. 120 | 121 | "Contribution" means: 122 | 123 | a) in the case of the initial Contributor, the initial code and documentation 124 | distributed under OSMC-PL, and 125 | 126 | b) in the case of each subsequent Contributor: 127 | i) changes to OpenModelica, and 128 | ii) additions to OpenModelica; 129 | 130 | where such changes and/or additions to OpenModelica originate from and are 131 | distributed by that particular Contributor. A Contribution 'originates' from 132 | a Contributor if it was added to OpenModelica by such Contributor itself or 133 | anyone acting on such Contributor's behalf. 134 | 135 | For Contributors licensing OpenModelica under OSMC-Internal-EPL or 136 | OSMC-External-EPL conditions, the following conditions also hold: 137 | 138 | Contributions do not include additions to the distributed Program which: (i) 139 | are separate modules of software distributed in conjunction with OpenModelica 140 | under their own license agreement, (ii) are separate modules which are not 141 | derivative works of OpenModelica, and (iii) are separate modules of software 142 | distributed in conjunction with OpenModelica under their own license agreement 143 | where these separate modules are merged with (weaved together with) modules of 144 | OpenModelica to form new modules that are distributed as object code or source 145 | code under their own license agreement, as allowed under the Additional 146 | Condition of internal distribution according to OSMC-Internal-EPL and/or 147 | Additional Condition for external distribution according to OSMC-External-EPL. 148 | 149 | "Transfer of Contribution Copyright" means that the Contributors of a 150 | Contribution transfer the ownership and the copyright of the Contribution to 151 | Open Source Modelica Consortium, the OpenModelica Copyright owner, for 152 | inclusion in OpenModelica. The transfer takes place upon the effective date 153 | when the Contribution is made available on the OSMC web site under OSMC-PL, by 154 | such Contributors themselves or anyone acting on such Contributors' behalf. 155 | The transfer is free of charge. If the Contributors or OSMC so wish, 156 | an optional Copyright transfer agreement can be signed between OSMC and the 157 | Contributors, as specified in an Appendix of the OSMC Bylaws. 158 | 159 | "Contribution License" means a license from OSMC to the Contributors of the 160 | Contribution, effective on the date of the Transfer of Contribution Copyright, 161 | where OSMC grants the Contributors a non-exclusive, world-wide, transferable, 162 | free of charge, perpetual license, including sublicensing rights, to use, 163 | have used, modify, have modified, reproduce and or have reproduced the 164 | contributed material, for business and other purposes, including but not 165 | limited to evaluation, development, testing, integration and merging with 166 | other software and distribution. The warranty and liability disclaimers of 167 | OSMC-PL apply to this license. 168 | 169 | "Contributor" means any person or entity that distributes (part of) 170 | OpenModelica. 171 | 172 | "The Program" means the Contributions distributed in accordance with OSMC-PL. 173 | 174 | "OpenModelica" means the Contributions distributed in accordance with OSMC-PL. 175 | 176 | "Recipient" means anyone who receives OpenModelica under OSMC-PL, 177 | including all Contributors. 178 | 179 | "Licensed Third Party Distributor" means a reseller/distributor having signed 180 | a redistribution/resale agreement in accordance with OSMC-PL and OSMC Bylaws, 181 | with an OSMC Level 2 organizational member which is not an Affiliate of the 182 | reseller/distributor, for distributing a product containing part(s) of 183 | OpenModelica. The Licensed Third Party Distributor shall only be allowed 184 | further redistribution to other resellers if the Level 2 member is granting 185 | such a right to it in the redistribution/resale agreement between the 186 | Level 2 member and the Licensed Third Party Distributor. 187 | 188 | "Affiliate" shall mean any legal entity, directly or indirectly, through one 189 | or more intermediaries, controlling or controlled by or under common control 190 | with any other legal entity, as the case may be. For purposes of this 191 | definition, the term "control" (including the terms "controlling", 192 | "controlled by" and "under common control with") means the possession, 193 | direct or indirect, of the power to direct or cause the direction of the 194 | management and policies of a legal entity, whether through the ownership of 195 | voting securities, by contract or otherwise. 196 | 197 | NO WARRANTY 198 | 199 | EXCEPT AS EXPRESSLY SET FORTH IN THE BY RECIPIENT SELECTED SUBSIDIARY 200 | LICENSE CONDITIONS OF OSMC-PL, OPENMODELICA IS PROVIDED ON AN "AS IS" 201 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 202 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 203 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 204 | PURPOSE. Each Recipient is solely responsible for determining the 205 | appropriateness of using and distributing OPENMODELICA and assumes all risks 206 | associated with its exercise of rights under OSMC-PL , including but not 207 | limited to the risks and costs of program errors, compliance with applicable 208 | laws, damage to or loss of data, programs or equipment, and unavailability 209 | or interruption of operations. 210 | 211 | DISCLAIMER OF LIABILITY 212 | 213 | EXCEPT AS EXPRESSLY SET FORTH IN THE BY RECIPIENT SELECTED SUBSIDIARY 214 | LICENSE CONDITIONS OF OSMC-PL, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 215 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 216 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 217 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 218 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 219 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF OPENMODELICA OR THE 220 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 221 | POSSIBILITY OF SUCH DAMAGES. 222 | 223 | A Contributor licensing OpenModelica under OSMC-Internal-EPL or 224 | OSMC-External-EPL may choose to distribute (parts of) OpenModelica in object 225 | code form under its own license agreement, provided that: 226 | 227 | a) it complies with the terms and conditions of OSMC-PL; or for the case of 228 | redistribution of OpenModelica together with proprietary code it is a dual 229 | license where the OpenModelica parts are distributed under OSMC-PL compatible 230 | conditions and the proprietary code is distributed under proprietary license 231 | conditions; and 232 | 233 | b) its license agreement: 234 | i) effectively disclaims on behalf of all Contributors all warranties and 235 | conditions, express and implied, including warranties or conditions of title 236 | and non-infringement, and implied warranties or conditions of merchantability 237 | and fitness for a particular purpose; 238 | ii) effectively excludes on behalf of all Contributors all liability for 239 | damages, including direct, indirect, special, incidental and consequential 240 | damages, such as lost profits; 241 | iii) states that any provisions which differ from OSMC-PL are offered by that 242 | Contributor alone and not by any other party; and 243 | iv) states from where the source code for OpenModelica is available, and 244 | informs licensees how to obtain it in a reasonable manner on or through a 245 | medium customarily used for software exchange. 246 | 247 | When OPENMODELICA is made available in source code form: 248 | 249 | a) it must be made available under OSMC-PL; and 250 | 251 | b) a copy of OSMC-PL must be included with each copy of OPENMODELICA. 252 | 253 | c) a copy of the subsidiary license associated with the selected mode of 254 | OSMC-PL must be included with each copy of OPENMODELICA. 255 | 256 | Contributors may not remove or alter any copyright notices contained within 257 | OPENMODELICA. 258 | 259 | If there is a conflict between OSMC-PL and the subsidiary license conditions, 260 | OSMC-PL has priority. 261 | 262 | This Agreement is governed by the laws of Sweden. The place of jurisdiction 263 | for all disagreements related to this Agreement, is Linköping, Sweden. 264 | 265 | The EPL 1.0 license definition has been obtained from: 266 | http://www.eclipse.org/legal/epl-v10.html. It is also reproduced in Appendix B 267 | of the OSMC Bylaws, and in the OpenModelica distribution. 268 | 269 | The AGPL Version 3 license definition has been obtained from 270 | https://www.gnu.org/licenses/licenses.html#GPL. It is also reproduced in 271 | Appendix C of the OSMC Bylaws, and in the OpenModelica distribution. 272 | 273 | --- End of Definition of OSMC Public License --- -------------------------------------------------------------------------------- /server/src/analysis/resolveReference.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of OpenModelica. 3 | * 4 | * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), 5 | * c/o Linköpings universitet, Department of Computer and Information Science, 6 | * SE-58183 Linköping, Sweden. 7 | * 8 | * All rights reserved. 9 | * 10 | * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR 11 | * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. 12 | * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES 13 | * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL 14 | * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. 15 | * 16 | * The OpenModelica software and the OSMC (Open Source Modelica Consortium) 17 | * Public License (OSMC-PL) are obtained from OSMC, either from the above 18 | * address, from the URLs: 19 | * http://www.openmodelica.org or 20 | * https://github.com/OpenModelica/ or 21 | * http://www.ida.liu.se/projects/OpenModelica, 22 | * and in the OpenModelica distribution. 23 | * 24 | * GNU AGPL version 3 is obtained from: 25 | * https://www.gnu.org/licenses/licenses.html#GPL 26 | * 27 | * This program is distributed WITHOUT ANY WARRANTY; without 28 | * even the implied warranty of MERCHANTABILITY or FITNESS 29 | * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH 30 | * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. 31 | * 32 | * See the full OSMC Public License conditions for more details. 33 | * 34 | */ 35 | 36 | import Parser from 'web-tree-sitter'; 37 | import * as fs from 'node:fs'; 38 | import * as path from 'node:path'; 39 | 40 | import * as TreeSitterUtil from '../util/tree-sitter'; 41 | import { 42 | ReferenceKind, 43 | ResolvedReference, 44 | UnresolvedAbsoluteReference, 45 | UnresolvedReference, 46 | UnresolvedRelativeReference, 47 | } from './reference'; 48 | import { logger } from '../util/logger'; 49 | import { ModelicaProject, ModelicaLibrary, ModelicaDocument } from '../project'; 50 | 51 | export type Resolution = 'declaration' | 'definition'; 52 | 53 | /** 54 | * Locates the declaration or definition of a symbol reference. 55 | * 56 | * @param project the project 57 | * @param reference a reference 58 | * @param resolution the kind of symbol to search for 59 | */ 60 | export default function resolveReference( 61 | project: ModelicaProject, 62 | reference: UnresolvedReference, 63 | resolution: Resolution, 64 | ): ResolvedReference | null { 65 | logger.debug(`Resolving ${resolution} ${reference}`); 66 | 67 | if (resolution === 'definition') { 68 | throw new Error('Resolving definitions not yet supported!'); 69 | } 70 | 71 | if (reference instanceof UnresolvedAbsoluteReference) { 72 | return resolveAbsoluteReference(project, reference); 73 | } 74 | 75 | for (const ref of getAbsoluteReferenceCandidates(reference)) { 76 | const resolved = resolveAbsoluteReference(project, ref); 77 | if (resolved) { 78 | return resolved; 79 | } 80 | } 81 | 82 | return null; 83 | } 84 | 85 | /** 86 | * Converts a relative reference to an absolute reference. 87 | * 88 | * @param reference a relative reference to a symbol declaration/definition 89 | * @returns an absolute reference to that symbol, or `null` if no such symbol exists. 90 | */ 91 | function* getAbsoluteReferenceCandidates( 92 | reference: UnresolvedRelativeReference, 93 | ): Generator { 94 | logger.debug(`Checking candidates for ${reference}`); 95 | 96 | for (const local of findReferenceInDocument(reference)) { 97 | if (local instanceof UnresolvedAbsoluteReference) { 98 | logger.debug(`Found ${local}`); 99 | yield local; 100 | continue; 101 | } 102 | 103 | const relativeReference = local ?? reference; 104 | 105 | const ancestors: string[] = []; 106 | let currentNode: Parser.SyntaxNode | null = relativeReference.node; 107 | while (currentNode) { 108 | if (currentNode.type === 'class_definition') { 109 | const identifier = TreeSitterUtil.getDeclaredIdentifiers(currentNode).at(0); 110 | if (identifier) { 111 | ancestors.unshift(identifier); 112 | } 113 | } 114 | 115 | currentNode = currentNode.parent; 116 | } 117 | 118 | if (relativeReference.node.type === 'class_definition') { 119 | ancestors.pop(); 120 | } 121 | 122 | logger.debug(`Found ${relativeReference} with ancestors: [${ancestors}]`); 123 | 124 | const classPath = [...relativeReference.document.within, ...ancestors]; 125 | while (true) { 126 | yield new UnresolvedAbsoluteReference( 127 | [...classPath, ...relativeReference.symbols], 128 | relativeReference.kind, 129 | ); 130 | if (classPath.length === 0) { 131 | break; 132 | } 133 | classPath.pop(); 134 | } 135 | } 136 | 137 | logger.debug(`Didn't find ${reference}`); 138 | } 139 | 140 | /** 141 | * Attempts to locate the definition of an `UnresolvedRelativeReference` within 142 | * the referenced document. 143 | * 144 | * If the reference is present in the document, an `UnresolvedRelativeReference` 145 | * pointing to the definition will be returned. If the reference may refer to an 146 | * import, an `UnresolvedAbsoluteReference` will be returned. If the reference 147 | * was not present at all, `undefined` will be returned. 148 | * 149 | * @param reference a reference to a local in which the `document` and `node` 150 | * properties reference the usage of the symbol. 151 | * @returns the reference candidates 152 | */ 153 | function* findReferenceInDocument( 154 | reference: UnresolvedRelativeReference, 155 | ): Generator { 156 | const maybeClass = reference.kind === 'class' || reference.kind === undefined; 157 | const maybeVariable = reference.kind === 'variable' || reference.kind === undefined; 158 | 159 | if (maybeClass) { 160 | if ( 161 | TreeSitterUtil.isDefinition(reference.node) && 162 | TreeSitterUtil.hasIdentifier(reference.node, reference.symbols[0]) 163 | ) { 164 | yield new UnresolvedRelativeReference( 165 | reference.document, 166 | reference.node, 167 | reference.symbols, 168 | 'class', 169 | ); 170 | return; 171 | } 172 | 173 | const classDecl = reference.node.children.find( 174 | (child) => 175 | TreeSitterUtil.isDefinition(child) && 176 | TreeSitterUtil.hasIdentifier(child, reference.symbols[0]), 177 | ); 178 | if (classDecl) { 179 | logger.debug('Found local class'); 180 | yield new UnresolvedRelativeReference( 181 | reference.document, 182 | classDecl, 183 | reference.symbols, 184 | 'class', 185 | ); 186 | return; 187 | } 188 | } 189 | 190 | if (maybeVariable) { 191 | const varDecl = reference.node.children.find( 192 | (child) => 193 | TreeSitterUtil.isVariableDeclaration(child) && 194 | TreeSitterUtil.hasIdentifier(child, reference.symbols[0]), 195 | ); 196 | if (varDecl) { 197 | logger.debug('Found local variable'); 198 | yield new UnresolvedRelativeReference( 199 | reference.document, 200 | varDecl, 201 | reference.symbols, 202 | 'variable', 203 | ); 204 | return; 205 | } 206 | } 207 | 208 | const declInClass = findDeclarationInClass( 209 | reference.document, 210 | reference.node, 211 | reference.symbols, 212 | reference.kind, 213 | ); 214 | if (declInClass) { 215 | yield declInClass; 216 | return; 217 | } 218 | 219 | const importClauses = reference.node.parent?.children.filter( 220 | (child) => child.type === 'import_clause', 221 | ); 222 | if (importClauses && importClauses.length > 0) { 223 | for (const importClause of importClauses) { 224 | const { importCandidate, wildcard } = resolveImportClause(reference.symbols, importClause); 225 | if (importCandidate) { 226 | if (wildcard) { 227 | yield importCandidate; 228 | } else { 229 | yield importCandidate; 230 | return; 231 | } 232 | } 233 | } 234 | } 235 | 236 | if (reference.node.parent) { 237 | yield* findReferenceInDocument( 238 | new UnresolvedRelativeReference( 239 | reference.document, 240 | reference.node.parent, 241 | reference.symbols, 242 | reference.kind, 243 | ), 244 | ); 245 | return; 246 | } 247 | 248 | logger.debug(`Not found in document. May be a global? ${reference}`); 249 | yield undefined; 250 | return; 251 | } 252 | 253 | /** 254 | * Searches for a declaration within a class (or a superclass). 255 | * 256 | * @param document the class' document 257 | * @param classNode the `class_definition` syntax node referencing the class 258 | * @param symbols the symbol to search for 259 | * @param referenceKind the type of reference 260 | * @returns an unresolved reference to the symbol, or `undefined` if not present 261 | */ 262 | function findDeclarationInClass( 263 | document: ModelicaDocument, 264 | classNode: Parser.SyntaxNode, 265 | symbols: string[], 266 | referenceKind: ReferenceKind | undefined, 267 | ): (UnresolvedRelativeReference & { kind: ReferenceKind }) | undefined { 268 | if (classNode.type !== 'class_definition') { 269 | return undefined; 270 | } 271 | 272 | const elements = classNode 273 | .childForFieldName('classSpecifier') 274 | ?.children?.filter(TreeSitterUtil.isElementList) 275 | ?.flatMap((element_list) => element_list.namedChildren) 276 | ?.map((element) => [element, TreeSitterUtil.getDeclaredIdentifiers(element)] as const); 277 | 278 | if (!elements) { 279 | logger.debug("Didn't find declaration in class"); 280 | return undefined; 281 | } 282 | 283 | const namedElement = elements.find( 284 | ([element, idents]) => element.type === 'named_element' && idents.includes(symbols[0]), 285 | ); 286 | if (namedElement) { 287 | logger.debug(`Resolved ${symbols[0]} to field: ${namedElement[1]}`); 288 | 289 | const classDef = namedElement[0].childForFieldName('classDefinition'); 290 | if (classDef) { 291 | return new UnresolvedRelativeReference( 292 | document, 293 | classDef, 294 | symbols, 295 | 'class', 296 | ) as UnresolvedRelativeReference & { kind: 'class' }; 297 | } 298 | 299 | const componentDef = namedElement[0].childForFieldName('componentClause')!; 300 | 301 | // TODO: this handles named_elements but what if it's an import clause? 302 | return new UnresolvedRelativeReference( 303 | document, 304 | componentDef, 305 | symbols, 306 | 'variable', 307 | ) as UnresolvedRelativeReference & { kind: 'variable' }; 308 | } 309 | 310 | // only check superclasses if we know we're not looking for a class 311 | if (referenceKind !== 'class') { 312 | const extendsClauses = elements 313 | .map(([element, _idents]) => element) 314 | .filter((element) => element.type === 'extends_clause'); 315 | for (const extendsClause of extendsClauses) { 316 | const superclassType = TreeSitterUtil.getTypeSpecifier(extendsClause); 317 | const unresolvedSuperclass = superclassType.isGlobal 318 | ? new UnresolvedAbsoluteReference(superclassType.symbols, 'class') 319 | : new UnresolvedRelativeReference(document, extendsClause, superclassType.symbols, 'class'); 320 | 321 | logger.debug( 322 | `Resolving superclass ${unresolvedSuperclass} (of ${ 323 | TreeSitterUtil.getDeclaredIdentifiers(classNode)[0] 324 | })`, 325 | ); 326 | 327 | // TODO: support "definition" resolution 328 | const superclass = resolveReference(document.project, unresolvedSuperclass, 'declaration'); 329 | if (!superclass) { 330 | logger.warn(`Could not find superclass ${unresolvedSuperclass}`); 331 | continue; 332 | } 333 | 334 | logger.debug(`Checking superclass ${superclass}`); 335 | const decl = findDeclarationInClass( 336 | superclass.document, 337 | superclass.node, 338 | symbols, 339 | 'variable', 340 | ); 341 | if (decl) { 342 | logger.debug(`Declaration ${decl} found in superclass ${superclass}`); 343 | return decl; 344 | } 345 | } 346 | } 347 | 348 | return undefined; 349 | } 350 | 351 | interface ResolveImportClauseResult { 352 | /** 353 | * The resolved import candidate, or `undefined` if none was found. 354 | */ 355 | importCandidate?: UnresolvedAbsoluteReference; 356 | /** 357 | * `true` if this was a wildcard import, and we are not sure if this import 358 | * even exists. `false` if this was not a wildcard import. 359 | */ 360 | wildcard: boolean; 361 | } 362 | 363 | /** 364 | * Given an import clause and a potentially-imported symbol, returns an 365 | * unresolved reference to check. 366 | * 367 | * @param symbols a symbol that may have been imported 368 | * @param importClause an import clause 369 | * @returns the resolved import 370 | */ 371 | function resolveImportClause( 372 | symbols: string[], 373 | importClause: Parser.SyntaxNode, 374 | ): ResolveImportClauseResult { 375 | // imports are always relative according to the grammar 376 | const importPath = TreeSitterUtil.getTypeSpecifier( 377 | importClause.childForFieldName('name')!, 378 | ).symbols; 379 | 380 | // wildcard import: import a.b.*; 381 | const isWildcard = importClause.childForFieldName('wildcard') != null; 382 | if (isWildcard) { 383 | const importCandidate = new UnresolvedAbsoluteReference([...importPath, ...symbols]); 384 | logger.debug(`Candidate: ${importCandidate} (from import ${importPath.join('.')}.*)`); 385 | 386 | // TODO: this should probably not resolve the reference fully, then immediately 387 | // discard it so it can do so again. 388 | return { importCandidate, wildcard: true }; 389 | } 390 | 391 | // import alias: import z = a.b.c; 392 | // TODO: Determine if import aliases should be counted as "declarations". 393 | // If so, then we should stop here for decls when symbols.length == 1. 394 | const alias = importClause.childForFieldName('alias')?.text; 395 | if (alias && alias === symbols[0]) { 396 | const importCandidate = new UnresolvedAbsoluteReference([...importPath, ...symbols.slice(1)]); 397 | logger.debug(`Candidate: ${importCandidate} (from import ${alias} = ${importPath.join('.')})`); 398 | 399 | return { importCandidate, wildcard: false }; 400 | } 401 | 402 | // multi-import: import a.b.{c, d, e}; 403 | const childImports = importClause 404 | .childForFieldName('imports') 405 | ?.namedChildren?.filter((node) => node.type === 'IDENT') 406 | ?.map((node) => node.text); 407 | 408 | if (childImports?.some((name) => name === symbols[0])) { 409 | const importCandidate = new UnresolvedAbsoluteReference([...importPath, ...symbols]); 410 | const importString = `import ${importPath.join('.')}.{ ${childImports.join(', ')} }`; 411 | logger.debug(`Candidate: ${importCandidate} (from ${importString})`); 412 | 413 | return { importCandidate, wildcard: false }; 414 | } 415 | 416 | // normal import: import a.b.c; 417 | if (importPath.at(-1) === symbols[0]) { 418 | const importCandidate = new UnresolvedAbsoluteReference([...importPath, ...symbols.slice(1)]); 419 | logger.debug(`Candidate: ${importCandidate} (from import ${importPath.join('.')})`); 420 | 421 | return { importCandidate, wildcard: false }; 422 | } 423 | 424 | return { wildcard: false }; 425 | } 426 | 427 | /** 428 | * Locates the declaration/definition of an absolute symbol reference. 429 | * 430 | * @param reference an absolute reference 431 | * @returns a resolved reference, or `null` if no such symbol exists 432 | */ 433 | function resolveAbsoluteReference( 434 | project: ModelicaProject, 435 | reference: UnresolvedAbsoluteReference, 436 | ): ResolvedReference | null { 437 | if (!(reference instanceof UnresolvedAbsoluteReference)) { 438 | throw new Error(`Reference is not an UnresolvedAbsoluteReference: ${reference}`); 439 | } 440 | 441 | logger.debug(`Resolving ${reference}`); 442 | 443 | logger.debug(project.libraries.map((x) => x.name + ' | ' + x.path).join('\n\t')); 444 | const library = project.libraries.find((lib) => lib.name === reference.symbols[0]); 445 | if (library == null) { 446 | logger.debug(`Couldn't find library: ${reference.symbols[0]}`); 447 | return null; 448 | } 449 | 450 | let alreadyResolved: ResolvedReference | null = null; 451 | for (let i = 0; i < reference.symbols.length; i++) { 452 | alreadyResolved = resolveNext(library, reference.symbols[i], alreadyResolved); 453 | if (alreadyResolved == null) { 454 | return null; 455 | } 456 | 457 | // If we're not done with the reference chain, we need to make sure that we 458 | // know the type of the variable in order to check its child variables 459 | if ( 460 | i < reference.symbols.length - 1 && 461 | TreeSitterUtil.isVariableDeclaration(alreadyResolved.node) 462 | ) { 463 | const classRef = variableRefToClassRef(alreadyResolved); 464 | if (classRef == null) { 465 | logger.debug(`Failed to find type of var ${alreadyResolved}`); 466 | return null; 467 | } 468 | 469 | alreadyResolved = classRef; 470 | } 471 | } 472 | 473 | logger.debug(`Resolved symbol ${alreadyResolved}`); 474 | 475 | return alreadyResolved; 476 | } 477 | 478 | /** 479 | * Performs a single iteration of the resolution algorithm. 480 | * 481 | * @param nextSymbol the next symbol to resolve 482 | * @param parentReference a resolved reference (to a class) 483 | * @returns the next resolved reference 484 | */ 485 | function resolveNext( 486 | library: ModelicaLibrary, 487 | nextSymbol: string, 488 | parentReference: ResolvedReference | null, 489 | ): ResolvedReference | null { 490 | // If at the root level, find the root package 491 | if (!parentReference) { 492 | let documentPath = path.join(library.path, 'package.mo'); 493 | if (!fs.existsSync(documentPath)) { 494 | documentPath = path.join(library.path, library.name + '.mo'); 495 | } 496 | 497 | const [document, packageClass] = getPackageClassFromFilePath(library, documentPath, nextSymbol); 498 | if (!document || !packageClass) { 499 | logger.debug(`Couldn't find package class: ${nextSymbol} in ${documentPath}`); 500 | return null; 501 | } 502 | 503 | return new ResolvedReference(document, packageClass, [nextSymbol], 'class'); 504 | } 505 | 506 | const dirName = path.dirname(parentReference.document.path); 507 | const potentialPaths = [ 508 | path.join(dirName, `${nextSymbol}.mo`), 509 | path.join(dirName, `${nextSymbol}/package.mo`), 510 | ]; 511 | 512 | for (const documentPath of potentialPaths) { 513 | if (!fs.existsSync(documentPath)) { 514 | continue; 515 | } 516 | 517 | const [document, packageClass] = getPackageClassFromFilePath(library, documentPath, nextSymbol); 518 | if (!document || !packageClass) { 519 | logger.debug(`Couldn't find package class: ${nextSymbol} in ${documentPath}`); 520 | return null; 521 | } 522 | 523 | return new ResolvedReference( 524 | document, 525 | packageClass, 526 | [...parentReference.symbols, nextSymbol], 527 | 'class', 528 | ); 529 | } 530 | 531 | // TODO: The `kind` parameter here should be `undefined` unless 532 | // `resolveReference` was called with kind = "class" by the superclass 533 | // handling section in findDeclarationInClass. ...or something like that 534 | // As it is now, we don't know if `child` is a class or variable. We can't use 535 | // `undefined` to indicate this because this results in infinite recursion. 536 | // This issue causes us to be unable to look up variables declared in a 537 | // superclass of a member variable. A redesign might be necessary to resolve 538 | // this. Perhaps if we could keep track of which classes we already visited, 539 | // we wouldn't need the whole "class"/"variable" trick at all! 540 | const child = findDeclarationInClass( 541 | parentReference.document, 542 | parentReference.node, 543 | [nextSymbol], 544 | parentReference.kind, 545 | ); 546 | if (child) { 547 | return new ResolvedReference( 548 | child.document, 549 | child.node, 550 | [...parentReference.symbols, nextSymbol], 551 | child.kind, 552 | ); 553 | } 554 | 555 | logger.debug(`Couldn't find: .${parentReference.symbols.join('.')}.${nextSymbol}`); 556 | 557 | return null; 558 | } 559 | 560 | function getPackageClassFromFilePath( 561 | library: ModelicaLibrary, 562 | filePath: string, 563 | symbol: string, 564 | ): [ModelicaDocument | undefined, Parser.SyntaxNode | undefined] { 565 | const document = library.documents.get(filePath); 566 | if (!document) { 567 | logger.debug(`getPackageClassFromFilePath: Couldn't find document ${filePath}`); 568 | return [undefined, undefined]; 569 | } 570 | 571 | const node = TreeSitterUtil.findFirst( 572 | document.tree.rootNode, 573 | (child) => child.type === 'class_definition' && TreeSitterUtil.hasIdentifier(child, symbol), 574 | ); 575 | if (!node) { 576 | logger.debug( 577 | `getPackageClassFromFilePath: Couldn't find package class node ${symbol} in ${filePath}`, 578 | ); 579 | return [document, undefined]; 580 | } 581 | 582 | return [document, node]; 583 | } 584 | 585 | /** 586 | * Finds the type of a variable declaration and returns a reference to that 587 | * type. 588 | * 589 | * @param varRef a reference to a variable declaration/definition 590 | * @returns a reference to the class definition, or `null` if the type is not a 591 | * class (e.g. a builtin like `Real`) 592 | */ 593 | function variableRefToClassRef(varRef: ResolvedReference): ResolvedReference | null { 594 | const type = TreeSitterUtil.getTypeSpecifier(varRef.node); 595 | 596 | const typeRef = type.isGlobal 597 | ? new UnresolvedAbsoluteReference(type.symbols, 'class') 598 | : new UnresolvedRelativeReference(varRef.document, varRef.node, type.symbols, 'class'); 599 | 600 | return resolveReference(varRef.document.project, typeRef, 'declaration'); 601 | } 602 | -------------------------------------------------------------------------------- /client/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modelica-language-server-client", 3 | "version": "0.2.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "modelica-language-server-client", 9 | "version": "0.2.0", 10 | "license": "OSMC-PL-1-8", 11 | "dependencies": { 12 | "vscode-languageclient": "^8.1.0" 13 | }, 14 | "devDependencies": { 15 | "@types/vscode": "^1.75.1", 16 | "@vscode/test-electron": "^2.3.8" 17 | }, 18 | "engines": { 19 | "node": "20", 20 | "vscode": "^1.75.0" 21 | } 22 | }, 23 | "node_modules/@types/vscode": { 24 | "version": "1.91.0", 25 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.91.0.tgz", 26 | "integrity": "sha512-PgPr+bUODjG3y+ozWUCyzttqR9EHny9sPAfJagddQjDwdtf66y2sDKJMnFZRuzBA2YtBGASqJGPil8VDUPvO6A==", 27 | "dev": true 28 | }, 29 | "node_modules/@vscode/test-electron": { 30 | "version": "2.4.0", 31 | "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.0.tgz", 32 | "integrity": "sha512-yojuDFEjohx6Jb+x949JRNtSn6Wk2FAh4MldLE3ck9cfvCqzwxF32QsNy1T9Oe4oT+ZfFcg0uPUCajJzOmPlTA==", 33 | "dev": true, 34 | "dependencies": { 35 | "http-proxy-agent": "^7.0.2", 36 | "https-proxy-agent": "^7.0.4", 37 | "jszip": "^3.10.1", 38 | "ora": "^7.0.1", 39 | "semver": "^7.6.2" 40 | }, 41 | "engines": { 42 | "node": ">=16" 43 | } 44 | }, 45 | "node_modules/agent-base": { 46 | "version": "7.1.1", 47 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", 48 | "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", 49 | "dev": true, 50 | "dependencies": { 51 | "debug": "^4.3.4" 52 | }, 53 | "engines": { 54 | "node": ">= 14" 55 | } 56 | }, 57 | "node_modules/ansi-regex": { 58 | "version": "6.0.1", 59 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", 60 | "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", 61 | "dev": true, 62 | "engines": { 63 | "node": ">=12" 64 | }, 65 | "funding": { 66 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 67 | } 68 | }, 69 | "node_modules/balanced-match": { 70 | "version": "1.0.2", 71 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 72 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 73 | }, 74 | "node_modules/base64-js": { 75 | "version": "1.5.1", 76 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 77 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 78 | "dev": true, 79 | "funding": [ 80 | { 81 | "type": "github", 82 | "url": "https://github.com/sponsors/feross" 83 | }, 84 | { 85 | "type": "patreon", 86 | "url": "https://www.patreon.com/feross" 87 | }, 88 | { 89 | "type": "consulting", 90 | "url": "https://feross.org/support" 91 | } 92 | ] 93 | }, 94 | "node_modules/bl": { 95 | "version": "5.1.0", 96 | "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", 97 | "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", 98 | "dev": true, 99 | "dependencies": { 100 | "buffer": "^6.0.3", 101 | "inherits": "^2.0.4", 102 | "readable-stream": "^3.4.0" 103 | } 104 | }, 105 | "node_modules/bl/node_modules/readable-stream": { 106 | "version": "3.6.2", 107 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 108 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 109 | "dev": true, 110 | "dependencies": { 111 | "inherits": "^2.0.3", 112 | "string_decoder": "^1.1.1", 113 | "util-deprecate": "^1.0.1" 114 | }, 115 | "engines": { 116 | "node": ">= 6" 117 | } 118 | }, 119 | "node_modules/brace-expansion": { 120 | "version": "2.0.1", 121 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 122 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 123 | "dependencies": { 124 | "balanced-match": "^1.0.0" 125 | } 126 | }, 127 | "node_modules/buffer": { 128 | "version": "6.0.3", 129 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", 130 | "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", 131 | "dev": true, 132 | "funding": [ 133 | { 134 | "type": "github", 135 | "url": "https://github.com/sponsors/feross" 136 | }, 137 | { 138 | "type": "patreon", 139 | "url": "https://www.patreon.com/feross" 140 | }, 141 | { 142 | "type": "consulting", 143 | "url": "https://feross.org/support" 144 | } 145 | ], 146 | "dependencies": { 147 | "base64-js": "^1.3.1", 148 | "ieee754": "^1.2.1" 149 | } 150 | }, 151 | "node_modules/chalk": { 152 | "version": "5.3.0", 153 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", 154 | "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", 155 | "dev": true, 156 | "engines": { 157 | "node": "^12.17.0 || ^14.13 || >=16.0.0" 158 | }, 159 | "funding": { 160 | "url": "https://github.com/chalk/chalk?sponsor=1" 161 | } 162 | }, 163 | "node_modules/cli-cursor": { 164 | "version": "4.0.0", 165 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", 166 | "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", 167 | "dev": true, 168 | "dependencies": { 169 | "restore-cursor": "^4.0.0" 170 | }, 171 | "engines": { 172 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 173 | }, 174 | "funding": { 175 | "url": "https://github.com/sponsors/sindresorhus" 176 | } 177 | }, 178 | "node_modules/cli-spinners": { 179 | "version": "2.9.2", 180 | "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", 181 | "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", 182 | "dev": true, 183 | "engines": { 184 | "node": ">=6" 185 | }, 186 | "funding": { 187 | "url": "https://github.com/sponsors/sindresorhus" 188 | } 189 | }, 190 | "node_modules/core-util-is": { 191 | "version": "1.0.3", 192 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 193 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", 194 | "dev": true 195 | }, 196 | "node_modules/debug": { 197 | "version": "4.3.5", 198 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", 199 | "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", 200 | "dev": true, 201 | "dependencies": { 202 | "ms": "2.1.2" 203 | }, 204 | "engines": { 205 | "node": ">=6.0" 206 | }, 207 | "peerDependenciesMeta": { 208 | "supports-color": { 209 | "optional": true 210 | } 211 | } 212 | }, 213 | "node_modules/eastasianwidth": { 214 | "version": "0.2.0", 215 | "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 216 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 217 | "dev": true 218 | }, 219 | "node_modules/emoji-regex": { 220 | "version": "10.3.0", 221 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", 222 | "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", 223 | "dev": true 224 | }, 225 | "node_modules/http-proxy-agent": { 226 | "version": "7.0.2", 227 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", 228 | "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", 229 | "dev": true, 230 | "dependencies": { 231 | "agent-base": "^7.1.0", 232 | "debug": "^4.3.4" 233 | }, 234 | "engines": { 235 | "node": ">= 14" 236 | } 237 | }, 238 | "node_modules/https-proxy-agent": { 239 | "version": "7.0.5", 240 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", 241 | "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", 242 | "dev": true, 243 | "dependencies": { 244 | "agent-base": "^7.0.2", 245 | "debug": "4" 246 | }, 247 | "engines": { 248 | "node": ">= 14" 249 | } 250 | }, 251 | "node_modules/ieee754": { 252 | "version": "1.2.1", 253 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 254 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 255 | "dev": true, 256 | "funding": [ 257 | { 258 | "type": "github", 259 | "url": "https://github.com/sponsors/feross" 260 | }, 261 | { 262 | "type": "patreon", 263 | "url": "https://www.patreon.com/feross" 264 | }, 265 | { 266 | "type": "consulting", 267 | "url": "https://feross.org/support" 268 | } 269 | ] 270 | }, 271 | "node_modules/immediate": { 272 | "version": "3.0.6", 273 | "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", 274 | "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", 275 | "dev": true 276 | }, 277 | "node_modules/inherits": { 278 | "version": "2.0.4", 279 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 280 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 281 | "dev": true 282 | }, 283 | "node_modules/is-interactive": { 284 | "version": "2.0.0", 285 | "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", 286 | "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", 287 | "dev": true, 288 | "engines": { 289 | "node": ">=12" 290 | }, 291 | "funding": { 292 | "url": "https://github.com/sponsors/sindresorhus" 293 | } 294 | }, 295 | "node_modules/is-unicode-supported": { 296 | "version": "1.3.0", 297 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", 298 | "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", 299 | "dev": true, 300 | "engines": { 301 | "node": ">=12" 302 | }, 303 | "funding": { 304 | "url": "https://github.com/sponsors/sindresorhus" 305 | } 306 | }, 307 | "node_modules/isarray": { 308 | "version": "1.0.0", 309 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 310 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", 311 | "dev": true 312 | }, 313 | "node_modules/jszip": { 314 | "version": "3.10.1", 315 | "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", 316 | "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", 317 | "dev": true, 318 | "dependencies": { 319 | "lie": "~3.3.0", 320 | "pako": "~1.0.2", 321 | "readable-stream": "~2.3.6", 322 | "setimmediate": "^1.0.5" 323 | } 324 | }, 325 | "node_modules/lie": { 326 | "version": "3.3.0", 327 | "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", 328 | "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", 329 | "dev": true, 330 | "dependencies": { 331 | "immediate": "~3.0.5" 332 | } 333 | }, 334 | "node_modules/log-symbols": { 335 | "version": "5.1.0", 336 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", 337 | "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", 338 | "dev": true, 339 | "dependencies": { 340 | "chalk": "^5.0.0", 341 | "is-unicode-supported": "^1.1.0" 342 | }, 343 | "engines": { 344 | "node": ">=12" 345 | }, 346 | "funding": { 347 | "url": "https://github.com/sponsors/sindresorhus" 348 | } 349 | }, 350 | "node_modules/mimic-fn": { 351 | "version": "2.1.0", 352 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 353 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 354 | "dev": true, 355 | "engines": { 356 | "node": ">=6" 357 | } 358 | }, 359 | "node_modules/minimatch": { 360 | "version": "5.1.6", 361 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", 362 | "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", 363 | "dependencies": { 364 | "brace-expansion": "^2.0.1" 365 | }, 366 | "engines": { 367 | "node": ">=10" 368 | } 369 | }, 370 | "node_modules/ms": { 371 | "version": "2.1.2", 372 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 373 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 374 | "dev": true 375 | }, 376 | "node_modules/onetime": { 377 | "version": "5.1.2", 378 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 379 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 380 | "dev": true, 381 | "dependencies": { 382 | "mimic-fn": "^2.1.0" 383 | }, 384 | "engines": { 385 | "node": ">=6" 386 | }, 387 | "funding": { 388 | "url": "https://github.com/sponsors/sindresorhus" 389 | } 390 | }, 391 | "node_modules/ora": { 392 | "version": "7.0.1", 393 | "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", 394 | "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", 395 | "dev": true, 396 | "dependencies": { 397 | "chalk": "^5.3.0", 398 | "cli-cursor": "^4.0.0", 399 | "cli-spinners": "^2.9.0", 400 | "is-interactive": "^2.0.0", 401 | "is-unicode-supported": "^1.3.0", 402 | "log-symbols": "^5.1.0", 403 | "stdin-discarder": "^0.1.0", 404 | "string-width": "^6.1.0", 405 | "strip-ansi": "^7.1.0" 406 | }, 407 | "engines": { 408 | "node": ">=16" 409 | }, 410 | "funding": { 411 | "url": "https://github.com/sponsors/sindresorhus" 412 | } 413 | }, 414 | "node_modules/pako": { 415 | "version": "1.0.11", 416 | "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", 417 | "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", 418 | "dev": true 419 | }, 420 | "node_modules/process-nextick-args": { 421 | "version": "2.0.1", 422 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 423 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", 424 | "dev": true 425 | }, 426 | "node_modules/readable-stream": { 427 | "version": "2.3.8", 428 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", 429 | "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", 430 | "dev": true, 431 | "dependencies": { 432 | "core-util-is": "~1.0.0", 433 | "inherits": "~2.0.3", 434 | "isarray": "~1.0.0", 435 | "process-nextick-args": "~2.0.0", 436 | "safe-buffer": "~5.1.1", 437 | "string_decoder": "~1.1.1", 438 | "util-deprecate": "~1.0.1" 439 | } 440 | }, 441 | "node_modules/restore-cursor": { 442 | "version": "4.0.0", 443 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", 444 | "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", 445 | "dev": true, 446 | "dependencies": { 447 | "onetime": "^5.1.0", 448 | "signal-exit": "^3.0.2" 449 | }, 450 | "engines": { 451 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 452 | }, 453 | "funding": { 454 | "url": "https://github.com/sponsors/sindresorhus" 455 | } 456 | }, 457 | "node_modules/safe-buffer": { 458 | "version": "5.1.2", 459 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 460 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", 461 | "dev": true 462 | }, 463 | "node_modules/semver": { 464 | "version": "7.6.2", 465 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", 466 | "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", 467 | "bin": { 468 | "semver": "bin/semver.js" 469 | }, 470 | "engines": { 471 | "node": ">=10" 472 | } 473 | }, 474 | "node_modules/setimmediate": { 475 | "version": "1.0.5", 476 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", 477 | "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", 478 | "dev": true 479 | }, 480 | "node_modules/signal-exit": { 481 | "version": "3.0.7", 482 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", 483 | "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", 484 | "dev": true 485 | }, 486 | "node_modules/stdin-discarder": { 487 | "version": "0.1.0", 488 | "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", 489 | "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", 490 | "dev": true, 491 | "dependencies": { 492 | "bl": "^5.0.0" 493 | }, 494 | "engines": { 495 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 496 | }, 497 | "funding": { 498 | "url": "https://github.com/sponsors/sindresorhus" 499 | } 500 | }, 501 | "node_modules/string_decoder": { 502 | "version": "1.1.1", 503 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 504 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 505 | "dev": true, 506 | "dependencies": { 507 | "safe-buffer": "~5.1.0" 508 | } 509 | }, 510 | "node_modules/string-width": { 511 | "version": "6.1.0", 512 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", 513 | "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", 514 | "dev": true, 515 | "dependencies": { 516 | "eastasianwidth": "^0.2.0", 517 | "emoji-regex": "^10.2.1", 518 | "strip-ansi": "^7.0.1" 519 | }, 520 | "engines": { 521 | "node": ">=16" 522 | }, 523 | "funding": { 524 | "url": "https://github.com/sponsors/sindresorhus" 525 | } 526 | }, 527 | "node_modules/strip-ansi": { 528 | "version": "7.1.0", 529 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 530 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 531 | "dev": true, 532 | "dependencies": { 533 | "ansi-regex": "^6.0.1" 534 | }, 535 | "engines": { 536 | "node": ">=12" 537 | }, 538 | "funding": { 539 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 540 | } 541 | }, 542 | "node_modules/util-deprecate": { 543 | "version": "1.0.2", 544 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 545 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 546 | "dev": true 547 | }, 548 | "node_modules/vscode-jsonrpc": { 549 | "version": "8.1.0", 550 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", 551 | "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", 552 | "engines": { 553 | "node": ">=14.0.0" 554 | } 555 | }, 556 | "node_modules/vscode-languageclient": { 557 | "version": "8.1.0", 558 | "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", 559 | "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", 560 | "dependencies": { 561 | "minimatch": "^5.1.0", 562 | "semver": "^7.3.7", 563 | "vscode-languageserver-protocol": "3.17.3" 564 | }, 565 | "engines": { 566 | "vscode": "^1.67.0" 567 | } 568 | }, 569 | "node_modules/vscode-languageserver-protocol": { 570 | "version": "3.17.3", 571 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", 572 | "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", 573 | "dependencies": { 574 | "vscode-jsonrpc": "8.1.0", 575 | "vscode-languageserver-types": "3.17.3" 576 | } 577 | }, 578 | "node_modules/vscode-languageserver-types": { 579 | "version": "3.17.3", 580 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", 581 | "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" 582 | } 583 | } 584 | } 585 | --------------------------------------------------------------------------------