Merge pull request #15569 from RyanCavanaugh/new_refactor

Refactoring support
This commit is contained in:
Ryan Cavanaugh 2017-05-19 11:23:46 -07:00 committed by GitHub
commit f489f5af4d
25 changed files with 776 additions and 78 deletions

View file

@ -3584,6 +3584,15 @@
"code": 90022
},
"Convert function to an ES2015 class": {
"category": "Message",
"code": 95001
},
"Convert function '{0}' to class": {
"category": "Message",
"code": 95002
},
"Octal literal types must use ES2015 syntax. Use the syntax '{0}'.": {
"category": "Error",
"code": 8017

View file

@ -200,7 +200,9 @@ namespace ts {
onSetSourceFile,
substituteNode,
onBeforeEmitNodeArray,
onAfterEmitNodeArray
onAfterEmitNodeArray,
onBeforeEmitToken,
onAfterEmitToken
} = handlers;
const newLine = getNewLineCharacter(printerOptions);
@ -406,7 +408,7 @@ namespace ts {
// Strict mode reserved words
// Contextual keywords
if (isKeyword(kind)) {
writeTokenText(kind);
writeTokenNode(node);
return;
}
@ -645,7 +647,7 @@ namespace ts {
}
if (isToken(node)) {
writeTokenText(kind);
writeTokenNode(node);
return;
}
}
@ -672,7 +674,7 @@ namespace ts {
case SyntaxKind.SuperKeyword:
case SyntaxKind.TrueKeyword:
case SyntaxKind.ThisKeyword:
writeTokenText(kind);
writeTokenNode(node);
return;
// Expressions
@ -1260,7 +1262,7 @@ namespace ts {
const operand = node.operand;
return operand.kind === SyntaxKind.PrefixUnaryExpression
&& ((node.operator === SyntaxKind.PlusToken && ((<PrefixUnaryExpression>operand).operator === SyntaxKind.PlusToken || (<PrefixUnaryExpression>operand).operator === SyntaxKind.PlusPlusToken))
|| (node.operator === SyntaxKind.MinusToken && ((<PrefixUnaryExpression>operand).operator === SyntaxKind.MinusToken || (<PrefixUnaryExpression>operand).operator === SyntaxKind.MinusMinusToken)));
|| (node.operator === SyntaxKind.MinusToken && ((<PrefixUnaryExpression>operand).operator === SyntaxKind.MinusToken || (<PrefixUnaryExpression>operand).operator === SyntaxKind.MinusMinusToken)));
}
function emitPostfixUnaryExpression(node: PostfixUnaryExpression) {
@ -1275,7 +1277,7 @@ namespace ts {
emitExpression(node.left);
increaseIndentIf(indentBeforeOperator, isCommaOperator ? " " : undefined);
writeTokenText(node.operatorToken.kind);
writeTokenNode(node.operatorToken);
increaseIndentIf(indentAfterOperator, " ");
emitExpression(node.right);
decreaseIndentIf(indentBeforeOperator, indentAfterOperator);
@ -2455,6 +2457,16 @@ namespace ts {
: writeTokenText(token, pos);
}
function writeTokenNode(node: Node) {
if (onBeforeEmitToken) {
onBeforeEmitToken(node);
}
writeTokenText(node.kind);
if (onAfterEmitToken) {
onAfterEmitToken(node);
}
}
function writeTokenText(token: SyntaxKind, pos?: number) {
const tokenString = tokenToString(token);
write(tokenString);
@ -2928,9 +2940,9 @@ namespace ts {
// Flags enum to track count of temp variables and a few dedicated names
const enum TempFlags {
Auto = 0x00000000, // No preferred name
Auto = 0x00000000, // No preferred name
CountMask = 0x0FFFFFFF, // Temp variable counter
_i = 0x10000000, // Use/preference flag for '_i'
_i = 0x10000000, // Use/preference flag for '_i'
}
const enum ListFormat {

View file

@ -984,10 +984,10 @@ namespace ts {
return node;
}
export function updateBinary(node: BinaryExpression, left: Expression, right: Expression) {
export function updateBinary(node: BinaryExpression, left: Expression, right: Expression, operator?: BinaryOperator | BinaryOperatorToken) {
return node.left !== left
|| node.right !== right
? updateNode(createBinary(left, node.operatorToken, right), node)
? updateNode(createBinary(left, operator || node.operatorToken, right), node)
: node;
}

View file

@ -3415,7 +3415,7 @@ namespace ts {
export enum DiagnosticCategory {
Warning,
Error,
Message,
Message
}
export enum ModuleResolutionKind {
@ -4273,6 +4273,8 @@ namespace ts {
/*@internal*/ onSetSourceFile?: (node: SourceFile) => void;
/*@internal*/ onBeforeEmitNodeArray?: (nodes: NodeArray<any>) => void;
/*@internal*/ onAfterEmitNodeArray?: (nodes: NodeArray<any>) => void;
/*@internal*/ onBeforeEmitToken?: (node: Node) => void;
/*@internal*/ onAfterEmitToken?: (node: Node) => void;
}
export interface PrinterOptions {

View file

@ -516,7 +516,8 @@ namespace ts {
case SyntaxKind.BinaryExpression:
return updateBinary(<BinaryExpression>node,
visitNode((<BinaryExpression>node).left, visitor, isExpression),
visitNode((<BinaryExpression>node).right, visitor, isExpression));
visitNode((<BinaryExpression>node).right, visitor, isExpression),
visitNode((<BinaryExpression>node).operatorToken, visitor, isToken));
case SyntaxKind.ConditionalExpression:
return updateConditional(<ConditionalExpression>node,

View file

@ -2354,7 +2354,8 @@ namespace FourSlash {
private applyCodeAction(fileName: string, actions: ts.CodeAction[], index?: number): void {
if (index === undefined) {
if (!(actions && actions.length === 1)) {
this.raiseError(`Should find exactly one codefix, but ${actions ? actions.length : "none"} found.`);
const actionText = (actions && actions.length) ? JSON.stringify(actions) : "none";
this.raiseError(`Should find exactly one codefix, but found ${actionText}`);
}
index = 0;
}
@ -2708,6 +2709,60 @@ namespace FourSlash {
}
}
public verifyApplicableRefactorAvailableAtMarker(negative: boolean, markerName: string) {
const marker = this.getMarkerByName(markerName);
const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, marker.position);
const isAvailable = applicableRefactors && applicableRefactors.length > 0;
if (negative && isAvailable) {
this.raiseError(`verifyApplicableRefactorAvailableAtMarker failed - expected no refactor at marker ${markerName} but found some.`);
}
if (!negative && !isAvailable) {
this.raiseError(`verifyApplicableRefactorAvailableAtMarker failed - expected a refactor at marker ${markerName} but found none.`);
}
}
public verifyApplicableRefactorAvailableForRange(negative: boolean) {
const ranges = this.getRanges();
if (!(ranges && ranges.length === 1)) {
throw new Error("Exactly one refactor range is allowed per test.");
}
const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, { pos: ranges[0].start, end: ranges[0].end });
const isAvailable = applicableRefactors && applicableRefactors.length > 0;
if (negative && isAvailable) {
this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected no refactor but found some.`);
}
if (!negative && !isAvailable) {
this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected a refactor but found none.`);
}
}
public verifyFileAfterApplyingRefactorAtMarker(
markerName: string,
expectedContent: string,
refactorNameToApply: string,
formattingOptions?: ts.FormatCodeSettings) {
formattingOptions = formattingOptions || this.formatCodeSettings;
const markerPos = this.getMarkerByName(markerName).position;
const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, markerPos);
const applicableRefactorToApply = ts.find(applicableRefactors, refactor => refactor.name === refactorNameToApply);
if (!applicableRefactorToApply) {
this.raiseError(`The expected refactor: ${refactorNameToApply} is not available at the marker location.`);
}
const codeActions = this.languageService.getRefactorCodeActions(this.activeFile.fileName, formattingOptions, markerPos, refactorNameToApply);
this.applyCodeAction(this.activeFile.fileName, codeActions);
const actualContent = this.getFileContent(this.activeFile.fileName);
if (this.normalizeNewlines(actualContent) !== this.normalizeNewlines(expectedContent)) {
this.raiseError(`verifyFileAfterApplyingRefactors failed: expected:\n${expectedContent}\nactual:\n${actualContent}`);
}
}
public printAvailableCodeFixes() {
const codeFixes = this.getCodeFixActions(this.activeFile.fileName);
Harness.IO.log(stringify(codeFixes));
@ -3521,6 +3576,14 @@ namespace FourSlashInterface {
public codeFixAvailable() {
this.state.verifyCodeFixAvailable(this.negative);
}
public applicableRefactorAvailableAtMarker(markerName: string) {
this.state.verifyApplicableRefactorAvailableAtMarker(this.negative, markerName);
}
public applicableRefactorAvailableForRange() {
this.state.verifyApplicableRefactorAvailableForRange(this.negative);
}
}
export class Verify extends VerifyNegatable {
@ -3735,6 +3798,10 @@ namespace FourSlashInterface {
this.state.verifyRangeAfterCodeFix(expectedText, includeWhiteSpace, errorCode, index);
}
public fileAfterApplyingRefactorAtMarker(markerName: string, expectedContent: string, refactorNameToApply: string, formattingOptions?: ts.FormatCodeSettings): void {
this.state.verifyFileAfterApplyingRefactorAtMarker(markerName, expectedContent, refactorNameToApply, formattingOptions);
}
public importFixAtPosition(expectedTextArray: string[], errorCode?: number): void {
this.state.verifyImportFixAtPosition(expectedTextArray, errorCode);
}

View file

@ -1983,5 +1983,5 @@ namespace Harness {
return { unitName: libFile, content: io.readFile(libFile) };
}
if (Error) (<any>Error).stackTraceLimit = 1;
if (Error) (<any>Error).stackTraceLimit = 100;
}

View file

@ -489,6 +489,15 @@ namespace Harness.LanguageService {
getCodeFixesAtPosition(): ts.CodeAction[] {
throw new Error("Not supported on the shim.");
}
getCodeFixDiagnostics(): ts.Diagnostic[] {
throw new Error("Not supported on the shim.");
}
getRefactorCodeActions(): ts.CodeAction[] {
throw new Error("Not supported on the shim.");
}
getApplicableRefactors(): ts.ApplicableRefactorInfo[] {
throw new Error("Not supported on the shim.");
}
getEmitOutput(fileName: string): ts.EmitOutput {
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
}

View file

@ -337,6 +337,7 @@ namespace ts.projectSystem {
this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args);
return timeoutId;
}
unregister(id: any) {
if (typeof id === "number") {
delete this.map[id];
@ -352,10 +353,13 @@ namespace ts.projectSystem {
}
invoke() {
// Note: invoking a callback may result in new callbacks been queued,
// so do not clear the entire callback list regardless. Only remove the
// ones we have invoked.
for (const key in this.map) {
this.map[key]();
delete this.map[key];
}
this.map = [];
}
}
@ -3743,7 +3747,7 @@ namespace ts.projectSystem {
// run first step
host.runQueuedTimeoutCallbacks();
assert.equal(host.getOutput().length, 1, "expect 1 messages");
assert.equal(host.getOutput().length, 1, "expect 1 message");
const e1 = <protocol.Event>getMessage(0);
assert.equal(e1.event, "syntaxDiag");
host.clearOutput();
@ -3765,11 +3769,12 @@ namespace ts.projectSystem {
// run first step
host.runQueuedTimeoutCallbacks();
assert.equal(host.getOutput().length, 1, "expect 1 messages");
assert.equal(host.getOutput().length, 1, "expect 1 message");
const e1 = <protocol.Event>getMessage(0);
assert.equal(e1.event, "syntaxDiag");
host.clearOutput();
// the semanticDiag message
host.runQueuedImmediateCallbacks();
assert.equal(host.getOutput().length, 2, "expect 2 messages");
const e2 = <protocol.Event>getMessage(0);
@ -3787,7 +3792,7 @@ namespace ts.projectSystem {
assert.equal(host.getOutput().length, 0, "expect 0 messages");
// run first step
host.runQueuedTimeoutCallbacks();
assert.equal(host.getOutput().length, 1, "expect 1 messages");
assert.equal(host.getOutput().length, 1, "expect 1 message");
const e1 = <protocol.Event>getMessage(0);
assert.equal(e1.event, "syntaxDiag");
host.clearOutput();

View file

@ -695,6 +695,46 @@ namespace ts.server {
return response.body.map(entry => this.convertCodeActions(entry, fileName));
}
private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs {
if (typeof positionOrRange === "number") {
const { line, offset } = this.positionToOneBasedLineOffset(fileName, positionOrRange);
return <protocol.FileLocationRequestArgs>{ file: fileName, line, offset };
}
const { line: startLine, offset: startOffset } = this.positionToOneBasedLineOffset(fileName, positionOrRange.pos);
const { line: endLine, offset: endOffset } = this.positionToOneBasedLineOffset(fileName, positionOrRange.end);
return <protocol.FileRangeRequestArgs>{
file: fileName,
startLine,
startOffset,
endLine,
endOffset
};
}
getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] {
const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName);
const request = this.processRequest<protocol.GetApplicableRefactorsRequest>(CommandNames.GetApplicableRefactors, args);
const response = this.processResponse<protocol.GetApplicableRefactorsResponse>(request);
return response.body;
}
getRefactorCodeActions(
fileName: string,
_formatOptions: FormatCodeSettings,
positionOrRange: number | TextRange,
refactorName: string) {
const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName) as protocol.GetRefactorCodeActionsRequestArgs;
args.refactorName = refactorName;
const request = this.processRequest<protocol.GetRefactorCodeActionsRequest>(CommandNames.GetRefactorCodeActions, args);
const response = this.processResponse<protocol.GetRefactorCodeActionsResponse>(request);
const codeActions = response.body.actions;
return map(codeActions, codeAction => this.convertCodeActions(codeAction, fileName));
}
convertCodeActions(entry: protocol.CodeAction, fileName: string): CodeAction {
return {
description: entry.description,

View file

@ -95,6 +95,10 @@ namespace ts.server.protocol {
/* @internal */
export type GetCodeFixesFull = "getCodeFixes-full";
export type GetSupportedCodeFixes = "getSupportedCodeFixes";
export type GetApplicableRefactors = "getApplicableRefactors";
export type GetRefactorCodeActions = "getRefactorCodeActions";
export type GetRefactorCodeActionsFull = "getRefactorCodeActions-full";
}
/**
@ -394,6 +398,54 @@ namespace ts.server.protocol {
position?: number;
}
export type FileLocationOrRangeRequestArgs = FileLocationRequestArgs | FileRangeRequestArgs;
export interface GetApplicableRefactorsRequest extends Request {
command: CommandTypes.GetApplicableRefactors;
arguments: GetApplicableRefactorsRequestArgs;
}
export type GetApplicableRefactorsRequestArgs = FileLocationOrRangeRequestArgs;
export interface ApplicableRefactorInfo {
name: string;
description: string;
}
export interface GetApplicableRefactorsResponse extends Response {
body?: ApplicableRefactorInfo[];
}
export interface GetRefactorCodeActionsRequest extends Request {
command: CommandTypes.GetRefactorCodeActions;
arguments: GetRefactorCodeActionsRequestArgs;
}
export type GetRefactorCodeActionsRequestArgs = FileLocationOrRangeRequestArgs & {
/* The kind of the applicable refactor */
refactorName: string;
};
export type RefactorCodeActions = {
actions: protocol.CodeAction[];
renameLocation?: number
};
/* @internal */
export type RefactorCodeActionsFull = {
actions: ts.CodeAction[];
renameLocation?: number
};
export interface GetRefactorCodeActionsResponse extends Response {
body: RefactorCodeActions;
}
/* @internal */
export interface GetRefactorCodeActionsFullResponse extends Response {
body: RefactorCodeActionsFull;
}
/**
* Request for the available codefixes at a specific position.
*/
@ -402,10 +454,7 @@ namespace ts.server.protocol {
arguments: CodeFixRequestArgs;
}
/**
* Instances of this interface specify errorcodes on a specific location in a sourcefile.
*/
export interface CodeFixRequestArgs extends FileRequestArgs {
export interface FileRangeRequestArgs extends FileRequestArgs {
/**
* The line number for the request (1-based).
*/
@ -437,7 +486,12 @@ namespace ts.server.protocol {
*/
/* @internal */
endPosition?: number;
}
/**
* Instances of this interface specify errorcodes on a specific location in a sourcefile.
*/
export interface CodeFixRequestArgs extends FileRangeRequestArgs {
/**
* Errorcodes we want to get the fixes for.
*/

View file

@ -100,7 +100,7 @@ namespace ts.server {
}
export interface EventSender {
event(payload: any, eventName: string): void;
event<T>(payload: T, eventName: string): void;
}
function allEditsBeforePos(edits: ts.TextChange[], pos: number) {
@ -205,6 +205,10 @@ namespace ts.server {
/* @internal */
export const GetCodeFixesFull: protocol.CommandTypes.GetCodeFixesFull = "getCodeFixes-full";
export const GetSupportedCodeFixes: protocol.CommandTypes.GetSupportedCodeFixes = "getSupportedCodeFixes";
export const GetApplicableRefactors: protocol.CommandTypes.GetApplicableRefactors = "getApplicableRefactors";
export const GetRefactorCodeActions: protocol.CommandTypes.GetRefactorCodeActions = "getRefactorCodeActions";
export const GetRefactorCodeActionsFull: protocol.CommandTypes.GetRefactorCodeActionsFull = "getRefactorCodeActions-full";
}
export function formatMessage<T extends protocol.Message>(msg: T, logger: server.Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string {
@ -432,7 +436,7 @@ namespace ts.server {
break;
case ProjectLanguageServiceStateEvent:
const eventName: protocol.ProjectLanguageServiceStateEventName = "projectLanguageServiceState";
this.event(<protocol.ProjectLanguageServiceStateEventBody>{
this.event<protocol.ProjectLanguageServiceStateEventBody>({
projectName: event.data.project.getProjectName(),
languageServiceEnabled: event.data.languageServiceEnabled
}, eventName);
@ -476,7 +480,7 @@ namespace ts.server {
this.send(ev);
}
public event(info: any, eventName: string) {
public event<T>(info: T, eventName: string) {
const ev: protocol.Event = {
seq: 0,
type: "event",
@ -511,7 +515,7 @@ namespace ts.server {
}
const bakedDiags = diags.map((diag) => formatDiag(file, project, diag));
this.event({ file: file, diagnostics: bakedDiags }, "semanticDiag");
this.event<protocol.DiagnosticEventBody>({ file: file, diagnostics: bakedDiags }, "semanticDiag");
}
catch (err) {
this.logError(err, "semantic check");
@ -523,7 +527,7 @@ namespace ts.server {
const diags = project.getLanguageService().getSyntacticDiagnostics(file);
if (diags) {
const bakedDiags = diags.map((diag) => formatDiag(file, project, diag));
this.event({ file: file, diagnostics: bakedDiags }, "syntaxDiag");
this.event<protocol.DiagnosticEventBody>({ file: file, diagnostics: bakedDiags }, "syntaxDiag");
}
}
catch (err) {
@ -1366,8 +1370,8 @@ namespace ts.server {
return !items
? undefined
: simplifiedResult
? this.decorateNavigationBarItems(items, project.getScriptInfoForNormalizedPath(file))
: items;
? this.decorateNavigationBarItems(items, project.getScriptInfoForNormalizedPath(file))
: items;
}
private decorateNavigationTree(tree: ts.NavigationTree, scriptInfo: ScriptInfo): protocol.NavigationTree {
@ -1393,8 +1397,8 @@ namespace ts.server {
return !tree
? undefined
: simplifiedResult
? this.decorateNavigationTree(tree, project.getScriptInfoForNormalizedPath(file))
: tree;
? this.decorateNavigationTree(tree, project.getScriptInfoForNormalizedPath(file))
: tree;
}
private getNavigateToItems(args: protocol.NavtoRequestArgs, simplifiedResult: boolean): protocol.NavtoItem[] | NavigateToItem[] {
@ -1481,6 +1485,60 @@ namespace ts.server {
return ts.getSupportedCodeFixes();
}
private isLocation(locationOrSpan: protocol.FileLocationOrRangeRequestArgs): locationOrSpan is protocol.FileLocationRequestArgs {
return (<protocol.FileLocationRequestArgs>locationOrSpan).line !== undefined;
}
private extractPositionAndRange(args: protocol.FileLocationOrRangeRequestArgs, scriptInfo: ScriptInfo): { position: number, textRange: TextRange } {
let position: number = undefined;
let textRange: TextRange;
if (this.isLocation(args)) {
position = getPosition(args);
}
else {
const { startPosition, endPosition } = this.getStartAndEndPosition(args, scriptInfo);
textRange = { pos: startPosition, end: endPosition };
}
return { position, textRange };
function getPosition(loc: protocol.FileLocationRequestArgs) {
return loc.position !== undefined ? loc.position : scriptInfo.lineOffsetToPosition(loc.line, loc.offset);
}
}
private getApplicableRefactors(args: protocol.GetApplicableRefactorsRequestArgs): protocol.ApplicableRefactorInfo[] {
const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args);
const scriptInfo = project.getScriptInfoForNormalizedPath(file);
const { position, textRange } = this.extractPositionAndRange(args, scriptInfo);
return project.getLanguageService().getApplicableRefactors(file, position || textRange);
}
private getRefactorCodeActions(args: protocol.GetRefactorCodeActionsRequestArgs, simplifiedResult: boolean): protocol.RefactorCodeActions | protocol.RefactorCodeActionsFull {
const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args);
const scriptInfo = project.getScriptInfoForNormalizedPath(file);
const { position, textRange } = this.extractPositionAndRange(args, scriptInfo);
const result: ts.CodeAction[] = project.getLanguageService().getRefactorCodeActions(
file,
this.projectService.getFormatCodeOptions(),
position || textRange,
args.refactorName
);
if (simplifiedResult) {
// Not full
return {
actions: result.map(action => this.mapCodeAction(action, scriptInfo))
};
}
else {
// Full
return {
actions: result
};
}
}
private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): protocol.CodeAction[] | CodeAction[] {
if (args.errorCodes.length === 0) {
return undefined;
@ -1488,8 +1546,7 @@ namespace ts.server {
const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args);
const scriptInfo = project.getScriptInfoForNormalizedPath(file);
const startPosition = getStartPosition();
const endPosition = getEndPosition();
const { startPosition, endPosition } = this.getStartAndEndPosition(args, scriptInfo);
const formatOptions = this.projectService.getFormatCodeOptions(file);
const codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes, formatOptions);
@ -1502,14 +1559,28 @@ namespace ts.server {
else {
return codeActions;
}
}
function getStartPosition() {
return args.startPosition !== undefined ? args.startPosition : scriptInfo.lineOffsetToPosition(args.startLine, args.startOffset);
private getStartAndEndPosition(args: protocol.FileRangeRequestArgs, scriptInfo: ScriptInfo) {
let startPosition: number = undefined, endPosition: number = undefined;
if (args.startPosition !== undefined) {
startPosition = args.startPosition;
}
else {
startPosition = scriptInfo.lineOffsetToPosition(args.startLine, args.startOffset);
// save the result so we don't always recompute
args.startPosition = startPosition;
}
function getEndPosition() {
return args.endPosition !== undefined ? args.endPosition : scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset);
if (args.endPosition !== undefined) {
endPosition = args.endPosition;
}
else {
endPosition = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset);
args.endPosition = endPosition;
}
return { startPosition, endPosition };
}
private mapCodeAction(codeAction: CodeAction, scriptInfo: ScriptInfo): protocol.CodeAction {
@ -1540,8 +1611,8 @@ namespace ts.server {
return !spans
? undefined
: simplifiedResult
? spans.map(span => this.decorateSpan(span, scriptInfo))
: spans;
? spans.map(span => this.decorateSpan(span, scriptInfo))
: spans;
}
private getDiagnosticsForProject(next: NextStep, delay: number, fileName: string): void {
@ -1846,6 +1917,15 @@ namespace ts.server {
},
[CommandNames.GetSupportedCodeFixes]: () => {
return this.requiredResponse(this.getSupportedCodeFixes());
},
[CommandNames.GetApplicableRefactors]: (request: protocol.GetApplicableRefactorsRequest) => {
return this.requiredResponse(this.getApplicableRefactors(request.arguments));
},
[CommandNames.GetRefactorCodeActions]: (request: protocol.GetRefactorCodeActionsRequest) => {
return this.requiredResponse(this.getRefactorCodeActions(request.arguments, /*simplifiedResult*/ true));
},
[CommandNames.GetRefactorCodeActionsFull]: (request: protocol.GetRefactorCodeActionsRequest) => {
return this.requiredResponse(this.getRefactorCodeActions(request.arguments, /*simplifiedResult*/ false));
}
});
@ -1903,7 +1983,7 @@ namespace ts.server {
let request: protocol.Request;
try {
request = <protocol.Request>JSON.parse(message);
const {response, responseRequired} = this.executeCommand(request);
const { response, responseRequired } = this.executeCommand(request);
if (this.logger.hasLevel(LogLevel.requestTime)) {
const elapsedTime = hrTimeToMilliseconds(this.hrtime(start)).toFixed(4);

View file

@ -19,14 +19,14 @@ namespace ts {
export namespace codefix {
const codeFixes: CodeFix[][] = [];
export function registerCodeFix(action: CodeFix) {
forEach(action.errorCodes, error => {
export function registerCodeFix(codeFix: CodeFix) {
forEach(codeFix.errorCodes, error => {
let fixes = codeFixes[error];
if (!fixes) {
fixes = [];
codeFixes[error] = fixes;
}
fixes.push(action);
fixes.push(codeFix);
});
}

View file

@ -0,0 +1,69 @@
/* @internal */
namespace ts {
export interface Refactor {
/** An unique code associated with each refactor */
name: string;
/** Description of the refactor to display in the UI of the editor */
description: string;
/** Compute the associated code actions */
getCodeActions(context: RefactorContext): CodeAction[];
/** A fast syntactic check to see if the refactor is applicable at given position. */
isApplicable(context: RefactorContext): boolean;
}
export interface RefactorContext {
file: SourceFile;
startPosition: number;
endPosition?: number;
program: Program;
newLineCharacter: string;
rulesProvider?: formatting.RulesProvider;
cancellationToken?: CancellationToken;
}
export namespace refactor {
// A map with the refactor code as key, the refactor itself as value
// e.g. nonSuggestableRefactors[refactorCode] -> the refactor you want
const refactors: Map<Refactor> = createMap<Refactor>();
export function registerRefactor(refactor: Refactor) {
refactors.set(refactor.name, refactor);
}
export function getApplicableRefactors(context: RefactorContext): ApplicableRefactorInfo[] | undefined {
let results: ApplicableRefactorInfo[];
const refactorList: Refactor[] = [];
refactors.forEach(refactor => {
refactorList.push(refactor);
});
for (const refactor of refactorList) {
if (context.cancellationToken && context.cancellationToken.isCancellationRequested()) {
return results;
}
if (refactor.isApplicable(context)) {
(results || (results = [])).push({ name: refactor.name, description: refactor.description });
}
}
return results;
}
export function getRefactorCodeActions(context: RefactorContext, refactorName: string): CodeAction[] | undefined {
let result: CodeAction[];
const refactor = refactors.get(refactorName);
if (!refactor) {
return undefined;
}
const codeActions = refactor.getCodeActions(context);
if (codeActions) {
addRange((result || (result = [])), codeActions);
}
return result;
}
}
}

View file

@ -0,0 +1,209 @@
/* @internal */
namespace ts.refactor {
const convertFunctionToES6Class: Refactor = {
name: "Convert to ES2015 class",
description: Diagnostics.Convert_function_to_an_ES2015_class.message,
getCodeActions,
isApplicable
};
registerRefactor(convertFunctionToES6Class);
function isApplicable(context: RefactorContext): boolean {
const start = context.startPosition;
const node = getTokenAtPosition(context.file, start);
const checker = context.program.getTypeChecker();
let symbol = checker.getSymbolAtLocation(node);
if (symbol && isDeclarationOfFunctionOrClassExpression(symbol)) {
symbol = (symbol.valueDeclaration as VariableDeclaration).initializer.symbol;
}
return symbol && symbol.flags & SymbolFlags.Function && symbol.members && symbol.members.size > 0;
}
function getCodeActions(context: RefactorContext): CodeAction[] | undefined {
const start = context.startPosition;
const sourceFile = context.file;
const checker = context.program.getTypeChecker();
const token = getTokenAtPosition(sourceFile, start);
const ctorSymbol = checker.getSymbolAtLocation(token);
const newLine = context.rulesProvider.getFormatOptions().newLineCharacter;
const deletedNodes: Node[] = [];
const deletes: (() => any)[] = [];
if (!(ctorSymbol.flags & (SymbolFlags.Function | SymbolFlags.Variable))) {
return [];
}
const ctorDeclaration = ctorSymbol.valueDeclaration;
const changeTracker = textChanges.ChangeTracker.fromCodeFixContext(context as { newLineCharacter: string, rulesProvider: formatting.RulesProvider });
let precedingNode: Node;
let newClassDeclaration: ClassDeclaration;
switch (ctorDeclaration.kind) {
case SyntaxKind.FunctionDeclaration:
precedingNode = ctorDeclaration;
deleteNode(ctorDeclaration);
newClassDeclaration = createClassFromFunctionDeclaration(ctorDeclaration as FunctionDeclaration);
break;
case SyntaxKind.VariableDeclaration:
precedingNode = ctorDeclaration.parent.parent;
if ((<VariableDeclarationList>ctorDeclaration.parent).declarations.length === 1) {
deleteNode(precedingNode);
}
else {
deleteNode(ctorDeclaration, /*inList*/ true);
}
newClassDeclaration = createClassFromVariableDeclaration(ctorDeclaration as VariableDeclaration);
break;
}
if (!newClassDeclaration) {
return [];
}
// Because the preceding node could be touched, we need to insert nodes before delete nodes.
changeTracker.insertNodeAfter(sourceFile, precedingNode, newClassDeclaration, { suffix: newLine });
for (const deleteCallback of deletes) {
deleteCallback();
}
return [{
description: formatStringFromArgs(Diagnostics.Convert_function_0_to_class.message, [ctorSymbol.name]),
changes: changeTracker.getChanges()
}];
function deleteNode(node: Node, inList = false) {
if (deletedNodes.some(n => isNodeDescendantOf(node, n))) {
// Parent node has already been deleted; do nothing
return;
}
deletedNodes.push(node);
if (inList) {
deletes.push(() => changeTracker.deleteNodeInList(sourceFile, node));
}
else {
deletes.push(() => changeTracker.deleteNode(sourceFile, node));
}
}
function createClassElementsFromSymbol(symbol: Symbol) {
const memberElements: ClassElement[] = [];
// all instance members are stored in the "member" array of symbol
if (symbol.members) {
symbol.members.forEach(member => {
const memberElement = createClassElement(member, /*modifiers*/ undefined);
if (memberElement) {
memberElements.push(memberElement);
}
});
}
// all static members are stored in the "exports" array of symbol
if (symbol.exports) {
symbol.exports.forEach(member => {
const memberElement = createClassElement(member, [createToken(SyntaxKind.StaticKeyword)]);
if (memberElement) {
memberElements.push(memberElement);
}
});
}
return memberElements;
function shouldConvertDeclaration(_target: PropertyAccessExpression, source: Expression) {
// Right now the only thing we can convert are function expressions - other values shouldn't get
// transformed. We can update this once ES public class properties are available.
return isFunctionLike(source);
}
function createClassElement(symbol: Symbol, modifiers: Modifier[]): ClassElement {
// both properties and methods are bound as property symbols
if (!(symbol.flags & SymbolFlags.Property)) {
return;
}
const memberDeclaration = symbol.valueDeclaration as PropertyAccessExpression;
const assignmentBinaryExpression = memberDeclaration.parent as BinaryExpression;
if (!shouldConvertDeclaration(memberDeclaration, assignmentBinaryExpression.right)) {
return;
}
// delete the entire statement if this expression is the sole expression to take care of the semicolon at the end
const nodeToDelete = assignmentBinaryExpression.parent && assignmentBinaryExpression.parent.kind === SyntaxKind.ExpressionStatement
? assignmentBinaryExpression.parent : assignmentBinaryExpression;
deleteNode(nodeToDelete);
if (!assignmentBinaryExpression.right) {
return createProperty([], modifiers, symbol.name, /*questionToken*/ undefined,
/*type*/ undefined, /*initializer*/ undefined);
}
switch (assignmentBinaryExpression.right.kind) {
case SyntaxKind.FunctionExpression:
const functionExpression = assignmentBinaryExpression.right as FunctionExpression;
return createMethod(/*decorators*/ undefined, modifiers, /*asteriskToken*/ undefined, memberDeclaration.name, /*questionToken*/ undefined,
/*typeParameters*/ undefined, functionExpression.parameters, /*type*/ undefined, functionExpression.body);
case SyntaxKind.ArrowFunction:
const arrowFunction = assignmentBinaryExpression.right as ArrowFunction;
const arrowFunctionBody = arrowFunction.body;
let bodyBlock: Block;
// case 1: () => { return [1,2,3] }
if (arrowFunctionBody.kind === SyntaxKind.Block) {
bodyBlock = arrowFunctionBody as Block;
}
// case 2: () => [1,2,3]
else {
const expression = arrowFunctionBody as Expression;
bodyBlock = createBlock([createReturn(expression)]);
}
return createMethod(/*decorators*/ undefined, modifiers, /*asteriskToken*/ undefined, memberDeclaration.name, /*questionToken*/ undefined,
/*typeParameters*/ undefined, arrowFunction.parameters, /*type*/ undefined, bodyBlock);
default:
// Don't try to declare members in JavaScript files
if (isSourceFileJavaScript(sourceFile)) {
return;
}
return createProperty(/*decorators*/ undefined, modifiers, memberDeclaration.name, /*questionToken*/ undefined,
/*type*/ undefined, assignmentBinaryExpression.right);
}
}
}
function createClassFromVariableDeclaration(node: VariableDeclaration): ClassDeclaration {
const initializer = node.initializer as FunctionExpression;
if (!initializer || initializer.kind !== SyntaxKind.FunctionExpression) {
return undefined;
}
if (node.name.kind !== SyntaxKind.Identifier) {
return undefined;
}
const memberElements = createClassElementsFromSymbol(initializer.symbol);
if (initializer.body) {
memberElements.unshift(createConstructor(/*decorators*/ undefined, /*modifiers*/ undefined, initializer.parameters, initializer.body));
}
return createClassDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, node.name,
/*typeParameters*/ undefined, /*heritageClauses*/ undefined, memberElements);
}
function createClassFromFunctionDeclaration(node: FunctionDeclaration): ClassDeclaration {
const memberElements = createClassElementsFromSymbol(ctorSymbol);
if (node.body) {
memberElements.unshift(createConstructor(/*decorators*/ undefined, /*modifiers*/ undefined, node.parameters, node.body));
}
return createClassDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, node.name,
/*typeParameters*/ undefined, /*heritageClauses*/ undefined, memberElements);
}
}
}

View file

@ -0,0 +1 @@
/// <reference path="convertFunctionToEs6Class.ts" />

View file

@ -25,7 +25,9 @@
/// <reference path='formatting\smartIndenter.ts' />
/// <reference path='textChanges.ts' />
/// <reference path='codeFixProvider.ts' />
/// <reference path='refactorProvider.ts' />
/// <reference path='codefixes\fixes.ts' />
/// <reference path='refactors\refactors.ts' />
namespace ts {
/** The version of the language service API */
@ -1959,11 +1961,43 @@ namespace ts {
return Rename.getRenameInfo(program.getTypeChecker(), defaultLibFileName, getCanonicalFileName, getValidSourceFile(fileName), position);
}
function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, formatOptions?: FormatCodeSettings): RefactorContext {
const [startPosition, endPosition] = typeof positionOrRange === "number" ? [positionOrRange, undefined] : [positionOrRange.pos, positionOrRange.end];
return {
file,
startPosition,
endPosition,
program: getProgram(),
newLineCharacter: host.getNewLine(),
rulesProvider: getRuleProvider(formatOptions),
cancellationToken
};
}
function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] {
synchronizeHostData();
const file = getValidSourceFile(fileName);
return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange));
}
function getRefactorCodeActions(
fileName: string,
formatOptions: FormatCodeSettings,
positionOrRange: number | TextRange,
refactorName: string): CodeAction[] | undefined {
synchronizeHostData();
const file = getValidSourceFile(fileName);
return refactor.getRefactorCodeActions(getRefactorContext(file, positionOrRange, formatOptions), refactorName);
}
return {
dispose,
cleanupSemanticCache,
getSyntacticDiagnostics,
getSemanticDiagnostics,
getApplicableRefactors,
getRefactorCodeActions,
getCompilerOptionsDiagnostics,
getSyntacticClassifications,
getSemanticClassifications,

View file

@ -157,7 +157,7 @@ namespace ts.textChanges {
private changes: Change[] = [];
private readonly newLineCharacter: string;
public static fromCodeFixContext(context: CodeFixContext) {
public static fromCodeFixContext(context: { newLineCharacter: string, rulesProvider: formatting.RulesProvider }) {
return new ChangeTracker(context.newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed, context.rulesProvider);
}
@ -254,9 +254,9 @@ namespace ts.textChanges {
public insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node, options: InsertNodeOptions & ConfigurableEnd = {}) {
if ((isStatementButNotDeclaration(after)) ||
after.kind === SyntaxKind.PropertyDeclaration ||
after.kind === SyntaxKind.PropertySignature ||
after.kind === SyntaxKind.MethodSignature) {
after.kind === SyntaxKind.PropertyDeclaration ||
after.kind === SyntaxKind.PropertySignature ||
after.kind === SyntaxKind.MethodSignature) {
// check if previous statement ends with semicolon
// if not - insert semicolon to preserve the code from changing the meaning due to ASI
if (sourceFile.text.charCodeAt(after.end - 1) !== CharacterCodes.semicolon) {
@ -481,7 +481,7 @@ namespace ts.textChanges {
return (options.prefix || "") + text + (options.suffix || "");
}
private static normalize(changes: Change[]) {
private static normalize(changes: Change[]): Change[] {
// order changes by start position
const normalized = stableSort(changes, (a, b) => a.range.pos - b.range.pos);
// verify that change intervals do not overlap, except possibly at end points.
@ -560,6 +560,8 @@ namespace ts.textChanges {
public readonly onEmitNode: PrintHandlers["onEmitNode"];
public readonly onBeforeEmitNodeArray: PrintHandlers["onBeforeEmitNodeArray"];
public readonly onAfterEmitNodeArray: PrintHandlers["onAfterEmitNodeArray"];
public readonly onBeforeEmitToken: PrintHandlers["onBeforeEmitToken"];
public readonly onAfterEmitToken: PrintHandlers["onAfterEmitToken"];
constructor(newLine: string) {
this.writer = createTextWriter(newLine);
@ -582,6 +584,16 @@ namespace ts.textChanges {
setEnd(nodes, this.lastNonTriviaPosition);
}
};
this.onBeforeEmitToken = node => {
if (node) {
setPos(node, this.lastNonTriviaPosition);
}
};
this.onAfterEmitToken = node => {
if (node) {
setEnd(node, this.lastNonTriviaPosition);
}
};
}
private setLastNonTriviaPosition(s: string, force: boolean) {

View file

@ -64,35 +64,12 @@
"signatureHelp.ts",
"symbolDisplay.ts",
"textChanges.ts",
"formatting/formatting.ts",
"formatting/formattingContext.ts",
"formatting/formattingRequestKind.ts",
"formatting/formattingScanner.ts",
"formatting/references.ts",
"formatting/rule.ts",
"formatting/ruleAction.ts",
"formatting/ruleDescriptor.ts",
"formatting/ruleFlag.ts",
"formatting/ruleOperation.ts",
"formatting/ruleOperationContext.ts",
"formatting/rules.ts",
"formatting/rulesMap.ts",
"formatting/rulesProvider.ts",
"formatting/smartIndenter.ts",
"formatting/tokenRange.ts",
"codeFixProvider.ts",
"codefixes/fixAddMissingMember.ts",
"codefixes/fixSpelling.ts",
"codefixes/fixExtendsInterfaceBecomesImplements.ts",
"codefixes/fixClassIncorrectlyImplementsInterface.ts",
"codefixes/fixClassDoesntImplementInheritedAbstractMember.ts",
"codefixes/fixClassSuperMustPrecedeThisAccess.ts",
"codefixes/fixConstructorForDerivedNeedSuperCall.ts",
"codefixes/fixForgottenThisPropertyAccess.ts",
"codefixes/fixes.ts",
"codefixes/helpers.ts",
"codefixes/importFixes.ts",
"codefixes/unusedIdentifierFixes.ts",
"codefixes/disableJsDiagnostics.ts"
"refactorProvider.ts",
"codeFixProvider.ts"
],
"include": [
"formatting/*",
"codefixes/*",
"refactors/*"
]
}

View file

@ -261,6 +261,8 @@ namespace ts {
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean;
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[];
getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[];
getRefactorCodeActions(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string): CodeAction[] | undefined;
getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput;
@ -352,6 +354,11 @@ namespace ts {
changes: FileTextChanges[];
}
export interface ApplicableRefactorInfo {
name: string;
description: string;
}
export interface TextInsertion {
newText: string;
/** The position in newText the caret should point to after the insertion. */

View file

@ -0,0 +1,26 @@
/// <reference path='fourslash.ts' />
// @allowNonTsExtensions: true
// @Filename: test123.js
//// [|function /*1*/foo() { }
//// /*2*/foo.prototype.instanceMethod1 = function() { return "this is name"; };
//// /*3*/foo.prototype.instanceMethod2 = () => { return "this is name"; };
//// /*4*/foo.prototype.instanceProp1 = "hello";
//// /*5*/foo.prototype.instanceProp2 = undefined;
//// /*6*/foo.staticProp = "world";
//// /*7*/foo.staticMethod1 = function() { return "this is static name"; };
//// /*8*/foo.staticMethod2 = () => "this is static name";|]
['1', '2', '3', '4', '5', '6', '7', '8'].forEach(m => verify.applicableRefactorAvailableAtMarker(m));
verify.fileAfterApplyingRefactorAtMarker('1',
`class foo {
constructor() { }
instanceMethod1() { return "this is name"; }
instanceMethod2() { return "this is name"; }
static staticMethod1() { return "this is static name"; }
static staticMethod2() { return "this is static name"; }
}
foo.prototype.instanceProp1 = "hello";
foo.prototype.instanceProp2 = undefined;
foo.staticProp = "world";
`, 'Convert to ES2015 class');

View file

@ -0,0 +1,27 @@
/// <reference path='fourslash.ts' />
// @allowNonTsExtensions: true
// @Filename: test123.js
//// [|var /*1*/foo = function() { };
//// /*2*/foo.prototype.instanceMethod1 = function() { return "this is name"; };
//// /*3*/foo.prototype.instanceMethod2 = () => { return "this is name"; };
//// /*4*/foo.instanceProp1 = "hello";
//// /*5*/foo.instanceProp2 = undefined;
//// /*6*/foo.staticProp = "world";
//// /*7*/foo.staticMethod1 = function() { return "this is static name"; };
//// /*8*/foo.staticMethod2 = () => "this is static name";|]
['1', '2', '3', '4', '5', '6', '7', '8'].forEach(m => verify.applicableRefactorAvailableAtMarker(m));
verify.fileAfterApplyingRefactorAtMarker('4',
`class foo {
constructor() { }
instanceMethod1() { return "this is name"; }
instanceMethod2() { return "this is name"; }
static staticMethod1() { return "this is static name"; }
static staticMethod2() { return "this is static name"; }
}
foo.instanceProp1 = "hello";
foo.instanceProp2 = undefined;
foo.staticProp = "world";
`, 'Convert to ES2015 class');

View file

@ -0,0 +1,28 @@
/// <reference path='fourslash.ts' />
// @allowNonTsExtensions: true
// @Filename: test123.js
//// [|var bar = 10, /*1*/foo = function() { };
//// /*2*/foo.prototype.instanceMethod1 = function() { return "this is name"; };
//// /*3*/foo.prototype.instanceMethod2 = () => { return "this is name"; };
//// /*4*/foo.prototype.instanceProp1 = "hello";
//// /*5*/foo.prototype.instanceProp2 = undefined;
//// /*6*/foo.staticProp = "world";
//// /*7*/foo.staticMethod1 = function() { return "this is static name"; };
//// /*8*/foo.staticMethod2 = () => "this is static name";|]
['1', '2', '3', '4', '5', '6', '7', '8'].forEach(m => verify.applicableRefactorAvailableAtMarker(m));
verify.fileAfterApplyingRefactorAtMarker('7',
`var bar = 10;
class foo {
constructor() { }
instanceMethod1() { return "this is name"; }
instanceMethod2() { return "this is name"; }
static staticMethod1() { return "this is static name"; }
static staticMethod2() { return "this is static name"; }
}
foo.prototype.instanceProp1 = "hello";
foo.prototype.instanceProp2 = undefined;
foo.staticProp = "world";
`, 'Convert to ES2015 class');

View file

@ -150,6 +150,9 @@ declare namespace FourSlashInterface {
implementationListIsEmpty(): void;
isValidBraceCompletionAtPosition(openingBrace?: string): void;
codeFixAvailable(): void;
applicableRefactorAvailableAtMarker(markerName: string): void;
codeFixDiagnosticsAvailableAtMarkers(markerNames: string[], diagnosticCode?: number): void;
applicableRefactorAvailableForRange(): void;
}
class verify extends verifyNegatable {
assertHasRanges(ranges: Range[]): void;
@ -234,6 +237,7 @@ declare namespace FourSlashInterface {
DocCommentTemplate(expectedText: string, expectedOffset: number, empty?: boolean): void;
noDocCommentTemplate(): void;
rangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number, index?: number): void;
fileAfterApplyingRefactorAtMarker(markerName: string, expectedContent: string, refactorNameToApply: string, formattingOptions?: FormatCodeOptions): void;
importFixAtPosition(expectedTextArray: string[], errorCode?: number): void;
navigationBar(json: any): void;

View file

@ -0,0 +1,25 @@
// @allowNonTsExtensions: true
// @Filename: test123.js
/// <reference path="../fourslash.ts" />
//// // Comment
//// function fn() {
//// this.baz = 10;
//// }
//// /*1*/fn.prototype.bar = function () {
//// console.log('hello world');
//// }
verify.applicableRefactorAvailableAtMarker('1');
verify.fileAfterApplyingRefactorAtMarker('1',
`// Comment
class fn {
constructor() {
this.baz = 10;
}
bar() {
console.log('hello world');
}
}
`, 'Convert to ES2015 class');