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/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..ad1183c 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, useDocumentText, 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,28 +19,45 @@ 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 activeDocumentText = useDocumentText(activeDocument) + 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 + }) + + const flush = debounce((doc: TextDocument, diagnostics: Diagnostic[]) => { + if (doc.version !== activeDocument.value?.version) + 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) + if (enabledRules.value.length === 0) + return + const root = extractor.parse(document) if (!root) return @@ -48,10 +65,6 @@ export function registerDiagnosticCollection(mapping: Record { - diagnosticCollection.set(document.uri, [...diagnostics]) - }, 100) - dependencies.forEach(async (dep) => { try { const pkg = await getPackageInfo(dep.name) @@ -68,7 +81,7 @@ export function registerDiagnosticCollection(mapping: Record { - 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([activeDocumentText, 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,