11import { GetConfig } from '../types'
2- import { dedentString , findChildContainingPositionMaxDepth } from '../utils'
2+ import {
3+ createDummySourceFile ,
4+ dedentString ,
5+ findChildContainingExactPosition ,
6+ findChildContainingPosition ,
7+ findChildContainingPositionMaxDepth ,
8+ } from '../utils'
39
4- export const processApplicableRefactors = ( refactor : ts . ApplicableRefactorInfo | undefined , c : GetConfig ) => {
10+ export const processApplicableRefactors = (
11+ refactor : ts . ApplicableRefactorInfo | undefined ,
12+ c : GetConfig ,
13+ posOrRange : number | ts . TextRange ,
14+ sourceFile : ts . SourceFile ,
15+ ) => {
516 if ( ! refactor ) return
617 const functionExtractors = refactor ?. actions . filter ( ( { notApplicableReason } ) => ! notApplicableReason )
718 if ( functionExtractors ?. length ) {
819 const kind = functionExtractors [ 0 ] ! . kind !
920 const blockScopeRefactor = functionExtractors . find ( e => e . description . startsWith ( 'Extract to inner function in' ) )
21+ const addArrowCodeActions : ts . RefactorActionInfo [ ] = [ ]
1022 if ( blockScopeRefactor ) {
11- refactor ! . actions . push ( {
23+ addArrowCodeActions . push ( {
1224 description : 'Extract to arrow function above' ,
1325 kind,
1426 name : `${ blockScopeRefactor . name } _local_arrow` ,
1527 } )
1628 }
29+ let addExtractToJsxRefactor = false
1730 const globalScopeRefactor = functionExtractors . find ( e =>
1831 [ 'Extract to function in global scope' , 'Extract to function in module scope' ] . includes ( e . description ) ,
1932 )
2033 if ( globalScopeRefactor ) {
21- refactor ! . actions . push ( {
34+ addArrowCodeActions . push ( {
2235 description : 'Extract to arrow function in global scope above' ,
2336 kind,
2437 name : `${ globalScopeRefactor . name } _arrow` ,
2538 } )
39+
40+ addExtractToJsxRefactor = typeof posOrRange !== 'number' && ! ! possiblyAddExtractToJsx ( sourceFile , posOrRange . pos , posOrRange . end )
2641 }
42+
43+ if ( addExtractToJsxRefactor ) {
44+ refactor . actions = refactor . actions . filter ( action => ! action . name . startsWith ( 'function_scope' ) )
45+ refactor . actions . push ( {
46+ description : 'Extract to JSX component' ,
47+ kind : 'refactor.extract.jsx' ,
48+ name : `${ globalScopeRefactor ! . name } _jsx` ,
49+ } )
50+ return
51+ }
52+
53+ refactor . actions . push ( ...addArrowCodeActions )
54+ }
55+ }
56+
57+ const possiblyAddExtractToJsx = ( sourceFile : ts . SourceFile , start : number , end : number ) : void | true => {
58+ if ( start === end ) return
59+ let node1 = findChildContainingPosition ( ts , sourceFile , start )
60+ const node2 = findChildContainingExactPosition ( sourceFile , end )
61+ if ( ! node1 || ! node2 ) return
62+ if ( ts . isIdentifier ( node1 ) ) node1 = node1 . parent
63+ const nodeStart = node1 . pos + node1 . getLeadingTriviaWidth ( )
64+ let validPosition = false
65+ if ( node1 === node2 && ts . isJsxSelfClosingElement ( node1 ) && start === nodeStart && end === node1 . end ) {
66+ validPosition = true
67+ }
68+ if ( ts . isJsxOpeningElement ( node1 ) && ts . isJsxClosingElement ( node2 ) && node2 . parent . openingElement === node1 && start === nodeStart && end === node2 . end ) {
69+ validPosition = true
2770 }
71+ if ( ! validPosition ) return
72+ return true
2873}
2974
3075export const handleFunctionRefactorEdits = (
@@ -37,8 +82,8 @@ export const handleFunctionRefactorEdits = (
3782 refactorName : string ,
3883 preferences : ts . UserPreferences | undefined ,
3984) : ts . RefactorEditInfo | undefined => {
40- if ( ! actionName . endsWith ( '_arrow' ) ) return
41- const originalAcitonName = actionName . replace ( '_local_arrow' , '' ) . replace ( '_arrow' , '' )
85+ if ( ! actionName . endsWith ( '_arrow' ) && ! actionName . endsWith ( '_jsx' ) ) return
86+ const originalAcitonName = actionName . replace ( '_local_arrow' , '' ) . replace ( '_arrow' , '' ) . replace ( '_jsx' , '' )
4287 const { edits : originalEdits , renameFilename } = languageService . getEditsForRefactor (
4388 fileName ,
4489 formatOptions ,
@@ -51,6 +96,43 @@ export const handleFunctionRefactorEdits = (
5196 const { textChanges } = originalEdits [ 0 ] !
5297 const functionChange = textChanges . at ( - 1 ) !
5398 const oldFunctionText = functionChange . newText
99+ const sourceFile = languageService . getProgram ( ) ! . getSourceFile ( fileName ) !
100+ if ( actionName . endsWith ( '_jsx' ) ) {
101+ const lines = oldFunctionText . trimStart ( ) . split ( '\n' )
102+ const oldFunctionSignature = lines [ 0 ] !
103+ const componentName = tsFull . getUniqueName ( 'ExtractedComponent' , sourceFile as FullSourceFile )
104+ const newFunctionSignature = changeArgumentsToDestructured ( oldFunctionSignature , formatOptions , sourceFile , componentName )
105+
106+ const insertChange = textChanges . at ( - 2 ) !
107+ let args = insertChange . newText . slice ( 1 , - 2 )
108+ args = args . slice ( args . indexOf ( '(' ) + 1 )
109+ const fileEdits = [
110+ {
111+ fileName,
112+ textChanges : [
113+ ...textChanges . slice ( 0 , - 2 ) ,
114+ {
115+ ...insertChange ,
116+ newText : `<${ componentName } ${ args
117+ . split ( ', ' )
118+ . map ( identifierText => `${ identifierText } ={${ identifierText } }` )
119+ . join ( ' ' ) } />`,
120+ } ,
121+ {
122+ span : functionChange . span ,
123+ newText : oldFunctionText . match ( / \s * / ) ! [ 0 ] + newFunctionSignature . slice ( 0 , - 2 ) + '\n' + lines . slice ( 1 ) . join ( '\n' ) ,
124+ } ,
125+ ] ,
126+ } ,
127+ ]
128+ return {
129+ edits : fileEdits ,
130+ renameFilename,
131+ renameLocation : insertChange . span . start + 1 ,
132+ // renameLocation: tsFull.getRenameLocation(fileEdits, fileName, componentName, /*preferLastLocation*/ false),
133+ }
134+ }
135+
54136 const functionName = oldFunctionText . slice ( oldFunctionText . indexOf ( 'function ' ) + 'function ' . length , oldFunctionText . indexOf ( '(' ) )
55137 functionChange . newText = oldFunctionText
56138 . replace ( / f u n c t i o n / , 'const ' )
@@ -73,11 +155,7 @@ export const handleFunctionRefactorEdits = (
73155
74156 // global scope
75157 if ( ! isLocal ) {
76- const lastNode = findChildContainingPositionMaxDepth (
77- languageService . getProgram ( ) ! . getSourceFile ( fileName ) ! ,
78- typeof positionOrRange === 'object' ? positionOrRange . pos : positionOrRange ,
79- 2 ,
80- )
158+ const lastNode = findChildContainingPositionMaxDepth ( sourceFile , typeof positionOrRange === 'object' ? positionOrRange . pos : positionOrRange , 2 )
81159 if ( lastNode ) {
82160 const pos = lastNode . pos + ( lastNode . getFullText ( ) . match ( / ^ \s + / ) ?. [ 0 ] ?. length ?? 1 ) - 1
83161 functionChange . span . start = pos
@@ -95,3 +173,44 @@ export const handleFunctionRefactorEdits = (
95173 renameFilename,
96174 }
97175}
176+
177+ export function changeArgumentsToDestructured (
178+ oldFunctionSignature : string ,
179+ formatOptions : ts . FormatCodeSettings ,
180+ sourceFile : ts . SourceFile ,
181+ componentName : string ,
182+ ) {
183+ const { factory } = ts
184+ const dummySourceFile = createDummySourceFile ( oldFunctionSignature )
185+ const functionDeclaration = dummySourceFile . statements [ 0 ] as ts . FunctionDeclaration
186+ const { parameters, type : returnType } = functionDeclaration
187+ const paramNames = parameters . map ( p => p . name as ts . Identifier )
188+ const paramTypes = parameters . map ( p => p . type ! )
189+ const newFunction = factory . createFunctionDeclaration (
190+ undefined ,
191+ undefined ,
192+ componentName ,
193+ undefined ,
194+ [
195+ factory . createParameterDeclaration (
196+ undefined ,
197+ undefined ,
198+ factory . createObjectBindingPattern ( paramNames . map ( paramName => factory . createBindingElement ( undefined , undefined , paramName ) ) ) ,
199+ undefined ,
200+ factory . createTypeLiteralNode (
201+ paramNames . map ( ( paramName , i ) => {
202+ const type = paramTypes [ i ] !
203+ return factory . createPropertySignature ( undefined , paramName , undefined , type )
204+ } ) ,
205+ ) ,
206+ ) ,
207+ ] ,
208+ returnType ,
209+ factory . createBlock ( [ ] ) ,
210+ )
211+ // const changesTracker = getChangesTracker(formatOptions)
212+ // changesTracker.insertNodeAt(sourceFile, 0, newFunction)
213+ // const newFunctionText = changesTracker.getChanges()[0]!.textChanges[0]!;
214+ const newFunctionText = ts . createPrinter ( ) . printNode ( ts . EmitHint . Unspecified , newFunction , sourceFile )
215+ return newFunctionText
216+ }
0 commit comments