Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@
| Package | Description |
| ------- | ----------- |
| [`extensions/vscode`](./extensions/vscode) | [VS Code extension](https://marketplace.visualstudio.com/items?itemName=npmx-dev.vscode-npmx) for npmx |
| [`extensions/zed`](./extensions/zed) | Zed extension for npmx, backed by the shared language server |
| [`packages/shared`](./packages/shared) | Shared constants, types, and LSP protocol definitions |
| [`packages/language-core`](./packages/language-core) | Core logic: extractors, API clients, workspace context |
| [`packages/language-service`](./packages/language-service) | Volar language service plugins (hover, completion, diagnostics, etc.) |
| [`packages/language-server`](./packages/language-server) | Volar language server |

## Features

- **Hover Information** – Quick links to package details and documentation on [npmx.dev](https://npmx.dev), with provenance verification status.
- **Hover Information** – Quick links to package details and documentation on [npmx.dev](https://npmx.dev), with provenance verification status. VS Code uses codicons, while other editors use emoji icons.
- **Version Completion** – Autocomplete package versions with provenance filtering and prerelease exclusion support.
- **Workspace-Aware Resolution** – Dependencies in `package.json`, `pnpm-workspace.yaml`, and `.yarnrc.yml` are resolved from a shared workspace context, including npm, pnpm, yarn, and bun package managers plus root `package.json` catalogs and workspace references.
- **Diagnostics**
Expand All @@ -36,6 +37,11 @@
- Open [npmx.dev](https://npmx.dev) in external browser
- Open `node_modules` files on [npmx.dev](https://npmx.dev) code viewer with syntax highlighting (from editor title, editor context menu, explorer context menu, or command palette)

## Editor Support

- **VS Code** – Primary extension package with hover, completion, diagnostics, document links, catalog decorations, code actions, and commands.
- **Zed** – In-repo development extension using the same language server over stdio. It supports the shared LSP features and forwards `lsp.npmx.settings` as `npmx` workspace configuration.

## Related

- [npmx.dev](https://npmx.dev) – A fast, modern browser for the npm registry
Expand Down
11 changes: 8 additions & 3 deletions extensions/vscode/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { LanguageClient, TransportKind } from '@volar/vscode/node'
import { DEPENDENCY_FILE_GLOB } from 'npmx-language-core/constants'
import { displayName, extensionId } from 'npmx-shared/meta'
import { Hover, MarkdownString } from 'vscode'
import { registerRequests } from './request'

const SUPPORTED_LANGUAGES = [
'javascript',
Expand Down Expand Up @@ -68,12 +67,18 @@ export function launch(serverPath: string) {
synchronize: {
configurationSection: [displayName],
},
initializationOptions: {
npmx: {
clientFeatures: {
catalogInlayHints: false,
iconStyle: 'codicon',
},
},
},
diagnosticCollectionName: displayName,
outputChannelName: `${displayName} Language Server`,
},
)

registerRequests(client)

return { client, ready: client.start() }
}
25 changes: 0 additions & 25 deletions extensions/vscode/src/request.ts

This file was deleted.

3 changes: 3 additions & 0 deletions extensions/zed/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/target
/extension.wasm
/Cargo.lock
12 changes: 12 additions & 0 deletions extensions/zed/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[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"
Comment on lines +1 to +12
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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:


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

84 changes: 84 additions & 0 deletions extensions/zed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# npmx for Zed

This is the in-repo Zed extension for `npmx`. It runs the shared `npmx-language-server`
over stdio, so Zed gets the same core package intelligence used by the VS Code extension.

## Status

- Uses the shared `npmx-language-server`
- Targets local development from this monorepo first
- Defaults to `packages/language-server/dist/index.cjs`
- Launches the language server over `--stdio`
- Supports overriding the launched command through Zed `lsp.npmx.binary` settings
- Forwards `lsp.npmx.settings` to the language server as `npmx` workspace configuration

## Features

- Hover links to package pages and docs on [npmx.dev](https://npmx.dev)
- Emoji hover icons for non-VS Code editors
- Version completion with provenance and prerelease settings
- Diagnostics for upgrades, deprecations, replacements, vulnerabilities, dist tags, and engine mismatches
- Document links for package names
- Workspace-aware dependency resolution for npm, pnpm, yarn, and bun projects

## Local Development

1. Build the language server from the repo root with `pnpm build`.
2. In Zed, install `extensions/zed` as a dev extension.
3. If you want a custom launch command, configure `lsp.npmx.binary` in your Zed settings.

## Settings

Zed settings under `lsp.npmx.settings` are forwarded directly to the language server.
Use scoped npmx settings without the leading `npmx.` prefix:

```json
{
"lsp": {
"npmx": {
"settings": {
"hover": {
"enabled": true
},
"completion": {
"version": "provenance-only",
"excludePrerelease": true
},
"diagnostics": {
"upgrade": true,
"deprecation": true,
"replacement": true,
"vulnerability": true,
"distTag": true,
"engineMismatch": true
},
"packageLinks": "declared"
}
}
}
}
```

To override the launched language server command:

```json
{
"lsp": {
"npmx": {
"binary": {
"path": "node",
"arguments": [
"/absolute/path/to/vscode-npmx/packages/language-server/dist/index.cjs",
"--stdio"
]
}
}
}
}
```

## Notes

- Zed dev extensions require Rust installed via `rustup`; the Zed docs explicitly call out that Homebrew Rust will not work for dev extension compilation.
- This dev extension expects the repo-local language server bundle at `packages/language-server/dist/index.cjs`, so build the monorepo before installing it in Zed.
- If you override `lsp.npmx.binary`, make sure the launched server process still receives an LSP transport argument such as `--stdio`.
34 changes: 34 additions & 0 deletions extensions/zed/extension.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
id = "npmx"
name = "npmx"
description = "npmx language support for Zed"
version = "0.0.1"
schema_version = 1
authors = [ "Xat <i@xat.sh>" ]
repository = "https://github.com/npmx-dev/vscode-npmx/tree/main/extensions/zed"

[language_servers.npmx]
name = "npmx"
languages = [
"JSON",
"YAML",
"JavaScript",
"JSX",
"TypeScript",
"TSX",
"HTML",
"Vue",
"Astro",
"Svelte"
]

[language_servers.npmx.language_ids]
JSON = "json"
YAML = "yaml"
JavaScript = "javascript"
JSX = "javascriptreact"
TypeScript = "typescript"
TSX = "typescriptreact"
HTML = "html"
Vue = "vue"
Astro = "astro"
Svelte = "svelte"
70 changes: 70 additions & 0 deletions extensions/zed/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use zed_extension_api::{self as zed, serde_json, settings::LspSettings, LanguageServerId};

struct NpmxExtension;

impl NpmxExtension {
fn language_server_settings(
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> LspSettings {
LspSettings::for_worktree(language_server_id.as_ref(), worktree)
.ok()
.unwrap_or_default()
}

fn default_server_script() -> String {
format!(
"{}/../../packages/language-server/dist/index.cjs",
env!("CARGO_MANIFEST_DIR")
)
}
}

impl zed::Extension for NpmxExtension {
fn new() -> Self {
Self
}

fn language_server_command(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> zed::Result<zed::Command> {
let lsp_settings = Self::language_server_settings(language_server_id, worktree);
if let Some(binary) = lsp_settings.binary {
let command = match binary.path {
Some(path) => path,
None => zed::node_binary_path()?,
};
let args = binary.arguments.unwrap_or_default();
let env = worktree
.shell_env()
.into_iter()
.chain(binary.env.unwrap_or_default())
.collect();

return Ok(zed::Command { command, args, env });
}

Ok(zed::Command {
command: zed::node_binary_path()?,
args: vec![Self::default_server_script(), String::from("--stdio")],
env: worktree.shell_env().into_iter().collect(),
})
}

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
})))
}
Comment on lines +56 to +67
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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:


🏁 Script executed:

# First, check the full lib.rs to see which Extension trait methods are currently implemented
cd extensions/zed && wc -l src/lib.rs

Repository: 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.rs

Repository: 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.rs

Repository: 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 -5

Repository: 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.ts

Repository: 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 2

Repository: 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.rs

Repository: 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 } })))
}

}

zed::register_extension!(NpmxExtension);
2 changes: 2 additions & 0 deletions packages/language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"npmx-language-service": "workspace:*",
"npmx-shared": "workspace:*",
"ocache": "catalog:inline",
"package-manager-detector": "catalog:inline",
"vscode-uri": "catalog:lsp"
},
"inlinedDependencies": {
Expand All @@ -42,6 +43,7 @@
"ocache": "0.1.4",
"ofetch": "2.0.0-alpha.3",
"ohash": "2.0.11",
"package-manager-detector": "1.6.0",
"path-browserify": "1.0.1",
"request-light": "0.7.0",
"semver": "7.7.4",
Expand Down
47 changes: 36 additions & 11 deletions packages/language-server/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
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 { name, version } from '../package.json' with { type: 'json' }
import { registerRequests } from './request'
import { createWorkspaceState } from './workspace'
Expand All @@ -12,17 +14,21 @@ export function startServer() {

connection.listen()

connection.onInitialize((params) => ({
serverInfo: {
name,
version,
},
...server.initialize(
params,
createSimpleProject([]),
createNpmxLanguageServicePlugins(workspaceState),
),
}))
connection.onInitialize((params) => {
workspaceState.setClientFeatures(readClientFeatures(params.initializationOptions))

return {
serverInfo: {
name,
version,
},
...server.initialize(
params,
createSimpleProject([]),
createNpmxLanguageServicePlugins(workspaceState),
),
}
})
connection.onInitialized(() => {
connection.console.info('npmx language server initialized')

Expand All @@ -32,3 +38,22 @@ export function startServer() {

registerRequests(connection, workspaceState)
}

function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}

function readClientFeatures(value: unknown): ClientFeatures {
if (!isObject(value) || !isObject(value.npmx) || !isObject(value.npmx.clientFeatures))
return DEFAULT_CLIENT_FEATURES

const cf = value.npmx.clientFeatures
return {
catalogInlayHints: typeof cf.catalogInlayHints === 'boolean'
? cf.catalogInlayHints
: DEFAULT_CLIENT_FEATURES.catalogInlayHints,
iconStyle: cf.iconStyle === 'codicon' || cf.iconStyle === 'emoji'
? cf.iconStyle
: DEFAULT_CLIENT_FEATURES.iconStyle,
}
}
Loading