Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat) add extract into const/function refactorings #230

Merged
merged 4 commits into from
Jun 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/language-server/src/lib/documents/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,20 @@ function getLineOffsets(text: string) {
return lineOffsets;
}

export function isInTag(position: Position, tagInfo: TagInformation | null): boolean {
export function isInTag(
position: Position,
tagInfo: TagInformation | null,
): tagInfo is TagInformation {
return !!tagInfo && isInRange(Range.create(tagInfo.startPos, tagInfo.endPos), position);
}

export function isRangeInTag(
range: Range,
tagInfo: TagInformation | null,
): tagInfo is TagInformation {
return isInTag(range.start, tagInfo) && isInTag(range.end, tagInfo);
}

export function getTextInRange(range: Range, text: string) {
return text.substring(offsetAt(range.start, text), offsetAt(range.end, text));
}
Expand Down
17 changes: 17 additions & 0 deletions packages/language-server/src/plugins/PluginHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,23 @@ export class PluginHost implements LSProvider, OnWatchFileChanges {
);
}

async executeCommand(
textDocument: TextDocumentIdentifier,
command: string,
args?: any[],
): Promise<WorkspaceEdit | null> {
const document = this.getDocument(textDocument.uri);
if (!document) {
throw new Error('Cannot call methods on an unopened document');
}

return await this.execute<WorkspaceEdit>(
'executeCommand',
[document, command, args],
ExecuteMode.FirstNonNull,
);
}

async updateImports(fileRename: FileRename): Promise<WorkspaceEdit | null> {
return await this.execute<WorkspaceEdit>(
'updateImports',
Expand Down
5 changes: 5 additions & 0 deletions packages/language-server/src/plugins/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ export interface CodeActionsProvider {
range: Range,
context: CodeActionContext,
): Resolvable<CodeAction[]>;
executeCommand?(
document: Document,
command: string,
args?: any[],
): Resolvable<WorkspaceEdit | null>;
}

export interface FileRename {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,18 @@ export class TypeScriptPlugin
return this.codeActionsProvider.getCodeActions(document, range, context);
}

async executeCommand(
document: Document,
command: string,
args?: any[],
): Promise<WorkspaceEdit | null> {
if (!this.featureEnabled('codeActions')) {
return null;
}

return this.codeActionsProvider.executeCommand(document, command, args);
}

async updateImports(fileRename: FileRename): Promise<WorkspaceEdit | null> {
if (!this.featureEnabled('rename')) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@ import {
TextDocumentEdit,
TextEdit,
VersionedTextDocumentIdentifier,
WorkspaceEdit,
} from 'vscode-languageserver';
import { Document, mapRangeToOriginal } from '../../../lib/documents';
import { Document, mapRangeToOriginal, isRangeInTag } from '../../../lib/documents';
import { pathToUrl } from '../../../utils';
import { CodeActionsProvider } from '../../interfaces';
import { SnapshotFragment } from '../DocumentSnapshot';
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
import { convertRange } from '../utils';
import { flatten } from '../../../utils';
import ts from 'typescript';

interface RefactorArgs {
type: 'refactor';
refactorName: string;
textRange: ts.TextRange;
originalRange: Range;
}

export class CodeActionsProviderImpl implements CodeActionsProvider {
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
Expand All @@ -26,10 +36,17 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
return await this.organizeImports(document);
}

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

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

return [];
}

Expand Down Expand Up @@ -124,6 +141,150 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
);
}

private async getApplicableRefactors(document: Document, range: Range): Promise<CodeAction[]> {
if (
!isRangeInTag(range, document.scriptInfo) &&
!isRangeInTag(range, document.moduleScriptInfo)
) {
return [];
}

const { lang, tsDoc } = this.getLSAndTSDoc(document);
const fragment = await tsDoc.getFragment();
const textRange = {
pos: fragment.offsetAt(fragment.getGeneratedPosition(range.start)),
end: fragment.offsetAt(fragment.getGeneratedPosition(range.end)),
};
const applicableRefactors = lang.getApplicableRefactors(
document.getFilePath() || '',
textRange,
undefined,
);

return (
this.applicableRefactorsToCodeActions(applicableRefactors, document, range, textRange)
// Only allow refactorings from which we know they work
.filter(
(refactor) =>
refactor.command?.command.includes('function_scope') ||
refactor.command?.command.includes('constant_scope'),
)
// The language server also proposes extraction into const/function in module scope,
// which is outside of the render function, which is svelte2tsx-specific and unmapped,
// so it would both not work and confuse the user ("What is this render? Never declared that").
// So filter out the module scope proposal and rename the render-title
.filter((refactor) => !refactor.title.includes('module scope'))
.map((refactor) => ({
...refactor,
title: refactor.title
.replace(
`Extract to inner function in function 'render'`,
'Extract to function',
)
.replace(`Extract to constant in function 'render'`, 'Extract to constant'),
}))
);
}

private applicableRefactorsToCodeActions(
applicableRefactors: ts.ApplicableRefactorInfo[],
document: Document,
originalRange: Range,
textRange: { pos: number; end: number },
) {
return flatten(
applicableRefactors.map((applicableRefactor) => {
if (applicableRefactor.inlineable === false) {
return [
CodeAction.create(applicableRefactor.description, {
title: applicableRefactor.description,
command: applicableRefactor.name,
arguments: [
document.uri,
<RefactorArgs>{
type: 'refactor',
textRange,
originalRange,
refactorName: 'Extract Symbol',
},
],
}),
];
}

return applicableRefactor.actions.map((action) => {
return CodeAction.create(action.description, {
title: action.description,
command: action.name,
arguments: [
document.uri,
<RefactorArgs>{
type: 'refactor',
textRange,
originalRange,
refactorName: applicableRefactor.name,
},
],
});
});
}),
);
}

async executeCommand(
document: Document,
command: string,
args?: any[],
): Promise<WorkspaceEdit | null> {
if (!(args?.[1]?.type === 'refactor')) {
return null;
}

const { lang, tsDoc } = this.getLSAndTSDoc(document);
const fragment = await tsDoc.getFragment();
const path = document.getFilePath() || '';
const { refactorName, originalRange, textRange } = <RefactorArgs>args[1];

const edits = lang.getEditsForRefactor(
path,
{},
textRange,
refactorName,
command,
undefined,
);
if (!edits || edits.edits.length === 0) {
return null;
}

const documentChanges = edits?.edits.map((edit) =>
TextDocumentEdit.create(
VersionedTextDocumentIdentifier.create(document.uri, null),
edit.textChanges.map((edit) => {
let range = mapRangeToOriginal(fragment, convertRange(fragment, edit.span));
// Some refactorings place the new code at the end of svelte2tsx' render function,
// which is unmapped. In this case, add it to the end of the script tag ourselves.
if (range.start.line < 0 || range.end.line < 0) {
if (isRangeInTag(originalRange, document.scriptInfo)) {
range = Range.create(
document.scriptInfo.endPos,
document.scriptInfo.endPos,
);
} else if (isRangeInTag(originalRange, document.moduleScriptInfo)) {
range = Range.create(
document.moduleScriptInfo.endPos,
document.moduleScriptInfo.endPos,
);
}
}
return TextEdit.replace(range, edit.newText);
}),
),
);

return { documentChanges };
}

private getLSAndTSDoc(document: Document) {
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
}
Expand Down
32 changes: 32 additions & 0 deletions packages/language-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
CodeActionKind,
RenameFile,
DocumentUri,
ApplyWorkspaceEditRequest,
ApplyWorkspaceEditParams,
} from 'vscode-languageserver';
import { DocumentManager, Document } from './lib/documents';
import {
Expand Down Expand Up @@ -90,6 +92,8 @@ export function startServer(options?: LSOptions) {
pluginHost.register(new CSSPlugin(docManager, configManager));
pluginHost.register(new TypeScriptPlugin(docManager, configManager, workspacePath));

const clientSupportApplyEditCommand = !!evt.capabilities.workspace?.applyEdit;

return {
capabilities: {
textDocumentSync: {
Expand Down Expand Up @@ -137,9 +141,24 @@ export function startServer(options?: LSOptions) {
codeActionKinds: [
CodeActionKind.QuickFix,
CodeActionKind.SourceOrganizeImports,
...(clientSupportApplyEditCommand ? [CodeActionKind.Refactor] : []),
],
}
: true,
executeCommandProvider: clientSupportApplyEditCommand
? {
commands: [
'function_scope_0',
'function_scope_1',
'function_scope_2',
'function_scope_3',
'constant_scope_0',
'constant_scope_1',
'constant_scope_2',
'constant_scope_3',
],
}
: undefined,
renameProvider: evt.capabilities.textDocument?.rename?.prepareSupport
? { prepareProvider: true }
: true,
Expand Down Expand Up @@ -175,9 +194,22 @@ export function startServer(options?: LSOptions) {
);
connection.onDocumentSymbol((evt) => pluginHost.getDocumentSymbols(evt.textDocument));
connection.onDefinition((evt) => pluginHost.getDefinitions(evt.textDocument, evt.position));

connection.onCodeAction((evt) =>
pluginHost.getCodeActions(evt.textDocument, evt.range, evt.context),
);
connection.onExecuteCommand(async (evt) => {
const result = await pluginHost.executeCommand(
{ uri: evt.arguments?.[0] },
evt.command,
evt.arguments,
);
if (result) {
const edit: ApplyWorkspaceEditParams = { edit: result };
connection?.sendRequest(ApplyWorkspaceEditRequest.type.method, edit);
}
});

connection.onCompletionResolve((completionItem) => {
const data = (completionItem as AppCompletionItem).data as TextDocumentIdentifier;

Expand Down
Loading