feat: add Zed extension and make language server editor-agnostic#125
feat: add Zed extension and make language server editor-agnostic#125withxat wants to merge 14 commits intonpmx-dev:mainfrom
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
📝 WalkthroughWalkthroughThis PR adds Zed editor extension support alongside existing VS Code integration. It relocates package manager detection from a VS Code-specific request handler to shared server-side logic using Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (4)
packages/language-server/src/server.ts (1)
1-4: Optional: collapse the two imports fromnpmx-language-service/types.Lines 1 and 4 both import from the same module; the type and value imports can be combined into a single statement using inline
typemarkers without violating the "type imports first" ordering rule.♻️ Proposed refactor
-import type { ClientFeatures } from 'npmx-language-service/types' import { createConnection, createServer, createSimpleProject } from '@volar/language-server/node' import { createNpmxLanguageServicePlugins } from 'npmx-language-service' -import { DEFAULT_CLIENT_FEATURES } from 'npmx-language-service/types' +import { type ClientFeatures, DEFAULT_CLIENT_FEATURES } from 'npmx-language-service/types'packages/language-server/src/workspace.ts (1)
17-32: Confirm the intent ofstopDir: rootPathand the implicit'deno' → 'npm'mapping.By default
detect()crawls upward to parent directories, but pinningstopDirtocwdhere effectively restricts detection to the workspace root only. That is likely what you want for a per-folderWorkspaceContext, but it does mean that opening, e.g., a sub-folder of a monorepo will silently fall back to'npm'rather than picking up the parent's lockfile. Worth confirming this matches your intended UX.Separately,
package-manager-detectorcan also return'deno'. Thedefaultbranch maps it (and any future agent) to'npm'. If'deno'is intentionally unsupported byPackageManager, consider an explicitcase 'deno':to make the fallback intent obvious and avoid a silent regression shouldPackageManagerever gain a'deno'member.packages/language-service/src/plugins/hover.ts (1)
10-35: Clean icon registry; small consolidation opportunity.The single
ICONSregistry plusiconLabel/iconTexthelpers nicely centralise the codicon-vs-emoji branching previously scattered through the renderer. The vs regular space split betweeniconLabel(used inside[…](…)link text where Markdown otherwise collapses whitespace) andiconText(plain text) is a sensible distinction.If you'd like to trim a few lines, the two helpers differ only in the separator character and could be unified, e.g.:
♻️ Optional consolidation
-function iconLabel(iconStyle: IconStyle, name: IconName, label: string): string { - const { codicon, emoji } = ICONS[name] - return iconStyle === 'codicon' - ? `$(${codicon}) ${label}` - : `${emoji} ${label}` -} - -function iconText(iconStyle: IconStyle, name: IconName, text: string): string { - const { codicon, emoji } = ICONS[name] - return iconStyle === 'codicon' - ? `$(${codicon}) ${text}` - : `${emoji} ${text}` -} +function renderIcon(iconStyle: IconStyle, name: IconName, text: string, sep: ' ' | ' ' = ' '): string { + const { codicon, emoji } = ICONS[name] + return iconStyle === 'codicon' ? `$(${codicon})${sep}${text}` : `${emoji} ${text}` +}Happy to leave as-is for clarity.
packages/language-service/src/config.ts (1)
90-115: Up to three sequentialworkspace/configurationround-trips pergetConfigcall.
getConfigis invoked on every hover/completion/diagnostic; in the worst case it now performs three sequentialgetConfiguration(...)LSP requests (section,spec.scopedKey,scopedConfigs.scope). VS Code clients typically resolve configuration synchronously from a local cache, but for Zed (and any client without aggressive caching) every miss is a real round-trip on the request thread, multiplied across feature calls.Two low-effort mitigations worth considering:
- Short-circuit when a known shape is expected (e.g., cache the detected "shape" per workspace after the first successful resolution and only query that source thereafter).
- Fetch the root
npmxobject once per request and serve all keys from it, falling back to per-key calls only if the root isundefined.Not a blocker — flagging because it's invisible from the diff but compounds quickly under load.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5d86122a-beae-4cda-b56c-61f68b3e21b1
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (20)
README.mdextensions/vscode/src/client.tsextensions/vscode/src/request.tsextensions/zed/.gitignoreextensions/zed/Cargo.tomlextensions/zed/README.mdextensions/zed/extension.tomlextensions/zed/src/lib.rspackages/language-server/package.jsonpackages/language-server/src/server.tspackages/language-server/src/workspace.test.tspackages/language-server/src/workspace.tspackages/language-server/tsdown.config.tspackages/language-service/src/config.tspackages/language-service/src/plugins/catalog.tspackages/language-service/src/plugins/hover.test.tspackages/language-service/src/plugins/hover.tspackages/language-service/src/types.tspackages/shared/src/protocol.tspnpm-workspace.yaml
💤 Files with no reviewable changes (1)
- extensions/vscode/src/request.ts
| [package] | ||
| name = "zed-npmx" | ||
| version = "0.0.1" | ||
| edition = "2021" | ||
| license = "MIT" | ||
|
|
||
| [lib] | ||
| crate-type = [ "cdylib" ] | ||
|
|
||
| [dependencies] | ||
| serde_json = "1" | ||
| zed_extension_api = "0.7.0" |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What is the latest released version of the zed_extension_api crate on crates.io and is 0.7.0 still compatible with current Zed releases?
💡 Result:
The latest released version of the zed_extension_api crate on crates.io is 0.7.0, published on 2025-09-12. 0.7.0 is compatible with Zed 0.205.x and older versions listed in the compatibility table (up to 0.205.x supports 0.0.1 - 0.7.0). Current Zed releases are in the 0.23x range (e.g., v0.230.0 on 2026-04-01, v0.232.2 latest), which are significantly newer. The README explicitly states "Extensions created using newer versions of the Zed extension API won't be compatible with older versions of Zed," implying forward compatibility issues. A newer extension API (v0.8.0+) with additional methods exists in Zed development (merged PR #48334 in March 2026), but not yet released on crates.io. Thus, 0.7.0 may not be fully compatible with current Zed releases, as they likely require or expect the newer API version.
Citations:
- 1: https://crates.io/crates/zed_extension_api
- 2: https://docs.rs/zed_extension_api/latest/zed_extension_api/
- 3: https://crates.io/crates/zed_extension_api/0.7.0
- 4: https://zed.dev/releases/stable/latest
- 5: https://github.com/zed-industries/zed/releases/tag/v0.219.4
- 6: https://github.com/zed-industries/zed/releases/tag/v0.222.2
- 7: https://github.com/zed-industries/zed/releases/tag/v0.230.0
- 8: https://zed.dev/releases/stable/0.230.0
- 9: extension_api: Add language server schema methods zed-industries/zed#48334
- 10: http://zed.dev/releases
Update zed_extension_api to a compatible version; optionally mark crate as publish = false.
The dependency zed_extension_api = "0.7.0" targets Zed 0.205.x and older. Current Zed releases are in the 0.23x range (as of April 2026). This version mismatch means the extension will likely not be compatible with current Zed releases. Whilst a newer API version (v0.8.0+) exists in Zed's development branch, it has not yet been released to crates.io. Verify the minimum required Zed version for this extension and update zed_extension_api accordingly.
Additionally, since zed-npmx is an in-repo extension only consumed by Zed (compiled to extension.wasm and registered via extension.toml), add publish = false to the manifest. This prevents accidental uploads to crates.io and silences cargo publish warnings about missing fields like description and repository.
♻️ Proposed change
[package]
name = "zed-npmx"
version = "0.0.1"
edition = "2021"
license = "MIT"
+publish = false
[lib]
crate-type = [ "cdylib" ]
[dependencies]
serde_json = "1"
-zed_extension_api = "0.7.0"
+zed_extension_api = "<updated version>"| fn language_server_workspace_configuration( | ||
| &mut self, | ||
| language_server_id: &LanguageServerId, | ||
| worktree: &zed::Worktree, | ||
| ) -> zed::Result<Option<serde_json::Value>> { | ||
| let settings = Self::language_server_settings(language_server_id, worktree); | ||
| let workspace_settings = settings.settings.unwrap_or_default(); | ||
|
|
||
| Ok(Some(serde_json::json!({ | ||
| "npmx": workspace_settings | ||
| }))) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
zed_extension_api 0.7 language_server_initialization_options trait method
💡 Result:
In zed_extension_api version 0.7.0, the language_server_initialization_options method is a provided method in the Extension trait. It allows extensions to customize the initialization options passed to a language server. Signature: fn language_server_initialization_options( &mut self, _language_server_id: &LanguageServerId, _worktree: &Worktree, ) -> Result<Option<serde_json::Value>> Purpose: Returns the initialization options (as an optional JSON Value) to pass to the specified language server. Defaults to Ok(None) if not overridden. Usage example: fn language_server_initialization_options( &mut self, _language_server_id: &LanguageServerId, _worktree: &Worktree, ) -> Result<Option<serde_json::Value>> { Ok(Some(json!({ "typescript": { "tsdk": "./node_modules/typescript/lib" }, "diagnostics": { "enable": true } }))) } This method is called by Zed when starting the language server for the given ID and worktree. Extensions implement it to provide custom LSP initializationOptions. Note: Compatible with Zed versions 0.205.x. Newer schema methods like language_server_initialization_options_schema are for v0.8+.
Citations:
- 1: https://docs.rs/zed_extension_api/latest/zed_extension_api/trait.Extension.html
- 2: https://docs.rs/zed_extension_api/latest/src/zed_extension_api/extension_api.rs.html
- 3: https://www.mintlify.com/Glass-HQ/Glass/extensions/getting-started
- 4: https://context7.com/mrtomatepng/zed_extension_api/llms.txt
- 5: https://crates.io/crates/zed_extension_api/0.7.0
🏁 Script executed:
# First, check the full lib.rs to see which Extension trait methods are currently implemented
cd extensions/zed && wc -l src/lib.rsRepository: npmx-dev/vscode-npmx
Length of output: 78
🏁 Script executed:
# Check if language_server_initialization_options is already implemented in lib.rs
rg "language_server_initialization_options" extensions/zed/src/lib.rsRepository: npmx-dev/vscode-npmx
Length of output: 46
🏁 Script executed:
# Check what methods are currently implemented in the Extension trait
rg -A 2 "fn language_server" extensions/zed/src/lib.rsRepository: npmx-dev/vscode-npmx
Length of output: 396
🏁 Script executed:
# Find the language-server directory and check server.ts
find . -name "server.ts" -type f | head -5Repository: npmx-dev/vscode-npmx
Length of output: 105
🏁 Script executed:
# Check server.ts around line 47 to see how clientFeatures is read
sed -n '40,60p' packages/language-server/src/server.tsRepository: npmx-dev/vscode-npmx
Length of output: 715
🏁 Script executed:
# Search for readClientFeatures and DEFAULT_CLIENT_FEATURES in server.ts
rg "readClientFeatures|DEFAULT_CLIENT_FEATURES|clientFeatures|initializationOptions" packages/language-server/src/server.ts -A 2 -B 2Repository: npmx-dev/vscode-npmx
Length of output: 1093
🏁 Script executed:
# View the full lib.rs file since it's only 70 lines
cat -n extensions/zed/src/lib.rsRepository: npmx-dev/vscode-npmx
Length of output: 2711
Implement language_server_initialization_options to allow Zed users to customise clientFeatures.
The server reads ClientFeatures exclusively from params.initializationOptions.npmx.clientFeatures (server.ts), but this extension only implements language_server_workspace_configuration. These are separate channels — workspace configuration data does not reach initializationOptions. As a result, even if a user sets lsp.npmx.settings.clientFeatures.iconStyle = "codicon", the server will not see it and remains locked to DEFAULT_CLIENT_FEATURES (emoji, catalogInlayHints: true).
The defaults match the README, so this is not broken — but consider implementing language_server_initialization_options to let Zed users opt out of inlay hints or choose a different icon style without forking the extension.
♻️ Sketch
fn language_server_initialization_options(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> zed::Result<Option<serde_json::Value>> {
let settings = Self::language_server_settings(language_server_id, worktree);
let workspace_settings = settings.settings.unwrap_or_default();
let client_features = workspace_settings
.get("clientFeatures")
.cloned()
.unwrap_or(serde_json::json!({ "iconStyle": "emoji", "catalogInlayHints": true }));
Ok(Some(serde_json::json!({ "npmx": { "clientFeatures": client_features } })))
}| import { GET_PACKAGE_MANAGER_METHOD } from 'npmx-shared/protocol' | ||
| import { DEFAULT_CLIENT_FEATURES } from 'npmx-language-service/types' | ||
| import { defineCachedFunction } from 'ocache' | ||
| import { detect } from 'package-manager-detector' |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the exports field of the installed package-manager-detector to confirm `detect` is reachable from the package root.
fd -t f 'package.json' -p 'node_modules/package-manager-detector' --no-ignore | head -n 5
fd -t f -g 'package-manager-detector' -d 6 --no-ignore 2>/dev/null | head -n 20
# Also check the declared dependency version to look up its exports map on npm if needed.
rg -n --no-heading -g '**/package.json' '"package-manager-detector"\s*:'Repository: npmx-dev/vscode-npmx
Length of output: 380
🌐 Web query:
package-manager-detector 1.6.0 npm exports subpath
💡 Result:
The npm package "package-manager-detector" version 1.6.0 (published Nov 27, 2025) defines subpath exports in its package.json "exports" field. These allow importing specific modules via subpath imports: - "./commands" → "./dist/commands.mjs" - "./detect" → "./dist/detect.mjs" - "./constants" → "./dist/constants.mjs" The main entry is "." → "./dist/index.mjs". This enables usage like: import { detect } from 'package-manager-detector/detect'; import { resolveCommand } from 'package-manager-detector/commands'; import { ... } from 'package-manager-detector/constants'; The package.json from the main branch shows version "1.6.0" with these exact exports, matching the published tarball structure (dist/ files listed). No changes noted in releases up to 1.6.0.
Citations:
- 1: https://www.npmjs.com/package/package-manager-detector
- 2: https://registry.npmjs.org/package-manager-detector
- 3: https://github.com/antfu-collective/package-manager-detector
- 4: https://github.com/antfu-collective/package-manager-detector/blob/main/package.json
- 5: https://www.npmjs.com/package/package-manager-detector?activeTab=dependencies
🌐 Web query:
package-manager-detector index.mjs main entry exports detect
💡 Result:
package-manager-detector is a Node.js package by Anthony Fu (antfu-collective) that detects the package manager (npm, yarn, pnpm, deno, bun) used in a project by checking lockfiles, package.json packageManager field, devEngines.packageManager, and install metadata, crawling up directories if needed. index.mjs is the main entry point at ./dist/index.mjs (281B), specified in package.json as "main": "./dist/index.mjs", "exports": { ".": "./dist/index.mjs", ... }. It likely re-exports key APIs like detect for filesystem detection and getUserAgent for runtime detection. Usage examples: import { detect } from 'package-manager-detector/detect' // or from main const pm = await detect // returns detected package manager info Other sub-exports: - ./detect -> ./dist/detect.mjs (main detect logic) - ./commands -> ./dist/commands.mjs (resolveCommand for PM commands) - ./constants -> ./dist/constants.mjs Built with unbuild (TypeScript ESM), src likely has index.ts re-exporting from submodules like detect.ts, commands.ts. Official sources: GitHub repo, npm page, package.json.
Citations:
- 1: https://www.npmjs.com/package/package-manager-detector
- 2: https://github.com/antfu-collective/package-manager-detector
- 3: https://registry.npmjs.org/package-manager-detector
- 4: https://github.com/antfu-collective/package-manager-detector/blob/main/package.json
- 5: https://npm.io/package/package-manager-detector
- 6: https://github.com/antfu-collective/package-manager-detector/blob/main/README.md
- 7: https://github.com/antfu-collective/package-manager-detector/blob/main/src/commands.ts
Use the documented subpath import for detect.
The official API for package-manager-detector@1.6.0 uses subpath exports. Replace import { detect } from 'package-manager-detector' with import { detect } from 'package-manager-detector/detect' to follow the package's documented import pattern and ensure compatibility.
| return [{ | ||
| position: document.positionAt(specEnd), | ||
| label: ` ${dependency.resolvedSpec}`, | ||
| paddingLeft: true, | ||
| }] |
There was a problem hiding this comment.
Minor: redundant spacing — label has a leading space and paddingLeft: true.
paddingLeft already renders padding before the hint, so prefixing the label with a literal space results in a double gap. Drop one of them.
♻️ Proposed fix
return [{
position: document.positionAt(specEnd),
- label: ` ${dependency.resolvedSpec}`,
+ label: dependency.resolvedSpec,
paddingLeft: true,
}]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return [{ | |
| position: document.positionAt(specEnd), | |
| label: ` ${dependency.resolvedSpec}`, | |
| paddingLeft: true, | |
| }] | |
| return [{ | |
| position: document.positionAt(specEnd), | |
| label: dependency.resolvedSpec, | |
| paddingLeft: true, | |
| }] |
Summary
npmx-language-serverover stdio, giving Zed users hover, version completion, diagnostics, document links, and catalog resolution.package-manager-detectorreplaces thenpmx/getPackageManagerrequest, and a newClientFeatureschannel lets each editor opt into codicons, catalog inlay hints, etc.lsp.npmx.settings.*shape works alongside VS Code's namespaced keys.What changed
New: Zed extension (
extensions/zed/)wasm32-wasip2, built againstzed_extension_api0.7.node packages/language-server/dist/index.cjs --stdioby default; respectslsp.npmx.binaryfor full overrides, mergingworktree.shell_env()with user-supplied env.lsp.npmx.settingsas workspace configuration under thenpmxnamespace.Language server: editor-agnostic
npmx/getPackageManagerrequest that depended on VS Code'snpm.packageManagercommand. Now usespackage-manager-detector, bundled into the dist.ClientFeatures(negotiated viainitializationOptions):iconStyle: 'codicon' | 'emoji'— VS Code uses codicons, others get emoji.catalogInlayHints: boolean— VS Code keeps its custom decorations; non-VS Code clients get LSP-native inlay hints.DEFAULT_CLIENT_FEATURESco-located with theClientFeaturesinterface inlanguage-service/types.ts.Language service
getConfigresolves keys in order: exact (npmx.foo.bar) → scoped (foo.bar) → nested undernpmx. Lets Zed users write flat config without the prefix.(useCodicons, codicon, emoji)triples at every call site.inlayHintProvideron the catalog plugin shows the resolved spec next tocatalog:fooreferences, gated onClientFeatures.catalogInlayHints.VS Code
iconStyle: 'codicon'andcatalogInlayHints: falseininitializationOptionsto preserve existing UX.extensions/vscode/src/request.ts.Tests
workspace.test.tscoversdetectPackageManagerFromProject(supported PMs, unsupported, no detection).hover.test.tssnapshotsrenderHoverMarkdownfor both icon styles plus the JSR fallback path.I'm not sure if it's too early to consider supporting editors beyond VS Code, but I've been using Zed and really enjoying it. I also love using npmx, so I thought this might be helpful.
I haven’t published the extension for Zed myself because I felt that might be stepping beyond my role and potentially disrespectful to the original maintainers, especially since expanding support to another editor is ultimately a project decision. I just wanted to share this in case it could be useful and help move things forward.
There are also quite a few choices in the code that reflect my personal preferences—such as using emoji or inlay hints—so please feel free to adjust or change anything as needed.