diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index d4fc40399eda7..873c09448fc96 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -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 diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index e5ff7de8353ef..0348469dae198 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -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 && ((operand).operator === SyntaxKind.PlusToken || (operand).operator === SyntaxKind.PlusPlusToken)) - || (node.operator === SyntaxKind.MinusToken && ((operand).operator === SyntaxKind.MinusToken || (operand).operator === SyntaxKind.MinusMinusToken))); + || (node.operator === SyntaxKind.MinusToken && ((operand).operator === SyntaxKind.MinusToken || (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 { diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 63c2028c4242d..b0af0c260734b 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -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; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index d5584cc445b4a..4b95c957f74ce 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -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) => void; /*@internal*/ onAfterEmitNodeArray?: (nodes: NodeArray) => void; + /*@internal*/ onBeforeEmitToken?: (node: Node) => void; + /*@internal*/ onAfterEmitToken?: (node: Node) => void; } export interface PrinterOptions { diff --git a/src/compiler/visitor.ts b/src/compiler/visitor.ts index eb28721a1424b..628b14794ea8a 100644 --- a/src/compiler/visitor.ts +++ b/src/compiler/visitor.ts @@ -516,7 +516,8 @@ namespace ts { case SyntaxKind.BinaryExpression: return updateBinary(node, visitNode((node).left, visitor, isExpression), - visitNode((node).right, visitor, isExpression)); + visitNode((node).right, visitor, isExpression), + visitNode((node).operatorToken, visitor, isToken)); case SyntaxKind.ConditionalExpression: return updateConditional(node, diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 0ce18e4f8f516..fc50e71478eab 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -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); } diff --git a/src/harness/harness.ts b/src/harness/harness.ts index c0ef2672b2716..00a04d609f87e 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -1983,5 +1983,5 @@ namespace Harness { return { unitName: libFile, content: io.readFile(libFile) }; } - if (Error) (Error).stackTraceLimit = 1; + if (Error) (Error).stackTraceLimit = 100; } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 3bf6f1cde9a3d..7aefb0f3a1fc3 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -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)); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 33838b5805f73..efa9249030050 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -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 = 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 = 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 = 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 = getMessage(0); assert.equal(e1.event, "syntaxDiag"); host.clearOutput(); diff --git a/src/server/client.ts b/src/server/client.ts index 623b00f1ed304..e29261f724446 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -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 { 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 { + file: fileName, + startLine, + startOffset, + endLine, + endOffset + }; + } + + getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] { + const args = this.createFileLocationOrRangeRequestArgs(positionOrRange, fileName); + + const request = this.processRequest(CommandNames.GetApplicableRefactors, args); + const response = this.processResponse(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(CommandNames.GetRefactorCodeActions, args); + const response = this.processResponse(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, diff --git a/src/server/protocol.ts b/src/server/protocol.ts index c4c9ecece2b6f..8d85ad125d588 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -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. */ diff --git a/src/server/session.ts b/src/server/session.ts index 7be33f8bd2ade..bae53dcafe8a0 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -100,7 +100,7 @@ namespace ts.server { } export interface EventSender { - event(payload: any, eventName: string): void; + event(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(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({ + this.event({ 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(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({ 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({ 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 (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 = 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); diff --git a/src/services/codeFixProvider.ts b/src/services/codeFixProvider.ts index bab5356e99c82..6ff78572f82a4 100644 --- a/src/services/codeFixProvider.ts +++ b/src/services/codeFixProvider.ts @@ -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); }); } diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts new file mode 100644 index 0000000000000..0b6ea3487ecdb --- /dev/null +++ b/src/services/refactorProvider.ts @@ -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 = createMap(); + + 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; + } + } +} diff --git a/src/services/refactors/convertFunctionToEs6Class.ts b/src/services/refactors/convertFunctionToEs6Class.ts new file mode 100644 index 0000000000000..3f1a359c7a1f0 --- /dev/null +++ b/src/services/refactors/convertFunctionToEs6Class.ts @@ -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 ((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); + } + } +} \ No newline at end of file diff --git a/src/services/refactors/refactors.ts b/src/services/refactors/refactors.ts new file mode 100644 index 0000000000000..4c4ecb33416b1 --- /dev/null +++ b/src/services/refactors/refactors.ts @@ -0,0 +1 @@ +/// diff --git a/src/services/services.ts b/src/services/services.ts index 723df40f3f00d..1a1ac47847ff8 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -25,7 +25,9 @@ /// /// /// +/// /// +/// 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, diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 5a1aaa21bcd3b..0551917b33c18 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -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) { diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index fd7269d46ddd3..0c1d76e7ae5d2 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -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/*" ] } diff --git a/src/services/types.ts b/src/services/types.ts index b1137bc4bc95e..6c64ce5b9ba65 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -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. */ diff --git a/tests/cases/fourslash/convertFunctionToEs6Class1.ts b/tests/cases/fourslash/convertFunctionToEs6Class1.ts new file mode 100644 index 0000000000000..6275e48e62775 --- /dev/null +++ b/tests/cases/fourslash/convertFunctionToEs6Class1.ts @@ -0,0 +1,26 @@ +/// + +// @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'); \ No newline at end of file diff --git a/tests/cases/fourslash/convertFunctionToEs6Class2.ts b/tests/cases/fourslash/convertFunctionToEs6Class2.ts new file mode 100644 index 0000000000000..5d9a9f3258585 --- /dev/null +++ b/tests/cases/fourslash/convertFunctionToEs6Class2.ts @@ -0,0 +1,27 @@ +/// + +// @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'); diff --git a/tests/cases/fourslash/convertFunctionToEs6Class3.ts b/tests/cases/fourslash/convertFunctionToEs6Class3.ts new file mode 100644 index 0000000000000..af8955dcb7183 --- /dev/null +++ b/tests/cases/fourslash/convertFunctionToEs6Class3.ts @@ -0,0 +1,28 @@ +/// + +// @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'); \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 31d8b8b4ef202..3fada673f354d 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -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; diff --git a/tests/cases/fourslash/server/convertFunctionToEs6Class-server.ts b/tests/cases/fourslash/server/convertFunctionToEs6Class-server.ts new file mode 100644 index 0000000000000..9ab47086f20e7 --- /dev/null +++ b/tests/cases/fourslash/server/convertFunctionToEs6Class-server.ts @@ -0,0 +1,25 @@ +// @allowNonTsExtensions: true +// @Filename: test123.js + +/// + +//// // 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');