Skip to content

Commit 9f73ae5

Browse files
authored
Merge pull request #10185 from Microsoft/pvb/codeaction/api
The API to support codefixes
2 parents 460de66 + 9b98d00 commit 9f73ae5

File tree

18 files changed

+537
-22
lines changed

18 files changed

+537
-22
lines changed

src/compiler/diagnosticMessages.json

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3073,13 +3073,40 @@
30733073
"category": "Error",
30743074
"code": 17010
30753075
},
3076-
30773076
"Circularity detected while resolving configuration: {0}": {
30783077
"category": "Error",
30793078
"code": 18000
30803079
},
30813080
"The path in an 'extends' options must be relative or rooted.": {
30823081
"category": "Error",
30833082
"code": 18001
3083+
},
3084+
"Add missing 'super()' call.": {
3085+
"category": "Message",
3086+
"code": 90001
3087+
},
3088+
"Make 'super()' call the first statement in the constructor.": {
3089+
"category": "Message",
3090+
"code": 90002
3091+
},
3092+
"Change 'extends' to 'implements'": {
3093+
"category": "Message",
3094+
"code": 90003
3095+
},
3096+
"Remove unused identifiers": {
3097+
"category": "Message",
3098+
"code": 90004
3099+
},
3100+
"Implement interface on reference": {
3101+
"category": "Message",
3102+
"code": 90005
3103+
},
3104+
"Implement interface on class": {
3105+
"category": "Message",
3106+
"code": 90006
3107+
},
3108+
"Implement inherited abstract class": {
3109+
"category": "Message",
3110+
"code": 90007
30843111
}
30853112
}

src/harness/fourslash.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//
1+
//
22
// Copyright (c) Microsoft Corporation. All rights reserved.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -427,7 +427,7 @@ namespace FourSlash {
427427

428428
if (exists !== negative) {
429429
this.printErrorLog(negative, this.getAllDiagnostics());
430-
throw new Error("Failure between markers: " + startMarkerName + ", " + endMarkerName);
430+
throw new Error(`Failure between markers: '${startMarkerName}', '${endMarkerName}'`);
431431
}
432432
}
433433

@@ -742,7 +742,6 @@ namespace FourSlash {
742742
}
743743
}
744744

745-
746745
public verifyCompletionListAllowsNewIdentifier(negative: boolean) {
747746
const completions = this.getCompletionListAtCaret();
748747

@@ -1611,7 +1610,7 @@ namespace FourSlash {
16111610
if (isFormattingEdit) {
16121611
const newContent = this.getFileContent(fileName);
16131612

1614-
if (newContent.replace(/\s/g, "") !== oldContent.replace(/\s/g, "")) {
1613+
if (this.removeWhitespace(newContent) !== this.removeWhitespace(oldContent)) {
16151614
this.raiseError("Formatting operation destroyed non-whitespace content");
16161615
}
16171616
}
@@ -1677,6 +1676,10 @@ namespace FourSlash {
16771676
}
16781677
}
16791678

1679+
private removeWhitespace(text: string): string {
1680+
return text.replace(/\s/g, "");
1681+
}
1682+
16801683
public goToBOF() {
16811684
this.goToPosition(0);
16821685
}
@@ -2038,6 +2041,47 @@ namespace FourSlash {
20382041
}
20392042
}
20402043

2044+
private getCodeFixes(errorCode?: number) {
2045+
const fileName = this.activeFile.fileName;
2046+
const diagnostics = this.getDiagnostics(fileName);
2047+
2048+
if (diagnostics.length === 0) {
2049+
this.raiseError("Errors expected.");
2050+
}
2051+
2052+
if (diagnostics.length > 1 && errorCode !== undefined) {
2053+
this.raiseError("When there's more than one error, you must specify the errror to fix.");
2054+
}
2055+
2056+
const diagnostic = !errorCode ? diagnostics[0] : ts.find(diagnostics, d => d.code == errorCode);
2057+
2058+
return this.languageService.getCodeFixesAtPosition(fileName, diagnostic.start, diagnostic.length, [diagnostic.code]);
2059+
}
2060+
2061+
public verifyCodeFixAtPosition(expectedText: string, errorCode?: number) {
2062+
const ranges = this.getRanges();
2063+
if (ranges.length == 0) {
2064+
this.raiseError("At least one range should be specified in the testfile.");
2065+
}
2066+
2067+
const actual = this.getCodeFixes(errorCode);
2068+
2069+
if (!actual || actual.length == 0) {
2070+
this.raiseError("No codefixes returned.");
2071+
}
2072+
2073+
if (actual.length > 1) {
2074+
this.raiseError("More than 1 codefix returned.");
2075+
}
2076+
2077+
this.applyEdits(actual[0].changes[0].fileName, actual[0].changes[0].textChanges, /*isFormattingEdit*/ false);
2078+
const actualText = this.rangeText(ranges[0]);
2079+
2080+
if (this.removeWhitespace(actualText) !== this.removeWhitespace(expectedText)) {
2081+
this.raiseError(`Actual text doesn't match expected text. Actual: '${actualText}' Expected: '${expectedText}'`);
2082+
}
2083+
}
2084+
20412085
public verifyDocCommentTemplate(expected?: ts.TextInsertion) {
20422086
const name = "verifyDocCommentTemplate";
20432087
const actual = this.languageService.getDocCommentTemplateAtPosition(this.activeFile.fileName, this.currentCaretPosition);
@@ -2309,6 +2353,18 @@ namespace FourSlash {
23092353
}
23102354
}
23112355

2356+
public verifyCodeFixAvailable(negative: boolean, errorCode?: number) {
2357+
const fixes = this.getCodeFixes(errorCode);
2358+
2359+
if (negative && fixes && fixes.length > 0) {
2360+
this.raiseError(`verifyCodeFixAvailable failed - expected no fixes, actual: ${fixes.length}`);
2361+
}
2362+
2363+
if (!negative && (fixes === undefined || fixes.length === 0)) {
2364+
this.raiseError(`verifyCodeFixAvailable failed - expected code fixes, actual: 0`);
2365+
}
2366+
}
2367+
23122368
// Get the text of the entire line the caret is currently at
23132369
private getCurrentLineContent() {
23142370
const text = this.getFileContent(this.activeFile.fileName);
@@ -3096,6 +3152,10 @@ namespace FourSlashInterface {
30963152
public isValidBraceCompletionAtPosition(openingBrace: string) {
30973153
this.state.verifyBraceCompletionAtPosition(this.negative, openingBrace);
30983154
}
3155+
3156+
public codeFixAvailable(errorCode?: number) {
3157+
this.state.verifyCodeFixAvailable(this.negative, errorCode);
3158+
}
30993159
}
31003160

31013161
export class Verify extends VerifyNegatable {
@@ -3275,6 +3335,10 @@ namespace FourSlashInterface {
32753335
this.DocCommentTemplate(/*expectedText*/ undefined, /*expectedOffset*/ undefined, /*empty*/ true);
32763336
}
32773337

3338+
public codeFixAtPosition(expectedText: string, errorCode?: number): void {
3339+
this.state.verifyCodeFixAtPosition(expectedText, errorCode);
3340+
}
3341+
32783342
public navigationBar(json: any) {
32793343
this.state.verifyNavigationBar(json);
32803344
}

src/harness/harnessLanguageService.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,9 @@ namespace Harness.LanguageService {
486486
isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean {
487487
return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace));
488488
}
489+
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): ts.CodeAction[] {
490+
throw new Error("Not supported on the shim.");
491+
}
489492
getEmitOutput(fileName: string): ts.EmitOutput {
490493
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
491494
}

src/server/client.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -425,11 +425,35 @@ namespace ts.server {
425425
}
426426

427427
getSyntacticDiagnostics(fileName: string): Diagnostic[] {
428-
throw new Error("Not Implemented Yet.");
428+
const args: protocol.SyntacticDiagnosticsSyncRequestArgs = { file: fileName };
429+
430+
const request = this.processRequest<protocol.SyntacticDiagnosticsSyncRequest>(CommandNames.SyntacticDiagnosticsSync, args);
431+
const response = this.processResponse<protocol.SyntacticDiagnosticsSyncResponse>(request);
432+
433+
return (<protocol.Diagnostic[]>response.body).map(entry => this.convertDiagnostic(entry, fileName));
429434
}
430435

431436
getSemanticDiagnostics(fileName: string): Diagnostic[] {
432-
throw new Error("Not Implemented Yet.");
437+
const args: protocol.SemanticDiagnosticsSyncRequestArgs = { file: fileName };
438+
439+
const request = this.processRequest<protocol.SemanticDiagnosticsSyncRequest>(CommandNames.SemanticDiagnosticsSync, args);
440+
const response = this.processResponse<protocol.SemanticDiagnosticsSyncResponse>(request);
441+
442+
return (<protocol.Diagnostic[]>response.body).map(entry => this.convertDiagnostic(entry, fileName));
443+
}
444+
445+
convertDiagnostic(entry: protocol.Diagnostic, fileName: string): Diagnostic {
446+
const start = this.lineOffsetToPosition(fileName, entry.start);
447+
const end = this.lineOffsetToPosition(fileName, entry.end);
448+
449+
return {
450+
file: undefined,
451+
start: start,
452+
length: end - start,
453+
messageText: entry.text,
454+
category: undefined,
455+
code: entry.code
456+
};
433457
}
434458

435459
getCompilerOptionsDiagnostics(): Diagnostic[] {
@@ -630,6 +654,48 @@ namespace ts.server {
630654
throw new Error("Not Implemented Yet.");
631655
}
632656

657+
getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): CodeAction[] {
658+
const startLineOffset = this.positionToOneBasedLineOffset(fileName, start);
659+
const endLineOffset = this.positionToOneBasedLineOffset(fileName, end);
660+
661+
const args: protocol.CodeFixRequestArgs = {
662+
file: fileName,
663+
startLine: startLineOffset.line,
664+
startOffset: startLineOffset.offset,
665+
endLine: endLineOffset.line,
666+
endOffset: endLineOffset.offset,
667+
errorCodes: errorCodes,
668+
};
669+
670+
const request = this.processRequest<protocol.CodeFixRequest>(CommandNames.GetCodeFixes, args);
671+
const response = this.processResponse<protocol.CodeFixResponse>(request);
672+
673+
return response.body.map(entry => this.convertCodeActions(entry, fileName));
674+
}
675+
676+
convertCodeActions(entry: protocol.CodeAction, fileName: string): CodeAction {
677+
return {
678+
description: entry.description,
679+
changes: entry.changes.map(change => ({
680+
fileName: change.fileName,
681+
textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName))
682+
}))
683+
};
684+
}
685+
686+
convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): ts.TextChange {
687+
const start = this.lineOffsetToPosition(fileName, change.start);
688+
const end = this.lineOffsetToPosition(fileName, change.end);
689+
690+
return {
691+
span: {
692+
start: start,
693+
length: end - start
694+
},
695+
newText: change.newText ? change.newText : ""
696+
};
697+
}
698+
633699
getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] {
634700
const lineOffset = this.positionToOneBasedLineOffset(fileName, position);
635701
const args: protocol.FileLocationRequestArgs = {

src/server/protocol.d.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**
1+
/**
22
* Declaration module describing the TypeScript Server protocol
33
*/
44
declare namespace ts.server.protocol {
@@ -236,6 +236,53 @@ declare namespace ts.server.protocol {
236236
position?: number;
237237
}
238238

239+
/**
240+
* Request for the available codefixes at a specific position.
241+
*/
242+
export interface CodeFixRequest extends Request {
243+
arguments: CodeFixRequestArgs;
244+
}
245+
246+
/**
247+
* Instances of this interface specify errorcodes on a specific location in a sourcefile.
248+
*/
249+
export interface CodeFixRequestArgs extends FileRequestArgs {
250+
/**
251+
* The line number for the request (1-based).
252+
*/
253+
startLine?: number;
254+
255+
/**
256+
* The character offset (on the line) for the request (1-based).
257+
*/
258+
startOffset?: number;
259+
260+
/**
261+
* Position (can be specified instead of line/offset pair)
262+
*/
263+
startPosition?: number;
264+
265+
/**
266+
* The line number for the request (1-based).
267+
*/
268+
endLine?: number;
269+
270+
/**
271+
* The character offset (on the line) for the request (1-based).
272+
*/
273+
endOffset?: number;
274+
275+
/**
276+
* Position (can be specified instead of line/offset pair)
277+
*/
278+
endPosition?: number;
279+
280+
/**
281+
* Errorcodes we want to get the fixes for.
282+
*/
283+
errorCodes?: number[];
284+
}
285+
239286
/**
240287
* A request whose arguments specify a file location (file, line, col).
241288
*/
@@ -1133,6 +1180,23 @@ declare namespace ts.server.protocol {
11331180
newText: string;
11341181
}
11351182

1183+
export interface FileCodeEdits {
1184+
fileName: string;
1185+
textChanges: CodeEdit[];
1186+
}
1187+
1188+
export interface CodeFixResponse extends Response {
1189+
/** The code actions that are available */
1190+
body?: CodeAction[];
1191+
}
1192+
1193+
export interface CodeAction {
1194+
/** Description of the code action to display in the UI of the editor */
1195+
description: string;
1196+
/** Text changes to apply to each file as part of the code action */
1197+
changes: FileCodeEdits[];
1198+
}
1199+
11361200
/**
11371201
* Format and format on key response message.
11381202
*/
@@ -1507,6 +1571,11 @@ declare namespace ts.server.protocol {
15071571
* Text of diagnostic message.
15081572
*/
15091573
text: string;
1574+
1575+
/**
1576+
* The error code of the diagnostic message.
1577+
*/
1578+
code?: number;
15101579
}
15111580

15121581
export interface DiagnosticEventBody {

0 commit comments

Comments
 (0)