Skip to content

Commit

Permalink
(feat) add organize-imports code action support
Browse files Browse the repository at this point in the history
closes #72
  • Loading branch information
Simon Holthausen committed May 24, 2020
1 parent 9593987 commit 9d2b7b0
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,12 @@ import {
Position,
Range,
SymbolInformation,
TextDocumentEdit,
TextEdit,
VersionedTextDocumentIdentifier,
} from 'vscode-languageserver';
import {
Document,
DocumentManager,
mapDiagnosticToOriginal,
mapHoverToParent,
mapRangeToOriginal,
mapSymbolInformationToOriginal,
} from '../../lib/documents';
import { LSConfigManager, LSTypescriptConfig } from '../../ls-config';
Expand All @@ -38,6 +34,7 @@ import {
OnWatchFileChanges,
} from '../interfaces';
import { DocumentSnapshot, SnapshotFragment } from './DocumentSnapshot';
import { CodeActionsProviderImpl } from './features/CodeActionsProvider';
import {
CompletionEntryWithIdentifer,
CompletionsProviderImpl,
Expand All @@ -63,11 +60,13 @@ export class TypeScriptPlugin
private configManager: LSConfigManager;
private readonly lsAndTsDocResolver: LSAndTSDocResolver;
private readonly completionProvider: CompletionsProviderImpl;
private readonly codeActionsProvider: CodeActionsProviderImpl;

constructor(docManager: DocumentManager, configManager: LSConfigManager) {
this.configManager = configManager;
this.lsAndTsDocResolver = new LSAndTSDocResolver(docManager);
this.completionProvider = new CompletionsProviderImpl(this.lsAndTsDocResolver);
this.codeActionsProvider = new CodeActionsProviderImpl(this.lsAndTsDocResolver);
}

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

const { lang, tsDoc } = this.getLSAndTSDoc(document);
const fragment = await tsDoc.getFragment();

const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start));
const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end));
const errorCodes: number[] = context.diagnostics.map((diag) => Number(diag.code));
const codeFixes = lang.getCodeFixesAtPosition(
tsDoc.filePath,
start,
end,
errorCodes,
{},
{},
);

const docs = new Map<string, SnapshotFragment>([[tsDoc.filePath, fragment]]);
return await Promise.all(
codeFixes.map(async (fix) => {
const documentChanges = await Promise.all(
fix.changes.map(async (change) => {
let doc = docs.get(change.fileName);
if (!doc) {
doc = await this.getSnapshot(change.fileName).getFragment();
docs.set(change.fileName, doc);
}
return TextDocumentEdit.create(
VersionedTextDocumentIdentifier.create(
pathToUrl(change.fileName),
null,
),
change.textChanges.map((edit) => {
return TextEdit.replace(
mapRangeToOriginal(doc!, convertRange(doc!, edit.span)),
edit.newText,
);
}),
);
}),
);
return CodeAction.create(
fix.description,
{
documentChanges,
},
fix.fixName,
);
}),
);
return this.codeActionsProvider.getCodeActions(document, range, context);
}

onWatchFileChanges(fileName: string, changeType: FileChangeType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {
CodeAction,
CodeActionContext,
CodeActionKind,
Range,
TextDocumentEdit,
TextEdit,
VersionedTextDocumentIdentifier,
} from 'vscode-languageserver';
import { Document, mapRangeToOriginal } from '../../../lib/documents';
import { pathToUrl } from '../../../utils';
import { CodeActionsProvider } from '../../interfaces';
import { SnapshotFragment } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { convertRange } from '../utils';

export class CodeActionsProviderImpl implements CodeActionsProvider {
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}

async getCodeActions(
document: Document,
range: Range,
context: CodeActionContext,
): Promise<CodeAction[]> {
if (context.only?.[0] === CodeActionKind.SourceOrganizeImports) {
return await this.organizeImports(document);
}

if (!context.only || context.only.includes(CodeActionKind.QuickFix)) {
return await this.applyQuickfix(document, range, context);
}

return [];
}

private async organizeImports(document: Document): Promise<CodeAction[]> {
if (!document.scriptInfo) {
return [];
}

const { lang, tsDoc } = this.getLSAndTSDoc(document);
const fragment = await tsDoc.getFragment();

const changes = lang.organizeImports({ fileName: tsDoc.filePath, type: 'file' }, {}, {});

const documentChanges = await Promise.all(
changes.map(async (change) => {
// Organize Imports will only affect the current file, so no need to check the file path
return TextDocumentEdit.create(
VersionedTextDocumentIdentifier.create(document.url, null),
change.textChanges.map((edit) => {
let range = mapRangeToOriginal(fragment, convertRange(fragment, edit.span));
// Handle svelte2tsx wrong import mapping:
// The character after the last import maps to the start of the script
// TODO find a way to fix this in svelte2tsx and then remove this
if (range.end.line === 0 && range.end.character === 1) {
edit.span.length -= 1;
range = mapRangeToOriginal(fragment, convertRange(fragment, edit.span));
range.end.character += 1;
}
return TextEdit.replace(range, edit.newText);
}),
);
}),
);

return [
CodeAction.create(
'Organize Imports',
{ documentChanges },
CodeActionKind.SourceOrganizeImports,
),
];
}

private async applyQuickfix(document: Document, range: Range, context: CodeActionContext) {
const { lang, tsDoc } = this.getLSAndTSDoc(document);
const fragment = await tsDoc.getFragment();

const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start));
const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end));
const errorCodes: number[] = context.diagnostics.map((diag) => Number(diag.code));
const codeFixes = lang.getCodeFixesAtPosition(
tsDoc.filePath,
start,
end,
errorCodes,
{},
{},
);

const docs = new Map<string, SnapshotFragment>([[tsDoc.filePath, fragment]]);
return await Promise.all(
codeFixes.map(async (fix) => {
const documentChanges = await Promise.all(
fix.changes.map(async (change) => {
let doc = docs.get(change.fileName);
if (!doc) {
doc = await this.getSnapshot(change.fileName).getFragment();
docs.set(change.fileName, doc);
}
return TextDocumentEdit.create(
VersionedTextDocumentIdentifier.create(
pathToUrl(change.fileName),
null,
),
change.textChanges.map((edit) => {
return TextEdit.replace(
mapRangeToOriginal(doc!, convertRange(doc!, edit.span)),
edit.newText,
);
}),
);
}),
);
return CodeAction.create(
fix.description,
{
documentChanges,
},
CodeActionKind.QuickFix,
);
}),
);
}

private getLSAndTSDoc(document: Document) {
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
}

private getSnapshot(filePath: string, document?: Document) {
return this.lsAndTsDocResolver.getSnapshot(filePath, document);
}
}
11 changes: 10 additions & 1 deletion packages/language-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
TextDocumentPositionParams,
TextDocumentIdentifier,
IConnection,
CodeActionKind,
} from 'vscode-languageserver';
import { DocumentManager, Document } from './lib/documents';
import {
Expand Down Expand Up @@ -113,7 +114,15 @@ export function startServer(options?: LSOptions) {
colorProvider: true,
documentSymbolProvider: true,
definitionProvider: true,
codeActionProvider: true,
codeActionProvider: evt.capabilities.textDocument?.codeAction
?.codeActionLiteralSupport
? {
codeActionKinds: [
CodeActionKind.QuickFix,
CodeActionKind.SourceOrganizeImports,
],
}
: true,
},
};
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as assert from 'assert';
import * as path from 'path';
import { dirname, join } from 'path';
import ts from 'typescript';
import { FileChangeType, Hover, Position, Range } from 'vscode-languageserver';
import { FileChangeType, Hover, Position } from 'vscode-languageserver';
import { DocumentManager, Document } from '../../../src/lib/documents';
import { LSConfigManager } from '../../../src/ls-config';
import { TypeScriptPlugin } from '../../../src/plugins';
Expand Down Expand Up @@ -288,58 +288,6 @@ describe('TypescriptPlugin', () => {
]);
});

it('provides code actions', async () => {
const { plugin, document } = setup('codeactions.svelte');

const codeActions = await plugin.getCodeActions(
document,
Range.create(Position.create(1, 4), Position.create(1, 5)),
{
diagnostics: [
{
code: 6133,
message: "'a' is declared but its value is never read.",
range: Range.create(Position.create(1, 4), Position.create(1, 5)),
source: 'ts',
},
],
only: ['quickfix'],
},
);

assert.deepStrictEqual(codeActions, [
{
edit: {
documentChanges: [
{
edits: [
{
newText: '',
range: {
start: {
character: 0,
line: 1,
},
end: {
character: 0,
line: 2,
},
},
},
],
textDocument: {
uri: getUri('codeactions.svelte'),
version: null,
},
},
],
},
kind: 'unusedIdentifier',
title: "Remove unused declaration for: 'a'",
},
]);
});

const setupForOnWatchedFileChanges = () => {
const { plugin, document } = setup('');
const filePath = document.getFilePath()!;
Expand Down
Loading

0 comments on commit 9d2b7b0

Please sign in to comment.