diff --git a/packages/nuxt/src/typescript/features/getDefinitionAndBoundSpan.ts b/packages/nuxt/src/typescript/features/getDefinitionAndBoundSpan.ts index 1888197..11b0d89 100644 --- a/packages/nuxt/src/typescript/features/getDefinitionAndBoundSpan.ts +++ b/packages/nuxt/src/typescript/features/getDefinitionAndBoundSpan.ts @@ -1,4 +1,4 @@ -import { forEachTouchingNode, isTextSpanEqual } from "@dxup/shared"; +import { forEachTouchingNode, isTextSpanWithin } from "@dxup/shared"; import { extname, join } from "pathe"; import { globSync } from "tinyglobby"; import type ts from "typescript"; @@ -250,7 +250,7 @@ function visitRuntimeConfig( else if (ts.isPropertySignature(node) && ts.isIdentifier(node.name)) { key = node.name.text; - if (isTextSpanEqual(node.name, definition.textSpan, sourceFile)) { + if (isTextSpanWithin(node.name, definition.textSpan, sourceFile)) { path.push(key); definitions = [...forwardRuntimeConfig(context, definition, path)]; break; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 798df96..c93ba11 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -40,13 +40,13 @@ function* binaryVisit( } } -export function isTextSpanEqual( +export function isTextSpanWithin( node: ts.Node, textSpan: ts.TextSpan, sourceFile: ts.SourceFile, ) { return ( - textSpan.start + textSpan.length === node.getEnd() && - textSpan.start === node.getStart(sourceFile) + textSpan.start + textSpan.length <= node.getEnd() && + textSpan.start >= node.getStart(sourceFile) ); } diff --git a/packages/unimport/src/index.ts b/packages/unimport/src/index.ts index 873a932..ea0941f 100644 --- a/packages/unimport/src/index.ts +++ b/packages/unimport/src/index.ts @@ -1,4 +1,4 @@ -import { forEachTouchingNode, isTextSpanEqual } from "@dxup/shared"; +import { forEachTouchingNode, isTextSpanWithin } from "@dxup/shared"; import type ts from "typescript"; const plugin: ts.server.PluginModuleFactory = (module) => { @@ -7,12 +7,13 @@ const plugin: ts.server.PluginModuleFactory = (module) => { return { create(info) { for (const [key, method] of [ - ["findRenameLocations", findRenameLocations.bind(null, ts, info)], - ["findReferences", findReferences.bind(null, ts, info)], - ["getDefinitionAndBoundSpan", getDefinitionAndBoundSpan.bind(null, ts, info)], + ["findRenameLocations", findRenameLocations], + ["findReferences", findReferences], + ["getDefinitionAndBoundSpan", getDefinitionAndBoundSpan], + ["getFileReferences", getFileReferences], ] as const) { const original = info.languageService[key]; - info.languageService[key] = method(original as any) as any; + info.languageService[key] = method(ts, info, original as any) as any; } return info.languageService; @@ -24,6 +25,78 @@ export default plugin; const declarationRE = /\.d\.(?:c|m)?ts$/; +function createVisitor(getter: ( + ts: typeof import("typescript"), + name: ts.PropertyName, + type: ts.TypeNode, + textSpan: ts.TextSpan, + sourceFile: ts.SourceFile, +) => ts.Node | undefined) { + return ( + ts: typeof import("typescript"), + textSpan: ts.TextSpan, + sourceFile: ts.SourceFile, + ) => { + for (const node of forEachTouchingNode(ts, sourceFile, textSpan.start)) { + if ( + ts.isPropertySignature(node) && node.type || + ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.type + ) { + const target = getter(ts, node.name as any, node.type, textSpan, sourceFile); + if (target) { + return target; + } + } + } + }; +} + +const visitForwardImports = createVisitor((ts, name, type, textSpan, sourceFile) => { + if (!isTextSpanWithin(name, textSpan, sourceFile)) { + return; + } + + while (ts.isTypeReferenceNode(type) && type.typeArguments?.length) { + type = type.typeArguments[0]; + } + + if (ts.isIndexedAccessTypeNode(type)) { + return type.indexType; + } + else if (ts.isImportTypeNode(type)) { + return type.qualifier ?? type.argument; + } +}); + +const visitBackwardImports = createVisitor((ts, name, type, textSpan, sourceFile) => { + while (ts.isTypeReferenceNode(type) && type.typeArguments?.length) { + type = type.typeArguments[0]; + } + + const targets: ts.Node[] = []; + if (ts.isIndexedAccessTypeNode(type)) { + if (ts.isLiteralTypeNode(type.indexType) && ts.isStringLiteral(type.indexType.literal)) { + targets.push(type.indexType); + if (type.indexType.literal.text === "default" && ts.isImportTypeNode(type.objectType)) { + targets.push(type.objectType.argument); + } + } + } + else if (ts.isImportTypeNode(type)) { + targets.push(type.qualifier ?? type.argument); + if (type.qualifier && ts.isIdentifier(type.qualifier) && type.qualifier.text === "default") { + targets.push(type.argument); + } + } + else { + return; + } + + if (targets.some((target) => isTextSpanWithin(target, textSpan, sourceFile))) { + return name; + } +}); + function findRenameLocations( ts: typeof import("typescript"), info: ts.server.PluginCreateInfo, @@ -50,7 +123,8 @@ function findRenameLocations( continue; } - const node = visitImports(ts, location.textSpan, sourceFile); + const args = [ts, location.textSpan, sourceFile] as const; + const node = visitForwardImports(...args) ?? visitBackwardImports(...args); if (!node) { continue; } @@ -92,7 +166,7 @@ function findReferences( continue; } - const node = visitImports(ts, reference.textSpan, sourceFile); + const node = visitBackwardImports(ts, reference.textSpan, sourceFile); if (!node) { continue; } @@ -129,7 +203,7 @@ function getDefinitionAndBoundSpan( } const program = info.languageService.getProgram()!; - const definitions = new Set(result.definitions); + const definitions = new Set(result.definitions); for (const definition of result.definitions) { const sourceFile = program.getSourceFile(definition.fileName); @@ -141,7 +215,7 @@ function getDefinitionAndBoundSpan( continue; } - const node = visitImports(ts, definition.textSpan, sourceFile); + const node = visitForwardImports(ts, definition.textSpan, sourceFile); if (!node) { continue; } @@ -163,76 +237,45 @@ function getDefinitionAndBoundSpan( }; } -function visitImports( +function getFileReferences( ts: typeof import("typescript"), - textSpan: ts.TextSpan, - sourceFile: ts.SourceFile, -) { - for (const node of forEachTouchingNode(ts, sourceFile, textSpan.start)) { - let target: ts.Node | undefined; - - if (ts.isPropertySignature(node) && node.type) { - const args = [ts, node.name, node.type, textSpan, sourceFile] as const; - target = forwardTypeofImport(...args) ?? backwardTypeofImport(...args); - } - else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.type) { - const args = [ts, node.name, node.type, textSpan, sourceFile] as const; - target = forwardTypeofImport(...args) ?? backwardTypeofImport(...args); - } - - if (target) { - return target; + info: ts.server.PluginCreateInfo, + getFileReferences: ts.LanguageService["getFileReferences"], +): ts.LanguageService["getFileReferences"] { + return (fileName) => { + const result = getFileReferences(fileName); + if (!result?.length) { + return result; } - } -} -function forwardTypeofImport( - ts: typeof import("typescript"), - name: ts.PropertyName, - type: ts.TypeNode, - textSpan: ts.TextSpan, - sourceFile: ts.SourceFile, -) { - if (!isTextSpanEqual(name, textSpan, sourceFile)) { - return; - } - - while (ts.isTypeReferenceNode(type) && type.typeArguments?.length) { - type = type.typeArguments[0]; - } + const program = info.languageService.getProgram()!; + const references = new Set(result); - if (ts.isIndexedAccessTypeNode(type)) { - return type.indexType; - } - else if (ts.isImportTypeNode(type)) { - return type.qualifier ?? type.argument; - } -} + for (const reference of result) { + const sourceFile = program.getSourceFile(reference.fileName); + if (!sourceFile) { + continue; + } -function backwardTypeofImport( - ts: typeof import("typescript"), - name: ts.PropertyName, - type: ts.TypeNode, - textSpan: ts.TextSpan, - sourceFile: ts.SourceFile, -) { - while (ts.isTypeReferenceNode(type) && type.typeArguments?.length) { - type = type.typeArguments[0]; - } + if (!declarationRE.test(reference.fileName)) { + continue; + } - let target: ts.Node; + const node = visitBackwardImports(ts, reference.textSpan, sourceFile); + if (!node) { + continue; + } - if (ts.isIndexedAccessTypeNode(type)) { - target = type.objectType; - } - else if (ts.isImportTypeNode(type)) { - target = type.qualifier ?? type.argument; - } - else { - return; - } + const position = node.getStart(sourceFile); + const res = info.languageService.getReferencesAtPosition(reference.fileName, position); + if (res?.length) { + for (const reference of res.slice(1)) { + references.add(reference); + } + references.delete(reference); + } + } - if (isTextSpanEqual(target, textSpan, sourceFile)) { - return name; - } + return [...references]; + }; } diff --git a/playground/app/components/foo-bar.vue b/playground/app/components/foo-bar.vue index 4a131f6..58f69ca 100644 --- a/playground/app/components/foo-bar.vue +++ b/playground/app/components/foo-bar.vue @@ -3,3 +3,8 @@ void FooBar; + + +.... diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e7c5cc..7eed594 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,11 +19,11 @@ catalogs: specifier: ^2.4.23 version: 2.4.23 '@vue/language-core': - specifier: ^3.1.1 - version: 3.1.1 + specifier: ^3.1.3 + version: 3.1.3 '@vue/typescript-plugin': - specifier: ^3.1.2 - version: 3.1.2 + specifier: ^3.1.3 + version: 3.1.3 '@zinkawaii/eslint-config': specifier: ^0.4.1 version: 0.4.1 @@ -128,7 +128,7 @@ importers: version: 2.4.23 '@vue/language-core': specifier: 'catalog:' - version: 3.1.1(typescript@5.9.3) + version: 3.1.3(typescript@5.9.3) nuxt: specifier: 'catalog:' version: 4.1.3(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(eslint@9.38.0(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.11(@types/node@24.9.1)(jiti@2.6.1)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1) @@ -168,7 +168,7 @@ importers: devDependencies: '@vue/typescript-plugin': specifier: 'catalog:' - version: 3.1.2(typescript@5.9.3) + version: 3.1.3(typescript@5.9.3) typescript: specifier: 'catalog:' version: 5.9.3 @@ -1620,16 +1620,8 @@ packages: '@vue/devtools-shared@7.7.7': resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} - '@vue/language-core@3.1.1': - resolution: {integrity: sha512-qjMY3Q+hUCjdH+jLrQapqgpsJ0rd/2mAY02lZoHG3VFJZZZKLjAlV+Oo9QmWIT4jh8+Rx8RUGUi++d7T9Wb6Mw==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@vue/language-core@3.1.2': - resolution: {integrity: sha512-PyFDCqpdfYUT+oMLqcc61oHfJlC6yjhybaefwQjRdkchIihToOEpJ2Wu/Ebq2yrnJdd1EsaAvZaXVAqcxtnDxQ==} + '@vue/language-core@3.1.3': + resolution: {integrity: sha512-KpR1F/eGAG9D1RZ0/T6zWJs6dh/pRLfY5WupecyYKJ1fjVmDMgTPw9wXmKv2rBjo4zCJiOSiyB8BDP1OUwpMEA==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -1653,8 +1645,8 @@ packages: '@vue/shared@3.5.22': resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} - '@vue/typescript-plugin@3.1.2': - resolution: {integrity: sha512-2n+tiPR8RM7axxkGpYKMefW74MkGAcgqzrUN2TP3Vcv8I51UnwAz8HGI9lkABeK+Kp+yyi0DU3CBjJ3AKRNnkA==} + '@vue/typescript-plugin@3.1.3': + resolution: {integrity: sha512-IKHfr/cLOS4O48JBfESYNGPdGXwi6nWwHreIee1jR6GMFVsUzCxrhlho/LrqiFRacOhhY+LTjzWVKFi/7s+JNQ==} '@zinkawaii/eslint-config@0.4.1': resolution: {integrity: sha512-OvXb5Oxza1kTVgHjn9LOONJIQM9isc8fHwtZMLv7GzNI4ubcbNAB3eaOeeRNeVTOyp7wPyWyUi+mqYVh/Cbwyg==} @@ -6061,19 +6053,7 @@ snapshots: dependencies: rfdc: 1.4.1 - '@vue/language-core@3.1.1(typescript@5.9.3)': - dependencies: - '@volar/language-core': 2.4.23 - '@vue/compiler-dom': 3.5.22 - '@vue/shared': 3.5.22 - alien-signals: 3.0.3 - muggle-string: 0.4.1 - path-browserify: 1.0.1 - picomatch: 4.0.3 - optionalDependencies: - typescript: 5.9.3 - - '@vue/language-core@3.1.2(typescript@5.9.3)': + '@vue/language-core@3.1.3(typescript@5.9.3)': dependencies: '@volar/language-core': 2.4.23 '@vue/compiler-dom': 3.5.22 @@ -6109,10 +6089,10 @@ snapshots: '@vue/shared@3.5.22': {} - '@vue/typescript-plugin@3.1.2(typescript@5.9.3)': + '@vue/typescript-plugin@3.1.3(typescript@5.9.3)': dependencies: '@volar/typescript': 2.4.23 - '@vue/language-core': 3.1.2(typescript@5.9.3) + '@vue/language-core': 3.1.3(typescript@5.9.3) '@vue/shared': 3.5.22 path-browserify: 1.0.1 transitivePeerDependencies: @@ -9086,7 +9066,7 @@ snapshots: dependencies: '@vue-macros/common': 3.0.0-beta.16(vue@3.5.22(typescript@5.9.3)) '@vue/compiler-sfc': 3.5.22 - '@vue/language-core': 3.1.1(typescript@5.9.3) + '@vue/language-core': 3.1.3(typescript@5.9.3) ast-walker-scope: 0.8.3 chokidar: 4.0.3 json5: 2.2.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4ad9b00..39078bf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,8 +9,8 @@ catalog: "@types/node": ^24.9.1 "@volar/language-core": ^2.4.23 "@volar/typescript": ^2.4.23 - "@vue/language-core": ^3.1.1 - "@vue/typescript-plugin": ^3.1.2 + "@vue/language-core": ^3.1.3 + "@vue/typescript-plugin": ^3.1.3 "@zinkawaii/eslint-config": ^0.4.1 "@zinkawaii/tsconfig": ^0.0.2 bumpp: ^10.3.1 diff --git a/test/__snapshots__/playground.test.ts.snap b/test/__snapshots__/playground.test.ts.snap index e2b9a51..02c226d 100644 --- a/test/__snapshots__/playground.test.ts.snap +++ b/test/__snapshots__/playground.test.ts.snap @@ -1,6 +1,75 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`playground > auto imports 1`] = ` +exports[`playground > app/app.vue > definition 1`] = ` +[ + { + "fileName": "layers/archer/nuxt.config.ts", + "textSpan": { + "length": 3, + "start": 82, + }, + }, + { + "fileName": "layers/berserker/nuxt.config.ts", + "textSpan": { + "length": 3, + "start": 82, + }, + }, + { + "fileName": "nuxt.config.ts", + "textSpan": { + "length": 3, + "start": 43, + }, + }, +] +`; + +exports[`playground > app/app.vue > definition 2`] = ` +[ + { + "fileName": "layers/berserker/nuxt.config.ts", + "textSpan": { + "length": 3, + "start": 102, + }, + }, + { + "fileName": "nuxt.config.ts", + "textSpan": { + "length": 3, + "start": 59, + }, + }, +] +`; + +exports[`playground > app/app.vue > definition 3`] = ` +[ + { + "fileName": "nuxt.config.ts", + "textSpan": { + "length": 3, + "start": 75, + }, + }, +] +`; + +exports[`playground > app/app.vue > definition 4`] = ` +[ + { + "fileName": "nuxt.config.ts", + "textSpan": { + "length": 5, + "start": 112, + }, + }, +] +`; + +exports[`playground > app/app.vue > definition 5`] = ` [ { "fileName": "app/utils/index.ts", @@ -12,7 +81,7 @@ exports[`playground > auto imports 1`] = ` ] `; -exports[`playground > import glob 1`] = ` +exports[`playground > app/app.vue > definition 6`] = ` [ { "fileName": "app/assets/maestrale.webp", @@ -31,7 +100,7 @@ exports[`playground > import glob 1`] = ` ] `; -exports[`playground > import glob 2`] = ` +exports[`playground > app/app.vue > definition 7`] = ` [ { "fileName": "app/assets/maestrale.webp", @@ -50,7 +119,7 @@ exports[`playground > import glob 2`] = ` ] `; -exports[`playground > nitro routes 1`] = ` +exports[`playground > app/app.vue > definition 8`] = ` [ { "fileName": "server/routes/sitemap.post.ts", @@ -69,7 +138,7 @@ exports[`playground > nitro routes 1`] = ` ] `; -exports[`playground > nitro routes 2`] = ` +exports[`playground > app/app.vue > definition 9`] = ` [ { "fileName": "public/fallback.json", @@ -81,7 +150,7 @@ exports[`playground > nitro routes 2`] = ` ] `; -exports[`playground > nitro routes 3`] = ` +exports[`playground > app/app.vue > definition 10`] = ` [ { "fileName": "server/api/foo.get.ts", @@ -93,7 +162,7 @@ exports[`playground > nitro routes 3`] = ` ] `; -exports[`playground > nitro routes 4`] = ` +exports[`playground > app/app.vue > definition 11`] = ` [ { "fileName": "server/api/foo.post.ts", @@ -105,70 +174,34 @@ exports[`playground > nitro routes 4`] = ` ] `; -exports[`playground > runtime config 1`] = ` +exports[`playground > app/components/foo-bar.vue > references 1`] = ` [ { - "fileName": "layers/archer/nuxt.config.ts", + "fileName": "app/app.vue", "textSpan": { - "length": 3, - "start": 82, + "length": 12, + "start": 1138, }, }, { - "fileName": "layers/berserker/nuxt.config.ts", + "fileName": "app/app.vue", "textSpan": { - "length": 3, - "start": 82, + "length": 6, + "start": 1123, }, }, { - "fileName": "nuxt.config.ts", + "fileName": "app/components/foo-bar.vue", "textSpan": { - "length": 3, - "start": 43, + "length": 6, + "start": 77, }, }, -] -`; - -exports[`playground > runtime config 2`] = ` -[ { - "fileName": "layers/berserker/nuxt.config.ts", + "fileName": "app/components/foo-bar.vue", "textSpan": { - "length": 3, - "start": 102, - }, - }, - { - "fileName": "nuxt.config.ts", - "textSpan": { - "length": 3, - "start": 59, - }, - }, -] -`; - -exports[`playground > runtime config 3`] = ` -[ - { - "fileName": "nuxt.config.ts", - "textSpan": { - "length": 3, - "start": 75, - }, - }, -] -`; - -exports[`playground > runtime config 4`] = ` -[ - { - "fileName": "nuxt.config.ts", - "textSpan": { - "length": 5, - "start": 112, + "length": 6, + "start": 38, }, }, ] diff --git a/test/playground.test.ts b/test/playground.test.ts index 2f362e8..279b4c8 100644 --- a/test/playground.test.ts +++ b/test/playground.test.ts @@ -8,8 +8,8 @@ import type { Language } from "@volar/language-core"; describe("playground", () => { const logger: ts.server.Logger = { - close() {}, - endGroup() {}, + close: () => {}, + endGroup: () => {}, getLogFileName: () => void 0, hasLevel: () => false, info: () => {}, @@ -20,11 +20,11 @@ describe("playground", () => { }; const session = new ts.server.Session({ - byteLength: (buf, encoding) => Buffer.byteLength(buf, encoding), + byteLength: Buffer.byteLength, cancellationToken: ts.server.nullCancellationToken, canUseEvents: true, host: ts.sys as any, - hrtime: (start) => process.hrtime(start), + hrtime: process.hrtime, logger, useInferredProjectPerProjectRoot: false, useSingleInferredProject: false, @@ -44,6 +44,7 @@ describe("playground", () => { const playgroundRoot = resolve(import.meta.dirname, "../playground"); const appVuePath = resolve(playgroundRoot, "app/app.vue"); + const buildDir = resolve(playgroundRoot, ".nuxt"); projectService.openClientFile(appVuePath); const project = projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(appVuePath), true)!; @@ -51,79 +52,74 @@ describe("playground", () => { const program = languageService.getProgram()!; const language = (project as any).__vue__.language as Language; - const sourceFile = program.getSourceFile(appVuePath)!; - const sourceScript = language.scripts.get(appVuePath); - const serviceScript = sourceScript?.generated!.languagePlugin.typescript?.getServiceScript( - sourceScript.generated!.root, - ); - const sourceText = sourceScript?.snapshot.getText(0, sourceScript.snapshot.getLength()) ?? sourceFile.text; + const operationRE = /(?<=(?:\/\/|