1
1
import { GetConfig } from '../types'
2
- import { dedentString , findChildContainingPositionMaxDepth } from '../utils'
2
+ import {
3
+ createDummySourceFile ,
4
+ dedentString ,
5
+ findChildContainingExactPosition ,
6
+ findChildContainingPosition ,
7
+ findChildContainingPositionMaxDepth ,
8
+ } from '../utils'
3
9
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
+ ) => {
5
16
if ( ! refactor ) return
6
17
const functionExtractors = refactor ?. actions . filter ( ( { notApplicableReason } ) => ! notApplicableReason )
7
18
if ( functionExtractors ?. length ) {
8
19
const kind = functionExtractors [ 0 ] ! . kind !
9
20
const blockScopeRefactor = functionExtractors . find ( e => e . description . startsWith ( 'Extract to inner function in' ) )
21
+ const addArrowCodeActions : ts . RefactorActionInfo [ ] = [ ]
10
22
if ( blockScopeRefactor ) {
11
- refactor ! . actions . push ( {
23
+ addArrowCodeActions . push ( {
12
24
description : 'Extract to arrow function above' ,
13
25
kind,
14
26
name : `${ blockScopeRefactor . name } _local_arrow` ,
15
27
} )
16
28
}
29
+ let addExtractToJsxRefactor = false
17
30
const globalScopeRefactor = functionExtractors . find ( e =>
18
31
[ 'Extract to function in global scope' , 'Extract to function in module scope' ] . includes ( e . description ) ,
19
32
)
20
33
if ( globalScopeRefactor ) {
21
- refactor ! . actions . push ( {
34
+ addArrowCodeActions . push ( {
22
35
description : 'Extract to arrow function in global scope above' ,
23
36
kind,
24
37
name : `${ globalScopeRefactor . name } _arrow` ,
25
38
} )
39
+
40
+ addExtractToJsxRefactor = typeof posOrRange !== 'number' && ! ! possiblyAddExtractToJsx ( sourceFile , posOrRange . pos , posOrRange . end )
26
41
}
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
27
70
}
71
+ if ( ! validPosition ) return
72
+ return true
28
73
}
29
74
30
75
export const handleFunctionRefactorEdits = (
@@ -37,8 +82,8 @@ export const handleFunctionRefactorEdits = (
37
82
refactorName : string ,
38
83
preferences : ts . UserPreferences | undefined ,
39
84
) : 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' , '' )
42
87
const { edits : originalEdits , renameFilename } = languageService . getEditsForRefactor (
43
88
fileName ,
44
89
formatOptions ,
@@ -51,6 +96,43 @@ export const handleFunctionRefactorEdits = (
51
96
const { textChanges } = originalEdits [ 0 ] !
52
97
const functionChange = textChanges . at ( - 1 ) !
53
98
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
+
54
136
const functionName = oldFunctionText . slice ( oldFunctionText . indexOf ( 'function ' ) + 'function ' . length , oldFunctionText . indexOf ( '(' ) )
55
137
functionChange . newText = oldFunctionText
56
138
. replace ( / f u n c t i o n / , 'const ' )
@@ -73,11 +155,7 @@ export const handleFunctionRefactorEdits = (
73
155
74
156
// global scope
75
157
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 )
81
159
if ( lastNode ) {
82
160
const pos = lastNode . pos + ( lastNode . getFullText ( ) . match ( / ^ \s + / ) ?. [ 0 ] ?. length ?? 1 ) - 1
83
161
functionChange . span . start = pos
@@ -95,3 +173,44 @@ export const handleFunctionRefactorEdits = (
95
173
renameFilename,
96
174
}
97
175
}
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