@@ -6,13 +6,23 @@ import {
6
6
TextDocumentEdit ,
7
7
TextEdit ,
8
8
VersionedTextDocumentIdentifier ,
9
+ WorkspaceEdit ,
9
10
} from 'vscode-languageserver' ;
10
- import { Document , mapRangeToOriginal } from '../../../lib/documents' ;
11
+ import { Document , mapRangeToOriginal , isRangeInTag } from '../../../lib/documents' ;
11
12
import { pathToUrl } from '../../../utils' ;
12
13
import { CodeActionsProvider } from '../../interfaces' ;
13
14
import { SnapshotFragment } from '../DocumentSnapshot' ;
14
15
import { LSAndTSDocResolver } from '../LSAndTSDocResolver' ;
15
16
import { convertRange } from '../utils' ;
17
+ import { flatten } from '../../../utils' ;
18
+ import ts from 'typescript' ;
19
+
20
+ interface RefactorArgs {
21
+ type : 'refactor' ;
22
+ refactorName : string ;
23
+ textRange : ts . TextRange ;
24
+ originalRange : Range ;
25
+ }
16
26
17
27
export class CodeActionsProviderImpl implements CodeActionsProvider {
18
28
constructor ( private readonly lsAndTsDocResolver : LSAndTSDocResolver ) { }
@@ -26,10 +36,17 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
26
36
return await this . organizeImports ( document ) ;
27
37
}
28
38
29
- if ( ! context . only || context . only . includes ( CodeActionKind . QuickFix ) ) {
39
+ if (
40
+ context . diagnostics . length &&
41
+ ( ! context . only || context . only . includes ( CodeActionKind . QuickFix ) )
42
+ ) {
30
43
return await this . applyQuickfix ( document , range , context ) ;
31
44
}
32
45
46
+ if ( ! context . only || context . only . includes ( CodeActionKind . Refactor ) ) {
47
+ return await this . getApplicableRefactors ( document , range ) ;
48
+ }
49
+
33
50
return [ ] ;
34
51
}
35
52
@@ -124,6 +141,150 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
124
141
) ;
125
142
}
126
143
144
+ private async getApplicableRefactors ( document : Document , range : Range ) : Promise < CodeAction [ ] > {
145
+ if (
146
+ ! isRangeInTag ( range , document . scriptInfo ) &&
147
+ ! isRangeInTag ( range , document . moduleScriptInfo )
148
+ ) {
149
+ return [ ] ;
150
+ }
151
+
152
+ const { lang, tsDoc } = this . getLSAndTSDoc ( document ) ;
153
+ const fragment = await tsDoc . getFragment ( ) ;
154
+ const textRange = {
155
+ pos : fragment . offsetAt ( fragment . getGeneratedPosition ( range . start ) ) ,
156
+ end : fragment . offsetAt ( fragment . getGeneratedPosition ( range . end ) ) ,
157
+ } ;
158
+ const applicableRefactors = lang . getApplicableRefactors (
159
+ document . getFilePath ( ) || '' ,
160
+ textRange ,
161
+ undefined ,
162
+ ) ;
163
+
164
+ return (
165
+ this . applicableRefactorsToCodeActions ( applicableRefactors , document , range , textRange )
166
+ // Only allow refactorings from which we know they work
167
+ . filter (
168
+ ( refactor ) =>
169
+ refactor . command ?. command . includes ( 'function_scope' ) ||
170
+ refactor . command ?. command . includes ( 'constant_scope' ) ,
171
+ )
172
+ // The language server also proposes extraction into const/function in module scope,
173
+ // which is outside of the render function, which is svelte2tsx-specific and unmapped,
174
+ // so it would both not work and confuse the user ("What is this render? Never declared that").
175
+ // So filter out the module scope proposal and rename the render-title
176
+ . filter ( ( refactor ) => ! refactor . title . includes ( 'module scope' ) )
177
+ . map ( ( refactor ) => ( {
178
+ ...refactor ,
179
+ title : refactor . title
180
+ . replace (
181
+ `Extract to inner function in function 'render'` ,
182
+ 'Extract to function' ,
183
+ )
184
+ . replace ( `Extract to constant in function 'render'` , 'Extract to constant' ) ,
185
+ } ) )
186
+ ) ;
187
+ }
188
+
189
+ private applicableRefactorsToCodeActions (
190
+ applicableRefactors : ts . ApplicableRefactorInfo [ ] ,
191
+ document : Document ,
192
+ originalRange : Range ,
193
+ textRange : { pos : number ; end : number } ,
194
+ ) {
195
+ return flatten (
196
+ applicableRefactors . map ( ( applicableRefactor ) => {
197
+ if ( applicableRefactor . inlineable === false ) {
198
+ return [
199
+ CodeAction . create ( applicableRefactor . description , {
200
+ title : applicableRefactor . description ,
201
+ command : applicableRefactor . name ,
202
+ arguments : [
203
+ document . uri ,
204
+ < RefactorArgs > {
205
+ type : 'refactor' ,
206
+ textRange,
207
+ originalRange,
208
+ refactorName : 'Extract Symbol' ,
209
+ } ,
210
+ ] ,
211
+ } ) ,
212
+ ] ;
213
+ }
214
+
215
+ return applicableRefactor . actions . map ( ( action ) => {
216
+ return CodeAction . create ( action . description , {
217
+ title : action . description ,
218
+ command : action . name ,
219
+ arguments : [
220
+ document . uri ,
221
+ < RefactorArgs > {
222
+ type : 'refactor' ,
223
+ textRange,
224
+ originalRange,
225
+ refactorName : applicableRefactor . name ,
226
+ } ,
227
+ ] ,
228
+ } ) ;
229
+ } ) ;
230
+ } ) ,
231
+ ) ;
232
+ }
233
+
234
+ async executeCommand (
235
+ document : Document ,
236
+ command : string ,
237
+ args ?: any [ ] ,
238
+ ) : Promise < WorkspaceEdit | null > {
239
+ if ( ! ( args ?. [ 1 ] ?. type === 'refactor' ) ) {
240
+ return null ;
241
+ }
242
+
243
+ const { lang, tsDoc } = this . getLSAndTSDoc ( document ) ;
244
+ const fragment = await tsDoc . getFragment ( ) ;
245
+ const path = document . getFilePath ( ) || '' ;
246
+ const { refactorName, originalRange, textRange } = < RefactorArgs > args [ 1 ] ;
247
+
248
+ const edits = lang . getEditsForRefactor (
249
+ path ,
250
+ { } ,
251
+ textRange ,
252
+ refactorName ,
253
+ command ,
254
+ undefined ,
255
+ ) ;
256
+ if ( ! edits || edits . edits . length === 0 ) {
257
+ return null ;
258
+ }
259
+
260
+ const documentChanges = edits ?. edits . map ( ( edit ) =>
261
+ TextDocumentEdit . create (
262
+ VersionedTextDocumentIdentifier . create ( document . uri , null ) ,
263
+ edit . textChanges . map ( ( edit ) => {
264
+ let range = mapRangeToOriginal ( fragment , convertRange ( fragment , edit . span ) ) ;
265
+ // Some refactorings place the new code at the end of svelte2tsx' render function,
266
+ // which is unmapped. In this case, add it to the end of the script tag ourselves.
267
+ if ( range . start . line < 0 || range . end . line < 0 ) {
268
+ if ( isRangeInTag ( originalRange , document . scriptInfo ) ) {
269
+ range = Range . create (
270
+ document . scriptInfo . endPos ,
271
+ document . scriptInfo . endPos ,
272
+ ) ;
273
+ } else if ( isRangeInTag ( originalRange , document . moduleScriptInfo ) ) {
274
+ range = Range . create (
275
+ document . moduleScriptInfo . endPos ,
276
+ document . moduleScriptInfo . endPos ,
277
+ ) ;
278
+ }
279
+ }
280
+ return TextEdit . replace ( range , edit . newText ) ;
281
+ } ) ,
282
+ ) ,
283
+ ) ;
284
+
285
+ return { documentChanges } ;
286
+ }
287
+
127
288
private getLSAndTSDoc ( document : Document ) {
128
289
return this . lsAndTsDocResolver . getLSAndTSDoc ( document ) ;
129
290
}
0 commit comments