From 629a89391453f3676e26ad0ae10afe5831d1393c Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Mar 2026 15:55:00 +0800 Subject: [PATCH 1/3] feat: migrate catalog-related providers to language-service --- extensions/vscode/src/index.ts | 2 - .../src/providers/completion-item/catalog.ts | 33 ----- .../src/providers/completion-item/index.ts | 13 +- .../src/providers/definition/catalog.ts | 39 ------ .../vscode/src/providers/definition/index.ts | 10 -- extensions/vscode/src/utils/constants.ts | 1 - packages/language-core/package.json | 8 +- packages/language-server/package.json | 24 ++-- packages/language-service/src/index.ts | 2 + .../language-service/src/plugins/catalog.ts | 118 ++++++++++++++++++ 10 files changed, 137 insertions(+), 113 deletions(-) delete mode 100644 extensions/vscode/src/providers/completion-item/catalog.ts delete mode 100644 extensions/vscode/src/providers/definition/catalog.ts delete mode 100644 extensions/vscode/src/providers/definition/index.ts create mode 100644 packages/language-service/src/plugins/catalog.ts diff --git a/extensions/vscode/src/index.ts b/extensions/vscode/src/index.ts index 3ef4b97..2760596 100644 --- a/extensions/vscode/src/index.ts +++ b/extensions/vscode/src/index.ts @@ -9,7 +9,6 @@ import { openInBrowser } from './commands/open-in-browser' import { useCodeActions } from './providers/code-actions' import { useCompletionItem } from './providers/completion-item' import { useDecorators } from './providers/decorators' -import { useDefinition } from './providers/definition' import { useDiagnostics } from './providers/diagnostics' import { useDocumentLink } from './providers/document-link' import { logger } from './state' @@ -28,7 +27,6 @@ export const { activate, deactivate } = defineExtension((ctx) => { useDecorators() useCodeActions() useDocumentLink() - useDefinition() useCommands({ [commands.openInBrowser]: openInBrowser, diff --git a/extensions/vscode/src/providers/completion-item/catalog.ts b/extensions/vscode/src/providers/completion-item/catalog.ts deleted file mode 100644 index f113efd..0000000 --- a/extensions/vscode/src/providers/completion-item/catalog.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { CompletionItemProvider, Position, TextDocument } from 'vscode' -import { getResolvedDependencyByOffset, getWorkspaceContext } from '#core/workspace' -import { CompletionItem, CompletionItemKind } from 'vscode' - -export class CatalogCompletionItemProvider implements CompletionItemProvider { - static triggers = [':'] - - async provideCompletionItems(document: TextDocument, position: Position) { - const offset = document.offsetAt(position) - const info = await getResolvedDependencyByOffset(document.uri, offset) - if (!info?.rawSpec.startsWith('catalog:')) - return - - const ctx = await getWorkspaceContext(document.uri) - if (!ctx) - return - - const catalogs = await ctx.getCatalogs() - - if (!catalogs) - return - - return Object.entries(catalogs).flatMap(([name, catalog]) => { - const version = catalog[info.resolvedName] - if (!version) - return [] - - const item = new CompletionItem(name, CompletionItemKind.Value) - item.detail = version - return [item] - }) - } -} diff --git a/extensions/vscode/src/providers/completion-item/index.ts b/extensions/vscode/src/providers/completion-item/index.ts index 944e5f2..209d3c7 100644 --- a/extensions/vscode/src/providers/completion-item/index.ts +++ b/extensions/vscode/src/providers/completion-item/index.ts @@ -1,8 +1,7 @@ import { config } from '#state' -import { PACKAGE_JSON_PATTERN, SUPPORTED_DOCUMENT_PATTERN } from '#utils/constants' +import { SUPPORTED_DOCUMENT_PATTERN } from '#utils/constants' import { watchEffect } from 'reactive-vscode' import { languages } from 'vscode' -import { CatalogCompletionItemProvider } from './catalog' import { VersionCompletionItemProvider } from './version' export function useCompletionItem() { @@ -18,14 +17,4 @@ export function useCompletionItem() { onCleanup(() => disposable.dispose()) }) - - watchEffect((onCleanup) => { - const disposable = languages.registerCompletionItemProvider( - { pattern: PACKAGE_JSON_PATTERN }, - new CatalogCompletionItemProvider(), - ...CatalogCompletionItemProvider.triggers, - ) - - onCleanup(() => disposable.dispose()) - }) } diff --git a/extensions/vscode/src/providers/definition/catalog.ts b/extensions/vscode/src/providers/definition/catalog.ts deleted file mode 100644 index a8881c5..0000000 --- a/extensions/vscode/src/providers/definition/catalog.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { DefinitionProvider, Position, TextDocument } from 'vscode' -import { getResolvedDependencyByOffset, getWorkspaceContext } from '#core/workspace' -import { offsetRangeToRange } from '#utils/ast' -import { normalizeCatalogName } from 'npmx-language-core/utils' -import { Location, workspace } from 'vscode' - -export class CatalogDefinitionProvider implements DefinitionProvider { - async provideDefinition(document: TextDocument, position: Position) { - const offset = document.offsetAt(position) - const info = await getResolvedDependencyByOffset(document.uri, offset) - if (!info?.rawSpec.startsWith('catalog:')) - return - - const ctx = await getWorkspaceContext(document.uri) - if (!ctx?.workspaceFilePath) - return - - const dependencies = (await ctx.loadWorkspaceFileInfo(ctx.workspaceFilePath))?.dependencies - if (!dependencies) - return - - const target = dependencies.find( - (dep) => - dep.rawName === info.resolvedName - && dep.categoryName != null && info.categoryName != null - && normalizeCatalogName(dep.categoryName) === normalizeCatalogName(info.categoryName), - ) - if (!target) - return - - const workspaceFileUri = document.uri.with({ path: ctx.workspaceFilePath }) - const workspaceDocument = await workspace.openTextDocument(workspaceFileUri) - - return new Location( - workspaceFileUri, - offsetRangeToRange(workspaceDocument, target.specRange), - ) - } -} diff --git a/extensions/vscode/src/providers/definition/index.ts b/extensions/vscode/src/providers/definition/index.ts deleted file mode 100644 index 4aa79f2..0000000 --- a/extensions/vscode/src/providers/definition/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { PACKAGE_JSON_PATTERN } from '#utils/constants' -import { useDisposable } from 'reactive-vscode' -import { languages } from 'vscode' -import { CatalogDefinitionProvider } from './catalog' - -export function useDefinition() { - useDisposable( - languages.registerDefinitionProvider({ pattern: PACKAGE_JSON_PATTERN }, new CatalogDefinitionProvider()), - ) -} diff --git a/extensions/vscode/src/utils/constants.ts b/extensions/vscode/src/utils/constants.ts index eaa62f0..e2a4bbc 100644 --- a/extensions/vscode/src/utils/constants.ts +++ b/extensions/vscode/src/utils/constants.ts @@ -1,6 +1,5 @@ import { PACKAGE_JSON_BASENAME, PNPM_WORKSPACE_BASENAME, YARN_WORKSPACE_BASENAME } from 'npmx-language-core/constants' -export const PACKAGE_JSON_PATTERN = `**/${PACKAGE_JSON_BASENAME}` export const SUPPORTED_DOCUMENT_PATTERN = `**/{${PACKAGE_JSON_BASENAME},${PNPM_WORKSPACE_BASENAME},${YARN_WORKSPACE_BASENAME}}` export const PRERELEASE_PATTERN = /-.+/ diff --git a/packages/language-core/package.json b/packages/language-core/package.json index 82f8c8d..fd7ca7e 100644 --- a/packages/language-core/package.json +++ b/packages/language-core/package.json @@ -42,10 +42,10 @@ "yaml": "catalog:inline" }, "inlinedDependencies": { - "module-replacements": "2.11.0", - "jsonc-parser": "3.3.1", - "yaml": "2.8.2", "fast-npm-meta": "1.4.2", - "pathe": "2.0.3" + "jsonc-parser": "3.3.1", + "module-replacements": "2.11.0", + "pathe": "2.0.3", + "yaml": "2.8.3" } } diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 0460508..93d5877 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -34,21 +34,21 @@ "vscode-uri": "catalog:lsp" }, "inlinedDependencies": { - "vscode-languageserver": "9.0.1", - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5", - "vscode-languageserver-protocol": "3.17.5", - "@volar/source-map": "2.4.28", "@volar/language-core": "2.4.28", - "vscode-uri": "3.1.0", - "@volar/language-service": "2.4.28", - "vscode-languageserver-textdocument": "1.0.12", - "request-light": "0.7.0", "@volar/language-server": "2.4.28", - "path-browserify": "1.0.1", + "@volar/language-service": "2.4.28", + "@volar/source-map": "2.4.28", "@volar/typescript": "2.4.28", - "semver": "7.7.4", + "ocache": "0.1.4", "ohash": "2.0.11", - "ocache": "0.1.4" + "path-browserify": "1.0.1", + "request-light": "0.7.0", + "semver": "7.7.4", + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver": "9.0.1", + "vscode-languageserver-protocol": "3.17.5", + "vscode-languageserver-textdocument": "1.0.12", + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "3.1.0" } } diff --git a/packages/language-service/src/index.ts b/packages/language-service/src/index.ts index ee872d9..d8d7f6f 100644 --- a/packages/language-service/src/index.ts +++ b/packages/language-service/src/index.ts @@ -1,9 +1,11 @@ import type { LanguageServicePlugin } from '@volar/language-service' import type { IWorkspaceState } from './types' +import { create as createNpmxCatalogService } from './plugins/catalog' import { create as createNpmxHoverService } from './plugins/hover' export function createNpmxLanguageServicePlugins(workspace: IWorkspaceState): LanguageServicePlugin[] { return [ + createNpmxCatalogService(workspace), createNpmxHoverService(workspace), ] } diff --git a/packages/language-service/src/plugins/catalog.ts b/packages/language-service/src/plugins/catalog.ts new file mode 100644 index 0000000..2467111 --- /dev/null +++ b/packages/language-service/src/plugins/catalog.ts @@ -0,0 +1,118 @@ +import type { CompletionItemKind, CompletionList, LanguageServicePlugin, LanguageServicePluginInstance, LocationLink } from '@volar/language-service' +import type { IWorkspaceState } from '../types' +import { isDependencyFile, normalizeCatalogName } from 'npmx-language-core/utils' +import { URI } from 'vscode-uri' +import { getResolvedDependencyAtOffset } from '../utils/range' + +export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { + return { + name: 'npmx-catalog', + capabilities: { + completionProvider: { + triggerCharacters: [':'], + }, + definitionProvider: true, + }, + create(context): LanguageServicePluginInstance { + return { + async provideCompletionItems(document, position): Promise { + const uri = URI.parse(document.uri) + if (uri.scheme !== 'file' || !isDependencyFile(uri.path)) + return + + const offset = document.offsetAt(position) + const dependencies = await workspaceState.getResolvedDependencies(document.uri) + if (!dependencies) + return + + const dep = getResolvedDependencyAtOffset(dependencies, offset) + if (!dep?.rawSpec.startsWith('catalog:')) + return + + const ctx = await workspaceState.getWorkspaceContext(document.uri) + if (!ctx) + return + + const catalogs = await ctx.getCatalogs() + if (!catalogs) + return + + const items: CompletionList['items'] = [] + + for (const [name, catalog] of Object.entries(catalogs)) { + const version = catalog[dep.resolvedName] + if (!version) + continue + + items.push({ + label: name, + kind: 12 satisfies typeof CompletionItemKind.Value, + detail: version, + }) + } + + return { isIncomplete: false, items } + }, + + async provideDefinition(document, position): Promise { + const uri = URI.parse(document.uri) + if (uri.scheme !== 'file' || !isDependencyFile(uri.path)) + return + + const offset = document.offsetAt(position) + const dependencies = await workspaceState.getResolvedDependencies(document.uri) + if (!dependencies) + return + + const dep = getResolvedDependencyAtOffset(dependencies, offset) + if (!dep?.rawSpec.startsWith('catalog:')) + return + + const ctx = await workspaceState.getWorkspaceContext(document.uri) + if (!ctx?.workspaceFilePath) + return + + const workspaceFileInfo = await ctx.loadWorkspaceFileInfo(ctx.workspaceFilePath) + if (!workspaceFileInfo) + return + + const target = workspaceFileInfo.dependencies.find( + (d) => + d.rawName === dep.resolvedName + && d.categoryName != null && dep.categoryName != null + && normalizeCatalogName(d.categoryName) === normalizeCatalogName(dep.categoryName), + ) + if (!target) + return + + const workspaceFileUri = uri.with({ path: ctx.workspaceFilePath }) + const sourceScript = context.language.scripts.get(workspaceFileUri) + if (!sourceScript) + return + + const workspaceDocument = context.documents.get(sourceScript.id, sourceScript.languageId, sourceScript.snapshot) + + const [targetStart, targetEnd] = target.specRange + const originStart = document.positionAt(dep.specRange[0]) + const originEnd = document.positionAt(dep.specRange[1]) + + return [{ + targetUri: workspaceFileUri.toString(), + targetRange: { + start: workspaceDocument.positionAt(targetStart), + end: workspaceDocument.positionAt(targetEnd), + }, + targetSelectionRange: { + start: workspaceDocument.positionAt(targetStart), + end: workspaceDocument.positionAt(targetEnd), + }, + originSelectionRange: { + start: originStart, + end: originEnd, + }, + }] + }, + } + }, + } +} From c8c99955e5d0a62de1f4ad2855efcfbdc63a038f Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Mar 2026 16:27:01 +0800 Subject: [PATCH 2/3] code simplifier --- .../language-service/src/plugins/catalog.ts | 83 +++++++++++-------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/packages/language-service/src/plugins/catalog.ts b/packages/language-service/src/plugins/catalog.ts index 2467111..a470682 100644 --- a/packages/language-service/src/plugins/catalog.ts +++ b/packages/language-service/src/plugins/catalog.ts @@ -1,10 +1,38 @@ import type { CompletionItemKind, CompletionList, LanguageServicePlugin, LanguageServicePluginInstance, LocationLink } from '@volar/language-service' +import type { DependencyInfo } from 'npmx-language-core/workspace' import type { IWorkspaceState } from '../types' import { isDependencyFile, normalizeCatalogName } from 'npmx-language-core/utils' import { URI } from 'vscode-uri' import { getResolvedDependencyAtOffset } from '../utils/range' export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { + function getDependencyFileUri(documentUri: string): URI | undefined { + const uri = URI.parse(documentUri) + if (uri.scheme !== 'file' || !isDependencyFile(uri.path)) + return + + return uri + } + + async function getCatalogDependency(documentUri: string, offset: number): Promise { + const dependencies = await workspaceState.getResolvedDependencies(documentUri) + if (!dependencies) + return + + const dependency = getResolvedDependencyAtOffset(dependencies, offset) + if (!dependency?.rawSpec.startsWith('catalog:')) + return + + return dependency + } + + function matchesCatalogDependency(candidate: DependencyInfo, dependency: DependencyInfo): boolean { + return candidate.rawName === dependency.resolvedName + && candidate.categoryName != null + && dependency.categoryName != null + && normalizeCatalogName(candidate.categoryName) === normalizeCatalogName(dependency.categoryName) + } + return { name: 'npmx-catalog', capabilities: { @@ -16,31 +44,27 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { create(context): LanguageServicePluginInstance { return { async provideCompletionItems(document, position): Promise { - const uri = URI.parse(document.uri) - if (uri.scheme !== 'file' || !isDependencyFile(uri.path)) + const dependencyFileUri = getDependencyFileUri(document.uri) + if (!dependencyFileUri) return const offset = document.offsetAt(position) - const dependencies = await workspaceState.getResolvedDependencies(document.uri) - if (!dependencies) - return - - const dep = getResolvedDependencyAtOffset(dependencies, offset) - if (!dep?.rawSpec.startsWith('catalog:')) + const dependency = await getCatalogDependency(document.uri, offset) + if (!dependency) return - const ctx = await workspaceState.getWorkspaceContext(document.uri) - if (!ctx) + const workspaceContext = await workspaceState.getWorkspaceContext(document.uri) + if (!workspaceContext) return - const catalogs = await ctx.getCatalogs() + const catalogs = await workspaceContext.getCatalogs() if (!catalogs) return const items: CompletionList['items'] = [] for (const [name, catalog] of Object.entries(catalogs)) { - const version = catalog[dep.resolvedName] + const version = catalog[dependency.resolvedName] if (!version) continue @@ -55,46 +79,39 @@ export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { }, async provideDefinition(document, position): Promise { - const uri = URI.parse(document.uri) - if (uri.scheme !== 'file' || !isDependencyFile(uri.path)) + const dependencyFileUri = getDependencyFileUri(document.uri) + if (!dependencyFileUri) return const offset = document.offsetAt(position) - const dependencies = await workspaceState.getResolvedDependencies(document.uri) - if (!dependencies) - return - - const dep = getResolvedDependencyAtOffset(dependencies, offset) - if (!dep?.rawSpec.startsWith('catalog:')) + const dependency = await getCatalogDependency(document.uri, offset) + if (!dependency) return - const ctx = await workspaceState.getWorkspaceContext(document.uri) - if (!ctx?.workspaceFilePath) + const workspaceContext = await workspaceState.getWorkspaceContext(document.uri) + if (!workspaceContext?.workspaceFilePath) return - const workspaceFileInfo = await ctx.loadWorkspaceFileInfo(ctx.workspaceFilePath) + const workspaceFileInfo = await workspaceContext.loadWorkspaceFileInfo(workspaceContext.workspaceFilePath) if (!workspaceFileInfo) return - const target = workspaceFileInfo.dependencies.find( - (d) => - d.rawName === dep.resolvedName - && d.categoryName != null && dep.categoryName != null - && normalizeCatalogName(d.categoryName) === normalizeCatalogName(dep.categoryName), + const targetDependency = workspaceFileInfo.dependencies.find((candidate) => + matchesCatalogDependency(candidate, dependency), ) - if (!target) + if (!targetDependency) return - const workspaceFileUri = uri.with({ path: ctx.workspaceFilePath }) + const workspaceFileUri = dependencyFileUri.with({ path: workspaceContext.workspaceFilePath }) const sourceScript = context.language.scripts.get(workspaceFileUri) if (!sourceScript) return const workspaceDocument = context.documents.get(sourceScript.id, sourceScript.languageId, sourceScript.snapshot) - const [targetStart, targetEnd] = target.specRange - const originStart = document.positionAt(dep.specRange[0]) - const originEnd = document.positionAt(dep.specRange[1]) + const [targetStart, targetEnd] = targetDependency.specRange + const originStart = document.positionAt(dependency.specRange[0]) + const originEnd = document.positionAt(dependency.specRange[1]) return [{ targetUri: workspaceFileUri.toString(), From 9a4c0ffb3dd8bb40ca446d84348f1f996d6d4e8b Mon Sep 17 00:00:00 2001 From: Vida Xie Date: Fri, 27 Mar 2026 17:52:25 +0800 Subject: [PATCH 3/3] fix: only check is package manifest file --- packages/language-service/src/plugins/catalog.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/language-service/src/plugins/catalog.ts b/packages/language-service/src/plugins/catalog.ts index a470682..41db272 100644 --- a/packages/language-service/src/plugins/catalog.ts +++ b/packages/language-service/src/plugins/catalog.ts @@ -1,14 +1,14 @@ import type { CompletionItemKind, CompletionList, LanguageServicePlugin, LanguageServicePluginInstance, LocationLink } from '@volar/language-service' import type { DependencyInfo } from 'npmx-language-core/workspace' import type { IWorkspaceState } from '../types' -import { isDependencyFile, normalizeCatalogName } from 'npmx-language-core/utils' +import { isPackageManifest, normalizeCatalogName } from 'npmx-language-core/utils' import { URI } from 'vscode-uri' import { getResolvedDependencyAtOffset } from '../utils/range' export function create(workspaceState: IWorkspaceState): LanguageServicePlugin { function getDependencyFileUri(documentUri: string): URI | undefined { const uri = URI.parse(documentUri) - if (uri.scheme !== 'file' || !isDependencyFile(uri.path)) + if (uri.scheme !== 'file' || !isPackageManifest(uri.path)) return return uri