diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1e493df --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "npmx" + ] +} diff --git a/package.json b/package.json index df2600e..f312372 100644 --- a/package.json +++ b/package.json @@ -167,7 +167,6 @@ "typescript": "^5.9.3", "vitest": "^4.0.18", "vscode-ext-gen": "1.3.0", - "vscode-uri": "^3.1.0", "yaml": "^2.8.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f293501..8efc04f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,9 +62,6 @@ importers: vscode-ext-gen: specifier: 1.3.0 version: 1.3.0 - vscode-uri: - specifier: ^3.1.0 - version: 3.1.0 yaml: specifier: ^2.8.2 version: 2.8.2 diff --git a/src/composables/active-extractor.ts b/src/composables/active-extractor.ts new file mode 100644 index 0000000..a1b6598 --- /dev/null +++ b/src/composables/active-extractor.ts @@ -0,0 +1,22 @@ +import type { Extractor } from '#types/extractor' +import { PACKAGE_JSON_BASENAME, PNPM_WORKSPACE_BASENAME } from '#constants' +import { computed, useActiveTextEditor } from 'reactive-vscode' +import { languages } from 'vscode' +import { PackageJsonExtractor } from '../extractors/package-json' +import { PnpmWorkspaceYamlExtractor } from '../extractors/pnpm-workspace-yaml' + +export const extractorEntries = [ + { pattern: `**/${PACKAGE_JSON_BASENAME}`, extractor: new PackageJsonExtractor() }, + { pattern: `**/${PNPM_WORKSPACE_BASENAME}`, extractor: new PnpmWorkspaceYamlExtractor() }, +] + +export function useActiveExtractor() { + const activeEditor = useActiveTextEditor() + + return computed(() => { + const document = activeEditor.value?.document + if (!document) + return + return extractorEntries.find(({ pattern }) => languages.match({ pattern }, document))?.extractor + }) +} diff --git a/src/constants.ts b/src/constants.ts index 345a2be..663c4ad 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,9 +1,6 @@ export const PACKAGE_JSON_BASENAME = 'package.json' export const PNPM_WORKSPACE_BASENAME = 'pnpm-workspace.yaml' -export const PACKAGE_JSON_PATTERN = `**/${PACKAGE_JSON_BASENAME}` -export const PNPM_WORKSPACE_PATTERN = `**/${PNPM_WORKSPACE_BASENAME}` - export const VERSION_TRIGGER_CHARACTERS = [':', '^', '~', '.', ...Array.from({ length: 10 }).map((_, i) => `${i}`)] export const PRERELEASE_PATTERN = /-.+/ diff --git a/src/index.ts b/src/index.ts index 048a7d3..d785549 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,43 +1,26 @@ -import { - PACKAGE_JSON_BASENAME, - PACKAGE_JSON_PATTERN, - PNPM_WORKSPACE_BASENAME, - PNPM_WORKSPACE_PATTERN, - VERSION_TRIGGER_CHARACTERS, -} from '#constants' +import { extractorEntries } from '#composables/active-extractor' +import { VERSION_TRIGGER_CHARACTERS } from '#constants' import { defineExtension, useCommands, watchEffect } from 'reactive-vscode' import { CodeActionKind, Disposable, languages } from 'vscode' import { openFileInNpmx } from './commands/open-file-in-npmx' import { openInBrowser } from './commands/open-in-browser' -import { PackageJsonExtractor } from './extractors/package-json' -import { PnpmWorkspaceYamlExtractor } from './extractors/pnpm-workspace-yaml' import { commands, displayName, version } from './generated-meta' import { UpgradeProvider } from './providers/code-actions/upgrade' import { VersionCompletionItemProvider } from './providers/completion-item/version' -import { registerDiagnosticCollection } from './providers/diagnostics' +import { useDiagnostics } from './providers/diagnostics' import { NpmxHoverProvider } from './providers/hover/npmx' import { config, logger } from './state' export const { activate, deactivate } = defineExtension(() => { logger.info(`${displayName} Activated, v${version}`) - const packageJsonExtractor = new PackageJsonExtractor() - const pnpmWorkspaceYamlExtractor = new PnpmWorkspaceYamlExtractor() - watchEffect((onCleanup) => { if (!config.hover.enabled) return - const disposables = [ - languages.registerHoverProvider( - { pattern: PACKAGE_JSON_PATTERN }, - new NpmxHoverProvider(packageJsonExtractor), - ), - languages.registerHoverProvider( - { pattern: PNPM_WORKSPACE_PATTERN }, - new NpmxHoverProvider(pnpmWorkspaceYamlExtractor), - ), - ] + const disposables = extractorEntries.map(({ pattern, extractor }) => + languages.registerHoverProvider({ pattern }, new NpmxHoverProvider(extractor)), + ) onCleanup(() => Disposable.from(...disposables).dispose()) }) @@ -46,18 +29,13 @@ export const { activate, deactivate } = defineExtension(() => { if (config.completion.version === 'off') return - const disposables = [ - languages.registerCompletionItemProvider( - { pattern: PACKAGE_JSON_PATTERN }, - new VersionCompletionItemProvider(packageJsonExtractor), - ...VERSION_TRIGGER_CHARACTERS, - ), + const disposables = extractorEntries.map(({ pattern, extractor }) => languages.registerCompletionItemProvider( - { pattern: PNPM_WORKSPACE_PATTERN }, - new VersionCompletionItemProvider(pnpmWorkspaceYamlExtractor), + { pattern }, + new VersionCompletionItemProvider(extractor), ...VERSION_TRIGGER_CHARACTERS, ), - ] + ) onCleanup(() => Disposable.from(...disposables).dispose()) }) @@ -68,18 +46,14 @@ export const { activate, deactivate } = defineExtension(() => { const provider = new UpgradeProvider() const options = { providedCodeActionKinds: [CodeActionKind.QuickFix] } - const disposable = Disposable.from( - languages.registerCodeActionsProvider({ pattern: PACKAGE_JSON_PATTERN }, provider, options), - languages.registerCodeActionsProvider({ pattern: PNPM_WORKSPACE_PATTERN }, provider, options), + const disposables = extractorEntries.map(({ pattern }) => + languages.registerCodeActionsProvider({ pattern }, provider, options), ) - onCleanup(() => disposable.dispose()) + onCleanup(() => Disposable.from(...disposables).dispose()) }) - registerDiagnosticCollection({ - [PACKAGE_JSON_BASENAME]: packageJsonExtractor, - [PNPM_WORKSPACE_BASENAME]: pnpmWorkspaceYamlExtractor, - }) + useDiagnostics() useCommands({ [commands.openInBrowser]: openInBrowser, diff --git a/src/providers/diagnostics/index.ts b/src/providers/diagnostics/index.ts index 629d6b4..0f3f941 100644 --- a/src/providers/diagnostics/index.ts +++ b/src/providers/diagnostics/index.ts @@ -1,13 +1,13 @@ -import type { DependencyInfo, Extractor, ValidNode } from '#types/extractor' +import type { DependencyInfo, ValidNode } from '#types/extractor' import type { PackageInfo } from '#utils/api/package' import type { Awaitable } from 'reactive-vscode' import type { Diagnostic, TextDocument } from 'vscode' +import { useActiveExtractor } from '#composables/active-extractor' import { config, logger } from '#state' import { getPackageInfo } from '#utils/api/package' import { debounce } from 'perfect-debounce' -import { computed, useActiveTextEditor, useDocumentText, watch } from 'reactive-vscode' +import { computed, useActiveTextEditor, useDisposable, watch } from 'reactive-vscode' import { languages } from 'vscode' -import { Utils } from 'vscode-uri' import { displayName } from '../../generated-meta' import { checkDeprecation } from './rules/deprecation' import { checkReplacement } from './rules/replacement' @@ -19,74 +19,91 @@ export interface NodeDiagnosticInfo extends Omit } export type DiagnosticRule = (dep: DependencyInfo, pkg: PackageInfo) => Awaitable -const enabledRules = computed(() => { - const rules: DiagnosticRule[] = [] - if (config.diagnostics.upgrade) - rules.push(checkUpgrade) - if (config.diagnostics.deprecation) - rules.push(checkDeprecation) - if (config.diagnostics.replacement) - rules.push(checkReplacement) - if (config.diagnostics.vulnerability) - rules.push(checkVulnerability) - return rules -}) - -export function registerDiagnosticCollection(mapping: Record) { - const diagnosticCollection = languages.createDiagnosticCollection(displayName) +export function useDiagnostics() { + const diagnosticCollection = useDisposable(languages.createDiagnosticCollection(displayName)) const activeEditor = useActiveTextEditor() - const activeDocumentText = useDocumentText(() => activeEditor.value?.document) + const activeDocument = computed(() => activeEditor.value?.document) + const activeDocumentVersion = computed(() => activeDocument.value?.version) + const activeExtractor = useActiveExtractor() + + const enabledRules = computed(() => { + const rules: DiagnosticRule[] = [] + if (config.diagnostics.upgrade) + rules.push(checkUpgrade) + if (config.diagnostics.deprecation) + rules.push(checkDeprecation) + if (config.diagnostics.replacement) + rules.push(checkReplacement) + if (config.diagnostics.vulnerability) + rules.push(checkVulnerability) + return rules + }) + + function isDocumentChanged(document: TextDocument, targetUri: string, targetVersion: number) { + return document.uri.toString() !== targetUri || document.version !== targetVersion + } + + const flush = debounce((doc: TextDocument, targetUri: string, targetVersion: number, diagnostics: Diagnostic[]) => { + if (isDocumentChanged(doc, targetUri, targetVersion)) + return + + diagnosticCollection.set(doc.uri, [...diagnostics]) + }, 100) + + async function collectDiagnostics() { + const extractor = activeExtractor.value + const document = activeEditor.value?.document + if (!extractor || !document) + return - async function collectDiagnostics(document: TextDocument, extractor: Extractor) { diagnosticCollection.delete(document.uri) + const rules = enabledRules.value + if (rules.length === 0) + return + const root = extractor.parse(document) if (!root) return + const targetUri = document.uri.toString() + const targetVersion = document.version + const dependencies = extractor.getDependenciesInfo(root) const diagnostics: Diagnostic[] = [] - const flush = debounce(() => { - diagnosticCollection.set(document.uri, [...diagnostics]) - }, 100) + for (const dep of dependencies) { + if (isDocumentChanged(document, targetUri, targetVersion)) + return - dependencies.forEach(async (dep) => { try { const pkg = await getPackageInfo(dep.name) - if (!pkg) + if (isDocumentChanged(document, targetUri, targetVersion)) return + if (!pkg) + continue - enabledRules.value.forEach(async (rule) => { + for (const rule of rules) { const diagnostic = await rule(dep, pkg) + if (isDocumentChanged(document, targetUri, targetVersion)) + return + if (!diagnostic) + continue - if (diagnostic) { - diagnostics.push({ - source: displayName, - range: extractor.getNodeRange(document, diagnostic.node), - ...diagnostic, - }) + diagnostics.push({ + source: displayName, + range: extractor.getNodeRange(document, diagnostic.node), + ...diagnostic, + }) - flush() - } - }) + flush(document, targetUri, targetVersion, diagnostics) + } } catch (err) { logger.warn(`Failed to check ${dep.name}: ${err}`) } - }) + } } - watch(activeDocumentText, async () => { - const editor = activeEditor.value - if (!editor) - return - - const document = editor.document - const filename = Utils.basename(document.uri) - const extractor = mapping[filename] - - if (extractor) - await collectDiagnostics(document, extractor) - }, { immediate: true }) + watch([activeDocument, activeDocumentVersion, enabledRules], collectDiagnostics, { immediate: true }) } diff --git a/tsconfig.json b/tsconfig.json index cb7dbef..b40ea2d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ "#constants": ["./src/constants.ts"], "#state": ["./src/state.ts"], "#types/*": ["./src/types/*"], - "#utils/*": ["./src/utils/*"] + "#utils/*": ["./src/utils/*"], + "#composables/*": ["./src/composables/*"] }, "resolveJsonModule": true, "strict": true, diff --git a/tsdown.config.ts b/tsdown.config.ts index 5da0808..28b3a65 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -17,7 +17,6 @@ export default defineConfig({ 'ofetch', 'perfect-debounce', 'reactive-vscode', - 'vscode-uri', 'yaml', ], minify: 'dce-only',