mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-26 21:05:54 -06:00
Merge pull request #10185 from Microsoft/pvb/codeaction/api
The API to support codefixes
This commit is contained in:
commit
9f73ae5903
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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<protocol.SyntacticDiagnosticsSyncRequest>(CommandNames.SyntacticDiagnosticsSync, args);
|
||||
const response = this.processResponse<protocol.SyntacticDiagnosticsSyncResponse>(request);
|
||||
|
||||
return (<protocol.Diagnostic[]>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<protocol.SemanticDiagnosticsSyncRequest>(CommandNames.SemanticDiagnosticsSync, args);
|
||||
const response = this.processResponse<protocol.SemanticDiagnosticsSyncResponse>(request);
|
||||
|
||||
return (<protocol.Diagnostic[]>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<protocol.CodeFixRequest>(CommandNames.GetCodeFixes, args);
|
||||
const response = this.processResponse<protocol.CodeFixResponse>(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 = {
|
||||
|
||||
71
src/server/protocol.d.ts
vendored
71
src/server/protocol.d.ts
vendored
@ -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 {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
/// <reference path="..\compiler\commandLineParser.ts" />
|
||||
/// <reference path="..\compiler\commandLineParser.ts" />
|
||||
/// <reference path="..\services\services.ts" />
|
||||
/// <reference path="protocol.d.ts" />
|
||||
/// <reference path="editorServices.ts" />
|
||||
@ -55,7 +55,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
|
||||
};
|
||||
}
|
||||
|
||||
@ -145,6 +146,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<T extends protocol.Message>(msg: T, logger: server.Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string {
|
||||
@ -772,7 +776,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) {
|
||||
@ -863,13 +867,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) {
|
||||
@ -1220,6 +1218,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);
|
||||
|
||||
@ -1542,6 +1589,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());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
48
src/services/codefixes/codeFixProvider.ts
Normal file
48
src/services/codefixes/codeFixProvider.ts
Normal file
@ -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<CodeFix[]>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/services/codefixes/fixes.ts
Normal file
1
src/services/codefixes/fixes.ts
Normal file
@ -0,0 +1 @@
|
||||
///<reference path='superFixes.ts' />
|
||||
81
src/services/codefixes/superFixes.ts
Normal file
81
src/services/codefixes/superFixes.ts
Normal file
@ -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(<ConstructorDeclaration>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((<ConstructorDeclaration>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 = (<CallExpression>superCall.expression).arguments;
|
||||
for (let i = 0; i < arguments.length; i++) {
|
||||
if ((<PropertyAccessExpression>arguments[i]).expression === token) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPosition = getOpenBraceEnd(<ConstructorDeclaration>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((<ExpressionStatement>n).expression)) {
|
||||
return <ExpressionStatement>n;
|
||||
}
|
||||
if (isFunctionLike(n)) {
|
||||
return undefined;
|
||||
}
|
||||
return forEachChild(n, findSuperCall);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -24,6 +24,8 @@
|
||||
/// <reference path='transpile.ts' />
|
||||
/// <reference path='formatting\formatting.ts' />
|
||||
/// <reference path='formatting\smartIndenter.ts' />
|
||||
/// <reference path='codefixes\codeFixProvider.ts' />
|
||||
/// <reference path='codefixes\fixes.ts' />
|
||||
|
||||
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<HostFileInformation>;
|
||||
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,
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@ -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. */
|
||||
|
||||
@ -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;
|
||||
|
||||
10
tests/cases/fourslash/server/codefix.ts
Normal file
10
tests/cases/fourslash/server/codefix.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
////class Base{
|
||||
////}
|
||||
////class C extends Base{
|
||||
//// constructor() {[| |]
|
||||
//// }
|
||||
////}
|
||||
|
||||
verify.codeFixAtPosition('super();');
|
||||
10
tests/cases/fourslash/superFix1.ts
Normal file
10
tests/cases/fourslash/superFix1.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
////class Base{
|
||||
////}
|
||||
////class C extends Base{
|
||||
//// constructor() {[| |]
|
||||
//// }
|
||||
////}
|
||||
|
||||
verify.codeFixAtPosition('super();');
|
||||
13
tests/cases/fourslash/superFix2.ts
Normal file
13
tests/cases/fourslash/superFix2.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
////class Base{
|
||||
////}
|
||||
////class C extends Base{
|
||||
//// private a:number;
|
||||
//// constructor() {[|
|
||||
//// this.a = 12;
|
||||
//// super();|]
|
||||
//// }
|
||||
////}
|
||||
|
||||
verify.codeFixAtPosition("super(); this.a = 12;");
|
||||
12
tests/cases/fourslash/superFix3.ts
Normal file
12
tests/cases/fourslash/superFix3.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/// <reference path='fourslash.ts' />
|
||||
|
||||
////class Base{
|
||||
//// constructor(id: number) { }
|
||||
////}
|
||||
////class C extends Base{
|
||||
//// constructor(private a:number) {
|
||||
//// super(this.a);
|
||||
//// }
|
||||
////}
|
||||
|
||||
verify.not.codeFixAvailable();
|
||||
Loading…
x
Reference in New Issue
Block a user