feat(lsp): support for loading lspServers configurations from extensions

This commit is contained in:
yiliang114 2026-01-27 11:21:29 +08:00
parent 0bff045d3d
commit 7ec79e6806
8 changed files with 278 additions and 211 deletions

View file

@ -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

168
package-lock.json generated
View file

@ -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",

View file

@ -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);
});
});

View file

@ -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<LspServerConfig[]> {
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<LspServerConfig[]> {
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<string, unknown>)
: 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[],

View file

@ -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
*/

View file

@ -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',

View file

@ -39,7 +39,7 @@ export interface ClaudePluginConfig {
hooks?: string;
mcpServers?: string | Record<string, MCPServerConfig>;
outputStyles?: string | string[];
lspServers?: string;
lspServers?: string | Record<string, unknown>;
}
/**
@ -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,
};
}

View file

@ -100,6 +100,7 @@ export interface ExtensionConfig {
name: string;
version: string;
mcpServers?: Record<string, MCPServerConfig>;
lspServers?: string | Record<string, unknown>;
contextFileName?: string | string[];
commands?: string | string[];
skills?: string | string[];