import { CallExpression , FunctionDeclaration , Node , NodePath , Printable , StringLiteral , TypeName , } from 'ast-types' ; import { JsCodeShift } from 'jscodeshift' ; import astService , { AstRoot , LanguageId , Selection } from './astService' ; interface UnordedSelection { start : number ; end : number ; } function fromSelection ( selection ) { let start ; let end ; if ( selection . anchor > selection . active ) { start = selection . active ; end = selection . anchor ; } else { start = selection . anchor ; end = selection . active ; } return { start , end , } ; } function toSelection ( result , previousSelection ) { if ( previousSelection . anchor > previousSelection . active ) { return { anchor : result . end , active : result . start , } ; } else { return { anchor : result . start , active : result . end , } ; } } function wrapBrackets ( source , start , end , brackets ) { while ( start > 0 && source [ start ] !== brackets [ 0 ] ) { start -- ; } while ( end < source . length && source [ end - 1 ] !== brackets [ 1 ] ) { end ++ ; } return { start , end , } ; } function equalSelections ( selA , selB ) { return selA . active === selB . active && selA . anchor === selB . anchor ; } class SmartSelectionService { private _selectionCache : Map < string , { source : string ; selectionStack : Selection [ ] [ ] ; } > = new Map ( ) ; public extendSelection ( { languageId , source , fileName , ast , selections , } ) { const j = astService . getCodeShift ( languageId ) ; let changed = false ; const newSelections = selections . map ( ( sel ) => { const result = this . _extendOneSelection ( j , source , ast , sel ) ; if ( ! result ) { return sel ; } else { changed = true ; return toSelection ( result , sel ) ; } } ) ; if ( ! changed ) { return selections ; } return this . _pushSelections ( fileName , source , newSelections , selections ) ; } public shrinkSelection ( { languageId , fileName , source , ast , selections , } ) { const jscodeshift = astService . getCodeShift ( languageId ) ; return this . _popSelections ( fileName , source , selections ) ; } private _extendOneSelection ( j , source , ast , selection ) { const target = ast . findNodeInRange ( selection . anchor , selection . active ) ; if ( target . length === 0 ) { return null ; } const { start , end } = fromSelection ( selection ) ; let result = { start , end , } ; let targetNode = target . firstNode < Node > ( ) ! ; let targetPath = target . firstPath < Node > ( ) ! ; while ( start === targetNode . start && end === targetNode . end ) { if ( ! targetPath . parentPath ) { return null ; } if ( Array . isArray ( targetPath . parentPath . value ) && targetPath . parentPath . value . length > 1 ) { const siblings = targetPath . parentPath . value as Node [ ] ; result . start = siblings [ 0 ] . start ; result . end = siblings [ siblings . length - 1 ] . end ; return result ; } targetPath = targetPath . parent as NodePath < Node > ; targetNode = targetPath . node ; } if ( ! j . Node . check ( targetNode ) ) { return null ; } switch ( targetNode . type as TypeName ) { case 'StringLiteral' : result = this . _extendStringLiteral ( targetNode as StringLiteral , start , end ) ; break ; case 'BlockStatement' : case 'ArrayExpression' : case 'ObjectExpression' : case 'ClassBody' : case 'TSTypeLiteral' : case 'TSInterfaceBody' : result = this . _extendBracketNode ( targetNode , start , end ) ; break ; case 'FunctionDeclaration' : result = this . _extendFunctionDeclaration ( targetNode as FunctionDeclaration , start , end , source ) ; break ; case 'CallExpression' : result = this . _extendCallExpression ( targetNode as CallExpression , start , end , source ) ; break ; default : result . start = targetNode . start ; result . end = targetNode . end ; break ; } return result ; } private _extendCallExpression ( targetNode , start , end , source ) { if ( targetNode . arguments . length > 0 && targetNode . arguments [ 0 ] . start === start && targetNode . arguments [ targetNode . arguments . length - 1 ] . end === end ) { return wrapBrackets ( source , start , end , '()' ) ; } else { start = targetNode . start ; end = targetNode . end ; } return { start , end } ; } private _extendFunctionDeclaration ( targetNode , start , end , source ) { if ( targetNode . params . length > 0 && targetNode . params [ 0 ] . start === start && targetNode . params [ targetNode . params . length - 1 ] . end === end ) { return wrapBrackets ( source , start , end , '()' ) ; } else { start = targetNode . start ; end = targetNode . end ; } return { start , end } ; } private _extendBracketNode ( targetNode , start , end ) { if ( targetNode . start === start || targetNode . end === end ) { start = targetNode . start ; end = targetNode . end ; } else if ( targetNode . start + 1 === start && targetNode . end - 1 === end ) { start = targetNode . start ; end = targetNode . end ; } else { start = targetNode . start + 1 ; end = targetNode . end - 1 ; } return { start , end } ; } private _extendStringLiteral ( targetNode , start , end ) { if ( targetNode . start === start || targetNode . end === end ) { start = targetNode . start ; end = targetNode . end ; } else if ( targetNode . start + 1 === start && targetNode . end - 1 === end ) { start = targetNode . start ; end = targetNode . end ; } else { const value = targetNode . value ; let startIndex = start - targetNode . start - 1 ; let endIndex = end - targetNode . start - 1 ; let expanded = false ; while ( startIndex > 0 && /[a-zA-Z0-9$_]/ . test ( value [ startIndex - 1 ] ) ) { startIndex -- ; expanded = true ; } while ( endIndex < value . length && /[a-zA-Z0-9$_]/ . test ( value [ endIndex ] ) ) { endIndex ++ ; expanded = true ; } if ( expanded ) { start = startIndex + targetNode . start + 1 ; end = endIndex + targetNode . start + 1 ; } else { start = targetNode . start + 1 ; end = targetNode . end - 1 ; } } return { start , end } ; } private _pushSelections ( fileName , source , newSelections , activeSelections ) { let cache = this . _selectionCache . get ( fileName ) ; let invalidCache = ! cache ; if ( cache ) { const storedActiveSelections = cache . selectionStack [ cache . selectionStack . length - 1 ] ; const allSelectionsPresentInCache = activeSelections . every ( ( sel ) => { return storedActiveSelections . some ( ( val ) => equalSelections ( sel , val ) ) ; } ) ; const cacheHasRemovedSelections = storedActiveSelections . some ( ( sel ) => { return ! activeSelections . some ( ( val ) => equalSelections ( sel , val ) ) ; } ) ; invalidCache = ! allSelectionsPresentInCache || cacheHasRemovedSelections ; } if ( invalidCache ) { cache = { source , selectionStack : [ activeSelections ] , } ; this . _selectionCache . set ( fileName , cache ) ; } cache ! . selectionStack . push ( newSelections ) ; return newSelections ; } private _popSelections ( fileName , source , activeSelections ) { function collapseSelection ( sel ) { return { active : sel . active , anchor : sel . active , } ; } const cache = this . _selectionCache . get ( fileName ) ; if ( ! cache || cache . selectionStack . length < 2 ) { this . _selectionCache . delete ( fileName ) ; return activeSelections . map ( collapseSelection ) ; } const storedActiveSelections = cache . selectionStack . pop ( ) ! ; const storedPrevSelections = cache . selectionStack [ cache . selectionStack . length - 1 ] ; const newSelections = activeSelections . map ( ( sel ) => { const index = storedActiveSelections . findIndex ( ( val ) => equalSelections ( sel , val ) ) ; if ( index !== - 1 ) { return storedPrevSelections [ index ] ; } else { return collapseSelection ( sel ) ; } } ) ; return newSelections ; } } export default new SmartSelectionService ( ) ;