diff --git a/server/package-lock.json b/server/package-lock.json index eee8282..81e6666 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "OSMC-PL-1-8", "dependencies": { - "tree-sitter": "^0.20.6", + "tree-sitter": "^0.21.1", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.11", "web-tree-sitter": "^0.20.8" @@ -18,417 +18,37 @@ "node": "20" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "node_modules/node-addon-api": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.0.0.tgz", + "integrity": "sha512-ipO7rsHEBqa9STO5C5T10fj732ml+5kLN1cAG8/jdHd56ldQeGj3Q7+scUS+VHK/qy1zLEwC4wMK5+yM0btPvw==", + "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" + "node": "^18 || ^20 || >= 21" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, - "node_modules/nan": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", - "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==" - }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" - }, - "node_modules/node-abi": { - "version": "3.52.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.52.0.tgz", - "integrity": "sha512-JJ98b02z16ILv7859irtXn4oUaFWADtvkzy2c0IAatNVX2Mc9Yoh8z6hZInn3QwvMEYhHuQloYi+TTQy67SIdQ==", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/prebuild-install": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, + "node_modules/node-gyp-build": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "license": "MIT", "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" } }, "node_modules/tree-sitter": { - "version": "0.20.6", - "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.20.6.tgz", - "integrity": "sha512-GxJodajVpfgb3UREzzIbtA1hyRnTxVbWVXrbC6sk4xTMH5ERMBJk9HJNq4c8jOJeUaIOmLcwg+t6mez/PDvGqg==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", "hasInstallScript": true, + "license": "MIT", "dependencies": { - "nan": "^2.18.0", - "prebuild-install": "^7.1.1" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", @@ -471,16 +91,6 @@ "version": "0.20.8", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz", "integrity": "sha512-weOVgZ3aAARgdnb220GqYuh7+rZU0Ka9k9yfKtGAzEYMa6GgiCzW9JjQRJyCJakvibQW+dfjJdihjInKuuCAUQ==" - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/server/package.json b/server/package.json index afe8908..7cca3dd 100644 --- a/server/package.json +++ b/server/package.json @@ -13,7 +13,7 @@ "url": "https://github.com/OpenModelica/modelica-language-server" }, "dependencies": { - "tree-sitter": "^0.20.6", + "tree-sitter": "^0.21.1", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.11", "web-tree-sitter": "^0.20.8" diff --git a/server/src/analyzer.ts b/server/src/analyzer.ts index a5d5ec2..c562531 100644 --- a/server/src/analyzer.ts +++ b/server/src/analyzer.ts @@ -41,47 +41,88 @@ import * as LSP from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; - import Parser = require('web-tree-sitter'); +import * as path from 'node:path'; +import * as fs from 'node:fs/promises'; +import * as fsSync from 'node:fs'; +import * as url from 'node:url'; + +import { ModelicaDocument, ModelicaLibrary, ModelicaProject } from './project'; import { getAllDeclarationsInTree } from './util/declarations'; import { logger } from './util/logger'; -type AnalyzedDocument = { - document: TextDocument; - declarations: LSP.SymbolInformation[]; - tree: Parser.Tree; -}; - export default class Analyzer { - #parser: Parser; - #uriToAnalyzedDocument: Record = {}; + #project: ModelicaProject; public constructor(parser: Parser) { - this.#parser = parser; + this.#project = new ModelicaProject(parser); } - public analyze(document: TextDocument): LSP.Diagnostic[] { - logger.debug('analyze:'); + /** + * Adds a library (and all of its documents) to the analyzer. + * + * @param uri uri to the library root + * @param isWorkspace `true` if this is a user workspace/project, `false` if + * this is a library. + */ + public async loadLibrary(uri: LSP.URI, isWorkspace: boolean): Promise { + const isLibrary = (folderPath: string) => + fsSync.existsSync(path.join(folderPath, 'package.mo')); + + const libraryPath = url.fileURLToPath(uri); + if (!isWorkspace || isLibrary(libraryPath)) { + const lib = await ModelicaLibrary.load(this.#project, libraryPath, isWorkspace); + this.#project.addLibrary(lib); + return; + } - const diagnostics: LSP.Diagnostic[] = []; - const fileContent = document.getText(); - const uri = document.uri; + // TODO: go deeper... something like `TreeSitterUtil.forEach` but for files + // would be good here + for (const nestedRelative of await fs.readdir(libraryPath)) { + const nested = path.resolve(nestedRelative); + if (!isLibrary(nested)) { + continue; + } - const tree = this.#parser.parse(fileContent); - logger.debug(tree.rootNode.toString()); + const library = await ModelicaLibrary.load(this.#project, nested, isWorkspace); + this.#project.addLibrary(library); + } + } - // Get declarations - const declarations = getAllDeclarationsInTree(tree, uri); + /** + * Adds a document to the analyzer. + * + * Note: {@link loadLibrary} already adds all discovered documents to the + * analyzer. It is only necessary to call this method on file creation. + * + * @param uri uri to document to add + * @throws if the document does not belong to a library + */ + public addDocument(uri: LSP.DocumentUri): void { + this.#project.addDocument(url.fileURLToPath(uri)); + } - // Update saved analysis for document uri - this.#uriToAnalyzedDocument[uri] = { - document, - declarations, - tree, - }; + /** + * Submits a modification to a document. Ignores documents that have not been + * added with {@link addDocument} or {@link loadLibrary}. + * + * @param uri uri to document to update + * @param text the modification + * @param range range to update, or `undefined` to replace the whole file + */ + public updateDocument(uri: LSP.DocumentUri, text: string): void { + this.#project.updateDocument(url.fileURLToPath(uri), text); + } - return diagnostics; + /** + * Removes a document from the analyzer. Ignores documents that have not been + * added or have already been removed. + * + * @param uri uri to document to remove + */ + public removeDocument(uri: LSP.DocumentUri): void { + this.#project.removeDocument(url.fileURLToPath(uri)); } /** @@ -90,7 +131,10 @@ export default class Analyzer { * TODO: convert to DocumentSymbol[] which is a hierarchy of symbols found in a given text document. */ public getDeclarationsForUri(uri: string): LSP.SymbolInformation[] { - const tree = this.#uriToAnalyzedDocument[uri]?.tree; + // TODO: convert to DocumentSymbol[] which is a hierarchy of symbols found + // in a given text document. + const path = url.fileURLToPath(uri); + const tree = this.#project.getDocument(path)?.tree; if (!tree?.rootNode) { return []; diff --git a/server/src/project/document.ts b/server/src/project/document.ts new file mode 100644 index 0000000..f0ac252 --- /dev/null +++ b/server/src/project/document.ts @@ -0,0 +1,166 @@ +/* + * This file is part of OpenModelica. + * + * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), + * c/o Linköpings universitet, Department of Computer and Information Science, + * SE-58183 Linköping, Sweden. + * + * All rights reserved. + * + * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR + * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. + * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES + * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL + * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. + * + * The OpenModelica software and the OSMC (Open Source Modelica Consortium) + * Public License (OSMC-PL) are obtained from OSMC, either from the above + * address, from the URLs: + * http://www.openmodelica.org or + * https://github.com/OpenModelica/ or + * http://www.ida.liu.se/projects/OpenModelica, + * and in the OpenModelica distribution. + * + * GNU AGPL version 3 is obtained from: + * https://www.gnu.org/licenses/licenses.html#GPL + * + * This program is distributed WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH + * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. + * + * See the full OSMC Public License conditions for more details. + * + */ + +import { TextDocument } from "vscode-languageserver-textdocument"; +import * as LSP from "vscode-languageserver/node"; +import Parser from "web-tree-sitter"; +import * as fs from "node:fs/promises"; +import * as url from "node:url"; +import * as path from "node:path"; + +import { logger } from "../util/logger"; +import { ModelicaLibrary } from "./library"; +import { ModelicaProject } from "./project"; + +export class ModelicaDocument implements TextDocument { + readonly #library: ModelicaLibrary; + readonly #document: TextDocument; + #tree: Parser.Tree; + + public constructor(library: ModelicaLibrary, document: TextDocument, tree: Parser.Tree) { + this.#library = library; + this.#document = document; + this.#tree = tree; + } + + /** + * Loads a document. + * + * @param library the containing {@link ModelicaLibrary} + * @param documentPath the path to the document + * @returns the document + */ + public static async load( + library: ModelicaLibrary, + documentPath: string, + ): Promise { + logger.debug(`Loading document at '${documentPath}'...`); + + const content = await fs.readFile(documentPath, "utf-8"); + // On caching: see issue https://github.com/tree-sitter/tree-sitter/issues/824 + // TL;DR: it's faster to re-parse the content than it is to deserialize the cached tree. + const tree = library.project.parser.parse(content); + + return new ModelicaDocument( + library, + TextDocument.create(url.fileURLToPath(documentPath), "modelica", 0, content), + tree + ); + } + + /** + * Updates a document. + * @param text the modification + */ + public async update(text: string): Promise { + TextDocument.update(this.#document, [{ text }], this.version + 1); + this.#tree = this.project.parser.parse(text); + return; + } + + public getText(range?: LSP.Range | undefined): string { + return this.#document.getText(range); + } + + public positionAt(offset: number): LSP.Position { + return this.#document.positionAt(offset); + } + + public offsetAt(position: LSP.Position): number { + return this.#document.offsetAt(position); + } + + public get uri(): LSP.DocumentUri { + return this.#document.uri; + } + + public get path(): string { + return url.fileURLToPath(this.#document.uri); + } + + public get languageId(): string { + return this.#document.languageId; + } + + public get version(): number { + return this.#document.version; + } + + public get lineCount(): number { + return this.#document.lineCount; + } + + /** + * The fully-qualified name of the class declared by this file. For instance, + * for a file named `MyLibrary/MyPackage/MyClass.mo`, this would be + * `["MyLibrary", "MyPackage", "MyClass"]`. + */ + public get packagePath(): string[] { + const directories = path.relative(this.#library.path, this.path).split(path.sep); + const fileName = directories.pop()!; + + const packagePath: string[] = [this.#library.name, ...directories]; + if (fileName !== "package.mo") { + packagePath.push(fileName.slice(0, fileName.length - ".mo".length)); + } + + return packagePath; + } + + /** + * The enclosing package of the class declared by this file. For instance, for + * a file named `MyLibrary/MyPackage/MyClass.mo`, this would be `["MyLibrary", + * "MyPackage"]`. + * + * Note: this property should be the same thing as the `within` clause + * declared in the document. However, we don't actually check the clause at + * all. The `within` clause is entirely redundant and completely ignored. + */ + public get within(): string[] { + return this.packagePath.slice(0, -1); + } + + public get project(): ModelicaProject { + return this.#library.project; + } + + public get library(): ModelicaLibrary { + return this.#library; + } + + public get tree(): Parser.Tree { + return this.#tree; + } +} diff --git a/server/src/project/index.ts b/server/src/project/index.ts new file mode 100644 index 0000000..a80780b --- /dev/null +++ b/server/src/project/index.ts @@ -0,0 +1,3 @@ +export { ModelicaDocument } from "./document"; +export { ModelicaLibrary } from "./library"; +export { ModelicaProject } from "./project"; diff --git a/server/src/project/library.ts b/server/src/project/library.ts new file mode 100644 index 0000000..e1dd631 --- /dev/null +++ b/server/src/project/library.ts @@ -0,0 +1,117 @@ +/* + * This file is part of OpenModelica. + * + * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), + * c/o Linköpings universitet, Department of Computer and Information Science, + * SE-58183 Linköping, Sweden. + * + * All rights reserved. + * + * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR + * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. + * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES + * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL + * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. + * + * The OpenModelica software and the OSMC (Open Source Modelica Consortium) + * Public License (OSMC-PL) are obtained from OSMC, either from the above + * address, from the URLs: + * http://www.openmodelica.org or + * https://github.com/OpenModelica/ or + * http://www.ida.liu.se/projects/OpenModelica, + * and in the OpenModelica distribution. + * + * GNU AGPL version 3 is obtained from: + * https://www.gnu.org/licenses/licenses.html#GPL + * + * This program is distributed WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH + * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. + * + * See the full OSMC Public License conditions for more details. + * + */ + +import * as LSP from "vscode-languageserver"; +import * as fsWalk from "@nodelib/fs.walk"; +import * as path from "node:path"; +import * as util from "node:util"; + +import { logger } from '../util/logger'; +import { ModelicaDocument } from "./document"; +import { ModelicaProject } from "./project"; + +export class ModelicaLibrary { + readonly #project: ModelicaProject; + readonly #documents: Map; + readonly #isWorkspace: boolean; + #path: string; + + public constructor(project: ModelicaProject, libraryPath: string, isWorkspace: boolean) { + this.#project = project; + this.#path = libraryPath, + this.#documents = new Map(); + this.#isWorkspace = isWorkspace; + } + + /** + * Loads a library and all of its {@link ModelicaDocument}s. + * + * @param project the containing project + * @param libraryPath the path to the library + * @param isWorkspace `true` if this is a user workspace + * @returns the loaded library + */ + public static async load( + project: ModelicaProject, + libraryPath: string, + isWorkspace: boolean, + ): Promise { + logger.info(`Loading ${isWorkspace ? 'workspace' : 'library'} at '${libraryPath}'...`); + + const library = new ModelicaLibrary(project, libraryPath, isWorkspace); + const workspaceRootDocument = await ModelicaDocument.load(library, path.join(libraryPath, "package.mo")); + + // Find the root path of the library and update library.#path. + // It might have been set incorrectly if we opened a child folder. + for (let i = 0; i < workspaceRootDocument.packagePath.length - 1; i++) { + library.#path = path.dirname(library.#path); + } + + logger.debug(`Set library path to ${library.path}`); + + const walk = util.promisify(fsWalk.walk); + const entries = await walk(library.#path, { + entryFilter: (entry) => !!entry.name.match(/.*\.mo/) && !entry.dirent.isDirectory(), + }); + + for (const entry of entries) { + const document = await ModelicaDocument.load(library, entry.path); + library.#documents.set(entry.path, document); + } + + logger.debug(`Loaded ${library.#documents.size} documents`); + return library; + } + + public get name(): string { + return path.basename(this.path); + } + + public get path(): string { + return this.#path; + } + + public get project(): ModelicaProject { + return this.#project; + } + + public get documents(): Map { + return this.#documents; + } + + public get isWorkspace(): boolean { + return this.#isWorkspace; + } +} diff --git a/server/src/project/project.ts b/server/src/project/project.ts new file mode 100644 index 0000000..ed4077a --- /dev/null +++ b/server/src/project/project.ts @@ -0,0 +1,151 @@ +/* + * This file is part of OpenModelica. + * + * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), + * c/o Linköpings universitet, Department of Computer and Information Science, + * SE-58183 Linköping, Sweden. + * + * All rights reserved. + * + * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR + * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. + * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES + * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL + * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. + * + * The OpenModelica software and the OSMC (Open Source Modelica Consortium) + * Public License (OSMC-PL) are obtained from OSMC, either from the above + * address, from the URLs: + * http://www.openmodelica.org or + * https://github.com/OpenModelica/ or + * http://www.ida.liu.se/projects/OpenModelica, + * and in the OpenModelica distribution. + * + * GNU AGPL version 3 is obtained from: + * https://www.gnu.org/licenses/licenses.html#GPL + * + * This program is distributed WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH + * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. + * + * See the full OSMC Public License conditions for more details. + * + */ + +import Parser from "web-tree-sitter"; +import * as LSP from "vscode-languageserver"; +import url from "node:url"; +import path from "node:path"; + +import { ModelicaLibrary } from "./library"; +import { ModelicaDocument } from './document'; +import { logger } from "../util/logger"; + +export class ModelicaProject { + readonly #parser: Parser; + readonly #libraries: ModelicaLibrary[]; + + public constructor(parser: Parser) { + this.#parser = parser; + this.#libraries = []; + } + + public get libraries(): readonly ModelicaLibrary[] { + return this.#libraries; + } + + public addLibrary(library: ModelicaLibrary) { + this.#libraries.push(library); + } + + /** + * Finds the document identified by the given path. + * + * @param documentPath file path pointing to the document + * @returns the document, or `undefined` if no such document exists + */ + public getDocument(documentPath: string): ModelicaDocument | undefined { + for (const library of this.#libraries) { + const doc = library.documents.get(documentPath); + if (doc) { + logger.debug(`Found document: ${doc.path}`); + return doc; + } + } + + logger.debug(`Couldn't find document: ${documentPath}`); + + return undefined; + } + + /** + * Adds a new document to the LSP. Calling this method multiple times for the + * same document has no effect. + * + * @param documentPath path to the document + * @throws if the document does not belong to a library + */ + public async addDocument(documentPath: string): Promise { + logger.info(`Adding document at '${documentPath}'...`); + + for (const library of this.#libraries) { + const relative = path.relative(library.path, documentPath); + const isSubdirectory = relative && !relative.startsWith("..") && !path.isAbsolute(relative); + + // Assume that files can't be inside multiple libraries at the same time + if (!isSubdirectory) { + continue; + } + + if (library.documents.get(documentPath) !== undefined) { + logger.warn(`Document '${documentPath}' already in library '${library.name}'; ignoring...`); + return; + } + + const document = await ModelicaDocument.load(library, documentPath); + library.documents.set(documentPath, document); + logger.debug(`Added document: ${documentPath}`); + return; + } + + throw new Error(`Failed to add document '${documentPath}': not a part of any libraries.`); + } + + /** + * Updates the content and tree of the given document. + * + * @param text the modification + * @param range range to update, or undefined to replace the whole file + */ + public updateDocument(documentPath: string, text: string): void { + logger.debug(`Updating document at '${documentPath}'...`); + + const doc = this.getDocument(documentPath); + if (doc) { + doc.update(text); + logger.debug(`Updated document '${documentPath}'`); + } else { + logger.warn(`Failed to update document '${documentPath}': not loaded`); + } + } + + /** + * Removes a document from the cache. + */ + public removeDocument(documentPath: string): void { + logger.info(`Removing document at '${documentPath}'...`); + + const doc = this.getDocument(documentPath); + if (doc) { + doc.library.documents.delete(documentPath); + } else { + logger.warn(`Failed to remove document '${documentPath}': not loaded`); + } + } + + public get parser(): Parser { + return this.#parser; + } + +} diff --git a/server/src/project/test/project.test.ts b/server/src/project/test/project.test.ts new file mode 100644 index 0000000..77dbb7f --- /dev/null +++ b/server/src/project/test/project.test.ts @@ -0,0 +1,55 @@ +/* + * This file is part of OpenModelica. + * + * Copyright (c) 1998-2024, Open Source Modelica Consortium (OSMC), + * c/o Linköpings universitet, Department of Computer and Information Science, + * SE-58183 Linköping, Sweden. + * + * All rights reserved. + * + * THIS PROGRAM IS PROVIDED UNDER THE TERMS OF AGPL VERSION 3 LICENSE OR + * THIS OSMC PUBLIC LICENSE (OSMC-PL) VERSION 1.8. + * ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES + * RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GNU AGPL + * VERSION 3, ACCORDING TO RECIPIENTS CHOICE. + * + * The OpenModelica software and the OSMC (Open Source Modelica Consortium) + * Public License (OSMC-PL) are obtained from OSMC, either from the above + * address, from the URLs: + * http://www.openmodelica.org or + * https://github.com/OpenModelica/ or + * http://www.ida.liu.se/projects/OpenModelica, + * and in the OpenModelica distribution. + * + * GNU AGPL version 3 is obtained from: + * https://www.gnu.org/licenses/licenses.html#GPL + * + * This program is distributed WITHOUT ANY WARRANTY; without + * even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH + * IN THE BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF OSMC-PL. + * + * See the full OSMC Public License conditions for more details. + * + */ + +import * as Mocha from "mocha"; +import * as assert from "assert"; +import Parser from "web-tree-sitter"; +import { ModelicaProject, ModelicaLibrary } from ".."; + +describe("ModelicaProject", () => { + it("Library loading", async () => { + const parser = new Parser(); + const project = new ModelicaProject(parser); + + assert.equal(project.libraries.length, 0); + + assert.throws(() => { + project.addDocument(); + }) + + const library = new ModelicaLibrary(project, "/a/b/c", true); + project.addLibrary(library); + }); +}); diff --git a/server/src/server.ts b/server/src/server.ts index f16d085..7cfc9ee 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -41,6 +41,8 @@ import * as LSP from 'vscode-languageserver/node'; import { TextDocument } from 'vscode-languageserver-textdocument'; +import url from 'node:url'; +import fs from 'node:fs/promises'; import { initializeParser } from './parser'; import Analyzer from './analyzer'; @@ -67,7 +69,7 @@ export class ModelicaServer { public static async initialize( connection: LSP.Connection, - { capabilities }: LSP.InitializeParams, + { capabilities, workspaceFolders }: LSP.InitializeParams, ): Promise { // Initialize logger setLogConnection(connection); @@ -76,11 +78,15 @@ export class ModelicaServer { const parser = await initializeParser(); const analyzer = new Analyzer(parser); - - const server = new ModelicaServer(analyzer, capabilities, connection); + if (workspaceFolders != null) { + for (const workspace of workspaceFolders) { + await analyzer.loadLibrary(workspace.uri, true); + } + } + // TODO: add libraries as well logger.debug('Initialized'); - return server; + return new ModelicaServer(analyzer, capabilities, connection); } /** @@ -88,52 +94,85 @@ export class ModelicaServer { */ public capabilities(): LSP.ServerCapabilities { return { - textDocumentSync: LSP.TextDocumentSyncKind.Full, completionProvider: undefined, hoverProvider: false, signatureHelpProvider: undefined, documentSymbolProvider: true, colorProvider: false, semanticTokensProvider: undefined, + textDocumentSync: LSP.TextDocumentSyncKind.Full, + workspace: { + workspaceFolders: { + supported: true, + changeNotifications: true, + }, + }, }; } public register(connection: LSP.Connection): void { - let currentDocument: TextDocument | null = null; - let initialized = false; - // Make the text document manager listen on the connection // for open, change and close text document events this.#documents.listen(this.#connection); + connection.onInitialized(this.onInitialized.bind(this)); + connection.onShutdown(this.onShutdown.bind(this)); + connection.onDidChangeTextDocument(this.onDidChangeTextDocument.bind(this)); + connection.onDidChangeWatchedFiles(this.onDidChangeWatchedFiles.bind(this)); connection.onDocumentSymbol(this.onDocumentSymbol.bind(this)); + } - connection.onInitialized(async () => { - initialized = true; - if (currentDocument) { - // If we already have a document, analyze it now that we're initialized - // and the linter is ready. - this.analyzeDocument(currentDocument); - } - }); - - // The content of a text document has changed. This event is emitted - // when the text document first opened or when its content has changed. - this.#documents.onDidChangeContent(({ document }) => { - logger.debug('onDidChangeContent'); + private async onInitialized(): Promise { + logger.debug('onInitialized'); + await connection.client.register( + new LSP.ProtocolNotificationType('workspace/didChangeWatchedFiles'), + { + watchers: [ + { + globPattern: '**/*.{mo,mos}', + }, + ], + }, + ); + + // If we opened a project, analyze it now that we're initialized + // and the linter is ready. + + // TODO: analysis + } - // We need to define some timing to wait some time or until whitespace is typed - // to update the tree or we are doing this on every key stroke + private async onShutdown(): Promise { + logger.debug('onShutdown'); + } - currentDocument = document; - if (initialized) { - this.analyzeDocument(document); - } - }); + private async onDidChangeTextDocument(params: LSP.DidChangeTextDocumentParams): Promise { + logger.debug('onDidChangeTextDocument'); + for (const change of params.contentChanges) { + this.#analyzer.updateDocument(params.textDocument.uri, change.text); + } } - private async analyzeDocument(document: TextDocument) { - const diagnostics = this.#analyzer.analyze(document); + private async onDidChangeWatchedFiles(params: LSP.DidChangeWatchedFilesParams): Promise { + logger.debug('onDidChangeWatchedFiles: ' + JSON.stringify(params, undefined, 4)); + + for (const change of params.changes) { + switch (change.type) { + case LSP.FileChangeType.Created: + this.#analyzer.addDocument(change.uri); + break; + case LSP.FileChangeType.Changed: { + // TODO: incremental? + const path = url.fileURLToPath(change.uri); + const content = await fs.readFile(path, 'utf-8'); + this.#analyzer.updateDocument(change.uri, content); + break; + } + case LSP.FileChangeType.Deleted: { + this.#analyzer.removeDocument(change.uri); + break; + } + } + } } /**