diff --git a/src/services/codefixes/codeFixProvider.ts b/src/services/codefixes/codeFixProvider.ts new file mode 100644 index 00000000000..ca877aef46a --- /dev/null +++ b/src/services/codefixes/codeFixProvider.ts @@ -0,0 +1,53 @@ +/* @internal */ +namespace ts { + export interface CodeFix { + name: string; + errorCodes: string[]; + getCodeActions(context: CodeFixContext): CodeAction[]; + } + + export interface CodeFixContext { + errorCode: string; + sourceFile: SourceFile; + span: TextSpan; + checker: TypeChecker; + newLineCharacter: string; + } + + export namespace codeFix { + const codeFixes: Map = {}; + + export function registerCodeFix(action: CodeFix) { + forEach(action.errorCodes, error => { + let fixes = codeFixes[error]; + if (!fixes) { + fixes = []; + codeFixes[error] = fixes; + } + fixes.push(action); + }); + } + + export class CodeFixProvider { + public static getSupportedErrorCodes() { + return getKeys(codeFixes); + } + + public getFixes(context: CodeFixContext): CodeAction[] { + const fixes = codeFixes[context.errorCode]; + let allActions: CodeAction[] = []; + + Debug.assert(fixes && fixes.length > 0, "No fixes found for error: '${errorCode}'."); + + forEach(fixes, f => { + const actions = f.getCodeActions(context); + if (actions && actions.length > 0) { + allActions = allActions.concat(actions); + } + }); + + return allActions; + } + } + } +} \ No newline at end of file diff --git a/src/services/codefixes/references.ts b/src/services/codefixes/references.ts new file mode 100644 index 00000000000..3675d626678 --- /dev/null +++ b/src/services/codefixes/references.ts @@ -0,0 +1,6 @@ +/// +/// +/// +/// +/// +/// diff --git a/src/services/codefixes/superFixes.ts b/src/services/codefixes/superFixes.ts new file mode 100644 index 00000000000..2c6a67de513 --- /dev/null +++ b/src/services/codefixes/superFixes.ts @@ -0,0 +1,65 @@ +/* @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({ + name: "AddMissingSuperCallFix", + errorCodes: ["TS2377"], + getCodeActions: (context: CodeFixContext) => { + const sourceFile = context.sourceFile; + const token = getTokenAtPosition(sourceFile, context.span.start); + Debug.assert(token.kind === SyntaxKind.ConstructorKeyword, "Failed to find the constructor."); + + 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({ + name: "MakeSuperCallTheFirstStatementInTheConstructor", + errorCodes: ["TS17009"], + getCodeActions: (context: CodeFixContext) => { + const sourceFile = context.sourceFile; + + const token = getTokenAtPosition(sourceFile, context.span.start); + const constructor = getContainingFunction(token); + Debug.assert(constructor.kind === SyntaxKind.Constructor, "Failed to find the constructor."); + + const superCall = findSuperCall((constructor).body); + Debug.assert(!!superCall, "Failed to find super call."); + + 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): Node { + if (n.kind === SyntaxKind.ExpressionStatement && isSuperCallExpression((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 aea4d5f872e..b189a6375b7 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -11,6 +11,7 @@ /// /// /// +/// namespace ts { /** The version of the language service API */ @@ -1237,6 +1238,8 @@ namespace ts { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: string[]): CodeAction[]; + getEmitOutput(fileName: string): EmitOutput; getProgram(): Program; @@ -1283,6 +1286,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. */ @@ -1886,9 +1901,13 @@ namespace ts { }; } - // Cache host information about script should be refreshed + export function getSupportedCodeFixes() { + return codeFix.CodeFixProvider.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; @@ -3022,6 +3041,7 @@ namespace ts { documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory())): LanguageService { const syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host); + const codeFixProvider: codeFix.CodeFixProvider = new codeFix.CodeFixProvider(); let ruleProvider: formatting.RulesProvider; let program: Program; let lastProjectVersion: string; @@ -7832,6 +7852,30 @@ namespace ts { return []; } + function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: string[]): CodeAction[] { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const checker = program.getTypeChecker(); + let allFixes: CodeAction[] = []; + + forEach(errorCodes, error => { + const context = { + errorCode: error, + sourceFile: sourceFile, + span: { start, length: end - start }, + checker: checker, + newLineCharacter: getNewLineOrDefaultFromHost(host) + }; + + const fixes = codeFixProvider.getFixes(context); + if (fixes) { + allFixes = allFixes.concat(fixes); + } + }); + + return allFixes; + } + /** * Checks if position points to a valid position to add JSDoc comments, and if so, * returns the appropriate template. Otherwise returns an empty string. @@ -8302,6 +8346,7 @@ namespace ts { getFormattingEditsAfterKeystroke, getDocCommentTemplateAtPosition, isValidBraceCompletionAtPosition, + getCodeFixesAtPosition, getEmitOutput, getNonBoundSourceFile, getProgram diff --git a/src/services/shims.ts b/src/services/shims.ts index 45c4b284ae7..dfa34d29acc 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -240,6 +240,8 @@ namespace ts { */ isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): string; + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: string): string; + getEmitOutput(fileName: string): string; getEmitOutputObject(fileName: string): EmitOutput; } @@ -255,6 +257,7 @@ namespace ts { getTSConfigFileInfo(fileName: string, sourceText: IScriptSnapshot): string; getDefaultCompilationSettings(): string; discoverTypings(discoverTypingsJson: string): string; + getSupportedCodeFixes(): string; } function logInternalError(logger: Logger, err: Error) { @@ -901,6 +904,16 @@ namespace ts { ); } + public getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: string): string { + return this.forwardJSONCall( + `getCodeFixesAtPosition( '${fileName}', ${start}, ${end}, ${errorCodes}')`, + () => { + const localErrors: string[] = JSON.parse(errorCodes); + return this.languageService.getCodeFixesAtPosition(fileName, start, end, localErrors); + } + ); + } + /// NAVIGATE TO /** Return a list of symbols that are interesting to navigate to */ @@ -1109,6 +1122,12 @@ namespace ts { info.compilerOptions); }); } + + public getSupportedCodeFixes(): string { + return this.forwardJSONCall("getSupportedCodeFixes()", + () => getSupportedCodeFixes() + ); + } } export class TypeScriptServicesFactory implements ShimFactory { diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index cfeb7c2fcd5..2cf75daa861 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -52,6 +52,9 @@ "formatting/rulesMap.ts", "formatting/rulesProvider.ts", "formatting/smartIndenter.ts", - "formatting/tokenRange.ts" + "formatting/tokenRange.ts", + "codeFixes/codeFixProvider.ts", + "codeFixes/references.ts", + "codeFixes/superFixes.ts" ] }