diff --git a/docs/users/features/lsp.md b/docs/users/features/lsp.md index bf6266fbc..c0ed7da9a 100644 --- a/docs/users/features/lsp.md +++ b/docs/users/features/lsp.md @@ -38,7 +38,7 @@ You need to have the language server for your programming language installed: ### .lsp.json File -You can configure language servers using a `.lsp.json` file in your project root. This follows the [Claude Code plugin LSP configuration format](https://code.claude.com/docs/en/plugins-reference#lsp-servers). +You can configure language servers using a `.lsp.json` file in your project root. This uses the language-keyed format described in the [Claude Code plugin LSP configuration reference](https://code.claude.com/docs/en/plugins-reference#lsp-servers). **Basic format:** @@ -57,28 +57,6 @@ You can configure language servers using a `.lsp.json` file in your project root } ``` -**Extended format with `languageServers` wrapper:** - -```json -{ - "languageServers": { - "typescript-language-server": { - "languages": [ - "typescript", - "javascript", - "typescriptreact", - "javascriptreact" - ], - "command": "typescript-language-server", - "args": ["--stdio"], - "transport": "stdio", - "initializationOptions": {}, - "settings": {} - } - } -} -``` - ### Configuration Options #### Required Fields @@ -346,7 +324,7 @@ Or check the LSP debugging guide at `packages/cli/LSP_DEBUGGING_GUIDE.md`. ## Claude Code Compatibility -Qwen Code supports Claude Code-style `.lsp.json` configuration files as defined in the [Claude Code plugins reference](https://code.claude.com/docs/en/plugins-reference#lsp-servers). If you're migrating from Claude Code, your existing LSP configuration should work with minimal changes. +Qwen Code supports Claude Code-style `.lsp.json` configuration files in the language-keyed format defined in the [Claude Code plugins reference](https://code.claude.com/docs/en/plugins-reference#lsp-servers). If you're migrating from Claude Code, use the language-as-key layout in your configuration. ### Configuration Format @@ -364,19 +342,7 @@ The recommended format follows Claude Code's specification: } ``` -The `languageServers` wrapper format is also supported: - -```json -{ - "languageServers": { - "gopls": { - "languages": ["go"], - "command": "gopls", - "args": ["serve"] - } - } -} -``` +Claude Code LSP plugins can also supply `lspServers` in `plugin.json` (or a referenced `.lsp.json`). Qwen Code loads those configs when the extension is enabled, and they must use the same language-keyed format. ## Best Practices diff --git a/package-lock.json b/package-lock.json index e3e7405e1..17963ad94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -596,6 +596,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -619,6 +620,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1933,6 +1935,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2447,7 +2450,6 @@ "hasInstallScript": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", @@ -2490,7 +2492,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2512,7 +2513,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2534,7 +2534,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2556,7 +2555,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2578,7 +2576,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2600,7 +2597,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2622,7 +2618,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2644,7 +2639,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2666,7 +2660,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2688,7 +2681,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2710,7 +2702,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2732,7 +2723,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2754,7 +2744,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10.0.0" }, @@ -2770,7 +2759,6 @@ "dev": true, "license": "Apache-2.0", "optional": true, - "peer": true, "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -2784,8 +2772,7 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -3428,6 +3415,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3878,19 +3866,13 @@ "version": "2.4.9", "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", "integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "kleur": "^3.0.3" } }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/qrcode-terminal": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/qrcode-terminal/-/qrcode-terminal-0.12.2.tgz", @@ -3918,6 +3900,7 @@ "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3928,6 +3911,7 @@ "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -4133,6 +4117,7 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -4907,6 +4892,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5314,8 +5300,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/array-includes": { "version": "3.1.9", @@ -5893,6 +5878,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -6645,7 +6631,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -7770,6 +7755,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8304,7 +8290,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -8366,7 +8351,6 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -8376,7 +8360,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -8386,7 +8369,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -8577,7 +8559,6 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", - "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -8596,7 +8577,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -8605,15 +8585,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/finalhandler/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -9602,8 +9580,7 @@ "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/import-fresh": { "version": "3.3.1", @@ -9700,6 +9677,7 @@ "resolved": "https://registry.npmjs.org/ink/-/ink-6.2.3.tgz", "integrity": "sha512-fQkfEJjKbLXIcVWEE3MvpYSnwtbbmRsmeNDNz1pIuOFlwE+UF2gsy228J36OXKZGWJWZJKUigphBSqCNMcARtg==", "license": "MIT", + "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", @@ -10695,6 +10673,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -11679,7 +11658,6 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -12980,8 +12958,7 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/path-type": { "version": "3.0.0", @@ -13141,6 +13118,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13654,6 +13632,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13664,6 +13643,7 @@ "integrity": "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -13697,6 +13677,7 @@ "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -14338,29 +14319,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/sass": { - "version": "1.94.2", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.94.2.tgz", - "integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", @@ -15759,6 +15717,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15938,7 +15897,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.20.3", @@ -15946,6 +15906,7 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -16140,6 +16101,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16448,7 +16410,6 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4.0" } @@ -16504,6 +16465,7 @@ "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", @@ -16617,6 +16579,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16630,6 +16593,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -17137,6 +17101,7 @@ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -17317,6 +17282,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -17329,7 +17295,6 @@ "@iarna/toml": "^2.2.5", "@modelcontextprotocol/sdk": "^1.25.1", "@qwen-code/qwen-code-core": "file:../core", - "@types/prompts": "^2.4.9", "@types/update-notifier": "^6.0.8", "ansi-regex": "^6.2.2", "command-exists": "^1.2.9", @@ -17373,6 +17338,7 @@ "@types/diff": "^7.0.2", "@types/dotenv": "^6.1.1", "@types/node": "^20.11.24", + "@types/prompts": "^2.4.9", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/semver": "^7.7.0", @@ -17416,6 +17382,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", @@ -18062,6 +18029,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.1.tgz", "integrity": "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", @@ -18473,6 +18441,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -19235,6 +19204,7 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -19732,6 +19702,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -20858,6 +20829,7 @@ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", @@ -21521,27 +21493,6 @@ "zod": "^3.25 || ^4" } }, - "packages/vscode-ide-companion/node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "packages/vscode-ide-companion/node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, "packages/vscode-ide-companion/node_modules/@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", @@ -21584,13 +21535,6 @@ "node": ">= 0.6" } }, - "packages/vscode-ide-companion/node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, "packages/vscode-ide-companion/node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -21671,40 +21615,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "packages/vscode-ide-companion/node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "packages/vscode-ide-companion/node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "packages/vscode-ide-companion/node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "packages/vscode-ide-companion/node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", diff --git a/packages/cli/src/services/lsp/LspConfigLoader.test.ts b/packages/cli/src/services/lsp/LspConfigLoader.test.ts new file mode 100644 index 000000000..2207aa5ea --- /dev/null +++ b/packages/cli/src/services/lsp/LspConfigLoader.test.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2025 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, afterEach } from 'vitest'; +import mock from 'mock-fs'; +import { LspConfigLoader } from './LspConfigLoader.js'; +import type { Extension } from '@qwen-code/qwen-code-core'; + +describe('LspConfigLoader extension configs', () => { + const workspaceRoot = '/workspace'; + const extensionPath = '/extensions/ts-plugin'; + + afterEach(() => { + mock.restore(); + }); + + it('loads inline lspServers config from extension', async () => { + const loader = new LspConfigLoader(workspaceRoot); + const extension = { + name: 'ts-plugin', + path: extensionPath, + config: { + lspServers: { + typescript: { + command: 'typescript-language-server', + args: ['--stdio'], + extensionToLanguage: { + '.ts': 'typescript', + }, + }, + }, + }, + } as Extension; + + const configs = await loader.loadExtensionConfigs([extension]); + + expect(configs).toHaveLength(1); + expect(configs[0]?.languages).toEqual(['typescript']); + expect(configs[0]?.command).toBe('typescript-language-server'); + expect(configs[0]?.args).toEqual(['--stdio']); + }); + + it('loads lspServers config from referenced file and hydrates variables', async () => { + mock({ + [extensionPath]: { + '.lsp.json': JSON.stringify({ + typescript: { + command: 'typescript-language-server', + args: ['--stdio'], + env: { + EXT_ROOT: '${CLAUDE_PLUGIN_ROOT}', + }, + extensionToLanguage: { + '.ts': 'typescript', + }, + }, + }), + }, + }); + + const loader = new LspConfigLoader(workspaceRoot); + const extension = { + name: 'ts-plugin', + path: extensionPath, + config: { + lspServers: './.lsp.json', + }, + } as Extension; + + const configs = await loader.loadExtensionConfigs([extension]); + + expect(configs).toHaveLength(1); + expect(configs[0]?.env?.EXT_ROOT).toBe(extensionPath); + }); +}); diff --git a/packages/cli/src/services/lsp/LspConfigLoader.ts b/packages/cli/src/services/lsp/LspConfigLoader.ts index a0c5cd08d..c82f5323a 100644 --- a/packages/cli/src/services/lsp/LspConfigLoader.ts +++ b/packages/cli/src/services/lsp/LspConfigLoader.ts @@ -7,6 +7,11 @@ import * as fs from 'node:fs'; import * as path from 'path'; import { pathToFileURL } from 'url'; +import { + recursivelyHydrateStrings, + type Extension, + type JsonValue, +} from '@qwen-code/qwen-code-core'; import type { LspInitializationOptions, LspServerConfig, @@ -18,9 +23,7 @@ export class LspConfigLoader { /** * Load user .lsp.json configuration. - * Supports two official formats: - * 1. Basic format: { "language": { "command": "...", "extensionToLanguage": {...} } } - * 2. LanguageServers format: { "languageServers": { "server-name": { "languages": [...], ... } } } + * Supports basic format: { "language": { "command": "...", "extensionToLanguage": {...} } } */ async loadUserConfigs(): Promise { const lspConfigPath = path.join(this.workspaceRoot, '.lsp.json'); @@ -39,10 +42,76 @@ export class LspConfigLoader { } /** - * Merge configs: built-in presets + user configs + compatibility layer + * Load LSP configurations declared by extensions (Claude plugins). + */ + async loadExtensionConfigs(extensions: Extension[]): Promise { + const configs: LspServerConfig[] = []; + + for (const extension of extensions) { + const lspServers = extension.config?.lspServers; + if (!lspServers) { + continue; + } + + const originBase = `extension ${extension.name}`; + if (typeof lspServers === 'string') { + const configPath = this.resolveExtensionConfigPath( + extension.path, + lspServers, + ); + if (!fs.existsSync(configPath)) { + console.warn( + `LSP config not found for ${originBase}: ${configPath}`, + ); + continue; + } + + try { + const configContent = fs.readFileSync(configPath, 'utf-8'); + const data = JSON.parse(configContent) as JsonValue; + const hydrated = this.hydrateExtensionLspConfig( + data, + extension.path, + ); + configs.push( + ...this.parseConfigSource( + hydrated, + `${originBase} (${configPath})`, + ), + ); + } catch (error) { + console.warn( + `Failed to load extension LSP config from ${configPath}:`, + error, + ); + } + } else if (this.isRecord(lspServers)) { + const hydrated = this.hydrateExtensionLspConfig( + lspServers as JsonValue, + extension.path, + ); + configs.push( + ...this.parseConfigSource( + hydrated, + `${originBase} (lspServers)`, + ), + ); + } else { + console.warn( + `LSP config for ${originBase} must be an object or a JSON file path.`, + ); + } + } + + return configs; + } + + /** + * Merge configs: built-in presets + extension configs + user configs */ mergeConfigs( detectedLanguages: string[], + extensionConfigs: LspServerConfig[], userConfigs: LspServerConfig[], ): LspServerConfig[] { // Built-in preset configurations @@ -51,17 +120,22 @@ export class LspConfigLoader { // Merge configs, user configs take priority const mergedConfigs = [...presets]; - for (const userConfig of userConfigs) { - // Find if there's a preset with the same name, if so replace it - const existingIndex = mergedConfigs.findIndex( - (c) => c.name === userConfig.name, - ); - if (existingIndex !== -1) { - mergedConfigs[existingIndex] = userConfig; - } else { - mergedConfigs.push(userConfig); + const applyConfigs = (configs: LspServerConfig[]) => { + for (const config of configs) { + // Find if there's a preset with the same name, if so replace it + const existingIndex = mergedConfigs.findIndex( + (c) => c.name === config.name, + ); + if (existingIndex !== -1) { + mergedConfigs[existingIndex] = config; + } else { + mergedConfigs.push(config); + } } - } + }; + + applyConfigs(extensionConfigs); + applyConfigs(userConfigs); return mergedConfigs; } @@ -155,7 +229,7 @@ export class LspConfigLoader { /** * Parse configuration source and extract server configs. - * Detects format based on presence of 'languageServers' key. + * Expects basic format keyed by language identifier. */ private parseConfigSource( source: unknown, @@ -167,31 +241,15 @@ export class LspConfigLoader { const configs: LspServerConfig[] = []; - // Determine format: languageServers wrapper vs basic format - const hasLanguageServersWrapper = this.isRecord(source['languageServers']); - const serverMap = hasLanguageServersWrapper - ? (source['languageServers'] as Record) - : source; - - for (const [key, spec] of Object.entries(serverMap)) { + for (const [key, spec] of Object.entries(source)) { if (!this.isRecord(spec)) { continue; } - // In basic format: key is language name, server name comes from command - // In languageServers format: key is server name, languages come from 'languages' array - const isBasicFormat = !hasLanguageServersWrapper && !spec['languages']; - - const languages = isBasicFormat - ? [key] - : (this.normalizeStringArray(spec['languages']) ?? - (typeof spec['languages'] === 'string' ? [spec['languages']] : [])); - - const name = isBasicFormat - ? typeof spec['command'] === 'string' - ? spec['command'] - : key - : key; + // In basic format: key is language name, server name comes from command. + const languages = [key]; + const name = + typeof spec['command'] === 'string' ? (spec['command'] as string) : key; const config = this.buildServerConfig(name, languages, spec, origin); if (config) { @@ -202,6 +260,28 @@ export class LspConfigLoader { return configs; } + private resolveExtensionConfigPath( + extensionPath: string, + configPath: string, + ): string { + return path.isAbsolute(configPath) + ? path.resolve(configPath) + : path.resolve(extensionPath, configPath); + } + + private hydrateExtensionLspConfig( + source: JsonValue, + extensionPath: string, + ): JsonValue { + return recursivelyHydrateStrings(source, { + extensionPath, + CLAUDE_PLUGIN_ROOT: extensionPath, + workspacePath: this.workspaceRoot, + '/': path.sep, + pathSeparator: path.sep, + }); + } + private buildServerConfig( name: string, languages: string[], diff --git a/packages/cli/src/services/lsp/NativeLspService.ts b/packages/cli/src/services/lsp/NativeLspService.ts index a7e12cfcf..a57ad3483 100644 --- a/packages/cli/src/services/lsp/NativeLspService.ts +++ b/packages/cli/src/services/lsp/NativeLspService.ts @@ -24,6 +24,7 @@ import type { LspSymbolInformation, LspTextEdit, LspWorkspaceEdit, + Extension, } from '@qwen-code/qwen-code-core'; import type { EventEmitter } from 'events'; import { LspConfigLoader } from './LspConfigLoader.js'; @@ -98,19 +99,35 @@ export class NativeLspService { // Detect languages in workspace const userConfigs = await this.configLoader.loadUserConfigs(); + const extensionConfigs = await this.configLoader.loadExtensionConfigs( + this.getActiveExtensions(), + ); const extensionOverrides = - this.configLoader.collectExtensionToLanguageOverrides(userConfigs); + this.configLoader.collectExtensionToLanguageOverrides([ + ...extensionConfigs, + ...userConfigs, + ]); const detectedLanguages = await this.languageDetector.detectLanguages(extensionOverrides); - // Merge configs: built-in presets + user .lsp.json + optional cclsp compatibility + // Merge configs: built-in presets + extension LSP configs + user .lsp.json const serverConfigs = this.configLoader.mergeConfigs( detectedLanguages, + extensionConfigs, userConfigs, ); this.serverManager.setServerConfigs(serverConfigs); } + private getActiveExtensions(): Extension[] { + const configWithExtensions = this.config as unknown as { + getActiveExtensions?: () => Extension[]; + }; + return typeof configWithExtensions.getActiveExtensions === 'function' + ? configWithExtensions.getActiveExtensions() + : []; + } + /** * Start all LSP servers */ diff --git a/packages/core/src/extension/claude-converter.test.ts b/packages/core/src/extension/claude-converter.test.ts index 9e74b07bf..84510a98f 100644 --- a/packages/core/src/extension/claude-converter.test.ts +++ b/packages/core/src/extension/claude-converter.test.ts @@ -43,6 +43,26 @@ describe('convertClaudeToQwenConfig', () => { expect(result.mcpServers).toBeUndefined(); }); + it('should preserve lspServers configuration', () => { + const claudeConfig: ClaudePluginConfig = { + name: 'lsp-plugin', + version: '1.0.0', + lspServers: { + typescript: { + command: 'typescript-language-server', + args: ['--stdio'], + extensionToLanguage: { + '.ts': 'typescript', + }, + }, + }, + }; + + const result = convertClaudeToQwenConfig(claudeConfig); + + expect(result.lspServers).toEqual(claudeConfig.lspServers); + }); + it('should throw error for missing name', () => { const invalidConfig = { version: '1.0.0', diff --git a/packages/core/src/extension/claude-converter.ts b/packages/core/src/extension/claude-converter.ts index 224a22b11..506bc804a 100644 --- a/packages/core/src/extension/claude-converter.ts +++ b/packages/core/src/extension/claude-converter.ts @@ -39,7 +39,7 @@ export interface ClaudePluginConfig { hooks?: string; mcpServers?: string | Record; outputStyles?: string | string[]; - lspServers?: string; + lspServers?: string | Record; } /** @@ -318,17 +318,12 @@ export function convertClaudeToQwenConfig( `[Claude Converter] Output styles are not yet supported in ${claudeConfig.name}`, ); } - if (claudeConfig.lspServers) { - console.warn( - `[Claude Converter] LSP servers are not yet supported in ${claudeConfig.name}`, - ); - } - // Direct field mapping - commands, skills, agents will be collected as folders return { name: claudeConfig.name, version: claudeConfig.version, mcpServers, + lspServers: claudeConfig.lspServers, }; } diff --git a/packages/core/src/extension/extensionManager.ts b/packages/core/src/extension/extensionManager.ts index 921d34739..72ffdb3df 100644 --- a/packages/core/src/extension/extensionManager.ts +++ b/packages/core/src/extension/extensionManager.ts @@ -100,6 +100,7 @@ export interface ExtensionConfig { name: string; version: string; mcpServers?: Record; + lspServers?: string | Record; contextFileName?: string | string[]; commands?: string | string[]; skills?: string | string[];