From ad2fe6fe2ab45a65b267f021012dbfe6dab8bad8 Mon Sep 17 00:00:00 2001 From: Angus Morton Date: Wed, 16 Oct 2024 21:16:51 +0800 Subject: [PATCH 01/14] Migrate to Volar --- package-lock.json | 324 +++++- packages/language-server/package.json | 11 + packages/language-server/src/index.ts | 336 ++---- .../src/service/core/marko-plugin.ts | 101 ++ .../src/service/core/parseHtml.ts | 47 + .../src/service/core/parseScript.ts | 50 + .../src/service/core/parseStyles.ts | 51 + .../src/service/format/index.ts | 125 ++ .../src/service/format/package.ts | 29 + .../language-server/src/service/html/index.ts | 223 ++-- packages/language-server/src/service/index.ts | 468 ++------ .../src/service/marko/complete/AttrName.ts | 24 +- .../src/service/marko/complete/AttrValue.ts | 125 +- .../src/service/marko/complete/Import.ts | 20 +- .../src/service/marko/complete/OpenTagName.ts | 24 +- .../src/service/marko/complete/Tag.ts | 18 +- .../src/service/marko/complete/index.ts | 61 +- .../src/service/marko/definition/AttrName.ts | 21 +- .../service/marko/definition/OpenTagName.ts | 20 +- .../src/service/marko/definition/index.ts | 49 +- .../src/service/marko/document-links.ts | 172 +-- .../src/service/marko/document-symbols.ts | 104 +- .../src/service/marko/format.ts | 64 -- .../src/service/marko/hover/OpenTagName.ts | 19 +- .../src/service/marko/hover/index.ts | 41 +- .../src/service/marko/index.ts | 156 ++- .../src/service/marko/validate.ts | 140 +-- .../src/service/script/index.ts | 1018 ----------------- .../service/script/util/print-jsdoc-tag.ts | 192 ---- .../src/service/style/index.ts | 486 -------- packages/language-server/src/service/types.ts | 65 -- .../src/utils/importPackage.ts | 74 ++ .../src/extractors/html/index.ts | 1 - .../src/extractors/html/keywords.ts | 13 +- packages/language-tools/src/util/extractor.ts | 24 +- packages/vscode/package.json | 22 +- packages/vscode/src/features/debug.ts | 78 ++ packages/vscode/src/index.ts | 218 +--- tsconfig.json | 2 +- 39 files changed, 1794 insertions(+), 3222 deletions(-) create mode 100644 packages/language-server/src/service/core/marko-plugin.ts create mode 100644 packages/language-server/src/service/core/parseHtml.ts create mode 100644 packages/language-server/src/service/core/parseScript.ts create mode 100644 packages/language-server/src/service/core/parseStyles.ts create mode 100644 packages/language-server/src/service/format/index.ts create mode 100644 packages/language-server/src/service/format/package.ts delete mode 100644 packages/language-server/src/service/marko/format.ts delete mode 100644 packages/language-server/src/service/script/index.ts delete mode 100644 packages/language-server/src/service/script/util/print-jsdoc-tag.ts delete mode 100644 packages/language-server/src/service/style/index.ts delete mode 100644 packages/language-server/src/service/types.ts create mode 100644 packages/language-server/src/utils/importPackage.ts create mode 100644 packages/vscode/src/features/debug.ts diff --git a/package-lock.json b/package-lock.json index d309c0a3..1c2eb276 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1106,6 +1106,54 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/@emmetio/abbreviation": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz", + "integrity": "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-abbreviation": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@emmetio/css-abbreviation/-/css-abbreviation-2.1.8.tgz", + "integrity": "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-parser": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emmetio/css-parser/-/css-parser-0.4.0.tgz", + "integrity": "sha512-z7wkxRSZgrQHXVzObGkXG+Vmj3uRlpM11oCZ9pbaz0nFejvCDmAiNDpY75+wgXOcffKpj4rzGtwGaZxfJKsJxw==", + "dependencies": { + "@emmetio/stream-reader": "^2.2.0", + "@emmetio/stream-reader-utils": "^0.1.0" + } + }, + "node_modules/@emmetio/html-matcher": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emmetio/html-matcher/-/html-matcher-1.3.0.tgz", + "integrity": "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==", + "dependencies": { + "@emmetio/scanner": "^1.0.0" + } + }, + "node_modules/@emmetio/scanner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.4.tgz", + "integrity": "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==" + }, + "node_modules/@emmetio/stream-reader": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz", + "integrity": "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==" + }, + "node_modules/@emmetio/stream-reader-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader-utils/-/stream-reader-utils-0.1.0.tgz", + "integrity": "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", @@ -2337,6 +2385,105 @@ "dev": true, "license": "ISC" }, + "node_modules/@volar/kit": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@volar/kit/-/kit-2.4.6.tgz", + "integrity": "sha512-OaMtpmLns6IYD1nOSd0NdG/F5KzJ7Jr4B7TLeb4byPzu+ExuuRVeO56Dn1C7Frnw6bGudUQd90cpQAmxdB+RlQ==", + "dependencies": { + "@volar/language-service": "2.4.6", + "@volar/typescript": "2.4.6", + "typesafe-path": "^0.2.2", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.6.tgz", + "integrity": "sha512-FxUfxaB8sCqvY46YjyAAV6c3mMIq/NWQMVvJ+uS4yxr1KzOvyg61gAuOnNvgCvO4TZ7HcLExBEsWcDu4+K4E8A==", + "dependencies": { + "@volar/source-map": "2.4.6" + } + }, + "node_modules/@volar/language-server": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@volar/language-server/-/language-server-2.4.6.tgz", + "integrity": "sha512-ARIbMXapEUPj9UFbZqWqw/iZ+ZuxUcY+vY212+2uutZVo/jrdzhLPu2TfZd9oB9akX8XXuslinT3051DyHLLRA==", + "dependencies": { + "@volar/language-core": "2.4.6", + "@volar/language-service": "2.4.6", + "@volar/typescript": "2.4.6", + "path-browserify": "^1.0.1", + "request-light": "^0.7.0", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/language-service": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.4.6.tgz", + "integrity": "sha512-wNeEVBgBKgpP1MfMYPrgTf1K8nhOGEh3ac0+9n6ECyk2N03+j0pWCpQ2i99mRWT/POvo1PgizDmYFH8S67bZOA==", + "dependencies": { + "@volar/language-core": "2.4.6", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.6.tgz", + "integrity": "sha512-Nsh7UW2ruK+uURIPzjJgF0YRGP5CX9nQHypA2OMqdM2FKy7rh+uv3XgPnWPw30JADbKvZ5HuBzG4gSbVDYVtiw==" + }, + "node_modules/@volar/typescript": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.6.tgz", + "integrity": "sha512-NMIrA7y5OOqddL9VtngPWYmdQU03htNKFtAYidbYfWA0TOhyGVd9tfcP4TsLWQ+RBWDZCbBqsr8xzU0ZOxYTCQ==", + "dependencies": { + "@volar/language-core": "2.4.6", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/vscode": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@volar/vscode/-/vscode-2.4.6.tgz", + "integrity": "sha512-E+IpXZHiGp90LFtCx5xmyLb7rfXechiJatLW/SLuqOWbKV8o5WAy4WwCXaj1XG0lOKgJpwXZ/dEMFPtqzYgGFQ==", + "dev": true, + "dependencies": { + "@volar/language-server": "2.4.6", + "path-browserify": "^1.0.1", + "vscode-languageclient": "^9.0.1", + "vscode-nls": "^5.2.0" + } + }, + "node_modules/@vscode/emmet-helper": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.9.3.tgz", + "integrity": "sha512-rB39LHWWPQYYlYfpv9qCoZOVioPCftKXXqrsyqN1mTWZM6dTnONT63Db+03vgrBbHzJN45IrgS/AGxw9iiqfEw==", + "dependencies": { + "emmet": "^2.4.3", + "jsonc-parser": "^2.3.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.15.1", + "vscode-uri": "^2.1.2" + } + }, + "node_modules/@vscode/emmet-helper/node_modules/jsonc-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==" + }, + "node_modules/@vscode/emmet-helper/node_modules/vscode-uri": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz", + "integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==" + }, "node_modules/@vscode/l10n": { "version": "0.0.18", "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", @@ -4074,6 +4221,15 @@ "integrity": "sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==", "license": "ISC" }, + "node_modules/emmet": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.11.tgz", + "integrity": "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==", + "dependencies": { + "@emmetio/abbreviation": "^2.3.3", + "@emmetio/css-abbreviation": "^2.1.8" + } + }, "node_modules/emoji-regex": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", @@ -7972,6 +8128,11 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8409,6 +8570,11 @@ "integrity": "sha512-ZvbtoduKQmD4PZeJPfH6Ql21qUWhaMxiHkIsH+FUnZqKDwNIXBtGg5zRZyHWomiGYk8n5+KMBPK7Mi4D0XWfNg==", "license": "MIT" }, + "node_modules/request-light": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.7.0.tgz", + "integrity": "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8696,7 +8862,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10122,6 +10287,11 @@ "underscore": "^1.12.1" } }, + "node_modules/typesafe-path": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz", + "integrity": "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==" + }, "node_modules/typescript": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", @@ -10135,6 +10305,14 @@ "node": ">=14.17" } }, + "node_modules/typescript-auto-import-cache": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.3.tgz", + "integrity": "sha512-ojEC7+Ci1ij9eE6hp8Jl9VUNnsEKzztktP5gtYNRMrTmfXVwA1PITYYAkpxCvvupdSYa/Re51B6KMcv1CTZEUA==", + "dependencies": { + "semver": "^7.3.8" + } + }, "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", @@ -10256,6 +10434,118 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/volar-service-css": { + "version": "0.0.62", + "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.62.tgz", + "integrity": "sha512-JwNyKsH3F8PuzZYuqPf+2e+4CTU8YoyUHEHVnoXNlrLe7wy9U3biomZ56llN69Ris7TTy/+DEX41yVxQpM4qvg==", + "dependencies": { + "vscode-css-languageservice": "^6.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-emmet": { + "version": "0.0.62", + "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.62.tgz", + "integrity": "sha512-U4dxWDBWz7Pi4plpbXf4J4Z/ss6kBO3TYrACxWNsE29abu75QzVS0paxDDhI6bhqpbDFXlpsDhZ9aXVFpnfGRQ==", + "dependencies": { + "@emmetio/css-parser": "^0.4.0", + "@emmetio/html-matcher": "^1.3.0", + "@vscode/emmet-helper": "^2.9.3", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-html": { + "version": "0.0.62", + "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.62.tgz", + "integrity": "sha512-Zw01aJsZRh4GTGUjveyfEzEqpULQUdQH79KNEiKVYHZyuGtdBRYCHlrus1sueSNMxwwkuF5WnOHfvBzafs8yyQ==", + "dependencies": { + "vscode-html-languageservice": "^5.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-prettier": { + "version": "0.0.62", + "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.62.tgz", + "integrity": "sha512-h2yk1RqRTE+vkYZaI9KYuwpDfOQRrTEMvoHol0yW4GFKc75wWQRrb5n/5abDrzMPrkQbSip8JH2AXbvrRtYh4w==", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0", + "prettier": "^2.2 || ^3.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + }, + "prettier": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript": { + "version": "0.0.62", + "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.62.tgz", + "integrity": "sha512-p7MPi71q7KOsH0eAbZwPBiKPp9B2+qrdHAd6VY5oTo9BUXatsOAdakTm9Yf0DUj6uWBAaOT01BSeVOPwucMV1g==", + "dependencies": { + "path-browserify": "^1.0.1", + "semver": "^7.6.2", + "typescript-auto-import-cache": "^0.3.3", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-nls": "^5.2.0", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript-twoslash-queries": { + "version": "0.0.62", + "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.62.tgz", + "integrity": "sha512-KxFt4zydyJYYI0kFAcWPTh4u0Ha36TASPZkAnNY784GtgajerUqM80nX/W1d0wVhmcOFfAxkVsf/Ed+tiYU7ng==", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, "node_modules/vscode-css-languageservice": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.1.tgz", @@ -10268,6 +10558,17 @@ "vscode-uri": "^3.0.8" } }, + "node_modules/vscode-html-languageservice": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.3.1.tgz", + "integrity": "sha512-ysUh4hFeW/WOWz/TO9gm08xigiSsV/FOAZ+DolgJfeLftna54YdmZ4A+lIn46RbdO3/Qv5QHTn1ZGqmrXQhZyA==", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.0.8" + } + }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", @@ -10339,6 +10640,11 @@ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", "license": "MIT" }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==" + }, "node_modules/vscode-uri": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", @@ -10817,12 +11123,23 @@ "@marko/compiler": "^5.37.15", "@marko/language-tools": "^2.3.1", "@marko/translator-default": "^6.0.16", + "@volar/kit": "^2.4.5", + "@volar/language-core": "^2.4.5", + "@volar/language-server": "^2.4.5", + "@volar/language-service": "^2.4.5", + "@volar/typescript": "^2.4.5", "htmljs-parser": "^5.5.2", "marko": "^5.35.20", "prettier": "^3.3.3", "prettier-plugin-marko": "^3.1.4", "relative-import-path": "^1.0.0", "typescript": "^5.6.2", + "volar-service-css": "^0.0.62", + "volar-service-emmet": "^0.0.62", + "volar-service-html": "^0.0.62", + "volar-service-prettier": "^0.0.62", + "volar-service-typescript": "^0.0.62", + "volar-service-typescript-twoslash-queries": "^0.0.62", "vscode-css-languageservice": "^6.3.1", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12", @@ -10887,6 +11204,8 @@ "devDependencies": { "@marko/language-server": "^1.2.1", "@types/vscode": "^1.80.0", + "@volar/language-server": "^2.4.5", + "@volar/vscode": "^2.4.5", "@vscode/test-electron": "^2.4.1", "@vscode/vsce": "^3.1.1", "ovsx": "^0.9.5", @@ -10896,9 +11215,6 @@ "engines": { "vscode": "^1.80.0" } - }, - "packages/vscode/node_modules/marko-ts-plugin": { - "extraneous": true } } } diff --git a/packages/language-server/package.json b/packages/language-server/package.json index a3690d3f..868b7dd1 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -18,6 +18,17 @@ "prettier-plugin-marko": "^3.1.4", "relative-import-path": "^1.0.0", "typescript": "^5.6.2", + "@volar/kit": "^2.4.5", + "@volar/language-core": "^2.4.5", + "@volar/language-server": "^2.4.5", + "@volar/language-service": "^2.4.5", + "@volar/typescript": "^2.4.5", + "volar-service-css": "^0.0.62", + "volar-service-emmet": "^0.0.62", + "volar-service-html": "^0.0.62", + "volar-service-prettier": "^0.0.62", + "volar-service-typescript": "^0.0.62", + "volar-service-typescript-twoslash-queries": "^0.0.62", "vscode-css-languageservice": "^6.3.1", "vscode-languageserver": "^9.0.1", "vscode-languageserver-textdocument": "^1.0.12", diff --git a/packages/language-server/src/index.ts b/packages/language-server/src/index.ts index b8f97e35..e1ecd0a6 100644 --- a/packages/language-server/src/index.ts +++ b/packages/language-server/src/index.ts @@ -1,280 +1,84 @@ -import { inspect, isDeepStrictEqual } from "util"; - +import { dirname } from "path"; import { - DefinitionLink, - Diagnostic, - ProposedFeatures, - TextDocumentSyncKind, createConnection, -} from "vscode-languageserver/node"; -import type { TextDocument } from "vscode-languageserver-textdocument"; -import { Project } from "@marko/language-tools"; - -import "./utils/project-defaults"; -import { clearMarkoCacheForFile } from "./utils/file"; -import * as documents from "./utils/text-documents"; -import * as workspace from "./utils/workspace"; -import setupMessages from "./utils/messages"; -import service from "./service"; - -if ( - typeof require !== "undefined" && - require.extensions && - !(".ts" in require.extensions) -) { - // Prevent compiler hooks written in typescript to explode the language server. - require.extensions[".ts"] = undefined; -} -const connection = createConnection(ProposedFeatures.all); -const prevDiags = new WeakMap(); -let diagnosticTimeout: ReturnType | undefined; - -console.log = (...args: unknown[]) => { - connection.console.log(args.map((v) => inspect(v)).join(" ")); -}; -console.error = (...args: unknown[]) => { - connection.console.error(args.map((v) => inspect(v)).join(" ")); -}; -process.on("uncaughtException", console.error); -process.on("unhandledRejection", console.error); - -connection.onInitialize(async (params) => { - setupMessages(connection); - await service.initialize(params); - - return { - capabilities: { - textDocumentSync: TextDocumentSyncKind.Incremental, - documentFormattingProvider: true, - definitionProvider: true, - hoverProvider: true, - renameProvider: true, - codeActionProvider: true, - referencesProvider: true, - documentLinkProvider: { resolveProvider: false }, - colorProvider: true, - documentHighlightProvider: true, - documentSymbolProvider: true, - completionProvider: { - resolveProvider: true, - triggerCharacters: [ - ".", - ":", - "<", - ">", - "@", - "/", - '"', - "'", - "`", - " ", - "=", - "*", - "#", - "$", - "+", - "^", - "(", - "[", - "-", - ], - }, - }, - }; -}); + createServer, + createTypeScriptProject, + loadTsdkByPath, +} from "@volar/language-server/node"; +import { + addMarkoTypes, + getLanguagePlugins, + getLanguageServicePlugins, +} from "./service"; +// import { getMarkoInstall } from "./util/getMarkoInstall.js"; +// import { addMarkoTypes } from "./core/index.js"; -workspace.setup(connection); -workspace.onConfigChange(validateDocs); +const connection = createConnection(); +const server = createServer(connection); -connection.onDidOpenTextDocument(async (params) => { - documents.doOpen(params); +connection.listen(); - const doc = documents.get(params.textDocument.uri); - if (doc) { - const diagnostics = (await service.doValidate(doc)) || []; - prevDiags.set(doc, diagnostics); - connection.sendDiagnostics({ - uri: doc.uri, - diagnostics, - }); - } -}); -connection.onDidChangeTextDocument(documents.doChange); -connection.onDidCloseTextDocument(documents.doClose); -connection.onDidChangeWatchedFiles(documents.doChangeWatchedFiles); +connection.onInitialize((params) => { + const tsdk = params.initializationOptions?.typescript?.tsdk; -documents.onFileChange((changeDoc) => { - if (changeDoc) { - queueDiagnostic(); - clearMarkoCacheForFile(changeDoc); - } else { - validateDocs(); + if (!tsdk) { + throw new Error( + "The `typescript.tsdk` init option is required. It should point to a directory containing a `typescript.js` or `tsserverlibrary.js` file, such as `node_modules/typescript/lib`.", + ); } -}); -connection.onCompletion(async (params, cancel) => { - return ( - (await service.doComplete( - documents.get(params.textDocument.uri)!, - params, - cancel, - )) || null + const { typescript, diagnosticMessages } = loadTsdkByPath( + tsdk, + params.locale, ); -}); - -connection.onCompletionResolve(async (item, cancel) => { - return (await service.doCompletionResolve(item, cancel)) || item; -}); -connection.onDefinition(async (params, cancel) => { - return ( - ((await service.findDefinition( - documents.get(params.textDocument.uri)!, - params, - cancel, - )) as DefinitionLink[]) || null + return server.initialize( + params, + createTypeScriptProject(typescript, diagnosticMessages, ({ env }) => { + return { + languagePlugins: getLanguagePlugins(typescript), + setup({ project }) { + const { languageServiceHost, configFileName } = project.typescript!; + + const rootPath = configFileName + ? configFileName.split("/").slice(0, -1).join("/") + : env.workspaceFolders[0]!.fsPath; + const nearestPackageJson = typescript.findConfigFile( + rootPath, + typescript.sys.fileExists, + "package.json", + ); + + // const markoInstall = getMarkoInstall([rootPath], { + // nearestPackageJson: nearestPackageJson, + // }); + + // if (!markoInstall) { + // connection.sendNotification(ShowMessageNotification.type, { + // message: `Couldn't find Marko in workspace "${rootPath}".`, + // type: MessageType.Warning, + // }); + // } + + if (nearestPackageJson) { + addMarkoTypes( + dirname(nearestPackageJson), + typescript, + languageServiceHost, + ); + } + }, + }; + }), + getLanguageServicePlugins(connection, typescript), ); }); -connection.onReferences(async (params, cancel) => { - return ( - (await service.findReferences( - documents.get(params.textDocument.uri)!, - params, - cancel, - )) || null - ); -}); - -connection.onDocumentLinks(async (params, cancel) => { - return ( - (await service.findDocumentLinks( - documents.get(params.textDocument.uri)!, - params, - cancel, - )) || null - ); -}); - -connection.onDocumentSymbol(async (params, cancel) => { - return ( - (await service.findDocumentSymbols( - documents.get(params.textDocument.uri)!, - params, - cancel, - )) || null - ); -}); - -connection.onDocumentHighlight(async (params, cancel) => { - return ( - (await service.findDocumentHighlights( - documents.get(params.textDocument.uri)!, - params, - cancel, - )) || null - ); +connection.onInitialized(() => { + server.initialized(); + server.fileWatcher.watchFiles([ + `**/*.{${["js", "cjs", "mjs", "ts", "cts", "mts", "json", "marko"].join( + ",", + )}}`, + ]); }); - -connection.onDocumentColor(async (params, cancel) => { - return ( - (await service.findDocumentColors( - documents.get(params.textDocument.uri)!, - params, - cancel, - )) || null - ); -}); - -connection.onColorPresentation(async (params, cancel) => { - return ( - (await service.getColorPresentations( - documents.get(params.textDocument.uri)!, - params, - cancel, - )) || null - ); -}); - -connection.onHover(async (params, cancel) => { - return ( - (await service.doHover( - documents.get(params.textDocument.uri)!, - params, - cancel, - )) || null - ); -}); - -connection.onRenameRequest(async (params, cancel) => { - return ( - (await service.doRename( - documents.get(params.textDocument.uri)!, - params, - cancel, - )) || null - ); -}); - -connection.onCodeAction(async (params, cancel) => { - return ( - (await service.doCodeActions( - documents.get(params.textDocument.uri)!, - params, - cancel, - )) || null - ); -}); - -connection.onDocumentFormatting(async (params, cancel) => { - return ( - (await service.format( - documents.get(params.textDocument.uri)!, - params, - cancel, - )) || null - ); -}); - -for (const command in service.commands) { - connection.onRequest(command, service.commands[command]); -} - -function validateDocs() { - queueDiagnostic(); - Project.clearCaches(); -} - -function queueDiagnostic() { - clearTimeout(diagnosticTimeout); - const id = (diagnosticTimeout = setTimeout(async () => { - const results = await Promise.all( - Array.from(documents.getAllOpen()).map(async (doc) => { - if (!documents.isOpen(doc)) { - prevDiags.delete(doc); - return; - } - const prevDiag = prevDiags.get(doc) || []; - const nextDiag = (await service.doValidate(doc)) || []; - if (isDeepStrictEqual(prevDiag, nextDiag)) return; - return [doc, nextDiag] as const; - }), - ); - - // Check that it wasn't canceled. - if (id === diagnosticTimeout) { - for (const result of results) { - if (result) { - const [doc, diag] = result; - prevDiags.set(doc, diag); - connection.sendDiagnostics({ - uri: doc.uri, - diagnostics: diag, - }); - } - } - } - }, 400)); -} - -connection.listen(); diff --git a/packages/language-server/src/service/core/marko-plugin.ts b/packages/language-server/src/service/core/marko-plugin.ts new file mode 100644 index 00000000..dec0935b --- /dev/null +++ b/packages/language-server/src/service/core/marko-plugin.ts @@ -0,0 +1,101 @@ +import path from "path"; +import { + type CodeMapping, + type LanguagePlugin, + type VirtualCode, + forEachEmbeddedCode, +} from "@volar/language-core"; +import type { URI } from "vscode-uri"; +import type ts from "typescript"; +import { Project, extractHTML, parse } from "@marko/language-tools"; +import { TaglibLookup } from "@marko/babel-utils"; +import { parseScripts } from "./parseScript"; +import { parseStyles } from "./parseStyles"; +import { parseHtml } from "./parseHtml"; + +export function getMarkoLanguagePlugin( + ts: typeof import("typescript"), +): LanguagePlugin { + return { + getLanguageId(uri) { + if (uri.path.endsWith(".marko")) { + return "marko"; + } + }, + createVirtualCode(uri, languageId, snapshot) { + if (languageId === "marko") { + const fileName = uri.fsPath.replace(/\\/g, "/"); + return new MarkoVirtualCode(fileName, snapshot, ts); + } + }, + typescript: { + extraFileExtensions: [ + { extension: "marko", isMixedContent: true, scriptKind: 7 }, + ], + getServiceScript(markoCode) { + for (const code of forEachEmbeddedCode(markoCode)) { + if (code.id === "script") { + return { + code, + extension: ".ts", + scriptKind: 3 satisfies ts.ScriptKind.TS, + }; + } + } + }, + }, + }; +} + +export class MarkoVirtualCode implements VirtualCode { + id = "root"; + languageId = "marko"; + mappings!: CodeMapping[]; + embeddedCodes!: VirtualCode[]; + markoAst: ReturnType; + tagLookup: TaglibLookup; + htmlAst?: ReturnType; + compiler?: typeof import("@marko/compiler"); + code: string; + + constructor( + public fileName: string, + public snapshot: ts.IScriptSnapshot, + public ts: typeof import("typescript"), + ) { + this.mappings = [ + { + sourceOffsets: [0], + generatedOffsets: [0], + lengths: [this.snapshot.getLength()], + data: { + verification: true, + completion: true, + semantic: true, + navigation: true, + structure: true, + format: true, + }, + }, + ]; + + this.embeddedCodes = []; + + this.code = this.snapshot.getText(0, this.snapshot.getLength()); + this.markoAst = parse(this.code, this.fileName); + + const dirname = path.dirname(fileName); + this.tagLookup = Project.getTagLookup(dirname); + this.compiler = Project.getCompiler(path.dirname(this.fileName)); + + const scripts = parseScripts(this.markoAst, this.ts, this.tagLookup); + this.embeddedCodes.push(...scripts); + + const styles = parseStyles(this.markoAst, this.tagLookup); + this.embeddedCodes.push(...styles); + + this.htmlAst = extractHTML(this.markoAst); + const html = parseHtml(this.htmlAst); + this.embeddedCodes.push(...html); + } +} diff --git a/packages/language-server/src/service/core/parseHtml.ts b/packages/language-server/src/service/core/parseHtml.ts new file mode 100644 index 00000000..e8a7824f --- /dev/null +++ b/packages/language-server/src/service/core/parseHtml.ts @@ -0,0 +1,47 @@ +import { Extracted, extractHTML } from "@marko/language-tools"; +import type { CodeMapping, VirtualCode } from "@volar/language-core"; + +export function parseHtml( + parsed: ReturnType, +): VirtualCode[] { + const scriptText = parsed.extracted.toString(); + const mappings: CodeMapping[] = generateMappingsFromExtracted( + parsed.extracted, + ); + + if (mappings.length > 0) { + return [ + { + id: "html", + languageId: "html", + snapshot: { + getText: (start, end) => scriptText.substring(start, end), + getLength: () => scriptText.length, + getChangeRange: () => undefined, + }, + mappings: mappings, + embeddedCodes: [], + }, + ]; + } + + return []; +} + +function generateMappingsFromExtracted(extracted: Extracted): CodeMapping[] { + return extracted.tokens.map((it) => { + return { + sourceOffsets: [it.sourceStart], + generatedOffsets: [it.generatedStart], + lengths: [it.length], + data: { + completion: true, + format: false, + navigation: true, + semantic: true, + structure: true, + verification: true, + }, + }; + }); +} diff --git a/packages/language-server/src/service/core/parseScript.ts b/packages/language-server/src/service/core/parseScript.ts new file mode 100644 index 00000000..e8143019 --- /dev/null +++ b/packages/language-server/src/service/core/parseScript.ts @@ -0,0 +1,50 @@ +import { ScriptLang, extractScript, parse } from "@marko/language-tools"; +import type { CodeMapping, VirtualCode } from "@volar/language-core"; + +export function parseScripts( + parsed: ReturnType, + ts: typeof import("typescript"), + tagLookup: any, +): VirtualCode[] { + const script = extractScript({ + parsed, + scriptLang: ScriptLang.ts, + lookup: tagLookup, + ts: ts, + }); + const scriptText = script.toString(); + const mappings: CodeMapping[] = []; + for (const token of script.tokens) { + mappings.push({ + sourceOffsets: [token.sourceStart], + generatedOffsets: [token.generatedStart], + lengths: [token.length], + data: { + completion: true, + format: false, + navigation: true, + semantic: true, + structure: true, + verification: true, + }, + }); + } + + if (mappings.length > 0) { + return [ + { + id: "script", + languageId: "typescript", + snapshot: { + getText: (start, end) => scriptText.substring(start, end), + getLength: () => scriptText.length, + getChangeRange: () => undefined, + }, + mappings: mappings, + embeddedCodes: [], + }, + ]; + } + + return []; +} diff --git a/packages/language-server/src/service/core/parseStyles.ts b/packages/language-server/src/service/core/parseStyles.ts new file mode 100644 index 00000000..fb2b74f5 --- /dev/null +++ b/packages/language-server/src/service/core/parseStyles.ts @@ -0,0 +1,51 @@ +import { Extracted, extractStyle, parse } from "@marko/language-tools"; +import type { CodeMapping, VirtualCode } from "@volar/language-core"; + +export function parseStyles( + parsed: ReturnType, + taglib: any, +): VirtualCode[] { + const styles = extractStyle({ parsed, lookup: taglib }); + + const result = []; + for (const [key, style] of styles.entries()) { + const styleText = style.toString(); + const stylesheetKey = key.slice(1); + + const languageId = + stylesheetKey === "scss" || stylesheetKey === "less" + ? stylesheetKey + : "css"; + result.push({ + id: `style_${key.slice(1)}`, + languageId, + snapshot: { + getText: (start, end) => styleText.substring(start, end), + getLength: () => styleText.length, + getChangeRange: () => undefined, + }, + mappings: generateMappingsFromExtracted(style), + embeddedCodes: [], + } satisfies VirtualCode); + } + + return result; +} + +function generateMappingsFromExtracted(extracted: Extracted): CodeMapping[] { + return extracted.tokens.map((it) => { + return { + sourceOffsets: [it.sourceStart], + generatedOffsets: [it.generatedStart], + lengths: [it.length], + data: { + completion: true, + format: false, + navigation: true, + semantic: true, + structure: true, + verification: true, + }, + }; + }); +} diff --git a/packages/language-server/src/service/format/index.ts b/packages/language-server/src/service/format/index.ts new file mode 100644 index 00000000..41934949 --- /dev/null +++ b/packages/language-server/src/service/format/index.ts @@ -0,0 +1,125 @@ +import { dirname } from "path"; +import { create as createPrettierService } from "volar-service-prettier"; +import type { Connection, LanguageServicePlugin } from "@volar/language-server"; +import { ShowMessageNotification } from "@volar/language-server"; +import { MessageType } from "@volar/language-server"; +import { URI } from "vscode-uri"; +import { Project } from "@marko/language-tools"; +import * as markoPrettier from "prettier-plugin-marko"; +import { dynamicRequire } from "../../utils/importPackage"; +import { getMarkoPrettierPluginPath, importPrettier } from "./package"; + +export function getMarkoPrettierService( + connection: Connection, +): LanguageServicePlugin { + let prettier: ReturnType; + let prettierPluginPath: ReturnType[0]; + let hasShownNotification = false; + + return createPrettierService( + (context) => { + for (const workspaceFolder of context.env.workspaceFolders) { + if (workspaceFolder.scheme === "file") { + prettier = importPrettier(workspaceFolder.fsPath); + const [path] = + getMarkoPrettierPluginPath(workspaceFolder.fsPath) ?? []; + prettierPluginPath = path; + if ((!prettier || !prettierPluginPath) && !hasShownNotification) { + connection.sendNotification(ShowMessageNotification.type, { + message: + "Couldn't load `prettier` or `prettier-plugin-marko`. Formatting will not work. Please make sure those two packages are installed into your project and restart the language server.", + type: MessageType.Warning, + }); + hasShownNotification = true; + } + } + return prettier; + } + }, + { + documentSelector: [{ language: "marko" }], + isFormattingEnabled: async (prettier, document, _) => { + const uri = URI.parse(document.uri); + if (uri.scheme === "file") { + const fileInfo = await prettier.getFileInfo(uri.fsPath, { + ignorePath: ".prettierignore", + resolveConfig: false, + }); + if (fileInfo.ignored) { + return false; + } + } + return true; + }, + getFormattingOptions: async ( + prettierInstance, + document, + formatOptions, + context, + ) => { + const filePath = URI.parse(document.uri).fsPath; + const fileDir = dirname(filePath); + let configOptions = null; + try { + configOptions = await prettierInstance.resolveConfig(filePath, { + useCache: false, + editorconfig: true, + }); + } catch (e) { + connection.sendNotification(ShowMessageNotification.type, { + message: `Failed to load Prettier config.\n\nError:\n${e}`, + type: MessageType.Warning, + }); + console.error("Failed to load Prettier config.", e); + } + + const editorOptions = await context.env.getConfiguration?.( + "prettier", + document.uri, + ); + + // Return a config with the following cascade: + // - Prettier config file should always win if it exists, if it doesn't: + // - Prettier config from the VS Code extension is used, if it doesn't exist: + // - Use the editor's basic configuration settings + const resolvedConfig = { + filepath: filePath, + tabWidth: formatOptions.tabSize, + useTabs: !formatOptions.insertSpaces, + ...editorOptions, + ...configOptions, + }; + + try { + let resolvedPlugin; + if (prettierPluginPath) { + resolvedPlugin = dynamicRequire(prettierPluginPath); + } else { + // TODO: Fallback to the built-in version of marko-prettier-plugin if the workspace doesn't have it installed. + resolvedPlugin = markoPrettier; + } + + resolvedPlugin.setCompiler( + Project.getCompiler(fileDir), + Project.getConfig(fileDir), + ); + + return { + ...resolvedConfig, + plugins: [resolvedPlugin, ...(resolvedConfig.plugins ?? [])], + parser: "marko", + }; + } catch (e) { + connection.sendNotification(ShowMessageNotification.type, { + message: `Failed to configure marko-prettier-plugin.\n\nError:\n${e}`, + type: MessageType.Warning, + }); + console.error("Failed to load Prettier config.", e); + return { + ...resolvedConfig, + }; + } + }, + }, + ); +} diff --git a/packages/language-server/src/service/format/package.ts b/packages/language-server/src/service/format/package.ts new file mode 100644 index 00000000..4272319c --- /dev/null +++ b/packages/language-server/src/service/format/package.ts @@ -0,0 +1,29 @@ +import type * as prettier from "prettier"; +import { getPackagePath } from "../../utils/importPackage"; + +export function importPrettier(fromPath: string): typeof prettier | undefined { + const prettierPkg = getPackagePath("prettier", [fromPath, __dirname]); + + if (!prettierPkg) { + return undefined; + } + + return require(prettierPkg); +} + +export function getMarkoPrettierPluginPath( + fromPath: string, +): [string, "prettier-plugin-marko"] | [undefined, undefined] { + const corePluginPath = getPackagePath( + "prettier-plugin-marko", + [fromPath, __dirname], + false, + ); + + // Prefer the official plugin if it's installed. + if (corePluginPath) { + return [corePluginPath, "prettier-plugin-marko"]; + } + + return [undefined, undefined]; +} diff --git a/packages/language-server/src/service/html/index.ts b/packages/language-server/src/service/html/index.ts index 8e08c34c..d57917f6 100644 --- a/packages/language-server/src/service/html/index.ts +++ b/packages/language-server/src/service/html/index.ts @@ -1,113 +1,134 @@ import type { TextDocument } from "vscode-languageserver-textdocument"; import axe from "axe-core"; -import { type Parsed, extractHTML } from "@marko/language-tools"; import { JSDOM } from "jsdom"; -import type { Plugin } from "../types"; -import { getMarkoFile } from "../../utils/file"; -import { get } from "../../utils/text-documents"; +import type { Diagnostic, LanguageServicePlugin } from "@volar/language-server"; +import { URI } from "vscode-uri"; +import { MarkoVirtualCode } from "../core/marko-plugin"; import { ruleExceptions } from "./axe-rules/rule-exceptions"; -const extractCache = new WeakMap>(); - -// const axeViolationImpact: { -// [impact in NonNullable]: DiagnosticSeverity; -// } = { -// minor: 1, -// moderate: 2, -// serious: 3, -// critical: 4, -// }; - -const HTMLService: Partial = { - commands: { - "$/showHtmlOutput": async (uri: string) => { - const doc = get(uri); - if (!doc) return; - - const { extracted } = extract(doc); - +export const create = (): LanguageServicePlugin => { + return { + name: "marko-template", + capabilities: { + diagnosticProvider: { + interFileDependencies: true, + workspaceDiagnostics: false, + }, + }, + create(context) { return { - language: "html", - content: extracted.toString(), + async provideDiagnostics(document, token) { + if (token.isCancellationRequested) return; + + return worker(document, async (virtualCode) => { + const htmlAst = virtualCode.htmlAst; + if (!htmlAst) { + return []; + } + + const jsdom = new JSDOM(htmlAst.toString(), { + includeNodeLocations: true, + }); + const { documentElement } = jsdom.window.document; + + const getViolationNodes = async (runOnly: string[]) => + ( + await axe.run(documentElement, { + runOnly, + rules: { + "color-contrast": { enabled: false }, + }, + resultTypes: ["violations"], + elementRef: true, + }) + ).violations.flatMap(({ nodes, id }) => + nodes.map((node) => ({ ...node, ruleId: id })), + ); + + const release = await acquireMutexLock(); + const violations = await getViolationNodes( + Object.keys(ruleExceptions), + ); + release(); + + return violations.flatMap((result) => { + const { element } = result; + if (!element) return []; + const ruleId = result.ruleId as keyof typeof ruleExceptions; + + if (element.dataset.markoNodeId) { + const details = + htmlAst.nodeDetails[element.dataset.markoNodeId]; + if ( + (ruleExceptions[ruleId].attrSpread && + details.hasDynamicAttrs) || + (ruleExceptions[ruleId].unknownBody && + details.hasDynamicBody) || + ruleExceptions[ruleId].dynamicAttrs?.some( + (attr) => element.getAttribute(attr) === "dynamic", + ) + ) { + return []; + } + } + + const generatedLoc = jsdom.nodeLocation(element); + if (!generatedLoc) return []; + + const sourceRange = htmlAst.extracted.sourceLocationAt( + generatedLoc.startOffset + 1, + generatedLoc.startOffset + 1 + element.tagName.length, + ); + if (!sourceRange) return []; + + return [ + { + range: sourceRange, + severity: 3, + source: `axe-core(${ruleId})`, + message: + result.failureSummary ?? "unknown accessibility issue", + } satisfies Diagnostic, + ]; + }); + }); + }, }; - }, - }, - async doValidate(doc) { - const { extracted, nodeDetails } = extract(doc); - - const jsdom = new JSDOM(extracted.toString(), { - includeNodeLocations: true, - }); - const { documentElement } = jsdom.window.document; - - const getViolationNodes = async (runOnly: string[]) => - ( - await axe.run(documentElement, { - runOnly, - rules: { - "color-contrast": { enabled: false }, - }, - resultTypes: ["violations"], - elementRef: true, - }) - ).violations.flatMap(({ nodes, id }) => - nodes.map((node) => ({ ...node, ruleId: id })), - ); - - const release = await acquireMutexLock(); - const violations = await getViolationNodes(Object.keys(ruleExceptions)); - release(); - - return violations.flatMap((result) => { - const { element } = result; - if (!element) return []; - const ruleId = result.ruleId as keyof typeof ruleExceptions; - if (element.dataset.markoNodeId) { - const details = nodeDetails[element.dataset.markoNodeId]; - if ( - (ruleExceptions[ruleId].attrSpread && details.hasDynamicAttrs) || - (ruleExceptions[ruleId].unknownBody && details.hasDynamicBody) || - ruleExceptions[ruleId].dynamicAttrs?.some( - (attr) => element.getAttribute(attr) === "dynamic", - ) - ) { - return []; - } + async function worker( + document: TextDocument, + callback: (markoDocument: MarkoVirtualCode) => T, + ): Promise | undefined> { + const decoded = context.decodeEmbeddedDocumentUri( + URI.parse(document.uri), + ); + const sourceScript = + decoded && context.language.scripts.get(decoded[0]); + const virtualCode = + decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + if (!(virtualCode instanceof MarkoVirtualCode)) return; + + return await callback(virtualCode); } - - const generatedLoc = jsdom.nodeLocation(element); - if (!generatedLoc) return []; - - const sourceRange = extracted.sourceLocationAt( - generatedLoc.startOffset + 1, - generatedLoc.startOffset + 1 + element.tagName.length, - ); - if (!sourceRange) return []; - - return [ - { - range: sourceRange, - severity: 3, - source: `axe-core(${ruleId})`, - message: result.failureSummary ?? "unknown accessibility issue", - }, - ]; - }); - }, + }, + }; }; - -function extract(doc: TextDocument) { - const { parsed } = getMarkoFile(doc); - let cached = extractCache.get(parsed); - - if (!cached) { - cached = extractHTML(parsed); - extractCache.set(parsed, cached); - } - - return cached; -} +// TODO: Actions are done outside the servie layer. +// const HTMLService: LanguageServicePlugin = { +// commands: { +// "$/showHtmlOutput": async (uri: string) => { +// const doc = get(uri); +// if (!doc) return; + +// const { extracted } = extract(doc); + +// return { +// language: "html", +// content: extracted.toString(), +// }; +// }, +// }, +// }; let lock: Promise | undefined; async function acquireMutexLock() { @@ -117,5 +138,3 @@ async function acquireMutexLock() { await currLock; return resolve; } - -export default HTMLService; diff --git a/packages/language-server/src/service/index.ts b/packages/language-server/src/service/index.ts index 9a7a71fd..21e7bd5d 100644 --- a/packages/language-server/src/service/index.ts +++ b/packages/language-server/src/service/index.ts @@ -1,383 +1,117 @@ +// import { create as createEmmetService } from "volar-service-emmet"; +import { create as createCssService } from "volar-service-css"; +import { create as createTypeScriptTwoSlashService } from "volar-service-typescript-twoslash-queries"; +import { create as createTypeScriptServices } from "volar-service-typescript"; import type { - CodeAction, - ColorInformation, - ColorPresentation, - Command, - CompletionItem, - DefinitionLink, - Diagnostic, - DocumentHighlight, - DocumentLink, - Hover, - Location, - MarkedString, - Range, - SymbolInformation, - WorkspaceEdit, -} from "vscode-languageserver"; - -import { MarkupContent, MarkupKind } from "vscode-languageserver"; - -import { - doChange, - doChangeWatchedFiles, - doClose, - doOpen, - get, -} from "../utils/text-documents"; -export const documents = { - get, - doChange, - doOpen, - doClose, - doChangeWatchedFiles, -}; - -import type { Plugin } from "./types"; -import HtmlPlugin from "./html"; -import MarkoPlugin from "./marko"; -import ScriptPlugin from "./script"; -import StylePlugin from "./style"; - -const REG_MARKDOWN_CHARS = /[\\`*_{}[\]<>()#+.!|-]/g; -const plugins = [MarkoPlugin, ScriptPlugin, StylePlugin, HtmlPlugin]; - -/** - * Facade to all embedded plugins, eg css, typescript and our own. - */ -const service: Plugin = { - commands: Object.assign({}, ...plugins.map(({ commands }) => commands)), - async initialize(params) { - await Promise.allSettled( - plugins.map((plugin) => plugin.initialize?.(params)), - ); - }, - async doComplete(doc, params, cancel) { - // TODO: this should handle CompletionList.itemDefaults. - // If there is a single responding plugin, pass through, otherwise need to apply the defaults to the completion items for the plugin. - - // Used to filter out duplicate labels (highest sortText wins). - const results = await Promise.allSettled( - plugins.map((plugin) => plugin.doComplete?.(doc, params, cancel)), - ); - - if (cancel.isCancellationRequested) return; - - const itemsByLabel = new Map(); - for (const result of results) { - if (result.status !== "fulfilled" || !result.value) continue; - - for (const item of Array.isArray(result.value) - ? result.value - : result.value.items) { - const { label } = item; - const existingItem = itemsByLabel.get(label); - if (existingItem) { - if ((existingItem.sortText || label) < (item.sortText || label)) { - itemsByLabel.set(label, item); - } - } else { - itemsByLabel.set(label, item); - } - } - } - - return { items: [...itemsByLabel.values()], isIncomplete: true }; - }, - async doCompletionResolve(item, cancel) { - for (const plugin of plugins) { - try { - const result = await plugin.doCompletionResolve?.(item, cancel); - if (cancel.isCancellationRequested) return; - if (result) return result; - } catch { - // ignore - } - } - }, - async findDefinition(doc, params, cancel) { - const results = await Promise.allSettled( - plugins.map((plugin) => plugin.findDefinition?.(doc, params, cancel)), - ); - - if (cancel.isCancellationRequested) return; - - let links: (Location | DefinitionLink)[] | undefined; - for (const result of results) { - if (result.status !== "fulfilled" || !result.value) continue; - links = (links || []).concat(result.value); - } - - return links; - }, - async findReferences(doc, params, cancel) { - const results = await Promise.allSettled( - plugins.map((plugin) => plugin.findReferences?.(doc, params, cancel)), - ); - - if (cancel.isCancellationRequested) return; - - let references: Location[] | undefined; - for (const result of results) { - if (result.status !== "fulfilled" || !result.value) continue; - references = (references || []).concat(result.value); - } - - return references; - }, - async findDocumentSymbols(doc, params, cancel) { - const results = await Promise.allSettled( - plugins.map((plugin) => - plugin.findDocumentSymbols?.(doc, params, cancel), - ), - ); - - if (cancel.isCancellationRequested) return; - - let symbols: SymbolInformation[] | undefined; - for (const result of results) { - if (result.status !== "fulfilled" || !result.value) continue; - symbols = (symbols || []).concat(result.value); - } - - return symbols; - }, - async findDocumentLinks(doc, params, cancel) { - const results = await Promise.allSettled( - plugins.map((plugin) => plugin.findDocumentLinks?.(doc, params, cancel)), - ); - - if (cancel.isCancellationRequested) return; - - let links: DocumentLink[] | undefined; - for (const result of results) { - if (result.status !== "fulfilled" || !result.value) continue; - links = (links || []).concat(result.value); - } - - return links; - }, - async findDocumentHighlights(doc, params, cancel) { - const results = await Promise.allSettled( - plugins.map((plugin) => - plugin.findDocumentHighlights?.(doc, params, cancel), - ), - ); - - if (cancel.isCancellationRequested) return; - - let highlights: DocumentHighlight[] | undefined; - for (const result of results) { - if (result.status !== "fulfilled" || !result.value) continue; - highlights = (highlights || []).concat(result.value); - } - - return highlights; - }, - async findDocumentColors(doc, params, cancel) { - const results = await Promise.allSettled( - plugins.map((plugin) => plugin.findDocumentColors?.(doc, params, cancel)), - ); - - if (cancel.isCancellationRequested) return; - - let colors: ColorInformation[] | undefined; - for (const result of results) { - if (result.status !== "fulfilled" || !result.value) continue; - colors = (colors || []).concat(result.value); - } - - return colors; - }, - async getColorPresentations(doc, params, cancel) { - const results = await Promise.allSettled( - plugins.map((plugin) => - plugin.getColorPresentations?.(doc, params, cancel), - ), - ); - - if (cancel.isCancellationRequested) return; - - let presentations: ColorPresentation[] | undefined; - for (const result of results) { - if (result.status !== "fulfilled" || !result.value) continue; - presentations = (presentations || []).concat(result.value); - } - - return presentations; - }, - async doHover(doc, params, cancel) { - const results = await Promise.allSettled( - plugins.map((plugin) => plugin.doHover?.(doc, params, cancel)), - ); - - if (cancel.isCancellationRequested) return; - - let hovers: Hover | undefined; - for (const result of results) { - if (result.status !== "fulfilled" || !result.value) continue; - if (hovers) { - hovers.range = maxRange(hovers.range, result.value.range); - hovers.contents = mergeHoverContents( - hovers.contents, - result.value.contents, - ); - } else { - hovers = result.value; - } - } - - return hovers; - }, - async doRename(doc, params, cancel) { - const results = await Promise.allSettled( - plugins.map((plugin) => plugin.doRename?.(doc, params, cancel)), - ); - - if (cancel.isCancellationRequested) return; - - let changes: WorkspaceEdit["changes"]; - let changeAnnotations: WorkspaceEdit["changeAnnotations"]; - let documentChanges: WorkspaceEdit["documentChanges"]; - for (const result of results) { - if (result.status !== "fulfilled" || !result.value) continue; - const { value } = result; - if (value.changes) { - if (changes) { - changes = { ...changes }; - - for (const uri in value.changes) { - changes[uri] = changes[uri] - ? changes[uri].concat(value.changes[uri]) - : value.changes[uri]; - } - } else { - changes = value.changes; - } - } + Connection, + LanguageServicePlugin, + LanguageServicePluginInstance, +} from "@volar/language-server"; +import { Project } from "@marko/language-tools"; +import ts from "typescript"; +import { URI } from "vscode-uri"; +import { create as createHtmlService } from "./html"; +import { create as createMarkoService } from "./marko"; +import { MarkoVirtualCode, getMarkoLanguagePlugin } from "./core/marko-plugin"; +import { getMarkoPrettierService } from "./format"; + +const decoratedHosts = new WeakSet(); + +export function addMarkoTypes( + rootDir: string, + ts: typeof import("typescript"), + host: ts.LanguageServiceHost, +) { + if (decoratedHosts.has(host)) { + return; + } + decoratedHosts.add(host); - if (value.changeAnnotations) { - changeAnnotations = changeAnnotations - ? { - ...changeAnnotations, - ...value.changeAnnotations, - } - : value.changeAnnotations; - } + const getScriptFileNames = host.getScriptFileNames.bind(host); - if (value.documentChanges) { - documentChanges = documentChanges - ? documentChanges.concat(value.documentChanges) - : value.documentChanges; - } - } - - if (changes || changeAnnotations || documentChanges) { - return { - changes, - changeAnnotations, - documentChanges, - }; - } - }, - async doCodeActions(doc, params, cancel) { - const results = await Promise.allSettled( - plugins.map((plugin) => plugin.doCodeActions?.(doc, params, cancel)), - ); + host.getScriptFileNames = () => { + const addedFileNames = []; - if (cancel.isCancellationRequested) return; + const typeLibs = Project.getTypeLibs(rootDir, ts, host); - let actions: (Command | CodeAction)[] | undefined; - for (const result of results) { - if (result.status !== "fulfilled" || !result.value) continue; - actions = (actions || []).concat(result.value); + addedFileNames.push(typeLibs.internalTypesFile); + if (typeLibs.markoRunTypesFile) { + addedFileNames.push(typeLibs.markoRunTypesFile); } - - return actions; - }, - async doValidate(doc) { - const results = await Promise.allSettled( - plugins.map((plugin) => plugin.doValidate?.(doc)), - ); - - let diagnostics: Diagnostic[] | undefined; - for (const result of results) { - if (result.status !== "fulfilled" || !result.value) continue; - diagnostics = (diagnostics || []).concat(result.value); + if (typeLibs.markoRunGeneratedTypesFile) { + addedFileNames.push(typeLibs.markoRunGeneratedTypesFile); } + addedFileNames.push(typeLibs.markoTypesFile); - return diagnostics; - }, - format: MarkoPlugin.format!, -}; - -function maxRange(a: Range | undefined, b: Range | undefined) { - if (!a) return b; - if (!b) return a; - return { - start: { - line: Math.min(a.start.line, b.start.line), - character: Math.min(a.start.character, b.start.character), - }, - end: { - line: Math.max(a.end.line, b.end.line), - character: Math.max(a.end.character, b.end.character), - }, + return [...getScriptFileNames(), ...addedFileNames]; }; } -function mergeHoverContents(a: Hover["contents"], b: Hover["contents"]) { - if (!a) return b; - if (!b) return a; - - if (!MarkupContent.is(a)) { - a = markedStringToMarkupContent(a); - } - - if (!MarkupContent.is(b)) { - b = markedStringToMarkupContent(b); - } - - if (a.kind === b.kind) { - return { - kind: a.kind, - value: `${a.value}\n${b.value}`, - }; - } - - return { - kind: MarkupKind.Markdown, - value: `${markupContentToMarkdown(a)}\n${markupContentToMarkdown(b)}`, - }; -} - -function markedStringToMarkupContent( - markedString: MarkedString | MarkedString[], -): MarkupContent { - return { - kind: MarkupKind.Markdown, - value: Array.isArray(markedString) - ? markedString.map((it) => markedStringToString(it)).join("\n") - : markedStringToString(markedString), - }; +export function getLanguagePlugins(ts: typeof import("typescript")) { + return [getMarkoLanguagePlugin(ts)]; } -function markedStringToString(markedString: MarkedString) { - if (typeof markedString === "string") { - return markedString; - } - - return `\`\`\`${markedString.language}\n${markedString.value}\n\`\`\``; -} +export function getLanguageServicePlugins( + connection: Connection, + ts: typeof import("typescript"), +) { + const result = [ + createHtmlService(), + createCssService(), + ...createTypeScriptServices(ts), + createTypeScriptTwoSlashService(ts), + createMarkoService(ts), + getMarkoPrettierService(connection), + { + name: "marko-debug", + capabilities: { + executeCommandProvider: { + commands: ["marko.extractScript", "marko.extractHtml"], + }, + }, + create(context): LanguageServicePluginInstance { + console.log("Creating marko-debug service"); + return { + executeCommand(command: string, [fileUri]: any[]) { + console.log("executeCommand", command, fileUri); + const uri = URI.parse(fileUri); + + const sourceFile = context.language.scripts.get(uri); + if (!sourceFile) { + return { content: "Error finding source file", language: "ts" }; + } -function markupContentToMarkdown(content: MarkupContent): string { - return content.kind === MarkupKind.Markdown - ? content.value - : escapeMarkdown(content.value); -} + const rootCode = sourceFile?.generated?.root; + if (!(rootCode instanceof MarkoVirtualCode)) { + return { content: "Error finding root code", language: "ts" }; + } -function escapeMarkdown(str: string) { - return str.replace(REG_MARKDOWN_CHARS, "$1"); + switch (command) { + case "marko.extractScript": { + const code = rootCode.embeddedCodes.find((code) => { + return code.id === "script"; + }); + const content = code?.snapshot.getText( + 0, + code.snapshot.getLength(), + ); + return { content, language: "typescript" }; + } + case "marko.extractHtml": { + const code = rootCode.embeddedCodes.find((code) => { + return code.id === "html"; + }); + const content = code?.snapshot.getText( + 0, + code.snapshot.getLength(), + ); + return { content, language: "html" }; + } + } + }, + }; + }, + } satisfies LanguageServicePlugin, + ]; + return result; } - -export { service as default }; diff --git a/packages/language-server/src/service/marko/complete/AttrName.ts b/packages/language-server/src/service/marko/complete/AttrName.ts index 9c22b52d..fb67cf41 100644 --- a/packages/language-server/src/service/marko/complete/AttrName.ts +++ b/packages/language-server/src/service/marko/complete/AttrName.ts @@ -6,17 +6,15 @@ import { MarkupKind, TextEdit, } from "vscode-languageserver"; - import type { Node } from "@marko/language-tools"; - -import type { CompletionMeta, CompletionResult } from "."; - -export function AttrName({ - offset, - node, - file: { parsed, lookup }, -}: CompletionMeta): CompletionResult { - let name = parsed.read(node); +import { MarkoVirtualCode } from "../../core/marko-plugin"; + +export function AttrName( + node: Node.AttrName, + file: MarkoVirtualCode, + offset: number, +): CompletionItem[] | undefined { + let name = file.markoAst.read(node); const modifierIndex = name.indexOf(":"); const hasModifier = modifierIndex !== -1; @@ -40,7 +38,7 @@ export function AttrName({ } const completions: CompletionItem[] = []; - const attrNameLoc = parsed.locationAt( + const attrNameLoc = file.markoAst.locationAt( hasModifier ? { start: node.start, @@ -50,7 +48,7 @@ export function AttrName({ ); const tagName = node.parent.parent.nameText || ""; - const tagDef = tagName && lookup.getTag(tagName); + const tagDef = tagName && file.tagLookup.getTag(tagName); const nestedTagAttrs: { [x: string]: boolean } = {}; if (tagDef && tagDef.nestedTags) { @@ -60,7 +58,7 @@ export function AttrName({ } } - lookup.forEachAttribute(tagName, (attr, parent) => { + file.tagLookup.forEachAttribute(tagName, (attr, parent) => { if ( attr.deprecated || nestedTagAttrs[attr.name] || diff --git a/packages/language-server/src/service/marko/complete/AttrValue.ts b/packages/language-server/src/service/marko/complete/AttrValue.ts index b35e3346..4ae0c36f 100644 --- a/packages/language-server/src/service/marko/complete/AttrValue.ts +++ b/packages/language-server/src/service/marko/complete/AttrValue.ts @@ -1,72 +1,73 @@ -import path from "path"; +// import path from "path"; -import { - CompletionItem, - CompletionItemKind, - TextEdit, -} from "vscode-languageserver"; +// import { +// CompletionItem, +// CompletionItemKind, +// TextEdit, +// } from "vscode-languageserver"; -import type { Node } from "@marko/language-tools"; -import isDocumentLinkAttr from "../util/is-document-link-attr"; -import fileSystem, { FileType } from "../../../utils/file-system"; -import resolveUrl from "../../../utils/resolve-url"; +// import type { Node } from "@marko/language-tools"; +// import isDocumentLinkAttr from "../util/is-document-link-attr"; +// import fileSystem, { FileType } from "../../../utils/file-system"; +// import resolveUrl from "../../../utils/resolve-url"; -import type { CompletionMeta } from "."; +// import type { CompletionMeta } from "."; +// TODO: This completion provider is not migrated because its not obvious how. -export async function AttrValue({ - offset, - node, - file: { uri, parsed, code }, -}: CompletionMeta): Promise { - const attr = node.parent; - if (isDocumentLinkAttr(code, attr.parent, attr)) { - const start = node.value.start + 1; - if (code[start] !== ".") return; // only resolve relative paths +// export async function AttrValue({ +// offset, +// node, +// file: { uri, parsed, code }, +// }: CompletionMeta): Promise { +// const attr = node.parent; +// if (isDocumentLinkAttr(code, attr.parent, attr)) { +// const start = node.value.start + 1; +// if (code[start] !== ".") return; // only resolve relative paths - const end = node.value.end - 1; - const relativeOffset = offset - start; - const rawValue = parsed.read({ - start, - end, - }); +// const end = node.value.end - 1; +// const relativeOffset = offset - start; +// const rawValue = parsed.read({ +// start, +// end, +// }); - const segmentStart = rawValue.lastIndexOf("/", relativeOffset); - if (segmentStart === -1) return; // only resolve after a slash. +// const segmentStart = rawValue.lastIndexOf("/", relativeOffset); +// if (segmentStart === -1) return; // only resolve after a slash. - const req = rawValue.slice(0, segmentStart); - const resolved = resolveUrl(req, uri); +// const req = rawValue.slice(0, segmentStart); +// const resolved = resolveUrl(req, uri); - if (resolved) { - const result: CompletionItem[] = []; - const curFile = req === "." ? path.basename(uri) : undefined; - const replaceRange = parsed.locationAt({ - start: start + segmentStart + 1, - end: start + rawValue.length, - }); +// if (resolved) { +// const result: CompletionItem[] = []; +// const curFile = req === "." ? path.basename(uri) : undefined; +// const replaceRange = parsed.locationAt({ +// start: start + segmentStart + 1, +// end: start + rawValue.length, +// }); - for (const [entry, type] of await fileSystem.readDirectory(resolved)) { - if (entry[0] !== "." && entry !== curFile) { - result.push( - type === FileType.Directory - ? { - label: `${entry}/`, - kind: CompletionItemKind.Folder, - textEdit: TextEdit.replace(replaceRange, `${entry}/`), - command: { - title: "Suggest", - command: "editor.action.triggerSuggest", - }, - } - : { - label: entry, - kind: CompletionItemKind.File, - textEdit: TextEdit.replace(replaceRange, entry), - }, - ); - } - } +// for (const [entry, type] of await fileSystem.readDirectory(resolved)) { +// if (entry[0] !== "." && entry !== curFile) { +// result.push( +// type === FileType.Directory +// ? { +// label: `${entry}/`, +// kind: CompletionItemKind.Folder, +// textEdit: TextEdit.replace(replaceRange, `${entry}/`), +// command: { +// title: "Suggest", +// command: "editor.action.triggerSuggest", +// }, +// } +// : { +// label: entry, +// kind: CompletionItemKind.File, +// textEdit: TextEdit.replace(replaceRange, entry), +// }, +// ); +// } +// } - return result; - } - } -} +// return result; +// } +// } +// } diff --git a/packages/language-server/src/service/marko/complete/Import.ts b/packages/language-server/src/service/marko/complete/Import.ts index 417a9677..266ff438 100644 --- a/packages/language-server/src/service/marko/complete/Import.ts +++ b/packages/language-server/src/service/marko/complete/Import.ts @@ -1,30 +1,28 @@ import { CompletionItem, TextEdit } from "vscode-languageserver"; import type { Node } from "@marko/language-tools"; - import getTagNameCompletion from "../util/get-tag-name-completion"; - -import type { CompletionMeta, CompletionResult } from "."; +import { MarkoVirtualCode } from "../../core/marko-plugin"; const importTagReg = /(['"])<((?:[^'"\\>]+|\\.)*)>?\1/; -export function Import({ - node, - file: { parsed, filename, lookup }, -}: CompletionMeta): CompletionResult { +export function Import( + node: Node.Import, + file: MarkoVirtualCode, +): CompletionItem[] | undefined { // check for import statement - const value = parsed.read(node); + const value = file.markoAst.read(node); const match = importTagReg.exec(value); if (match) { const [{ length }] = match; const fromStart = node.start + match.index; - const range = parsed.locationAt({ + const range = file.markoAst.locationAt({ start: fromStart + 1, end: fromStart + length - 1, }); const result: CompletionItem[] = []; - for (const tag of lookup.getTagsSorted()) { + for (const tag of file.tagLookup.getTagsSorted()) { if ( (tag.template || tag.renderer) && !( @@ -40,7 +38,7 @@ export function Import({ ) { const completion = getTagNameCompletion({ tag, - importer: filename, + importer: file.fileName, }); completion.label = `<${completion.label}>`; diff --git a/packages/language-server/src/service/marko/complete/OpenTagName.ts b/packages/language-server/src/service/marko/complete/OpenTagName.ts index bb369cc9..5a4b9b0e 100644 --- a/packages/language-server/src/service/marko/complete/OpenTagName.ts +++ b/packages/language-server/src/service/marko/complete/OpenTagName.ts @@ -1,16 +1,14 @@ import type { CompletionItem } from "vscode-languageserver"; import { type Node, NodeType } from "@marko/language-tools"; - import getTagNameCompletion from "../util/get-tag-name-completion"; +import { MarkoVirtualCode } from "../../core/marko-plugin"; -import type { CompletionMeta, CompletionResult } from "."; - -export function OpenTagName({ - node, - file: { parsed, filename, lookup }, -}: CompletionMeta): CompletionResult { +export function OpenTagName( + node: Node.OpenTagName, + file: MarkoVirtualCode, +): CompletionItem[] | undefined { const tag = node.parent; - const range = parsed.locationAt(node); + const range = file.markoAst.locationAt(node); const isAttrTag = tag.type === NodeType.AttrTag; const result: CompletionItem[] = []; @@ -18,7 +16,9 @@ export function OpenTagName({ let parentTag = tag.owner; while (parentTag?.type === NodeType.AttrTag) parentTag = parentTag.owner; const parentTagDef = - parentTag && parentTag.nameText && lookup.getTag(parentTag.nameText); + parentTag && + parentTag.nameText && + file.tagLookup.getTag(parentTag.nameText); if (parentTagDef) { const { nestedTags } = parentTagDef; @@ -29,7 +29,7 @@ export function OpenTagName({ getTagNameCompletion({ tag, range, - importer: filename, + importer: file.fileName, showAutoComplete: true, }), ); @@ -40,7 +40,7 @@ export function OpenTagName({ const skipStatements = !( tag.concise && tag.parent.type === NodeType.Program ); - for (const tag of lookup.getTagsSorted()) { + for (const tag of file.tagLookup.getTagsSorted()) { if ( !( tag.name === "*" || @@ -53,7 +53,7 @@ export function OpenTagName({ const completion = getTagNameCompletion({ tag, range, - importer: filename, + importer: file.fileName, showAutoComplete: true, }); completion.sortText = `0${completion.label}`; // Ensure higher priority than typescript. diff --git a/packages/language-server/src/service/marko/complete/Tag.ts b/packages/language-server/src/service/marko/complete/Tag.ts index 72a48a1c..30618198 100644 --- a/packages/language-server/src/service/marko/complete/Tag.ts +++ b/packages/language-server/src/service/marko/complete/Tag.ts @@ -1,23 +1,22 @@ import { + CompletionItem, CompletionItemKind, InsertTextFormat, TextEdit, } from "vscode-languageserver"; - import { type Node, UNFINISHED } from "@marko/language-tools"; - -import type { CompletionMeta, CompletionResult } from "."; +import { MarkoVirtualCode } from "../../core/marko-plugin"; const partialCloseTagReg = /<\/(?:[^><]*>)?/iy; /** * Provide completion for the closing tag. */ -export function Tag({ - node, - offset, - file: { parsed, code }, -}: CompletionMeta): CompletionResult { +export function Tag( + node: Node.Tag, + file: MarkoVirtualCode, + offset: number, +): CompletionItem[] | undefined { const isClosed = node.end !== UNFINISHED; if (isClosed || node.concise) return; @@ -37,6 +36,7 @@ export function Tag({ // We have an unfinished closing tag. const start = node.close.start; partialCloseTagReg.lastIndex = start; + const code = file.snapshot.getText(0, file.snapshot.getLength()); const [{ length }] = partialCloseTagReg.exec(code)!; const end = start + length; @@ -46,7 +46,7 @@ export function Tag({ kind: CompletionItemKind.Class, insertTextFormat: InsertTextFormat.Snippet, textEdit: TextEdit.replace( - parsed.locationAt({ + file.markoAst.locationAt({ start, end, }), diff --git a/packages/language-server/src/service/marko/complete/index.ts b/packages/language-server/src/service/marko/complete/index.ts index c0a00468..967243ea 100644 --- a/packages/language-server/src/service/marko/complete/index.ts +++ b/packages/language-server/src/service/marko/complete/index.ts @@ -1,46 +1,29 @@ -import type { CompletionItem, CompletionParams } from "vscode-languageserver"; import { NodeType } from "@marko/language-tools"; - -import { MarkoFile, getMarkoFile } from "../../../utils/file"; -import type { Plugin, Result } from "../../types"; - +import { CompletionItem } from "vscode-languageserver"; +import { MarkoVirtualCode } from "../../core/marko-plugin"; import { AttrName } from "./AttrName"; -import { AttrValue } from "./AttrValue"; import { Import } from "./Import"; import { OpenTagName } from "./OpenTagName"; import { Tag } from "./Tag"; -export type CompletionResult = Result; -export interface CompletionMeta { - file: MarkoFile; - params: CompletionParams; - offset: number; - node: N; -} +export function provideCompletions( + doc: MarkoVirtualCode, + offset: number, +): CompletionItem[] | undefined { + const node = doc.markoAst.nodeAt(offset); -const handlers: Record< - string, - (data: CompletionMeta) => CompletionResult -> = { - Tag, - OpenTagName, - AttrName, - AttrValue, - Import, -}; - -export const doComplete: Plugin["doComplete"] = async (doc, params) => { - const file = getMarkoFile(doc); - const offset = doc.offsetAt(params.position); - const node = file.parsed.nodeAt(offset); - return { - items: - (await handlers[NodeType[node.type]]?.({ - file, - params, - offset, - node, - })) || [], - isIncomplete: true, - }; -}; + switch (node?.type) { + case NodeType.AttrName: + return AttrName(node, doc, offset); + // case NodeType.AttrValue: + // return AttrValue(node, doc, offset); + case NodeType.Import: + return Import(node, doc); + case NodeType.Tag: + return Tag(node, doc, offset); + case NodeType.OpenTagName: + return OpenTagName(node, doc); + default: + return; + } +} diff --git a/packages/language-server/src/service/marko/definition/AttrName.ts b/packages/language-server/src/service/marko/definition/AttrName.ts index 12bccb6b..caf27e7f 100644 --- a/packages/language-server/src/service/marko/definition/AttrName.ts +++ b/packages/language-server/src/service/marko/definition/AttrName.ts @@ -1,20 +1,19 @@ import fs from "fs"; import { URI } from "vscode-uri"; import { type Node, getLines, getLocation } from "@marko/language-tools"; - +import { LocationLink } from "@volar/language-service"; import RegExpBuilder from "../../../utils/regexp-builder"; import { START_LOCATION } from "../../../utils/constants"; +import { MarkoVirtualCode } from "../../core/marko-plugin"; -import type { DefinitionMeta, DefinitionResult } from "."; - -export function AttrName({ - node, - file: { parsed, lookup }, -}: DefinitionMeta): DefinitionResult { +export function AttrName( + node: Node.AttrName, + file: MarkoVirtualCode, +): LocationLink[] | undefined { const tagName = node.parent.parent.nameText; - const attrName = parsed.read(node); - const tagDef = tagName ? lookup.getTag(tagName) : undefined; - const attrDef = lookup.getAttribute(tagName || "", attrName); + const attrName = file.markoAst.read(node); + const tagDef = tagName ? file.tagLookup.getTag(tagName) : undefined; + const attrDef = file.tagLookup.getAttribute(tagName || "", attrName); let range = START_LOCATION; if (!attrDef) { @@ -45,7 +44,7 @@ export function AttrName({ targetUri: URI.file(attrEntryFile).toString(), targetRange: range, targetSelectionRange: range, - originSelectionRange: parsed.locationAt(node), + originSelectionRange: file.markoAst.locationAt(node), }, ]; } diff --git a/packages/language-server/src/service/marko/definition/OpenTagName.ts b/packages/language-server/src/service/marko/definition/OpenTagName.ts index 892d55d8..f6c183f5 100644 --- a/packages/language-server/src/service/marko/definition/OpenTagName.ts +++ b/packages/language-server/src/service/marko/definition/OpenTagName.ts @@ -1,6 +1,5 @@ import fs from "fs"; import path from "path"; - import { URI } from "vscode-uri"; import type { TagDefinition } from "@marko/babel-utils"; import { @@ -9,16 +8,15 @@ import { getLines, getLocation, } from "@marko/language-tools"; - +import { LocationLink } from "@volar/language-service"; import RegExpBuilder from "../../../utils/regexp-builder"; import { START_LOCATION } from "../../../utils/constants"; +import { MarkoVirtualCode } from "../../core/marko-plugin"; -import type { DefinitionMeta, DefinitionResult } from "."; - -export function OpenTagName({ - node, - file: { parsed, lookup }, -}: DefinitionMeta): DefinitionResult { +export function OpenTagName( + node: Node.OpenTagName, + file: MarkoVirtualCode, +): LocationLink[] | undefined { const tag = node.parent; let tagDef: TagDefinition | null | undefined; let range = START_LOCATION; @@ -28,10 +26,10 @@ export function OpenTagName({ while (parentTag?.type === NodeType.AttrTag) parentTag = parentTag.owner; tagDef = parentTag && parentTag.nameText - ? lookup.getTag(parentTag.nameText) + ? file.tagLookup.getTag(parentTag.nameText) : undefined; } else { - tagDef = tag.nameText ? lookup.getTag(tag.nameText) : undefined; + tagDef = tag.nameText ? file.tagLookup.getTag(tag.nameText) : undefined; } if (!tagDef) { @@ -65,7 +63,7 @@ export function OpenTagName({ targetUri: URI.file(tagEntryFile).toString(), targetRange: range, targetSelectionRange: range, - originSelectionRange: parsed.locationAt(node), + originSelectionRange: file.markoAst.locationAt(node), }, ]; } diff --git a/packages/language-server/src/service/marko/definition/index.ts b/packages/language-server/src/service/marko/definition/index.ts index 6664809f..eb4084bc 100644 --- a/packages/language-server/src/service/marko/definition/index.ts +++ b/packages/language-server/src/service/marko/definition/index.ts @@ -1,38 +1,21 @@ -import type { DefinitionLink, DefinitionParams } from "vscode-languageserver"; import { NodeType } from "@marko/language-tools"; - -import { MarkoFile, getMarkoFile } from "../../../utils/file"; -import type { Plugin, Result } from "../../types"; - +import { LocationLink } from "vscode-languageserver"; +import { MarkoVirtualCode } from "../../core/marko-plugin"; import { AttrName } from "./AttrName"; import { OpenTagName } from "./OpenTagName"; -export type DefinitionResult = Result; -export interface DefinitionMeta { - file: MarkoFile; - params: DefinitionParams; - offset: number; - node: N; -} +export function provideDefinitions( + doc: MarkoVirtualCode, + offset: number, +): LocationLink[] | undefined { + const node = doc.markoAst.nodeAt(offset); -const handlers: Record< - string, - (data: DefinitionMeta) => DefinitionResult -> = { - OpenTagName, - AttrName, -}; - -export const findDefinition: Plugin["findDefinition"] = async (doc, params) => { - const file = getMarkoFile(doc); - const offset = doc.offsetAt(params.position); - const node = file.parsed.nodeAt(offset); - return ( - (await handlers[NodeType[node.type]]?.({ - file, - params, - offset, - node, - })) || [] - ); -}; + switch (node?.type) { + case NodeType.AttrName: + return AttrName(node, doc); + case NodeType.OpenTagName: + return OpenTagName(node, doc); + default: + return; + } +} diff --git a/packages/language-server/src/service/marko/document-links.ts b/packages/language-server/src/service/marko/document-links.ts index 42211c84..5703181a 100644 --- a/packages/language-server/src/service/marko/document-links.ts +++ b/packages/language-server/src/service/marko/document-links.ts @@ -1,97 +1,97 @@ -import type { DocumentLink } from "vscode-languageserver"; -import { type Node, NodeType } from "@marko/language-tools"; -import { MarkoFile, processDoc } from "../../utils/file"; +// import type { DocumentLink } from "vscode-languageserver"; +// import { type Node, NodeType } from "@marko/language-tools"; +// import { MarkoFile, processDoc } from "../../utils/file"; -import resolveUrl from "../../utils/resolve-url"; -import type { Plugin } from "../types"; +// import resolveUrl from "../../utils/resolve-url"; +// import type { Plugin } from "../types"; -import isDocumentLinkAttr from "./util/is-document-link-attr"; +// import isDocumentLinkAttr from "./util/is-document-link-attr"; -const importTagReg = /(['"])<((?:[^'"\\>]+|\\.)*)>?\1/g; +// const importTagReg = /(['"])<((?:[^'"\\>]+|\\.)*)>?\1/g; -export const findDocumentLinks: Plugin["findDocumentLinks"] = async (doc) => { - return processDoc(doc, extractDocumentLinks); -}; +// export const findDocumentLinks: Plugin["findDocumentLinks"] = async (doc) => { +// return processDoc(doc, extractDocumentLinks); +// }; -/** - * Iterate over the Marko CST and extract all the file links in the document. - */ -function extractDocumentLinks({ - uri, - scheme, - parsed, - code, - lookup, -}: MarkoFile): DocumentLink[] { - if (scheme !== "file") { - return []; - } +// /** +// * Iterate over the Marko CST and extract all the file links in the document. +// */ +// function extractDocumentLinks({ +// uri, +// scheme, +// parsed, +// code, +// lookup, +// }: MarkoFile): DocumentLink[] { +// if (scheme !== "file") { +// return []; +// } - const links: DocumentLink[] = []; - const { program, read } = parsed; - const visit = (node: Node.ChildNode) => { - switch (node.type) { - case NodeType.AttrTag: - if (node.body) { - for (const child of node.body) { - visit(child); - } - } - break; - case NodeType.Tag: - if (node.attrs && node.nameText) { - for (const attr of node.attrs) { - if (isDocumentLinkAttr(code, node, attr)) { - const resolved = resolveUrl( - read(attr.value.value).slice(1, -1), - uri, - ); - if (resolved) { - links.push({ - range: parsed.locationAt(attr.value.value), - target: resolveUrl(read(attr.value.value).slice(1, -1), uri), - }); - } - } - } - } - if (node.body) { - for (const child of node.body) { - visit(child); - } - } +// const links: DocumentLink[] = []; +// const { program, read } = parsed; +// const visit = (node: Node.ChildNode) => { +// switch (node.type) { +// case NodeType.AttrTag: +// if (node.body) { +// for (const child of node.body) { +// visit(child); +// } +// } +// break; +// case NodeType.Tag: +// if (node.attrs && node.nameText) { +// for (const attr of node.attrs) { +// if (isDocumentLinkAttr(code, node, attr)) { +// const resolved = resolveUrl( +// read(attr.value.value).slice(1, -1), +// uri, +// ); +// if (resolved) { +// links.push({ +// range: parsed.locationAt(attr.value.value), +// target: resolveUrl(read(attr.value.value).slice(1, -1), uri), +// }); +// } +// } +// } +// } +// if (node.body) { +// for (const child of node.body) { +// visit(child); +// } +// } - break; - } - }; +// break; +// } +// }; - for (const node of program.static) { - // check for import statement (this currently only support the tag import shorthand). - if (node.type === NodeType.Import) { - importTagReg.lastIndex = 0; - const value = parsed.read(node); - const match = importTagReg.exec(value); - if (match) { - const [{ length }, , tagName] = match; - const tagDef = lookup.getTag(tagName); - const fileForTag = tagDef && (tagDef.template || tagDef.renderer); +// for (const node of program.static) { +// // check for import statement (this currently only support the tag import shorthand). +// if (node.type === NodeType.Import) { +// importTagReg.lastIndex = 0; +// const value = parsed.read(node); +// const match = importTagReg.exec(value); +// if (match) { +// const [{ length }, , tagName] = match; +// const tagDef = lookup.getTag(tagName); +// const fileForTag = tagDef && (tagDef.template || tagDef.renderer); - if (fileForTag) { - links.push({ - range: parsed.locationAt({ - start: node.start + match.index, - end: node.start + match.index + length, - }), - target: fileForTag, - }); - } - } - } - } +// if (fileForTag) { +// links.push({ +// range: parsed.locationAt({ +// start: node.start + match.index, +// end: node.start + match.index + length, +// }), +// target: fileForTag, +// }); +// } +// } +// } +// } - for (const node of program.body) { - visit(node); - } +// for (const node of program.body) { +// visit(node); +// } - return links; -} +// return links; +// } diff --git a/packages/language-server/src/service/marko/document-symbols.ts b/packages/language-server/src/service/marko/document-symbols.ts index b81eeec4..de0af2ae 100644 --- a/packages/language-server/src/service/marko/document-symbols.ts +++ b/packages/language-server/src/service/marko/document-symbols.ts @@ -1,59 +1,59 @@ -import { SymbolInformation, SymbolKind } from "vscode-languageserver"; -import { type Node, NodeType } from "@marko/language-tools"; -import type { Plugin } from "../types"; -import { MarkoFile, processDoc } from "../../utils/file"; +// import { SymbolInformation, SymbolKind } from "vscode-languageserver"; +// import { type Node, NodeType } from "@marko/language-tools"; +// import type { Plugin } from "../types"; +// import { MarkoFile, processDoc } from "../../utils/file"; -export const findDocumentSymbols: Plugin["findDocumentSymbols"] = async (doc) => - processDoc(doc, extractDocumentSymbols); +// export const findDocumentSymbols: Plugin["findDocumentSymbols"] = async (doc) => +// processDoc(doc, extractDocumentSymbols); -/** - * Iterate over the Marko CST and extract all the symbols (mostly tags) in the document. - */ -function extractDocumentSymbols({ - uri, - scheme, - parsed, - lookup, -}: MarkoFile): SymbolInformation[] { - if (scheme !== "file") { - return []; - } +// /** +// * Iterate over the Marko CST and extract all the symbols (mostly tags) in the document. +// */ +// function extractDocumentSymbols({ +// uri, +// scheme, +// parsed, +// lookup, +// }: MarkoFile): SymbolInformation[] { +// if (scheme !== "file") { +// return []; +// } - const symbols: SymbolInformation[] = []; - const { program } = parsed; - const visit = (node: Node.ChildNode) => { - switch (node.type) { - case NodeType.Tag: - case NodeType.AttrTag: - symbols.push({ - name: - (node.type === NodeType.AttrTag - ? node.nameText?.slice(node.nameText.indexOf("@")) - : node.nameText) || "<${...}>", - kind: - (node.nameText && - lookup.getTag(node.nameText)?.html && - SymbolKind.Property) || - SymbolKind.Class, - location: { - uri, - range: parsed.locationAt(node), - }, - }); +// const symbols: SymbolInformation[] = []; +// const { program } = parsed; +// const visit = (node: Node.ChildNode) => { +// switch (node.type) { +// case NodeType.Tag: +// case NodeType.AttrTag: +// symbols.push({ +// name: +// (node.type === NodeType.AttrTag +// ? node.nameText?.slice(node.nameText.indexOf("@")) +// : node.nameText) || "<${...}>", +// kind: +// (node.nameText && +// lookup.getTag(node.nameText)?.html && +// SymbolKind.Property) || +// SymbolKind.Class, +// location: { +// uri, +// range: parsed.locationAt(node), +// }, +// }); - if (node.body) { - for (const child of node.body) { - visit(child); - } - } +// if (node.body) { +// for (const child of node.body) { +// visit(child); +// } +// } - break; - } - }; +// break; +// } +// }; - for (const item of program.body) { - visit(item); - } +// for (const item of program.body) { +// visit(item); +// } - return symbols; -} +// return symbols; +// } diff --git a/packages/language-server/src/service/marko/format.ts b/packages/language-server/src/service/marko/format.ts deleted file mode 100644 index 96175edc..00000000 --- a/packages/language-server/src/service/marko/format.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Project } from "@marko/language-tools"; -import * as prettier from "prettier"; -import * as markoPrettier from "prettier-plugin-marko"; -import { CancellationToken, TextEdit } from "vscode-languageserver"; - -import { TextDocument } from "vscode-languageserver-textdocument"; -import { START_POSITION } from "../../utils/constants"; -import { getFSDir, getFSPath } from "../../utils/file"; -import { displayError } from "../../utils/messages"; -import type { Plugin } from "../types"; - -export interface FormatOptions { - tabSize: number; - insertSpaces: boolean; - mode?: "concise" | "html"; -} - -export async function formatDocument( - doc: TextDocument, - formatOptions: FormatOptions, - cancel?: CancellationToken, -) { - try { - const dir = getFSDir(doc); - const filepath = getFSPath(doc); - const text = doc.getText(); - const options: prettier.Options = { - parser: "marko", - filepath, - plugins: [markoPrettier], - tabWidth: formatOptions.tabSize, - useTabs: formatOptions.insertSpaces === false, - markoSyntax: formatOptions.mode ?? "auto", - ...(filepath - ? await prettier - .resolveConfig(filepath, { - editorconfig: true, - }) - .catch(() => null) - : null), - }; - - markoPrettier.setCompiler(Project.getCompiler(dir), Project.getConfig(dir)); - - if (cancel?.isCancellationRequested) return; - - // TODO: format selection - return [ - TextEdit.replace( - { - start: START_POSITION, - end: doc.positionAt(text.length), - }, - await prettier.format(text, options), - ), - ]; - } catch (e) { - displayError(e); - } -} - -export const format: Plugin["format"] = async (doc, params, cancel) => { - return formatDocument(doc, params.options, cancel); -}; diff --git a/packages/language-server/src/service/marko/hover/OpenTagName.ts b/packages/language-server/src/service/marko/hover/OpenTagName.ts index 838f5397..dafea3c0 100644 --- a/packages/language-server/src/service/marko/hover/OpenTagName.ts +++ b/packages/language-server/src/service/marko/hover/OpenTagName.ts @@ -1,23 +1,22 @@ import type { Node } from "@marko/language-tools"; - +import { Hover } from "vscode-languageserver"; import getTagNameCompletion from "../util/get-tag-name-completion"; import { START_LOCATION } from "../../../utils/constants"; +import { MarkoVirtualCode } from "../../core/marko-plugin"; -import type { HoverMeta, HoverResult } from "."; - -export function OpenTagName({ - node, - file: { parsed, filename, lookup }, -}: HoverMeta): HoverResult { +export function OpenTagName( + node: Node.OpenTagName, + file: MarkoVirtualCode, +): Hover | undefined { const tag = node.parent; - const range = parsed.locationAt(node); - const tagDef = tag.nameText && lookup.getTag(tag.nameText); + const range = file.markoAst.locationAt(node); + const tagDef = tag.nameText && file.tagLookup.getTag(tag.nameText); if (tagDef) { const completion = getTagNameCompletion({ tag: tagDef, range: START_LOCATION, - importer: filename, + importer: file.fileName, }); if (completion.documentation) { diff --git a/packages/language-server/src/service/marko/hover/index.ts b/packages/language-server/src/service/marko/hover/index.ts index 2c103ee7..8621b9f7 100644 --- a/packages/language-server/src/service/marko/hover/index.ts +++ b/packages/language-server/src/service/marko/hover/index.ts @@ -1,31 +1,18 @@ -import type { Hover, HoverParams } from "vscode-languageserver"; import { NodeType } from "@marko/language-tools"; - -import { MarkoFile, getMarkoFile } from "../../../utils/file"; -import type { Plugin, Result } from "../../types"; - +import { Hover } from "vscode-languageserver"; +import { MarkoVirtualCode } from "../../core/marko-plugin"; import { OpenTagName } from "./OpenTagName"; -export type HoverResult = Result; -export interface HoverMeta { - file: MarkoFile; - params: HoverParams; - offset: number; - node: N; -} +export function provideHover( + doc: MarkoVirtualCode, + offset: number, +): Hover | undefined { + const node = doc.markoAst.nodeAt(offset); -const handlers: Record) => HoverResult> = { - OpenTagName, -}; - -export const doHover: Plugin["doHover"] = async (doc, params) => { - const file = getMarkoFile(doc); - const offset = doc.offsetAt(params.position); - const node = file.parsed.nodeAt(offset); - return await handlers[NodeType[node.type]]?.({ - file, - params, - offset, - node, - }); -}; + switch (node?.type) { + case NodeType.OpenTagName: + return OpenTagName(node, doc); + default: + return; + } +} diff --git a/packages/language-server/src/service/marko/index.ts b/packages/language-server/src/service/marko/index.ts index 12fbdc36..aff82c96 100644 --- a/packages/language-server/src/service/marko/index.ts +++ b/packages/language-server/src/service/marko/index.ts @@ -1,33 +1,129 @@ -import type { Plugin } from "../types"; +import { + LanguageServicePlugin, + LanguageServicePluginInstance, +} from "@volar/language-service"; +import { TextDocument } from "vscode-languageserver-textdocument"; +import { URI } from "vscode-uri"; +import { MarkoVirtualCode } from "../core/marko-plugin"; +import { provideCompletions } from "./complete"; +import { provideHover } from "./hover"; +import { provideValidations } from "./validate"; +import { provideDefinitions } from "./definition"; -import * as documents from "../../utils/text-documents"; -import { doComplete } from "./complete"; -import { findDefinition } from "./definition"; -import { findDocumentLinks } from "./document-links"; -import { findDocumentSymbols } from "./document-symbols"; -import { FormatOptions, format, formatDocument } from "./format"; -import { doHover } from "./hover"; -import { doValidate } from "./validate"; +export const create = ( + _: typeof import("typescript"), +): LanguageServicePlugin => { + return { + name: "marko", + capabilities: { + hoverProvider: true, + definitionProvider: true, + diagnosticProvider: { + interFileDependencies: true, + workspaceDiagnostics: false, + }, + completionProvider: { + triggerCharacters: [ + ".", + ":", + "<", + ">", + "@", + "/", + '"', + "'", + "`", + " ", + "=", + "*", + "#", + "$", + "+", + "^", + "(", + "[", + "-", + ], + }, + }, + create(context): LanguageServicePluginInstance { + return { + provideDefinition(document, position, token) { + if (token.isCancellationRequested) return; + return worker(document, (virtualCode) => { + const offset = document.offsetAt(position); + return provideDefinitions(virtualCode, offset); + }); + }, + provideDiagnostics(document, token) { + if (token.isCancellationRequested) return; + return worker(document, (virtualCode) => { + return provideValidations(virtualCode); + }); + }, + provideHover(document, position, token) { + if (token.isCancellationRequested) return; + return worker(document, (virtualCode) => { + const offset = document.offsetAt(position); + return provideHover(virtualCode, offset); + }); + }, + provideCompletionItems(document, position, _, token) { + if (token.isCancellationRequested) return; + return worker(document, (virtualCode) => { + const offset = document.offsetAt(position); + const completions = provideCompletions(virtualCode, offset); + + if (completions) { + return { + isIncomplete: false, + items: completions, + }; + } + + return { + items: [], + isIncomplete: true, + }; + }); + }, + }; -export default { - doComplete, - doValidate, - doHover, - findDefinition, - findDocumentLinks, - findDocumentSymbols, - format, - commands: { - "$/formatWithMode": async ({ - doc: docURI, - options, - }: { - doc: string; - options: FormatOptions; - }) => { - const doc = documents.get(docURI)!; - const formatted = await formatDocument(doc, options); - return formatted; + function worker( + document: TextDocument, + callback: (markoDocument: MarkoVirtualCode) => T, + ): T | undefined { + const decoded = context.decodeEmbeddedDocumentUri( + URI.parse(document.uri), + ); + const sourceScript = + decoded && context.language.scripts.get(decoded[0]); + const virtualCode = + decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + if (!(virtualCode instanceof MarkoVirtualCode)) return; + + return callback(virtualCode); + } }, - }, -} as Partial; + }; +}; + +// export default { +// findDefinition, +// findDocumentLinks, +// findDocumentSymbols, +// format, +// commands: { +// "$/formatWithMode": async ({ +// doc: docURI, +// options, +// }: { +// doc: string; +// options: FormatOptions; +// }) => { +// const doc = documents.get(docURI)!; +// const formatted = await formatDocument(doc, options); +// return formatted; +// }, +// }, +// } as Partial; diff --git a/packages/language-server/src/service/marko/validate.ts b/packages/language-server/src/service/marko/validate.ts index 45306b94..0304fe91 100644 --- a/packages/language-server/src/service/marko/validate.ts +++ b/packages/language-server/src/service/marko/validate.ts @@ -1,82 +1,67 @@ -import path from "path"; -import { Project } from "@marko/language-tools"; import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver"; import { DiagnosticType } from "@marko/babel-utils"; -import type { Config } from "@marko/compiler"; -import { getFSPath } from "../../utils/file"; -import type { Plugin } from "../types"; +import { Config } from "@marko/compiler"; +import { MarkoVirtualCode } from "../core/marko-plugin"; const markoErrorRegExp = /^(.+?)\.marko(?:\((\d+)(?:\s*,\s*(\d+))?\))?: (.*)$/gm; -const compilerConfig: Config = { - code: false, - output: "migrate", - sourceMaps: false, - errorRecovery: true, - babelConfig: { - babelrc: false, - configFile: false, - browserslistConfigFile: false, - caller: { - name: "@marko/language-server", - supportsStaticESM: true, - supportsDynamicImport: true, - supportsTopLevelAwait: true, - supportsExportNamespaceFrom: true, - }, - }, -}; -export const doValidate: Plugin["doValidate"] = (doc) => { - const filename = getFSPath(doc); +export async function provideValidations( + file: MarkoVirtualCode, +): Promise { const diagnostics: Diagnostic[] = []; - try { - const { meta } = Project.getCompiler( - filename && path.dirname(filename), - ).compileSync(doc.getText(), filename || "untitled.marko", compilerConfig); + // Instead of compiling in the virtual code, we compile the code as part of the diagnostics + // callback to avoid blocking other more urgent requests like completions or hovers. + const compilerResult = await file.compiler?.compile( + file.code, + file.fileName, + compilerConfig, + ); - if (meta.diagnostics) { - for (const diag of meta.diagnostics) { - const range = diag.loc - ? { - start: { - line: diag.loc.start.line - 1, - character: diag.loc.start.column, - }, - end: { - line: diag.loc.end.line - 1, - character: diag.loc.end.column, - }, - } - : { - start: { line: 0, character: 0 }, - end: { line: 0, character: 0 }, - }; + if (compilerResult) { + if (compilerResult.meta.diagnostics) { + for (const diag of compilerResult.meta.diagnostics) { + const range = diag.loc + ? { + start: { + line: diag.loc.start.line - 1, + character: diag.loc.start.column, + }, + end: { + line: diag.loc.end.line - 1, + character: diag.loc.end.column, + }, + } + : { + start: { line: 0, character: 0 }, + end: { line: 0, character: 0 }, + }; - let severity: DiagnosticSeverity | undefined; + let severity: DiagnosticSeverity | undefined; - switch (diag.type) { - case DiagnosticType.Warning: - case DiagnosticType.Deprecation: - severity = DiagnosticSeverity.Warning; - break; - case DiagnosticType.Suggestion: - severity = DiagnosticSeverity.Hint; - break; - default: - severity = DiagnosticSeverity.Error; - break; - } + switch (diag.type) { + case DiagnosticType.Warning: + case DiagnosticType.Deprecation: + severity = DiagnosticSeverity.Warning; + break; + case DiagnosticType.Suggestion: + severity = DiagnosticSeverity.Hint; + break; + default: + severity = DiagnosticSeverity.Error; + break; + } - diagnostics.push({ - range, - source: "marko", - code: undefined, - tags: undefined, - severity, - message: diag.label, - }); + diagnostics.push({ + range, + source: "marko", + code: undefined, + tags: undefined, + severity, + message: diag.label, + }); + } } } } catch (err) { @@ -84,7 +69,7 @@ export const doValidate: Plugin["doValidate"] = (doc) => { } return diagnostics; -}; +} function addDiagnosticsForError(err: unknown, diagnostics: Diagnostic[]) { if (!isError(err)) { @@ -177,3 +162,22 @@ function isErrorWithLoc(err: unknown): err is Error & { typeof (loc as { end: { column: unknown } }).end.column === "number" ); } + +const compilerConfig: Config = { + code: false, + output: "migrate", + sourceMaps: false, + errorRecovery: true, + babelConfig: { + babelrc: false, + configFile: false, + browserslistConfigFile: false, + caller: { + name: "@marko/language-server", + supportsStaticESM: true, + supportsDynamicImport: true, + supportsTopLevelAwait: true, + supportsExportNamespaceFrom: true, + }, + }, +}; diff --git a/packages/language-server/src/service/script/index.ts b/packages/language-server/src/service/script/index.ts deleted file mode 100644 index 7ed6f58f..00000000 --- a/packages/language-server/src/service/script/index.ts +++ /dev/null @@ -1,1018 +0,0 @@ -import path from "path"; - -import { relativeImportPath } from "relative-import-path"; -import ts from "typescript/lib/tsserverlibrary"; -import { - CompletionItem, - CompletionItemKind, - CompletionItemTag, - DefinitionLink, - Diagnostic, - DiagnosticSeverity, - DiagnosticTag, - InsertTextFormat, - type Range, - TextEdit, -} from "vscode-languageserver"; -import type { TextDocument } from "vscode-languageserver-textdocument"; -import { URI } from "vscode-uri"; -import * as prettier from "prettier"; - -import { - type Extracted, - type Location, - Node, - NodeType, - type Parsed, - Project, - ScriptLang, - extractScript, -} from "@marko/language-tools"; -import { getFSPath, getMarkoFile, processDoc } from "../../utils/file"; -import * as documents from "../../utils/text-documents"; -import * as workspace from "../../utils/workspace"; -import { START_LOCATION } from "../../utils/constants"; -import type { Plugin } from "../types"; - -import { ExtractedSnapshot, patch } from "../../ts-plugin/host"; -import printJSDocTag from "./util/print-jsdoc-tag"; - -// Filter out some syntax errors from the TS compiler which will be surfaced from the marko compiler. -const IGNORE_DIAG_REG = - /^(?:(?:Expression|Identifier|['"][^\w]['"]) expected|Invalid character)\b/i; - -interface TSProject { - rootDir: string; - host: ts.LanguageServiceHost; - service: ts.LanguageService; - markoScriptLang: ScriptLang; -} - -const extractCache = new Map(); -const snapshotCache = new Map(); -const insertModuleStatementLocCache = new WeakMap(); -const markoFileReg = /\.marko$/; -const tsTriggerChars = new Set([".", '"', "'", "`", "/", "@", "<", "#", " "]); -const optionalModifierReg = /\boptional\b/; -const deprecatedModifierReg = /\bdeprecated\b/; -const colorModifierReg = /\bcolor\b/; -const localInternalsPrefix = "__marko_internal_"; -const requiredTSCompilerOptions: ts.CompilerOptions = { - module: ts.ModuleKind.ESNext, - moduleResolution: ts.ModuleResolutionKind.Bundler, - noEmit: true, - allowJs: true, - composite: false, - declaration: false, - skipLibCheck: true, - importHelpers: false, - isolatedModules: true, - resolveJsonModule: true, - skipDefaultLibCheck: true, - emitDeclarationOnly: false, - allowNonTsExtensions: true, - emitDecoratorMetadata: false, -}; -const defaultTSConfig = { - include: [], - compilerOptions: { - lib: ["dom", "node", "esnext"], - } satisfies ts.CompilerOptions, -}; -const extraTSCompilerExtensions: readonly ts.FileExtensionInfo[] = [ - { - extension: ".marko", - isMixedContent: false, - scriptKind: ts.ScriptKind.Deferred, - }, -]; - -const ScriptService: Partial = { - commands: { - "$/showScriptOutput": async (uri: string) => { - const doc = documents.get(uri); - if (doc?.languageId !== "marko") return; - const filename = getFSPath(doc); - if (!filename) return; - const tsProject = getTSProject(filename); - const extracted = processScript(doc, tsProject); - const lang = Project.getScriptLang( - filename, - tsProject.markoScriptLang, - ts, - tsProject.host, - ); - const generated = extracted.toString(); - const content = await prettier - .format(generated, { - parser: lang === ScriptLang.ts ? "typescript" : "babel", - }) - .catch(() => generated); - return { - language: lang === ScriptLang.ts ? "typescript" : "javascript", - content, - }; - }, - }, - async initialize() { - workspace.onConfigChange(() => { - snapshotCache.clear(); - }); - - documents.onFileChange((doc) => { - if (doc) { - const filename = getFSPath(doc)!; - extractCache.delete(filename); - snapshotCache.delete(filename); - } else { - extractCache.clear(); - snapshotCache.clear(); - } - }); - }, - async doComplete(doc, params) { - const fileName = getFSPath(doc); - if (!fileName) return; - - const project = getTSProject(fileName); - const extracted = processScript(doc, project); - const sourceOffset = doc.offsetAt(params.position); - const generatedOffset = extracted.generatedOffsetAt(sourceOffset); - if (generatedOffset === undefined) return; - - const completions = project.service.getCompletionsAtPosition( - fileName, - generatedOffset, - { - ...(await getPreferences(project.markoScriptLang)), - ...params.context, - triggerCharacter: getTSTriggerChar(params.context?.triggerCharacter), - }, - ); - if (!completions?.entries.length) return; - - const result: CompletionItem[] = []; - - for (const completion of completions.entries) { - let { name: label, insertText, sortText } = completion; - if (label.startsWith(localInternalsPrefix)) continue; - - const { replacementSpan } = completion; - let textEdit: CompletionItem["textEdit"]; - let detail: CompletionItem["detail"]; - let kind: CompletionItem["kind"]; - let tags: CompletionItem["tags"]; - let labelDetails: CompletionItem["labelDetails"]; - let source = completion.source; - - if (source && completion.hasAction) { - if (source[0] === ".") { - source = path.resolve(fileName, "..", source); - } - detail = relativeImportPath(fileName, source); - // De-prioritize auto-imported completions. - sortText = `\uffff${sortText}`; - } else if (completion.sourceDisplay) { - const description = ts.displayPartsToString(completion.sourceDisplay); - if (description !== label) { - labelDetails = { description }; - } - } - - if (completion.kindModifiers) { - if (optionalModifierReg.test(completion.kindModifiers)) { - insertText = label; - label += "?"; - } - - if (deprecatedModifierReg.test(completion.kindModifiers)) { - tags = [CompletionItemTag.Deprecated]; - } - - if (colorModifierReg.test(completion.kindModifiers)) { - kind = CompletionItemKind.Color; - } - } - - if (replacementSpan) { - const sourceRange = sourceLocationAtTextSpan( - extracted, - replacementSpan, - ); - - if (sourceRange) { - textEdit = { - range: sourceRange, - newText: insertText || label, - }; - } else { - continue; - } - } - - result.push({ - tags, - label, - detail, - textEdit, - sortText, - insertText, - labelDetails, - filterText: insertText, - preselect: completion.isRecommended || undefined, - kind: kind || convertCompletionItemKind(completion.kind), - insertTextFormat: completion.isSnippet - ? InsertTextFormat.Snippet - : undefined, - data: completion.data && { - originalData: completion.data, - originalName: completion.name, - originalSource: source, - generatedOffset, - fileName, - }, - }); - } - - return { - isIncomplete: true, - items: result, - }; - }, - async doCompletionResolve(item) { - const { data } = item; - if (!data) return; - const { fileName } = data; - if (!fileName) return; - const doc = documents.get(filenameToURI(fileName)); - if (!doc) return; - - const project = getTSProject(fileName); - const detail = project.service.getCompletionEntryDetails( - fileName, - data.generatedOffset, - data.originalName, - {}, - data.originalSource, - await getPreferences(project.markoScriptLang), - data.originalData, - ); - - if (!detail?.codeActions) return; - - const extracted = processScript(doc, project); - const textEdits: CompletionItem["additionalTextEdits"] = - (item.additionalTextEdits = item.additionalTextEdits || []); - - for (const action of detail.codeActions) { - for (const change of action.changes) { - if (change.fileName !== fileName) continue; - for (const { span, newText: rawText } of change.textChanges) { - let range: Range | undefined; - let newText = rawText; - - if (span.length === 0 && /^\s*(?:import|export) /.test(newText)) { - const cached = insertModuleStatementLocCache.get(extracted); - newText = newText.replace(/\n\s*$/, "\n"); - - if (cached) { - range = cached; - } else { - const { parsed } = getMarkoFile(doc); - const offset = getInsertModuleStatementOffset(parsed); - const start = parsed.positionAt(offset); - range = { - start, - end: start, - }; - insertModuleStatementLocCache.set(extracted, range); - } - } else { - range = sourceLocationAtTextSpan(extracted, span); - } - - if (range) { - textEdits.push({ newText, range }); - } - } - } - } - - return item; - }, - findDefinition(doc, params) { - const fileName = getFSPath(doc); - if (!fileName) return; - - const project = getTSProject(fileName); - const extracted = processScript(doc, project); - const sourceOffset = doc.offsetAt(params.position); - const generatedOffset = extracted.generatedOffsetAt(sourceOffset); - if (generatedOffset === undefined) return; - - const boundary = project.service.getDefinitionAndBoundSpan( - fileName, - generatedOffset, - ); - if (!boundary?.definitions) return; - - const originSelectionRange = sourceLocationAtTextSpan( - extracted, - boundary.textSpan, - ); - let result: DefinitionLink[] | DefinitionLink | undefined; - - for (const def of boundary.definitions) { - const targetUri = filenameToURI(def.fileName); - const defDoc = documents.get(targetUri); - if (!defDoc) continue; - - let link: DefinitionLink | undefined; - - if (markoFileReg.test(targetUri)) { - const extracted = processScript(defDoc, project); - const targetSelectionRange = - sourceLocationAtTextSpan(extracted, def.textSpan) || START_LOCATION; - const targetRange = - (def.contextSpan && - sourceLocationAtTextSpan(extracted, def.contextSpan)) || - START_LOCATION; - link = { - targetUri, - targetRange, - targetSelectionRange, - originSelectionRange, - }; - } else { - link = { - targetUri, - targetRange: def.contextSpan - ? docLocationAtTextSpan(defDoc, def.contextSpan) - : START_LOCATION, - targetSelectionRange: docLocationAtTextSpan(defDoc, def.textSpan), - originSelectionRange, - }; - } - - if (link) { - if (result) { - if (Array.isArray(result)) { - result.push(link); - } else { - result = [result, link]; - } - } else { - result = link; - } - } - } - - return result; - }, - doHover(doc, params) { - const fileName = getFSPath(doc); - if (!fileName) return; - - const project = getTSProject(fileName); - const extracted = processScript(doc, project); - const sourceOffset = doc.offsetAt(params.position); - const generatedOffset = extracted.generatedOffsetAt(sourceOffset); - if (generatedOffset === undefined) return; - - const quickInfo = project.service.getQuickInfoAtPosition( - fileName, - generatedOffset, - ); - if (!quickInfo) return; - - const sourceRange = sourceLocationAtTextSpan(extracted, quickInfo.textSpan); - if (!sourceRange) return; - - let contents = ""; - - const displayParts = ts.displayPartsToString(quickInfo.displayParts); - if (displayParts) { - contents += `\`\`\`typescript\n${displayParts}\n\`\`\``; - } - - const documentation = printDocumentation( - quickInfo.documentation, - quickInfo.tags, - ); - if (documentation) { - contents += `\n---\n${documentation}`; - } - - return { - range: sourceRange, - contents, - }; - }, - doRename(doc, params) { - const fileName = getFSPath(doc); - if (!fileName) return; - - const project = getTSProject(fileName); - const extracted = processScript(doc, project); - const sourceOffset = doc.offsetAt(params.position); - const generatedOffset = extracted.generatedOffsetAt(sourceOffset); - if (generatedOffset === undefined) return; - - const renameLocations = project.service.findRenameLocations( - fileName, - generatedOffset, - false, - false, - false, - ); - - if (!renameLocations) return; - - const changes: { [uri: string]: TextEdit[] } = {}; - - for (const rename of renameLocations) { - const renameURI = filenameToURI(rename.fileName); - const renameDoc = documents.get(renameURI); - let edit: TextEdit | undefined; - if (!renameDoc) continue; - if (markoFileReg.test(renameURI)) { - const extracted = processScript(renameDoc, project); - const sourceRange = sourceLocationAtTextSpan( - extracted, - rename.textSpan, - ); - if (sourceRange) { - edit = { - newText: params.newName, - range: sourceRange, - }; - } - } else { - edit = { - newText: params.newName, - range: docLocationAtTextSpan(renameDoc, rename.textSpan), - }; - } - - if (edit) { - if (changes[renameURI]) { - changes[renameURI].push(edit); - } else { - changes[renameURI] = [edit]; - } - } - } - - return { - changes, - }; - }, - doValidate(doc) { - const fileName = getFSPath(doc); - if (!fileName) return; - - const project = getTSProject(fileName); - const extracted = processScript(doc, project); - - let results: Diagnostic[] | undefined; - for (const tsDiag of project.service.getSuggestionDiagnostics(fileName)) { - addDiag(tsDiag); - } - - for (const tsDiag of project.service.getSyntacticDiagnostics(fileName)) { - addDiag(tsDiag); - } - - for (const tsDiag of project.service.getSemanticDiagnostics(fileName)) { - addDiag(tsDiag); - } - - return results; - - function addDiag(tsDiag: ts.Diagnostic) { - const diag = convertDiag(extracted, tsDiag); - if (diag && !IGNORE_DIAG_REG.test(diag.message)) { - if (results) { - results.push(diag); - } else { - results = [diag]; - } - } - } - }, -}; - -function processScript(doc: TextDocument, tsProject: TSProject) { - return processDoc(doc, ({ filename, parsed, lookup }) => { - const { host, markoScriptLang } = tsProject; - return extractScript({ - ts, - parsed, - lookup, - scriptLang: filename - ? Project.getScriptLang(filename, markoScriptLang, ts, host) - : markoScriptLang, - runtimeTypesCode: Project.getTypeLibs(tsProject.rootDir, ts, host) - ?.markoTypesCode, - }); - }); -} - -function getInsertModuleStatementOffset(parsed: Parsed) { - const { program } = parsed; - let firstNode: Node.AnyNode | undefined; - - if (program.static.length) { - // Prefer before the first export, or after the last import. - let lastImport: Node.Import | undefined; - for (const node of program.static) { - switch (node.type) { - case NodeType.Export: - return node.start; - case NodeType.Import: - lastImport = node; - break; - } - } - - if (lastImport) { - return lastImport.end + 1; - } - - firstNode = program.static[0]; - } - - if (program.body.length) { - if (!firstNode || firstNode.start > program.body[0].start) { - firstNode = program.body[0]; - } - } - - // Fall back to after the comments of the first node, - // or the start of the document. - if (firstNode) { - return getOffsetAfterComments(firstNode); - } - - return 0; -} - -function getOffsetAfterComments(node: Node.AnyNode) { - const { comments } = node as Node.Commentable; - if (comments) { - return comments.at(-1)!.end + 1; - } - - return Math.max(0, node.start - 1); -} - -function sourceLocationAtTextSpan( - extracted: Extracted, - { start, length }: ts.TextSpan, -) { - if (start === 0 && length === 0) return START_LOCATION; - return extracted.sourceLocationAt(start, start + length); -} - -function docLocationAtTextSpan( - doc: TextDocument, - { start, length }: ts.TextSpan, -) { - return { - start: doc.positionAt(start), - end: doc.positionAt(start + length), - }; -} - -function getTSConfigFile(fileName: string) { - let configFile: string | undefined; - const docFsDir = path.dirname(fileName); - const cache = Project.getCache(docFsDir); - let configFileCache = cache.get(getTSConfigFile) as - | Map - | undefined; - - if (configFileCache) { - configFile = configFileCache.get(docFsDir); - } else { - configFileCache = new Map(); - cache.set(getTSConfigFile, configFileCache); - } - - if (!configFile) { - configFile = - ts.findConfigFile(fileName, ts.sys.fileExists, "tsconfig.json") || - ts.findConfigFile(fileName, ts.sys.fileExists, "jsconfig.json"); - } - - configFileCache.set(docFsDir, configFile); - - return configFile; -} - -function getTSProject(docFsPath: string): TSProject { - let configFile: string | undefined; - let markoScriptLang = ScriptLang.js; - - if (docFsPath) { - configFile = getTSConfigFile(docFsPath); - if (configFile?.endsWith("tsconfig.json")) { - markoScriptLang = ScriptLang.ts; - } - } - - const basePath = (configFile && path.dirname(configFile)) || process.cwd(); - const cache = Project.getCache(configFile && basePath); - let projectCache = cache.get(getTSProject) as - | Map - | undefined; - let cached: TSProject | undefined; - - // The typescript project and it's language service is - // cached with the Marko compiler cache. - // This causes the cache to be properly cleared when files change. - if (projectCache) { - cached = projectCache.get(basePath); - if (cached) return cached; - } else { - // Within the compiler cache we store a map - // of project paths to project info. - projectCache = new Map(); - cache.set(getTSProject, projectCache); - } - - const { fileNames, options, projectReferences } = - ts.parseJsonConfigFileContent( - (configFile && ts.readConfigFile(configFile, ts.sys.readFile).config) || - defaultTSConfig, - ts.sys, - basePath, - requiredTSCompilerOptions, - configFile, - undefined, - extraTSCompilerExtensions, - ); - - options.rootDir = basePath; - - // Only ts like files can inject globals into the project, so we filter out everything else. - const potentialGlobalFiles = new Set( - fileNames.filter((file) => /\.[cm]?ts$/.test(file)), - ); - - const tsPkgFile = - configFile && - ts.resolveModuleName("typescript/package.json", configFile, options, ts.sys) - .resolvedModule?.resolvedFileName; - const defaultLibFile = path.join( - tsPkgFile ? path.join(tsPkgFile, "../lib") : __dirname, - ts.getDefaultLibFileName(options), - ); - - const resolutionCache = ts.createModuleResolutionCache( - basePath, - getCanonicalFileName, - options, - ); - - const host: ts.LanguageServiceHost = patch( - ts, - configFile, - extractCache, - resolutionCache, - { - getNewLine() { - return ts.sys.newLine; - }, - - useCaseSensitiveFileNames() { - return ts.sys.useCaseSensitiveFileNames; - }, - - getCompilationSettings() { - return options; - }, - - getCurrentDirectory() { - return options.rootDir!; - }, - - getProjectVersion() { - return documents.projectVersion.toString(32); - }, - - getDefaultLibFileName() { - return defaultLibFile; - }, - - getProjectReferences() { - return projectReferences; - }, - - resolveModuleNameLiterals( - moduleLiterals, - containingFile, - redirectedReference, - options, - _containingSourceFile, - _reusedNames, - ) { - return moduleLiterals.map((moduleLiteral) => { - return ts.bundlerModuleNameResolver( - moduleLiteral.text, - containingFile, - options, - host, - resolutionCache, - redirectedReference, - ); - }); - }, - - readDirectory: ts.sys.readDirectory, - - readFile: (filename) => documents.get(filenameToURI(filename))?.getText(), - - fileExists: (filename) => documents.exists(filenameToURI(filename)), - - getScriptFileNames() { - const result = new Set(potentialGlobalFiles); - for (const doc of documents.getAllOpen()) { - const { scheme, fsPath } = URI.parse(doc.uri); - if (scheme === "file") { - const projectForFile = getTSProject(fsPath); - if (projectForFile === tsProject) { - result.add(fsPath); - } - } - } - - return [...result]; - }, - - getScriptVersion(filename) { - return `${documents.get(filenameToURI(filename))?.version ?? -1}`; - }, - - getScriptKind(filename) { - switch (path.extname(filename)) { - case ts.Extension.Js: - case ts.Extension.Cjs: - case ts.Extension.Mjs: - return ts.ScriptKind.JS; - case ts.Extension.Jsx: - return ts.ScriptKind.JSX; - case ts.Extension.Ts: - case ts.Extension.Cts: - case ts.Extension.Mts: - return ts.ScriptKind.TS; - case ts.Extension.Tsx: - return ts.ScriptKind.TSX; - case ts.Extension.Json: - return ts.ScriptKind.JSON; - default: - return ts.ScriptKind.Unknown; - } - }, - - getScriptSnapshot(filename) { - let snapshot = snapshotCache.get(filename); - if (!snapshot) { - const doc = documents.get(filenameToURI(filename)); - if (!doc) return; - snapshot = ts.ScriptSnapshot.fromString(doc.getText()); - snapshotCache.set(filename, snapshot); - } - - return snapshot; - }, - }, - ); - - const tsProject: TSProject = { - host, - rootDir: options.rootDir!, - service: ts.createLanguageService(host), - markoScriptLang, - }; - - projectCache.set(basePath, tsProject); - return tsProject; -} - -function filenameToURI(filename: string) { - return URI.file(filename).toString(); -} - -async function getPreferences( - scriptLang: ScriptLang, -): Promise { - const configName = scriptLang === ScriptLang.js ? "javascript" : "typescript"; - const [preferencesConfig, suggestConfig, inlayHintsConfig] = - await Promise.all([ - workspace.getConfig(`${configName}.preferences`), - workspace.getConfig(`${configName}.suggest`), - workspace.getConfig(`${configName}.inlayHints`), - ]); - - return { - disableSuggestions: suggestConfig.enabled === false, - quotePreference: preferencesConfig.quoteStyle || "auto", - includeCompletionsForModuleExports: suggestConfig.autoImports ?? true, - includeCompletionsForImportStatements: - suggestConfig.includeCompletionsForImportStatements ?? true, - includeCompletionsWithSnippetText: - suggestConfig.includeCompletionsWithSnippetText ?? true, - includeAutomaticOptionalChainCompletions: - suggestConfig.includeAutomaticOptionalChainCompletions ?? true, - includeCompletionsWithInsertText: true, - includeCompletionsWithClassMemberSnippets: - suggestConfig.classMemberSnippets?.enabled ?? true, - includeCompletionsWithObjectLiteralMethodSnippets: - suggestConfig.objectLiteralMethodSnippets?.enabled ?? true, - useLabelDetailsInCompletionEntries: true, - allowIncompleteCompletions: true, - importModuleSpecifierPreference: - preferencesConfig.importModuleSpecifierPreference, - importModuleSpecifierEnding: - preferencesConfig.importModuleSpecifierEnding || "auto", - allowTextChangesInNewFiles: true, - providePrefixAndSuffixTextForRename: true, - includePackageJsonAutoImports: - preferencesConfig.includePackageJsonAutoImports ?? true, - provideRefactorNotApplicableReason: true, - jsxAttributeCompletionStyle: - preferencesConfig.jsxAttributeCompletionStyle ?? "auto", - includeInlayParameterNameHints: - inlayHintsConfig.parameterNames?.enabled ?? "none", - includeInlayParameterNameHintsWhenArgumentMatchesName: - !inlayHintsConfig.parameterNames?.suppressWhenArgumentMatchesName, - includeInlayFunctionParameterTypeHints: - inlayHintsConfig.parameterTypes?.enabled ?? true, - includeInlayVariableTypeHints: - inlayHintsConfig.variableTypes?.enabled ?? true, - includeInlayPropertyDeclarationTypeHints: - inlayHintsConfig.propertyDeclarationTypes?.enabled ?? true, - includeInlayFunctionLikeReturnTypeHints: - inlayHintsConfig.functionLikeReturnTypes?.enabled ?? true, - includeInlayEnumMemberValueHints: - inlayHintsConfig.enumMemberValues?.enabled ?? true, - }; -} - -function printDocumentation( - docs: ts.SymbolDisplayPart[] | undefined, - tags: ts.JSDocTagInfo[] | undefined, -) { - let result = ""; - let sep = ""; - if (docs) { - result += ts.displayPartsToString(docs); - sep = " \n\n"; - } - - if (tags) { - for (const tag of tags) { - result += sep + printJSDocTag(tag); - sep = " \n\n"; - } - } - - return result; -} - -function convertDiag( - extracted: Extracted, - tsDiag: ts.Diagnostic, -): Diagnostic | undefined { - const sourceRange = - tsDiag.start === undefined - ? START_LOCATION - : sourceLocationAtTextSpan(extracted, tsDiag as ts.TextSpan); - - if (sourceRange) { - return { - range: sourceRange, - source: "script", - code: tsDiag.code, - tags: convertDiagTags(tsDiag), - severity: convertDiagSeverity(tsDiag), - message: ts.flattenDiagnosticMessageText(tsDiag.messageText, "\n"), - }; - } -} - -function convertDiagSeverity(tsDiag: ts.Diagnostic) { - switch (tsDiag.category) { - case ts.DiagnosticCategory.Error: - return DiagnosticSeverity.Error; - case ts.DiagnosticCategory.Warning: - return DiagnosticSeverity.Warning; - case ts.DiagnosticCategory.Suggestion: - return DiagnosticSeverity.Hint; - default: - return DiagnosticSeverity.Information; - } -} - -function convertDiagTags(tsDiag: ts.Diagnostic) { - let tags: DiagnosticTag[] | undefined; - - if (tsDiag.reportsDeprecated) { - tags = [DiagnosticTag.Deprecated]; - } - - if (tsDiag.reportsUnnecessary) { - if (tags) tags.push(DiagnosticTag.Unnecessary); - else tags = [DiagnosticTag.Unnecessary]; - } - - return tags; -} - -function convertCompletionItemKind(kind: ts.ScriptElementKind) { - switch (kind) { - case ts.ScriptElementKind.warning: - case ts.ScriptElementKind.linkText: - return CompletionItemKind.Text; - - case ts.ScriptElementKind.keyword: - case ts.ScriptElementKind.primitiveType: - return CompletionItemKind.Keyword; - - case ts.ScriptElementKind.scriptElement: - return CompletionItemKind.File; - - case ts.ScriptElementKind.directory: - return CompletionItemKind.Folder; - - case ts.ScriptElementKind.label: - case ts.ScriptElementKind.string: - return CompletionItemKind.Constant; - - case ts.ScriptElementKind.moduleElement: - case ts.ScriptElementKind.externalModuleName: - return CompletionItemKind.Module; - - case ts.ScriptElementKind.typeElement: - case ts.ScriptElementKind.classElement: - case ts.ScriptElementKind.localClassElement: - return CompletionItemKind.Class; - - case ts.ScriptElementKind.interfaceElement: - return CompletionItemKind.Interface; - - case ts.ScriptElementKind.enumElement: - return CompletionItemKind.Enum; - - case ts.ScriptElementKind.enumMemberElement: - return CompletionItemKind.EnumMember; - - case ts.ScriptElementKind.alias: - case ts.ScriptElementKind.letElement: - case ts.ScriptElementKind.constElement: - case ts.ScriptElementKind.variableElement: - case ts.ScriptElementKind.parameterElement: - case ts.ScriptElementKind.localVariableElement: - return CompletionItemKind.Variable; - - case ts.ScriptElementKind.functionElement: - case ts.ScriptElementKind.localFunctionElement: - return CompletionItemKind.Function; - - case ts.ScriptElementKind.callSignatureElement: - case ts.ScriptElementKind.memberFunctionElement: - case ts.ScriptElementKind.indexSignatureElement: - case ts.ScriptElementKind.constructSignatureElement: - return CompletionItemKind.Method; - - case ts.ScriptElementKind.memberGetAccessorElement: - case ts.ScriptElementKind.memberSetAccessorElement: - case ts.ScriptElementKind.memberVariableElement: - return CompletionItemKind.Field; - - case ts.ScriptElementKind.constructorImplementationElement: - return CompletionItemKind.Constructor; - - case ts.ScriptElementKind.typeParameterElement: - return CompletionItemKind.TypeParameter; - - case ts.ScriptElementKind.link: - case ts.ScriptElementKind.linkName: - return CompletionItemKind.Reference; - - default: - return CompletionItemKind.Property; - } -} - -function getTSTriggerChar(char: string | undefined) { - if (char && tsTriggerChars.has(char)) - return char as ts.CompletionsTriggerCharacter; -} - -function getCanonicalFileName(fileName: string) { - return ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); -} - -export { ScriptService as default }; diff --git a/packages/language-server/src/service/script/util/print-jsdoc-tag.ts b/packages/language-server/src/service/script/util/print-jsdoc-tag.ts deleted file mode 100644 index eef298bb..00000000 --- a/packages/language-server/src/service/script/util/print-jsdoc-tag.ts +++ /dev/null @@ -1,192 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See "Notice" file in this package for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * adopted from https://github.com/microsoft/vscode/blob/b9202b64081bb1267792a8d2d023e4c746ed0f73/extensions/typescript-language-features/src/languageFeatures/util/textRendering.ts - */ - -import type ts from "typescript/lib/tsserverlibrary"; - -const REG_BACK_TICK = /`/g; -const REG_LINE = /\r\n|\n/; -const REG_CODE_BLOCK = /^\s*[~`]{3}/m; -const REG_CAPTION = /^(.*?)<\/caption>\s*(\r\n|\n)/; - -export default function printJSDocTag( - tag: ts.JSDocTagInfo, -): string | undefined { - switch (tag.name) { - case "augments": - case "extends": - case "param": - case "template": { - const body = getTagBodyParts(tag); - if (body?.length === 3) { - const [, param, text] = body; - return `${printTagName(tag.name)} \`${param}\`${printTagBody( - replaceLinks(text), - )}`; - } - break; - } - - case "return": - case "returns": { - if (!tag.text?.length) return undefined; - break; - } - } - - return printTagName(tag.name) + printTagBody(getTagBodyText(tag)); -} - -function getTagBodyParts(tag: ts.JSDocTagInfo): Array | undefined { - if (tag.name === "template") { - const parts = tag.text; - if (parts) { - const params = parts - .filter((p) => p.kind === "typeParameterName") - .map((p) => p.text) - .join(", "); - const docs = parts - .filter((p) => p.kind === "text") - .map((p) => convertLinkTags(p.text.replace(/^\s*-?\s*/, ""))) - .join(" "); - return params ? ["", params, docs] : undefined; - } - } - return convertLinkTags(tag.text).split(/^(\S+)\s*-?\s*/); -} - -function getTagBodyText(tag: ts.JSDocTagInfo): string { - if (!tag.text) return ""; - - const text = convertLinkTags(tag.text); - switch (tag.name) { - case "example": { - const captionTagMatches = REG_CAPTION.exec(text); - if (captionTagMatches) { - const [captionMatch, captionText] = captionTagMatches; - return `${captionText}\n${ensureCodeblock( - captionText.slice(captionMatch.length), - )}`; - } else { - return ensureCodeblock(text); - } - } - case "author": { - const emailMatch = text.match(/(.+)\s<([-.\w]+@[-.\w]+)>/); - - if (emailMatch) { - return `${emailMatch[1]} ${emailMatch[2]}`; - } - - return text; - } - case "default": - return ensureCodeblock(text); - } - - return replaceLinks(text); -} - -function convertLinkTags( - parts: readonly ts.SymbolDisplayPart[] | string | undefined, -): string { - if (!parts) return ""; - if (typeof parts === "string") return parts; - - let result = ""; - let currentLink: - | { name?: string; target?: any; text?: string; readonly linkcode: boolean } - | undefined; - - for (const part of parts) { - switch (part.kind) { - case "link": - if (currentLink) { - if (currentLink.target) { - const linkText = currentLink.text - ? currentLink.text - : escapeBackTicks(currentLink.name ?? ""); - result += `[${ - currentLink.linkcode ? "`" + linkText + "`" : linkText - }](${currentLink.target.file})`; - } else { - const text = currentLink.text ?? currentLink.name; - if (text) { - if (/^https?:/.test(text)) { - const parts = text.split(" "); - if (parts.length === 1) { - result += parts[0]; - } else if (parts.length > 1) { - const linkText = escapeBackTicks(parts.slice(1).join(" ")); - result += `[${ - currentLink.linkcode ? "`" + linkText + "`" : linkText - }](${parts[0]})`; - } - } else { - result += escapeBackTicks(text); - } - } - } - currentLink = undefined; - } else { - currentLink = { - linkcode: part.text === "{@linkcode ", - }; - } - break; - - case "linkName": - if (currentLink) { - currentLink.name = part.text; - currentLink.target = (part as ts.JSDocLinkDisplayPart).target; - } - break; - - case "linkText": - if (currentLink) { - currentLink.text = part.text; - } - break; - - default: - result += part.text; - break; - } - } - return replaceLinks(result); -} - -function replaceLinks(text: string): string { - return text.replace( - /\{@(link|linkplain|linkcode) (https?:\/\/[^ |}]+?)(?:[| ]([^{}\n]+?))?\}/gi, - (_, tag: string, link: string, text?: string) => { - const alt = text ? text.trim() : link; - return `[${tag === "linkcode" ? `\`${alt}\`` : alt}](${link})`; - }, - ); -} - -function printTagBody(text: string | undefined) { - if (text) { - return (REG_LINE.test(text) ? " \n" : " — ") + text; - } - - return ""; -} - -function printTagName(name: string) { - return `*@${name}*`; -} - -function ensureCodeblock(text: string): string { - return REG_CODE_BLOCK.test(text) ? text : "```\n" + text + "\n```"; -} - -function escapeBackTicks(text: string): string { - return text.replace(REG_BACK_TICK, "\\$&"); -} diff --git a/packages/language-server/src/service/style/index.ts b/packages/language-server/src/service/style/index.ts deleted file mode 100644 index bbacdb3b..00000000 --- a/packages/language-server/src/service/style/index.ts +++ /dev/null @@ -1,486 +0,0 @@ -import { - type LanguageService, - type LanguageServiceOptions, - type Stylesheet, - getCSSLanguageService, - getLESSLanguageService, - getSCSSLanguageService, -} from "vscode-css-languageservice"; -import { - ColorInformation, - ColorPresentation, - Diagnostic, - DocumentHighlight, - DocumentLink, - InitializeParams, - InsertReplaceEdit, - Location, - Range, - SymbolInformation, - TextDocumentEdit, - TextEdit, -} from "vscode-languageserver"; -import { TextDocument } from "vscode-languageserver-textdocument"; -import { type Extracted, extractStyle } from "@marko/language-tools"; -import { processDoc } from "../../utils/file"; -import fileSystemProvider from "../../utils/file-system"; -import resolveReference from "../../utils/resolve-url"; -import type { Plugin } from "../types"; - -interface ProcessedStyle { - parsed: Stylesheet; - extracted: Extracted; - virtualDoc: TextDocument; - service: LanguageService; -} - -const services: Record< - string, - (options: LanguageServiceOptions) => LanguageService -> = { - ".css": getCSSLanguageService, - ".less": getLESSLanguageService, - ".scss": getSCSSLanguageService, -}; -let clientCapabilities: InitializeParams["capabilities"] | undefined; - -const StyleSheetService: Partial = { - initialize(params) { - clientCapabilities = params.capabilities; - }, - async doComplete(doc, params) { - const sourceOffset = doc.offsetAt(params.position); - for (const style of processStyle(doc)) { - // Find the first stylesheet data that contains the offset. - const generatedPos = style.extracted.generatedPositionAt(sourceOffset); - if (generatedPos === undefined) continue; - - const result = await style.service.doComplete2( - style.virtualDoc, - generatedPos, - style.parsed, - { resolveReference }, - ); - - if (result.itemDefaults) { - const { editRange } = result.itemDefaults; - if (editRange) { - if ("start" in editRange) { - result.itemDefaults.editRange = getSourceRange(style, editRange); - } else { - editRange.insert = getSourceRange(style, editRange.insert)!; - editRange.replace = getSourceRange(style, editRange.replace)!; - } - } - } - - for (const item of result.items) { - if (item.textEdit) { - item.textEdit = getSourceInsertReplaceEdit(style, item.textEdit); - } - - if (item.additionalTextEdits) { - item.additionalTextEdits = getSourceEdits( - style, - item.additionalTextEdits, - ); - } - } - - return result; - } - }, - findDefinition(doc, params) { - const sourceOffset = doc.offsetAt(params.position); - for (const style of processStyle(doc)) { - // Find the first stylesheet data that contains the offset. - const generatedPos = style.extracted.generatedPositionAt(sourceOffset); - if (generatedPos === undefined) continue; - - const result = style.service.findDefinition( - style.virtualDoc, - generatedPos, - style.parsed, - ); - - if (result) { - const sourceRange = getSourceRange(style, result.range); - if (sourceRange) { - return { - range: sourceRange, - uri: doc.uri, - }; - } - } - - break; - } - }, - findReferences(doc, params) { - const sourceOffset = doc.offsetAt(params.position); - for (const style of processStyle(doc)) { - // Find the first stylesheet data that contains the offset. - const generatedPos = style.extracted.generatedPositionAt(sourceOffset); - if (generatedPos === undefined) continue; - - const result: Location[] = []; - for (const location of style.service.findReferences( - style.virtualDoc, - generatedPos, - style.parsed, - )) { - const sourceRange = getSourceRange(style, location.range); - if (sourceRange) { - result.push({ - range: sourceRange, - uri: location.uri, - }); - } - } - - return result.length ? result : undefined; - } - }, - findDocumentSymbols(doc) { - const result: SymbolInformation[] = []; - for (const extracted of processStyle(doc)) { - for (const symbol of extracted.service.findDocumentSymbols( - extracted.virtualDoc, - extracted.parsed, - )) { - if (symbol.location.uri === doc.uri) { - const sourceRange = getSourceRange(extracted, symbol.location.range); - if (sourceRange) { - result.push({ - kind: symbol.kind, - name: symbol.name, - tags: symbol.tags, - deprecated: symbol.deprecated, - containerName: symbol.containerName, - location: { uri: doc.uri, range: sourceRange }, - }); - } - } else { - result.push(symbol); - } - } - } - - return result.length ? result : undefined; - }, - async findDocumentLinks(doc) { - const result: DocumentLink[] = []; - for (const extracted of processStyle(doc)) { - for (const link of await extracted.service.findDocumentLinks2( - extracted.virtualDoc, - extracted.parsed, - { resolveReference }, - )) { - const sourceRange = getSourceRange(extracted, link.range); - if (sourceRange) { - result.push({ - range: sourceRange, - target: link.target, - tooltip: link.tooltip, - data: link.data, - }); - } - } - } - - return result.length ? result : undefined; - }, - findDocumentHighlights(doc, params) { - const sourceOffset = doc.offsetAt(params.position); - for (const style of processStyle(doc)) { - // Find the first stylesheet data that contains the offset. - const generatedPos = style.extracted.generatedPositionAt(sourceOffset); - if (generatedPos === undefined) continue; - - const result: DocumentHighlight[] = []; - for (const highlight of style.service.findDocumentHighlights( - style.virtualDoc, - generatedPos, - style.parsed, - )) { - const sourceRange = getSourceRange(style, highlight.range); - if (sourceRange) { - result.push({ - range: sourceRange, - kind: highlight.kind, - }); - } - } - - return result.length ? result : undefined; - } - }, - findDocumentColors(doc) { - const result: ColorInformation[] = []; - for (const extracted of processStyle(doc)) { - for (const colorInfo of extracted.service.findDocumentColors( - extracted.virtualDoc, - extracted.parsed, - )) { - const sourceRange = getSourceRange(extracted, colorInfo.range); - if (sourceRange) { - result.push({ - range: sourceRange, - color: colorInfo.color, - }); - } - } - } - - return result.length ? result : undefined; - }, - getColorPresentations(doc, params) { - for (const extracted of processStyle(doc)) { - const generatedRange = getGeneratedRange(doc, extracted, params.range); - // Find the first stylesheet data that contains the offset. - if (generatedRange === undefined) continue; - - const result: ColorPresentation[] = []; - for (const colorPresentation of extracted.service.getColorPresentations( - extracted.virtualDoc, - extracted.parsed, - params.color, - generatedRange, - )) { - const textEdit = - colorPresentation.textEdit && - getSourceEdit(extracted, colorPresentation.textEdit); - const additionalTextEdits = - colorPresentation.additionalTextEdits && - getSourceEdits(extracted, colorPresentation.additionalTextEdits); - - if (textEdit || additionalTextEdits) { - result.push({ - label: colorPresentation.label, - textEdit, - additionalTextEdits, - }); - } - } - - return result.length ? result : undefined; - } - }, - doHover(doc, params) { - const sourceOffset = doc.offsetAt(params.position); - for (const style of processStyle(doc)) { - // Find the first stylesheet data that contains the offset. - const generatedPos = style.extracted.generatedPositionAt(sourceOffset); - if (generatedPos === undefined) continue; - - const result = style.service.doHover( - style.virtualDoc, - generatedPos, - style.parsed, - ); - - if (result) { - if (result.range) { - const sourceRange = getSourceRange(style, result.range); - if (sourceRange) { - return { - range: sourceRange, - contents: result.contents, - }; - } - } else { - return result; - } - } - } - }, - async doRename(doc, params) { - const sourceOffset = doc.offsetAt(params.position); - for (const style of processStyle(doc)) { - // Find the first stylesheet data that contains the offset. - const generatedOffset = style.extracted.generatedOffsetAt(sourceOffset); - if (generatedOffset === undefined) continue; - - const result = style.service.doRename( - style.virtualDoc, - style.virtualDoc.positionAt(generatedOffset), - params.newName, - style.parsed, - ); - - if (result.changes) { - for (const uri in result.changes) { - if (uri === doc.uri) { - result.changes[uri] = - getSourceEdits(style, result.changes[uri]) || []; - } - } - } - - if (result.documentChanges) { - for (const change of result.documentChanges) { - if (TextDocumentEdit.is(change)) { - if (change.textDocument.uri === doc.uri) { - change.edits = getSourceEdits(style, change.edits) || []; - } - } - } - } - - return result; - } - }, - doCodeActions(doc, params) { - for (const extracted of processStyle(doc)) { - // Find the first stylesheet data that contains the offset. - const generatedRange = getGeneratedRange(doc, extracted, params.range); - if (generatedRange === undefined) continue; - - const result = extracted.service.doCodeActions( - extracted.virtualDoc, - generatedRange, - params.context, - extracted.parsed, - ); - - for (const command of result) { - const edits = command.arguments?.[2]; - if (edits && Array.isArray(edits) && isTextEdit(edits[0])) { - command.arguments![2] = getSourceEdits(extracted, edits); - } - } - - return result; - } - }, - doValidate(doc) { - const result: Diagnostic[] = []; - for (const extracted of processStyle(doc)) { - for (const diag of extracted.service.doValidation( - extracted.virtualDoc, - extracted.parsed, - )) { - const sourceRange = getSourceRange(extracted, diag.range); - if (sourceRange) { - diag.source = "style"; - diag.range = sourceRange; - result.push(diag); - } - } - } - - return result.length ? result : undefined; - }, -}; - -export { StyleSheetService as default }; - -function processStyle(doc: TextDocument) { - return processDoc(doc, ({ uri, version, parsed, lookup }) => { - const result: ProcessedStyle[] = []; - for (const [ext, extracted] of extractStyle({ - parsed, - lookup, - })) { - const service = services[ext]?.({ - fileSystemProvider, - clientCapabilities, - }); - if (service) { - const virtualDoc = TextDocument.create( - uri, - "css", - version, - extracted.toString(), - ); - result.push({ - service, - extracted, - virtualDoc, - parsed: service.parseStylesheet(virtualDoc), - }); - } - } - - return result; - }); -} - -function getSourceEdits( - extracted: ProcessedStyle, - edits: TextEdit[], -): TextEdit[] | undefined { - const result: TextEdit[] = []; - - for (const edit of edits) { - const sourceEdit = getSourceEdit(extracted, edit); - if (sourceEdit) { - result.push(sourceEdit); - } - } - - return result.length ? result : undefined; -} - -function getSourceEdit( - extracted: ProcessedStyle, - textEdit: TextEdit, -): TextEdit | undefined { - const sourceRange = getSourceRange(extracted, textEdit.range); - if (sourceRange) { - return { - newText: textEdit.newText, - range: sourceRange, - }; - } -} - -function getSourceInsertReplaceEdit( - extracted: ProcessedStyle, - textEdit: TextEdit | InsertReplaceEdit, -): TextEdit | InsertReplaceEdit | undefined { - if (isTextEdit(textEdit)) { - return getSourceEdit(extracted, textEdit); - } else if (textEdit.replace) { - const sourceRange = getSourceRange(extracted, textEdit.replace); - if (sourceRange) { - return { - newText: textEdit.newText, - replace: sourceRange, - } as InsertReplaceEdit; - } - } else { - const sourceRange = getSourceRange(extracted, textEdit.insert); - if (sourceRange) { - return { - newText: textEdit.newText, - insert: sourceRange, - } as InsertReplaceEdit; - } - } -} - -function getSourceRange( - style: ProcessedStyle, - range: Range, -): Range | undefined { - return style.extracted.sourceLocationAt( - style.virtualDoc.offsetAt(range.start), - style.virtualDoc.offsetAt(range.end), - ); -} - -function getGeneratedRange( - doc: TextDocument, - style: ProcessedStyle, - range: Range, -): Range | undefined { - return style.extracted.generatedLocationAt( - doc.offsetAt(range.start), - doc.offsetAt(range.end), - ); -} - -function isTextEdit(edit: TextEdit | InsertReplaceEdit): edit is TextEdit { - return (edit as TextEdit).range !== undefined; -} diff --git a/packages/language-server/src/service/types.ts b/packages/language-server/src/service/types.ts deleted file mode 100644 index 65c4753e..00000000 --- a/packages/language-server/src/service/types.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { - CancellationToken, - CodeAction, - CodeActionParams, - ColorInformation, - ColorPresentation, - ColorPresentationParams, - Command, - CompletionItem, - CompletionList, - CompletionParams, - DefinitionParams, - Diagnostic, - DocumentColorParams, - DocumentFormattingParams, - DocumentHighlight, - DocumentHighlightParams, - DocumentLink, - DocumentLinkParams, - DocumentSymbolParams, - GenericRequestHandler, - Hover, - HoverParams, - InitializeParams, - Location, - LocationLink, - ReferenceParams, - RenameParams, - SymbolInformation, - TextEdit, - WorkspaceEdit, -} from "vscode-languageserver"; -import type { TextDocument } from "vscode-languageserver-textdocument"; - -export type Result = Promise | V | void; -type Handler = ( - doc: TextDocument, - params: P extends null ? CancellationToken : P, - token: P extends null ? never : CancellationToken, -) => Result; - -export type Plugin = { - commands: Record>; - initialize: (params: InitializeParams) => Promise | void; - doComplete: Handler; - doCompletionResolve: ( - item: CompletionItem, - token: CancellationToken, - ) => Result; - doValidate: (doc: TextDocument) => Result; - doHover: Handler; - doRename: Handler; - doCodeActions: Handler; - findDefinition: Handler< - DefinitionParams, - Location | LocationLink | (Location | LocationLink)[] - >; - findReferences: Handler; - findDocumentSymbols: Handler; - findDocumentLinks: Handler; - findDocumentHighlights: Handler; - findDocumentColors: Handler; - getColorPresentations: Handler; - format: Handler; -}; diff --git a/packages/language-server/src/utils/importPackage.ts b/packages/language-server/src/utils/importPackage.ts new file mode 100644 index 00000000..49ac111e --- /dev/null +++ b/packages/language-server/src/utils/importPackage.ts @@ -0,0 +1,74 @@ +import { dirname } from "path"; + +/** + * Whether or not the current workspace can be trusted. + * TODO rework this to a class which depends on the LsConfigManager + * and inject that class into all places where it's needed (Document etc.) + */ +let isTrusted = true; + +export function setIsTrusted(_isTrusted: boolean) { + isTrusted = _isTrusted; +} + +export interface PackageInfo { + path: string; + version: { + full: string; + major: number; + minor: number; + patch: number; + }; +} + +/** + * This function encapsulates the require call in one place + * so we can replace its content inside rollup builds + * so it's not transformed. + */ +export function dynamicRequire(dynamicFileToRequire: string): any { + // prettier-ignore + return require(dynamicFileToRequire); +} + +export function getPackageInfo(packageName: string, fromPaths: string[]) { + const paths = [__dirname, ...fromPaths]; + + const packageJSONPath = require.resolve(`${packageName}/package.json`, { + paths, + }); + const { version } = dynamicRequire(packageJSONPath); + const [major, minor, patch] = version.split("."); + + return { + path: dirname(packageJSONPath), + version: { + full: version, + major: Number(major), + minor: Number(minor), + patch: Number(patch), + }, + }; +} + +/** + * Get the path of a package's directory from the paths in `fromPath`, if `root` is set to false, it will return the path of the package's entry point + */ +export function getPackagePath( + packageName: string, + fromPath: string[], + root = true, +): string | undefined { + const paths = []; + if (isTrusted) { + paths.unshift(...fromPath); + } + + try { + return root + ? dirname(require.resolve(packageName + "/package.json", { paths })) + : require.resolve(packageName, { paths }); + } catch (e) { + return undefined; + } +} diff --git a/packages/language-tools/src/extractors/html/index.ts b/packages/language-tools/src/extractors/html/index.ts index fc091aee..9c26bb42 100644 --- a/packages/language-tools/src/extractors/html/index.ts +++ b/packages/language-tools/src/extractors/html/index.ts @@ -135,7 +135,6 @@ class HTMLExtractor { const valueString = this.#read(attr.value); const valueType = getAttributeValueType(valueString); if (valueType === undefined) return; - switch (valueType) { case AttributeValueType.True: break; diff --git a/packages/language-tools/src/extractors/html/keywords.ts b/packages/language-tools/src/extractors/html/keywords.ts index 0b9be135..2ac5280c 100644 --- a/packages/language-tools/src/extractors/html/keywords.ts +++ b/packages/language-tools/src/extractors/html/keywords.ts @@ -33,11 +33,18 @@ export function getAttributeValueType( if ( // double quote string - /^"(?:[^"\\]+|\\.)*"$/.test(value) || + /^"(?:[^"\\]*(?:\\.)?)*"$/.test(value) || // single quote string - /^'(?:[^'\\]+|\\.)*'$/.test(value) || + /^'(?:[^'\\]+|\\.)*'$/.test(value) + // *************************************************************************** + // BREAKING CHANGE: This regex is disabled because it causes the LSP to hang. + // *************************************************************************** + // This regex causes the LSP to hang for the following attribute value: + // `test?id=${state.id}` no idea why... + // It doesn't hang in the non-volar version. + // *************************************************************************** // template literal without any interpolations - /^`(?:[^`\\$]+|\\.|\$(?!\{))*`$/.test(value) + // /^`(?:[^`\\$]+|\\.|\$(?!\{))*`$/.test(value) ) { return AttributeValueType.QuotedString; } else if ( diff --git a/packages/language-tools/src/util/extractor.ts b/packages/language-tools/src/util/extractor.ts index d7d5b432..defd5814 100644 --- a/packages/language-tools/src/util/extractor.ts +++ b/packages/language-tools/src/util/extractor.ts @@ -32,7 +32,7 @@ const emptyView = { export class Extractor { #parsed: Parsed; #generated = ""; - #tokens: Token[] = []; + tokens: Token[] = []; constructor(parsed: Parsed) { this.#parsed = parsed; } @@ -47,7 +47,7 @@ export class Extractor { if (typeof range === "string") { this.#generated += range; } else { - this.#tokens.push({ + this.tokens.push({ generatedStart: this.#generated.length, sourceStart: range.start, length: Math.min(this.#parsed.code.length, range.end) - range.start, @@ -60,7 +60,7 @@ export class Extractor { } end() { - return new Extracted(this.#parsed, this.#generated, this.#tokens); + return new Extracted(this.#parsed, this.#generated, this.tokens); } } @@ -69,11 +69,13 @@ export class Extracted { #sourceToGenerated: SourceToGeneratedView | typeof emptyView; #generatedToSource: GeneratedToSourceView | typeof emptyView; #cachedGeneratedLines: number[] | undefined; + tokens: Token[]; constructor( public parsed: Parsed, generated: string, tokens: Token[], ) { + this.tokens = tokens; this.#generated = generated; if (tokens.length === 0) { @@ -159,12 +161,12 @@ export class Extracted { * on the view type. */ abstract class TokenView { - #tokens: Token[]; + tokens: Token[]; #last: number; abstract inStart(token: Token): number; abstract outStart(token: Token): number; constructor(tokens: Token[]) { - this.#tokens = tokens; + this.tokens = tokens; this.#last = tokens.length - 1; } @@ -175,14 +177,14 @@ abstract class TokenView { while (min < max) { const mid = (1 + min + max) >>> 1; - if (this.inStart(this.#tokens[mid]) <= offset) { + if (this.inStart(this.tokens[mid]) <= offset) { min = mid; } else { max = mid - 1; } } - const token = this.#tokens[min]; + const token = this.tokens[min]; const index = offset - this.inStart(token); if (index >= 0 && index <= token.length) { return this.outStart(token) + index; @@ -195,7 +197,7 @@ abstract class TokenView { while (min < max) { const mid = (min + max) >> 1; - const token = this.#tokens[mid]; + const token = this.tokens[mid]; const tokenInEnd = this.inStart(token) + token.length; if (tokenInEnd > inStart) { @@ -205,7 +207,7 @@ abstract class TokenView { } } - const startToken = this.#tokens[max]; + const startToken = this.tokens[max]; const startTokenInStart = this.inStart(startToken); if (startTokenInStart >= inEnd) return; @@ -213,7 +215,7 @@ abstract class TokenView { while (min < max) { const mid = (1 + min + max) >>> 1; - const token = this.#tokens[mid]; + const token = this.tokens[mid]; const tokenEnd = this.inStart(token) + token.length; if (tokenEnd <= inEnd) { @@ -223,7 +225,7 @@ abstract class TokenView { } } - const endToken = this.#tokens[min]; + const endToken = this.tokens[min]; const endTokenInStart = this.inStart(endToken); const endTokenInEnd = endTokenInStart + endToken.length; if (endTokenInEnd < inStart) return; diff --git a/packages/vscode/package.json b/packages/vscode/package.json index f39d97b0..64462d7d 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -25,20 +25,24 @@ ], "commands": [ { - "command": "marko.showScriptOutput", - "title": "Marko: Debug: Show Extracted Script Output" + "command": "marko.debug.showScriptOutput", + "title": "Show Extracted Script Output", + "category": "Marko (Debug)" }, { - "command": "marko.showHtmlOutput", - "title": "Marko: Debug: Show Extracted Static HTML Output" + "command": "marko.debug.showHtmlOutput", + "title": "Show Extracted Static HTML Output", + "category": "Marko (Debug)" }, { - "command": "marko.formatToConciseMode", - "title": "Marko: Format: Force Concise Mode" + "command": "marko.actions.formatToConciseMode", + "title": "Format in Concise Mode", + "category": "Marko" }, { - "command": "marko.formatToHtmlMode", - "title": "Marko: Format: Force Html Mode" + "command": "marko.actions.formatToHtmlMode", + "title": "Format in Html Mode", + "category": "Marko" } ], "typescriptServerPlugins": [ @@ -107,6 +111,8 @@ "@types/vscode": "^1.80.0", "@vscode/test-electron": "^2.4.1", "@vscode/vsce": "^3.1.1", + "@volar/language-server": "^2.4.5", + "@volar/vscode": "^2.4.5", "ovsx": "^0.9.5", "tsx": "^4.19.1", "vscode-languageclient": "^9.0.1" diff --git a/packages/vscode/src/features/debug.ts b/packages/vscode/src/features/debug.ts new file mode 100644 index 00000000..b1d36260 --- /dev/null +++ b/packages/vscode/src/features/debug.ts @@ -0,0 +1,78 @@ +import { + type BaseLanguageClient, + ExecuteCommandParams, + ExecuteCommandRequest, +} from "@volar/vscode"; +import * as vscode from "vscode"; + +export function register( + context: vscode.ExtensionContext, + client: BaseLanguageClient, +) { + context.subscriptions.push( + vscode.commands.registerCommand( + "marko.debug.showScriptOutput", + async () => { + const editor = vscode.window.activeTextEditor; + + if (!editor) { + vscode.window.showErrorMessage( + "You must have an open Marko file to view the script output for.", + ); + return; + } + + const response = await client.sendRequest(ExecuteCommandRequest.type, { + command: "marko.extractScript", + arguments: [client.code2ProtocolConverter.asUri(editor.document.uri)], + } satisfies ExecuteCommandParams); + + if (response) { + await vscode.window.showTextDocument( + await vscode.workspace.openTextDocument(response), + { + preview: true, + viewColumn: vscode.ViewColumn.Beside, + }, + ); + } else { + vscode.window.showErrorMessage( + "Unable to extract script for Marko document.", + ); + } + }, + ), + ); + + context.subscriptions.push( + vscode.commands.registerCommand("marko.debug.showHtmlOutput", async () => { + const editor = vscode.window.activeTextEditor; + + if (!editor) { + vscode.window.showErrorMessage( + "You must have an open Marko file to view the HTML output for.", + ); + return; + } + + const response = await client.sendRequest(ExecuteCommandRequest.type, { + command: "marko.extractHtml", + arguments: [client.code2ProtocolConverter.asUri(editor.document.uri)], + } satisfies ExecuteCommandParams); + + if (response) { + await vscode.window.showTextDocument( + await vscode.workspace.openTextDocument(response), + { + preview: true, + viewColumn: vscode.ViewColumn.Beside, + }, + ); + } else { + vscode.window.showErrorMessage( + "Unable to extract HTML for Marko document.", + ); + } + }), + ); +} diff --git a/packages/vscode/src/index.ts b/packages/vscode/src/index.ts index fa40de0f..c9fb0e20 100644 --- a/packages/vscode/src/index.ts +++ b/packages/vscode/src/index.ts @@ -1,173 +1,71 @@ +import * as serverProtocol from "@volar/language-server/protocol"; import { - type ExtensionContext, - Position, - Range, - TextEdit, - ViewColumn, - WorkspaceEdit, - commands, - window, - workspace, -} from "vscode"; - -import { - LanguageClient, - LanguageClientOptions, - ServerOptions, - TransportKind, -} from "vscode-languageclient/node"; - -let client: LanguageClient; - -export async function activate(ctx: ExtensionContext) { - const module = ctx.asAbsolutePath("dist/server"); - const transport = TransportKind.ipc; - const serverOptions: ServerOptions = { - run: { module, transport }, + LabsInfo, + activateAutoInsertion, + activateWriteVirtualFiles, + createLabsInfo, + getTsdk, +} from "@volar/vscode"; +import * as vscode from "vscode"; +import * as lsp from "vscode-languageclient/node"; +import * as debug from "./features/debug"; + +let client: lsp.BaseLanguageClient; + +export async function activate( + context: vscode.ExtensionContext, +): Promise { + const serverModule = vscode.Uri.joinPath( + context.extensionUri, + "dist", + "server.js", + ); + const runOptions = { execArgv: [] }; + const debugOptions = { execArgv: ["--nolazy", "--inspect=" + 6009] }; + const serverOptions: lsp.ServerOptions = { + run: { + module: serverModule.fsPath, + transport: lsp.TransportKind.ipc, + options: runOptions, + }, debug: { - module, - transport, - options: { - execArgv: ["--inspect-brk=6009", "--enable-source-maps"], - env: { DEBUG: true }, - }, + module: serverModule.fsPath, + transport: lsp.TransportKind.ipc, + options: debugOptions, }, }; - - // Options to control the language client - const clientOptions: LanguageClientOptions = { - // Register the server for marko text documents - documentSelector: [ - { scheme: "file", language: "marko" }, - { scheme: "untitled", language: "marko" }, - ], - synchronize: { - // Synchronize the setting section 'marko' to the server - configurationSection: "marko", - fileEvents: workspace.createFileSystemWatcher( - "**/{*.ts,*.mts,*.cts,*.js,*.mjs,*.cts,*.marko,marko.json,marko-tag.json,tsconfig.json,jsconfig.json,package.json,package-lock.json,pnpm-lock.yaml,yarn.lock}", - false, - false, - false, - ), + const tssdk = await getTsdk(context); + const initializationOptions = { + typescript: { + tsdk: tssdk!.tsdk, }, }; - - // Create the language client and start the client. - client = new LanguageClient("marko", "Marko", serverOptions, clientOptions); - - client.onNotification("showError", window.showErrorMessage); - client.onNotification("showWarning", window.showWarningMessage); - client.onNotification("showInformation", window.showInformationMessage); - client.onNotification("executeCommand", commands.executeCommand); - - ctx.subscriptions.push( - commands.registerCommand("marko.showScriptOutput", async () => { - if (!window.activeTextEditor) { - window.showErrorMessage( - "You must have an open Marko file to view the script output for.", - ); - return; - } - - const result = await client.sendRequest< - { language: string; content: string } | undefined - >("$/showScriptOutput", window.activeTextEditor.document.uri.toString()); - - if (result) { - await window.showTextDocument( - await workspace.openTextDocument(result), - { - preview: true, - viewColumn: ViewColumn.Beside, - }, - ); - } else { - window.showErrorMessage("Unable to extract script for Marko document."); - } - }), - ); - - ctx.subscriptions.push( - commands.registerCommand("marko.showHtmlOutput", async () => { - if (!window.activeTextEditor) { - window.showErrorMessage( - "You must have an open Marko file to view the static HTML output for.", - ); - return; - } - - const result = await client.sendRequest< - { language: string; content: string } | undefined - >("$/showHtmlOutput", window.activeTextEditor.document.uri.toString()); - - if (result) { - await window.showTextDocument( - await workspace.openTextDocument(result), - { - preview: true, - viewColumn: ViewColumn.Beside, - }, - ); - } else { - window.showErrorMessage( - "Unable to extract static HTML for Marko document.", - ); - } - }), - ); - - ctx.subscriptions.push( - commands.registerCommand("marko.formatToConciseMode", async () => { - formatForced("concise"); - }), - ); - - ctx.subscriptions.push( - commands.registerCommand("marko.formatToHtmlMode", async () => { - formatForced("html"); - }), + const clientOptions: lsp.LanguageClientOptions = { + documentSelector: [{ language: "marko" }], + initializationOptions, + }; + client = new lsp.LanguageClient( + "marko", + "Marko Language Server", + serverOptions, + clientOptions, ); - - // Start the client. This will also launch the server await client.start(); -} -export function deactivate(): Thenable | void { - if (!client) { - return undefined; - } + // support for auto close tag + activateAutoInsertion("marko", client); + activateWriteVirtualFiles("marko.action.writeVirtualFiles", client); - return client.stop(); -} + // Register VSCode features. + debug.register(context, client); -async function formatForced(mode: "concise" | "html") { - const { activeTextEditor } = window; - if (!activeTextEditor) { - window.showErrorMessage("No open Marko file detected for formatting"); - return; - } - const edits: TextEdit[] = await client.sendRequest("$/formatWithMode", { - doc: activeTextEditor.document.uri.toString(), - options: { - tabSize: activeTextEditor.options.tabSize, - insertSpaces: activeTextEditor.options.insertSpaces, - mode, - }, - }); - - const workspaceEdit = new WorkspaceEdit(); - - for (const edit of edits) { - workspaceEdit.replace( - activeTextEditor.document.uri, - new Range( - new Position(edit.range.start.line, edit.range.start.character), - new Position(edit.range.end.line, edit.range.end.character), - ), - edit.newText, - ); - } + // support for https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volarjs-labs + // ref: https://twitter.com/johnsoncodehk/status/1656126976774791168 + const labsInfo = createLabsInfo(serverProtocol); + labsInfo.addLanguageClient(client); + return labsInfo.extensionExports; +} - await workspace.applyEdit(workspaceEdit); +export function deactivate(): Thenable | undefined { + return client?.stop(); } diff --git a/tsconfig.json b/tsconfig.json index 64294297..44bf61f1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "module": "ESNext", "skipLibCheck": true, "stripInternal": true, - "noUnusedLocals": true, + "noUnusedLocals": false, // Temporarily disabled. "esModuleInterop": true, "moduleResolution": "node", "resolveJsonModule": true, From 65fb911b4d4e66c7a1d2d3b1dd60fc5422c08070 Mon Sep 17 00:00:00 2001 From: Angus Morton Date: Sat, 19 Oct 2024 16:06:43 +0800 Subject: [PATCH 02/14] Add formatting action and fix formatting --- .../src/service/format/index.ts | 202 +++++++++++------- .../src/service/format/package.ts | 6 +- packages/language-server/src/service/index.ts | 65 +----- .../src/service/marko-action-format.ts | 70 ++++++ .../src/service/marko-debug.ts | 54 +++++ .../src/service/marko/index.ts | 16 -- packages/vscode/package.json | 4 +- packages/vscode/src/features/debug.ts | 5 + packages/vscode/src/features/syntax.ts | 65 ++++++ packages/vscode/src/index.ts | 2 + 10 files changed, 332 insertions(+), 157 deletions(-) create mode 100644 packages/language-server/src/service/marko-action-format.ts create mode 100644 packages/language-server/src/service/marko-debug.ts create mode 100644 packages/vscode/src/features/syntax.ts diff --git a/packages/language-server/src/service/format/index.ts b/packages/language-server/src/service/format/index.ts index 41934949..17dbb0e3 100644 --- a/packages/language-server/src/service/format/index.ts +++ b/packages/language-server/src/service/format/index.ts @@ -1,40 +1,45 @@ import { dirname } from "path"; import { create as createPrettierService } from "volar-service-prettier"; -import type { Connection, LanguageServicePlugin } from "@volar/language-server"; -import { ShowMessageNotification } from "@volar/language-server"; +import type { + FormattingOptions, + LanguageServiceContext, + LanguageServicePlugin, +} from "@volar/language-service"; +import { Connection, ShowMessageNotification } from "@volar/language-server"; import { MessageType } from "@volar/language-server"; import { URI } from "vscode-uri"; import { Project } from "@marko/language-tools"; import * as markoPrettier from "prettier-plugin-marko"; +import { Options } from "prettier"; import { dynamicRequire } from "../../utils/importPackage"; import { getMarkoPrettierPluginPath, importPrettier } from "./package"; export function getMarkoPrettierService( connection: Connection, ): LanguageServicePlugin { - let prettier: ReturnType; - let prettierPluginPath: ReturnType[0]; + let prettier: typeof import("prettier") | undefined; + let prettierPluginPath: string | undefined; let hasShownNotification = false; return createPrettierService( (context) => { - for (const workspaceFolder of context.env.workspaceFolders) { - if (workspaceFolder.scheme === "file") { - prettier = importPrettier(workspaceFolder.fsPath); - const [path] = - getMarkoPrettierPluginPath(workspaceFolder.fsPath) ?? []; - prettierPluginPath = path; - if ((!prettier || !prettierPluginPath) && !hasShownNotification) { - connection.sendNotification(ShowMessageNotification.type, { - message: - "Couldn't load `prettier` or `prettier-plugin-marko`. Formatting will not work. Please make sure those two packages are installed into your project and restart the language server.", - type: MessageType.Warning, - }); - hasShownNotification = true; - } - } - return prettier; + const { prettierInstance, markoPluginPath } = + getPrettierInstance(context); + + prettier = prettierInstance; + prettierPluginPath = markoPluginPath; + + // Show a warning notification if Prettier or the Marko plugin isn't installed. + if ((!prettier || !prettierPluginPath) && !hasShownNotification) { + connection.sendNotification(ShowMessageNotification.type, { + message: + "Couldn't load `prettier` or `prettier-plugin-marko`. Formatting will not work. Please make sure those two packages are installed into your project and restart the language server.", + type: MessageType.Warning, + }); + hasShownNotification = true; } + + return prettierInstance; }, { documentSelector: [{ language: "marko" }], @@ -57,69 +62,110 @@ export function getMarkoPrettierService( formatOptions, context, ) => { - const filePath = URI.parse(document.uri).fsPath; - const fileDir = dirname(filePath); - let configOptions = null; - try { - configOptions = await prettierInstance.resolveConfig(filePath, { - useCache: false, - editorconfig: true, - }); - } catch (e) { - connection.sendNotification(ShowMessageNotification.type, { - message: `Failed to load Prettier config.\n\nError:\n${e}`, - type: MessageType.Warning, - }); - console.error("Failed to load Prettier config.", e); - } - - const editorOptions = await context.env.getConfiguration?.( - "prettier", + return getFormattingOptions( + prettierInstance, + prettierPluginPath, document.uri, + formatOptions, + context, + (message) => { + connection.sendNotification(ShowMessageNotification.type, { + message, + type: MessageType.Warning, + }); + }, ); + }, + }, + ); +} - // Return a config with the following cascade: - // - Prettier config file should always win if it exists, if it doesn't: - // - Prettier config from the VS Code extension is used, if it doesn't exist: - // - Use the editor's basic configuration settings - const resolvedConfig = { - filepath: filePath, - tabWidth: formatOptions.tabSize, - useTabs: !formatOptions.insertSpaces, - ...editorOptions, - ...configOptions, - }; +export function getPrettierInstance(context: LanguageServiceContext): { + prettierInstance?: typeof import("prettier"); + markoPluginPath?: string; +} { + for (const workspaceFolder of context.env.workspaceFolders) { + if (workspaceFolder.scheme === "file") { + const prettierInstance = importPrettier(workspaceFolder.fsPath); + const markoPluginPath = getMarkoPrettierPluginPath( + workspaceFolder.fsPath, + ); - try { - let resolvedPlugin; - if (prettierPluginPath) { - resolvedPlugin = dynamicRequire(prettierPluginPath); - } else { - // TODO: Fallback to the built-in version of marko-prettier-plugin if the workspace doesn't have it installed. - resolvedPlugin = markoPrettier; - } + return { prettierInstance, markoPluginPath }; + } + } + return {}; +} - resolvedPlugin.setCompiler( - Project.getCompiler(fileDir), - Project.getConfig(fileDir), - ); +export async function getFormattingOptions( + prettierInstance: typeof import("prettier"), + markoPluginPath: string | undefined, + documentUriString: string, + formatOptions: FormattingOptions, + context: LanguageServiceContext, + onError?: (message: string) => void, +): Promise { + const uri = URI.parse(documentUriString); + const documentUri = context.decodeEmbeddedDocumentUri(uri)?.[0] ?? uri; + const filePath = documentUri.fsPath; - return { - ...resolvedConfig, - plugins: [resolvedPlugin, ...(resolvedConfig.plugins ?? [])], - parser: "marko", - }; - } catch (e) { - connection.sendNotification(ShowMessageNotification.type, { - message: `Failed to configure marko-prettier-plugin.\n\nError:\n${e}`, - type: MessageType.Warning, - }); - console.error("Failed to load Prettier config.", e); - return { - ...resolvedConfig, - }; - } - }, - }, + if (!filePath) { + return {}; + } + + const fileDir = dirname(filePath); + let configOptions = null; + try { + configOptions = await prettierInstance.resolveConfig(filePath, { + useCache: false, + editorconfig: true, + }); + } catch (e) { + onError && onError(`Failed to load Prettier config.\n\nError:\n${e}`); + console.error("Failed to load Prettier config.", e); + } + + const editorOptions = await context.env.getConfiguration?.( + "prettier", + documentUriString, ); + + // Return a config with the following cascade: + // - Prettier config file should always win if it exists, if it doesn't: + // - Prettier config from the VS Code extension is used, if it doesn't exist: + // - Use the editor's basic configuration settings + const resolvedConfig = { + filepath: filePath, + tabWidth: formatOptions.tabSize, + useTabs: !formatOptions.insertSpaces, + ...editorOptions, + ...configOptions, + }; + + try { + let resolvedPlugin; + if (markoPluginPath) { + resolvedPlugin = dynamicRequire(markoPluginPath); + } else { + resolvedPlugin = markoPrettier; + } + + resolvedPlugin.setCompiler( + Project.getCompiler(fileDir), + Project.getConfig(fileDir), + ); + + return { + ...resolvedConfig, + plugins: [resolvedPlugin, ...(resolvedConfig.plugins ?? [])], + parser: "marko", + }; + } catch (e) { + onError && + onError(`Failed to configure marko-prettier-plugin.\n\nError:\n${e}`); + console.error("Failed to load Prettier config.", e); + return { + ...resolvedConfig, + }; + } } diff --git a/packages/language-server/src/service/format/package.ts b/packages/language-server/src/service/format/package.ts index 4272319c..6e6547d9 100644 --- a/packages/language-server/src/service/format/package.ts +++ b/packages/language-server/src/service/format/package.ts @@ -13,7 +13,7 @@ export function importPrettier(fromPath: string): typeof prettier | undefined { export function getMarkoPrettierPluginPath( fromPath: string, -): [string, "prettier-plugin-marko"] | [undefined, undefined] { +): string | undefined { const corePluginPath = getPackagePath( "prettier-plugin-marko", [fromPath, __dirname], @@ -22,8 +22,8 @@ export function getMarkoPrettierPluginPath( // Prefer the official plugin if it's installed. if (corePluginPath) { - return [corePluginPath, "prettier-plugin-marko"]; + return corePluginPath; } - return [undefined, undefined]; + return undefined; } diff --git a/packages/language-server/src/service/index.ts b/packages/language-server/src/service/index.ts index 21e7bd5d..181eec9a 100644 --- a/packages/language-server/src/service/index.ts +++ b/packages/language-server/src/service/index.ts @@ -2,18 +2,15 @@ import { create as createCssService } from "volar-service-css"; import { create as createTypeScriptTwoSlashService } from "volar-service-typescript-twoslash-queries"; import { create as createTypeScriptServices } from "volar-service-typescript"; -import type { - Connection, - LanguageServicePlugin, - LanguageServicePluginInstance, -} from "@volar/language-server"; +import type { Connection } from "@volar/language-server"; import { Project } from "@marko/language-tools"; import ts from "typescript"; -import { URI } from "vscode-uri"; import { create as createHtmlService } from "./html"; import { create as createMarkoService } from "./marko"; -import { MarkoVirtualCode, getMarkoLanguagePlugin } from "./core/marko-plugin"; +import { getMarkoLanguagePlugin } from "./core/marko-plugin"; import { getMarkoPrettierService } from "./format"; +import { create as createMarkoDebugService } from "./marko-debug"; +import { create as createMarkoFormatActionService } from "./marko-action-format"; const decoratedHosts = new WeakSet(); @@ -56,62 +53,14 @@ export function getLanguageServicePlugins( ts: typeof import("typescript"), ) { const result = [ + createMarkoService(ts), createHtmlService(), createCssService(), ...createTypeScriptServices(ts), createTypeScriptTwoSlashService(ts), - createMarkoService(ts), getMarkoPrettierService(connection), - { - name: "marko-debug", - capabilities: { - executeCommandProvider: { - commands: ["marko.extractScript", "marko.extractHtml"], - }, - }, - create(context): LanguageServicePluginInstance { - console.log("Creating marko-debug service"); - return { - executeCommand(command: string, [fileUri]: any[]) { - console.log("executeCommand", command, fileUri); - const uri = URI.parse(fileUri); - - const sourceFile = context.language.scripts.get(uri); - if (!sourceFile) { - return { content: "Error finding source file", language: "ts" }; - } - - const rootCode = sourceFile?.generated?.root; - if (!(rootCode instanceof MarkoVirtualCode)) { - return { content: "Error finding root code", language: "ts" }; - } - - switch (command) { - case "marko.extractScript": { - const code = rootCode.embeddedCodes.find((code) => { - return code.id === "script"; - }); - const content = code?.snapshot.getText( - 0, - code.snapshot.getLength(), - ); - return { content, language: "typescript" }; - } - case "marko.extractHtml": { - const code = rootCode.embeddedCodes.find((code) => { - return code.id === "html"; - }); - const content = code?.snapshot.getText( - 0, - code.snapshot.getLength(), - ); - return { content, language: "html" }; - } - } - }, - }; - }, - } satisfies LanguageServicePlugin, + createMarkoDebugService(), + createMarkoFormatActionService(), ]; return result; } diff --git a/packages/language-server/src/service/marko-action-format.ts b/packages/language-server/src/service/marko-action-format.ts new file mode 100644 index 00000000..89a9a864 --- /dev/null +++ b/packages/language-server/src/service/marko-action-format.ts @@ -0,0 +1,70 @@ +import { LanguageServicePlugin } from "@volar/language-service"; +import { URI } from "vscode-uri"; +import { MarkoVirtualCode } from "./core/marko-plugin"; +import { getFormattingOptions, getPrettierInstance } from "./format"; + +export const create = (): LanguageServicePlugin => { + return { + name: "marko-action-format", + capabilities: { + executeCommandProvider: { + commands: ["marko.formatWithSyntax"], + }, + }, + create(context) { + return { + async executeCommand(_command: string, [fileUri, options]: any[]) { + const uri = URI.parse(fileUri); + + const sourceFile = context.language.scripts.get(uri); + if (!sourceFile) { + return; + } + + const rootCode = sourceFile?.generated?.root; + if (!(rootCode instanceof MarkoVirtualCode)) { + return; + } + + const { prettierInstance, markoPluginPath } = + getPrettierInstance(context); + if (!prettierInstance || !markoPluginPath) { + return; + } + + const prettierOptions = await getFormattingOptions( + prettierInstance, + markoPluginPath, + fileUri, + options, + context, + ); + + // Override the configured options with the options passed in. + const prettierConfig = { ...prettierOptions, ...options }; + const oldText = rootCode.code; + const newText = await prettierInstance.format( + oldText, + prettierConfig, + ); + + // TODO: Is there another way to do positionAt that doesn't involve getting the document? + const document = context.documents.get( + fileUri, + "marko", + rootCode.snapshot, + ); + return [ + { + newText, + range: { + start: { line: 0, character: 0 }, + end: document.positionAt(oldText.length), + }, + }, + ]; + }, + }; + }, + }; +}; diff --git a/packages/language-server/src/service/marko-debug.ts b/packages/language-server/src/service/marko-debug.ts new file mode 100644 index 00000000..e0350cac --- /dev/null +++ b/packages/language-server/src/service/marko-debug.ts @@ -0,0 +1,54 @@ +import { LanguageServicePlugin } from "@volar/language-service"; +import { URI } from "vscode-uri"; +import { MarkoVirtualCode } from "./core/marko-plugin"; + +export const create = (): LanguageServicePlugin => { + return { + name: "marko-debug", + capabilities: { + executeCommandProvider: { + commands: ["marko.extractScript", "marko.extractHtml"], + }, + }, + create(context) { + return { + executeCommand(command: string, [fileUri]: any[]) { + const uri = URI.parse(fileUri); + + const sourceFile = context.language.scripts.get(uri); + if (!sourceFile) { + return; + } + + const rootCode = sourceFile?.generated?.root; + if (!(rootCode instanceof MarkoVirtualCode)) { + return; + } + + switch (command) { + case "marko.extractScript": { + const code = rootCode.embeddedCodes.find((code) => { + return code.id === "script"; + }); + const content = code?.snapshot.getText( + 0, + code.snapshot.getLength(), + ); + return { content, language: "typescript" }; + } + case "marko.extractHtml": { + const code = rootCode.embeddedCodes.find((code) => { + return code.id === "html"; + }); + const content = code?.snapshot.getText( + 0, + code.snapshot.getLength(), + ); + return { content, language: "html" }; + } + } + }, + }; + }, + }; +}; diff --git a/packages/language-server/src/service/marko/index.ts b/packages/language-server/src/service/marko/index.ts index aff82c96..ce5b2f2f 100644 --- a/packages/language-server/src/service/marko/index.ts +++ b/packages/language-server/src/service/marko/index.ts @@ -109,21 +109,5 @@ export const create = ( }; // export default { -// findDefinition, // findDocumentLinks, // findDocumentSymbols, -// format, -// commands: { -// "$/formatWithMode": async ({ -// doc: docURI, -// options, -// }: { -// doc: string; -// options: FormatOptions; -// }) => { -// const doc = documents.get(docURI)!; -// const formatted = await formatDocument(doc, options); -// return formatted; -// }, -// }, -// } as Partial; diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 64462d7d..56819176 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -35,12 +35,12 @@ "category": "Marko (Debug)" }, { - "command": "marko.actions.formatToConciseMode", + "command": "marko.actions.formatToConciseSyntax", "title": "Format in Concise Mode", "category": "Marko" }, { - "command": "marko.actions.formatToHtmlMode", + "command": "marko.actions.formatToHtmlSyntax", "title": "Format in Html Mode", "category": "Marko" } diff --git a/packages/vscode/src/features/debug.ts b/packages/vscode/src/features/debug.ts index b1d36260..e4a95ab7 100644 --- a/packages/vscode/src/features/debug.ts +++ b/packages/vscode/src/features/debug.ts @@ -5,6 +5,11 @@ import { } from "@volar/vscode"; import * as vscode from "vscode"; +/** + * Registers the debug commands for the Marko extension. This is useful if you don't have the Volar Labs + * extension installed. Prefer using the Volar Labs extension if you have it installed because it's more + * feature-rich. + */ export function register( context: vscode.ExtensionContext, client: BaseLanguageClient, diff --git a/packages/vscode/src/features/syntax.ts b/packages/vscode/src/features/syntax.ts new file mode 100644 index 00000000..004025a6 --- /dev/null +++ b/packages/vscode/src/features/syntax.ts @@ -0,0 +1,65 @@ +import { + type BaseLanguageClient, + ExecuteCommandParams, + ExecuteCommandRequest, + TextEdit, +} from "@volar/vscode"; +import * as vscode from "vscode"; + +export function register( + context: vscode.ExtensionContext, + client: BaseLanguageClient, +) { + context.subscriptions.push( + vscode.commands.registerCommand( + "marko.actions.formatToConciseSyntax", + formatWithSyntax("concise"), + ), + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + "marko.actions.formatToHtmlSyntax", + formatWithSyntax("html"), + ), + ); + + function formatWithSyntax(mode: "concise" | "html") { + return async () => { + const { activeTextEditor } = vscode.window; + if (!activeTextEditor) { + vscode.window.showErrorMessage( + "No open Marko file detected for formatting", + ); + return; + } + + const response: TextEdit[] = await client.sendRequest( + ExecuteCommandRequest.type, + { + command: "marko.formatWithSyntax", + arguments: [ + client.code2ProtocolConverter.asUri(activeTextEditor.document.uri), + { + markoSyntax: mode, + }, + ], + } satisfies ExecuteCommandParams, + ); + + const edits = await client.protocol2CodeConverter.asTextEdits(response); + + if (edits) { + activeTextEditor.edit((editBuilder) => { + for (const edit of edits) { + editBuilder.replace(edit.range, edit.newText); + } + }); + } else { + vscode.window.showInformationMessage( + "No changes were made to the document", + ); + } + }; + } +} diff --git a/packages/vscode/src/index.ts b/packages/vscode/src/index.ts index c9fb0e20..7709a7ed 100644 --- a/packages/vscode/src/index.ts +++ b/packages/vscode/src/index.ts @@ -9,6 +9,7 @@ import { import * as vscode from "vscode"; import * as lsp from "vscode-languageclient/node"; import * as debug from "./features/debug"; +import * as syntax from "./features/syntax"; let client: lsp.BaseLanguageClient; @@ -58,6 +59,7 @@ export async function activate( // Register VSCode features. debug.register(context, client); + syntax.register(context, client); // support for https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volarjs-labs // ref: https://twitter.com/johnsoncodehk/status/1656126976774791168 From 577f64c6ea73859b7bddfae524af3aa4b07d7b71 Mon Sep 17 00:00:00 2001 From: Angus Morton Date: Sun, 20 Oct 2024 14:23:01 +0800 Subject: [PATCH 03/14] Defer to the HTML service for document links --- .../language-server/src/service/html/index.ts | 31 +++--- packages/language-server/src/service/index.ts | 1 + .../src/service/marko/document-links.ts | 97 ------------------- .../src/service/marko/index.ts | 23 ++++- packages/vscode/build.mts | 28 ++++-- 5 files changed, 53 insertions(+), 127 deletions(-) delete mode 100644 packages/language-server/src/service/marko/document-links.ts diff --git a/packages/language-server/src/service/html/index.ts b/packages/language-server/src/service/html/index.ts index d57917f6..90f2b4a6 100644 --- a/packages/language-server/src/service/html/index.ts +++ b/packages/language-server/src/service/html/index.ts @@ -3,20 +3,33 @@ import axe from "axe-core"; import { JSDOM } from "jsdom"; import type { Diagnostic, LanguageServicePlugin } from "@volar/language-server"; import { URI } from "vscode-uri"; +import { create as createHtmlService } from "volar-service-html"; import { MarkoVirtualCode } from "../core/marko-plugin"; import { ruleExceptions } from "./axe-rules/rule-exceptions"; export const create = (): LanguageServicePlugin => { + const baseService = createHtmlService({ + configurationSections: { + autoCreateQuotes: "", + autoClosingTags: "", + }, + }); return { name: "marko-template", capabilities: { diagnosticProvider: { - interFileDependencies: true, + interFileDependencies: false, workspaceDiagnostics: false, }, + documentLinkProvider: baseService.capabilities.documentLinkProvider, }, create(context) { + const baseServiceInstance = baseService.create(context); return { + provideDocumentLinks(document, token) { + // Defer to the HTML service to provide links for us. + return baseServiceInstance.provideDocumentLinks?.(document, token); + }, async provideDiagnostics(document, token) { if (token.isCancellationRequested) return; @@ -113,22 +126,6 @@ export const create = (): LanguageServicePlugin => { }, }; }; -// TODO: Actions are done outside the servie layer. -// const HTMLService: LanguageServicePlugin = { -// commands: { -// "$/showHtmlOutput": async (uri: string) => { -// const doc = get(uri); -// if (!doc) return; - -// const { extracted } = extract(doc); - -// return { -// language: "html", -// content: extracted.toString(), -// }; -// }, -// }, -// }; let lock: Promise | undefined; async function acquireMutexLock() { diff --git a/packages/language-server/src/service/index.ts b/packages/language-server/src/service/index.ts index 181eec9a..17caa840 100644 --- a/packages/language-server/src/service/index.ts +++ b/packages/language-server/src/service/index.ts @@ -1,3 +1,4 @@ +import "../utils/project-defaults"; // import { create as createEmmetService } from "volar-service-emmet"; import { create as createCssService } from "volar-service-css"; import { create as createTypeScriptTwoSlashService } from "volar-service-typescript-twoslash-queries"; diff --git a/packages/language-server/src/service/marko/document-links.ts b/packages/language-server/src/service/marko/document-links.ts deleted file mode 100644 index 5703181a..00000000 --- a/packages/language-server/src/service/marko/document-links.ts +++ /dev/null @@ -1,97 +0,0 @@ -// import type { DocumentLink } from "vscode-languageserver"; -// import { type Node, NodeType } from "@marko/language-tools"; -// import { MarkoFile, processDoc } from "../../utils/file"; - -// import resolveUrl from "../../utils/resolve-url"; -// import type { Plugin } from "../types"; - -// import isDocumentLinkAttr from "./util/is-document-link-attr"; - -// const importTagReg = /(['"])<((?:[^'"\\>]+|\\.)*)>?\1/g; - -// export const findDocumentLinks: Plugin["findDocumentLinks"] = async (doc) => { -// return processDoc(doc, extractDocumentLinks); -// }; - -// /** -// * Iterate over the Marko CST and extract all the file links in the document. -// */ -// function extractDocumentLinks({ -// uri, -// scheme, -// parsed, -// code, -// lookup, -// }: MarkoFile): DocumentLink[] { -// if (scheme !== "file") { -// return []; -// } - -// const links: DocumentLink[] = []; -// const { program, read } = parsed; -// const visit = (node: Node.ChildNode) => { -// switch (node.type) { -// case NodeType.AttrTag: -// if (node.body) { -// for (const child of node.body) { -// visit(child); -// } -// } -// break; -// case NodeType.Tag: -// if (node.attrs && node.nameText) { -// for (const attr of node.attrs) { -// if (isDocumentLinkAttr(code, node, attr)) { -// const resolved = resolveUrl( -// read(attr.value.value).slice(1, -1), -// uri, -// ); -// if (resolved) { -// links.push({ -// range: parsed.locationAt(attr.value.value), -// target: resolveUrl(read(attr.value.value).slice(1, -1), uri), -// }); -// } -// } -// } -// } -// if (node.body) { -// for (const child of node.body) { -// visit(child); -// } -// } - -// break; -// } -// }; - -// for (const node of program.static) { -// // check for import statement (this currently only support the tag import shorthand). -// if (node.type === NodeType.Import) { -// importTagReg.lastIndex = 0; -// const value = parsed.read(node); -// const match = importTagReg.exec(value); -// if (match) { -// const [{ length }, , tagName] = match; -// const tagDef = lookup.getTag(tagName); -// const fileForTag = tagDef && (tagDef.template || tagDef.renderer); - -// if (fileForTag) { -// links.push({ -// range: parsed.locationAt({ -// start: node.start + match.index, -// end: node.start + match.index + length, -// }), -// target: fileForTag, -// }); -// } -// } -// } -// } - -// for (const node of program.body) { -// visit(node); -// } - -// return links; -// } diff --git a/packages/language-server/src/service/marko/index.ts b/packages/language-server/src/service/marko/index.ts index ce5b2f2f..ac5fcfbc 100644 --- a/packages/language-server/src/service/marko/index.ts +++ b/packages/language-server/src/service/marko/index.ts @@ -9,6 +9,7 @@ import { provideCompletions } from "./complete"; import { provideHover } from "./hover"; import { provideValidations } from "./validate"; import { provideDefinitions } from "./definition"; +// import { provideDocumentSymbols } from "./document-symbols"; export const create = ( _: typeof import("typescript"), @@ -19,7 +20,7 @@ export const create = ( hoverProvider: true, definitionProvider: true, diagnosticProvider: { - interFileDependencies: true, + interFileDependencies: false, workspaceDiagnostics: false, }, completionProvider: { @@ -48,6 +49,13 @@ export const create = ( }, create(context): LanguageServicePluginInstance { return { + // TODO: Is this necessary? + // provideDocumentSymbols(document, token) { + // if (token.isCancellationRequested) return; + // return worker(document, (virtualCode) => { + // return provideDocumentSymbols(virtualCode); + // }); + // }, provideDefinition(document, position, token) { if (token.isCancellationRequested) return; return worker(document, (virtualCode) => { @@ -57,8 +65,12 @@ export const create = ( }, provideDiagnostics(document, token) { if (token.isCancellationRequested) return; - return worker(document, (virtualCode) => { - return provideValidations(virtualCode); + return worker(document, async (virtualCode) => { + const validations = await provideValidations(virtualCode); + if (validations.length) { + console.log("validations", validations); + } + return validations; }); }, provideHover(document, position, token) { @@ -77,7 +89,10 @@ export const create = ( if (completions) { return { isIncomplete: false, - items: completions, + items: completions.map((it) => { + it.data.source = "marko"; + return it; + }), }; } diff --git a/packages/vscode/build.mts b/packages/vscode/build.mts index 64e29311..9dbf39c0 100644 --- a/packages/vscode/build.mts +++ b/packages/vscode/build.mts @@ -14,7 +14,7 @@ await Promise.all([ // since we're bundling typescript and it will look for them relative to the `__dirname` of the script. const tsLibDir = path.join( require.resolve("typescript/package.json"), - "../lib" + "../lib", ); const [dir] = await Promise.all([ fs.opendir(tsLibDir), @@ -24,18 +24,18 @@ await Promise.all([ if (entry.isFile() && /^lib\..*\.d\.ts$/.test(entry.name)) { await fs.copyFile( path.join(tsLibDir, entry.name), - path.join(distDir, entry.name) + path.join(distDir, entry.name), ); } } })(), fs.copyFile( path.join(thisDir, "../language-tools/marko.internal.d.ts"), - path.join(distDir, "marko.internal.d.ts") + path.join(distDir, "marko.internal.d.ts"), ), fs.copyFile( path.join(require.resolve("marko/package.json"), "../index.d.ts"), - path.join(distDir, "marko.runtime.d.ts") + path.join(distDir, "marko.runtime.d.ts"), ), build({ bundle: true, @@ -65,10 +65,10 @@ await Promise.all([ "@babel/preset-typescript", ], define: { - "import.meta.url": "_importMetaUrl" + "import.meta.url": "_importMetaUrl", }, banner: { - js: "const _importMetaUrl = require('url').pathToFileURL(__filename);" + js: "const _importMetaUrl = require('url').pathToFileURL(__filename);", }, plugins: [ { @@ -82,7 +82,17 @@ await Promise.all([ path: path.join( require.resolve(pkg), "..", - require(pkg).module as string + require(pkg).module as string, + ), + })); + + const htmlPkg = "vscode-html-languageservice/package.json"; + + build.onResolve({ filter: /^vscode-html-languageservice$/ }, () => ({ + path: path.join( + require.resolve(htmlPkg), + "..", + require(htmlPkg).module as string, ), })); }, @@ -105,10 +115,10 @@ await Promise.all([ loader: "js", contents: (await fs.readFile(args.path, "utf8")).replace( 'require.resolve ? require.resolve("./xhr-sync-worker.js") :', - "" + "", ), }; - } + }, ); }, }, From 7aad93df3b563f8e446e78a6a1778cd3221ca4ea Mon Sep 17 00:00:00 2001 From: AngusMorton Date: Mon, 21 Oct 2024 22:23:08 +1100 Subject: [PATCH 04/14] Fix accessibility diagnostics --- packages/language-server/src/service/index.ts | 6 +++-- .../axe-rules/axe-rules.ts | 0 .../axe-rules/generate-axe-rules.ts | 0 .../axe-rules/rule-exceptions.ts | 0 .../{html => marko-accessibility}/index.ts | 19 ++++----------- .../language-server/src/service/marko-html.ts | 24 +++++++++++++++++++ .../src/service/marko/validate.ts | 2 +- 7 files changed, 33 insertions(+), 18 deletions(-) rename packages/language-server/src/service/{html => marko-accessibility}/axe-rules/axe-rules.ts (100%) rename packages/language-server/src/service/{html => marko-accessibility}/axe-rules/generate-axe-rules.ts (100%) rename packages/language-server/src/service/{html => marko-accessibility}/axe-rules/rule-exceptions.ts (100%) rename packages/language-server/src/service/{html => marko-accessibility}/index.ts (86%) create mode 100644 packages/language-server/src/service/marko-html.ts diff --git a/packages/language-server/src/service/index.ts b/packages/language-server/src/service/index.ts index 17caa840..22511340 100644 --- a/packages/language-server/src/service/index.ts +++ b/packages/language-server/src/service/index.ts @@ -6,12 +6,13 @@ import { create as createTypeScriptServices } from "volar-service-typescript"; import type { Connection } from "@volar/language-server"; import { Project } from "@marko/language-tools"; import ts from "typescript"; -import { create as createHtmlService } from "./html"; +import { create as createAccessibilityService } from "./marko-accessibility"; import { create as createMarkoService } from "./marko"; import { getMarkoLanguagePlugin } from "./core/marko-plugin"; import { getMarkoPrettierService } from "./format"; import { create as createMarkoDebugService } from "./marko-debug"; import { create as createMarkoFormatActionService } from "./marko-action-format"; +import { create as createMarkoHtmlService } from "./marko-html"; const decoratedHosts = new WeakSet(); @@ -55,11 +56,12 @@ export function getLanguageServicePlugins( ) { const result = [ createMarkoService(ts), - createHtmlService(), + createMarkoHtmlService(), createCssService(), ...createTypeScriptServices(ts), createTypeScriptTwoSlashService(ts), getMarkoPrettierService(connection), + createAccessibilityService(), createMarkoDebugService(), createMarkoFormatActionService(), ]; diff --git a/packages/language-server/src/service/html/axe-rules/axe-rules.ts b/packages/language-server/src/service/marko-accessibility/axe-rules/axe-rules.ts similarity index 100% rename from packages/language-server/src/service/html/axe-rules/axe-rules.ts rename to packages/language-server/src/service/marko-accessibility/axe-rules/axe-rules.ts diff --git a/packages/language-server/src/service/html/axe-rules/generate-axe-rules.ts b/packages/language-server/src/service/marko-accessibility/axe-rules/generate-axe-rules.ts similarity index 100% rename from packages/language-server/src/service/html/axe-rules/generate-axe-rules.ts rename to packages/language-server/src/service/marko-accessibility/axe-rules/generate-axe-rules.ts diff --git a/packages/language-server/src/service/html/axe-rules/rule-exceptions.ts b/packages/language-server/src/service/marko-accessibility/axe-rules/rule-exceptions.ts similarity index 100% rename from packages/language-server/src/service/html/axe-rules/rule-exceptions.ts rename to packages/language-server/src/service/marko-accessibility/axe-rules/rule-exceptions.ts diff --git a/packages/language-server/src/service/html/index.ts b/packages/language-server/src/service/marko-accessibility/index.ts similarity index 86% rename from packages/language-server/src/service/html/index.ts rename to packages/language-server/src/service/marko-accessibility/index.ts index 90f2b4a6..d922ffa4 100644 --- a/packages/language-server/src/service/html/index.ts +++ b/packages/language-server/src/service/marko-accessibility/index.ts @@ -3,33 +3,21 @@ import axe from "axe-core"; import { JSDOM } from "jsdom"; import type { Diagnostic, LanguageServicePlugin } from "@volar/language-server"; import { URI } from "vscode-uri"; -import { create as createHtmlService } from "volar-service-html"; import { MarkoVirtualCode } from "../core/marko-plugin"; import { ruleExceptions } from "./axe-rules/rule-exceptions"; +// This plugin provides accessibility diagnostics for Marko templates. export const create = (): LanguageServicePlugin => { - const baseService = createHtmlService({ - configurationSections: { - autoCreateQuotes: "", - autoClosingTags: "", - }, - }); return { - name: "marko-template", + name: "marko-accessibility", capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false, }, - documentLinkProvider: baseService.capabilities.documentLinkProvider, }, create(context) { - const baseServiceInstance = baseService.create(context); return { - provideDocumentLinks(document, token) { - // Defer to the HTML service to provide links for us. - return baseServiceInstance.provideDocumentLinks?.(document, token); - }, async provideDiagnostics(document, token) { if (token.isCancellationRequested) return; @@ -39,7 +27,8 @@ export const create = (): LanguageServicePlugin => { return []; } - const jsdom = new JSDOM(htmlAst.toString(), { + const htmlText = htmlAst.extracted.toString(); + const jsdom = new JSDOM(htmlText, { includeNodeLocations: true, }); const { documentElement } = jsdom.window.document; diff --git a/packages/language-server/src/service/marko-html.ts b/packages/language-server/src/service/marko-html.ts new file mode 100644 index 00000000..f81cc22f --- /dev/null +++ b/packages/language-server/src/service/marko-html.ts @@ -0,0 +1,24 @@ +import type { LanguageServicePlugin } from "@volar/language-server"; +import { create as createHtmlService } from "volar-service-html"; + +export const create = (): LanguageServicePlugin => { + // We currently opt-in to specific features of the HTML service because not all + // of them are useful given the virtual HTML template we generate. + const baseService = createHtmlService({}); + return { + name: "marko-html", + capabilities: { + documentLinkProvider: baseService.capabilities.documentLinkProvider, + }, + create(context) { + const baseServiceInstance = baseService.create(context); + return { + provideDocumentLinks(document, token) { + // Defer to the HTML service to provide links for us. + // This is used for things like src attributes that link to local files. + return baseServiceInstance.provideDocumentLinks?.(document, token); + }, + }; + }, + }; +}; diff --git a/packages/language-server/src/service/marko/validate.ts b/packages/language-server/src/service/marko/validate.ts index 0304fe91..dee696aa 100644 --- a/packages/language-server/src/service/marko/validate.ts +++ b/packages/language-server/src/service/marko/validate.ts @@ -1,4 +1,4 @@ -import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver"; +import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver-protocol"; import { DiagnosticType } from "@marko/babel-utils"; import { Config } from "@marko/compiler"; import { MarkoVirtualCode } from "../core/marko-plugin"; From cb5a041b9780255d8e4fa643ff8a24c7d01932a6 Mon Sep 17 00:00:00 2001 From: AngusMorton Date: Mon, 21 Oct 2024 22:31:57 +1100 Subject: [PATCH 05/14] Fix marko completions --- packages/language-server/src/service/marko/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-server/src/service/marko/index.ts b/packages/language-server/src/service/marko/index.ts index ac5fcfbc..ea92611a 100644 --- a/packages/language-server/src/service/marko/index.ts +++ b/packages/language-server/src/service/marko/index.ts @@ -90,7 +90,7 @@ export const create = ( return { isIncomplete: false, items: completions.map((it) => { - it.data.source = "marko"; + it.data = { source: "marko" }; return it; }), }; From 5ddbc2c3932879b0aab3df2dfc30daf88111d08e Mon Sep 17 00:00:00 2001 From: AngusMorton Date: Tue, 22 Oct 2024 11:35:44 +1100 Subject: [PATCH 06/14] Refactor service plugins --- .../service/{marko-html.ts => html/index.ts} | 0 packages/language-server/src/service/index.ts | 7 +- .../index.ts} | 4 +- .../{marko-debug.ts => marko-debug/index.ts} | 2 +- .../src/service/marko/complete/AttrValue.ts | 73 ------------------- .../src/service/marko/complete/index.ts | 2 - .../src/service/{format => prettier}/index.ts | 2 +- .../service/{format => prettier}/package.ts | 0 8 files changed, 7 insertions(+), 83 deletions(-) rename packages/language-server/src/service/{marko-html.ts => html/index.ts} (100%) rename packages/language-server/src/service/{marko-action-format.ts => marko-action-format/index.ts} (93%) rename packages/language-server/src/service/{marko-debug.ts => marko-debug/index.ts} (96%) delete mode 100644 packages/language-server/src/service/marko/complete/AttrValue.ts rename packages/language-server/src/service/{format => prettier}/index.ts (99%) rename packages/language-server/src/service/{format => prettier}/package.ts (100%) diff --git a/packages/language-server/src/service/marko-html.ts b/packages/language-server/src/service/html/index.ts similarity index 100% rename from packages/language-server/src/service/marko-html.ts rename to packages/language-server/src/service/html/index.ts diff --git a/packages/language-server/src/service/index.ts b/packages/language-server/src/service/index.ts index 22511340..136df735 100644 --- a/packages/language-server/src/service/index.ts +++ b/packages/language-server/src/service/index.ts @@ -1,5 +1,4 @@ import "../utils/project-defaults"; -// import { create as createEmmetService } from "volar-service-emmet"; import { create as createCssService } from "volar-service-css"; import { create as createTypeScriptTwoSlashService } from "volar-service-typescript-twoslash-queries"; import { create as createTypeScriptServices } from "volar-service-typescript"; @@ -9,10 +8,10 @@ import ts from "typescript"; import { create as createAccessibilityService } from "./marko-accessibility"; import { create as createMarkoService } from "./marko"; import { getMarkoLanguagePlugin } from "./core/marko-plugin"; -import { getMarkoPrettierService } from "./format"; +import { createMarkoPrettierService } from "./prettier"; import { create as createMarkoDebugService } from "./marko-debug"; import { create as createMarkoFormatActionService } from "./marko-action-format"; -import { create as createMarkoHtmlService } from "./marko-html"; +import { create as createMarkoHtmlService } from "./html"; const decoratedHosts = new WeakSet(); @@ -60,7 +59,7 @@ export function getLanguageServicePlugins( createCssService(), ...createTypeScriptServices(ts), createTypeScriptTwoSlashService(ts), - getMarkoPrettierService(connection), + createMarkoPrettierService(connection), createAccessibilityService(), createMarkoDebugService(), createMarkoFormatActionService(), diff --git a/packages/language-server/src/service/marko-action-format.ts b/packages/language-server/src/service/marko-action-format/index.ts similarity index 93% rename from packages/language-server/src/service/marko-action-format.ts rename to packages/language-server/src/service/marko-action-format/index.ts index 89a9a864..f16d1afa 100644 --- a/packages/language-server/src/service/marko-action-format.ts +++ b/packages/language-server/src/service/marko-action-format/index.ts @@ -1,7 +1,7 @@ import { LanguageServicePlugin } from "@volar/language-service"; import { URI } from "vscode-uri"; -import { MarkoVirtualCode } from "./core/marko-plugin"; -import { getFormattingOptions, getPrettierInstance } from "./format"; +import { MarkoVirtualCode } from "../core/marko-plugin"; +import { getFormattingOptions, getPrettierInstance } from "../prettier"; export const create = (): LanguageServicePlugin => { return { diff --git a/packages/language-server/src/service/marko-debug.ts b/packages/language-server/src/service/marko-debug/index.ts similarity index 96% rename from packages/language-server/src/service/marko-debug.ts rename to packages/language-server/src/service/marko-debug/index.ts index e0350cac..09d60a96 100644 --- a/packages/language-server/src/service/marko-debug.ts +++ b/packages/language-server/src/service/marko-debug/index.ts @@ -1,6 +1,6 @@ import { LanguageServicePlugin } from "@volar/language-service"; import { URI } from "vscode-uri"; -import { MarkoVirtualCode } from "./core/marko-plugin"; +import { MarkoVirtualCode } from "../core/marko-plugin"; export const create = (): LanguageServicePlugin => { return { diff --git a/packages/language-server/src/service/marko/complete/AttrValue.ts b/packages/language-server/src/service/marko/complete/AttrValue.ts deleted file mode 100644 index 4ae0c36f..00000000 --- a/packages/language-server/src/service/marko/complete/AttrValue.ts +++ /dev/null @@ -1,73 +0,0 @@ -// import path from "path"; - -// import { -// CompletionItem, -// CompletionItemKind, -// TextEdit, -// } from "vscode-languageserver"; - -// import type { Node } from "@marko/language-tools"; -// import isDocumentLinkAttr from "../util/is-document-link-attr"; -// import fileSystem, { FileType } from "../../../utils/file-system"; -// import resolveUrl from "../../../utils/resolve-url"; - -// import type { CompletionMeta } from "."; -// TODO: This completion provider is not migrated because its not obvious how. - -// export async function AttrValue({ -// offset, -// node, -// file: { uri, parsed, code }, -// }: CompletionMeta): Promise { -// const attr = node.parent; -// if (isDocumentLinkAttr(code, attr.parent, attr)) { -// const start = node.value.start + 1; -// if (code[start] !== ".") return; // only resolve relative paths - -// const end = node.value.end - 1; -// const relativeOffset = offset - start; -// const rawValue = parsed.read({ -// start, -// end, -// }); - -// const segmentStart = rawValue.lastIndexOf("/", relativeOffset); -// if (segmentStart === -1) return; // only resolve after a slash. - -// const req = rawValue.slice(0, segmentStart); -// const resolved = resolveUrl(req, uri); - -// if (resolved) { -// const result: CompletionItem[] = []; -// const curFile = req === "." ? path.basename(uri) : undefined; -// const replaceRange = parsed.locationAt({ -// start: start + segmentStart + 1, -// end: start + rawValue.length, -// }); - -// for (const [entry, type] of await fileSystem.readDirectory(resolved)) { -// if (entry[0] !== "." && entry !== curFile) { -// result.push( -// type === FileType.Directory -// ? { -// label: `${entry}/`, -// kind: CompletionItemKind.Folder, -// textEdit: TextEdit.replace(replaceRange, `${entry}/`), -// command: { -// title: "Suggest", -// command: "editor.action.triggerSuggest", -// }, -// } -// : { -// label: entry, -// kind: CompletionItemKind.File, -// textEdit: TextEdit.replace(replaceRange, entry), -// }, -// ); -// } -// } - -// return result; -// } -// } -// } diff --git a/packages/language-server/src/service/marko/complete/index.ts b/packages/language-server/src/service/marko/complete/index.ts index 967243ea..f77b2033 100644 --- a/packages/language-server/src/service/marko/complete/index.ts +++ b/packages/language-server/src/service/marko/complete/index.ts @@ -15,8 +15,6 @@ export function provideCompletions( switch (node?.type) { case NodeType.AttrName: return AttrName(node, doc, offset); - // case NodeType.AttrValue: - // return AttrValue(node, doc, offset); case NodeType.Import: return Import(node, doc); case NodeType.Tag: diff --git a/packages/language-server/src/service/format/index.ts b/packages/language-server/src/service/prettier/index.ts similarity index 99% rename from packages/language-server/src/service/format/index.ts rename to packages/language-server/src/service/prettier/index.ts index 17dbb0e3..a3954fe2 100644 --- a/packages/language-server/src/service/format/index.ts +++ b/packages/language-server/src/service/prettier/index.ts @@ -14,7 +14,7 @@ import { Options } from "prettier"; import { dynamicRequire } from "../../utils/importPackage"; import { getMarkoPrettierPluginPath, importPrettier } from "./package"; -export function getMarkoPrettierService( +export function createMarkoPrettierService( connection: Connection, ): LanguageServicePlugin { let prettier: typeof import("prettier") | undefined; diff --git a/packages/language-server/src/service/format/package.ts b/packages/language-server/src/service/prettier/package.ts similarity index 100% rename from packages/language-server/src/service/format/package.ts rename to packages/language-server/src/service/prettier/package.ts From 109a995def01f18fc22d6e23fcd36a87eb2e4337 Mon Sep 17 00:00:00 2001 From: AngusMorton Date: Tue, 22 Oct 2024 12:07:44 +1100 Subject: [PATCH 07/14] Remove unused files --- packages/language-server/src/index.ts | 13 -- .../src/service/marko-accessibility/index.ts | 2 +- .../src/service/prettier/package.ts | 24 +-- .../language-server/src/utils/file-system.ts | 52 ------ packages/language-server/src/utils/file.ts | 91 ---------- .../language-server/src/utils/messages.ts | 42 ----- .../language-server/src/utils/resolve-url.ts | 8 - .../src/utils/text-documents.ts | 171 ------------------ .../language-server/src/utils/workspace.ts | 43 ----- 9 files changed, 8 insertions(+), 438 deletions(-) delete mode 100644 packages/language-server/src/utils/file-system.ts delete mode 100644 packages/language-server/src/utils/file.ts delete mode 100644 packages/language-server/src/utils/messages.ts delete mode 100644 packages/language-server/src/utils/resolve-url.ts delete mode 100644 packages/language-server/src/utils/text-documents.ts delete mode 100644 packages/language-server/src/utils/workspace.ts diff --git a/packages/language-server/src/index.ts b/packages/language-server/src/index.ts index e1ecd0a6..53c0ebdc 100644 --- a/packages/language-server/src/index.ts +++ b/packages/language-server/src/index.ts @@ -10,8 +10,6 @@ import { getLanguagePlugins, getLanguageServicePlugins, } from "./service"; -// import { getMarkoInstall } from "./util/getMarkoInstall.js"; -// import { addMarkoTypes } from "./core/index.js"; const connection = createConnection(); const server = createServer(connection); @@ -49,17 +47,6 @@ connection.onInitialize((params) => { "package.json", ); - // const markoInstall = getMarkoInstall([rootPath], { - // nearestPackageJson: nearestPackageJson, - // }); - - // if (!markoInstall) { - // connection.sendNotification(ShowMessageNotification.type, { - // message: `Couldn't find Marko in workspace "${rootPath}".`, - // type: MessageType.Warning, - // }); - // } - if (nearestPackageJson) { addMarkoTypes( dirname(nearestPackageJson), diff --git a/packages/language-server/src/service/marko-accessibility/index.ts b/packages/language-server/src/service/marko-accessibility/index.ts index d922ffa4..47691fc3 100644 --- a/packages/language-server/src/service/marko-accessibility/index.ts +++ b/packages/language-server/src/service/marko-accessibility/index.ts @@ -21,7 +21,7 @@ export const create = (): LanguageServicePlugin => { async provideDiagnostics(document, token) { if (token.isCancellationRequested) return; - return worker(document, async (virtualCode) => { + return await worker(document, async (virtualCode) => { const htmlAst = virtualCode.htmlAst; if (!htmlAst) { return []; diff --git a/packages/language-server/src/service/prettier/package.ts b/packages/language-server/src/service/prettier/package.ts index 6e6547d9..02eb1bc4 100644 --- a/packages/language-server/src/service/prettier/package.ts +++ b/packages/language-server/src/service/prettier/package.ts @@ -1,29 +1,19 @@ -import type * as prettier from "prettier"; +import * as prettier from "prettier"; import { getPackagePath } from "../../utils/importPackage"; -export function importPrettier(fromPath: string): typeof prettier | undefined { +export function importPrettier(fromPath: string): typeof prettier { const prettierPkg = getPackagePath("prettier", [fromPath, __dirname]); - if (!prettierPkg) { - return undefined; + if (prettierPkg) { + return require(prettierPkg); } - return require(prettierPkg); + // Return the built-in prettier instance if the user doesn't have it installed. + return prettier; } export function getMarkoPrettierPluginPath( fromPath: string, ): string | undefined { - const corePluginPath = getPackagePath( - "prettier-plugin-marko", - [fromPath, __dirname], - false, - ); - - // Prefer the official plugin if it's installed. - if (corePluginPath) { - return corePluginPath; - } - - return undefined; + return getPackagePath("prettier-plugin-marko", [fromPath, __dirname], false); } diff --git a/packages/language-server/src/utils/file-system.ts b/packages/language-server/src/utils/file-system.ts deleted file mode 100644 index 9fefd877..00000000 --- a/packages/language-server/src/utils/file-system.ts +++ /dev/null @@ -1,52 +0,0 @@ -import fs from "fs/promises"; -import { fileURLToPath } from "url"; - -import { type FileStat, FileType } from "vscode-css-languageservice"; - -export { FileStat, FileType }; -export default { - stat, - readDirectory, -}; - -async function stat(uri: string): Promise { - let type = FileType.Unknown; - let ctime = -1; - let mtime = -1; - let size = -1; - - try { - const stat = await fs.stat(fileURLToPath(uri)); - if (stat.isDirectory()) type = FileType.Directory; - else if (stat.isFile()) type = FileType.File; - ctime = stat.ctimeMs; - mtime = stat.mtimeMs; - size = stat.size; - } catch { - // ignore - } - - return { - type, - ctime, - mtime, - size, - }; -} - -async function readDirectory(uri: string): Promise<[string, FileType][]> { - const result: [string, FileType][] = []; - - try { - for await (const entry of await fs.opendir(fileURLToPath(uri))) { - if (entry.isFile()) { - result.push([entry.name, FileType.File]); - } else if (entry.isDirectory()) { - result.push([entry.name, FileType.Directory]); - } - } - } catch { - // ignore - } - return result; -} diff --git a/packages/language-server/src/utils/file.ts b/packages/language-server/src/utils/file.ts deleted file mode 100644 index 428fca07..00000000 --- a/packages/language-server/src/utils/file.ts +++ /dev/null @@ -1,91 +0,0 @@ -import path from "path"; - -import type { TextDocument } from "vscode-languageserver-textdocument"; -import type { TaglibLookup } from "@marko/babel-utils"; -import { type Parsed, Project, parse } from "@marko/language-tools"; -import { URI } from "vscode-uri"; - -const processorCaches = new WeakMap>(); - -export interface MarkoFile { - uri: string; - scheme: string; - version: number; - lookup: TaglibLookup; - filename: string | undefined; - dirname: string | undefined; - parsed: Parsed; - code: string; -} - -export function getFSDir(doc: TextDocument): string | undefined { - const filename = getFSPath(doc); - return filename && path.dirname(filename); -} - -export function getFSPath(doc: TextDocument): string | undefined { - const parsed = URI.parse(doc.uri); - return parsed.scheme === "file" ? parsed.fsPath : undefined; -} - -export function getMarkoFile(doc: TextDocument): MarkoFile { - const { uri } = doc; - const { fsPath, scheme } = URI.parse(uri); - const filename = scheme === "file" ? fsPath : undefined; - const dirname = filename ? path.dirname(filename) : process.cwd(); - const cache = Project.getCache(dirname) as Map; - let file = cache.get(doc); - if (!file) { - const { version } = doc; - const code = doc.getText(); - const parsed = parse(code, filename); - const lookup = Project.getTagLookup(dirname); - cache.set( - doc, - (file = { - uri, - scheme, - version, - lookup, - filename, - dirname, - parsed, - code, - }), - ); - } - - return file; -} - -export function clearMarkoCacheForFile(doc: TextDocument) { - (Project.getCache(getFSDir(doc)) as Map).delete(doc); -} - -/** - * Run some processing against a parsed document and cache the result. - * Anytime the document changes, the cache is cleared. - */ -export function processDoc( - doc: TextDocument, - process: (file: MarkoFile) => T, -): T { - const file = getMarkoFile(doc); - const cache = processorCaches.get(file.parsed) as - | Map - | undefined; - let result: T | undefined; - - if (cache) { - result = cache.get(process); - if (!result) { - result = process(file); - cache.set(process, result); - } - } else { - result = process(file); - processorCaches.set(file.parsed, new Map([[process, result]])); - } - - return result; -} diff --git a/packages/language-server/src/utils/messages.ts b/packages/language-server/src/utils/messages.ts deleted file mode 100644 index 8af345d9..00000000 --- a/packages/language-server/src/utils/messages.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { inspect } from "util"; - -import type { Connection } from "vscode-languageserver"; - -let connection!: Connection; -const previousMessagesByType = new Map(); -export default function setup(_: Connection) { - connection = _; -} - -export function displayInformation(data: unknown) { - display("showInformation", data); -} - -export function displayWarning(data: unknown) { - display("showWarning", data); -} - -export function displayError(data: unknown) { - display("showError", data); -} - -function display(type: string, data: unknown) { - const msg = - typeof data === "string" ? data : inspect(data, { colors: false }); - - const previousMessages = previousMessagesByType.get(type); - if (previousMessages) { - if (previousMessages.includes(msg)) return; - - previousMessages.push(msg); - - // Only keep the last 3 messages. - if (previousMessages.length > 3) { - previousMessages.unshift(); - } - } else { - previousMessagesByType.set(type, [msg]); - } - - setImmediate(() => connection.sendNotification(type, msg)); -} diff --git a/packages/language-server/src/utils/resolve-url.ts b/packages/language-server/src/utils/resolve-url.ts deleted file mode 100644 index 82fd79dd..00000000 --- a/packages/language-server/src/utils/resolve-url.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default function resolveUrl(to: string, base: string) { - try { - const url = new URL(to, base); - if (url.protocol === "file:") return url.toString(); - } catch { - return undefined; - } -} diff --git a/packages/language-server/src/utils/text-documents.ts b/packages/language-server/src/utils/text-documents.ts deleted file mode 100644 index 82e848a0..00000000 --- a/packages/language-server/src/utils/text-documents.ts +++ /dev/null @@ -1,171 +0,0 @@ -import fs from "fs"; -import { URI } from "vscode-uri"; -import { - DidChangeTextDocumentParams, - DidChangeWatchedFilesParams, - DidCloseTextDocumentParams, - DidOpenTextDocumentParams, - FileChangeType, -} from "vscode-languageserver"; -import { TextDocument } from "vscode-languageserver-textdocument"; - -export type FileChangeHandler = (document: TextDocument | undefined) => void; - -const docs = new Map(); -const openDocs = new Set(); -const fileExists = new Map(); -const fileChangeHandlers: Set = new Set(); - -export let projectVersion = 0; - -export function onFileChange(handler: FileChangeHandler) { - fileChangeHandlers.add(handler); -} - -export function getAllOpen() { - return openDocs; -} - -export function get(uri: string) { - const doc = docs.get(uri); - if (doc) return doc; - - const { fsPath, scheme } = URI.parse(uri); - if (scheme === "file") { - if (fileExists.get(uri) === false) return undefined; - try { - const newDoc = TextDocument.create( - uri, - getLanguageId(uri), - fs.statSync(fsPath).mtimeMs, - fs.readFileSync(fsPath, "utf-8"), - ); - - docs.set(uri, newDoc); - fileExists.set(uri, true); - return newDoc; - } catch { - fileExists.set(uri, false); - } - } -} - -export function exists(uri: string) { - const cached = fileExists.get(uri); - if (cached !== undefined) return cached; - - const { fsPath, scheme } = URI.parse(uri); - if (scheme === "file") { - try { - fs.accessSync(fsPath); - fileExists.set(uri, true); - return true; - } catch { - fileExists.set(uri, false); - return false; - } - } - - return false; -} - -export function isOpen(doc: TextDocument) { - return openDocs.has(doc); -} - -export function doOpen(params: DidOpenTextDocumentParams) { - const ref = params.textDocument; - const existingDoc = docs.get(ref.uri); - projectVersion++; - - if (existingDoc) { - if (existingDoc.version === ref.version) { - openDocs.add(existingDoc); - return; - } - - openDocs.delete(existingDoc); - docs.delete(ref.uri); - } - - const newDoc = TextDocument.create( - ref.uri, - ref.languageId, - ref.version, - ref.text, - ); - - openDocs.add(newDoc); - fileExists.set(ref.uri, true); - docs.set(ref.uri, newDoc); -} - -export function doChange(params: DidChangeTextDocumentParams) { - const ref = params.textDocument; - const changes = params.contentChanges; - const doc = docs.get(ref.uri); - if (changes.length > 0 && ref.version != null && doc) { - TextDocument.update(doc, changes, ref.version); - emitFileChange(doc); - } -} - -export function doClose(params: DidCloseTextDocumentParams) { - const ref = params.textDocument; - const doc = docs.get(ref.uri); - if (doc) { - projectVersion++; - openDocs.delete(doc); - - if (URI.parse(ref.uri).scheme !== "file") { - docs.delete(ref.uri); - } - } -} - -export function doChangeWatchedFiles(params: DidChangeWatchedFilesParams) { - for (const change of params.changes) { - switch (change.type) { - case FileChangeType.Created: - fileExists.set(change.uri, true); - break; - case FileChangeType.Deleted: - case FileChangeType.Changed: { - fileExists.set(change.uri, change.type === FileChangeType.Changed); - - // When a file that's in our cache is changed or deleted and not in an open editor - // we clear the file from the cache since it will be reloaded when read. - const doc = docs.get(change.uri); - if (doc && !openDocs.has(doc)) { - docs.delete(change.uri); - } - } - } - } - - emitFileChange(undefined); -} - -function getLanguageId(uri: string) { - const ext = uri.slice(uri.lastIndexOf(".") + 1); - switch (ext) { - case "cjs": - case "mjs": - case "js": - return "javascript"; - case "cts": - case "mts": - case "ts": - return "typescript"; - default: - return ext; - } -} - -function emitFileChange(doc: TextDocument | undefined) { - projectVersion++; - - for (const handler of fileChangeHandlers) { - handler(doc); - } -} diff --git a/packages/language-server/src/utils/workspace.ts b/packages/language-server/src/utils/workspace.ts deleted file mode 100644 index 240de662..00000000 --- a/packages/language-server/src/utils/workspace.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { Connection } from "vscode-languageserver"; - -let isInitialized = false; -let connection!: Connection; -const configChangeHandlers: Set = new Set(); -const settingsCache = new Map(); - -export async function getConfig(section: string): Promise { - let cached = settingsCache.get(section); - if (!cached) { - try { - cached = (await connection.workspace.getConfiguration(section)) || {}; - settingsCache.set(section, cached); - } catch { - // ignore - } - } - - return cached; -} - -export type ConfigChangeHandler = () => void; -export function onConfigChange(handler: ConfigChangeHandler) { - configChangeHandlers.add(handler); -} - -export function setup(_: Connection) { - connection = _; - connection.onDidChangeConfiguration(() => { - if (isInitialized) { - settingsCache.clear(); - emitConfigChange(); - } else { - isInitialized = true; - } - }); -} - -function emitConfigChange() { - for (const handler of configChangeHandlers) { - handler(); - } -} From d60147b918569aae5ee169ec9ef8a06366569014 Mon Sep 17 00:00:00 2001 From: AngusMorton Date: Tue, 22 Oct 2024 12:17:50 +1100 Subject: [PATCH 08/14] Rm commit --- packages/language-server/src/service/marko/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/language-server/src/service/marko/index.ts b/packages/language-server/src/service/marko/index.ts index ea92611a..cec8908d 100644 --- a/packages/language-server/src/service/marko/index.ts +++ b/packages/language-server/src/service/marko/index.ts @@ -66,11 +66,7 @@ export const create = ( provideDiagnostics(document, token) { if (token.isCancellationRequested) return; return worker(document, async (virtualCode) => { - const validations = await provideValidations(virtualCode); - if (validations.length) { - console.log("validations", validations); - } - return validations; + return await provideValidations(virtualCode); }); }, provideHover(document, position, token) { From 875e4643de1448933904c0dc8325be40d13a68eb Mon Sep 17 00:00:00 2001 From: AngusMorton Date: Tue, 22 Oct 2024 15:58:13 +1100 Subject: [PATCH 09/14] Reorganize --- .../form-field-multiple-labels/index.marko | 17 +++++--- .../index.marko | 7 ++-- .../src/__tests__/index.test.ts | 2 +- packages/language-server/src/index.ts | 9 ++--- .../marko-plugin.ts => language/index.ts} | 39 ++++++++++++++++-- .../{service/core => language}/parseHtml.ts | 0 .../{service/core => language}/parseScript.ts | 0 .../{service/core => language}/parseStyles.ts | 0 .../src/{service => plugins}/html/index.ts | 0 .../src/{service => plugins}/index.ts | 40 ------------------- .../axe-rules/axe-rules.ts | 0 .../axe-rules/generate-axe-rules.ts | 0 .../axe-rules/rule-exceptions.ts | 0 .../marko-accessibility/index.ts | 2 +- .../marko-action-format/index.ts | 2 +- .../{service => plugins}/marko-debug/index.ts | 2 +- .../marko/complete/AttrName.ts | 2 +- .../marko/complete/Import.ts | 2 +- .../marko/complete/OpenTagName.ts | 2 +- .../marko/complete/Tag.ts | 2 +- .../marko/complete/index.ts | 2 +- .../marko/definition/AttrName.ts | 2 +- .../marko/definition/OpenTagName.ts | 2 +- .../marko/definition/index.ts | 2 +- .../marko/document-symbols.ts | 0 .../marko/hover/OpenTagName.ts | 2 +- .../{service => plugins}/marko/hover/index.ts | 2 +- .../src/{service => plugins}/marko/index.ts | 10 ++--- .../marko/util/get-tag-name-completion.ts | 0 .../marko/util/is-document-link-attr.ts | 0 .../{service => plugins}/marko/validate.ts | 4 +- .../{service => plugins}/prettier/index.ts | 0 .../{service => plugins}/prettier/package.ts | 0 33 files changed, 75 insertions(+), 79 deletions(-) rename packages/language-server/src/{service/core/marko-plugin.ts => language/index.ts} (72%) rename packages/language-server/src/{service/core => language}/parseHtml.ts (100%) rename packages/language-server/src/{service/core => language}/parseScript.ts (100%) rename packages/language-server/src/{service/core => language}/parseStyles.ts (100%) rename packages/language-server/src/{service => plugins}/html/index.ts (100%) rename packages/language-server/src/{service => plugins}/index.ts (50%) rename packages/language-server/src/{service => plugins}/marko-accessibility/axe-rules/axe-rules.ts (100%) rename packages/language-server/src/{service => plugins}/marko-accessibility/axe-rules/generate-axe-rules.ts (100%) rename packages/language-server/src/{service => plugins}/marko-accessibility/axe-rules/rule-exceptions.ts (100%) rename packages/language-server/src/{service => plugins}/marko-accessibility/index.ts (98%) rename packages/language-server/src/{service => plugins}/marko-action-format/index.ts (97%) rename packages/language-server/src/{service => plugins}/marko-debug/index.ts (96%) rename packages/language-server/src/{service => plugins}/marko/complete/AttrName.ts (98%) rename packages/language-server/src/{service => plugins}/marko/complete/Import.ts (96%) rename packages/language-server/src/{service => plugins}/marko/complete/OpenTagName.ts (96%) rename packages/language-server/src/{service => plugins}/marko/complete/Tag.ts (96%) rename packages/language-server/src/{service => plugins}/marko/complete/index.ts (92%) rename packages/language-server/src/{service => plugins}/marko/definition/AttrName.ts (95%) rename packages/language-server/src/{service => plugins}/marko/definition/OpenTagName.ts (96%) rename packages/language-server/src/{service => plugins}/marko/definition/index.ts (89%) rename packages/language-server/src/{service => plugins}/marko/document-symbols.ts (100%) rename packages/language-server/src/{service => plugins}/marko/hover/OpenTagName.ts (92%) rename packages/language-server/src/{service => plugins}/marko/hover/index.ts (87%) rename packages/language-server/src/{service => plugins}/marko/index.ts (94%) rename packages/language-server/src/{service => plugins}/marko/util/get-tag-name-completion.ts (100%) rename packages/language-server/src/{service => plugins}/marko/util/is-document-link-attr.ts (100%) rename packages/language-server/src/{service => plugins}/marko/validate.ts (97%) rename packages/language-server/src/{service => plugins}/prettier/index.ts (100%) rename packages/language-server/src/{service => plugins}/prettier/package.ts (100%) diff --git a/packages/language-server/src/__tests__/fixtures/html/form-field-multiple-labels/index.marko b/packages/language-server/src/__tests__/fixtures/html/form-field-multiple-labels/index.marko index 9deaff2e..2c9c7d56 100644 --- a/packages/language-server/src/__tests__/fixtures/html/form-field-multiple-labels/index.marko +++ b/packages/language-server/src/__tests__/fixtures/html/form-field-multiple-labels/index.marko @@ -1,8 +1,15 @@ - - + + - - diff --git a/packages/language-server/src/__tests__/fixtures/script/bound-attr-const-dynamic-member/index.marko b/packages/language-server/src/__tests__/fixtures/script/bound-attr-const-dynamic-member/index.marko index 17015993..e0809d5a 100644 --- a/packages/language-server/src/__tests__/fixtures/script/bound-attr-const-dynamic-member/index.marko +++ b/packages/language-server/src/__tests__/fixtures/script/bound-attr-const-dynamic-member/index.marko @@ -1,4 +1,5 @@ - + + + +// ^? ^? - -// ^? ^? \ No newline at end of file diff --git a/packages/language-server/src/__tests__/index.test.ts b/packages/language-server/src/__tests__/index.test.ts index 03e12505..b8ad8ea3 100644 --- a/packages/language-server/src/__tests__/index.test.ts +++ b/packages/language-server/src/__tests__/index.test.ts @@ -7,7 +7,7 @@ import { URI } from "vscode-uri"; import { TextDocument } from "vscode-languageserver-textdocument"; import { CancellationToken, Position } from "vscode-languageserver"; import { Project } from "@marko/language-tools"; -import MarkoLangaugeService, { documents } from "../service"; +import MarkoLangaugeService, { documents } from "../plugins"; import { codeFrame } from "./util/code-frame"; Project.setDefaultTypePaths({ diff --git a/packages/language-server/src/index.ts b/packages/language-server/src/index.ts index 53c0ebdc..109651b9 100644 --- a/packages/language-server/src/index.ts +++ b/packages/language-server/src/index.ts @@ -5,11 +5,8 @@ import { createTypeScriptProject, loadTsdkByPath, } from "@volar/language-server/node"; -import { - addMarkoTypes, - getLanguagePlugins, - getLanguageServicePlugins, -} from "./service"; +import { getLanguageServicePlugins } from "./plugins"; +import { addMarkoTypes, createMarkoLanguagePlugin } from "./language"; const connection = createConnection(); const server = createServer(connection); @@ -34,7 +31,7 @@ connection.onInitialize((params) => { params, createTypeScriptProject(typescript, diagnosticMessages, ({ env }) => { return { - languagePlugins: getLanguagePlugins(typescript), + languagePlugins: [createMarkoLanguagePlugin(typescript)], setup({ project }) { const { languageServiceHost, configFileName } = project.typescript!; diff --git a/packages/language-server/src/service/core/marko-plugin.ts b/packages/language-server/src/language/index.ts similarity index 72% rename from packages/language-server/src/service/core/marko-plugin.ts rename to packages/language-server/src/language/index.ts index dec0935b..28af4f93 100644 --- a/packages/language-server/src/service/core/marko-plugin.ts +++ b/packages/language-server/src/language/index.ts @@ -12,8 +12,41 @@ import { TaglibLookup } from "@marko/babel-utils"; import { parseScripts } from "./parseScript"; import { parseStyles } from "./parseStyles"; import { parseHtml } from "./parseHtml"; +import "../utils/project-defaults"; -export function getMarkoLanguagePlugin( +const decoratedHosts = new WeakSet(); + +export function addMarkoTypes( + rootDir: string, + ts: typeof import("typescript"), + host: ts.LanguageServiceHost, +) { + if (decoratedHosts.has(host)) { + return; + } + decoratedHosts.add(host); + + const getScriptFileNames = host.getScriptFileNames.bind(host); + + host.getScriptFileNames = () => { + const addedFileNames = []; + + const typeLibs = Project.getTypeLibs(rootDir, ts, host); + + addedFileNames.push(typeLibs.internalTypesFile); + if (typeLibs.markoRunTypesFile) { + addedFileNames.push(typeLibs.markoRunTypesFile); + } + if (typeLibs.markoRunGeneratedTypesFile) { + addedFileNames.push(typeLibs.markoRunGeneratedTypesFile); + } + addedFileNames.push(typeLibs.markoTypesFile); + + return [...getScriptFileNames(), ...addedFileNames]; + }; +} + +export function createMarkoLanguagePlugin( ts: typeof import("typescript"), ): LanguagePlugin { return { @@ -54,8 +87,8 @@ export class MarkoVirtualCode implements VirtualCode { embeddedCodes!: VirtualCode[]; markoAst: ReturnType; tagLookup: TaglibLookup; - htmlAst?: ReturnType; - compiler?: typeof import("@marko/compiler"); + htmlAst: ReturnType; + compiler: typeof import("@marko/compiler"); code: string; constructor( diff --git a/packages/language-server/src/service/core/parseHtml.ts b/packages/language-server/src/language/parseHtml.ts similarity index 100% rename from packages/language-server/src/service/core/parseHtml.ts rename to packages/language-server/src/language/parseHtml.ts diff --git a/packages/language-server/src/service/core/parseScript.ts b/packages/language-server/src/language/parseScript.ts similarity index 100% rename from packages/language-server/src/service/core/parseScript.ts rename to packages/language-server/src/language/parseScript.ts diff --git a/packages/language-server/src/service/core/parseStyles.ts b/packages/language-server/src/language/parseStyles.ts similarity index 100% rename from packages/language-server/src/service/core/parseStyles.ts rename to packages/language-server/src/language/parseStyles.ts diff --git a/packages/language-server/src/service/html/index.ts b/packages/language-server/src/plugins/html/index.ts similarity index 100% rename from packages/language-server/src/service/html/index.ts rename to packages/language-server/src/plugins/html/index.ts diff --git a/packages/language-server/src/service/index.ts b/packages/language-server/src/plugins/index.ts similarity index 50% rename from packages/language-server/src/service/index.ts rename to packages/language-server/src/plugins/index.ts index 136df735..7f7b2740 100644 --- a/packages/language-server/src/service/index.ts +++ b/packages/language-server/src/plugins/index.ts @@ -1,54 +1,14 @@ -import "../utils/project-defaults"; import { create as createCssService } from "volar-service-css"; import { create as createTypeScriptTwoSlashService } from "volar-service-typescript-twoslash-queries"; import { create as createTypeScriptServices } from "volar-service-typescript"; import type { Connection } from "@volar/language-server"; -import { Project } from "@marko/language-tools"; -import ts from "typescript"; import { create as createAccessibilityService } from "./marko-accessibility"; import { create as createMarkoService } from "./marko"; -import { getMarkoLanguagePlugin } from "./core/marko-plugin"; import { createMarkoPrettierService } from "./prettier"; import { create as createMarkoDebugService } from "./marko-debug"; import { create as createMarkoFormatActionService } from "./marko-action-format"; import { create as createMarkoHtmlService } from "./html"; -const decoratedHosts = new WeakSet(); - -export function addMarkoTypes( - rootDir: string, - ts: typeof import("typescript"), - host: ts.LanguageServiceHost, -) { - if (decoratedHosts.has(host)) { - return; - } - decoratedHosts.add(host); - - const getScriptFileNames = host.getScriptFileNames.bind(host); - - host.getScriptFileNames = () => { - const addedFileNames = []; - - const typeLibs = Project.getTypeLibs(rootDir, ts, host); - - addedFileNames.push(typeLibs.internalTypesFile); - if (typeLibs.markoRunTypesFile) { - addedFileNames.push(typeLibs.markoRunTypesFile); - } - if (typeLibs.markoRunGeneratedTypesFile) { - addedFileNames.push(typeLibs.markoRunGeneratedTypesFile); - } - addedFileNames.push(typeLibs.markoTypesFile); - - return [...getScriptFileNames(), ...addedFileNames]; - }; -} - -export function getLanguagePlugins(ts: typeof import("typescript")) { - return [getMarkoLanguagePlugin(ts)]; -} - export function getLanguageServicePlugins( connection: Connection, ts: typeof import("typescript"), diff --git a/packages/language-server/src/service/marko-accessibility/axe-rules/axe-rules.ts b/packages/language-server/src/plugins/marko-accessibility/axe-rules/axe-rules.ts similarity index 100% rename from packages/language-server/src/service/marko-accessibility/axe-rules/axe-rules.ts rename to packages/language-server/src/plugins/marko-accessibility/axe-rules/axe-rules.ts diff --git a/packages/language-server/src/service/marko-accessibility/axe-rules/generate-axe-rules.ts b/packages/language-server/src/plugins/marko-accessibility/axe-rules/generate-axe-rules.ts similarity index 100% rename from packages/language-server/src/service/marko-accessibility/axe-rules/generate-axe-rules.ts rename to packages/language-server/src/plugins/marko-accessibility/axe-rules/generate-axe-rules.ts diff --git a/packages/language-server/src/service/marko-accessibility/axe-rules/rule-exceptions.ts b/packages/language-server/src/plugins/marko-accessibility/axe-rules/rule-exceptions.ts similarity index 100% rename from packages/language-server/src/service/marko-accessibility/axe-rules/rule-exceptions.ts rename to packages/language-server/src/plugins/marko-accessibility/axe-rules/rule-exceptions.ts diff --git a/packages/language-server/src/service/marko-accessibility/index.ts b/packages/language-server/src/plugins/marko-accessibility/index.ts similarity index 98% rename from packages/language-server/src/service/marko-accessibility/index.ts rename to packages/language-server/src/plugins/marko-accessibility/index.ts index 47691fc3..8f060290 100644 --- a/packages/language-server/src/service/marko-accessibility/index.ts +++ b/packages/language-server/src/plugins/marko-accessibility/index.ts @@ -3,7 +3,7 @@ import axe from "axe-core"; import { JSDOM } from "jsdom"; import type { Diagnostic, LanguageServicePlugin } from "@volar/language-server"; import { URI } from "vscode-uri"; -import { MarkoVirtualCode } from "../core/marko-plugin"; +import { MarkoVirtualCode } from "../../language"; import { ruleExceptions } from "./axe-rules/rule-exceptions"; // This plugin provides accessibility diagnostics for Marko templates. diff --git a/packages/language-server/src/service/marko-action-format/index.ts b/packages/language-server/src/plugins/marko-action-format/index.ts similarity index 97% rename from packages/language-server/src/service/marko-action-format/index.ts rename to packages/language-server/src/plugins/marko-action-format/index.ts index f16d1afa..0eba175f 100644 --- a/packages/language-server/src/service/marko-action-format/index.ts +++ b/packages/language-server/src/plugins/marko-action-format/index.ts @@ -1,6 +1,6 @@ import { LanguageServicePlugin } from "@volar/language-service"; import { URI } from "vscode-uri"; -import { MarkoVirtualCode } from "../core/marko-plugin"; +import { MarkoVirtualCode } from "../../language"; import { getFormattingOptions, getPrettierInstance } from "../prettier"; export const create = (): LanguageServicePlugin => { diff --git a/packages/language-server/src/service/marko-debug/index.ts b/packages/language-server/src/plugins/marko-debug/index.ts similarity index 96% rename from packages/language-server/src/service/marko-debug/index.ts rename to packages/language-server/src/plugins/marko-debug/index.ts index 09d60a96..95ac1159 100644 --- a/packages/language-server/src/service/marko-debug/index.ts +++ b/packages/language-server/src/plugins/marko-debug/index.ts @@ -1,6 +1,6 @@ import { LanguageServicePlugin } from "@volar/language-service"; import { URI } from "vscode-uri"; -import { MarkoVirtualCode } from "../core/marko-plugin"; +import { MarkoVirtualCode } from "../../language"; export const create = (): LanguageServicePlugin => { return { diff --git a/packages/language-server/src/service/marko/complete/AttrName.ts b/packages/language-server/src/plugins/marko/complete/AttrName.ts similarity index 98% rename from packages/language-server/src/service/marko/complete/AttrName.ts rename to packages/language-server/src/plugins/marko/complete/AttrName.ts index fb67cf41..d711caea 100644 --- a/packages/language-server/src/service/marko/complete/AttrName.ts +++ b/packages/language-server/src/plugins/marko/complete/AttrName.ts @@ -7,7 +7,7 @@ import { TextEdit, } from "vscode-languageserver"; import type { Node } from "@marko/language-tools"; -import { MarkoVirtualCode } from "../../core/marko-plugin"; +import { MarkoVirtualCode } from "../../../language"; export function AttrName( node: Node.AttrName, diff --git a/packages/language-server/src/service/marko/complete/Import.ts b/packages/language-server/src/plugins/marko/complete/Import.ts similarity index 96% rename from packages/language-server/src/service/marko/complete/Import.ts rename to packages/language-server/src/plugins/marko/complete/Import.ts index 266ff438..fd61f469 100644 --- a/packages/language-server/src/service/marko/complete/Import.ts +++ b/packages/language-server/src/plugins/marko/complete/Import.ts @@ -1,7 +1,7 @@ import { CompletionItem, TextEdit } from "vscode-languageserver"; import type { Node } from "@marko/language-tools"; import getTagNameCompletion from "../util/get-tag-name-completion"; -import { MarkoVirtualCode } from "../../core/marko-plugin"; +import { MarkoVirtualCode } from "../../../language"; const importTagReg = /(['"])<((?:[^'"\\>]+|\\.)*)>?\1/; diff --git a/packages/language-server/src/service/marko/complete/OpenTagName.ts b/packages/language-server/src/plugins/marko/complete/OpenTagName.ts similarity index 96% rename from packages/language-server/src/service/marko/complete/OpenTagName.ts rename to packages/language-server/src/plugins/marko/complete/OpenTagName.ts index 5a4b9b0e..d486f76d 100644 --- a/packages/language-server/src/service/marko/complete/OpenTagName.ts +++ b/packages/language-server/src/plugins/marko/complete/OpenTagName.ts @@ -1,7 +1,7 @@ import type { CompletionItem } from "vscode-languageserver"; import { type Node, NodeType } from "@marko/language-tools"; import getTagNameCompletion from "../util/get-tag-name-completion"; -import { MarkoVirtualCode } from "../../core/marko-plugin"; +import { MarkoVirtualCode } from "../../../language"; export function OpenTagName( node: Node.OpenTagName, diff --git a/packages/language-server/src/service/marko/complete/Tag.ts b/packages/language-server/src/plugins/marko/complete/Tag.ts similarity index 96% rename from packages/language-server/src/service/marko/complete/Tag.ts rename to packages/language-server/src/plugins/marko/complete/Tag.ts index 30618198..221d9876 100644 --- a/packages/language-server/src/service/marko/complete/Tag.ts +++ b/packages/language-server/src/plugins/marko/complete/Tag.ts @@ -5,7 +5,7 @@ import { TextEdit, } from "vscode-languageserver"; import { type Node, UNFINISHED } from "@marko/language-tools"; -import { MarkoVirtualCode } from "../../core/marko-plugin"; +import { MarkoVirtualCode } from "../../../language"; const partialCloseTagReg = /<\/(?:[^><]*>)?/iy; diff --git a/packages/language-server/src/service/marko/complete/index.ts b/packages/language-server/src/plugins/marko/complete/index.ts similarity index 92% rename from packages/language-server/src/service/marko/complete/index.ts rename to packages/language-server/src/plugins/marko/complete/index.ts index f77b2033..4dc87cfa 100644 --- a/packages/language-server/src/service/marko/complete/index.ts +++ b/packages/language-server/src/plugins/marko/complete/index.ts @@ -1,6 +1,6 @@ import { NodeType } from "@marko/language-tools"; import { CompletionItem } from "vscode-languageserver"; -import { MarkoVirtualCode } from "../../core/marko-plugin"; +import { MarkoVirtualCode } from "../../../language"; import { AttrName } from "./AttrName"; import { Import } from "./Import"; import { OpenTagName } from "./OpenTagName"; diff --git a/packages/language-server/src/service/marko/definition/AttrName.ts b/packages/language-server/src/plugins/marko/definition/AttrName.ts similarity index 95% rename from packages/language-server/src/service/marko/definition/AttrName.ts rename to packages/language-server/src/plugins/marko/definition/AttrName.ts index caf27e7f..9a7f9a88 100644 --- a/packages/language-server/src/service/marko/definition/AttrName.ts +++ b/packages/language-server/src/plugins/marko/definition/AttrName.ts @@ -4,7 +4,7 @@ import { type Node, getLines, getLocation } from "@marko/language-tools"; import { LocationLink } from "@volar/language-service"; import RegExpBuilder from "../../../utils/regexp-builder"; import { START_LOCATION } from "../../../utils/constants"; -import { MarkoVirtualCode } from "../../core/marko-plugin"; +import { MarkoVirtualCode } from "../../../language"; export function AttrName( node: Node.AttrName, diff --git a/packages/language-server/src/service/marko/definition/OpenTagName.ts b/packages/language-server/src/plugins/marko/definition/OpenTagName.ts similarity index 96% rename from packages/language-server/src/service/marko/definition/OpenTagName.ts rename to packages/language-server/src/plugins/marko/definition/OpenTagName.ts index f6c183f5..bbd05438 100644 --- a/packages/language-server/src/service/marko/definition/OpenTagName.ts +++ b/packages/language-server/src/plugins/marko/definition/OpenTagName.ts @@ -11,7 +11,7 @@ import { import { LocationLink } from "@volar/language-service"; import RegExpBuilder from "../../../utils/regexp-builder"; import { START_LOCATION } from "../../../utils/constants"; -import { MarkoVirtualCode } from "../../core/marko-plugin"; +import { MarkoVirtualCode } from "../../../language"; export function OpenTagName( node: Node.OpenTagName, diff --git a/packages/language-server/src/service/marko/definition/index.ts b/packages/language-server/src/plugins/marko/definition/index.ts similarity index 89% rename from packages/language-server/src/service/marko/definition/index.ts rename to packages/language-server/src/plugins/marko/definition/index.ts index eb4084bc..558f3282 100644 --- a/packages/language-server/src/service/marko/definition/index.ts +++ b/packages/language-server/src/plugins/marko/definition/index.ts @@ -1,6 +1,6 @@ import { NodeType } from "@marko/language-tools"; import { LocationLink } from "vscode-languageserver"; -import { MarkoVirtualCode } from "../../core/marko-plugin"; +import { MarkoVirtualCode } from "../../../language"; import { AttrName } from "./AttrName"; import { OpenTagName } from "./OpenTagName"; diff --git a/packages/language-server/src/service/marko/document-symbols.ts b/packages/language-server/src/plugins/marko/document-symbols.ts similarity index 100% rename from packages/language-server/src/service/marko/document-symbols.ts rename to packages/language-server/src/plugins/marko/document-symbols.ts diff --git a/packages/language-server/src/service/marko/hover/OpenTagName.ts b/packages/language-server/src/plugins/marko/hover/OpenTagName.ts similarity index 92% rename from packages/language-server/src/service/marko/hover/OpenTagName.ts rename to packages/language-server/src/plugins/marko/hover/OpenTagName.ts index dafea3c0..1b700a4f 100644 --- a/packages/language-server/src/service/marko/hover/OpenTagName.ts +++ b/packages/language-server/src/plugins/marko/hover/OpenTagName.ts @@ -2,7 +2,7 @@ import type { Node } from "@marko/language-tools"; import { Hover } from "vscode-languageserver"; import getTagNameCompletion from "../util/get-tag-name-completion"; import { START_LOCATION } from "../../../utils/constants"; -import { MarkoVirtualCode } from "../../core/marko-plugin"; +import { MarkoVirtualCode } from "../../../language"; export function OpenTagName( node: Node.OpenTagName, diff --git a/packages/language-server/src/service/marko/hover/index.ts b/packages/language-server/src/plugins/marko/hover/index.ts similarity index 87% rename from packages/language-server/src/service/marko/hover/index.ts rename to packages/language-server/src/plugins/marko/hover/index.ts index 8621b9f7..a2342bf2 100644 --- a/packages/language-server/src/service/marko/hover/index.ts +++ b/packages/language-server/src/plugins/marko/hover/index.ts @@ -1,6 +1,6 @@ import { NodeType } from "@marko/language-tools"; import { Hover } from "vscode-languageserver"; -import { MarkoVirtualCode } from "../../core/marko-plugin"; +import { MarkoVirtualCode } from "../../../language"; import { OpenTagName } from "./OpenTagName"; export function provideHover( diff --git a/packages/language-server/src/service/marko/index.ts b/packages/language-server/src/plugins/marko/index.ts similarity index 94% rename from packages/language-server/src/service/marko/index.ts rename to packages/language-server/src/plugins/marko/index.ts index cec8908d..63b1717d 100644 --- a/packages/language-server/src/service/marko/index.ts +++ b/packages/language-server/src/plugins/marko/index.ts @@ -4,7 +4,7 @@ import { } from "@volar/language-service"; import { TextDocument } from "vscode-languageserver-textdocument"; import { URI } from "vscode-uri"; -import { MarkoVirtualCode } from "../core/marko-plugin"; +import { MarkoVirtualCode } from "../../language"; import { provideCompletions } from "./complete"; import { provideHover } from "./hover"; import { provideValidations } from "./validate"; @@ -66,7 +66,9 @@ export const create = ( provideDiagnostics(document, token) { if (token.isCancellationRequested) return; return worker(document, async (virtualCode) => { - return await provideValidations(virtualCode); + const diagnostics = await provideValidations(virtualCode); + console.log("Diagnostics", diagnostics); + return diagnostics; }); }, provideHover(document, position, token) { @@ -118,7 +120,3 @@ export const create = ( }, }; }; - -// export default { -// findDocumentLinks, -// findDocumentSymbols, diff --git a/packages/language-server/src/service/marko/util/get-tag-name-completion.ts b/packages/language-server/src/plugins/marko/util/get-tag-name-completion.ts similarity index 100% rename from packages/language-server/src/service/marko/util/get-tag-name-completion.ts rename to packages/language-server/src/plugins/marko/util/get-tag-name-completion.ts diff --git a/packages/language-server/src/service/marko/util/is-document-link-attr.ts b/packages/language-server/src/plugins/marko/util/is-document-link-attr.ts similarity index 100% rename from packages/language-server/src/service/marko/util/is-document-link-attr.ts rename to packages/language-server/src/plugins/marko/util/is-document-link-attr.ts diff --git a/packages/language-server/src/service/marko/validate.ts b/packages/language-server/src/plugins/marko/validate.ts similarity index 97% rename from packages/language-server/src/service/marko/validate.ts rename to packages/language-server/src/plugins/marko/validate.ts index dee696aa..6a428208 100644 --- a/packages/language-server/src/service/marko/validate.ts +++ b/packages/language-server/src/plugins/marko/validate.ts @@ -1,7 +1,7 @@ import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver-protocol"; import { DiagnosticType } from "@marko/babel-utils"; import { Config } from "@marko/compiler"; -import { MarkoVirtualCode } from "../core/marko-plugin"; +import { MarkoVirtualCode } from "../../language"; const markoErrorRegExp = /^(.+?)\.marko(?:\((\d+)(?:\s*,\s*(\d+))?\))?: (.*)$/gm; @@ -13,7 +13,7 @@ export async function provideValidations( try { // Instead of compiling in the virtual code, we compile the code as part of the diagnostics // callback to avoid blocking other more urgent requests like completions or hovers. - const compilerResult = await file.compiler?.compile( + const compilerResult = await file.compiler.compile( file.code, file.fileName, compilerConfig, diff --git a/packages/language-server/src/service/prettier/index.ts b/packages/language-server/src/plugins/prettier/index.ts similarity index 100% rename from packages/language-server/src/service/prettier/index.ts rename to packages/language-server/src/plugins/prettier/index.ts diff --git a/packages/language-server/src/service/prettier/package.ts b/packages/language-server/src/plugins/prettier/package.ts similarity index 100% rename from packages/language-server/src/service/prettier/package.ts rename to packages/language-server/src/plugins/prettier/package.ts From cb9fe8f42e90aab0bf47d9898e9ffbcc3676077c Mon Sep 17 00:00:00 2001 From: AngusMorton Date: Thu, 24 Oct 2024 15:46:25 +1100 Subject: [PATCH 10/14] Cleanup --- .../src/__tests__/index.test.ts | 172 +++++++++-------- .../src/__tests__/util/language-service.ts | 181 ++++-------------- packages/language-server/src/index.ts | 21 +- .../language-server/src/language/index.ts | 17 +- .../src/plugins/marko/index.ts | 4 +- tsconfig.json | 2 +- 6 files changed, 146 insertions(+), 251 deletions(-) diff --git a/packages/language-server/src/__tests__/index.test.ts b/packages/language-server/src/__tests__/index.test.ts index b8ad8ea3..1ec6286b 100644 --- a/packages/language-server/src/__tests__/index.test.ts +++ b/packages/language-server/src/__tests__/index.test.ts @@ -2,13 +2,11 @@ import fs from "fs"; import path from "path"; import snapshot from "mocha-snap"; // import { bench, run } from "mitata"; - -import { URI } from "vscode-uri"; import { TextDocument } from "vscode-languageserver-textdocument"; -import { CancellationToken, Position } from "vscode-languageserver"; +import { Position } from "vscode-languageserver"; import { Project } from "@marko/language-tools"; -import MarkoLangaugeService, { documents } from "../plugins"; import { codeFrame } from "./util/code-frame"; +import { getLanguageServer } from "./util/language-service"; Project.setDefaultTypePaths({ internalTypesFile: require.resolve( @@ -21,93 +19,105 @@ Project.setDefaultTypePaths({ // const BENCHED = new Set(); const FIXTURE_DIR = path.join(__dirname, "fixtures"); -for (const subdir of fs.readdirSync(FIXTURE_DIR)) { - const fixtureSubdir = path.join(FIXTURE_DIR, subdir); - - if (!fs.statSync(fixtureSubdir).isDirectory()) continue; - for (const entry of fs.readdirSync(fixtureSubdir)) { - it(entry, async () => { - const fixtureDir = path.join(fixtureSubdir, entry); - - for (const filename of loadMarkoFiles(fixtureDir)) { - const doc = documents.get(URI.file(filename).toString())!; - const code = doc.getText(); - const params = { - textDocument: { - uri: doc.uri, - languageId: doc.languageId, - version: doc.version, - text: code, - }, - } as const; - documents.doOpen(params); - - let results = ""; - - for (const position of getHovers(doc)) { - const hoverInfo = await MarkoLangaugeService.doHover( - doc, - { +describe("server", () => { + after(async () => { + const handle = await getLanguageServer(); + await handle.shutdown(); + }); + + for (const subdir of fs.readdirSync(FIXTURE_DIR)) { + const fixtureSubdir = path.join(FIXTURE_DIR, subdir); + + if (!fs.statSync(fixtureSubdir).isDirectory()) continue; + for (const entry of fs.readdirSync(fixtureSubdir)) { + it(entry, async () => { + const serverHandle = await getLanguageServer(); + + const fixtureDir = path.join(fixtureSubdir, entry); + + for (const filename of loadMarkoFiles(fixtureDir)) { + const doc = await serverHandle.openTextDocument(filename, "marko"); + const code = doc.getText(); + + let results = ""; + + for (const position of getHovers(doc)) { + const hoverInfo = await serverHandle.sendHoverRequest( + doc.uri, position, - textDocument: doc, - }, - CancellationToken.None, - ); - const loc = { start: position, end: position }; - - let message = ""; - const contents = hoverInfo?.contents; - if (contents) { - if (Array.isArray(contents)) { - message = "\n" + contents.join("\n "); - } else if (typeof contents === "object") { - message = contents.value; - } else { - message = contents; + ); + + const loc = { start: position, end: position }; + + let message = ""; + const contents = hoverInfo?.contents; + if (contents) { + if (Array.isArray(contents)) { + message = "\n" + contents.join("\n "); + } else if (typeof contents === "object") { + message = contents.value; + } else { + message = contents; + } } - } - if (message) { - results += `### Ln ${position.line + 1}, Col ${ - position.character + 1 - }\n\`\`\`marko\n${codeFrame( - code, - message.replace(/```typescript\r?\n([\s\S]*)\r?\n```/gm, "$1"), - loc, - )}\n\`\`\`\n\n`; + if (message) { + results += `### Ln ${position.line + 1}, Col ${ + position.character + 1 + }\n\`\`\`marko\n${codeFrame( + code, + message.replace(/```typescript\r?\n([\s\S]*)\r?\n```/gm, "$1"), + loc, + )}\n\`\`\`\n\n`; + } } - } - if (results.length) { - results = `## Hovers\n${results}`; - } - - const errors = await MarkoLangaugeService.doValidate(doc); - - if (errors && errors.length) { - results += "## Diagnostics\n"; + if (results.length) { + results = `## Hovers\n${results}`; + } - for (const error of errors) { - const loc = { - start: error.range.start, - end: error.range.end, - }; - results += `### Ln ${loc.start.line + 1}, Col ${ - loc.start.character + 1 - }\n\`\`\`marko\n${codeFrame(code, error.message, loc)}\n\`\`\`\n\n`; + const diagnosticReport = + await serverHandle.sendDocumentDiagnosticRequest(doc.uri); + + if ( + diagnosticReport.kind === "full" && + diagnosticReport.items && + diagnosticReport.items.length + ) { + results += "## Diagnostics\n"; + + diagnosticReport.items.sort((a, b) => { + const lineDiff = a.range.start.line - b.range.start.line; + if (lineDiff === 0) { + return a.range.start.character - b.range.start.character; + } + return lineDiff; + }); + for (const error of diagnosticReport.items) { + const loc = { + start: error.range.start, + end: error.range.end, + }; + results += `### Ln ${loc.start.line + 1}, Col ${ + loc.start.character + 1 + }\n\`\`\`marko\n${codeFrame(code, error.message, loc)}\n\`\`\`\n\n`; + } } - } - documents.doClose(params); + await serverHandle.closeTextDocument(doc.uri); - await snapshot(results, { - file: path.relative(fixtureDir, filename.replace(/\.marko$/, ".md")), - dir: fixtureDir, - }); - } - }); + await snapshot(results, { + file: path.relative( + fixtureDir, + filename.replace(/\.marko$/, ".md"), + ), + dir: fixtureDir, + }); + } + }); + } } -} +}); // if (SHOULD_BENCH) { // after(async function () { diff --git a/packages/language-server/src/__tests__/util/language-service.ts b/packages/language-server/src/__tests__/util/language-service.ts index efe01497..813a48bd 100644 --- a/packages/language-server/src/__tests__/util/language-service.ts +++ b/packages/language-server/src/__tests__/util/language-service.ts @@ -1,34 +1,22 @@ import fs from "fs"; import path from "path"; import ts from "typescript"; -import { - createFSBackedSystem, - createVirtualLanguageServiceHost, -} from "@typescript/vfs"; -import type { Extracted } from "@marko/language-tools"; -import { getExt } from "@marko/language-tools"; +import { LanguageServerHandle, startLanguageServer } from "@volar/test-utils"; +import * as protocol from "vscode-languageserver-protocol/node"; +import { Project } from "@marko/language-tools"; const rootDir = process.cwd(); -const startPosition: ts.LineAndCharacter = { - line: 0, - character: 0, -}; -export type Processors = Record< - string, - { - ext: ts.Extension; - kind: ts.ScriptKind; - extract(filename: string, code: string): Extracted; - } ->; +let serverHandle: LanguageServerHandle | undefined; + +Project.setDefaultTypePaths({ + internalTypesFile: require.resolve( + "@marko/language-tools/marko.internal.d.ts", + ), + markoTypesFile: require.resolve("marko/index.d.ts"), +}); -export function createLanguageService( - fsMap: Map, - processors: Processors, -) { - const getProcessor = (filename: string) => - processors[getExt(filename)?.slice(1) || ""]; +export async function getLanguageServer() { const compilerOptions: ts.CompilerOptions = { ...ts.getDefaultCompilerOptions(), rootDir, @@ -41,132 +29,37 @@ export function createLanguageService( allowNonTsExtensions: true, module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ESNext, - moduleResolution: ts.ModuleResolutionKind.NodeJs, - }; - const rootFiles = [...fsMap.keys()]; - const sys = createFSBackedSystem(fsMap, rootDir, ts); - - const { languageServiceHost: lsh } = createVirtualLanguageServiceHost( - sys, - rootFiles, - compilerOptions, - ts, - ); - - const ls = ts.createLanguageService(lsh); - const snapshotCache = new Map(); - - /** - * Trick TypeScript into thinking Marko files are TS/JS files. - */ - lsh.getScriptKind = (filename: string) => { - const processor = getProcessor(filename); - return processor ? processor.kind : ts.ScriptKind.TS; + moduleResolution: ts.ModuleResolutionKind.NodeNext, }; - /** - * A script snapshot is an immutable string of text representing the contents of a file. - * We patch it so that Marko files instead return their extracted ts code. - */ - const getScriptSnapshot = lsh.getScriptSnapshot!.bind(lsh); - lsh.getScriptSnapshot = (filename: string) => { - const processor = getProcessor(filename); - if (processor) { - let cached = snapshotCache.get(filename); - if (!cached) { - const extracted = processor.extract( - filename, - lsh.readFile(filename, "utf-8") || "", - ); - snapshotCache.set( - filename, - (cached = [ - extracted, - ts.ScriptSnapshot.fromString(extracted.toString()), - ]), - ); - } - - return cached[1]; - } - - return getScriptSnapshot(filename); - }; - - /** - * This ensures that any directory reads with specific file extensions also include Marko. - * It is used for example when completing the `from` property of the `import` statement. - */ - const readDirectory = lsh.readDirectory!.bind(lsh); - const additionalExts = Object.keys(processors); - lsh.readDirectory = (path, extensions, exclude, include, depth) => { - return readDirectory( - path, - extensions?.concat(additionalExts), - exclude, - include, - depth, + if (!serverHandle) { + console.log("Starting language server"); + console.log(" - bin, ", path.resolve("./bin.js")); + console.log( + " - Working Dir", + path.resolve(rootDir, "./__tests__/fixtures/"), ); - }; + serverHandle = startLanguageServer(path.resolve("./bin.js")); - /** - * TypeScript doesn't know how to resolve `.marko` files. - * Below we first try to use TypeScripts normal resolution, and then fallback - * to seeing if a `.marko` file exists at the same location. - */ - lsh.resolveModuleNames = (moduleNames, containingFile) => { - const resolvedModules: ( - | ts.ResolvedModuleFull - | ts.ResolvedModule - | undefined - )[] = moduleNames.map( - (moduleName) => - ts.resolveModuleName(moduleName, containingFile, compilerOptions, sys) - .resolvedModule, + const tsdkPath = path.dirname( + require.resolve("typescript/lib/typescript.js"), ); + console.log(" - tsdkPath", tsdkPath); + await serverHandle.initialize(path.resolve("./"), { + typescript: { + tsdk: tsdkPath, + compilerOptions, + }, + }); + + // Ensure that our first test does not suffer from a TypeScript overhead + await serverHandle.sendCompletionRequest( + "file://doesnt-exists", + protocol.Position.create(0, 0), + ); + } - for (let i = resolvedModules.length; i--; ) { - if (!resolvedModules[i]) { - const moduleName = moduleNames[i]; - const processor = moduleName[0] !== "*" && getProcessor(moduleName); - if (processor && moduleName[0] === ".") { - // For relative paths just see if it exists on disk. - const resolvedFileName = path.resolve( - containingFile, - "..", - moduleName, - ); - if (lsh.fileExists(resolvedFileName)) { - resolvedModules[i] = { - resolvedFileName, - extension: processor.ext, - isExternalLibraryImport: false, - }; - } - } - } - } - - return resolvedModules; - }; - - /** - * Whenever TypeScript requests line/character info we return with the source - * file line/character if it exists. - */ - const toLineColumnOffset = ls.toLineColumnOffset!; - ls.toLineColumnOffset = (fileName, pos) => { - if (pos === 0) return startPosition; - - const extracted = snapshotCache.get(fileName)?.[0]; - if (extracted) { - return extracted.sourcePositionAt(pos) || startPosition; - } - - return toLineColumnOffset(fileName, pos); - }; - - return ls; + return serverHandle; } export function loadMarkoFiles(dir: string, all = new Set()) { diff --git a/packages/language-server/src/index.ts b/packages/language-server/src/index.ts index 109651b9..5361925f 100644 --- a/packages/language-server/src/index.ts +++ b/packages/language-server/src/index.ts @@ -1,10 +1,10 @@ -import { dirname } from "path"; import { createConnection, createServer, createTypeScriptProject, loadTsdkByPath, } from "@volar/language-server/node"; +import { URI } from "vscode-uri"; import { getLanguageServicePlugins } from "./plugins"; import { addMarkoTypes, createMarkoLanguagePlugin } from "./language"; @@ -31,26 +31,19 @@ connection.onInitialize((params) => { params, createTypeScriptProject(typescript, diagnosticMessages, ({ env }) => { return { - languagePlugins: [createMarkoLanguagePlugin(typescript)], + languagePlugins: [ + createMarkoLanguagePlugin(typescript, (uri: URI) => + uri.fsPath.replace(/\\/g, "/"), + ), + ], setup({ project }) { const { languageServiceHost, configFileName } = project.typescript!; const rootPath = configFileName ? configFileName.split("/").slice(0, -1).join("/") : env.workspaceFolders[0]!.fsPath; - const nearestPackageJson = typescript.findConfigFile( - rootPath, - typescript.sys.fileExists, - "package.json", - ); - if (nearestPackageJson) { - addMarkoTypes( - dirname(nearestPackageJson), - typescript, - languageServiceHost, - ); - } + addMarkoTypes(rootPath, typescript, languageServiceHost); }, }; }), diff --git a/packages/language-server/src/language/index.ts b/packages/language-server/src/language/index.ts index 28af4f93..fe2050d2 100644 --- a/packages/language-server/src/language/index.ts +++ b/packages/language-server/src/language/index.ts @@ -5,7 +5,6 @@ import { type VirtualCode, forEachEmbeddedCode, } from "@volar/language-core"; -import type { URI } from "vscode-uri"; import type ts from "typescript"; import { Project, extractHTML, parse } from "@marko/language-tools"; import { TaglibLookup } from "@marko/babel-utils"; @@ -30,7 +29,6 @@ export function addMarkoTypes( host.getScriptFileNames = () => { const addedFileNames = []; - const typeLibs = Project.getTypeLibs(rootDir, ts, host); addedFileNames.push(typeLibs.internalTypesFile); @@ -46,18 +44,21 @@ export function addMarkoTypes( }; } -export function createMarkoLanguagePlugin( +export function createMarkoLanguagePlugin( ts: typeof import("typescript"), -): LanguagePlugin { + asFileName: (scriptId: T) => string, +): LanguagePlugin { return { - getLanguageId(uri) { - if (uri.path.endsWith(".marko")) { + getLanguageId(scriptId) { + const fileName = asFileName(scriptId); + + if (fileName.endsWith(".marko")) { return "marko"; } }, - createVirtualCode(uri, languageId, snapshot) { + createVirtualCode(scriptId, languageId, snapshot) { if (languageId === "marko") { - const fileName = uri.fsPath.replace(/\\/g, "/"); + const fileName = asFileName(scriptId); return new MarkoVirtualCode(fileName, snapshot, ts); } }, diff --git a/packages/language-server/src/plugins/marko/index.ts b/packages/language-server/src/plugins/marko/index.ts index 63b1717d..8c211b98 100644 --- a/packages/language-server/src/plugins/marko/index.ts +++ b/packages/language-server/src/plugins/marko/index.ts @@ -66,9 +66,7 @@ export const create = ( provideDiagnostics(document, token) { if (token.isCancellationRequested) return; return worker(document, async (virtualCode) => { - const diagnostics = await provideValidations(virtualCode); - console.log("Diagnostics", diagnostics); - return diagnostics; + return await provideValidations(virtualCode); }); }, provideHover(document, position, token) { diff --git a/tsconfig.json b/tsconfig.json index 44bf61f1..64294297 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "module": "ESNext", "skipLibCheck": true, "stripInternal": true, - "noUnusedLocals": false, // Temporarily disabled. + "noUnusedLocals": true, "esModuleInterop": true, "moduleResolution": "node", "resolveJsonModule": true, From 703056fd64cfa5097bf4532e053b994f7bd9f73c Mon Sep 17 00:00:00 2001 From: AngusMorton Date: Thu, 24 Oct 2024 17:08:15 +1100 Subject: [PATCH 11/14] Cleanup formatting and tests --- .../form-field-multiple-labels/index.marko | 17 +- .../index.marko | 7 +- .../src/__tests__/index.test.ts | 171 +++++++++--------- .../src/plugins/marko-action-format/index.ts | 6 +- .../src/plugins/prettier/index.ts | 51 +++--- .../src/plugins/prettier/package.ts | 37 ++-- .../src/utils/importPackage.ts | 74 -------- 7 files changed, 146 insertions(+), 217 deletions(-) delete mode 100644 packages/language-server/src/utils/importPackage.ts diff --git a/packages/language-server/src/__tests__/fixtures/html/form-field-multiple-labels/index.marko b/packages/language-server/src/__tests__/fixtures/html/form-field-multiple-labels/index.marko index 2c9c7d56..9deaff2e 100644 --- a/packages/language-server/src/__tests__/fixtures/html/form-field-multiple-labels/index.marko +++ b/packages/language-server/src/__tests__/fixtures/html/form-field-multiple-labels/index.marko @@ -1,15 +1,8 @@ - - + + - - \ No newline at end of file diff --git a/packages/language-server/src/__tests__/fixtures/script/bound-attr-const-dynamic-member/index.marko b/packages/language-server/src/__tests__/fixtures/script/bound-attr-const-dynamic-member/index.marko index e0809d5a..17015993 100644 --- a/packages/language-server/src/__tests__/fixtures/script/bound-attr-const-dynamic-member/index.marko +++ b/packages/language-server/src/__tests__/fixtures/script/bound-attr-const-dynamic-member/index.marko @@ -1,5 +1,4 @@ - - - -// ^? ^? + + +// ^? ^? \ No newline at end of file diff --git a/packages/language-server/src/__tests__/index.test.ts b/packages/language-server/src/__tests__/index.test.ts index 1ec6286b..fa033ec5 100644 --- a/packages/language-server/src/__tests__/index.test.ts +++ b/packages/language-server/src/__tests__/index.test.ts @@ -19,105 +19,100 @@ Project.setDefaultTypePaths({ // const BENCHED = new Set(); const FIXTURE_DIR = path.join(__dirname, "fixtures"); -describe("server", () => { - after(async () => { - const handle = await getLanguageServer(); - await handle.shutdown(); - }); - - for (const subdir of fs.readdirSync(FIXTURE_DIR)) { - const fixtureSubdir = path.join(FIXTURE_DIR, subdir); - - if (!fs.statSync(fixtureSubdir).isDirectory()) continue; - for (const entry of fs.readdirSync(fixtureSubdir)) { - it(entry, async () => { - const serverHandle = await getLanguageServer(); - - const fixtureDir = path.join(fixtureSubdir, entry); - - for (const filename of loadMarkoFiles(fixtureDir)) { - const doc = await serverHandle.openTextDocument(filename, "marko"); - const code = doc.getText(); - - let results = ""; - - for (const position of getHovers(doc)) { - const hoverInfo = await serverHandle.sendHoverRequest( - doc.uri, - position, - ); - - const loc = { start: position, end: position }; - - let message = ""; - const contents = hoverInfo?.contents; - if (contents) { - if (Array.isArray(contents)) { - message = "\n" + contents.join("\n "); - } else if (typeof contents === "object") { - message = contents.value; - } else { - message = contents; - } - } +after(async () => { + const handle = await getLanguageServer(); + await handle.shutdown(); +}); + +for (const subdir of fs.readdirSync(FIXTURE_DIR)) { + const fixtureSubdir = path.join(FIXTURE_DIR, subdir); + + if (!fs.statSync(fixtureSubdir).isDirectory()) continue; + for (const entry of fs.readdirSync(fixtureSubdir)) { + it(entry, async () => { + const serverHandle = await getLanguageServer(); + + const fixtureDir = path.join(fixtureSubdir, entry); + + for (const filename of loadMarkoFiles(fixtureDir)) { + const doc = await serverHandle.openTextDocument(filename, "marko"); + const code = doc.getText(); - if (message) { - results += `### Ln ${position.line + 1}, Col ${ - position.character + 1 - }\n\`\`\`marko\n${codeFrame( - code, - message.replace(/```typescript\r?\n([\s\S]*)\r?\n```/gm, "$1"), - loc, - )}\n\`\`\`\n\n`; + let results = ""; + + for (const position of getHovers(doc)) { + const hoverInfo = await serverHandle.sendHoverRequest( + doc.uri, + position, + ); + + const loc = { start: position, end: position }; + + let message = ""; + const contents = hoverInfo?.contents; + if (contents) { + if (Array.isArray(contents)) { + message = "\n" + contents.join("\n "); + } else if (typeof contents === "object") { + message = contents.value; + } else { + message = contents; } } - if (results.length) { - results = `## Hovers\n${results}`; + if (message) { + results += `### Ln ${position.line + 1}, Col ${ + position.character + 1 + }\n\`\`\`marko\n${codeFrame( + code, + message.replace(/```typescript\r?\n([\s\S]*)\r?\n```/gm, "$1"), + loc, + )}\n\`\`\`\n\n`; } + } - const diagnosticReport = - await serverHandle.sendDocumentDiagnosticRequest(doc.uri); - - if ( - diagnosticReport.kind === "full" && - diagnosticReport.items && - diagnosticReport.items.length - ) { - results += "## Diagnostics\n"; - - diagnosticReport.items.sort((a, b) => { - const lineDiff = a.range.start.line - b.range.start.line; - if (lineDiff === 0) { - return a.range.start.character - b.range.start.character; - } - return lineDiff; - }); - for (const error of diagnosticReport.items) { - const loc = { - start: error.range.start, - end: error.range.end, - }; - results += `### Ln ${loc.start.line + 1}, Col ${ - loc.start.character + 1 - }\n\`\`\`marko\n${codeFrame(code, error.message, loc)}\n\`\`\`\n\n`; - } - } + if (results.length) { + results = `## Hovers\n${results}`; + } + + const diagnosticReport = + await serverHandle.sendDocumentDiagnosticRequest(doc.uri); - await serverHandle.closeTextDocument(doc.uri); + if ( + diagnosticReport.kind === "full" && + diagnosticReport.items && + diagnosticReport.items.length + ) { + results += "## Diagnostics\n"; - await snapshot(results, { - file: path.relative( - fixtureDir, - filename.replace(/\.marko$/, ".md"), - ), - dir: fixtureDir, + diagnosticReport.items.sort((a, b) => { + const lineDiff = a.range.start.line - b.range.start.line; + if (lineDiff === 0) { + return a.range.start.character - b.range.start.character; + } + return lineDiff; }); + for (const error of diagnosticReport.items) { + const loc = { + start: error.range.start, + end: error.range.end, + }; + results += `### Ln ${loc.start.line + 1}, Col ${ + loc.start.character + 1 + }\n\`\`\`marko\n${codeFrame(code, error.message, loc)}\n\`\`\`\n\n`; + } } - }); - } + + await serverHandle.closeTextDocument(doc.uri); + + await snapshot(results, { + file: path.relative(fixtureDir, filename.replace(/\.marko$/, ".md")), + dir: fixtureDir, + }); + } + }); } -}); +} // if (SHOULD_BENCH) { // after(async function () { diff --git a/packages/language-server/src/plugins/marko-action-format/index.ts b/packages/language-server/src/plugins/marko-action-format/index.ts index 0eba175f..32ce74cb 100644 --- a/packages/language-server/src/plugins/marko-action-format/index.ts +++ b/packages/language-server/src/plugins/marko-action-format/index.ts @@ -26,15 +26,15 @@ export const create = (): LanguageServicePlugin => { return; } - const { prettierInstance, markoPluginPath } = + const { prettierInstance, prettierPluginMarko } = getPrettierInstance(context); - if (!prettierInstance || !markoPluginPath) { + if (!prettierInstance || !prettierPluginMarko) { return; } const prettierOptions = await getFormattingOptions( prettierInstance, - markoPluginPath, + prettierPluginMarko, fileUri, options, context, diff --git a/packages/language-server/src/plugins/prettier/index.ts b/packages/language-server/src/plugins/prettier/index.ts index a3954fe2..ac3115bb 100644 --- a/packages/language-server/src/plugins/prettier/index.ts +++ b/packages/language-server/src/plugins/prettier/index.ts @@ -10,36 +10,46 @@ import { MessageType } from "@volar/language-server"; import { URI } from "vscode-uri"; import { Project } from "@marko/language-tools"; import * as markoPrettier from "prettier-plugin-marko"; -import { Options } from "prettier"; -import { dynamicRequire } from "../../utils/importPackage"; -import { getMarkoPrettierPluginPath, importPrettier } from "./package"; +import type { Options } from "prettier"; +import * as prettierBuiltIn from "prettier"; +import { importMarkoPrettierPlugin, importPrettier } from "./package"; export function createMarkoPrettierService( connection: Connection, ): LanguageServicePlugin { - let prettier: typeof import("prettier") | undefined; - let prettierPluginPath: string | undefined; + let prettier: typeof prettierBuiltIn | undefined; + let prettierPlugin: typeof markoPrettier | undefined; let hasShownNotification = false; return createPrettierService( (context) => { - const { prettierInstance, markoPluginPath } = + const { prettierInstance, prettierPluginMarko } = getPrettierInstance(context); prettier = prettierInstance; - prettierPluginPath = markoPluginPath; + prettierPlugin = prettierPluginMarko; // Show a warning notification if Prettier or the Marko plugin isn't installed. - if ((!prettier || !prettierPluginPath) && !hasShownNotification) { + if ((!prettier || !prettierPlugin) && !hasShownNotification) { connection.sendNotification(ShowMessageNotification.type, { message: - "Couldn't load `prettier` or `prettier-plugin-marko`. Formatting will not work. Please make sure those two packages are installed into your project and restart the language server.", + "Couldn't load `prettier` or `prettier-plugin-marko`. Falling back to built-in versions.", type: MessageType.Warning, }); hasShownNotification = true; } - return prettierInstance; + if (!prettier) { + // Fallback to the built-in version of prettier. + prettier = prettierBuiltIn; + } + + if (!prettierPlugin) { + // Fallback to the built-in version of prettier-plugin-marko. + prettierPlugin = markoPrettier; + } + + return prettier; }, { documentSelector: [{ language: "marko" }], @@ -64,7 +74,7 @@ export function createMarkoPrettierService( ) => { return getFormattingOptions( prettierInstance, - prettierPluginPath, + prettierPlugin!, document.uri, formatOptions, context, @@ -82,16 +92,16 @@ export function createMarkoPrettierService( export function getPrettierInstance(context: LanguageServiceContext): { prettierInstance?: typeof import("prettier"); - markoPluginPath?: string; + prettierPluginMarko?: typeof import("prettier-plugin-marko"); } { for (const workspaceFolder of context.env.workspaceFolders) { if (workspaceFolder.scheme === "file") { const prettierInstance = importPrettier(workspaceFolder.fsPath); - const markoPluginPath = getMarkoPrettierPluginPath( + const prettierPluginMarko = importMarkoPrettierPlugin( workspaceFolder.fsPath, ); - return { prettierInstance, markoPluginPath }; + return { prettierInstance, prettierPluginMarko }; } } return {}; @@ -99,7 +109,7 @@ export function getPrettierInstance(context: LanguageServiceContext): { export async function getFormattingOptions( prettierInstance: typeof import("prettier"), - markoPluginPath: string | undefined, + prettierPlugin: typeof markoPrettier, documentUriString: string, formatOptions: FormattingOptions, context: LanguageServiceContext, @@ -143,21 +153,14 @@ export async function getFormattingOptions( }; try { - let resolvedPlugin; - if (markoPluginPath) { - resolvedPlugin = dynamicRequire(markoPluginPath); - } else { - resolvedPlugin = markoPrettier; - } - - resolvedPlugin.setCompiler( + prettierPlugin.setCompiler( Project.getCompiler(fileDir), Project.getConfig(fileDir), ); return { ...resolvedConfig, - plugins: [resolvedPlugin, ...(resolvedConfig.plugins ?? [])], + plugins: [prettierPlugin, ...(resolvedConfig.plugins ?? [])], parser: "marko", }; } catch (e) { diff --git a/packages/language-server/src/plugins/prettier/package.ts b/packages/language-server/src/plugins/prettier/package.ts index 02eb1bc4..f7f230d5 100644 --- a/packages/language-server/src/plugins/prettier/package.ts +++ b/packages/language-server/src/plugins/prettier/package.ts @@ -1,19 +1,32 @@ -import * as prettier from "prettier"; -import { getPackagePath } from "../../utils/importPackage"; +import type * as prettier from "prettier"; +import type * as prettierPluginMarko from "prettier-plugin-marko"; -export function importPrettier(fromPath: string): typeof prettier { - const prettierPkg = getPackagePath("prettier", [fromPath, __dirname]); - - if (prettierPkg) { - return require(prettierPkg); +export function importPrettier(fromPath: string): typeof prettier | undefined { + try { + const packagePath = require.resolve("prettier", { + paths: [fromPath, __dirname], + }); + console.log("Found Prettier", packagePath); + return require(packagePath); + } catch (error) { + console.error(error); } - // Return the built-in prettier instance if the user doesn't have it installed. - return prettier; + return undefined; } -export function getMarkoPrettierPluginPath( +export function importMarkoPrettierPlugin( fromPath: string, -): string | undefined { - return getPackagePath("prettier-plugin-marko", [fromPath, __dirname], false); +): typeof prettierPluginMarko | undefined { + try { + const packagePath = require.resolve("prettier-plugin-marko", { + paths: [fromPath, __dirname], + }); + console.log("prettier-plugin-marko", packagePath); + return require(packagePath); + } catch (error) { + console.error(error); + } + + return undefined; } diff --git a/packages/language-server/src/utils/importPackage.ts b/packages/language-server/src/utils/importPackage.ts deleted file mode 100644 index 49ac111e..00000000 --- a/packages/language-server/src/utils/importPackage.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { dirname } from "path"; - -/** - * Whether or not the current workspace can be trusted. - * TODO rework this to a class which depends on the LsConfigManager - * and inject that class into all places where it's needed (Document etc.) - */ -let isTrusted = true; - -export function setIsTrusted(_isTrusted: boolean) { - isTrusted = _isTrusted; -} - -export interface PackageInfo { - path: string; - version: { - full: string; - major: number; - minor: number; - patch: number; - }; -} - -/** - * This function encapsulates the require call in one place - * so we can replace its content inside rollup builds - * so it's not transformed. - */ -export function dynamicRequire(dynamicFileToRequire: string): any { - // prettier-ignore - return require(dynamicFileToRequire); -} - -export function getPackageInfo(packageName: string, fromPaths: string[]) { - const paths = [__dirname, ...fromPaths]; - - const packageJSONPath = require.resolve(`${packageName}/package.json`, { - paths, - }); - const { version } = dynamicRequire(packageJSONPath); - const [major, minor, patch] = version.split("."); - - return { - path: dirname(packageJSONPath), - version: { - full: version, - major: Number(major), - minor: Number(minor), - patch: Number(patch), - }, - }; -} - -/** - * Get the path of a package's directory from the paths in `fromPath`, if `root` is set to false, it will return the path of the package's entry point - */ -export function getPackagePath( - packageName: string, - fromPath: string[], - root = true, -): string | undefined { - const paths = []; - if (isTrusted) { - paths.unshift(...fromPath); - } - - try { - return root - ? dirname(require.resolve(packageName + "/package.json", { paths })) - : require.resolve(packageName, { paths }); - } catch (e) { - return undefined; - } -} From 8e472f903195eb2f658906e4645124d97e0a3104 Mon Sep 17 00:00:00 2001 From: AngusMorton Date: Sat, 2 Nov 2024 21:21:56 +1100 Subject: [PATCH 12/14] Add types to parseScript --- packages/language-server/src/language/parseScript.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/language-server/src/language/parseScript.ts b/packages/language-server/src/language/parseScript.ts index e8143019..43a43922 100644 --- a/packages/language-server/src/language/parseScript.ts +++ b/packages/language-server/src/language/parseScript.ts @@ -1,10 +1,11 @@ +import { TaglibLookup } from "@marko/babel-utils"; import { ScriptLang, extractScript, parse } from "@marko/language-tools"; import type { CodeMapping, VirtualCode } from "@volar/language-core"; export function parseScripts( parsed: ReturnType, ts: typeof import("typescript"), - tagLookup: any, + tagLookup: TaglibLookup, ): VirtualCode[] { const script = extractScript({ parsed, @@ -34,7 +35,7 @@ export function parseScripts( return [ { id: "script", - languageId: "typescript", + languageId: "ts", snapshot: { getText: (start, end) => scriptText.substring(start, end), getLength: () => scriptText.length, From bd7b1fcda83e8c7fd5f5737c17d2efe801322dd0 Mon Sep 17 00:00:00 2001 From: AngusMorton Date: Sat, 2 Nov 2024 22:44:51 +1100 Subject: [PATCH 13/14] rm document-symbols --- .../src/plugins/marko/document-symbols.ts | 59 ------------------- 1 file changed, 59 deletions(-) delete mode 100644 packages/language-server/src/plugins/marko/document-symbols.ts diff --git a/packages/language-server/src/plugins/marko/document-symbols.ts b/packages/language-server/src/plugins/marko/document-symbols.ts deleted file mode 100644 index de0af2ae..00000000 --- a/packages/language-server/src/plugins/marko/document-symbols.ts +++ /dev/null @@ -1,59 +0,0 @@ -// import { SymbolInformation, SymbolKind } from "vscode-languageserver"; -// import { type Node, NodeType } from "@marko/language-tools"; -// import type { Plugin } from "../types"; -// import { MarkoFile, processDoc } from "../../utils/file"; - -// export const findDocumentSymbols: Plugin["findDocumentSymbols"] = async (doc) => -// processDoc(doc, extractDocumentSymbols); - -// /** -// * Iterate over the Marko CST and extract all the symbols (mostly tags) in the document. -// */ -// function extractDocumentSymbols({ -// uri, -// scheme, -// parsed, -// lookup, -// }: MarkoFile): SymbolInformation[] { -// if (scheme !== "file") { -// return []; -// } - -// const symbols: SymbolInformation[] = []; -// const { program } = parsed; -// const visit = (node: Node.ChildNode) => { -// switch (node.type) { -// case NodeType.Tag: -// case NodeType.AttrTag: -// symbols.push({ -// name: -// (node.type === NodeType.AttrTag -// ? node.nameText?.slice(node.nameText.indexOf("@")) -// : node.nameText) || "<${...}>", -// kind: -// (node.nameText && -// lookup.getTag(node.nameText)?.html && -// SymbolKind.Property) || -// SymbolKind.Class, -// location: { -// uri, -// range: parsed.locationAt(node), -// }, -// }); - -// if (node.body) { -// for (const child of node.body) { -// visit(child); -// } -// } - -// break; -// } -// }; - -// for (const item of program.body) { -// visit(item); -// } - -// return symbols; -// } From 908ffd3bf936cbd3e18ec1f213c1f358c336ef53 Mon Sep 17 00:00:00 2001 From: AngusMorton Date: Wed, 6 Nov 2024 21:04:35 +1100 Subject: [PATCH 14/14] Migrate TS Plugin --- .../language-server/src/ts-plugin/host.ts | 261 ------------------ .../language-server/src/ts-plugin/index.ts | 210 +------------- packages/vscode/package.json | 2 +- packages/vscode/release.mts | 4 +- 4 files changed, 12 insertions(+), 465 deletions(-) delete mode 100644 packages/language-server/src/ts-plugin/host.ts diff --git a/packages/language-server/src/ts-plugin/host.ts b/packages/language-server/src/ts-plugin/host.ts deleted file mode 100644 index bb904104..00000000 --- a/packages/language-server/src/ts-plugin/host.ts +++ /dev/null @@ -1,261 +0,0 @@ -import path from "path"; -import type ts from "typescript/lib/tsserverlibrary"; -import { - type Extracted, - Processors, - getExt, - isDefinitionFile, -} from "@marko/language-tools"; - -const fsPathReg = /^(?:[./\\]|[A-Z]:)/i; -const modulePartsReg = /^((?:@(?:[^/]+)\/)?(?:[^/]+))(.*)$/; - -export interface ExtractedSnapshot extends Extracted { - snapshot: ts.IScriptSnapshot; -} - -export function patch( - ts: typeof import("typescript/lib/tsserverlibrary"), - configFile: string | undefined, - extractCache: Map< - string, - ExtractedSnapshot | { snapshot: ts.IScriptSnapshot } - >, - resolutionCache: ts.ModuleResolutionCache | undefined, - host: ts.LanguageServiceHost, - ps?: InstanceType, -) { - const processors = Processors.create({ - ts, - host, - configFile, - }); - const rootNames = Object.values(processors) - .map((processor) => processor.getRootNames?.()) - .flat() - .filter(Boolean) as string[]; - - const trackFile = ps - ? (fileName: string) => { - // Ensure the project service knows about the file. - // Without this the project never registers a `ScriptInfo`. - // TODO: maybe we should patch readFile instead of getScriptSnapshot? - - ps.getOrCreateScriptInfoForNormalizedPath( - fileName as any, - true, - undefined, - ts.ScriptKind.Deferred, - false, - host, - ); - } - : () => {}; - - /** - * Ensure the processor runtime definitions are always loaded. - */ - const getScriptFileNames = host.getScriptFileNames.bind(host); - host.getScriptFileNames = () => [ - ...new Set(rootNames.concat(getScriptFileNames())), - ]; - - /** - * Trick TypeScript into thinking Marko files are TS/JS files. - */ - const getScriptKind = host.getScriptKind?.bind(host); - if (getScriptKind) { - host.getScriptKind = (fileName: string) => { - const processor = getProcessor(fileName); - if (processor) return processor.getScriptKind(fileName); - return getScriptKind(fileName); - }; - } - - /** - * A script snapshot is an immutable string of text representing the contents of a file. - * We patch it so that Marko files instead return their extracted ts code. - */ - const getScriptSnapshot = host.getScriptSnapshot.bind(host); - host.getScriptSnapshot = (fileName: string) => { - const processor = getProcessor(fileName); - if (processor) { - let cached = extractCache.get(fileName); - - if (!cached) { - const code = host.readFile(fileName, "utf-8") || ""; - - try { - cached = processor.extract(fileName, code) as ExtractedSnapshot; - cached.snapshot = ts.ScriptSnapshot.fromString(cached.toString()); - } catch { - cached = { snapshot: ts.ScriptSnapshot.fromString("") }; - } - - trackFile(fileName); - extractCache.set(fileName, cached); - } - - return cached.snapshot; - } - - return getScriptSnapshot(fileName); - }; - - if (host.getProjectVersion) { - const getScriptVersion = host.getScriptVersion.bind(host); - host.getScriptVersion = (fileName: string) => { - const processor = getProcessor(fileName); - if (processor) return host.getProjectVersion!(); - return getScriptVersion(fileName); - }; - } - - /** - * This ensures that any directory reads with specific file extensions also include Marko. - * It is used for example when completing the `from` property of the `import` statement. - */ - const readDirectory = host.readDirectory?.bind(host); - if (readDirectory) { - host.readDirectory = (path, extensions, exclude, include, depth) => { - return readDirectory( - path, - extensions?.concat(Processors.extensions), - exclude, - include, - depth, - ); - }; - } - - /** - * TypeScript doesn't know how to resolve `.marko` files. - * Below we first try to use TypeScripts normal resolution, and then fallback - * to seeing if a `.marko` file exists at the same location. - */ - const resolveModuleNameLiterals = host.resolveModuleNameLiterals?.bind(host); - - if (resolveModuleNameLiterals) { - host.resolveModuleNameLiterals = ( - moduleLiterals, - containingFile, - redirectedReference, - options, - containingSourceFile, - reusedNames, - ) => { - let normalModuleLiterals = moduleLiterals as ts.StringLiteralLike[]; - let resolvedModules: - | undefined - | (ts.ResolvedModuleWithFailedLookupLocations | undefined)[]; - - for (let i = 0; i < moduleLiterals.length; i++) { - const moduleLiteral = moduleLiterals[i]; - const moduleName = moduleLiteral.text; - const processor = - moduleName[0] !== "*" ? getProcessor(moduleName) : undefined; - if (processor) { - let resolvedFileName: string | undefined; - if (fsPathReg.test(moduleName)) { - // For fs paths just see if it exists on disk. - resolvedFileName = path.resolve(containingFile, "..", moduleName); - } else { - // For other paths we treat it as a node_module and try resolving - // that modules `marko.json`. If the `marko.json` exists then we'll - // try resolving the `.marko` file relative to that. - const [, nodeModuleName, relativeModulePath] = - modulePartsReg.exec(moduleName)!; - const { resolvedModule } = ts.nodeModuleNameResolver( - `${nodeModuleName}/package.json`, - containingFile, - options, - host, - resolutionCache, - redirectedReference, - ); - - if (resolvedModule) { - resolvedFileName = path.join( - resolvedModule.resolvedFileName, - "..", - relativeModulePath, - ); - } - } - - if (!resolvedModules) { - resolvedModules = []; - normalModuleLiterals = []; - for (let j = 0; j < i; j++) { - resolvedModules.push(undefined); - normalModuleLiterals.push(moduleLiterals[j]); - } - } - - if (resolvedFileName) { - if (isDefinitionFile(resolvedFileName)) { - if (!host.fileExists(resolvedFileName)) { - resolvedFileName = undefined; - } - } else { - const ext = getExt(resolvedFileName)!; - const definitionFile = `${resolvedFileName.slice( - 0, - -ext.length, - )}.d${ext}`; - if (host.fileExists(definitionFile)) { - resolvedFileName = definitionFile; - } else if (!host.fileExists(resolvedFileName)) { - resolvedFileName = undefined; - } - } - } - - resolvedModules.push({ - resolvedModule: resolvedFileName - ? { - resolvedFileName, - extension: processor.getScriptExtension(resolvedFileName), - isExternalLibraryImport: false, - } - : undefined, - }); - } else if (resolvedModules) { - resolvedModules.push(undefined); - normalModuleLiterals.push(moduleLiteral); - } - } - - const normalResolvedModules = normalModuleLiterals.length - ? resolveModuleNameLiterals( - normalModuleLiterals, - containingFile, - redirectedReference, - options, - containingSourceFile, - reusedNames, - ) - : undefined; - - if (resolvedModules) { - if (normalResolvedModules) { - for (let i = 0, j = 0; i < resolvedModules.length; i++) { - if (!resolvedModules[i]) { - resolvedModules[i] = normalResolvedModules[j++]; - } - } - } - return resolvedModules as readonly ts.ResolvedModuleWithFailedLookupLocations[]; - } else { - return normalResolvedModules!; - } - }; - } - - return host; - - function getProcessor(fileName: string) { - const ext = getExt(fileName); - return ext ? processors[ext] : undefined; - } -} diff --git a/packages/language-server/src/ts-plugin/index.ts b/packages/language-server/src/ts-plugin/index.ts index bdef48fc..a1ba490a 100644 --- a/packages/language-server/src/ts-plugin/index.ts +++ b/packages/language-server/src/ts-plugin/index.ts @@ -1,205 +1,13 @@ -import type ts from "typescript/lib/tsserverlibrary"; -import { Extracted, Processors, Project } from "@marko/language-tools"; -import { START_POSITION } from "../utils/constants"; -import { ExtractedSnapshot, patch } from "./host"; -import "../utils/project-defaults"; +import { createLanguageServicePlugin } from "@volar/typescript/lib/quickstart/createLanguageServicePlugin.js"; +import { addMarkoTypes, createMarkoLanguagePlugin } from "../language"; -const markoTaglibFilesReg = /[\\/]marko(?:-tag)\.json$/; -const getStartLineCharacter = () => START_POSITION; -// TODO: improve the import name for Marko components. - -export interface InitOptions { - typescript: typeof ts; -} - -export function init({ typescript: ts }: InitOptions): ts.server.PluginModule { +export const init = createLanguageServicePlugin((ts, info) => { return { - getExternalFiles(project) { - return project - .getFileNames(false, true) - .filter((it) => Processors.has(it)); - }, - create(info) { - const { - project: tsProject, - languageService: ls, - languageServiceHost: lsh, - } = info; - - try { - const { projectService: ps } = tsProject; - const extraExtensions = (ps as any)?.hostConfiguration - ?.extraFileExtensions; - - if (extraExtensions) { - let processedExtCount = 0; - for (const { extension } of extraExtensions) { - if (Processors.extensions.includes(extension)) processedExtCount++; - } - - if (processedExtCount !== Processors.extensions.length) { - // The first time we install the plugin we update the config to allow our processed extensions. - // This will cause the plugin to be called again, so we check that the extension is not already added. - ps.setHostConfiguration({ - extraFileExtensions: extraExtensions.concat( - Processors.extensions.map((extension) => ({ - extension, - isMixedContent: false, - scriptKind: ts.ScriptKind.Deferred, - })), - ), - }); - } - } - - const extractCache = new Map(); - patch( - ts, - (tsProject as ts.server.ConfiguredProject).canonicalConfigFilePath, - extractCache, - (tsProject as any).getModuleResolutionCache(), - lsh, - ps, - ); - - /** - * Here we invalidate our snapshot cache when TypeScript invalidates the file. - */ - const onSourceFileChanged = (ps as any).onSourceFileChanged?.bind(ps); - - if (onSourceFileChanged) { - (ps as any).onSourceFileChanged = ( - info: ts.server.ScriptInfo, - eventKind: ts.FileWatcherEventKind, - ) => { - if (markoTaglibFilesReg.test(info.fileName)) { - Project.clearCaches(); - } - - extractCache.delete(info.fileName); - return onSourceFileChanged(info, eventKind); - }; - } - - /** - * Whenever TypeScript requests line/character info we return with the source - * file line/character if it exists. - */ - const toLineColumnOffset = ( - ls.toLineColumnOffset || getStartLineCharacter - ).bind(ls); - ls.toLineColumnOffset = (fileName, pos) => { - if (pos === 0) return START_POSITION; - - const extracted = extractCache.get(fileName); - if (extracted) { - return extracted.sourcePositionAt(pos) || START_POSITION; - } - - return toLineColumnOffset(fileName, pos); - }; - - const findReferences = ls.findReferences.bind(ls); - ls.findReferences = (fileName, position) => { - const symbols = findReferences(fileName, position); - if (!symbols) return; - - const result: ts.ReferencedSymbol[] = []; - for (const symbol of symbols) { - let definition: ts.ReferencedSymbolDefinitionInfo | undefined = - symbol.definition; - const defExtracted = extractCache.get(definition.fileName); - - if (defExtracted) { - definition = mapTextSpans(defExtracted, definition); - if (!definition) continue; - } - - const references: ts.ReferencedSymbolEntry[] = []; - for (const reference of symbol.references) { - const refExtracted = extractCache.get(reference.fileName); - if (refExtracted) { - const updated = mapTextSpans(refExtracted, reference); - if (updated) references.push(updated); - } else { - references.push(reference); - } - } - - result.push({ - definition, - references, - }); - } - - return result; - }; - - const findRenameLocations = ls.findRenameLocations.bind(ls); - ls.findRenameLocations = ( - fileName, - position, - findInStrings, - findInComments, - preferences, - ) => { - const renames = findRenameLocations( - fileName, - position, - findInStrings, - findInComments, - preferences as any, - ); - if (!renames) return; - - const result: ts.RenameLocation[] = []; - for (const rename of renames) { - const extracted = extractCache.get(rename.fileName); - if (extracted) { - const updated = mapTextSpans(extracted, rename); - if (updated) result.push(updated); - } else { - result.push(rename); - } - } - - return result; - }; - } catch (err) { - console.error(err); - } - - return ls; + languagePlugins: [createMarkoLanguagePlugin(ts, (id) => id)], + setup(_language) { + const { languageServiceHost } = info; + const rootPath = languageServiceHost.getCurrentDirectory(); + addMarkoTypes(rootPath, ts, languageServiceHost); }, }; -} - -function mapTextSpans< - T extends { - textSpan: ts.TextSpan; - contextSpan?: ts.TextSpan; - }, ->(extracted: ExtractedSnapshot, data: T) { - const textSpan = sourceTextSpan(extracted, data.textSpan); - if (textSpan) { - return { - ...data, - textSpan, - contextSpan: - data.contextSpan && sourceTextSpan(extracted, data.contextSpan), - }; - } -} - -function sourceTextSpan( - extracted: Extracted, - { start, length }: ts.TextSpan, -): ts.TextSpan | undefined { - const sourceStart = extracted.sourceOffsetAt(start); - if (sourceStart !== undefined) { - return { - start: sourceStart, - length, - }; - } -} +}); diff --git a/packages/vscode/package.json b/packages/vscode/package.json index b1e54e52..cefed218 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -1,7 +1,7 @@ { "name": "marko-vscode", "description": "Marko Syntax Highlighting, Pretty-printing, TypeScript, IntelliSense, and more", - "version": "1.4.5", + "version": "2.0.0-alpha01", "activationEvents": [ "workspaceContains:**/*.marko" ], diff --git a/packages/vscode/release.mts b/packages/vscode/release.mts index a65f4535..d1dcbe4b 100644 --- a/packages/vscode/release.mts +++ b/packages/vscode/release.mts @@ -15,8 +15,8 @@ await fs.rename(workspacePkg, workspacePkgTmp); try { await exec( - "vsce package -o $dist && vsce publish --packagePath $dist && ovsx publish $dist", - { env: { ...process.env, dist } } + "vsce package -o $dist", // && vsce publish --packagePath $dist && ovsx publish $dist", + { env: { ...process.env, dist } }, ); } finally { await fs.rename(workspacePkgTmp, workspacePkg);