diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 58c0e6ee3f21e..1b8c995ea057f 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3073,7 +3073,6 @@ "category": "Error", "code": 17010 }, - "Circularity detected while resolving configuration: {0}": { "category": "Error", "code": 18000 @@ -3081,5 +3080,33 @@ "The path in an 'extends' options must be relative or rooted.": { "category": "Error", "code": 18001 + }, + "Add missing 'super()' call.": { + "category": "Message", + "code": 90001 + }, + "Make 'super()' call the first statement in the constructor.": { + "category": "Message", + "code": 90002 + }, + "Change 'extends' to 'implements'": { + "category": "Message", + "code": 90003 + }, + "Remove unused identifiers": { + "category": "Message", + "code": 90004 + }, + "Implement interface on reference": { + "category": "Message", + "code": 90005 + }, + "Implement interface on class": { + "category": "Message", + "code": 90006 + }, + "Implement inherited abstract class": { + "category": "Message", + "code": 90007 } } diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 49f834963ee1e..f54804c0eb5d4 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -1,4 +1,4 @@ -// +// // Copyright (c) Microsoft Corporation. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -427,7 +427,7 @@ namespace FourSlash { if (exists !== negative) { this.printErrorLog(negative, this.getAllDiagnostics()); - throw new Error("Failure between markers: " + startMarkerName + ", " + endMarkerName); + throw new Error(`Failure between markers: '${startMarkerName}', '${endMarkerName}'`); } } @@ -742,7 +742,6 @@ namespace FourSlash { } } - public verifyCompletionListAllowsNewIdentifier(negative: boolean) { const completions = this.getCompletionListAtCaret(); @@ -1611,7 +1610,7 @@ namespace FourSlash { if (isFormattingEdit) { const newContent = this.getFileContent(fileName); - if (newContent.replace(/\s/g, "") !== oldContent.replace(/\s/g, "")) { + if (this.removeWhitespace(newContent) !== this.removeWhitespace(oldContent)) { this.raiseError("Formatting operation destroyed non-whitespace content"); } } @@ -1677,6 +1676,10 @@ namespace FourSlash { } } + private removeWhitespace(text: string): string { + return text.replace(/\s/g, ""); + } + public goToBOF() { this.goToPosition(0); } @@ -2038,6 +2041,47 @@ namespace FourSlash { } } + private getCodeFixes(errorCode?: number) { + const fileName = this.activeFile.fileName; + const diagnostics = this.getDiagnostics(fileName); + + if (diagnostics.length === 0) { + this.raiseError("Errors expected."); + } + + if (diagnostics.length > 1 && errorCode !== undefined) { + this.raiseError("When there's more than one error, you must specify the errror to fix."); + } + + const diagnostic = !errorCode ? diagnostics[0] : ts.find(diagnostics, d => d.code == errorCode); + + return this.languageService.getCodeFixesAtPosition(fileName, diagnostic.start, diagnostic.length, [diagnostic.code]); + } + + public verifyCodeFixAtPosition(expectedText: string, errorCode?: number) { + const ranges = this.getRanges(); + if (ranges.length == 0) { + this.raiseError("At least one range should be specified in the testfile."); + } + + const actual = this.getCodeFixes(errorCode); + + if (!actual || actual.length == 0) { + this.raiseError("No codefixes returned."); + } + + if (actual.length > 1) { + this.raiseError("More than 1 codefix returned."); + } + + this.applyEdits(actual[0].changes[0].fileName, actual[0].changes[0].textChanges, /*isFormattingEdit*/ false); + const actualText = this.rangeText(ranges[0]); + + if (this.removeWhitespace(actualText) !== this.removeWhitespace(expectedText)) { + this.raiseError(`Actual text doesn't match expected text. Actual: '${actualText}' Expected: '${expectedText}'`); + } + } + public verifyDocCommentTemplate(expected?: ts.TextInsertion) { const name = "verifyDocCommentTemplate"; const actual = this.languageService.getDocCommentTemplateAtPosition(this.activeFile.fileName, this.currentCaretPosition); @@ -2309,6 +2353,18 @@ namespace FourSlash { } } + public verifyCodeFixAvailable(negative: boolean, errorCode?: number) { + const fixes = this.getCodeFixes(errorCode); + + if (negative && fixes && fixes.length > 0) { + this.raiseError(`verifyCodeFixAvailable failed - expected no fixes, actual: ${fixes.length}`); + } + + if (!negative && (fixes === undefined || fixes.length === 0)) { + this.raiseError(`verifyCodeFixAvailable failed - expected code fixes, actual: 0`); + } + } + // Get the text of the entire line the caret is currently at private getCurrentLineContent() { const text = this.getFileContent(this.activeFile.fileName); @@ -3096,6 +3152,10 @@ namespace FourSlashInterface { public isValidBraceCompletionAtPosition(openingBrace: string) { this.state.verifyBraceCompletionAtPosition(this.negative, openingBrace); } + + public codeFixAvailable(errorCode?: number) { + this.state.verifyCodeFixAvailable(this.negative, errorCode); + } } export class Verify extends VerifyNegatable { @@ -3275,6 +3335,10 @@ namespace FourSlashInterface { this.DocCommentTemplate(/*expectedText*/ undefined, /*expectedOffset*/ undefined, /*empty*/ true); } + public codeFixAtPosition(expectedText: string, errorCode?: number): void { + this.state.verifyCodeFixAtPosition(expectedText, errorCode); + } + public navigationBar(json: any) { this.state.verifyNavigationBar(json); } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 8d90166ea8442..99cee7dbd41f6 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -486,6 +486,9 @@ namespace Harness.LanguageService { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean { return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace)); } + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): ts.CodeAction[] { + throw new Error("Not supported on the shim."); + } getEmitOutput(fileName: string): ts.EmitOutput { return unwrapJSONCallResult(this.shim.getEmitOutput(fileName)); } diff --git a/src/server/client.ts b/src/server/client.ts index 688408dfb88d3..2acc0a58cadd6 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -425,11 +425,35 @@ namespace ts.server { } getSyntacticDiagnostics(fileName: string): Diagnostic[] { - throw new Error("Not Implemented Yet."); + const args: protocol.SyntacticDiagnosticsSyncRequestArgs = { file: fileName }; + + const request = this.processRequest(CommandNames.SyntacticDiagnosticsSync, args); + const response = this.processResponse(request); + + return (response.body).map(entry => this.convertDiagnostic(entry, fileName)); } getSemanticDiagnostics(fileName: string): Diagnostic[] { - throw new Error("Not Implemented Yet."); + const args: protocol.SemanticDiagnosticsSyncRequestArgs = { file: fileName }; + + const request = this.processRequest(CommandNames.SemanticDiagnosticsSync, args); + const response = this.processResponse(request); + + return (response.body).map(entry => this.convertDiagnostic(entry, fileName)); + } + + convertDiagnostic(entry: protocol.Diagnostic, fileName: string): Diagnostic { + const start = this.lineOffsetToPosition(fileName, entry.start); + const end = this.lineOffsetToPosition(fileName, entry.end); + + return { + file: undefined, + start: start, + length: end - start, + messageText: entry.text, + category: undefined, + code: entry.code + }; } getCompilerOptionsDiagnostics(): Diagnostic[] { @@ -630,6 +654,48 @@ namespace ts.server { throw new Error("Not Implemented Yet."); } + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): CodeAction[] { + const startLineOffset = this.positionToOneBasedLineOffset(fileName, start); + const endLineOffset = this.positionToOneBasedLineOffset(fileName, end); + + const args: protocol.CodeFixRequestArgs = { + file: fileName, + startLine: startLineOffset.line, + startOffset: startLineOffset.offset, + endLine: endLineOffset.line, + endOffset: endLineOffset.offset, + errorCodes: errorCodes, + }; + + const request = this.processRequest(CommandNames.GetCodeFixes, args); + const response = this.processResponse(request); + + return response.body.map(entry => this.convertCodeActions(entry, fileName)); + } + + convertCodeActions(entry: protocol.CodeAction, fileName: string): CodeAction { + return { + description: entry.description, + changes: entry.changes.map(change => ({ + fileName: change.fileName, + textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName)) + })) + }; + } + + convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): ts.TextChange { + const start = this.lineOffsetToPosition(fileName, change.start); + const end = this.lineOffsetToPosition(fileName, change.end); + + return { + span: { + start: start, + length: end - start + }, + newText: change.newText ? change.newText : "" + }; + } + getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] { const lineOffset = this.positionToOneBasedLineOffset(fileName, position); const args: protocol.FileLocationRequestArgs = { diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index cd6f12453d1ed..0ae54b8e0cfdd 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -1,4 +1,4 @@ -/** +/** * Declaration module describing the TypeScript Server protocol */ declare namespace ts.server.protocol { @@ -236,6 +236,53 @@ declare namespace ts.server.protocol { position?: number; } + /** + * Request for the available codefixes at a specific position. + */ + export interface CodeFixRequest extends Request { + arguments: CodeFixRequestArgs; + } + + /** + * Instances of this interface specify errorcodes on a specific location in a sourcefile. + */ + export interface CodeFixRequestArgs extends FileRequestArgs { + /** + * The line number for the request (1-based). + */ + startLine?: number; + + /** + * The character offset (on the line) for the request (1-based). + */ + startOffset?: number; + + /** + * Position (can be specified instead of line/offset pair) + */ + startPosition?: number; + + /** + * The line number for the request (1-based). + */ + endLine?: number; + + /** + * The character offset (on the line) for the request (1-based). + */ + endOffset?: number; + + /** + * Position (can be specified instead of line/offset pair) + */ + endPosition?: number; + + /** + * Errorcodes we want to get the fixes for. + */ + errorCodes?: number[]; + } + /** * A request whose arguments specify a file location (file, line, col). */ @@ -1133,6 +1180,23 @@ declare namespace ts.server.protocol { newText: string; } + export interface FileCodeEdits { + fileName: string; + textChanges: CodeEdit[]; + } + + export interface CodeFixResponse extends Response { + /** The code actions that are available */ + body?: CodeAction[]; + } + + export interface CodeAction { + /** Description of the code action to display in the UI of the editor */ + description: string; + /** Text changes to apply to each file as part of the code action */ + changes: FileCodeEdits[]; + } + /** * Format and format on key response message. */ @@ -1507,6 +1571,11 @@ declare namespace ts.server.protocol { * Text of diagnostic message. */ text: string; + + /** + * The error code of the diagnostic message. + */ + code?: number; } export interface DiagnosticEventBody { diff --git a/src/server/session.ts b/src/server/session.ts index 3b95bdd6fb9b6..a80b1bea2f39a 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1,4 +1,4 @@ -/// +/// /// /// /// @@ -44,7 +44,8 @@ namespace ts.server { return { start: scriptInfo.positionToLineOffset(diag.start), end: scriptInfo.positionToLineOffset(diag.start + diag.length), - text: ts.flattenDiagnosticMessageText(diag.messageText, "\n") + text: ts.flattenDiagnosticMessageText(diag.messageText, "\n"), + code: diag.code }; } @@ -134,6 +135,9 @@ namespace ts.server { export const NameOrDottedNameSpan = "nameOrDottedNameSpan"; export const BreakpointStatement = "breakpointStatement"; export const CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects"; + export const GetCodeFixes = "getCodeFixes"; + export const GetCodeFixesFull = "getCodeFixes-full"; + export const GetSupportedCodeFixes = "getSupportedCodeFixes"; } export function formatMessage(msg: T, logger: server.Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string { @@ -757,7 +761,7 @@ namespace ts.server { return this.getFileAndProjectWorker(args.file, args.projectFileName, /*refreshInferredProjects*/ false, errorOnMissingProject); } - private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string, refreshInferredProjects: boolean, errorOnMissingProject: boolean) { + private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string, refreshInferredProjects: boolean, errorOnMissingProject: boolean) { const file = toNormalizedPath(uncheckedFileName); const project: Project = this.getProject(projectFileName) || this.projectService.getDefaultProjectForFile(file, refreshInferredProjects); if (!project && errorOnMissingProject) { @@ -848,13 +852,7 @@ namespace ts.server { return undefined; } - return edits.map((edit) => { - return { - start: scriptInfo.positionToLineOffset(edit.span.start), - end: scriptInfo.positionToLineOffset(ts.textSpanEnd(edit.span)), - newText: edit.newText ? edit.newText : "" - }; - }); + return edits.map(edit => this.convertTextChangeToCodeEdit(edit, scriptInfo)); } private getFormattingEditsForRangeFull(args: protocol.FormatRequestArgs) { @@ -1205,6 +1203,55 @@ namespace ts.server { } } + private getSupportedCodeFixes(): string[] { + return ts.getSupportedCodeFixes(); + } + + private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): protocol.CodeAction[] | CodeAction[] { + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); + + const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const startPosition = getStartPosition(); + const endPosition = getEndPosition(); + + const codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes); + if (!codeActions) { + return undefined; + } + if (simplifiedResult) { + return codeActions.map(codeAction => this.mapCodeAction(codeAction, scriptInfo)); + } + else { + return codeActions; + } + + function getStartPosition() { + return args.startPosition !== undefined ? args.startPosition : scriptInfo.lineOffsetToPosition(args.startLine, args.startOffset); + } + + function getEndPosition() { + return args.endPosition !== undefined ? args.endPosition : scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); + } + } + + private mapCodeAction(codeAction: CodeAction, scriptInfo: ScriptInfo): protocol.CodeAction { + return { + description: codeAction.description, + changes: codeAction.changes.map(change => ({ + fileName: change.fileName, + textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)) + })) + }; + } + + private convertTextChangeToCodeEdit(change: ts.TextChange, scriptInfo: ScriptInfo): protocol.CodeEdit { + return { + start: scriptInfo.positionToLineOffset(change.span.start), + end: scriptInfo.positionToLineOffset(change.span.start + change.span.length), + newText: change.newText ? change.newText : "" + }; + } + private getBraceMatching(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.TextSpan[] | TextSpan[] { const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); @@ -1527,6 +1574,15 @@ namespace ts.server { [CommandNames.ReloadProjects]: (request: protocol.ReloadProjectsRequest) => { this.projectService.reloadProjects(); return this.notRequired(); + }, + [CommandNames.GetCodeFixes]: (request: protocol.CodeFixRequest) => { + return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.GetCodeFixesFull]: (request: protocol.CodeFixRequest) => { + return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ false)); + }, + [CommandNames.GetSupportedCodeFixes]: (request: protocol.Request) => { + return this.requiredResponse(this.getSupportedCodeFixes()); } }); diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 3f99da9977ea0..beceab79229c0 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -201,7 +201,8 @@ namespace ts.server { dispose: (): any => throwLanguageServiceIsDisabledError(), getCompletionEntrySymbol: (): any => throwLanguageServiceIsDisabledError(), getImplementationAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getSourceFile: (): any => throwLanguageServiceIsDisabledError() + getSourceFile: (): any => throwLanguageServiceIsDisabledError(), + getCodeFixesAtPosition: (): any => throwLanguageServiceIsDisabledError() }; export interface ServerLanguageServiceHost { diff --git a/src/services/codefixes/codeFixProvider.ts b/src/services/codefixes/codeFixProvider.ts new file mode 100644 index 0000000000000..c61cbe1b19ea5 --- /dev/null +++ b/src/services/codefixes/codeFixProvider.ts @@ -0,0 +1,48 @@ +/* @internal */ +namespace ts { + export interface CodeFix { + errorCodes: number[]; + getCodeActions(context: CodeFixContext): CodeAction[] | undefined; + } + + export interface CodeFixContext { + errorCode: number; + sourceFile: SourceFile; + span: TextSpan; + program: Program; + newLineCharacter: string; + } + + export namespace codefix { + const codeFixes = createMap(); + + export function registerCodeFix(action: CodeFix) { + forEach(action.errorCodes, error => { + let fixes = codeFixes[error]; + if (!fixes) { + fixes = []; + codeFixes[error] = fixes; + } + fixes.push(action); + }); + } + + export function getSupportedErrorCodes() { + return Object.keys(codeFixes); + } + + export function getFixes(context: CodeFixContext): CodeAction[] { + const fixes = codeFixes[context.errorCode]; + let allActions: CodeAction[] = []; + + forEach(fixes, f => { + const actions = f.getCodeActions(context); + if (actions && actions.length > 0) { + allActions = allActions.concat(actions); + } + }); + + return allActions; + } + } +} diff --git a/src/services/codefixes/fixes.ts b/src/services/codefixes/fixes.ts new file mode 100644 index 0000000000000..d64a99ca1b9e6 --- /dev/null +++ b/src/services/codefixes/fixes.ts @@ -0,0 +1 @@ +/// diff --git a/src/services/codefixes/superFixes.ts b/src/services/codefixes/superFixes.ts new file mode 100644 index 0000000000000..f117799f3e1c2 --- /dev/null +++ b/src/services/codefixes/superFixes.ts @@ -0,0 +1,81 @@ +/* @internal */ +namespace ts.codefix { + function getOpenBraceEnd(constructor: ConstructorDeclaration, sourceFile: SourceFile) { + // First token is the open curly, this is where we want to put the 'super' call. + return constructor.body.getFirstToken(sourceFile).getEnd(); + } + + registerCodeFix({ + errorCodes: [Diagnostics.Constructors_for_derived_classes_must_contain_a_super_call.code], + getCodeActions: (context: CodeFixContext) => { + const sourceFile = context.sourceFile; + const token = getTokenAtPosition(sourceFile, context.span.start); + + if (token.kind !== SyntaxKind.ConstructorKeyword) { + return undefined; + } + + const newPosition = getOpenBraceEnd(token.parent, sourceFile); + return [{ + description: getLocaleSpecificMessage(Diagnostics.Add_missing_super_call), + changes: [{ fileName: sourceFile.fileName, textChanges: [{ newText: "super();", span: { start: newPosition, length: 0 } }] }] + }]; + } + }); + + registerCodeFix({ + errorCodes: [Diagnostics.super_must_be_called_before_accessing_this_in_the_constructor_of_a_derived_class.code], + getCodeActions: (context: CodeFixContext) => { + const sourceFile = context.sourceFile; + + const token = getTokenAtPosition(sourceFile, context.span.start); + if (token.kind !== SyntaxKind.ThisKeyword) { + return undefined; + } + + const constructor = getContainingFunction(token); + const superCall = findSuperCall((constructor).body); + if (!superCall) { + return undefined; + } + + // figure out if the this access is actuall inside the supercall + // i.e. super(this.a), since in that case we won't suggest a fix + if (superCall.expression && superCall.expression.kind == SyntaxKind.CallExpression) { + const arguments = (superCall.expression).arguments; + for (let i = 0; i < arguments.length; i++) { + if ((arguments[i]).expression === token) { + return undefined; + } + } + } + + const newPosition = getOpenBraceEnd(constructor, sourceFile); + const changes = [{ + fileName: sourceFile.fileName, textChanges: [{ + newText: superCall.getText(sourceFile), + span: { start: newPosition, length: 0 } + }, + { + newText: "", + span: { start: superCall.getStart(sourceFile), length: superCall.getWidth(sourceFile) } + }] + }]; + + return [{ + description: getLocaleSpecificMessage(Diagnostics.Make_super_call_the_first_statement_in_the_constructor), + changes + }]; + + function findSuperCall(n: Node): ExpressionStatement { + if (n.kind === SyntaxKind.ExpressionStatement && isSuperCall((n).expression)) { + return n; + } + if (isFunctionLike(n)) { + return undefined; + } + return forEachChild(n, findSuperCall); + } + } + }); +} \ No newline at end of file diff --git a/src/services/services.ts b/src/services/services.ts index ddac0d51459e3..cdad39477ce2a 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -24,6 +24,8 @@ /// /// /// +/// +/// namespace ts { /** The version of the language service API */ @@ -664,6 +666,7 @@ namespace ts { return { getNodeConstructor: () => NodeObject, getTokenConstructor: () => TokenObject, + getIdentifierConstructor: () => IdentifierObject, getSourceFileConstructor: () => SourceFileObject, getSymbolConstructor: () => SymbolObject, @@ -730,9 +733,13 @@ namespace ts { }; } - // Cache host information about script should be refreshed + export function getSupportedCodeFixes() { + return codefix.getSupportedErrorCodes(); + } + + // Cache host information about script Should be refreshed // at each language service public entry point, since we don't know when - // set of scripts handled by the host changes. + // the set of scripts handled by the host changes. class HostCache { private fileNameToEntry: FileMap; private _compilationSettings: CompilerOptions; @@ -1654,6 +1661,34 @@ namespace ts { return []; } + function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): CodeAction[] { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const span = { start, length: end - start }; + const newLineChar = getNewLineOrDefaultFromHost(host); + + let allFixes: CodeAction[] = []; + + forEach(errorCodes, error => { + cancellationToken.throwIfCancellationRequested(); + + const context = { + errorCode: error, + sourceFile: sourceFile, + span: span, + program: program, + newLineCharacter: newLineChar + }; + + const fixes = codefix.getFixes(context); + if (fixes) { + allFixes = allFixes.concat(fixes); + } + }); + + return allFixes; + } + function getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion { return JsDoc.getDocCommentTemplateAtPosition(getNewLineOrDefaultFromHost(host), syntaxTreeCache.getCurrentSourceFile(fileName), position); } @@ -1877,6 +1912,7 @@ namespace ts { getFormattingEditsAfterKeystroke, getDocCommentTemplateAtPosition, isValidBraceCompletionAtPosition, + getCodeFixesAtPosition, getEmitOutput, getNonBoundSourceFile, getSourceFile, diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 58312c6f38ff3..0066ef94343d1 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -78,6 +78,8 @@ "formatting/rulesMap.ts", "formatting/rulesProvider.ts", "formatting/smartIndenter.ts", - "formatting/tokenRange.ts" + "formatting/tokenRange.ts", + "codeFixes/codeFixProvider.ts", + "codeFixes/fixes.ts" ] } diff --git a/src/services/types.ts b/src/services/types.ts index ac81a87daf55c..fafe274bce1df 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -239,6 +239,8 @@ namespace ts { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): CodeAction[]; + getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getProgram(): Program; @@ -291,6 +293,18 @@ namespace ts { newText: string; } + export interface FileTextChanges { + fileName: string; + textChanges: TextChange[]; + } + + export interface CodeAction { + /** Description of the code action to display in the UI of the editor */ + description: string; + /** Text changes to apply to each file as part of the code action */ + changes: FileTextChanges[]; + } + export interface TextInsertion { newText: string; /** The position in newText the caret should point to after the insertion. */ diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 5f86cbbafa488..5e8666e8670c5 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -136,6 +136,7 @@ declare namespace FourSlashInterface { typeDefinitionCountIs(expectedCount: number): void; implementationListIsEmpty(): void; isValidBraceCompletionAtPosition(openingBrace?: string): void; + codeFixAvailable(): void; } class verify extends verifyNegatable { assertHasRanges(ranges: Range[]): void; @@ -208,6 +209,7 @@ declare namespace FourSlashInterface { noMatchingBracePositionInCurrentFile(bracePosition: number): void; DocCommentTemplate(expectedText: string, expectedOffset: number, empty?: boolean): void; noDocCommentTemplate(): void; + codeFixAtPosition(expectedText: string, errorCode?: number): void; navigationBar(json: any): void; navigationItemsListCount(count: number, searchValue: string, matchKind?: string, fileName?: string): void; diff --git a/tests/cases/fourslash/server/codefix.ts b/tests/cases/fourslash/server/codefix.ts new file mode 100644 index 0000000000000..7fbe2cb4fd7bd --- /dev/null +++ b/tests/cases/fourslash/server/codefix.ts @@ -0,0 +1,10 @@ +/// + +////class Base{ +////} +////class C extends Base{ +//// constructor() {[| |] +//// } +////} + +verify.codeFixAtPosition('super();'); diff --git a/tests/cases/fourslash/superFix1.ts b/tests/cases/fourslash/superFix1.ts new file mode 100644 index 0000000000000..7fbe2cb4fd7bd --- /dev/null +++ b/tests/cases/fourslash/superFix1.ts @@ -0,0 +1,10 @@ +/// + +////class Base{ +////} +////class C extends Base{ +//// constructor() {[| |] +//// } +////} + +verify.codeFixAtPosition('super();'); diff --git a/tests/cases/fourslash/superFix2.ts b/tests/cases/fourslash/superFix2.ts new file mode 100644 index 0000000000000..880b5d43167c4 --- /dev/null +++ b/tests/cases/fourslash/superFix2.ts @@ -0,0 +1,13 @@ +/// + +////class Base{ +////} +////class C extends Base{ +//// private a:number; +//// constructor() {[| +//// this.a = 12; +//// super();|] +//// } +////} + +verify.codeFixAtPosition("super(); this.a = 12;"); \ No newline at end of file diff --git a/tests/cases/fourslash/superFix3.ts b/tests/cases/fourslash/superFix3.ts new file mode 100644 index 0000000000000..9b443b7df62f6 --- /dev/null +++ b/tests/cases/fourslash/superFix3.ts @@ -0,0 +1,12 @@ +/// + +////class Base{ +//// constructor(id: number) { } +////} +////class C extends Base{ +//// constructor(private a:number) { +//// super(this.a); +//// } +////} + +verify.not.codeFixAvailable(); \ No newline at end of file