import { Position , WorkspaceEdit , Range } from 'vscode-languageserver' ; import { Document , mapRangeToOriginal , getLineAtPosition , offsetAt } from '../../../lib/documents' ; import { filterAsync , isNotNullOrUndefined , pathToUrl } from '../../../utils' ; import { RenameProvider } from '../../interfaces' ; import { SnapshotFragment , SvelteSnapshotFragment , SvelteDocumentSnapshot } from '../DocumentSnapshot' ; import { convertRange } from '../utils' ; import { LSAndTSDocResolver } from '../LSAndTSDocResolver' ; import ts from 'typescript' ; import { uniqWith , isEqual } from 'lodash' ; import { isComponentAtPosition , isAfterSvelte2TsxPropsReturn , isNoTextSpanInGeneratedCode , SnapshotFragmentMap , findContainingNode } from './utils' ; export class RenameProviderImpl implements RenameProvider { constructor ( private readonly lsAndTsDocResolver ) { } async prepareRename ( document , position ) { const { lang , tsDoc } = await this . getLSAndTSDoc ( document ) ; const fragment = await tsDoc . getFragment ( ) ; const offset = fragment . offsetAt ( fragment . getGeneratedPosition ( position ) ) ; const renameInfo = this . getRenameInfo ( lang , tsDoc , document , position , offset ) ; if ( ! renameInfo ) { return null ; } return this . mapRangeToOriginal ( fragment , renameInfo . triggerSpan ) ; } async rename ( document , position , newName ) { const { lang , tsDoc } = await this . getLSAndTSDoc ( document ) ; const fragment = await tsDoc . getFragment ( ) ; const offset = fragment . offsetAt ( fragment . getGeneratedPosition ( position ) ) ; if ( ! this . getRenameInfo ( lang , tsDoc , document , position , offset ) ) { return null ; } const renameLocations = lang . findRenameLocations ( tsDoc . filePath , offset , false , false , true ) ; if ( ! renameLocations ) { return null ; } const docs = new SnapshotFragmentMap ( this . lsAndTsDocResolver ) ; docs . set ( tsDoc . filePath , { fragment , snapshot : tsDoc } ) ; let convertedRenameLocations = await this . mapAndFilterRenameLocations ( renameLocations , docs ) ; convertedRenameLocations = this . checkShortHandBindingOrSlotLetLocation ( lang , convertedRenameLocations , docs ) ; const additionalRenameForPropRenameInsideComponentWithProp = await this . getAdditionLocationsForRenameOfPropInsideComponentWithProp ( document , tsDoc , fragment , position , convertedRenameLocations , docs , lang ) ; const additionalRenamesForPropRenameOutsideComponentWithProp = additionalRenameForPropRenameInsideComponentWithProp . length > 0 ? [ ] : await this . getAdditionalLocationsForRenameOfPropInsideOtherComponent ( convertedRenameLocations , docs , lang ) ; convertedRenameLocations = [ ... convertedRenameLocations , ... additionalRenameForPropRenameInsideComponentWithProp , ... additionalRenamesForPropRenameOutsideComponentWithProp ] ; return unique ( convertedRenameLocations . filter ( ( loc ) => loc . range . start . line >= 0 && loc . range . end . line >= 0 ) ) . reduce ( ( acc , loc ) => { const uri = pathToUrl ( loc . fileName ) ; if ( ! acc . changes [ uri ] ) { acc . changes [ uri ] = [ ] ; } acc . changes [ uri ] . push ( { newText : ( loc . prefixText || '' ) + newName + ( loc . suffixText || '' ) , range : loc . range } ) ; return acc ; } , < Required < Pick < WorkspaceEdit , 'changes' > > > { changes : { } } ) ; } private getRenameInfo ( lang , tsDoc , doc , originalPosition , generatedOffset ) { if ( tsDoc . parserError ) { return null ; } const renameInfo = lang . getRenameInfo ( tsDoc . filePath , generatedOffset , { allowRenameOfImportPath : false } ) ; if ( ! renameInfo . canRename || renameInfo . fullDisplayName ?. includes ( 'JSX.IntrinsicElements' ) || ( renameInfo . kind === ts . ScriptElementKind . jsxAttribute && ! isComponentAtPosition ( doc , tsDoc , originalPosition ) ) ) { return null ; } return renameInfo ; } /**
     * If user renames prop of component A inside component A,
     * we need to handle the rename of the prop of A ourselves.
     * Reason: the rename will do {oldPropName: newPropName}, meaning
     * the rename will not propagate further, so we have to handle
     * the conversion to {newPropName: newPropName} ourselves.
     */ private async getAdditionLocationsForRenameOfPropInsideComponentWithProp ( document , tsDoc , fragment , position , convertedRenameLocations , fragments , lang ) { const regex = new RegExp ( `export\\s+let\\s+${ this . getVariableAtPosition ( tsDoc , fragment , lang , position ) }($|\\s|;|:)` ) ; const isRenameInsideComponentWithProp = regex . test ( getLineAtPosition ( position , document . getText ( ) ) ) ; if ( ! isRenameInsideComponentWithProp ) { return [ ] ; } const updatePropLocation = this . findLocationWhichWantsToUpdatePropName ( convertedRenameLocations , fragments ) ; if ( ! updatePropLocation ) { return [ ] ; } const idxOfOldPropName = fragment . text . lastIndexOf ( ':' , updatePropLocation . textSpan . start ) ; const replacementsForProp = ( lang . findRenameLocations ( updatePropLocation . fileName , idxOfOldPropName , false , false ) || [ ] ) . filter ( ( rename ) => rename . fileName !== updatePropLocation . fileName || this . isInSvelte2TsxPropLine ( fragment , rename ) ) ; return await this . mapAndFilterRenameLocations ( replacementsForProp , fragments ) ; } /**
     * If user renames prop of component A inside component B,
     * we need to handle the rename of the prop of A ourselves.
     * Reason: the rename will rename the prop in the computed svelte2tsx code,
     * but not the `export let X` code in the original because the
     * rename does not propagate further than the prop.
     * This additional logic/propagation is done in this method.
     */ private async getAdditionalLocationsForRenameOfPropInsideOtherComponent ( convertedRenameLocations , fragments , lang ) { const updatePropLocation = this . findLocationWhichWantsToUpdatePropName ( convertedRenameLocations , fragments ) ; if ( ! updatePropLocation ) { return [ ] ; } const doc = < SvelteSnapshotFragment > fragments . getFragment ( updatePropLocation . fileName ) ; const match = this . matchGeneratedExportLet ( doc , updatePropLocation ) ; if ( ! match ) { return [ ] ; } const idx = ( match . index || 0 ) + match [ 0 ] . lastIndexOf ( match [ 1 ] ) ; const replacementsForProp = lang . findRenameLocations ( updatePropLocation . fileName , idx , false , false ) || [ ] ; return this . checkShortHandBindingOrSlotLetLocation ( lang , await this . mapAndFilterRenameLocations ( replacementsForProp , fragments ) , fragments ) ; } private matchGeneratedExportLet ( fragment , updatePropLocation ) { const regex = new RegExp ( `\\s+let\\s+(${ fragment . text . substr ( updatePropLocation . textSpan . start , updatePropLocation . textSpan . length ) })($|\\s|;|:)` ) ; const match = fragment . text . match ( regex ) ; return match ; } private findLocationWhichWantsToUpdatePropName ( convertedRenameLocations , fragments ) { return convertedRenameLocations . find ( ( loc ) => { if ( loc . range . start . line >= 0 && loc . range . end . line >= 0 ) { return ; } const fragment = fragments . getFragment ( loc . fileName ) ; if ( ! ( fragment instanceof SvelteSnapshotFragment ) ) { return false ; } return this . isInSvelte2TsxPropLine ( fragment , loc ) ; } ) ; } private isInSvelte2TsxPropLine ( fragment , loc ) { return isAfterSvelte2TsxPropsReturn ( fragment . text , loc . textSpan . start ) ; } /**
     * The rename locations the ts language services hands back are relative to the
     * svelte2tsx generated code -> map it back to the original document positions.
     * Some of those positions could be unmapped (line=-1), these are handled elsewhere.
     * Also filter out wrong renames.
     */ private async mapAndFilterRenameLocations ( renameLocations , fragments ) { const mappedLocations = await Promise . all ( renameLocations . map ( async ( loc ) => { const { fragment , snapshot } = await fragments . retrieve ( loc . fileName ) ; if ( isNoTextSpanInGeneratedCode ( snapshot . getFullText ( ) , loc . textSpan ) ) { return { ... loc , range : this . mapRangeToOriginal ( fragment , loc . textSpan ) } ; } } ) ) ; return this . filterWrongRenameLocations ( mappedLocations . filter ( isNotNullOrUndefined ) ) ; } private filterWrongRenameLocations ( mappedLocations ) { return filterAsync ( mappedLocations , async ( loc ) => { const snapshot = await this . getSnapshot ( loc . fileName ) ; if ( ! ( snapshot instanceof SvelteDocumentSnapshot ) ) { return true ; } const content = snapshot . getText ( 0 , snapshot . getLength ( ) ) ; return ( notPrecededBy ( '__sveltets_1_instanceOf(' ) && notPrecededBy ( '__sveltets_1_ensureType(' ) && notPrecededBy ( '= __sveltets_1_store_get(' ) ) ; function notPrecededBy ( str ) { return ( content . lastIndexOf ( str , loc . textSpan . start ) !== loc . textSpan . start - str . length ) ; } } ) ; } private mapRangeToOriginal ( doc , textSpan ) { const range = mapRangeToOriginal ( doc , convertRange ( doc , textSpan ) ) ; if ( range . end . character - range . start . character < textSpan . length ) { range . end . character ++ ; } return range ; } private getVariableAtPosition ( tsDoc , fragment , lang , position ) { const offset = fragment . offsetAt ( fragment . getGeneratedPosition ( position ) ) ; const { start , length } = lang . getSmartSelectionRange ( tsDoc . filePath , offset ) . textSpan ; return tsDoc . getText ( start , start + length ) ; } private async getLSAndTSDoc ( document ) { return this . lsAndTsDocResolver . getLSAndTSDoc ( document ) ; } private getSnapshot ( filePath ) { return this . lsAndTsDocResolver . getSnapshot ( filePath ) ; } private checkShortHandBindingOrSlotLetLocation ( lang , renameLocations , fragments ) { const bind = 'bind:' ; return renameLocations . map ( ( location ) => { const sourceFile = lang . getProgram ( ) ?. getSourceFile ( location . fileName ) ; if ( ! sourceFile || location . fileName !== sourceFile . fileName || location . range . start . line < 0 || location . range . end . line < 0 ) { return location ; } const fragment = fragments . getFragment ( location . fileName ) ; if ( ! ( fragment instanceof SvelteSnapshotFragment ) ) { return location ; } const { originalText } = fragment ; const renamingInfo = this . getShorthandPropInfo ( sourceFile , location ) ?? this . getSlotLetInfo ( sourceFile , location ) ; if ( ! renamingInfo ) { return location ; } const [ renamingNode , identifierName ] = renamingInfo ; const originalStart = offsetAt ( location . range . start , originalText ) ; const isShortHandBinding = originalText . substr ( originalStart - bind . length , bind . length ) === bind ; const directiveName = ( isShortHandBinding ? bind : '' ) + identifierName ; const prefixText = directiveName + '={' ; const newRange = mapRangeToOriginal ( fragment , convertRange ( fragment , { start : renamingNode . getStart ( ) , length : renamingNode . getWidth ( ) } ) ) ; if ( isShortHandBinding || originalText . substring ( offsetAt ( newRange . start , originalText ) , originalStart ) . trimLeft ( ) === '{' ) { newRange . start . character ++ ; } return { ... location , prefixText , suffixText : '}' , range : newRange } ; } ) ; } private getShorthandPropInfo ( sourceFile , location ) { const possibleJsxAttribute = findContainingNode ( sourceFile , location . textSpan , ts . isJsxAttribute ) ; if ( ! possibleJsxAttribute ) { return null ; } const attributeName = possibleJsxAttribute . name . getText ( ) ; const { initializer } = possibleJsxAttribute ; if ( ! initializer || ! ts . isJsxExpression ( initializer ) || attributeName !== initializer . expression ?. getText ( ) ) { return null ; } return [ possibleJsxAttribute , attributeName ] ; } private getSlotLetInfo ( sourceFile , location ) { const possibleSlotLet = findContainingNode ( sourceFile , location . textSpan , ts . isVariableDeclaration ) ; if ( ! possibleSlotLet || ! ts . isObjectBindingPattern ( possibleSlotLet . name ) ) { return null ; } const bindingElement = findContainingNode ( possibleSlotLet . name , location . textSpan , ts . isBindingElement ) ; if ( ! bindingElement || bindingElement . propertyName ) { return null ; } const identifierName = bindingElement . name . getText ( ) ; return [ bindingElement , identifierName ] ; } } function unique < T > ( array ) { return uniqWith ( array , isEqual ) ; }