diff --git a/packages/compiler-tsx/src/vue/compile.ts b/packages/compiler-tsx/src/vue/compile.ts index b7710004..3ed806d9 100644 --- a/packages/compiler-tsx/src/vue/compile.ts +++ b/packages/compiler-tsx/src/vue/compile.ts @@ -123,11 +123,16 @@ export function compileWithDecodedSourceMap( builder.append( [ `import * as ${resolvedOptions.typeIdentifier} from '${resolvedOptions.typeCheckModuleName}';`, - `import { ${['defineComponent', 'GlobalComponents'] - .map( - (id) => `${id} as ${resolvedOptions.internalIdentifierPrefix}${id}`, - ) - .join(', ')} } from '${resolvedOptions.runtimeModuleName}';`, + `declare const ${ + resolvedOptions.internalIdentifierPrefix + }defineComponent: typeof import(${JSON.stringify( + resolvedOptions.runtimeModuleName, + )}).defineComponent;`, + `type ${ + resolvedOptions.internalIdentifierPrefix + }GlobalComponents = import(${JSON.stringify( + resolvedOptions.runtimeModuleName, + )}).GlobalComponents;`, ].join('\n'), ) builder.nextLine() diff --git a/packages/typescript-plugin-vue/src/features/CodeFixService.ts b/packages/typescript-plugin-vue/src/features/CodeFixService.ts index 8b372040..c98413d0 100644 --- a/packages/typescript-plugin-vue/src/features/CodeFixService.ts +++ b/packages/typescript-plugin-vue/src/features/CodeFixService.ts @@ -1,4 +1,4 @@ -import { debug } from '@vuedx/shared' +import { VueSFCDocument } from '@vuedx/vue-virtual-textdocument' import { inject, injectable } from 'inversify' import type { TSLanguageService, TypeScript } from '../contracts/TypeScript' import { FilesystemService } from '../services/FilesystemService' @@ -16,7 +16,6 @@ export class CodeFixService private readonly fs: FilesystemService, ) {} - @debug() public getCodeFixesAtPosition( fileName: string, start: number, @@ -25,53 +24,67 @@ export class CodeFixService formatOptions: TypeScript.FormatCodeSettings, preferences: TypeScript.UserPreferences, ): readonly TypeScript.CodeFixAction[] { - return this.#resolveCodeFixActions( - this.fs.isVueFile(fileName) - ? this.#getVueCodeFixesAtPosition( - fileName, + return ( + this.pick(fileName, start, { + script: (file) => { + const span = file.findGeneratedTextSpan({ start, - end, - errorCodes, - formatOptions, - preferences, + length: end - start, + }) + if (span == null) return [] + + return this.processCodeFixActions( + this.ts.service.getCodeFixesAtPosition( + file.generatedFileName, + span.start, + span.start + span.length, + errorCodes, + formatOptions, + preferences, + ), ) - : this.ts.service.getCodeFixesAtPosition( - fileName, - start, - end, - errorCodes, - formatOptions, - preferences, - ), + }, + }) ?? [] ) } - #getVueCodeFixesAtPosition( - fileName: string, - start: number, - end: number, - errorCodes: readonly number[], + public getCombinedCodeFix( + scope: TypeScript.CombinedCodeFixScope, + fixId: {}, formatOptions: TypeScript.FormatCodeSettings, preferences: TypeScript.UserPreferences, - ): readonly TypeScript.CodeFixAction[] { - const block = this.fs.getVueFile(fileName) - if (block == null) return [] - - const genreatedStart = block.generatedOffsetAt(start) - const generatedEnd = block.generatedOffsetAt(end) - if (genreatedStart == null || generatedEnd == null) return [] + ): TypeScript.CombinedCodeActions { + const file = this.fs.getVueFile(scope.fileName) + if (file == null) return { changes: [], commands: [] } - return this.ts.service.getCodeFixesAtPosition( - block.generatedFileName, - genreatedStart, - generatedEnd, - errorCodes, + const result = this.ts.service.getCombinedCodeFix( + { ...scope, fileName: file.generatedFileName }, + fixId, formatOptions, preferences, ) + + return { + ...result, + changes: this.fs.resolveAllFileTextChanges(result.changes), + } } - #resolveCodeFixActions( + private pick( + fileName: string, + position: number, + fns: Record R>, + ): R | undefined { + const file = this.fs.getVueFile(fileName) + if (file == null) return + const block = file.getBlockAt(position) + if (block == null) return + const fn = fns[block.type] + if (fn == null) return + return fn(file) + } + + public processCodeFixActions( fixes: readonly TypeScript.CodeFixAction[], ): TypeScript.CodeFixAction[] { return fixes.map((fix) => ({ @@ -79,43 +92,4 @@ export class CodeFixService changes: this.fs.resolveAllFileTextChanges(fix.changes), })) } - - @debug() - public getCombinedCodeFix( - scope: TypeScript.CombinedCodeFixScope, - fixId: {}, - formatOptions: TypeScript.FormatCodeSettings, - preferences: TypeScript.UserPreferences, - ): TypeScript.CombinedCodeActions { - const result = this.fs.isVueFile(scope.fileName) - ? this.#getVueCombinedCodeFix(scope, fixId, formatOptions, preferences) - : this.ts.service.getCombinedCodeFix( - scope, - fixId, - formatOptions, - preferences, - ) - - return { - commands: result.commands, - changes: this.fs.resolveAllFileTextChanges(result.changes), - } - } - - #getVueCombinedCodeFix( - scope: TypeScript.CombinedCodeFixScope, - fixId: {}, - formatOptions: TypeScript.FormatCodeSettings, - preferences: TypeScript.UserPreferences, - ): TypeScript.CombinedCodeActions { - const file = this.fs.getVueFile(scope.fileName) - if (file == null) return { changes: [] } - - return this.ts.service.getCombinedCodeFix( - { type: 'file', fileName: file.generatedFileName }, - fixId, - formatOptions, - preferences, - ) - } } diff --git a/packages/typescript-plugin-vue/src/features/CompletionsService.ts b/packages/typescript-plugin-vue/src/features/CompletionsService.ts index 9d2b4ba2..dbce5d44 100644 --- a/packages/typescript-plugin-vue/src/features/CompletionsService.ts +++ b/packages/typescript-plugin-vue/src/features/CompletionsService.ts @@ -1,15 +1,8 @@ -import { annotations } from '@vuedx/compiler-tsx' -import { debug } from '@vuedx/shared' -import { isElementNode } from '@vuedx/template-ast-types' +import { VueSFCDocument } from '@vuedx/vue-virtual-textdocument' import { inject, injectable } from 'inversify' import type { TSLanguageService, TypeScript } from '../contracts/TypeScript' -import { isOffsetInSourceLocation } from '../helpers/isOffsetInSourceLocation' import { FilesystemService } from '../services/FilesystemService' import { LoggerService } from '../services/LoggerService' -import { - TemplateDeclarationsService, - GeneratedPositionKind, -} from '../services/TemplateDeclarationsService' import { TypescriptContextService } from '../services/TypescriptContextService' @injectable() @@ -17,11 +10,11 @@ export class CompletionsService implements Pick< TSLanguageService, - 'getCompletionsAtPosition' - // | 'getCompletionEntryDetails' - // | 'getCompletionEntrySymbol' - // | 'getDocCommentTemplateAtPosition' - // | 'getJsxClosingTagAtPosition' + | 'getCompletionsAtPosition' + | 'getCompletionEntryDetails' + | 'getCompletionEntrySymbol' + | 'getDocCommentTemplateAtPosition' + | 'getJsxClosingTagAtPosition' > { public readonly logger = new LoggerService(CompletionsService) @@ -31,54 +24,158 @@ export class CompletionsService private readonly fs: FilesystemService, @inject(TypescriptContextService) private readonly ts: TypescriptContextService, - @inject(TemplateDeclarationsService) - private readonly declarations: TemplateDeclarationsService, ) {} // TODO: provide template completions, e.g. v-model, v-for, etc. // TODO: provide v-bind and v-on completions for props and events. // TODO: provide modifiers completions for directives. - @debug() public getCompletionsAtPosition( fileName: string, position: number, options: TypeScript.GetCompletionsAtPositionOptions | undefined, ): TypeScript.WithMetadata | undefined { - const file = this.fs.getVueFile(fileName) - if (file == null) return - const generated = this.declarations.findGeneratedPosition(file, position) - if (generated == null) return - if (generated.kind === GeneratedPositionKind.TEMPLATE_NODE) { - const { node, templateRange } = generated - const offset = position - templateRange.start - if ( - isElementNode(node) && - isOffsetInSourceLocation(node.startTagLoc, offset) && - node.tag !== '' - ) { - const generatedPosition = file.generatedOffsetAt( - node.tagLoc.start.offset + templateRange.start, + return this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.processCompletionInfo( + this.ts.service.getCompletionsAtPosition( + file.generatedFileName, + generatedPosition, + options, + ), ) + }, + }) + } - if (generatedPosition != null) { - return this.ts.service.getCompletionsAtPosition( + public getCompletionEntryDetails( + fileName: string, + position: number, + entryName: string, + formatOptions: + | TypeScript.FormatCodeOptions + | TypeScript.FormatCodeSettings + | undefined, + source: string | undefined, + preferences: TypeScript.UserPreferences | undefined, + data: TypeScript.CompletionEntryData | undefined, + ): TypeScript.CompletionEntryDetails | undefined { + return this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.processCompletionEntryDetails( + this.ts.service.getCompletionEntryDetails( file.generatedFileName, - generatedPosition + - file.generated - .getText() - .slice(generatedPosition) - .indexOf(annotations.tsxCompletions), - options, - ) - } - } + generatedPosition, + entryName, + formatOptions, + source, + preferences, + data, + ), + ) + }, + }) + } + + public getCompletionEntrySymbol( + fileName: string, + position: number, + name: string, + source: string | undefined, + ): TypeScript.Symbol | undefined { + return this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.ts.service.getCompletionEntrySymbol( + file.generatedFileName, + generatedPosition, + name, + source, + ) + }, + }) + } + + public getDocCommentTemplateAtPosition( + fileName: string, + position: number, + options?: TypeScript.DocCommentTemplateOptions, + ): TypeScript.TextInsertion | undefined { + return this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.ts.service.getDocCommentTemplateAtPosition( + file.generatedFileName, + generatedPosition, + options, + ) + }, + }) + } + + public getJsxClosingTagAtPosition( + fileName: string, + position: number, + ): TypeScript.JsxClosingTagInfo | undefined { + return this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.ts.service.getJsxClosingTagAtPosition( + file.generatedFileName, + generatedPosition, + ) + }, + }) + } + + private pick( + fileName: string, + position: number, + fns: Record R>, + ): R | undefined { + const file = this.fs.getVueFile(fileName) + if (file == null) return + const block = file.getBlockAt(position) + if (block == null) return + const fn = fns[block.type] + if (fn == null) return + return fn(file) + } + + public processCompletionInfo( + info: T, + ): T { + if (info == null) return info + + return { + ...info, + entries: info.entries.flatMap((entry) => { + if (entry.name.startsWith('__VueDX_')) return [] // exclude internals + + return [entry] + }), } + } - return this.ts.service.getCompletionsAtPosition( - file.generatedFileName, - generated.position, - options, - ) + public processCompletionEntryDetails( + entryDetails: TypeScript.CompletionEntryDetails | undefined, + ): TypeScript.CompletionEntryDetails | undefined { + if (entryDetails == null) return entryDetails + + return { + ...entryDetails, + codeActions: entryDetails.codeActions?.flatMap((action) => { + const changes = this.fs.resolveAllFileTextChanges(action.changes) + if (changes.length === 0) return [] + return { ...action, changes } + }), + } } } diff --git a/packages/typescript-plugin-vue/src/features/RefactorService.ts b/packages/typescript-plugin-vue/src/features/RefactorService.ts index dbb0c52e..f914bc4f 100644 --- a/packages/typescript-plugin-vue/src/features/RefactorService.ts +++ b/packages/typescript-plugin-vue/src/features/RefactorService.ts @@ -1,5 +1,6 @@ import { inject, injectable } from 'inversify' import type { TSLanguageService, TypeScript } from '../contracts/TypeScript' +import { FilesystemService } from '../services/FilesystemService' import { TypescriptContextService } from '../services/TypescriptContextService' @injectable() @@ -9,6 +10,8 @@ export class RefactorService constructor( @inject(TypescriptContextService) private readonly ts: TypescriptContextService, + @inject(FilesystemService) + private readonly fs: FilesystemService, ) {} public organizeImports( @@ -16,6 +19,15 @@ export class RefactorService formatOptions: TypeScript.FormatCodeSettings, preferences: TypeScript.UserPreferences | undefined, ): readonly TypeScript.FileTextChanges[] { - return this.ts.service.organizeImports(args, formatOptions, preferences) + const file = this.fs.getVueFile(args.fileName) + if (file == null) return [] + + return this.fs.resolveAllFileTextChanges( + this.ts.service.organizeImports( + { ...args, fileName: file.generatedFileName }, + formatOptions, + preferences, + ), + ) } } diff --git a/packages/typescript-plugin-vue/src/features/ReferencesService.ts b/packages/typescript-plugin-vue/src/features/ReferencesService.ts index 2b9a6f50..c282208c 100644 --- a/packages/typescript-plugin-vue/src/features/ReferencesService.ts +++ b/packages/typescript-plugin-vue/src/features/ReferencesService.ts @@ -1,9 +1,11 @@ -import { debug } from '@vuedx/shared' - +import { first } from '@vuedx/shared' +import { VueSFCDocument } from '@vuedx/vue-virtual-textdocument' import { inject, injectable } from 'inversify' import type { TypeScript } from '../contracts/TypeScript' import { FilesystemService } from '../services/FilesystemService' +import { TemplateDeclarationsService } from '../services/TemplateDeclarationsService' import { TypescriptContextService } from '../services/TypescriptContextService' +import { DefinitionService } from './DefinitionService' @injectable() export class ReferencesService @@ -18,29 +20,130 @@ export class ReferencesService private readonly fs: FilesystemService, @inject(TypescriptContextService) private readonly ts: TypescriptContextService, + @inject(TemplateDeclarationsService) + private readonly declarations: TemplateDeclarationsService, + @inject(DefinitionService) + private readonly definitions: DefinitionService, ) {} - @debug() public getReferencesAtPosition( fileName: string, position: number, ): TypeScript.ReferenceEntry[] | undefined { - if (this.fs.isVueFile(fileName)) return - return this.ts.service.getReferencesAtPosition(fileName, position) + return this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.processReferences( + this.ts.service.getReferencesAtPosition( + file.generatedFileName, + generatedPosition, + ), + ) + }, + }) } - @debug() public findReferences( fileName: string, position: number, ): TypeScript.ReferencedSymbol[] | undefined { - if (this.fs.isVueFile(fileName)) return - return this.ts.service.findReferences(fileName, position) + return this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.processReferencedSymbols( + this.ts.service.findReferences( + file.generatedFileName, + generatedPosition, + ), + ) + }, + }) } - @debug() public getFileReferences(fileName: string): TypeScript.ReferenceEntry[] { - if (this.fs.isVueFile(fileName)) return [] - return this.ts.service.getFileReferences(fileName) + return this.ts.service.getFileReferences( + this.ts.getGeneratedFileName(fileName), + ) + } + + private pick( + fileName: string, + position: number, + fns: Record R>, + ): R | undefined { + const file = this.fs.getVueFile(fileName) + if (file == null) return + const block = file.getBlockAt(position) + if (block == null) return + const fn = fns[block.type] + if (fn == null) return + return fn(file) + } + + public processReferences( + references: TypeScript.ReferenceEntry[] | undefined, + ): TypeScript.ReferenceEntry[] { + if (references == null) return [] + + return references.flatMap((reference) => { + if (!this.fs.isGeneratedVueFile(reference.fileName)) return reference + const file = this.fs.getVueFile(reference.fileName) + if (file == null) return [] + + const fileName = file.originalFileName + const textSpan = file.findOriginalTextSpan(reference.textSpan) + if (textSpan != null) { + return { ...reference, fileName, textSpan } + } + + const declarations = this.declarations.getTemplateDeclaration(fileName) + const { line } = file.generated.positionAt(reference.textSpan.start) + const declaration = declarations.byLine.get(line) + if (declaration == null) return [] + + if (declaration.kind === 'variable') { + return declaration.references.flatMap((span) => { + const textSpan = file.findOriginalTextSpan(span) + if (textSpan == null) return [] + return { ...reference, fileName, textSpan } + }) + } else if (declaration.kind === 'identifier') { + return declaration.references.flatMap(({ start }) => { + const { line } = file.generated.positionAt(start) + const declaration = declarations.byLine.get(line) + if (declaration == null) return [] + if (declaration.kind !== 'variable') return [] + return declaration.references.flatMap((span) => { + const textSpan = file.findOriginalTextSpan(span) + if (textSpan == null) return [] + + return { ...reference, fileName, textSpan } + }) + }) + } + + return [] + }) + } + + public processReferencedSymbols( + symbols: TypeScript.ReferencedSymbol[] | undefined, + ): TypeScript.ReferencedSymbol[] | undefined { + if (symbols == null) return + return symbols.flatMap((symbol) => { + const definitions = this.definitions.processDefinitionInfo( + symbol.definition, + ) + if (definitions.length === 0) return [] + const references = this.processReferences(symbol.references) + if (references.length === 0) return [] + return { + ...symbol, + definition: { ...symbol.definition, ...first(definitions) }, + references, + } + }) } } diff --git a/packages/typescript-plugin-vue/src/features/RenameService.ts b/packages/typescript-plugin-vue/src/features/RenameService.ts index 742cbe4f..afb232bc 100644 --- a/packages/typescript-plugin-vue/src/features/RenameService.ts +++ b/packages/typescript-plugin-vue/src/features/RenameService.ts @@ -1,4 +1,4 @@ -import { debug } from '@vuedx/shared' +import { VueSFCDocument } from '@vuedx/vue-virtual-textdocument' import { inject, injectable } from 'inversify' import type { FormatCodeSettings, @@ -6,6 +6,7 @@ import type { } from 'typescript/lib/tsserverlibrary' import type { TSLanguageService, TypeScript } from '../contracts/TypeScript' import { FilesystemService } from '../services/FilesystemService' +import { TemplateDeclarationsService } from '../services/TemplateDeclarationsService' import { TypescriptContextService } from '../services/TypescriptContextService' @injectable() @@ -21,25 +22,36 @@ export class RenameService private readonly ts: TypescriptContextService, @inject(FilesystemService) private readonly fs: FilesystemService, + @inject(TemplateDeclarationsService) + private readonly declarations: TemplateDeclarationsService, ) {} - @debug() public getRenameInfo( fileName: string, position: number, - options?: TypeScript.RenameInfoOptions, + preferences: TypeScript.UserPreferences, ): TypeScript.RenameInfo { - if (this.fs.isVueFile(fileName)) { - return { + return ( + this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.processRenameInfo( + file, + this.ts.service.getRenameInfo( + file.generatedFileName, + generatedPosition, + preferences, + ), + ) + }, + }) ?? { canRename: false, - localizedErrorMessage: 'Cannot rename Vue files.', + localizedErrorMessage: 'Cannot rename this element.', } - } - - return this.ts.service.getRenameInfo(fileName, position, options) + ) } - @debug() public findRenameLocations( fileName: string, position: number, @@ -47,30 +59,139 @@ export class RenameService findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean, ): readonly TypeScript.RenameLocation[] | undefined { - if (this.fs.isVueFile(fileName)) return - - return this.ts.service.findRenameLocations( - fileName, - position, - findInStrings, - findInComments, - providePrefixAndSuffixTextForRename, - ) + return this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.processRenameLocations( + this.ts.service.findRenameLocations( + file.generatedFileName, + generatedPosition, + findInStrings, + findInComments, + providePrefixAndSuffixTextForRename, + ), + ) + }, + }) } - @debug() public getEditsForFileRename( oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined, ): readonly TypeScript.FileTextChanges[] { - if (this.fs.isVueFile(oldFilePath)) return [] - return this.ts.service.getEditsForFileRename( - oldFilePath, - newFilePath, - formatOptions, - preferences, + const generatedOldFilePath = this.fs.isVueFile(oldFilePath) + ? oldFilePath + : this.ts.getGeneratedFileName(oldFilePath) + const generatedNewFilePath = this.fs.isVueFile(newFilePath) + ? newFilePath + : this.ts.getGeneratedFileName(newFilePath) + return this.fs.resolveAllFileTextChanges( + this.ts.service.getEditsForFileRename( + generatedOldFilePath, + generatedNewFilePath, + formatOptions, + preferences, + ), ) } + + private pick( + fileName: string, + position: number, + fns: Record R>, + ): R | undefined { + const file = this.fs.getVueFile(fileName) + if (file == null) return + const block = file.getBlockAt(position) + if (block == null) return + const fn = fns[block.type] + if (fn == null) return + return fn(file) + } + + private processRenameInfo( + file: VueSFCDocument, + info: TypeScript.RenameInfo, + ): TypeScript.RenameInfo { + if (!info.canRename) return info + + const triggerSpan = file.findOriginalTextSpan(info.triggerSpan) + if (triggerSpan == null) { + return { + canRename: false, + localizedErrorMessage: 'Cannot rename this element.', + } + } + + if (info.fileToRename != null) { + info.fileToRename = this.fs.getRealFileNameIfAny(info.fileToRename) + } + + info.triggerSpan = triggerSpan + + return info + } + + public processRenameLocations( + locations: readonly TypeScript.RenameLocation[] | undefined, + ): readonly TypeScript.RenameLocation[] | undefined { + if (locations == null) return + + return locations.flatMap((location) => { + if (!this.fs.isGeneratedVueFile(location.fileName)) return location + + const file = this.fs.getVueFile(location.fileName) + if (file == null) return [] + + const fileName = file.originalFileName + const textSpan = file.findOriginalTextSpan(location.textSpan) + const contextSpan = + location.contextSpan != null + ? file.findOriginalTextSpan(location.contextSpan) ?? undefined + : undefined + + if (textSpan != null) { + return { ...location, fileName, textSpan, contextSpan } + } + + const { line } = file.generated.positionAt(location.textSpan.start) + const declarations = this.declarations.getTemplateDeclaration(fileName) + const declaration = declarations.byLine.get(line) + + if (declaration == null) return [] + if (declaration.kind === 'identifier') { + // const __VueDX_get_identifier_a = () => unref(a) + return declaration.references.flatMap((reference) => { + const { line } = file.generated.positionAt(reference.start) + const declaration = declarations.byLine.get(line) + if (declaration == null) return [] + if (declaration.kind !== 'variable') return [] + return declaration.references.flatMap((reference) => { + const textSpan = file.findOriginalTextSpan(reference) + if (textSpan == null) return [] + + const contextSpan = + location.contextSpan == null ? undefined : textSpan + + return { ...location, fileName, textSpan, contextSpan } + }) + }) + } else if (declaration.kind === 'variable') { + // let a = __VueDX_get_identifier_a() + return declaration.references.flatMap((reference) => { + const textSpan = file.findOriginalTextSpan(reference) + if (textSpan == null) return [] + const contextSpan = + location.contextSpan == null ? undefined : textSpan + + return { ...location, fileName, textSpan, contextSpan } + }) + } + + return [] + }) + } } diff --git a/packages/typescript-plugin-vue/src/features/SignatureHelpService.ts b/packages/typescript-plugin-vue/src/features/SignatureHelpService.ts index 71e7495b..5e0dfd80 100644 --- a/packages/typescript-plugin-vue/src/features/SignatureHelpService.ts +++ b/packages/typescript-plugin-vue/src/features/SignatureHelpService.ts @@ -1,3 +1,4 @@ +import { VueSFCDocument } from '@vuedx/vue-virtual-textdocument' import { inject, injectable } from 'inversify' import type { SignatureHelpItems } from 'typescript/lib/tsserverlibrary' import type { TSLanguageService, TypeScript } from '../contracts/TypeScript' @@ -30,37 +31,87 @@ export class SignatureHelpService position: number, options: TypeScript.SignatureHelpItemsOptions | undefined, ): SignatureHelpItems | undefined { - return this.ts.service.getSignatureHelpItems(fileName, position, options) + return this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.ts.service.getSignatureHelpItems( + file.generatedFileName, + generatedPosition, + options, + ) + }, + }) } public prepareCallHierarchy( fileName: string, position: number, ): TypeScript.CallHierarchyItem | TypeScript.CallHierarchyItem[] | undefined { - return this.ts.service.prepareCallHierarchy(fileName, position) + return this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.ts.service.prepareCallHierarchy( + file.generatedFileName, + generatedPosition, + ) + }, + }) } public provideCallHierarchyIncomingCalls( fileName: string, position: number, ): TypeScript.CallHierarchyIncomingCall[] { - if (this.fs.isVueFile(fileName)) return [] - return this.ts.service.provideCallHierarchyIncomingCalls(fileName, position) + return ( + this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.ts.service.provideCallHierarchyIncomingCalls( + file.generatedFileName, + generatedPosition, + ) + }, + }) ?? [] + ) } public provideCallHierarchyOutgoingCalls( fileName: string, position: number, ): TypeScript.CallHierarchyOutgoingCall[] { - if (this.fs.isVueFile(fileName)) return [] - return this.ts.service.provideCallHierarchyOutgoingCalls(fileName, position) + return ( + this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.ts.service.provideCallHierarchyOutgoingCalls( + file.generatedFileName, + generatedPosition, + ) + }, + }) ?? [] + ) } public getBraceMatchingAtPosition( fileName: string, position: number, ): TypeScript.TextSpan[] { - return this.ts.service.getBraceMatchingAtPosition(fileName, position) + return ( + this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + return this.ts.service.getBraceMatchingAtPosition( + file.generatedFileName, + generatedPosition, + ) + }, + }) ?? [] + ) } public isValidBraceCompletionAtPosition( @@ -68,10 +119,20 @@ export class SignatureHelpService position: number, openingBrace: number, ): boolean { - return this.ts.service.isValidBraceCompletionAtPosition( - fileName, - position, - openingBrace, + return ( + this.pick(fileName, position, { + script: (file) => { + const generatedPosition = file.generatedOffsetAt(position) + if (generatedPosition == null) return + const generatedOpeningBrace = file.generatedOffsetAt(openingBrace) + if (generatedOpeningBrace == null) return + return this.ts.service.isValidBraceCompletionAtPosition( + file.generatedFileName, + generatedPosition, + generatedOpeningBrace, + ) + }, + }) ?? false ) } @@ -80,6 +141,32 @@ export class SignatureHelpService startPos: number, endPos: number, ): TypeScript.TextSpan | undefined { - return this.ts.service.getNameOrDottedNameSpan(fileName, startPos, endPos) + return this.pick(fileName, startPos, { + script: (file) => { + const generatedStartPos = file.generatedOffsetAt(startPos) + if (generatedStartPos == null) return + const generatedEndPos = file.generatedOffsetAt(endPos) + if (generatedEndPos == null) return + return this.ts.service.getNameOrDottedNameSpan( + file.generatedFileName, + generatedStartPos, + generatedEndPos, + ) + }, + }) + } + + private pick( + fileName: string, + position: number, + fns: Record R>, + ): R | undefined { + const file = this.fs.getVueFile(fileName) + if (file == null) return + const block = file.getBlockAt(position) + if (block == null) return + const fn = fns[block.type] + if (fn == null) return + return fn(file) } } diff --git a/packages/typescript-plugin-vue/src/services/FilesystemService.ts b/packages/typescript-plugin-vue/src/services/FilesystemService.ts index b7b5c88d..3bb48d73 100644 --- a/packages/typescript-plugin-vue/src/services/FilesystemService.ts +++ b/packages/typescript-plugin-vue/src/services/FilesystemService.ts @@ -294,7 +294,7 @@ export class FilesystemService implements Disposable { public resolveFileTextChanges( fileTextChanges: T, ): T | null { - if (this.isVueTsFile(fileTextChanges.fileName)) { + if (this.isGeneratedVueFile(fileTextChanges.fileName)) { const asFileTextChanges = (changes: T): T => { if (fileTextChanges.isNewFile !== true) return changes return { ...changes, isNewFile: fileTextChanges.isNewFile } diff --git a/packages/typescript-plugin-vue/src/services/TypescriptContextService.ts b/packages/typescript-plugin-vue/src/services/TypescriptContextService.ts index e51f4dde..2e94e441 100644 --- a/packages/typescript-plugin-vue/src/services/TypescriptContextService.ts +++ b/packages/typescript-plugin-vue/src/services/TypescriptContextService.ts @@ -338,9 +338,7 @@ export class TypescriptContextService implements Disposable { public ensureUptoDate(fileName: string): void { this.project.getLanguageService(true) // forces update if (isVueFile(fileName)) { - fileName = this.isTypeScriptProject - ? `${fileName}.tsx` - : `${fileName}.jsx` + fileName = this.getGeneratedFileName(fileName) } if ( @@ -355,6 +353,11 @@ export class TypescriptContextService implements Disposable { } } + public getGeneratedFileName(fileName: string): string { + invariant(isVueFile(fileName), 'fileName must be a vue file') + return this.isTypeScriptProject ? `${fileName}.tsx` : `${fileName}.jsx` + } + /** @deprecated */ public ensureProject(fileName: string): void { const filePath = this.toNormalizedPath(fileName) diff --git a/packages/typescript-plugin-vue/src/services/TypescriptPluginService.ts b/packages/typescript-plugin-vue/src/services/TypescriptPluginService.ts index 217c705f..c4d66abf 100644 --- a/packages/typescript-plugin-vue/src/services/TypescriptPluginService.ts +++ b/packages/typescript-plugin-vue/src/services/TypescriptPluginService.ts @@ -1,4 +1,4 @@ -import { cache, debug, invariant } from '@vuedx/shared' +import { cache, invariant } from '@vuedx/shared' import { inject, injectable } from 'inversify' import type { ExtendedTSLanguageService, @@ -356,15 +356,28 @@ export class TypescriptPluginService preferences: TypeScript.UserPreferences | undefined, data: TypeScript.CompletionEntryData | undefined, ): TypeScript.CompletionEntryDetails | undefined { - if (this.fs.isVueFile(fileName)) return - return this.ts.service.getCompletionEntryDetails( + return this.pick( fileName, - position, - entryName, - formatOptions, - source, - preferences, - data, + () => + this.completions.getCompletionEntryDetails( + fileName, + position, + entryName, + formatOptions, + source, + preferences, + data, + ), + () => + this.ts.service.getCompletionEntryDetails( + fileName, + position, + entryName, + formatOptions, + source, + preferences, + data, + ), ) } @@ -374,12 +387,22 @@ export class TypescriptPluginService name: string, source: string | undefined, ): TypeScript.Symbol | undefined { - if (this.fs.isVueFile(fileName)) return - return this.ts.service.getCompletionEntrySymbol( + return this.pick( fileName, - position, - name, - source, + () => + this.completions.getCompletionEntrySymbol( + fileName, + position, + name, + source, + ), + () => + this.ts.service.getCompletionEntrySymbol( + fileName, + position, + name, + source, + ), ) } @@ -388,11 +411,20 @@ export class TypescriptPluginService position: number, options?: TypeScript.DocCommentTemplateOptions, ): TypeScript.TextInsertion | undefined { - if (this.fs.isVueFile(fileName)) return - return this.ts.service.getDocCommentTemplateAtPosition( + return this.pick( fileName, - position, - options, + () => + this.completions.getDocCommentTemplateAtPosition( + fileName, + position, + options, + ), + () => + this.ts.service.getDocCommentTemplateAtPosition( + fileName, + position, + options, + ), ) } @@ -400,8 +432,11 @@ export class TypescriptPluginService fileName: string, position: number, ): TypeScript.JsxClosingTagInfo | undefined { - if (this.fs.isVueFile(fileName)) return - return this.ts.service.getJsxClosingTagAtPosition(fileName, position) + return this.pick( + fileName, + () => this.completions.getJsxClosingTagAtPosition(fileName, position), + () => this.ts.service.getJsxClosingTagAtPosition(fileName, position), + ) } //#endregion @@ -433,22 +468,30 @@ export class TypescriptPluginService fileName: string, position: number, ): TypeScript.ReferenceEntry[] | undefined { - if (this.fs.isVueFile(fileName)) return - return this.ts.service.getReferencesAtPosition(fileName, position) + return this.pick( + fileName, + () => this.references.getReferencesAtPosition(fileName, position), + () => this.ts.service.getReferencesAtPosition(fileName, position), + ) } - @debug() public findReferences( fileName: string, position: number, ): TypeScript.ReferencedSymbol[] | undefined { - if (this.fs.isVueFile(fileName)) return - return this.ts.service.findReferences(fileName, position) + return this.pick( + fileName, + () => this.references.findReferences(fileName, position), + () => this.ts.service.findReferences(fileName, position), + ) } public getFileReferences(fileName: string): TypeScript.ReferenceEntry[] { - if (this.fs.isVueFile(fileName)) return [] - return this.ts.service.getFileReferences(fileName) + return this.pick( + fileName, + () => this.references.getFileReferences(fileName), + () => this.ts.service.getFileReferences(fileName), + ) } //#endregion @@ -505,8 +548,11 @@ export class TypescriptPluginService formatOptions: TypeScript.FormatCodeSettings, preferences: TypeScript.UserPreferences | undefined, ): readonly TypeScript.FileTextChanges[] { - if (this.fs.isVueFile(args.fileName)) return [] - return this.ts.service.organizeImports(args, formatOptions, preferences) + return this.pick( + args.fileName, + () => this.refactor.organizeImports(args, formatOptions, preferences), + () => this.ts.service.organizeImports(args, formatOptions, preferences), + ) } public toggleLineComment( @@ -548,14 +594,11 @@ export class TypescriptPluginService position: number, preferences: TypeScript.UserPreferences, ): TypeScript.RenameInfo { - if (this.fs.isVueFile(fileName)) { - return { - canRename: false, - localizedErrorMessage: 'Cannot rename in .vue files', - } - } - - return this.ts.service.getRenameInfo(fileName, position, preferences) + return this.pick( + fileName, + () => this.rename.getRenameInfo(fileName, position, preferences), + () => this.ts.service.getRenameInfo(fileName, position, preferences), + ) } public findRenameLocations( @@ -565,13 +608,24 @@ export class TypescriptPluginService findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean, ): readonly TypeScript.RenameLocation[] | undefined { - if (this.fs.isVueFile(fileName)) return - return this.ts.service.findRenameLocations( + return this.pick( fileName, - position, - findInStrings, - findInComments, - providePrefixAndSuffixTextForRename, + () => + this.rename.findRenameLocations( + fileName, + position, + findInStrings, + findInComments, + providePrefixAndSuffixTextForRename, + ), + () => + this.ts.service.findRenameLocations( + fileName, + position, + findInStrings, + findInComments, + providePrefixAndSuffixTextForRename, + ), ) } @@ -581,13 +635,21 @@ export class TypescriptPluginService formatOptions: TypeScript.FormatCodeSettings, preferences: TypeScript.UserPreferences | undefined, ): readonly TypeScript.FileTextChanges[] { - if (this.fs.isVueFile(oldFilePath)) return [] - return this.ts.service.getEditsForFileRename( - oldFilePath, - newFilePath, - formatOptions, - preferences, - ) + if (this.fs.isVueFile(oldFilePath) || this.fs.isVueFile(newFilePath)) { + return this.rename.getEditsForFileRename( + oldFilePath, + newFilePath, + formatOptions, + preferences, + ) + } else { + return this.ts.service.getEditsForFileRename( + oldFilePath, + newFilePath, + formatOptions, + preferences, + ) + } } //#endregion @@ -600,14 +662,26 @@ export class TypescriptPluginService formatOptions: TypeScript.FormatCodeSettings, preferences: TypeScript.UserPreferences, ): readonly TypeScript.CodeFixAction[] { - if (this.fs.isVueFile(fileName)) return [] - return this.ts.service.getCodeFixesAtPosition( + return this.pick( fileName, - start, - end, - errorCodes, - formatOptions, - preferences, + () => + this.codeFix.getCodeFixesAtPosition( + fileName, + start, + end, + errorCodes, + formatOptions, + preferences, + ), + () => + this.ts.service.getCodeFixesAtPosition( + fileName, + start, + end, + errorCodes, + formatOptions, + preferences, + ), ) } @@ -617,12 +691,22 @@ export class TypescriptPluginService formatOptions: TypeScript.FormatCodeSettings, preferences: TypeScript.UserPreferences, ): TypeScript.CombinedCodeActions { - if (this.fs.isVueFile(scope.fileName)) return { changes: [] } - return this.ts.service.getCombinedCodeFix( - scope, - fixId, - formatOptions, - preferences, + return this.pick( + scope.fileName, + () => + this.codeFix.getCombinedCodeFix( + scope, + fixId, + formatOptions, + preferences, + ), + () => + this.ts.service.getCombinedCodeFix( + scope, + fixId, + formatOptions, + preferences, + ), ) } //#endregion @@ -681,39 +765,59 @@ export class TypescriptPluginService position: number, options: TypeScript.SignatureHelpItemsOptions, ): TypeScript.SignatureHelpItems | undefined { - if (this.fs.isVueFile(fileName)) return - return this.ts.service.getSignatureHelpItems(fileName, position, options) + return this.pick( + fileName, + () => this.signature.getSignatureHelpItems(fileName, position, options), + () => this.ts.service.getSignatureHelpItems(fileName, position, options), + ) } public prepareCallHierarchy( fileName: string, position: number, ): TypeScript.CallHierarchyItem | TypeScript.CallHierarchyItem[] | undefined { - if (this.fs.isVueFile(fileName)) return - return this.ts.service.prepareCallHierarchy(fileName, position) + return this.pick( + fileName, + () => this.signature.prepareCallHierarchy(fileName, position), + () => this.ts.service.prepareCallHierarchy(fileName, position), + ) } public provideCallHierarchyIncomingCalls( fileName: string, position: number, ): TypeScript.CallHierarchyIncomingCall[] { - if (this.fs.isVueFile(fileName)) return [] - return this.ts.service.provideCallHierarchyIncomingCalls(fileName, position) + return this.pick( + fileName, + () => + this.signature.provideCallHierarchyIncomingCalls(fileName, position), + () => + this.ts.service.provideCallHierarchyIncomingCalls(fileName, position), + ) } public provideCallHierarchyOutgoingCalls( fileName: string, position: number, ): TypeScript.CallHierarchyOutgoingCall[] { - if (this.fs.isVueFile(fileName)) return [] - return this.ts.service.provideCallHierarchyOutgoingCalls(fileName, position) + return this.pick( + fileName, + () => + this.signature.provideCallHierarchyOutgoingCalls(fileName, position), + () => + this.ts.service.provideCallHierarchyOutgoingCalls(fileName, position), + ) } public getBraceMatchingAtPosition( fileName: string, position: number, ): TypeScript.TextSpan[] { - return this.ts.service.getBraceMatchingAtPosition(fileName, position) + return this.pick( + fileName, + () => this.signature.getBraceMatchingAtPosition(fileName, position), + () => this.ts.service.getBraceMatchingAtPosition(fileName, position), + ) } public isValidBraceCompletionAtPosition( @@ -721,11 +825,20 @@ export class TypescriptPluginService position: number, openingBrace: number, ): boolean { - if (this.fs.isVueFile(fileName)) return false - return this.ts.service.isValidBraceCompletionAtPosition( + return this.pick( fileName, - position, - openingBrace, + () => + this.signature.isValidBraceCompletionAtPosition( + fileName, + position, + openingBrace, + ), + () => + this.ts.service.isValidBraceCompletionAtPosition( + fileName, + position, + openingBrace, + ), ) } @@ -734,8 +847,11 @@ export class TypescriptPluginService startPos: number, endPos: number, ): TypeScript.TextSpan | undefined { - if (this.fs.isVueFile(fileName)) return - return this.ts.service.getNameOrDottedNameSpan(fileName, startPos, endPos) + return this.pick( + fileName, + () => this.signature.getNameOrDottedNameSpan(fileName, startPos, endPos), + () => this.ts.service.getNameOrDottedNameSpan(fileName, startPos, endPos), + ) } //#endregion