@@ -6,13 +6,23 @@ import {
66 TextDocumentEdit ,
77 TextEdit ,
88 VersionedTextDocumentIdentifier ,
9+ WorkspaceEdit ,
910} from 'vscode-languageserver' ;
10- import { Document , mapRangeToOriginal } from '../../../lib/documents' ;
11+ import { Document , mapRangeToOriginal , isRangeInTag } from '../../../lib/documents' ;
1112import { pathToUrl } from '../../../utils' ;
1213import { CodeActionsProvider } from '../../interfaces' ;
1314import { SnapshotFragment } from '../DocumentSnapshot' ;
1415import { LSAndTSDocResolver } from '../LSAndTSDocResolver' ;
1516import { 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+ }
1626
1727export class CodeActionsProviderImpl implements CodeActionsProvider {
1828 constructor ( private readonly lsAndTsDocResolver : LSAndTSDocResolver ) { }
@@ -26,10 +36,17 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
2636 return await this . organizeImports ( document ) ;
2737 }
2838
29- if ( ! context . only || context . only . includes ( CodeActionKind . QuickFix ) ) {
39+ if (
40+ context . diagnostics . length &&
41+ ( ! context . only || context . only . includes ( CodeActionKind . QuickFix ) )
42+ ) {
3043 return await this . applyQuickfix ( document , range , context ) ;
3144 }
3245
46+ if ( ! context . only || context . only . includes ( CodeActionKind . Refactor ) ) {
47+ return await this . getApplicableRefactors ( document , range ) ;
48+ }
49+
3350 return [ ] ;
3451 }
3552
@@ -124,6 +141,150 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
124141 ) ;
125142 }
126143
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+
127288 private getLSAndTSDoc ( document : Document ) {
128289 return this . lsAndTsDocResolver . getLSAndTSDoc ( document ) ;
129290 }
0 commit comments