From e6b8b2df7aa8b96d8a28dc0302946816d7d6586e Mon Sep 17 00:00:00 2001 From: Xat Date: Fri, 24 Apr 2026 16:56:15 +0800 Subject: [PATCH 01/24] refactor(language-core): add package manager fallback detection --- packages/language-core/src/workspace.test.ts | 43 ++++++++++++- packages/language-core/src/workspace.ts | 63 ++++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/packages/language-core/src/workspace.test.ts b/packages/language-core/src/workspace.test.ts index 8b548b7..842d6f9 100644 --- a/packages/language-core/src/workspace.test.ts +++ b/packages/language-core/src/workspace.test.ts @@ -1,6 +1,6 @@ import type { WorkspaceAdapter } from './workspace' import { describe, expect, it } from 'vitest' -import { WorkspaceContext } from './workspace' +import { detectPackageManagerFromFiles, WorkspaceContext } from './workspace' describe('workspaceContext', () => { it('loads bun workspace catalogs from the root package.json', async () => { @@ -174,4 +174,45 @@ describe('workspaceContext', () => { }, ]) }) + + it('detects the package manager from package.json packageManager', async () => { + const adapter = { + async readFile() { + return `{ + "packageManager": "pnpm@10.0.0" + }` + }, + async fileExists(path: string) { + return path === '/repo/package.json' + }, + } + + expect(await detectPackageManagerFromFiles('/repo', adapter)).toBe('pnpm') + }) + + it('falls back to lockfiles when packageManager is not declared', async () => { + const adapter = { + async readFile() { + throw new Error('this test should not read package.json') + }, + async fileExists(path: string) { + return path === '/repo/pnpm-lock.yaml' + }, + } + + expect(await detectPackageManagerFromFiles('/repo', adapter)).toBe('pnpm') + }) + + it('falls back to npm when no package-manager hints exist', async () => { + const adapter = { + async readFile() { + throw new Error('this test should not read package.json') + }, + async fileExists() { + return false + }, + } + + expect(await detectPackageManagerFromFiles('/repo', adapter)).toBe('npm') + }) }) diff --git a/packages/language-core/src/workspace.ts b/packages/language-core/src/workspace.ts index 3379fca..3d05513 100644 --- a/packages/language-core/src/workspace.ts +++ b/packages/language-core/src/workspace.ts @@ -31,6 +31,69 @@ export interface WorkspaceAdapter { detectPackageManager: (rootPath: string) => Promise } +const PACKAGE_MANAGER_PATTERN = /^(bun|npm|pnpm|yarn)(?:@|$)/ +const PACKAGE_MANAGER_LOCKFILES: [PackageManager, string[]][] = [ + ['bun', ['bun.lock', 'bun.lockb']], + ['pnpm', ['pnpm-lock.yaml']], + ['yarn', ['yarn.lock']], + ['npm', ['package-lock.json', 'npm-shrinkwrap.json']], +] + +function normalizePackageManager(value: string | undefined): PackageManager | undefined { + if (!value) + return + + const match = PACKAGE_MANAGER_PATTERN.exec(value.trim()) + const packageManager = match?.[1] + switch (packageManager) { + case 'bun': + case 'npm': + case 'pnpm': + case 'yarn': + return packageManager + } +} + +export async function detectPackageManagerFromFiles( + rootPath: string, + adapter: Pick, +): Promise { + const manifestPath = join(rootPath, PACKAGE_JSON_BASENAME) + if (await adapter.fileExists(manifestPath)) { + try { + const parsed = JSON.parse(await adapter.readFile(manifestPath)) + const packageManager = isPackageManagerManifest(parsed) + ? normalizePackageManager(parsed.packageManager) + : undefined + if (packageManager) + return packageManager + } catch { + } + } + + for (const [packageManager, basenames] of PACKAGE_MANAGER_LOCKFILES) { + for (const basename of basenames) { + if (await adapter.fileExists(join(rootPath, basename))) + return packageManager + } + } + + if (await adapter.fileExists(join(rootPath, PNPM_WORKSPACE_BASENAME))) + return 'pnpm' + + if (await adapter.fileExists(join(rootPath, YARN_WORKSPACE_BASENAME))) + return 'yarn' + + return 'npm' +} + +function isPackageManagerManifest(value: unknown): value is { packageManager?: string } { + if (typeof value !== 'object' || value === null) + return false + + return !('packageManager' in value) || typeof value.packageManager === 'string' +} + function getWorkspaceFileBasename(packageManager: PackageManager): string | undefined { switch (packageManager) { case 'bun': From c9e210b36c18a6c3bfe2f08e07f8da01470fc062 Mon Sep 17 00:00:00 2001 From: Xat Date: Fri, 24 Apr 2026 16:56:26 +0800 Subject: [PATCH 02/24] refactor(language-service): support nested config fallbacks --- packages/language-service/src/config.ts | 133 +++++++++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/packages/language-service/src/config.ts b/packages/language-service/src/config.ts index a781dc1..b38c725 100644 --- a/packages/language-service/src/config.ts +++ b/packages/language-service/src/config.ts @@ -1,6 +1,135 @@ import type { LanguageServiceContext } from '@volar/language-service' import type { ConfigKey, ConfigKeyTypeMap } from 'npmx-shared/meta' +import { scopedConfigs } from 'npmx-shared/meta' -export async function getConfig(context: LanguageServiceContext, section: T): Promise { - return (await context.env.getConfiguration!(section))! +type ConfigValue = ConfigKeyTypeMap[ConfigKey] + +const defaultConfigs = { + 'npmx.hover.enabled': scopedConfigs.defaults['hover.enabled'], + 'npmx.completion.version': scopedConfigs.defaults['completion.version'], + 'npmx.completion.excludePrerelease': scopedConfigs.defaults['completion.excludePrerelease'], + 'npmx.diagnostics.upgrade': scopedConfigs.defaults['diagnostics.upgrade'], + 'npmx.diagnostics.deprecation': scopedConfigs.defaults['diagnostics.deprecation'], + 'npmx.diagnostics.replacement': scopedConfigs.defaults['diagnostics.replacement'], + 'npmx.diagnostics.vulnerability': scopedConfigs.defaults['diagnostics.vulnerability'], + 'npmx.diagnostics.distTag': scopedConfigs.defaults['diagnostics.distTag'], + 'npmx.diagnostics.engineMismatch': scopedConfigs.defaults['diagnostics.engineMismatch'], + 'npmx.packageLinks': scopedConfigs.defaults.packageLinks, + 'npmx.ignore.upgrade': scopedConfigs.defaults['ignore.upgrade'], + 'npmx.ignore.deprecation': scopedConfigs.defaults['ignore.deprecation'], + 'npmx.ignore.replacement': scopedConfigs.defaults['ignore.replacement'], + 'npmx.ignore.vulnerability': scopedConfigs.defaults['ignore.vulnerability'], +} satisfies { [K in ConfigKey]: ConfigKeyTypeMap[K] } + +export function getConfig(context: LanguageServiceContext, section: 'npmx.hover.enabled'): Promise +export function getConfig(context: LanguageServiceContext, section: 'npmx.completion.version'): Promise<'all' | 'provenance-only' | 'off'> +export function getConfig(context: LanguageServiceContext, section: 'npmx.completion.excludePrerelease'): Promise +export function getConfig(context: LanguageServiceContext, section: 'npmx.diagnostics.upgrade'): Promise +export function getConfig(context: LanguageServiceContext, section: 'npmx.diagnostics.deprecation'): Promise +export function getConfig(context: LanguageServiceContext, section: 'npmx.diagnostics.replacement'): Promise +export function getConfig(context: LanguageServiceContext, section: 'npmx.diagnostics.vulnerability'): Promise +export function getConfig(context: LanguageServiceContext, section: 'npmx.diagnostics.distTag'): Promise +export function getConfig(context: LanguageServiceContext, section: 'npmx.diagnostics.engineMismatch'): Promise +export function getConfig(context: LanguageServiceContext, section: 'npmx.packageLinks'): Promise<'off' | 'latest' | 'declared' | 'resolved'> +export function getConfig(context: LanguageServiceContext, section: 'npmx.ignore.upgrade'): Promise +export function getConfig(context: LanguageServiceContext, section: 'npmx.ignore.deprecation'): Promise +export function getConfig(context: LanguageServiceContext, section: 'npmx.ignore.replacement'): Promise +export function getConfig(context: LanguageServiceContext, section: 'npmx.ignore.vulnerability'): Promise +export async function getConfig(context: LanguageServiceContext, section: ConfigKey): Promise { + const getConfiguration = context.env.getConfiguration + const fallback = getDefaultConfig(section) + if (!getConfiguration) + return fallback + + const exact = validateConfig(await getConfiguration(section), section) + if (exact !== undefined) + return exact + + const scopedSection = section.slice(scopedConfigs.scope.length + 1) + const scoped = validateConfig(await getConfiguration(scopedSection), section) + if (scoped !== undefined) + return scoped + + const root = readConfigFromRoot(await getConfiguration(scopedConfigs.scope), section) + if (root !== undefined) + return root + + return fallback +} + +function getDefaultConfig(section: ConfigKey): ConfigValue { + return defaultConfigs[section] +} + +function validateConfig(value: unknown, section: ConfigKey): ConfigValue | undefined { + switch (section) { + case 'npmx.hover.enabled': + case 'npmx.completion.excludePrerelease': + case 'npmx.diagnostics.upgrade': + case 'npmx.diagnostics.deprecation': + case 'npmx.diagnostics.replacement': + case 'npmx.diagnostics.vulnerability': + case 'npmx.diagnostics.distTag': + case 'npmx.diagnostics.engineMismatch': + return typeof value === 'boolean' ? value : undefined + case 'npmx.completion.version': + return value === 'all' || value === 'provenance-only' || value === 'off' + ? value + : undefined + case 'npmx.packageLinks': + return value === 'off' || value === 'latest' || value === 'declared' || value === 'resolved' + ? value + : undefined + case 'npmx.ignore.upgrade': + case 'npmx.ignore.deprecation': + case 'npmx.ignore.replacement': + case 'npmx.ignore.vulnerability': + return isStringArray(value) ? value : undefined + } +} + +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} + +function isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.every((item) => typeof item === 'string') +} + +function readConfigFromRoot(value: unknown, section: ConfigKey): ConfigValue | undefined { + if (!isObject(value)) + return + + const root = isObject(value.npmx) ? value.npmx : value + + switch (section) { + case 'npmx.hover.enabled': + return validateConfig(isObject(root.hover) ? root.hover.enabled : undefined, section) + case 'npmx.completion.version': + return validateConfig(isObject(root.completion) ? root.completion.version : undefined, section) + case 'npmx.completion.excludePrerelease': + return validateConfig(isObject(root.completion) ? root.completion.excludePrerelease : undefined, section) + case 'npmx.diagnostics.upgrade': + return validateConfig(isObject(root.diagnostics) ? root.diagnostics.upgrade : undefined, section) + case 'npmx.diagnostics.deprecation': + return validateConfig(isObject(root.diagnostics) ? root.diagnostics.deprecation : undefined, section) + case 'npmx.diagnostics.replacement': + return validateConfig(isObject(root.diagnostics) ? root.diagnostics.replacement : undefined, section) + case 'npmx.diagnostics.vulnerability': + return validateConfig(isObject(root.diagnostics) ? root.diagnostics.vulnerability : undefined, section) + case 'npmx.diagnostics.distTag': + return validateConfig(isObject(root.diagnostics) ? root.diagnostics.distTag : undefined, section) + case 'npmx.diagnostics.engineMismatch': + return validateConfig(isObject(root.diagnostics) ? root.diagnostics.engineMismatch : undefined, section) + case 'npmx.packageLinks': + return validateConfig(root.packageLinks, section) + case 'npmx.ignore.upgrade': + return validateConfig(isObject(root.ignore) ? root.ignore.upgrade : undefined, section) + case 'npmx.ignore.deprecation': + return validateConfig(isObject(root.ignore) ? root.ignore.deprecation : undefined, section) + case 'npmx.ignore.replacement': + return validateConfig(isObject(root.ignore) ? root.ignore.replacement : undefined, section) + case 'npmx.ignore.vulnerability': + return validateConfig(isObject(root.ignore) ? root.ignore.vulnerability : undefined, section) + } } From 41d29a89e7eba86879202233e90818c1e74acc68 Mon Sep 17 00:00:00 2001 From: Xat Date: Fri, 24 Apr 2026 16:56:33 +0800 Subject: [PATCH 03/24] feat(zed): add dev extension launcher --- extensions/zed/.gitignore | 3 ++ extensions/zed/Cargo.toml | 12 +++++ extensions/zed/README.md | 23 +++++++++ extensions/zed/extension.toml | 34 +++++++++++++ extensions/zed/src/lib.rs | 94 +++++++++++++++++++++++++++++++++++ 5 files changed, 166 insertions(+) create mode 100644 extensions/zed/.gitignore create mode 100644 extensions/zed/Cargo.toml create mode 100644 extensions/zed/README.md create mode 100644 extensions/zed/extension.toml create mode 100644 extensions/zed/src/lib.rs diff --git a/extensions/zed/.gitignore b/extensions/zed/.gitignore new file mode 100644 index 0000000..9d531f2 --- /dev/null +++ b/extensions/zed/.gitignore @@ -0,0 +1,3 @@ +/target +/extension.wasm +/Cargo.lock diff --git a/extensions/zed/Cargo.toml b/extensions/zed/Cargo.toml new file mode 100644 index 0000000..f30e741 --- /dev/null +++ b/extensions/zed/Cargo.toml @@ -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" diff --git a/extensions/zed/README.md b/extensions/zed/README.md new file mode 100644 index 0000000..2445737 --- /dev/null +++ b/extensions/zed/README.md @@ -0,0 +1,23 @@ +# npmx for Zed + +This is the in-repo Zed port of the `npmx` VS Code extension. + +Current 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 + +For 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. + +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`. diff --git a/extensions/zed/extension.toml b/extensions/zed/extension.toml new file mode 100644 index 0000000..6bca92d --- /dev/null +++ b/extensions/zed/extension.toml @@ -0,0 +1,34 @@ +id = "npmx" +name = "npmx" +description = "npmx language support for Zed" +version = "0.0.1" +schema_version = 1 +authors = [ "Xat " ] +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" diff --git a/extensions/zed/src/lib.rs b/extensions/zed/src/lib.rs new file mode 100644 index 0000000..05e82d6 --- /dev/null +++ b/extensions/zed/src/lib.rs @@ -0,0 +1,94 @@ +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 { + let lsp_settings = Self::language_server_settings(language_server_id, worktree); + if let Some(binary) = lsp_settings.binary { + let command = binary + .path + .unwrap_or_else(|| zed::node_binary_path().unwrap_or_else(|_| String::from("node"))); + let args = binary.arguments.unwrap_or_default(); + let env = binary.env.unwrap_or_default().into_iter().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_initialization_options( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> zed::Result> { + let settings = Self::language_server_settings(language_server_id, worktree); + let initialization_options = settings.initialization_options.unwrap_or_default(); + + let merged = match initialization_options { + serde_json::Value::Object(mut root) => { + let npmx = root + .remove("npmx") + .and_then(|value| value.as_object().cloned()) + .unwrap_or_default(); + let mut npmx = npmx; + npmx.insert(String::from("editor"), serde_json::Value::String(String::from("zed"))); + root.insert(String::from("npmx"), serde_json::Value::Object(npmx)); + serde_json::Value::Object(root) + } + _ => serde_json::json!({ + "npmx": { + "editor": "zed" + } + }), + }; + + Ok(Some(merged)) + } + + fn language_server_workspace_configuration( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> zed::Result> { + 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 + }))) + } +} + +zed::register_extension!(NpmxExtension); From 8baf11f35ee5bca2f1183f95ae543da662d55bd0 Mon Sep 17 00:00:00 2001 From: Xat Date: Fri, 24 Apr 2026 16:56:42 +0800 Subject: [PATCH 04/24] refactor(language-service): preserve editor-specific ux --- packages/language-server/src/server.ts | 63 ++++++++++--- packages/language-server/src/workspace.ts | 91 ++++++++++++++++++- .../language-service/src/plugins/catalog.ts | 32 +++++++ .../language-service/src/plugins/hover.ts | 33 ++++--- packages/language-service/src/types.ts | 3 + 5 files changed, 197 insertions(+), 25 deletions(-) diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index f04fc77..43698c6 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -1,9 +1,12 @@ +import type { IWorkspaceState } from 'npmx-language-service/types' import { createConnection, createServer, createSimpleProject } from '@volar/language-server/node' import { createNpmxLanguageServicePlugins } from 'npmx-language-service' import { name, version } from '../package.json' with { type: 'json' } import { registerRequests } from './request' import { createWorkspaceState } from './workspace' +type EditorFlavor = ReturnType + export function startServer() { const connection = createConnection() const server = createServer(connection) @@ -12,17 +15,21 @@ export function startServer() { connection.listen() - connection.onInitialize((params) => ({ - serverInfo: { - name, - version, - }, - ...server.initialize( - params, - createSimpleProject([]), - createNpmxLanguageServicePlugins(workspaceState), - ), - })) + connection.onInitialize((params) => { + workspaceState.setEditorFlavor(detectEditorFlavor(params)) + + return { + serverInfo: { + name, + version, + }, + ...server.initialize( + params, + createSimpleProject([]), + createNpmxLanguageServicePlugins(workspaceState), + ), + } + }) connection.onInitialized(() => { connection.console.info('npmx language server initialized') @@ -32,3 +39,37 @@ export function startServer() { registerRequests(connection, workspaceState) } + +function detectEditorFlavor(params: { + clientInfo?: { name?: string } + initializationOptions?: unknown +}): EditorFlavor { + const editor = readEditorFromInitializationOptions(params.initializationOptions) + if (editor) + return editor + + const clientName = params.clientInfo?.name?.toLowerCase() + if (clientName?.includes('zed')) + return 'zed' + if (clientName?.includes('visual studio code') || clientName?.includes('vscode')) + return 'vscode' + + return 'unknown' +} + +function readEditorFromInitializationOptions(value: unknown): EditorFlavor | undefined { + if (typeof value !== 'object' || value === null) + return + + if (!('npmx' in value)) + return + + const npmx = value.npmx + if (typeof npmx !== 'object' || npmx === null || !('editor' in npmx)) + return + + const editor = npmx.editor + return editor === 'vscode' || editor === 'zed' + ? editor + : undefined +} diff --git a/packages/language-server/src/workspace.ts b/packages/language-server/src/workspace.ts index d6c6518..85e0763 100644 --- a/packages/language-server/src/workspace.ts +++ b/packages/language-server/src/workspace.ts @@ -4,19 +4,93 @@ import type { IWorkspaceState } from 'npmx-language-service/types' import type { GetPackageManagerRequest } from 'npmx-shared/protocol' import { access, readFile } from 'node:fs/promises' import { RequestType } from '@volar/language-server' -import { DEPENDENCY_FILE_GLOB, PACKAGE_JSON_BASENAME } from 'npmx-language-core/constants' +import { + DEPENDENCY_FILE_GLOB, + PACKAGE_JSON_BASENAME, + PNPM_WORKSPACE_BASENAME, + YARN_WORKSPACE_BASENAME, +} from 'npmx-language-core/constants' import { isDependencyFile, isPackageManifest } from 'npmx-language-core/utils' import { WorkspaceContext } from 'npmx-language-core/workspace' import { GET_PACKAGE_MANAGER_METHOD } from 'npmx-shared/protocol' import { defineCachedFunction } from 'ocache' import { URI } from 'vscode-uri' +type EditorFlavor = ReturnType + const getPackageManagerRequestType = new RequestType< GetPackageManagerRequest.ParamsType, GetPackageManagerRequest.ResponseType, GetPackageManagerRequest.ErrorType >(GET_PACKAGE_MANAGER_METHOD) +const PACKAGE_MANAGER_PATTERN = /^(bun|npm|pnpm|yarn)(?:@|$)/ +const PACKAGE_MANAGER_LOCKFILES: [PackageManager, string[]][] = [ + ['bun', ['bun.lock', 'bun.lockb']], + ['pnpm', ['pnpm-lock.yaml']], + ['yarn', ['yarn.lock']], + ['npm', ['package-lock.json', 'npm-shrinkwrap.json']], +] + +function normalizePackageManager(value: string | undefined): PackageManager | undefined { + if (!value) + return + + const match = PACKAGE_MANAGER_PATTERN.exec(value.trim()) + const packageManager = match?.[1] + switch (packageManager) { + case 'bun': + case 'npm': + case 'pnpm': + case 'yarn': + return packageManager + } +} + +function isPackageManagerManifest(value: unknown): value is { packageManager?: string } { + if (typeof value !== 'object' || value === null) + return false + + return !('packageManager' in value) || typeof value.packageManager === 'string' +} + +async function detectPackageManagerFromFiles( + rootPath: string, + adapter: Pick, +): Promise { + const manifestPath = folderPath(rootPath, PACKAGE_JSON_BASENAME) + if (await adapter.fileExists(manifestPath)) { + try { + const parsed = JSON.parse(await adapter.readFile(manifestPath)) + const packageManager = isPackageManagerManifest(parsed) + ? normalizePackageManager(parsed.packageManager) + : undefined + if (packageManager) + return packageManager + } catch { + } + } + + for (const [packageManager, basenames] of PACKAGE_MANAGER_LOCKFILES) { + for (const basename of basenames) { + if (await adapter.fileExists(folderPath(rootPath, basename))) + return packageManager + } + } + + if (await adapter.fileExists(folderPath(rootPath, PNPM_WORKSPACE_BASENAME))) + return 'pnpm' + + if (await adapter.fileExists(folderPath(rootPath, YARN_WORKSPACE_BASENAME))) + return 'yarn' + + return 'npm' +} + +function folderPath(rootPath: string, basename: string) { + return `${rootPath}/${basename}` +} + function createLanguageServerAdapter(folderUri: URI, connection: Connection, server: LanguageServer): WorkspaceAdapter { return { async readFile(path: string): Promise { @@ -38,13 +112,15 @@ function createLanguageServerAdapter(folderUri: URI, connection: Connection, ser }, async detectPackageManager(rootPath): Promise { + const detected = await detectPackageManagerFromFiles(rootPath, this) + try { const result = await connection.sendRequest(getPackageManagerRequestType, { uri: rootPath, }) - return result || 'npm' + return result || detected } catch { - return 'npm' + return detected } }, } @@ -53,6 +129,7 @@ function createLanguageServerAdapter(folderUri: URI, connection: Connection, ser export class WorkspaceState implements IWorkspaceState { #connection: Connection #server: LanguageServer + #editorFlavor: EditorFlavor = 'unknown' constructor(connection: Connection, server: LanguageServer) { this.#connection = connection @@ -82,6 +159,14 @@ export class WorkspaceState implements IWorkspaceState { }) } + setEditorFlavor(editorFlavor: EditorFlavor) { + this.#editorFlavor = editorFlavor + } + + getEditorFlavor(): EditorFlavor { + return this.#editorFlavor + } + async #invalidateDependencyCacheByUri(uri: URI) { const folderUri = this.#getWorkspaceFolderUri(uri.toString()) if (!folderUri || !this.#cachedFolderPaths.has(folderUri.path)) diff --git a/packages/language-service/src/plugins/catalog.ts b/packages/language-service/src/plugins/catalog.ts index 2ca372a..623ab83 100644 --- a/packages/language-service/src/plugins/catalog.ts +++ b/packages/language-service/src/plugins/catalog.ts @@ -40,6 +40,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { triggerCharacters: [':'], }, definitionProvider: true, + inlayHintProvider: {}, }, create(context): LanguageServicePluginInstance { return { @@ -127,6 +128,37 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { }, }] }, + + async provideInlayHints(document, range) { + if (workspaceState.getEditorFlavor() === 'vscode') + return + + const dependencyFileUri = getDependencyFileUri(document.uri) + if (!dependencyFileUri) + return + + const dependencies = await workspaceState.getResolvedDependencies(document.uri) + if (!dependencies) + return + + const startOffset = document.offsetAt(range.start) + const endOffset = document.offsetAt(range.end) + + return dependencies.flatMap((dependency) => { + if (dependency.protocol !== 'catalog') + return [] + + const [specStart, specEnd] = dependency.specRange + if (specEnd < startOffset || specStart > endOffset) + return [] + + return [{ + position: document.positionAt(specEnd), + label: ` ${dependency.resolvedSpec}`, + paddingLeft: true, + }] + }) + }, } }, } diff --git a/packages/language-service/src/plugins/hover.ts b/packages/language-service/src/plugins/hover.ts index 74358b2..453066b 100644 --- a/packages/language-service/src/plugins/hover.ts +++ b/packages/language-service/src/plugins/hover.ts @@ -8,19 +8,21 @@ import { getConfig } from '../config' import { getResolvedDependencyAtOffset } from '../utils/document' export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { - const SPACER = ' ' - - async function renderHover(dep: DependencyInfo): Promise { + async function renderHover(dep: DependencyInfo, useCodicons: boolean): Promise { const { resolvedName, resolvedSpec, resolvedProtocol, packageInfo } = dep switch (resolvedProtocol) { case 'jsr': { - const jsrPackageLink = `[$(package)${SPACER}View on jsr.io](${jsrPackageUrl(resolvedName)})` + const jsrPackageLink = useCodicons + ? `[$(package) View on jsr.io](${jsrPackageUrl(resolvedName)})` + : `[View on jsr.io](${jsrPackageUrl(resolvedName)})` return { contents: { kind: 'markdown', - value: `${jsrPackageLink} | $(warning) Not on npmx`, + value: useCodicons + ? `${jsrPackageLink} | $(warning) Not on npmx.dev` + : `${jsrPackageLink} | Not on npmx.dev`, }, } satisfies Hover } @@ -30,7 +32,9 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { return { contents: { kind: 'markdown', - value: '$(warning) Unable to fetch package information', + value: useCodicons + ? '$(warning) Unable to fetch package information' + : 'Unable to fetch package information.', }, } satisfies Hover } @@ -38,11 +42,17 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { const resolvedVersion = await dep.resolvedVersion() let content = '' if (resolvedVersion && pkg.versionsMeta[resolvedVersion]?.provenance) { - content += `[$(verified)${SPACER}Verified provenance](${npmxPackageUrl(resolvedName, resolvedSpec)}#provenance)\n\n` + content += useCodicons + ? `[$(verified) Verified provenance](${npmxPackageUrl(resolvedName, resolvedSpec)}#provenance)\n\n` + : `[Verified provenance](${npmxPackageUrl(resolvedName, resolvedSpec)}#provenance)\n\n` } - const packageLink = `[$(package)${SPACER}View on npmx.dev](${npmxPackageUrl(resolvedName)})` - const docsLink = `[$(book)${SPACER}View docs on npmx.dev](${npmxDocsUrl(resolvedName, resolvedSpec)})` + const packageLink = useCodicons + ? `[$(package) View on npmx.dev](${npmxPackageUrl(resolvedName)})` + : `[View on npmx.dev](${npmxPackageUrl(resolvedName)})` + const docsLink = useCodicons + ? `[$(book) View docs on npmx.dev](${npmxDocsUrl(resolvedName, resolvedSpec)})` + : `[View docs on npmx.dev](${npmxDocsUrl(resolvedName, resolvedSpec)})` content += `${packageLink} | ${docsLink}` @@ -66,6 +76,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { async provideHover(document, position): Promise { if (!await getConfig(context, 'npmx.hover.enabled')) return + const useCodicons = workspaceState.getEditorFlavor() === 'vscode' const uri = URI.parse(document.uri) if (uri.scheme !== 'file') @@ -81,7 +92,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { if (!dep) return - return renderHover(dep) + return renderHover(dep, useCodicons) } else { const text = document.getText() const specifier = getImportSpecifierAtOffset(text, offset) @@ -95,7 +106,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { if (!dep) return - return renderHover(dep) + return renderHover(dep, useCodicons) } }, } diff --git a/packages/language-service/src/types.ts b/packages/language-service/src/types.ts index 4a086cd..84961af 100644 --- a/packages/language-service/src/types.ts +++ b/packages/language-service/src/types.ts @@ -1,6 +1,9 @@ import type { DependencyInfo, WorkspaceContext } from 'npmx-language-core/workspace' +export type EditorFlavor = 'unknown' | 'vscode' | 'zed' + export interface IWorkspaceState { + getEditorFlavor: () => EditorFlavor getWorkspaceContext: (uri: string) => Promise getResolvedDependencies: (uri: string) => Promise getResolvedDependenciesForContainingPackage: (uri: string) => Promise From e922aaacd7f9df5f6dacfaa102c3c1fb5de12432 Mon Sep 17 00:00:00 2001 From: Xat Date: Fri, 24 Apr 2026 17:24:16 +0800 Subject: [PATCH 05/24] refactor(workspace): adopt package-manager-detector fallback --- packages/language-core/src/workspace.test.ts | 43 +--------- packages/language-core/src/workspace.ts | 63 -------------- packages/language-server/package.json | 4 + .../language-server/src/workspace.test.ts | 36 ++++++++ packages/language-server/src/workspace.ts | 86 ++++--------------- packages/language-server/tsdown.config.ts | 2 + pnpm-workspace.yaml | 1 + 7 files changed, 60 insertions(+), 175 deletions(-) create mode 100644 packages/language-server/src/workspace.test.ts diff --git a/packages/language-core/src/workspace.test.ts b/packages/language-core/src/workspace.test.ts index 842d6f9..8b548b7 100644 --- a/packages/language-core/src/workspace.test.ts +++ b/packages/language-core/src/workspace.test.ts @@ -1,6 +1,6 @@ import type { WorkspaceAdapter } from './workspace' import { describe, expect, it } from 'vitest' -import { detectPackageManagerFromFiles, WorkspaceContext } from './workspace' +import { WorkspaceContext } from './workspace' describe('workspaceContext', () => { it('loads bun workspace catalogs from the root package.json', async () => { @@ -174,45 +174,4 @@ describe('workspaceContext', () => { }, ]) }) - - it('detects the package manager from package.json packageManager', async () => { - const adapter = { - async readFile() { - return `{ - "packageManager": "pnpm@10.0.0" - }` - }, - async fileExists(path: string) { - return path === '/repo/package.json' - }, - } - - expect(await detectPackageManagerFromFiles('/repo', adapter)).toBe('pnpm') - }) - - it('falls back to lockfiles when packageManager is not declared', async () => { - const adapter = { - async readFile() { - throw new Error('this test should not read package.json') - }, - async fileExists(path: string) { - return path === '/repo/pnpm-lock.yaml' - }, - } - - expect(await detectPackageManagerFromFiles('/repo', adapter)).toBe('pnpm') - }) - - it('falls back to npm when no package-manager hints exist', async () => { - const adapter = { - async readFile() { - throw new Error('this test should not read package.json') - }, - async fileExists() { - return false - }, - } - - expect(await detectPackageManagerFromFiles('/repo', adapter)).toBe('npm') - }) }) diff --git a/packages/language-core/src/workspace.ts b/packages/language-core/src/workspace.ts index 3d05513..3379fca 100644 --- a/packages/language-core/src/workspace.ts +++ b/packages/language-core/src/workspace.ts @@ -31,69 +31,6 @@ export interface WorkspaceAdapter { detectPackageManager: (rootPath: string) => Promise } -const PACKAGE_MANAGER_PATTERN = /^(bun|npm|pnpm|yarn)(?:@|$)/ -const PACKAGE_MANAGER_LOCKFILES: [PackageManager, string[]][] = [ - ['bun', ['bun.lock', 'bun.lockb']], - ['pnpm', ['pnpm-lock.yaml']], - ['yarn', ['yarn.lock']], - ['npm', ['package-lock.json', 'npm-shrinkwrap.json']], -] - -function normalizePackageManager(value: string | undefined): PackageManager | undefined { - if (!value) - return - - const match = PACKAGE_MANAGER_PATTERN.exec(value.trim()) - const packageManager = match?.[1] - switch (packageManager) { - case 'bun': - case 'npm': - case 'pnpm': - case 'yarn': - return packageManager - } -} - -export async function detectPackageManagerFromFiles( - rootPath: string, - adapter: Pick, -): Promise { - const manifestPath = join(rootPath, PACKAGE_JSON_BASENAME) - if (await adapter.fileExists(manifestPath)) { - try { - const parsed = JSON.parse(await adapter.readFile(manifestPath)) - const packageManager = isPackageManagerManifest(parsed) - ? normalizePackageManager(parsed.packageManager) - : undefined - if (packageManager) - return packageManager - } catch { - } - } - - for (const [packageManager, basenames] of PACKAGE_MANAGER_LOCKFILES) { - for (const basename of basenames) { - if (await adapter.fileExists(join(rootPath, basename))) - return packageManager - } - } - - if (await adapter.fileExists(join(rootPath, PNPM_WORKSPACE_BASENAME))) - return 'pnpm' - - if (await adapter.fileExists(join(rootPath, YARN_WORKSPACE_BASENAME))) - return 'yarn' - - return 'npm' -} - -function isPackageManagerManifest(value: unknown): value is { packageManager?: string } { - if (typeof value !== 'object' || value === null) - return false - - return !('packageManager' in value) || typeof value.packageManager === 'string' -} - function getWorkspaceFileBasename(packageManager: PackageManager): string | undefined { switch (packageManager) { case 'bun': diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 90f4e4c..2717dd6 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -25,6 +25,9 @@ "dev": "tsdown --watch", "build": "tsdown" }, + "dependencies": { + "package-manager-detector": "catalog:inline" + }, "devDependencies": { "@volar/language-server": "catalog:lsp", "npmx-language-core": "workspace:*", @@ -42,6 +45,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", diff --git a/packages/language-server/src/workspace.test.ts b/packages/language-server/src/workspace.test.ts new file mode 100644 index 0000000..2cc86bf --- /dev/null +++ b/packages/language-server/src/workspace.test.ts @@ -0,0 +1,36 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { detectPackageManagerFromProject } from './workspace' + +vi.mock('package-manager-detector', () => ({ + detect: vi.fn(), +})) + +const { detect } = await import('package-manager-detector') + +describe('detectPackageManagerFromProject', () => { + afterEach(() => { + vi.mocked(detect).mockReset() + }) + + it('returns supported package managers directly', async () => { + vi.mocked(detect).mockResolvedValue({ name: 'pnpm', agent: 'pnpm' }) + + await expect(detectPackageManagerFromProject('/repo')).resolves.toBe('pnpm') + expect(detect).toHaveBeenCalledWith({ + cwd: '/repo', + stopDir: '/repo', + }) + }) + + it('falls back to npm for unsupported detectors', async () => { + vi.mocked(detect).mockResolvedValue({ name: 'deno', agent: 'deno' }) + + await expect(detectPackageManagerFromProject('/repo')).resolves.toBe('npm') + }) + + it('falls back to npm when no package manager is detected', async () => { + vi.mocked(detect).mockResolvedValue(null) + + await expect(detectPackageManagerFromProject('/repo')).resolves.toBe('npm') + }) +}) diff --git a/packages/language-server/src/workspace.ts b/packages/language-server/src/workspace.ts index 85e0763..792599c 100644 --- a/packages/language-server/src/workspace.ts +++ b/packages/language-server/src/workspace.ts @@ -4,16 +4,12 @@ import type { IWorkspaceState } from 'npmx-language-service/types' import type { GetPackageManagerRequest } from 'npmx-shared/protocol' import { access, readFile } from 'node:fs/promises' import { RequestType } from '@volar/language-server' -import { - DEPENDENCY_FILE_GLOB, - PACKAGE_JSON_BASENAME, - PNPM_WORKSPACE_BASENAME, - YARN_WORKSPACE_BASENAME, -} from 'npmx-language-core/constants' +import { DEPENDENCY_FILE_GLOB, PACKAGE_JSON_BASENAME } from 'npmx-language-core/constants' import { isDependencyFile, isPackageManifest } from 'npmx-language-core/utils' import { WorkspaceContext } from 'npmx-language-core/workspace' import { GET_PACKAGE_MANAGER_METHOD } from 'npmx-shared/protocol' import { defineCachedFunction } from 'ocache' +import { detect } from 'package-manager-detector' import { URI } from 'vscode-uri' type EditorFlavor = ReturnType @@ -24,71 +20,21 @@ const getPackageManagerRequestType = new RequestType< GetPackageManagerRequest.ErrorType >(GET_PACKAGE_MANAGER_METHOD) -const PACKAGE_MANAGER_PATTERN = /^(bun|npm|pnpm|yarn)(?:@|$)/ -const PACKAGE_MANAGER_LOCKFILES: [PackageManager, string[]][] = [ - ['bun', ['bun.lock', 'bun.lockb']], - ['pnpm', ['pnpm-lock.yaml']], - ['yarn', ['yarn.lock']], - ['npm', ['package-lock.json', 'npm-shrinkwrap.json']], -] - -function normalizePackageManager(value: string | undefined): PackageManager | undefined { - if (!value) - return - - const match = PACKAGE_MANAGER_PATTERN.exec(value.trim()) - const packageManager = match?.[1] - switch (packageManager) { +export async function detectPackageManagerFromProject(rootPath: string): Promise { + const result = await detect({ + cwd: rootPath, + stopDir: rootPath, + }) + + switch (result?.name) { case 'bun': case 'npm': case 'pnpm': case 'yarn': - return packageManager - } -} - -function isPackageManagerManifest(value: unknown): value is { packageManager?: string } { - if (typeof value !== 'object' || value === null) - return false - - return !('packageManager' in value) || typeof value.packageManager === 'string' -} - -async function detectPackageManagerFromFiles( - rootPath: string, - adapter: Pick, -): Promise { - const manifestPath = folderPath(rootPath, PACKAGE_JSON_BASENAME) - if (await adapter.fileExists(manifestPath)) { - try { - const parsed = JSON.parse(await adapter.readFile(manifestPath)) - const packageManager = isPackageManagerManifest(parsed) - ? normalizePackageManager(parsed.packageManager) - : undefined - if (packageManager) - return packageManager - } catch { - } - } - - for (const [packageManager, basenames] of PACKAGE_MANAGER_LOCKFILES) { - for (const basename of basenames) { - if (await adapter.fileExists(folderPath(rootPath, basename))) - return packageManager - } + return result.name + default: + return 'npm' } - - if (await adapter.fileExists(folderPath(rootPath, PNPM_WORKSPACE_BASENAME))) - return 'pnpm' - - if (await adapter.fileExists(folderPath(rootPath, YARN_WORKSPACE_BASENAME))) - return 'yarn' - - return 'npm' -} - -function folderPath(rootPath: string, basename: string) { - return `${rootPath}/${basename}` } function createLanguageServerAdapter(folderUri: URI, connection: Connection, server: LanguageServer): WorkspaceAdapter { @@ -112,16 +58,16 @@ function createLanguageServerAdapter(folderUri: URI, connection: Connection, ser }, async detectPackageManager(rootPath): Promise { - const detected = await detectPackageManagerFromFiles(rootPath, this) - try { const result = await connection.sendRequest(getPackageManagerRequestType, { uri: rootPath, }) - return result || detected + if (result) + return result } catch { - return detected } + + return await detectPackageManagerFromProject(rootPath) }, } } diff --git a/packages/language-server/tsdown.config.ts b/packages/language-server/tsdown.config.ts index 8705473..bccb620 100644 --- a/packages/language-server/tsdown.config.ts +++ b/packages/language-server/tsdown.config.ts @@ -15,9 +15,11 @@ export default defineConfig({ }, minify: 'dce-only', deps: { + alwaysBundle: ['package-manager-detector'], onlyBundle: [ /^vscode-/, /^@volar\//, + 'package-manager-detector', 'request-light', 'path-browserify', 'semver', diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ea0646d..04e6f2b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -23,6 +23,7 @@ catalogs: jsonc-parser: ^3.3.1 ocache: ^0.1.4 ofetch: ^2.0.0-alpha.3 + package-manager-detector: ^1.6.0 path-browserify: ^1.0.1 reactive-vscode: ^1.0.1 semver: ^7.7.4 From a4ef65d58374f664a1815f3c92432aef87d4464c Mon Sep 17 00:00:00 2001 From: Xat Date: Mon, 27 Apr 2026 13:42:54 +0800 Subject: [PATCH 06/24] refactor(language-server): unify package manager detection --- extensions/vscode/src/client.ts | 3 --- extensions/vscode/src/request.ts | 25 ----------------------- packages/language-server/src/workspace.ts | 22 ++------------------ packages/shared/src/protocol.ts | 12 +---------- pnpm-lock.yaml | 7 +++++++ 5 files changed, 10 insertions(+), 59 deletions(-) delete mode 100644 extensions/vscode/src/request.ts diff --git a/extensions/vscode/src/client.ts b/extensions/vscode/src/client.ts index c17dcb0..090c901 100644 --- a/extensions/vscode/src/client.ts +++ b/extensions/vscode/src/client.ts @@ -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', @@ -73,7 +72,5 @@ export function launch(serverPath: string) { }, ) - registerRequests(client) - return { client, ready: client.start() } } diff --git a/extensions/vscode/src/request.ts b/extensions/vscode/src/request.ts deleted file mode 100644 index 561828a..0000000 --- a/extensions/vscode/src/request.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { BaseLanguageClient } from '@volar/vscode' -import type { GetPackageManagerRequest } from 'npmx-shared/protocol' -import { RequestType } from '@volar/vscode' -import { GET_PACKAGE_MANAGER_METHOD } from 'npmx-shared/protocol' -import { commands, Uri } from 'vscode' - -const getPackageManagerRequestType = new RequestType< - GetPackageManagerRequest.ParamsType, - GetPackageManagerRequest.ResponseType, - GetPackageManagerRequest.ErrorType ->(GET_PACKAGE_MANAGER_METHOD) - -export function registerRequests(client: BaseLanguageClient) { - client.onRequest( - getPackageManagerRequestType, - async (params): Promise => { - try { - const result = await commands.executeCommand('npm.packageManager', Uri.parse(params.uri)) - return result || 'npm' - } catch { - return 'npm' - } - }, - ) -} diff --git a/packages/language-server/src/workspace.ts b/packages/language-server/src/workspace.ts index 792599c..185e3fb 100644 --- a/packages/language-server/src/workspace.ts +++ b/packages/language-server/src/workspace.ts @@ -1,25 +1,16 @@ import type { Connection, LanguageServer } from '@volar/language-server' import type { DependencyInfo, PackageManager, WorkspaceAdapter } from 'npmx-language-core/workspace' import type { IWorkspaceState } from 'npmx-language-service/types' -import type { GetPackageManagerRequest } from 'npmx-shared/protocol' import { access, readFile } from 'node:fs/promises' -import { RequestType } from '@volar/language-server' import { DEPENDENCY_FILE_GLOB, PACKAGE_JSON_BASENAME } from 'npmx-language-core/constants' import { isDependencyFile, isPackageManifest } from 'npmx-language-core/utils' import { WorkspaceContext } from 'npmx-language-core/workspace' -import { GET_PACKAGE_MANAGER_METHOD } from 'npmx-shared/protocol' import { defineCachedFunction } from 'ocache' import { detect } from 'package-manager-detector' import { URI } from 'vscode-uri' type EditorFlavor = ReturnType -const getPackageManagerRequestType = new RequestType< - GetPackageManagerRequest.ParamsType, - GetPackageManagerRequest.ResponseType, - GetPackageManagerRequest.ErrorType ->(GET_PACKAGE_MANAGER_METHOD) - export async function detectPackageManagerFromProject(rootPath: string): Promise { const result = await detect({ cwd: rootPath, @@ -37,7 +28,7 @@ export async function detectPackageManagerFromProject(rootPath: string): Promise } } -function createLanguageServerAdapter(folderUri: URI, connection: Connection, server: LanguageServer): WorkspaceAdapter { +function createLanguageServerAdapter(folderUri: URI, server: LanguageServer): WorkspaceAdapter { return { async readFile(path: string): Promise { const uri = folderUri.with({ path }) @@ -58,15 +49,6 @@ function createLanguageServerAdapter(folderUri: URI, connection: Connection, ser }, async detectPackageManager(rootPath): Promise { - try { - const result = await connection.sendRequest(getPackageManagerRequestType, { - uri: rootPath, - }) - if (result) - return result - } catch { - } - return await detectPackageManagerFromProject(rootPath) }, } @@ -139,7 +121,7 @@ export class WorkspaceState implements IWorkspaceState { async (folderUri) => { const ctx = await WorkspaceContext.create( folderUri.path, - createLanguageServerAdapter(folderUri, this.#connection, this.#server), + createLanguageServerAdapter(folderUri, this.#server), ) this.#cachedFolderPaths.add(folderUri.path) diff --git a/packages/shared/src/protocol.ts b/packages/shared/src/protocol.ts index 8aa224f..64a8280 100644 --- a/packages/shared/src/protocol.ts +++ b/packages/shared/src/protocol.ts @@ -1,16 +1,6 @@ /* eslint-disable ts/no-namespace */ -import type { DependencyInfo, PackageManager } from 'npmx-language-core/workspace' - -export const GET_PACKAGE_MANAGER_METHOD = 'npmx/getPackageManager' - -export namespace GetPackageManagerRequest { - export interface ParamsType { - uri: string - } - export type ResponseType = PackageManager - export type ErrorType = never -} +import type { DependencyInfo } from 'npmx-language-core/workspace' export const GET_RESOLVED_DEPENDENCIES_METHOD = 'npmx/getResolvedDependencies' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b89a01..27fba0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,6 +52,9 @@ catalogs: ofetch: specifier: ^2.0.0-alpha.3 version: 2.0.0-alpha.3 + package-manager-detector: + specifier: ^1.6.0 + version: 1.6.0 path-browserify: specifier: ^1.0.1 version: 1.0.1 @@ -206,6 +209,10 @@ importers: version: 2.8.3 packages/language-server: + dependencies: + package-manager-detector: + specifier: catalog:inline + version: 1.6.0 devDependencies: '@volar/language-server': specifier: catalog:lsp From 3601a47c63dc91ec3adf106bb063ba5d2adf06e0 Mon Sep 17 00:00:00 2001 From: Xat Date: Mon, 27 Apr 2026 13:48:01 +0800 Subject: [PATCH 07/24] chore(language-server): align detector dependency placement --- packages/language-server/package.json | 4 +--- pnpm-lock.yaml | 7 +++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 2717dd6..0c68b67 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -25,15 +25,13 @@ "dev": "tsdown --watch", "build": "tsdown" }, - "dependencies": { - "package-manager-detector": "catalog:inline" - }, "devDependencies": { "@volar/language-server": "catalog:lsp", "npmx-language-core": "workspace:*", "npmx-language-service": "workspace:*", "npmx-shared": "workspace:*", "ocache": "catalog:inline", + "package-manager-detector": "catalog:inline", "vscode-uri": "catalog:lsp" }, "inlinedDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27fba0e..2ebd2a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -209,10 +209,6 @@ importers: version: 2.8.3 packages/language-server: - dependencies: - package-manager-detector: - specifier: catalog:inline - version: 1.6.0 devDependencies: '@volar/language-server': specifier: catalog:lsp @@ -229,6 +225,9 @@ importers: ocache: specifier: catalog:inline version: 0.1.4 + package-manager-detector: + specifier: catalog:inline + version: 1.6.0 vscode-uri: specifier: catalog:lsp version: 3.1.0 From bd342c73812fca54c2ef8ddb8ef91a9ec34fcc47 Mon Sep 17 00:00:00 2001 From: Xat Date: Mon, 27 Apr 2026 13:58:50 +0800 Subject: [PATCH 08/24] refactor(language-server): use client feature flags --- extensions/vscode/src/client.ts | 8 +++ extensions/zed/src/lib.rs | 29 ---------- packages/language-server/src/server.ts | 54 ++++++++----------- packages/language-server/src/workspace.ts | 17 +++--- .../language-service/src/plugins/catalog.ts | 2 +- .../language-service/src/plugins/hover.ts | 6 +-- packages/language-service/src/types.ts | 7 ++- 7 files changed, 50 insertions(+), 73 deletions(-) diff --git a/extensions/vscode/src/client.ts b/extensions/vscode/src/client.ts index 090c901..fd68bbe 100644 --- a/extensions/vscode/src/client.ts +++ b/extensions/vscode/src/client.ts @@ -67,6 +67,14 @@ export function launch(serverPath: string) { synchronize: { configurationSection: [displayName], }, + initializationOptions: { + npmx: { + clientFeatures: { + catalogInlayHints: false, + markdownIcons: true, + }, + }, + }, diagnosticCollectionName: displayName, outputChannelName: `${displayName} Language Server`, }, diff --git a/extensions/zed/src/lib.rs b/extensions/zed/src/lib.rs index 05e82d6..a091824 100644 --- a/extensions/zed/src/lib.rs +++ b/extensions/zed/src/lib.rs @@ -48,35 +48,6 @@ impl zed::Extension for NpmxExtension { }) } - fn language_server_initialization_options( - &mut self, - language_server_id: &LanguageServerId, - worktree: &zed::Worktree, - ) -> zed::Result> { - let settings = Self::language_server_settings(language_server_id, worktree); - let initialization_options = settings.initialization_options.unwrap_or_default(); - - let merged = match initialization_options { - serde_json::Value::Object(mut root) => { - let npmx = root - .remove("npmx") - .and_then(|value| value.as_object().cloned()) - .unwrap_or_default(); - let mut npmx = npmx; - npmx.insert(String::from("editor"), serde_json::Value::String(String::from("zed"))); - root.insert(String::from("npmx"), serde_json::Value::Object(npmx)); - serde_json::Value::Object(root) - } - _ => serde_json::json!({ - "npmx": { - "editor": "zed" - } - }), - }; - - Ok(Some(merged)) - } - fn language_server_workspace_configuration( &mut self, language_server_id: &LanguageServerId, diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 43698c6..25a9126 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -1,11 +1,9 @@ -import type { IWorkspaceState } from 'npmx-language-service/types' +import type { ClientFeatures } from 'npmx-language-service/types' import { createConnection, createServer, createSimpleProject } from '@volar/language-server/node' import { createNpmxLanguageServicePlugins } from 'npmx-language-service' import { name, version } from '../package.json' with { type: 'json' } import { registerRequests } from './request' -import { createWorkspaceState } from './workspace' - -type EditorFlavor = ReturnType +import { createWorkspaceState, DEFAULT_CLIENT_FEATURES } from './workspace' export function startServer() { const connection = createConnection() @@ -16,7 +14,7 @@ export function startServer() { connection.listen() connection.onInitialize((params) => { - workspaceState.setEditorFlavor(detectEditorFlavor(params)) + workspaceState.setClientFeatures(readClientFeatures(params.initializationOptions)) return { serverInfo: { @@ -40,36 +38,30 @@ export function startServer() { registerRequests(connection, workspaceState) } -function detectEditorFlavor(params: { - clientInfo?: { name?: string } - initializationOptions?: unknown -}): EditorFlavor { - const editor = readEditorFromInitializationOptions(params.initializationOptions) - if (editor) - return editor - - const clientName = params.clientInfo?.name?.toLowerCase() - if (clientName?.includes('zed')) - return 'zed' - if (clientName?.includes('visual studio code') || clientName?.includes('vscode')) - return 'vscode' - - return 'unknown' -} - -function readEditorFromInitializationOptions(value: unknown): EditorFlavor | undefined { +function readClientFeatures(value: unknown): ClientFeatures { if (typeof value !== 'object' || value === null) - return + return DEFAULT_CLIENT_FEATURES if (!('npmx' in value)) - return + return DEFAULT_CLIENT_FEATURES const npmx = value.npmx - if (typeof npmx !== 'object' || npmx === null || !('editor' in npmx)) - return + if (typeof npmx !== 'object' || npmx === null || !('clientFeatures' in npmx)) + return DEFAULT_CLIENT_FEATURES + + const clientFeatures = npmx.clientFeatures + if (typeof clientFeatures !== 'object' || clientFeatures === null) + return DEFAULT_CLIENT_FEATURES + + return { + catalogInlayHints: readBoolean(clientFeatures, 'catalogInlayHints', DEFAULT_CLIENT_FEATURES.catalogInlayHints), + markdownIcons: readBoolean(clientFeatures, 'markdownIcons', DEFAULT_CLIENT_FEATURES.markdownIcons), + } +} - const editor = npmx.editor - return editor === 'vscode' || editor === 'zed' - ? editor - : undefined +function readBoolean(value: object, key: string, fallback: boolean): boolean { + const candidate = Object.entries(value).find(([name]) => name === key)?.[1] + return typeof candidate === 'boolean' + ? candidate + : fallback } diff --git a/packages/language-server/src/workspace.ts b/packages/language-server/src/workspace.ts index 185e3fb..e7f92b5 100644 --- a/packages/language-server/src/workspace.ts +++ b/packages/language-server/src/workspace.ts @@ -1,6 +1,6 @@ import type { Connection, LanguageServer } from '@volar/language-server' import type { DependencyInfo, PackageManager, WorkspaceAdapter } from 'npmx-language-core/workspace' -import type { IWorkspaceState } from 'npmx-language-service/types' +import type { ClientFeatures, IWorkspaceState } from 'npmx-language-service/types' import { access, readFile } from 'node:fs/promises' import { DEPENDENCY_FILE_GLOB, PACKAGE_JSON_BASENAME } from 'npmx-language-core/constants' import { isDependencyFile, isPackageManifest } from 'npmx-language-core/utils' @@ -9,7 +9,10 @@ import { defineCachedFunction } from 'ocache' import { detect } from 'package-manager-detector' import { URI } from 'vscode-uri' -type EditorFlavor = ReturnType +export const DEFAULT_CLIENT_FEATURES: ClientFeatures = { + catalogInlayHints: true, + markdownIcons: false, +} export async function detectPackageManagerFromProject(rootPath: string): Promise { const result = await detect({ @@ -57,7 +60,7 @@ function createLanguageServerAdapter(folderUri: URI, server: LanguageServer): Wo export class WorkspaceState implements IWorkspaceState { #connection: Connection #server: LanguageServer - #editorFlavor: EditorFlavor = 'unknown' + #clientFeatures: ClientFeatures = DEFAULT_CLIENT_FEATURES constructor(connection: Connection, server: LanguageServer) { this.#connection = connection @@ -87,12 +90,12 @@ export class WorkspaceState implements IWorkspaceState { }) } - setEditorFlavor(editorFlavor: EditorFlavor) { - this.#editorFlavor = editorFlavor + setClientFeatures(clientFeatures: ClientFeatures) { + this.#clientFeatures = clientFeatures } - getEditorFlavor(): EditorFlavor { - return this.#editorFlavor + getClientFeatures(): ClientFeatures { + return this.#clientFeatures } async #invalidateDependencyCacheByUri(uri: URI) { diff --git a/packages/language-service/src/plugins/catalog.ts b/packages/language-service/src/plugins/catalog.ts index 623ab83..90f6aea 100644 --- a/packages/language-service/src/plugins/catalog.ts +++ b/packages/language-service/src/plugins/catalog.ts @@ -130,7 +130,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { }, async provideInlayHints(document, range) { - if (workspaceState.getEditorFlavor() === 'vscode') + if (!workspaceState.getClientFeatures().catalogInlayHints) return const dependencyFileUri = getDependencyFileUri(document.uri) diff --git a/packages/language-service/src/plugins/hover.ts b/packages/language-service/src/plugins/hover.ts index 453066b..3cd797c 100644 --- a/packages/language-service/src/plugins/hover.ts +++ b/packages/language-service/src/plugins/hover.ts @@ -76,7 +76,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { async provideHover(document, position): Promise { if (!await getConfig(context, 'npmx.hover.enabled')) return - const useCodicons = workspaceState.getEditorFlavor() === 'vscode' + const { markdownIcons } = workspaceState.getClientFeatures() const uri = URI.parse(document.uri) if (uri.scheme !== 'file') @@ -92,7 +92,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { if (!dep) return - return renderHover(dep, useCodicons) + return renderHover(dep, markdownIcons) } else { const text = document.getText() const specifier = getImportSpecifierAtOffset(text, offset) @@ -106,7 +106,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { if (!dep) return - return renderHover(dep, useCodicons) + return renderHover(dep, markdownIcons) } }, } diff --git a/packages/language-service/src/types.ts b/packages/language-service/src/types.ts index 84961af..95753cd 100644 --- a/packages/language-service/src/types.ts +++ b/packages/language-service/src/types.ts @@ -1,9 +1,12 @@ import type { DependencyInfo, WorkspaceContext } from 'npmx-language-core/workspace' -export type EditorFlavor = 'unknown' | 'vscode' | 'zed' +export interface ClientFeatures { + catalogInlayHints: boolean + markdownIcons: boolean +} export interface IWorkspaceState { - getEditorFlavor: () => EditorFlavor + getClientFeatures: () => ClientFeatures getWorkspaceContext: (uri: string) => Promise getResolvedDependencies: (uri: string) => Promise getResolvedDependenciesForContainingPackage: (uri: string) => Promise From 5723821568201666f3cc62112cf7e628e2f46788 Mon Sep 17 00:00:00 2001 From: Xat Date: Mon, 27 Apr 2026 14:33:02 +0800 Subject: [PATCH 09/24] fix(language-service): use unicode hover icons outside vscode --- .../src/plugins/hover.test.ts | 65 ++++++++++ .../language-service/src/plugins/hover.ts | 121 ++++++++++-------- 2 files changed, 131 insertions(+), 55 deletions(-) create mode 100644 packages/language-service/src/plugins/hover.test.ts diff --git a/packages/language-service/src/plugins/hover.test.ts b/packages/language-service/src/plugins/hover.test.ts new file mode 100644 index 0000000..0f9d34d --- /dev/null +++ b/packages/language-service/src/plugins/hover.test.ts @@ -0,0 +1,65 @@ +import type { PackageInfo } from 'npmx-language-core/api/package' +import type { DependencyInfo } from 'npmx-language-core/workspace' +import { describe, expect, it } from 'vitest' +import { renderHoverMarkdown } from './hover' + +const packageInfo = { + name: 'lodash', + distTags: { + latest: '1.0.0', + }, + versionsMeta: { + '1.0.0': { + provenance: true, + }, + }, + timeCreated: '2026-01-01T00:00:00.000Z', + timeModified: '2026-01-01T00:00:00.000Z', + lastSynced: 0, + specifier: 'lodash', + versionToTag: new Map([['1.0.0', 'latest']]), +} satisfies PackageInfo + +function createDependency(overrides: Partial = {}): DependencyInfo { + return { + category: 'dependencies', + rawName: 'lodash', + rawSpec: '^1.0.0', + nameRange: [0, 0], + specRange: [0, 0], + protocol: 'npm', + resolvedName: 'lodash', + resolvedSpec: '^1.0.0', + resolvedProtocol: 'npm', + packageInfo: async () => packageInfo, + resolvedVersion: async () => '1.0.0', + ...overrides, + } +} + +describe('renderHoverMarkdown', () => { + it('should use codicons when markdown icons are enabled', async () => { + await expect(renderHoverMarkdown(createDependency(), true)).resolves.toMatchInlineSnapshot(` + "[$(verified) Verified provenance](https://npmx.dev/package/lodash/v/^1.0.0#provenance) + + [$(package) View on npmx.dev](https://npmx.dev/package/lodash) | [$(book) View docs on npmx.dev](https://npmx.dev/docs/lodash/v/^1.0.0)" + `) + }) + + it('should use unicode icons when markdown icons are disabled', async () => { + await expect(renderHoverMarkdown(createDependency(), false)).resolves.toMatchInlineSnapshot(` + "[✓ Verified provenance](https://npmx.dev/package/lodash/v/^1.0.0#provenance) + + [📦 View on npmx.dev](https://npmx.dev/package/lodash) | [📖 View docs on npmx.dev](https://npmx.dev/docs/lodash/v/^1.0.0)" + `) + }) + + it('should use unicode icons for non-npm packages without markdown icons', async () => { + await expect(renderHoverMarkdown(createDependency({ + protocol: 'jsr', + resolvedName: '@std/fs', + resolvedSpec: '^1.0.0', + resolvedProtocol: 'jsr', + }), false)).resolves.toMatchInlineSnapshot('"[📦 View on jsr.io](https://jsr.io/@std/fs) | ⚠ Not on npmx.dev"') + }) +}) diff --git a/packages/language-service/src/plugins/hover.ts b/packages/language-service/src/plugins/hover.ts index 3cd797c..ebd7caa 100644 --- a/packages/language-service/src/plugins/hover.ts +++ b/packages/language-service/src/plugins/hover.ts @@ -7,65 +7,76 @@ import { URI } from 'vscode-uri' import { getConfig } from '../config' import { getResolvedDependencyAtOffset } from '../utils/document' -export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { - async function renderHover(dep: DependencyInfo, useCodicons: boolean): Promise { - const { resolvedName, resolvedSpec, resolvedProtocol, packageInfo } = dep - - switch (resolvedProtocol) { - case 'jsr': { - const jsrPackageLink = useCodicons - ? `[$(package) View on jsr.io](${jsrPackageUrl(resolvedName)})` - : `[View on jsr.io](${jsrPackageUrl(resolvedName)})` - - return { - contents: { - kind: 'markdown', - value: useCodicons - ? `${jsrPackageLink} | $(warning) Not on npmx.dev` - : `${jsrPackageLink} | Not on npmx.dev`, - }, - } satisfies Hover - } - case 'npm': { - const pkg = await packageInfo() - if (!pkg) { - return { - contents: { - kind: 'markdown', - value: useCodicons - ? '$(warning) Unable to fetch package information' - : 'Unable to fetch package information.', - }, - } satisfies Hover - } - - const resolvedVersion = await dep.resolvedVersion() - let content = '' - if (resolvedVersion && pkg.versionsMeta[resolvedVersion]?.provenance) { - content += useCodicons - ? `[$(verified) Verified provenance](${npmxPackageUrl(resolvedName, resolvedSpec)}#provenance)\n\n` - : `[Verified provenance](${npmxPackageUrl(resolvedName, resolvedSpec)}#provenance)\n\n` - } - - const packageLink = useCodicons - ? `[$(package) View on npmx.dev](${npmxPackageUrl(resolvedName)})` - : `[View on npmx.dev](${npmxPackageUrl(resolvedName)})` - const docsLink = useCodicons - ? `[$(book) View docs on npmx.dev](${npmxDocsUrl(resolvedName, resolvedSpec)})` - : `[View docs on npmx.dev](${npmxDocsUrl(resolvedName, resolvedSpec)})` - - content += `${packageLink} | ${docsLink}` - - return { - contents: { - kind: 'markdown', - value: content, - }, - } +function iconLabel(useCodicons: boolean, codicon: string, unicode: string, label: string): string { + return useCodicons + ? `$(${codicon}) ${label}` + : `${unicode} ${label}` +} + +function iconText(useCodicons: boolean, codicon: string, unicode: string, text: string): string { + return useCodicons + ? `$(${codicon}) ${text}` + : `${unicode} ${text}` +} + +function markdownLink(label: string, url: string): string { + return `[${label}](${url})` +} + +export async function renderHoverMarkdown(dep: DependencyInfo, useCodicons: boolean): Promise { + const { resolvedName, resolvedSpec, resolvedProtocol, packageInfo } = dep + + switch (resolvedProtocol) { + case 'jsr': { + const jsrPackageLink = markdownLink( + iconLabel(useCodicons, 'package', '📦', 'View on jsr.io'), + jsrPackageUrl(resolvedName), + ) + + return `${jsrPackageLink} | ${iconText(useCodicons, 'warning', '⚠', 'Not on npmx.dev')}` + } + case 'npm': { + const pkg = await packageInfo() + if (!pkg) + return iconText(useCodicons, 'warning', '⚠', 'Unable to fetch package information.') + + const resolvedVersion = await dep.resolvedVersion() + let content = '' + if (resolvedVersion && pkg.versionsMeta[resolvedVersion]?.provenance) { + content += `${markdownLink( + iconLabel(useCodicons, 'verified', '✓', 'Verified provenance'), + `${npmxPackageUrl(resolvedName, resolvedSpec)}#provenance`, + )}\n\n` } + + const packageLink = markdownLink( + iconLabel(useCodicons, 'package', '📦', 'View on npmx.dev'), + npmxPackageUrl(resolvedName), + ) + const docsLink = markdownLink( + iconLabel(useCodicons, 'book', '📖', 'View docs on npmx.dev'), + npmxDocsUrl(resolvedName, resolvedSpec), + ) + + return `${content}${packageLink} | ${docsLink}` } } +} + +async function renderHover(dep: DependencyInfo, useCodicons: boolean): Promise { + const content = await renderHoverMarkdown(dep, useCodicons) + if (!content) + return + return { + contents: { + kind: 'markdown', + value: content, + }, + } satisfies Hover +} + +export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { return { name: 'npmx-hover', capabilities: { From fea387267fd82328b4f519fd1c567b53787107d8 Mon Sep 17 00:00:00 2001 From: Xat Date: Mon, 27 Apr 2026 14:35:18 +0800 Subject: [PATCH 10/24] refactor(language-service): rename markdown icons option --- extensions/vscode/src/client.ts | 2 +- packages/language-server/src/server.ts | 2 +- packages/language-server/src/workspace.ts | 2 +- packages/language-service/src/plugins/hover.test.ts | 2 +- packages/language-service/src/plugins/hover.ts | 6 +++--- packages/language-service/src/types.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/extensions/vscode/src/client.ts b/extensions/vscode/src/client.ts index fd68bbe..10bf756 100644 --- a/extensions/vscode/src/client.ts +++ b/extensions/vscode/src/client.ts @@ -71,7 +71,7 @@ export function launch(serverPath: string) { npmx: { clientFeatures: { catalogInlayHints: false, - markdownIcons: true, + codicons: true, }, }, }, diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 25a9126..658f56a 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -55,7 +55,7 @@ function readClientFeatures(value: unknown): ClientFeatures { return { catalogInlayHints: readBoolean(clientFeatures, 'catalogInlayHints', DEFAULT_CLIENT_FEATURES.catalogInlayHints), - markdownIcons: readBoolean(clientFeatures, 'markdownIcons', DEFAULT_CLIENT_FEATURES.markdownIcons), + codicons: readBoolean(clientFeatures, 'codicons', DEFAULT_CLIENT_FEATURES.codicons), } } diff --git a/packages/language-server/src/workspace.ts b/packages/language-server/src/workspace.ts index e7f92b5..295ebce 100644 --- a/packages/language-server/src/workspace.ts +++ b/packages/language-server/src/workspace.ts @@ -11,7 +11,7 @@ import { URI } from 'vscode-uri' export const DEFAULT_CLIENT_FEATURES: ClientFeatures = { catalogInlayHints: true, - markdownIcons: false, + codicons: false, } export async function detectPackageManagerFromProject(rootPath: string): Promise { diff --git a/packages/language-service/src/plugins/hover.test.ts b/packages/language-service/src/plugins/hover.test.ts index 0f9d34d..16b4021 100644 --- a/packages/language-service/src/plugins/hover.test.ts +++ b/packages/language-service/src/plugins/hover.test.ts @@ -38,7 +38,7 @@ function createDependency(overrides: Partial = {}): DependencyIn } describe('renderHoverMarkdown', () => { - it('should use codicons when markdown icons are enabled', async () => { + it('should use codicons when codicons are enabled', async () => { await expect(renderHoverMarkdown(createDependency(), true)).resolves.toMatchInlineSnapshot(` "[$(verified) Verified provenance](https://npmx.dev/package/lodash/v/^1.0.0#provenance) diff --git a/packages/language-service/src/plugins/hover.ts b/packages/language-service/src/plugins/hover.ts index ebd7caa..4528669 100644 --- a/packages/language-service/src/plugins/hover.ts +++ b/packages/language-service/src/plugins/hover.ts @@ -87,7 +87,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { async provideHover(document, position): Promise { if (!await getConfig(context, 'npmx.hover.enabled')) return - const { markdownIcons } = workspaceState.getClientFeatures() + const { codicons } = workspaceState.getClientFeatures() const uri = URI.parse(document.uri) if (uri.scheme !== 'file') @@ -103,7 +103,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { if (!dep) return - return renderHover(dep, markdownIcons) + return renderHover(dep, codicons) } else { const text = document.getText() const specifier = getImportSpecifierAtOffset(text, offset) @@ -117,7 +117,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { if (!dep) return - return renderHover(dep, markdownIcons) + return renderHover(dep, codicons) } }, } diff --git a/packages/language-service/src/types.ts b/packages/language-service/src/types.ts index 95753cd..bb1db2d 100644 --- a/packages/language-service/src/types.ts +++ b/packages/language-service/src/types.ts @@ -2,7 +2,7 @@ import type { DependencyInfo, WorkspaceContext } from 'npmx-language-core/worksp export interface ClientFeatures { catalogInlayHints: boolean - markdownIcons: boolean + codicons: boolean } export interface IWorkspaceState { From 2fe430149e361666bf244453926229be39dd8db5 Mon Sep 17 00:00:00 2001 From: Xat Date: Mon, 27 Apr 2026 14:38:57 +0800 Subject: [PATCH 11/24] style(language-service): use emoji hover icons --- .../language-service/src/plugins/hover.test.ts | 8 ++++---- packages/language-service/src/plugins/hover.ts | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/language-service/src/plugins/hover.test.ts b/packages/language-service/src/plugins/hover.test.ts index 16b4021..f08f516 100644 --- a/packages/language-service/src/plugins/hover.test.ts +++ b/packages/language-service/src/plugins/hover.test.ts @@ -46,20 +46,20 @@ describe('renderHoverMarkdown', () => { `) }) - it('should use unicode icons when markdown icons are disabled', async () => { + it('should use emoji icons when codicons are disabled', async () => { await expect(renderHoverMarkdown(createDependency(), false)).resolves.toMatchInlineSnapshot(` - "[✓ Verified provenance](https://npmx.dev/package/lodash/v/^1.0.0#provenance) + "[✅ Verified provenance](https://npmx.dev/package/lodash/v/^1.0.0#provenance) [📦 View on npmx.dev](https://npmx.dev/package/lodash) | [📖 View docs on npmx.dev](https://npmx.dev/docs/lodash/v/^1.0.0)" `) }) - it('should use unicode icons for non-npm packages without markdown icons', async () => { + it('should use emoji icons for non-npm packages without codicons', async () => { await expect(renderHoverMarkdown(createDependency({ protocol: 'jsr', resolvedName: '@std/fs', resolvedSpec: '^1.0.0', resolvedProtocol: 'jsr', - }), false)).resolves.toMatchInlineSnapshot('"[📦 View on jsr.io](https://jsr.io/@std/fs) | ⚠ Not on npmx.dev"') + }), false)).resolves.toMatchInlineSnapshot('"[📦 View on jsr.io](https://jsr.io/@std/fs) | ⚠️ Not on npmx.dev"') }) }) diff --git a/packages/language-service/src/plugins/hover.ts b/packages/language-service/src/plugins/hover.ts index 4528669..82d214b 100644 --- a/packages/language-service/src/plugins/hover.ts +++ b/packages/language-service/src/plugins/hover.ts @@ -7,16 +7,16 @@ import { URI } from 'vscode-uri' import { getConfig } from '../config' import { getResolvedDependencyAtOffset } from '../utils/document' -function iconLabel(useCodicons: boolean, codicon: string, unicode: string, label: string): string { +function iconLabel(useCodicons: boolean, codicon: string, emoji: string, label: string): string { return useCodicons ? `$(${codicon}) ${label}` - : `${unicode} ${label}` + : `${emoji} ${label}` } -function iconText(useCodicons: boolean, codicon: string, unicode: string, text: string): string { +function iconText(useCodicons: boolean, codicon: string, emoji: string, text: string): string { return useCodicons ? `$(${codicon}) ${text}` - : `${unicode} ${text}` + : `${emoji} ${text}` } function markdownLink(label: string, url: string): string { @@ -33,18 +33,18 @@ export async function renderHoverMarkdown(dep: DependencyInfo, useCodicons: bool jsrPackageUrl(resolvedName), ) - return `${jsrPackageLink} | ${iconText(useCodicons, 'warning', '⚠', 'Not on npmx.dev')}` + return `${jsrPackageLink} | ${iconText(useCodicons, 'warning', '⚠️', 'Not on npmx.dev')}` } case 'npm': { const pkg = await packageInfo() if (!pkg) - return iconText(useCodicons, 'warning', '⚠', 'Unable to fetch package information.') + return iconText(useCodicons, 'warning', '⚠️', 'Unable to fetch package information.') const resolvedVersion = await dep.resolvedVersion() let content = '' if (resolvedVersion && pkg.versionsMeta[resolvedVersion]?.provenance) { content += `${markdownLink( - iconLabel(useCodicons, 'verified', '✓', 'Verified provenance'), + iconLabel(useCodicons, 'verified', '✅', 'Verified provenance'), `${npmxPackageUrl(resolvedName, resolvedSpec)}#provenance`, )}\n\n` } From e94126dac132b9ac2db44a25fe5933225069d72d Mon Sep 17 00:00:00 2001 From: Xat Date: Mon, 27 Apr 2026 16:10:53 +0800 Subject: [PATCH 12/24] refactor(language-service): simplify config lookup --- packages/language-service/src/config.ts | 221 +++++++++++++----------- 1 file changed, 121 insertions(+), 100 deletions(-) diff --git a/packages/language-service/src/config.ts b/packages/language-service/src/config.ts index b38c725..09c6e40 100644 --- a/packages/language-service/src/config.ts +++ b/packages/language-service/src/config.ts @@ -1,135 +1,156 @@ import type { LanguageServiceContext } from '@volar/language-service' -import type { ConfigKey, ConfigKeyTypeMap } from 'npmx-shared/meta' +import type { ConfigKey, ConfigKeyTypeMap, ScopedConfigKeyTypeMap } from 'npmx-shared/meta' import { scopedConfigs } from 'npmx-shared/meta' type ConfigValue = ConfigKeyTypeMap[ConfigKey] +type ConfigValidator = (value: unknown) => ConfigKeyTypeMap[K] | undefined -const defaultConfigs = { - 'npmx.hover.enabled': scopedConfigs.defaults['hover.enabled'], - 'npmx.completion.version': scopedConfigs.defaults['completion.version'], - 'npmx.completion.excludePrerelease': scopedConfigs.defaults['completion.excludePrerelease'], - 'npmx.diagnostics.upgrade': scopedConfigs.defaults['diagnostics.upgrade'], - 'npmx.diagnostics.deprecation': scopedConfigs.defaults['diagnostics.deprecation'], - 'npmx.diagnostics.replacement': scopedConfigs.defaults['diagnostics.replacement'], - 'npmx.diagnostics.vulnerability': scopedConfigs.defaults['diagnostics.vulnerability'], - 'npmx.diagnostics.distTag': scopedConfigs.defaults['diagnostics.distTag'], - 'npmx.diagnostics.engineMismatch': scopedConfigs.defaults['diagnostics.engineMismatch'], - 'npmx.packageLinks': scopedConfigs.defaults.packageLinks, - 'npmx.ignore.upgrade': scopedConfigs.defaults['ignore.upgrade'], - 'npmx.ignore.deprecation': scopedConfigs.defaults['ignore.deprecation'], - 'npmx.ignore.replacement': scopedConfigs.defaults['ignore.replacement'], - 'npmx.ignore.vulnerability': scopedConfigs.defaults['ignore.vulnerability'], -} satisfies { [K in ConfigKey]: ConfigKeyTypeMap[K] } - -export function getConfig(context: LanguageServiceContext, section: 'npmx.hover.enabled'): Promise -export function getConfig(context: LanguageServiceContext, section: 'npmx.completion.version'): Promise<'all' | 'provenance-only' | 'off'> -export function getConfig(context: LanguageServiceContext, section: 'npmx.completion.excludePrerelease'): Promise -export function getConfig(context: LanguageServiceContext, section: 'npmx.diagnostics.upgrade'): Promise -export function getConfig(context: LanguageServiceContext, section: 'npmx.diagnostics.deprecation'): Promise -export function getConfig(context: LanguageServiceContext, section: 'npmx.diagnostics.replacement'): Promise -export function getConfig(context: LanguageServiceContext, section: 'npmx.diagnostics.vulnerability'): Promise -export function getConfig(context: LanguageServiceContext, section: 'npmx.diagnostics.distTag'): Promise -export function getConfig(context: LanguageServiceContext, section: 'npmx.diagnostics.engineMismatch'): Promise -export function getConfig(context: LanguageServiceContext, section: 'npmx.packageLinks'): Promise<'off' | 'latest' | 'declared' | 'resolved'> -export function getConfig(context: LanguageServiceContext, section: 'npmx.ignore.upgrade'): Promise -export function getConfig(context: LanguageServiceContext, section: 'npmx.ignore.deprecation'): Promise -export function getConfig(context: LanguageServiceContext, section: 'npmx.ignore.replacement'): Promise -export function getConfig(context: LanguageServiceContext, section: 'npmx.ignore.vulnerability'): Promise +interface ConfigSpec { + scopedKey: keyof ScopedConfigKeyTypeMap + validate: ConfigValidator +} + +const booleanConfig = (value: unknown): boolean | undefined => typeof value === 'boolean' ? value : undefined + +const completionVersionConfig: ConfigValidator<'npmx.completion.version'> = (value) => + value === 'all' || value === 'provenance-only' || value === 'off' + ? value + : undefined + +const packageLinksConfig: ConfigValidator<'npmx.packageLinks'> = (value) => + value === 'off' || value === 'latest' || value === 'declared' || value === 'resolved' + ? value + : undefined + +function stringArrayConfig(value: unknown): string[] | undefined { + return Array.isArray(value) && value.every((item) => typeof item === 'string') + ? value + : undefined +} + +const configSpecs = { + 'npmx.hover.enabled': { + scopedKey: 'hover.enabled', + validate: booleanConfig, + }, + 'npmx.completion.version': { + scopedKey: 'completion.version', + validate: completionVersionConfig, + }, + 'npmx.completion.excludePrerelease': { + scopedKey: 'completion.excludePrerelease', + validate: booleanConfig, + }, + 'npmx.diagnostics.upgrade': { + scopedKey: 'diagnostics.upgrade', + validate: booleanConfig, + }, + 'npmx.diagnostics.deprecation': { + scopedKey: 'diagnostics.deprecation', + validate: booleanConfig, + }, + 'npmx.diagnostics.replacement': { + scopedKey: 'diagnostics.replacement', + validate: booleanConfig, + }, + 'npmx.diagnostics.vulnerability': { + scopedKey: 'diagnostics.vulnerability', + validate: booleanConfig, + }, + 'npmx.diagnostics.distTag': { + scopedKey: 'diagnostics.distTag', + validate: booleanConfig, + }, + 'npmx.diagnostics.engineMismatch': { + scopedKey: 'diagnostics.engineMismatch', + validate: booleanConfig, + }, + 'npmx.packageLinks': { + scopedKey: 'packageLinks', + validate: packageLinksConfig, + }, + 'npmx.ignore.upgrade': { + scopedKey: 'ignore.upgrade', + validate: stringArrayConfig, + }, + 'npmx.ignore.deprecation': { + scopedKey: 'ignore.deprecation', + validate: stringArrayConfig, + }, + 'npmx.ignore.replacement': { + scopedKey: 'ignore.replacement', + validate: stringArrayConfig, + }, + 'npmx.ignore.vulnerability': { + scopedKey: 'ignore.vulnerability', + validate: stringArrayConfig, + }, +} satisfies { [K in ConfigKey]: ConfigSpec } + +export function getConfig( + context: LanguageServiceContext, + section: K, +): Promise export async function getConfig(context: LanguageServiceContext, section: ConfigKey): Promise { const getConfiguration = context.env.getConfiguration - const fallback = getDefaultConfig(section) + const spec = configSpecs[section] + const fallback = getDefaultConfig(spec) + if (!getConfiguration) return fallback - const exact = validateConfig(await getConfiguration(section), section) + const exact = spec.validate(await getConfiguration(section)) if (exact !== undefined) return exact - const scopedSection = section.slice(scopedConfigs.scope.length + 1) - const scoped = validateConfig(await getConfiguration(scopedSection), section) + const scoped = spec.validate(await getConfiguration(spec.scopedKey)) if (scoped !== undefined) return scoped - const root = readConfigFromRoot(await getConfiguration(scopedConfigs.scope), section) + const root = readConfigFromRoot(await getConfiguration(scopedConfigs.scope), spec) if (root !== undefined) return root return fallback } -function getDefaultConfig(section: ConfigKey): ConfigValue { - return defaultConfigs[section] +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null } -function validateConfig(value: unknown, section: ConfigKey): ConfigValue | undefined { - switch (section) { - case 'npmx.hover.enabled': - case 'npmx.completion.excludePrerelease': - case 'npmx.diagnostics.upgrade': - case 'npmx.diagnostics.deprecation': - case 'npmx.diagnostics.replacement': - case 'npmx.diagnostics.vulnerability': - case 'npmx.diagnostics.distTag': - case 'npmx.diagnostics.engineMismatch': - return typeof value === 'boolean' ? value : undefined - case 'npmx.completion.version': - return value === 'all' || value === 'provenance-only' || value === 'off' - ? value - : undefined - case 'npmx.packageLinks': - return value === 'off' || value === 'latest' || value === 'declared' || value === 'resolved' - ? value - : undefined - case 'npmx.ignore.upgrade': - case 'npmx.ignore.deprecation': - case 'npmx.ignore.replacement': - case 'npmx.ignore.vulnerability': - return isStringArray(value) ? value : undefined +function readPath(value: unknown, path: readonly string[]): unknown { + let current = value + for (const key of path) { + if (!isObject(current)) + return + + current = current[key] } -} -function isObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null + return current } -function isStringArray(value: unknown): value is string[] { - return Array.isArray(value) && value.every((item) => typeof item === 'string') +function getDefaultConfig(spec: { + scopedKey: keyof ScopedConfigKeyTypeMap + validate: (value: unknown) => ConfigValue | undefined +}): ConfigValue { + const value = spec.validate(scopedConfigs.defaults[spec.scopedKey]) + if (value === undefined) + throw new Error(`Invalid default configuration for ${String(spec.scopedKey)}`) + + return value } -function readConfigFromRoot(value: unknown, section: ConfigKey): ConfigValue | undefined { +function readConfigFromRoot( + value: unknown, + spec: { + scopedKey: keyof ScopedConfigKeyTypeMap + validate: (value: unknown) => ConfigValue | undefined + }, +): ConfigValue | undefined { if (!isObject(value)) return const root = isObject(value.npmx) ? value.npmx : value - - switch (section) { - case 'npmx.hover.enabled': - return validateConfig(isObject(root.hover) ? root.hover.enabled : undefined, section) - case 'npmx.completion.version': - return validateConfig(isObject(root.completion) ? root.completion.version : undefined, section) - case 'npmx.completion.excludePrerelease': - return validateConfig(isObject(root.completion) ? root.completion.excludePrerelease : undefined, section) - case 'npmx.diagnostics.upgrade': - return validateConfig(isObject(root.diagnostics) ? root.diagnostics.upgrade : undefined, section) - case 'npmx.diagnostics.deprecation': - return validateConfig(isObject(root.diagnostics) ? root.diagnostics.deprecation : undefined, section) - case 'npmx.diagnostics.replacement': - return validateConfig(isObject(root.diagnostics) ? root.diagnostics.replacement : undefined, section) - case 'npmx.diagnostics.vulnerability': - return validateConfig(isObject(root.diagnostics) ? root.diagnostics.vulnerability : undefined, section) - case 'npmx.diagnostics.distTag': - return validateConfig(isObject(root.diagnostics) ? root.diagnostics.distTag : undefined, section) - case 'npmx.diagnostics.engineMismatch': - return validateConfig(isObject(root.diagnostics) ? root.diagnostics.engineMismatch : undefined, section) - case 'npmx.packageLinks': - return validateConfig(root.packageLinks, section) - case 'npmx.ignore.upgrade': - return validateConfig(isObject(root.ignore) ? root.ignore.upgrade : undefined, section) - case 'npmx.ignore.deprecation': - return validateConfig(isObject(root.ignore) ? root.ignore.deprecation : undefined, section) - case 'npmx.ignore.replacement': - return validateConfig(isObject(root.ignore) ? root.ignore.replacement : undefined, section) - case 'npmx.ignore.vulnerability': - return validateConfig(isObject(root.ignore) ? root.ignore.vulnerability : undefined, section) - } + return spec.validate(readPath(root, spec.scopedKey.split('.'))) } From 07e5986234075ac8d5842983376b0ced9c6d38b8 Mon Sep 17 00:00:00 2001 From: Xat Date: Mon, 27 Apr 2026 16:16:54 +0800 Subject: [PATCH 13/24] docs: update zed extension readmes --- README.md | 8 ++++- extensions/zed/README.md | 69 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5f17cc7..3e120f3 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ | 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.) | @@ -19,7 +20,7 @@ ## 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** @@ -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 diff --git a/extensions/zed/README.md b/extensions/zed/README.md index 2445737..98e1808 100644 --- a/extensions/zed/README.md +++ b/extensions/zed/README.md @@ -1,22 +1,83 @@ # npmx for Zed -This is the in-repo Zed port of the `npmx` VS Code extension. +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. -Current status: +## 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 -For local development: +## 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. -Notes: +## 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. From fc8da4e8b422f6216dc3b340db8dfef5412afe7e Mon Sep 17 00:00:00 2001 From: Xat Date: Mon, 27 Apr 2026 17:27:48 +0800 Subject: [PATCH 14/24] feat: polish the implementation --- extensions/vscode/src/client.ts | 2 +- extensions/zed/src/lib.rs | 13 ++++-- packages/language-server/src/server.ts | 36 ++++++--------- packages/language-server/src/workspace.ts | 14 +++--- packages/language-service/src/config.ts | 13 +----- .../src/plugins/hover.test.ts | 12 ++--- .../language-service/src/plugins/hover.ts | 45 ++++++++++++------- packages/language-service/src/types.ts | 9 +++- 8 files changed, 73 insertions(+), 71 deletions(-) diff --git a/extensions/vscode/src/client.ts b/extensions/vscode/src/client.ts index 10bf756..b9e0327 100644 --- a/extensions/vscode/src/client.ts +++ b/extensions/vscode/src/client.ts @@ -71,7 +71,7 @@ export function launch(serverPath: string) { npmx: { clientFeatures: { catalogInlayHints: false, - codicons: true, + iconStyle: 'codicon', }, }, }, diff --git a/extensions/zed/src/lib.rs b/extensions/zed/src/lib.rs index a091824..aeb87af 100644 --- a/extensions/zed/src/lib.rs +++ b/extensions/zed/src/lib.rs @@ -32,11 +32,16 @@ impl zed::Extension for NpmxExtension { ) -> zed::Result { let lsp_settings = Self::language_server_settings(language_server_id, worktree); if let Some(binary) = lsp_settings.binary { - let command = binary - .path - .unwrap_or_else(|| zed::node_binary_path().unwrap_or_else(|_| String::from("node"))); + let command = match binary.path { + Some(path) => path, + None => zed::node_binary_path()?, + }; let args = binary.arguments.unwrap_or_default(); - let env = binary.env.unwrap_or_default().into_iter().collect(); + let env = worktree + .shell_env() + .into_iter() + .chain(binary.env.unwrap_or_default()) + .collect(); return Ok(zed::Command { command, args, env }); } diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 658f56a..4465143 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -1,9 +1,10 @@ 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, DEFAULT_CLIENT_FEATURES } from './workspace' +import { createWorkspaceState } from './workspace' export function startServer() { const connection = createConnection() @@ -38,30 +39,21 @@ export function startServer() { registerRequests(connection, workspaceState) } -function readClientFeatures(value: unknown): ClientFeatures { - if (typeof value !== 'object' || value === null) - return DEFAULT_CLIENT_FEATURES - - if (!('npmx' in value)) - return DEFAULT_CLIENT_FEATURES - - const npmx = value.npmx - if (typeof npmx !== 'object' || npmx === null || !('clientFeatures' in npmx)) - return DEFAULT_CLIENT_FEATURES +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null +} - const clientFeatures = npmx.clientFeatures - if (typeof clientFeatures !== 'object' || clientFeatures === 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: readBoolean(clientFeatures, 'catalogInlayHints', DEFAULT_CLIENT_FEATURES.catalogInlayHints), - codicons: readBoolean(clientFeatures, 'codicons', DEFAULT_CLIENT_FEATURES.codicons), + catalogInlayHints: typeof cf.catalogInlayHints === 'boolean' + ? cf.catalogInlayHints + : DEFAULT_CLIENT_FEATURES.catalogInlayHints, + iconStyle: cf.iconStyle === 'codicon' || cf.iconStyle === 'emoji' + ? cf.iconStyle + : DEFAULT_CLIENT_FEATURES.iconStyle, } } - -function readBoolean(value: object, key: string, fallback: boolean): boolean { - const candidate = Object.entries(value).find(([name]) => name === key)?.[1] - return typeof candidate === 'boolean' - ? candidate - : fallback -} diff --git a/packages/language-server/src/workspace.ts b/packages/language-server/src/workspace.ts index 295ebce..33958f1 100644 --- a/packages/language-server/src/workspace.ts +++ b/packages/language-server/src/workspace.ts @@ -5,15 +5,15 @@ import { access, readFile } from 'node:fs/promises' import { DEPENDENCY_FILE_GLOB, PACKAGE_JSON_BASENAME } from 'npmx-language-core/constants' import { isDependencyFile, isPackageManifest } from 'npmx-language-core/utils' import { WorkspaceContext } from 'npmx-language-core/workspace' +import { DEFAULT_CLIENT_FEATURES } from 'npmx-language-service/types' import { defineCachedFunction } from 'ocache' import { detect } from 'package-manager-detector' import { URI } from 'vscode-uri' -export const DEFAULT_CLIENT_FEATURES: ClientFeatures = { - catalogInlayHints: true, - codicons: false, -} - +/** + * Exported for unit tests only. + * @internal + */ export async function detectPackageManagerFromProject(rootPath: string): Promise { const result = await detect({ cwd: rootPath, @@ -51,9 +51,7 @@ function createLanguageServerAdapter(folderUri: URI, server: LanguageServer): Wo } }, - async detectPackageManager(rootPath): Promise { - return await detectPackageManagerFromProject(rootPath) - }, + detectPackageManager: detectPackageManagerFromProject, } } diff --git a/packages/language-service/src/config.ts b/packages/language-service/src/config.ts index 09c6e40..328b0e9 100644 --- a/packages/language-service/src/config.ts +++ b/packages/language-service/src/config.ts @@ -94,7 +94,7 @@ export function getConfig( export async function getConfig(context: LanguageServiceContext, section: ConfigKey): Promise { const getConfiguration = context.env.getConfiguration const spec = configSpecs[section] - const fallback = getDefaultConfig(spec) + const fallback = scopedConfigs.defaults[spec.scopedKey] if (!getConfiguration) return fallback @@ -130,17 +130,6 @@ function readPath(value: unknown, path: readonly string[]): unknown { return current } -function getDefaultConfig(spec: { - scopedKey: keyof ScopedConfigKeyTypeMap - validate: (value: unknown) => ConfigValue | undefined -}): ConfigValue { - const value = spec.validate(scopedConfigs.defaults[spec.scopedKey]) - if (value === undefined) - throw new Error(`Invalid default configuration for ${String(spec.scopedKey)}`) - - return value -} - function readConfigFromRoot( value: unknown, spec: { diff --git a/packages/language-service/src/plugins/hover.test.ts b/packages/language-service/src/plugins/hover.test.ts index f08f516..ef46831 100644 --- a/packages/language-service/src/plugins/hover.test.ts +++ b/packages/language-service/src/plugins/hover.test.ts @@ -38,28 +38,28 @@ function createDependency(overrides: Partial = {}): DependencyIn } describe('renderHoverMarkdown', () => { - it('should use codicons when codicons are enabled', async () => { - await expect(renderHoverMarkdown(createDependency(), true)).resolves.toMatchInlineSnapshot(` + it('renders codicon markup for codicon-capable clients', async () => { + await expect(renderHoverMarkdown(createDependency(), 'codicon')).resolves.toMatchInlineSnapshot(` "[$(verified) Verified provenance](https://npmx.dev/package/lodash/v/^1.0.0#provenance) [$(package) View on npmx.dev](https://npmx.dev/package/lodash) | [$(book) View docs on npmx.dev](https://npmx.dev/docs/lodash/v/^1.0.0)" `) }) - it('should use emoji icons when codicons are disabled', async () => { - await expect(renderHoverMarkdown(createDependency(), false)).resolves.toMatchInlineSnapshot(` + it('renders emoji icons for non-codicon clients', async () => { + await expect(renderHoverMarkdown(createDependency(), 'emoji')).resolves.toMatchInlineSnapshot(` "[✅ Verified provenance](https://npmx.dev/package/lodash/v/^1.0.0#provenance) [📦 View on npmx.dev](https://npmx.dev/package/lodash) | [📖 View docs on npmx.dev](https://npmx.dev/docs/lodash/v/^1.0.0)" `) }) - it('should use emoji icons for non-npm packages without codicons', async () => { + it('renders emoji icons for non-npm packages with emoji style', async () => { await expect(renderHoverMarkdown(createDependency({ protocol: 'jsr', resolvedName: '@std/fs', resolvedSpec: '^1.0.0', resolvedProtocol: 'jsr', - }), false)).resolves.toMatchInlineSnapshot('"[📦 View on jsr.io](https://jsr.io/@std/fs) | ⚠️ Not on npmx.dev"') + }), 'emoji')).resolves.toMatchInlineSnapshot('"[📦 View on jsr.io](https://jsr.io/@std/fs) | ⚠️ Not on npmx.dev"') }) }) diff --git a/packages/language-service/src/plugins/hover.ts b/packages/language-service/src/plugins/hover.ts index 82d214b..35703cd 100644 --- a/packages/language-service/src/plugins/hover.ts +++ b/packages/language-service/src/plugins/hover.ts @@ -1,20 +1,31 @@ import type { Hover, LanguageServicePlugin, LanguageServicePluginInstance } from '@volar/language-service' import type { DependencyInfo } from 'npmx-language-core/workspace' -import type { IWorkspaceState } from '../types' +import type { IconStyle, IWorkspaceState } from '../types' import { jsrPackageUrl, npmxDocsUrl, npmxPackageUrl } from 'npmx-language-core/links' import { getImportSpecifierAtOffset, isDependencyFile } from 'npmx-language-core/utils' import { URI } from 'vscode-uri' import { getConfig } from '../config' import { getResolvedDependencyAtOffset } from '../utils/document' -function iconLabel(useCodicons: boolean, codicon: string, emoji: string, label: string): string { - return useCodicons +const ICONS = { + package: { codicon: 'package', emoji: '📦' }, + verified: { codicon: 'verified', emoji: '✅' }, + book: { codicon: 'book', emoji: '📖' }, + warning: { codicon: 'warning', emoji: '⚠️' }, +} as const + +type IconName = keyof typeof ICONS + +function iconLabel(iconStyle: IconStyle, name: IconName, label: string): string { + const { codicon, emoji } = ICONS[name] + return iconStyle === 'codicon' ? `$(${codicon}) ${label}` : `${emoji} ${label}` } -function iconText(useCodicons: boolean, codicon: string, emoji: string, text: string): string { - return useCodicons +function iconText(iconStyle: IconStyle, name: IconName, text: string): string { + const { codicon, emoji } = ICONS[name] + return iconStyle === 'codicon' ? `$(${codicon}) ${text}` : `${emoji} ${text}` } @@ -23,38 +34,38 @@ function markdownLink(label: string, url: string): string { return `[${label}](${url})` } -export async function renderHoverMarkdown(dep: DependencyInfo, useCodicons: boolean): Promise { +export async function renderHoverMarkdown(dep: DependencyInfo, iconStyle: IconStyle): Promise { const { resolvedName, resolvedSpec, resolvedProtocol, packageInfo } = dep switch (resolvedProtocol) { case 'jsr': { const jsrPackageLink = markdownLink( - iconLabel(useCodicons, 'package', '📦', 'View on jsr.io'), + iconLabel(iconStyle, 'package', 'View on jsr.io'), jsrPackageUrl(resolvedName), ) - return `${jsrPackageLink} | ${iconText(useCodicons, 'warning', '⚠️', 'Not on npmx.dev')}` + return `${jsrPackageLink} | ${iconText(iconStyle, 'warning', 'Not on npmx.dev')}` } case 'npm': { const pkg = await packageInfo() if (!pkg) - return iconText(useCodicons, 'warning', '⚠️', 'Unable to fetch package information.') + return iconText(iconStyle, 'warning', 'Unable to fetch package information.') const resolvedVersion = await dep.resolvedVersion() let content = '' if (resolvedVersion && pkg.versionsMeta[resolvedVersion]?.provenance) { content += `${markdownLink( - iconLabel(useCodicons, 'verified', '✅', 'Verified provenance'), + iconLabel(iconStyle, 'verified', 'Verified provenance'), `${npmxPackageUrl(resolvedName, resolvedSpec)}#provenance`, )}\n\n` } const packageLink = markdownLink( - iconLabel(useCodicons, 'package', '📦', 'View on npmx.dev'), + iconLabel(iconStyle, 'package', 'View on npmx.dev'), npmxPackageUrl(resolvedName), ) const docsLink = markdownLink( - iconLabel(useCodicons, 'book', '📖', 'View docs on npmx.dev'), + iconLabel(iconStyle, 'book', 'View docs on npmx.dev'), npmxDocsUrl(resolvedName, resolvedSpec), ) @@ -63,8 +74,8 @@ export async function renderHoverMarkdown(dep: DependencyInfo, useCodicons: bool } } -async function renderHover(dep: DependencyInfo, useCodicons: boolean): Promise { - const content = await renderHoverMarkdown(dep, useCodicons) +async function renderHover(dep: DependencyInfo, iconStyle: IconStyle): Promise { + const content = await renderHoverMarkdown(dep, iconStyle) if (!content) return @@ -87,7 +98,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { async provideHover(document, position): Promise { if (!await getConfig(context, 'npmx.hover.enabled')) return - const { codicons } = workspaceState.getClientFeatures() + const { iconStyle } = workspaceState.getClientFeatures() const uri = URI.parse(document.uri) if (uri.scheme !== 'file') @@ -103,7 +114,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { if (!dep) return - return renderHover(dep, codicons) + return renderHover(dep, iconStyle) } else { const text = document.getText() const specifier = getImportSpecifierAtOffset(text, offset) @@ -117,7 +128,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { if (!dep) return - return renderHover(dep, codicons) + return renderHover(dep, iconStyle) } }, } diff --git a/packages/language-service/src/types.ts b/packages/language-service/src/types.ts index bb1db2d..f1e2d1a 100644 --- a/packages/language-service/src/types.ts +++ b/packages/language-service/src/types.ts @@ -1,8 +1,15 @@ import type { DependencyInfo, WorkspaceContext } from 'npmx-language-core/workspace' +export type IconStyle = 'codicon' | 'emoji' + export interface ClientFeatures { catalogInlayHints: boolean - codicons: boolean + iconStyle: IconStyle +} + +export const DEFAULT_CLIENT_FEATURES: ClientFeatures = { + catalogInlayHints: true, + iconStyle: 'emoji', } export interface IWorkspaceState { From eaae9c338c65aa71608413a33ef0976aaf0cadf9 Mon Sep 17 00:00:00 2001 From: Xat Date: Thu, 7 May 2026 09:01:27 +0800 Subject: [PATCH 15/24] chore(zed): mark extension crate private --- extensions/zed/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/zed/Cargo.toml b/extensions/zed/Cargo.toml index f30e741..8d93a99 100644 --- a/extensions/zed/Cargo.toml +++ b/extensions/zed/Cargo.toml @@ -3,6 +3,7 @@ name = "zed-npmx" version = "0.0.1" edition = "2021" license = "MIT" +publish = false [lib] crate-type = [ "cdylib" ] From a4311c0d995c0b387ff411fedd8b24e63b862b68 Mon Sep 17 00:00:00 2001 From: Xat Date: Thu, 7 May 2026 09:04:18 +0800 Subject: [PATCH 16/24] feat(zed): pass client feature initialization options --- extensions/zed/src/lib.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/extensions/zed/src/lib.rs b/extensions/zed/src/lib.rs index aeb87af..f5c448d 100644 --- a/extensions/zed/src/lib.rs +++ b/extensions/zed/src/lib.rs @@ -53,6 +53,29 @@ impl zed::Extension for NpmxExtension { }) } + fn language_server_initialization_options( + &mut self, + language_server_id: &LanguageServerId, + worktree: &zed::Worktree, + ) -> zed::Result> { + 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!({ + "catalogInlayHints": true, + "iconStyle": "emoji", + })); + + Ok(Some(serde_json::json!({ + "npmx": { + "clientFeatures": client_features + } + }))) + } + fn language_server_workspace_configuration( &mut self, language_server_id: &LanguageServerId, From 91eebca3c1cd347e29ab2ef21cc0c02617b7ef4a Mon Sep 17 00:00:00 2001 From: Xat Date: Thu, 7 May 2026 09:05:52 +0800 Subject: [PATCH 17/24] fix(language-server): use detector subpath import --- packages/language-server/src/workspace.test.ts | 4 ++-- packages/language-server/src/workspace.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/language-server/src/workspace.test.ts b/packages/language-server/src/workspace.test.ts index 2cc86bf..d3bf3d0 100644 --- a/packages/language-server/src/workspace.test.ts +++ b/packages/language-server/src/workspace.test.ts @@ -1,11 +1,11 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { detectPackageManagerFromProject } from './workspace' -vi.mock('package-manager-detector', () => ({ +vi.mock('package-manager-detector/detect', () => ({ detect: vi.fn(), })) -const { detect } = await import('package-manager-detector') +const { detect } = await import('package-manager-detector/detect') describe('detectPackageManagerFromProject', () => { afterEach(() => { diff --git a/packages/language-server/src/workspace.ts b/packages/language-server/src/workspace.ts index 33958f1..d871d12 100644 --- a/packages/language-server/src/workspace.ts +++ b/packages/language-server/src/workspace.ts @@ -7,7 +7,7 @@ import { isDependencyFile, isPackageManifest } from 'npmx-language-core/utils' import { WorkspaceContext } from 'npmx-language-core/workspace' import { DEFAULT_CLIENT_FEATURES } from 'npmx-language-service/types' import { defineCachedFunction } from 'ocache' -import { detect } from 'package-manager-detector' +import { detect } from 'package-manager-detector/detect' import { URI } from 'vscode-uri' /** From 8e6b4eb3943ee6b759e502f9584aaab705b85a08 Mon Sep 17 00:00:00 2001 From: Xat Date: Thu, 7 May 2026 09:07:05 +0800 Subject: [PATCH 18/24] fix(language-service): remove duplicate inlay hint spacing --- packages/language-service/src/plugins/catalog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language-service/src/plugins/catalog.ts b/packages/language-service/src/plugins/catalog.ts index 90f6aea..ea51908 100644 --- a/packages/language-service/src/plugins/catalog.ts +++ b/packages/language-service/src/plugins/catalog.ts @@ -154,7 +154,7 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { return [{ position: document.positionAt(specEnd), - label: ` ${dependency.resolvedSpec}`, + label: dependency.resolvedSpec, paddingLeft: true, }] }) From b53f85522a9984dbf112457e3eb02f9f8f6801a2 Mon Sep 17 00:00:00 2001 From: Xat Date: Fri, 8 May 2026 13:06:59 +0800 Subject: [PATCH 19/24] feat: sync version with vscode extension --- extensions/zed/extension.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/zed/extension.toml b/extensions/zed/extension.toml index 6bca92d..5646dd1 100644 --- a/extensions/zed/extension.toml +++ b/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "npmx" name = "npmx" description = "npmx language support for Zed" -version = "0.0.1" +version = "0.7.0" schema_version = 1 authors = [ "Xat " ] repository = "https://github.com/npmx-dev/vscode-npmx/tree/main/extensions/zed" From c751f50e940daa33651751e75a1b570d7861141a Mon Sep 17 00:00:00 2001 From: Xat Date: Fri, 8 May 2026 14:01:29 +0800 Subject: [PATCH 20/24] ci(workflows): add zed release workflows --- .github/workflows/publish-language-server.yml | 48 +++++++++++++++++++ .../{publish.yml => publish-vscode.yml} | 6 +-- .github/workflows/publish-zed.yml | 37 ++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/publish-language-server.yml rename .github/workflows/{publish.yml => publish-vscode.yml} (94%) create mode 100644 .github/workflows/publish-zed.yml diff --git a/.github/workflows/publish-language-server.yml b/.github/workflows/publish-language-server.yml new file mode 100644 index 0000000..047622b --- /dev/null +++ b/.github/workflows/publish-language-server.yml @@ -0,0 +1,48 @@ +name: Publish Language Server + +on: + push: + tags: + - v* + paths: + - 'packages/language-server/**' + - 'packages/language-core/**' + - 'packages/language-service/**' + - 'packages/shared/**' + +jobs: + publish-language-server: + permissions: + contents: write + + runs-on: ubuntu-slim + steps: + - name: Setup JS + uses: sxzz/workflows/setup-js@69098296b6f6083ed99f38e2040f2a7238580e27 # v1.4.0 + with: + fetch-all: true + + - name: Build + run: pnpm build + + - name: Verify language server version + run: | + expected_version="${GITHUB_REF_NAME#v}" + actual_version="$(sed -n 's/^ \"version\": \"\(.*\)\",$/\1/p' packages/language-server/package.json | head -n 1)" + if [ "$actual_version" != "$expected_version" ]; then + echo "Expected packages/language-server/package.json version $expected_version but found $actual_version" + exit 1 + fi + + - name: Pack language server + working-directory: packages/language-server + run: pnpm pack + + - name: Rename tarball + working-directory: packages/language-server + run: mv npmx-language-server-*.tgz "npmx-language-server-${GITHUB_REF_NAME#v}.tgz" + + - name: Upload language server tarball + uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.0.8 + with: + files: packages/language-server/npmx-language-server-*.tgz diff --git a/.github/workflows/publish.yml b/.github/workflows/publish-vscode.yml similarity index 94% rename from .github/workflows/publish.yml rename to .github/workflows/publish-vscode.yml index de75e3f..eed49be 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish-vscode.yml @@ -1,9 +1,7 @@ -name: Publish Extension +name: Publish VS Code Extension on: - push: - tags: - - v* + workflow_dispatch: jobs: publish-extension: diff --git a/.github/workflows/publish-zed.yml b/.github/workflows/publish-zed.yml new file mode 100644 index 0000000..0d7a6ce --- /dev/null +++ b/.github/workflows/publish-zed.yml @@ -0,0 +1,37 @@ +name: Publish Zed Extension + +on: + push: + tags: + - v* + paths: + - 'extensions/zed/**' + +jobs: + publish-zed-extension: + permissions: + contents: write + + runs-on: ubuntu-slim + steps: + - name: Setup JS + uses: sxzz/workflows/setup-js@69098296b6f6083ed99f38e2040f2a7238580e27 # v1.4.0 + with: + fetch-all: true + + - name: Verify Zed extension version + run: | + expected_version="${GITHUB_REF_NAME#v}" + actual_version="$(sed -n 's/^version = \"\(.*\)\"$/\1/p' extensions/zed/extension.toml | head -n 1)" + if [ "$actual_version" != "$expected_version" ]; then + echo "Expected extensions/zed/extension.toml version $expected_version but found $actual_version" + exit 1 + fi + + - name: Update Zed extensions registry + uses: huacnlee/zed-extension-action@v2 + with: + extension-name: npmx + push-to: withxat/issue-zed-extensions + env: + COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }} From aaca0283e5ddc0b041fedcfbed5db79dbbb05e1d Mon Sep 17 00:00:00 2001 From: Xat Date: Fri, 8 May 2026 14:05:00 +0800 Subject: [PATCH 21/24] ci(release): sync zed versions during bump --- package.json | 2 +- scripts/sync-zed-version.mjs | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 scripts/sync-zed-version.mjs diff --git a/package.json b/package.json index fc5bc0c..1270733 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "typecheck": "tsgo -b --noEmit", - "release": "npx bumpp -r" + "release": "npx bumpp -r --execute \"node scripts/sync-zed-version.mjs\"" }, "nano-staged": { "*": "eslint --fix" diff --git a/scripts/sync-zed-version.mjs b/scripts/sync-zed-version.mjs new file mode 100644 index 0000000..a0f003e --- /dev/null +++ b/scripts/sync-zed-version.mjs @@ -0,0 +1,25 @@ +import { readFileSync, writeFileSync } from 'node:fs' + +const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) +const version = packageJson.version + +const zedExtensionTomlUrl = new URL('../extensions/zed/extension.toml', import.meta.url) +const zedCargoTomlUrl = new URL('../extensions/zed/Cargo.toml', import.meta.url) + +const zedExtensionToml = readFileSync(zedExtensionTomlUrl, 'utf8') +const nextZedExtensionToml = zedExtensionToml.replace( + /^version = ".*"$/m, + `version = "${version}"`, +) + +if (nextZedExtensionToml !== zedExtensionToml) + writeFileSync(zedExtensionTomlUrl, nextZedExtensionToml) + +const zedCargoToml = readFileSync(zedCargoTomlUrl, 'utf8') +const nextZedCargoToml = zedCargoToml.replace( + /^version = ".*"$/m, + `version = "${version}"`, +) + +if (nextZedCargoToml !== zedCargoToml) + writeFileSync(zedCargoTomlUrl, nextZedCargoToml) From 9fe8e0cc13757cc7a86bc30d0f35bd441a08fe1d Mon Sep 17 00:00:00 2001 From: Xat Date: Fri, 8 May 2026 14:05:38 +0800 Subject: [PATCH 22/24] chore(zed): align version and add license --- extensions/zed/Cargo.toml | 2 +- extensions/zed/LICENSE.md | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 extensions/zed/LICENSE.md diff --git a/extensions/zed/Cargo.toml b/extensions/zed/Cargo.toml index 8d93a99..8f66c4f 100644 --- a/extensions/zed/Cargo.toml +++ b/extensions/zed/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed-npmx" -version = "0.0.1" +version = "0.7.0" edition = "2021" license = "MIT" publish = false diff --git a/extensions/zed/LICENSE.md b/extensions/zed/LICENSE.md new file mode 100644 index 0000000..b1abfa0 --- /dev/null +++ b/extensions/zed/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Xat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 5ae5306cef4cce007c48cfdcc1c7c4b7e341e0cf Mon Sep 17 00:00:00 2001 From: Xat Date: Fri, 8 May 2026 14:09:02 +0800 Subject: [PATCH 23/24] ci(workflows): simplify release triggers --- .github/workflows/publish-language-server.yml | 5 ----- .github/workflows/publish-vscode.yml | 4 ++++ .github/workflows/publish-zed.yml | 2 -- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish-language-server.yml b/.github/workflows/publish-language-server.yml index 047622b..91f9afc 100644 --- a/.github/workflows/publish-language-server.yml +++ b/.github/workflows/publish-language-server.yml @@ -4,11 +4,6 @@ on: push: tags: - v* - paths: - - 'packages/language-server/**' - - 'packages/language-core/**' - - 'packages/language-service/**' - - 'packages/shared/**' jobs: publish-language-server: diff --git a/.github/workflows/publish-vscode.yml b/.github/workflows/publish-vscode.yml index eed49be..9b0e9d4 100644 --- a/.github/workflows/publish-vscode.yml +++ b/.github/workflows/publish-vscode.yml @@ -1,6 +1,10 @@ name: Publish VS Code Extension on: + # TODO: Restore this trigger after these Zed release changes are merged to the main branch. + # push: + # tags: + # - v* workflow_dispatch: jobs: diff --git a/.github/workflows/publish-zed.yml b/.github/workflows/publish-zed.yml index 0d7a6ce..8c44c31 100644 --- a/.github/workflows/publish-zed.yml +++ b/.github/workflows/publish-zed.yml @@ -4,8 +4,6 @@ on: push: tags: - v* - paths: - - 'extensions/zed/**' jobs: publish-zed-extension: From 4e5451d40dde6aabd861e744750140efdbc72a67 Mon Sep 17 00:00:00 2001 From: Xat Date: Fri, 8 May 2026 14:11:18 +0800 Subject: [PATCH 24/24] docs(workflows): clarify zed publish TODOs --- .github/workflows/publish-zed.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish-zed.yml b/.github/workflows/publish-zed.yml index 8c44c31..4befdd0 100644 --- a/.github/workflows/publish-zed.yml +++ b/.github/workflows/publish-zed.yml @@ -30,6 +30,8 @@ jobs: uses: huacnlee/zed-extension-action@v2 with: extension-name: npmx + # TODO: Update this to the correct fork before enabling automated publishing. push-to: withxat/issue-zed-extensions env: + # TODO: Add the COMMITTER_TOKEN secret before enabling automated publishing. COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }}