Skip to content

Commit 9d2b7b0

Browse files
author
Simon Holthausen
committed
(feat) add organize-imports code action support
closes sveltejs#72
1 parent 9593987 commit 9d2b7b0

File tree

6 files changed

+319
-106
lines changed

6 files changed

+319
-106
lines changed

packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts

+4-52
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,12 @@ import {
1212
Position,
1313
Range,
1414
SymbolInformation,
15-
TextDocumentEdit,
16-
TextEdit,
17-
VersionedTextDocumentIdentifier,
1815
} from 'vscode-languageserver';
1916
import {
2017
Document,
2118
DocumentManager,
2219
mapDiagnosticToOriginal,
2320
mapHoverToParent,
24-
mapRangeToOriginal,
2521
mapSymbolInformationToOriginal,
2622
} from '../../lib/documents';
2723
import { LSConfigManager, LSTypescriptConfig } from '../../ls-config';
@@ -38,6 +34,7 @@ import {
3834
OnWatchFileChanges,
3935
} from '../interfaces';
4036
import { DocumentSnapshot, SnapshotFragment } from './DocumentSnapshot';
37+
import { CodeActionsProviderImpl } from './features/CodeActionsProvider';
4138
import {
4239
CompletionEntryWithIdentifer,
4340
CompletionsProviderImpl,
@@ -63,11 +60,13 @@ export class TypeScriptPlugin
6360
private configManager: LSConfigManager;
6461
private readonly lsAndTsDocResolver: LSAndTSDocResolver;
6562
private readonly completionProvider: CompletionsProviderImpl;
63+
private readonly codeActionsProvider: CodeActionsProviderImpl;
6664

6765
constructor(docManager: DocumentManager, configManager: LSConfigManager) {
6866
this.configManager = configManager;
6967
this.lsAndTsDocResolver = new LSAndTSDocResolver(docManager);
7068
this.completionProvider = new CompletionsProviderImpl(this.lsAndTsDocResolver);
69+
this.codeActionsProvider = new CodeActionsProviderImpl(this.lsAndTsDocResolver);
7170
}
7271

7372
async getDiagnostics(document: Document): Promise<Diagnostic[]> {
@@ -255,54 +254,7 @@ export class TypeScriptPlugin
255254
return [];
256255
}
257256

258-
const { lang, tsDoc } = this.getLSAndTSDoc(document);
259-
const fragment = await tsDoc.getFragment();
260-
261-
const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start));
262-
const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end));
263-
const errorCodes: number[] = context.diagnostics.map((diag) => Number(diag.code));
264-
const codeFixes = lang.getCodeFixesAtPosition(
265-
tsDoc.filePath,
266-
start,
267-
end,
268-
errorCodes,
269-
{},
270-
{},
271-
);
272-
273-
const docs = new Map<string, SnapshotFragment>([[tsDoc.filePath, fragment]]);
274-
return await Promise.all(
275-
codeFixes.map(async (fix) => {
276-
const documentChanges = await Promise.all(
277-
fix.changes.map(async (change) => {
278-
let doc = docs.get(change.fileName);
279-
if (!doc) {
280-
doc = await this.getSnapshot(change.fileName).getFragment();
281-
docs.set(change.fileName, doc);
282-
}
283-
return TextDocumentEdit.create(
284-
VersionedTextDocumentIdentifier.create(
285-
pathToUrl(change.fileName),
286-
null,
287-
),
288-
change.textChanges.map((edit) => {
289-
return TextEdit.replace(
290-
mapRangeToOriginal(doc!, convertRange(doc!, edit.span)),
291-
edit.newText,
292-
);
293-
}),
294-
);
295-
}),
296-
);
297-
return CodeAction.create(
298-
fix.description,
299-
{
300-
documentChanges,
301-
},
302-
fix.fixName,
303-
);
304-
}),
305-
);
257+
return this.codeActionsProvider.getCodeActions(document, range, context);
306258
}
307259

308260
onWatchFileChanges(fileName: string, changeType: FileChangeType) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import {
2+
CodeAction,
3+
CodeActionContext,
4+
CodeActionKind,
5+
Range,
6+
TextDocumentEdit,
7+
TextEdit,
8+
VersionedTextDocumentIdentifier,
9+
} from 'vscode-languageserver';
10+
import { Document, mapRangeToOriginal } from '../../../lib/documents';
11+
import { pathToUrl } from '../../../utils';
12+
import { CodeActionsProvider } from '../../interfaces';
13+
import { SnapshotFragment } from '../DocumentSnapshot';
14+
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
15+
import { convertRange } from '../utils';
16+
17+
export class CodeActionsProviderImpl implements CodeActionsProvider {
18+
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
19+
20+
async getCodeActions(
21+
document: Document,
22+
range: Range,
23+
context: CodeActionContext,
24+
): Promise<CodeAction[]> {
25+
if (context.only?.[0] === CodeActionKind.SourceOrganizeImports) {
26+
return await this.organizeImports(document);
27+
}
28+
29+
if (!context.only || context.only.includes(CodeActionKind.QuickFix)) {
30+
return await this.applyQuickfix(document, range, context);
31+
}
32+
33+
return [];
34+
}
35+
36+
private async organizeImports(document: Document): Promise<CodeAction[]> {
37+
if (!document.scriptInfo) {
38+
return [];
39+
}
40+
41+
const { lang, tsDoc } = this.getLSAndTSDoc(document);
42+
const fragment = await tsDoc.getFragment();
43+
44+
const changes = lang.organizeImports({ fileName: tsDoc.filePath, type: 'file' }, {}, {});
45+
46+
const documentChanges = await Promise.all(
47+
changes.map(async (change) => {
48+
// Organize Imports will only affect the current file, so no need to check the file path
49+
return TextDocumentEdit.create(
50+
VersionedTextDocumentIdentifier.create(document.url, null),
51+
change.textChanges.map((edit) => {
52+
let range = mapRangeToOriginal(fragment, convertRange(fragment, edit.span));
53+
// Handle svelte2tsx wrong import mapping:
54+
// The character after the last import maps to the start of the script
55+
// TODO find a way to fix this in svelte2tsx and then remove this
56+
if (range.end.line === 0 && range.end.character === 1) {
57+
edit.span.length -= 1;
58+
range = mapRangeToOriginal(fragment, convertRange(fragment, edit.span));
59+
range.end.character += 1;
60+
}
61+
return TextEdit.replace(range, edit.newText);
62+
}),
63+
);
64+
}),
65+
);
66+
67+
return [
68+
CodeAction.create(
69+
'Organize Imports',
70+
{ documentChanges },
71+
CodeActionKind.SourceOrganizeImports,
72+
),
73+
];
74+
}
75+
76+
private async applyQuickfix(document: Document, range: Range, context: CodeActionContext) {
77+
const { lang, tsDoc } = this.getLSAndTSDoc(document);
78+
const fragment = await tsDoc.getFragment();
79+
80+
const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start));
81+
const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end));
82+
const errorCodes: number[] = context.diagnostics.map((diag) => Number(diag.code));
83+
const codeFixes = lang.getCodeFixesAtPosition(
84+
tsDoc.filePath,
85+
start,
86+
end,
87+
errorCodes,
88+
{},
89+
{},
90+
);
91+
92+
const docs = new Map<string, SnapshotFragment>([[tsDoc.filePath, fragment]]);
93+
return await Promise.all(
94+
codeFixes.map(async (fix) => {
95+
const documentChanges = await Promise.all(
96+
fix.changes.map(async (change) => {
97+
let doc = docs.get(change.fileName);
98+
if (!doc) {
99+
doc = await this.getSnapshot(change.fileName).getFragment();
100+
docs.set(change.fileName, doc);
101+
}
102+
return TextDocumentEdit.create(
103+
VersionedTextDocumentIdentifier.create(
104+
pathToUrl(change.fileName),
105+
null,
106+
),
107+
change.textChanges.map((edit) => {
108+
return TextEdit.replace(
109+
mapRangeToOriginal(doc!, convertRange(doc!, edit.span)),
110+
edit.newText,
111+
);
112+
}),
113+
);
114+
}),
115+
);
116+
return CodeAction.create(
117+
fix.description,
118+
{
119+
documentChanges,
120+
},
121+
CodeActionKind.QuickFix,
122+
);
123+
}),
124+
);
125+
}
126+
127+
private getLSAndTSDoc(document: Document) {
128+
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
129+
}
130+
131+
private getSnapshot(filePath: string, document?: Document) {
132+
return this.lsAndTsDocResolver.getSnapshot(filePath, document);
133+
}
134+
}

packages/language-server/src/server.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
TextDocumentPositionParams,
88
TextDocumentIdentifier,
99
IConnection,
10+
CodeActionKind,
1011
} from 'vscode-languageserver';
1112
import { DocumentManager, Document } from './lib/documents';
1213
import {
@@ -113,7 +114,15 @@ export function startServer(options?: LSOptions) {
113114
colorProvider: true,
114115
documentSymbolProvider: true,
115116
definitionProvider: true,
116-
codeActionProvider: true,
117+
codeActionProvider: evt.capabilities.textDocument?.codeAction
118+
?.codeActionLiteralSupport
119+
? {
120+
codeActionKinds: [
121+
CodeActionKind.QuickFix,
122+
CodeActionKind.SourceOrganizeImports,
123+
],
124+
}
125+
: true,
117126
},
118127
};
119128
});

packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts

+1-53
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as assert from 'assert';
22
import * as path from 'path';
33
import { dirname, join } from 'path';
44
import ts from 'typescript';
5-
import { FileChangeType, Hover, Position, Range } from 'vscode-languageserver';
5+
import { FileChangeType, Hover, Position } from 'vscode-languageserver';
66
import { DocumentManager, Document } from '../../../src/lib/documents';
77
import { LSConfigManager } from '../../../src/ls-config';
88
import { TypeScriptPlugin } from '../../../src/plugins';
@@ -288,58 +288,6 @@ describe('TypescriptPlugin', () => {
288288
]);
289289
});
290290

291-
it('provides code actions', async () => {
292-
const { plugin, document } = setup('codeactions.svelte');
293-
294-
const codeActions = await plugin.getCodeActions(
295-
document,
296-
Range.create(Position.create(1, 4), Position.create(1, 5)),
297-
{
298-
diagnostics: [
299-
{
300-
code: 6133,
301-
message: "'a' is declared but its value is never read.",
302-
range: Range.create(Position.create(1, 4), Position.create(1, 5)),
303-
source: 'ts',
304-
},
305-
],
306-
only: ['quickfix'],
307-
},
308-
);
309-
310-
assert.deepStrictEqual(codeActions, [
311-
{
312-
edit: {
313-
documentChanges: [
314-
{
315-
edits: [
316-
{
317-
newText: '',
318-
range: {
319-
start: {
320-
character: 0,
321-
line: 1,
322-
},
323-
end: {
324-
character: 0,
325-
line: 2,
326-
},
327-
},
328-
},
329-
],
330-
textDocument: {
331-
uri: getUri('codeactions.svelte'),
332-
version: null,
333-
},
334-
},
335-
],
336-
},
337-
kind: 'unusedIdentifier',
338-
title: "Remove unused declaration for: 'a'",
339-
},
340-
]);
341-
});
342-
343291
const setupForOnWatchedFileChanges = () => {
344292
const { plugin, document } = setup('');
345293
const filePath = document.getFilePath()!;

0 commit comments

Comments
 (0)