Skip to content

Commit 5c70641

Browse files
author
Andy Hanson
committed
Support a "getCombinedCodeFix" service
1 parent 618b670 commit 5c70641

File tree

67 files changed

+1720
-1117
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+1720
-1117
lines changed

Diff for: src/compiler/core.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -215,13 +215,27 @@ namespace ts {
215215

216216
export function zipWith<T, U, V>(arrayA: ReadonlyArray<T>, arrayB: ReadonlyArray<U>, callback: (a: T, b: U, index: number) => V): V[] {
217217
const result: V[] = [];
218-
Debug.assert(arrayA.length === arrayB.length);
218+
Debug.assertEqual(arrayA.length, arrayB.length);
219219
for (let i = 0; i < arrayA.length; i++) {
220220
result.push(callback(arrayA[i], arrayB[i], i));
221221
}
222222
return result;
223223
}
224224

225+
export function zipToIterator<T, U>(arrayA: ReadonlyArray<T>, arrayB: ReadonlyArray<U>): Iterator<[T, U]> {
226+
Debug.assertEqual(arrayA.length, arrayB.length);
227+
let i = 0;
228+
return {
229+
next() {
230+
if (i === arrayA.length) {
231+
return { value: undefined as never, done: true };
232+
}
233+
i++;
234+
return { value: [arrayA[i - 1], arrayB[i - 1]], done: false };
235+
}
236+
};
237+
}
238+
225239
export function zipToMap<T>(keys: ReadonlyArray<string>, values: ReadonlyArray<T>): Map<T> {
226240
Debug.assert(keys.length === values.length);
227241
const map = createMap<T>();
@@ -1345,7 +1359,6 @@ namespace ts {
13451359
this.set(key, values = [value]);
13461360
}
13471361
return values;
1348-
13491362
}
13501363
function multiMapRemove<T>(this: MultiMap<T>, key: string, value: T) {
13511364
const values = this.get(key);

Diff for: src/compiler/types.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -949,22 +949,22 @@ namespace ts {
949949

950950
export interface ConstructorDeclaration extends FunctionLikeDeclarationBase, ClassElement, JSDocContainer {
951951
kind: SyntaxKind.Constructor;
952-
parent?: ClassDeclaration | ClassExpression;
952+
parent?: ClassLikeDeclaration;
953953
body?: FunctionBody;
954954
/* @internal */ returnFlowNode?: FlowNode;
955955
}
956956

957957
/** For when we encounter a semicolon in a class declaration. ES6 allows these as class elements. */
958958
export interface SemicolonClassElement extends ClassElement {
959959
kind: SyntaxKind.SemicolonClassElement;
960-
parent?: ClassDeclaration | ClassExpression;
960+
parent?: ClassLikeDeclaration;
961961
}
962962

963963
// See the comment on MethodDeclaration for the intuition behind GetAccessorDeclaration being a
964964
// ClassElement and an ObjectLiteralElement.
965965
export interface GetAccessorDeclaration extends FunctionLikeDeclarationBase, ClassElement, ObjectLiteralElement, JSDocContainer {
966966
kind: SyntaxKind.GetAccessor;
967-
parent?: ClassDeclaration | ClassExpression | ObjectLiteralExpression;
967+
parent?: ClassLikeDeclaration | ObjectLiteralExpression;
968968
name: PropertyName;
969969
body: FunctionBody;
970970
}
@@ -973,7 +973,7 @@ namespace ts {
973973
// ClassElement and an ObjectLiteralElement.
974974
export interface SetAccessorDeclaration extends FunctionLikeDeclarationBase, ClassElement, ObjectLiteralElement, JSDocContainer {
975975
kind: SyntaxKind.SetAccessor;
976-
parent?: ClassDeclaration | ClassExpression | ObjectLiteralExpression;
976+
parent?: ClassLikeDeclaration | ObjectLiteralExpression;
977977
name: PropertyName;
978978
body: FunctionBody;
979979
}
@@ -982,7 +982,7 @@ namespace ts {
982982

983983
export interface IndexSignatureDeclaration extends SignatureDeclarationBase, ClassElement, TypeElement {
984984
kind: SyntaxKind.IndexSignature;
985-
parent?: ClassDeclaration | ClassExpression | InterfaceDeclaration | TypeLiteralNode;
985+
parent?: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode;
986986
}
987987

988988
export interface TypeNode extends Node {
@@ -1986,7 +1986,7 @@ namespace ts {
19861986

19871987
export interface HeritageClause extends Node {
19881988
kind: SyntaxKind.HeritageClause;
1989-
parent?: InterfaceDeclaration | ClassDeclaration | ClassExpression;
1989+
parent?: InterfaceDeclaration | ClassLikeDeclaration;
19901990
token: SyntaxKind.ExtendsKeyword | SyntaxKind.ImplementsKeyword;
19911991
types: NodeArray<ExpressionWithTypeArguments>;
19921992
}

Diff for: src/harness/fourslash.ts

+33-11
Original file line numberDiff line numberDiff line change
@@ -2371,7 +2371,7 @@ Actual: ${stringify(fullActual)}`);
23712371
*/
23722372
public getAndApplyCodeActions(errorCode?: number, index?: number) {
23732373
const fileName = this.activeFile.fileName;
2374-
this.applyCodeActions(this.getCodeFixActions(fileName, errorCode), index);
2374+
this.applyCodeActions(this.getCodeFixes(fileName, errorCode), index);
23752375
}
23762376

23772377
public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) {
@@ -2424,6 +2424,16 @@ Actual: ${stringify(fullActual)}`);
24242424
this.verifyRangeIs(expectedText, includeWhiteSpace);
24252425
}
24262426

2427+
public verifyCodeFixAll(options: FourSlashInterface.VerifyCodeFixAllOptions): void {
2428+
const { groupId, newFileContent } = options;
2429+
const groupIds = ts.mapDefined(this.getCodeFixes(this.activeFile.fileName), a => a.groupId);
2430+
ts.Debug.assert(ts.contains(groupIds, groupId), "No available code fix has that group id.", () => `Expected '${groupId}'. Available group ids: ${groupIds}`);
2431+
const { changes, commands } = this.languageService.getCombinedCodeFix(this.activeFile.fileName, groupId, this.formatCodeSettings);
2432+
assert.deepEqual(commands, options.commands);
2433+
this.applyChanges(changes);
2434+
this.verifyCurrentFileContent(newFileContent);
2435+
}
2436+
24272437
/**
24282438
* Applies fixes for the errors in fileName and compares the results to
24292439
* expectedContents after all fixes have been applied.
@@ -2436,7 +2446,7 @@ Actual: ${stringify(fullActual)}`);
24362446
public verifyFileAfterCodeFix(expectedContents: string, fileName?: string) {
24372447
fileName = fileName ? fileName : this.activeFile.fileName;
24382448

2439-
this.applyCodeActions(this.getCodeFixActions(fileName));
2449+
this.applyCodeActions(this.getCodeFixes(fileName));
24402450

24412451
const actualContents: string = this.getFileContent(fileName);
24422452
if (this.removeWhitespace(actualContents) !== this.removeWhitespace(expectedContents)) {
@@ -2446,7 +2456,7 @@ Actual: ${stringify(fullActual)}`);
24462456

24472457
public verifyCodeFix(options: FourSlashInterface.VerifyCodeFixOptions) {
24482458
const fileName = this.activeFile.fileName;
2449-
const actions = this.getCodeFixActions(fileName, options.errorCode);
2459+
const actions = this.getCodeFixes(fileName, options.errorCode);
24502460
let index = options.index;
24512461
if (index === undefined) {
24522462
if (!(actions && actions.length === 1)) {
@@ -2472,8 +2482,8 @@ Actual: ${stringify(fullActual)}`);
24722482
}
24732483

24742484
private verifyNewContent(options: FourSlashInterface.NewContentOptions) {
2475-
if (options.newFileContent) {
2476-
assert(!options.newRangeContent);
2485+
if (options.newFileContent !== undefined) {
2486+
assert(options.newRangeContent === undefined);
24772487
this.verifyCurrentFileContent(options.newFileContent);
24782488
}
24792489
else {
@@ -2485,7 +2495,7 @@ Actual: ${stringify(fullActual)}`);
24852495
* Rerieves a codefix satisfying the parameters, or undefined if no such codefix is found.
24862496
* @param fileName Path to file where error should be retrieved from.
24872497
*/
2488-
private getCodeFixActions(fileName: string, errorCode?: number): ts.CodeAction[] {
2498+
private getCodeFixes(fileName: string, errorCode?: number): ts.CodeFix[] {
24892499
const diagnosticsForCodeFix = this.getDiagnostics(fileName).map(diagnostic => ({
24902500
start: diagnostic.start,
24912501
length: diagnostic.length,
@@ -2501,7 +2511,7 @@ Actual: ${stringify(fullActual)}`);
25012511
});
25022512
}
25032513

2504-
private applyCodeActions(actions: ts.CodeAction[], index?: number): void {
2514+
private applyCodeActions(actions: ReadonlyArray<ts.CodeAction>, index?: number): void {
25052515
if (index === undefined) {
25062516
if (!(actions && actions.length === 1)) {
25072517
this.raiseError(`Should find exactly one codefix, but ${actions ? actions.length : "none"} found. ${actions ? actions.map(a => `${Harness.IO.newLine()} "${a.description}"`) : ""}`);
@@ -2514,8 +2524,10 @@ Actual: ${stringify(fullActual)}`);
25142524
}
25152525
}
25162526

2517-
const changes = actions[index].changes;
2527+
this.applyChanges(actions[index].changes);
2528+
}
25182529

2530+
private applyChanges(changes: ReadonlyArray<ts.FileTextChanges>): void {
25192531
for (const change of changes) {
25202532
this.applyEdits(change.fileName, change.textChanges, /*isFormattingEdit*/ false);
25212533
}
@@ -2527,7 +2539,7 @@ Actual: ${stringify(fullActual)}`);
25272539
this.raiseError("At least one range should be specified in the testfile.");
25282540
}
25292541

2530-
const codeFixes = this.getCodeFixActions(this.activeFile.fileName, errorCode);
2542+
const codeFixes = this.getCodeFixes(this.activeFile.fileName, errorCode);
25312543

25322544
if (codeFixes.length === 0) {
25332545
if (expectedTextArray.length !== 0) {
@@ -2866,7 +2878,7 @@ Actual: ${stringify(fullActual)}`);
28662878
}
28672879

28682880
public verifyCodeFixAvailable(negative: boolean, info: FourSlashInterface.VerifyCodeFixAvailableOptions[] | undefined) {
2869-
const codeFixes = this.getCodeFixActions(this.activeFile.fileName);
2881+
const codeFixes = this.getCodeFixes(this.activeFile.fileName);
28702882

28712883
if (negative) {
28722884
if (codeFixes.length) {
@@ -3033,7 +3045,7 @@ Actual: ${stringify(fullActual)}`);
30333045
}
30343046

30353047
public printAvailableCodeFixes() {
3036-
const codeFixes = this.getCodeFixActions(this.activeFile.fileName);
3048+
const codeFixes = this.getCodeFixes(this.activeFile.fileName);
30373049
Harness.IO.log(stringify(codeFixes));
30383050
}
30393051

@@ -4143,6 +4155,10 @@ namespace FourSlashInterface {
41434155
this.state.verifyRangeAfterCodeFix(expectedText, includeWhiteSpace, errorCode, index);
41444156
}
41454157

4158+
public codeFixAll(options: VerifyCodeFixAllOptions): void {
4159+
this.state.verifyCodeFixAll(options);
4160+
}
4161+
41464162
public fileAfterApplyingRefactorAtMarker(markerName: string, expectedContent: string, refactorNameToApply: string, actionName: string, formattingOptions?: ts.FormatCodeSettings): void {
41474163
this.state.verifyFileAfterApplyingRefactorAtMarker(markerName, expectedContent, refactorNameToApply, actionName, formattingOptions);
41484164
}
@@ -4578,6 +4594,12 @@ namespace FourSlashInterface {
45784594
commands?: ts.CodeActionCommand[];
45794595
}
45804596

4597+
export interface VerifyCodeFixAllOptions {
4598+
groupId: string;
4599+
newFileContent: string;
4600+
commands: ReadonlyArray<{}>;
4601+
}
4602+
45814603
export interface VerifyRefactorOptions {
45824604
name: string;
45834605
actionName: string;

Diff for: src/harness/harnessLanguageService.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -503,9 +503,10 @@ namespace Harness.LanguageService {
503503
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan {
504504
return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine));
505505
}
506-
getCodeFixesAtPosition(): ts.CodeAction[] {
506+
getCodeFixesAtPosition(): never {
507507
throw new Error("Not supported on the shim.");
508508
}
509+
getCombinedCodeFix = ts.notImplemented;
509510
applyCodeActionCommand = ts.notImplemented;
510511
getCodeFixDiagnostics(): ts.Diagnostic[] {
511512
throw new Error("Not supported on the shim.");

Diff for: src/server/client.ts

+10-11
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ namespace ts.server {
199199
const response = this.processResponse<protocol.CompletionDetailsResponse>(request);
200200
Debug.assert(response.body.length === 1, "Unexpected length of completion details response body.");
201201

202-
const convertedCodeActions = map(response.body[0].codeActions, codeAction => this.convertCodeActions(codeAction, fileName));
202+
const convertedCodeActions = map(response.body[0].codeActions, ({ description, changes }) => ({ description, changes: this.convertChanges(changes, fileName) }));
203203
return { ...response.body[0], codeActions: convertedCodeActions };
204204
}
205205

@@ -552,15 +552,17 @@ namespace ts.server {
552552
return notImplemented();
553553
}
554554

555-
getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: number[]): CodeAction[] {
555+
getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: number[]): CodeFix[] {
556556
const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes };
557557

558558
const request = this.processRequest<protocol.CodeFixRequest>(CommandNames.GetCodeFixes, args);
559559
const response = this.processResponse<protocol.CodeFixResponse>(request);
560560

561-
return response.body.map(entry => this.convertCodeActions(entry, file));
561+
return response.body.map(({ description, changes, groupId }) => ({ description, changes: this.convertChanges(changes, file), groupId }));
562562
}
563563

564+
getCombinedCodeFix = notImplemented;
565+
564566
applyCodeActionCommand = notImplemented;
565567

566568
private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs {
@@ -637,14 +639,11 @@ namespace ts.server {
637639
});
638640
}
639641

640-
convertCodeActions(entry: protocol.CodeAction, fileName: string): CodeAction {
641-
return {
642-
description: entry.description,
643-
changes: entry.changes.map(change => ({
644-
fileName: change.fileName,
645-
textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName))
646-
}))
647-
};
642+
private convertChanges(changes: protocol.FileCodeEdits[], fileName: string): FileTextChanges[] {
643+
return changes.map(change => ({
644+
fileName: change.fileName,
645+
textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName))
646+
}));
648647
}
649648

650649
convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): ts.TextChange {

Diff for: src/server/protocol.ts

+24-2
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,12 @@ namespace ts.server.protocol {
9999
BreakpointStatement = "breakpointStatement",
100100
CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects",
101101
GetCodeFixes = "getCodeFixes",
102-
ApplyCodeActionCommand = "applyCodeActionCommand",
103102
/* @internal */
104103
GetCodeFixesFull = "getCodeFixes-full",
104+
GetCombinedCodeFix = "getCombinedCodeFix",
105+
/* @internal */
106+
GetCombinedCodeFixFull = "getCombinedCodeFix",
107+
ApplyCodeActionCommand = "applyCodeActionCommand",
105108
GetSupportedCodeFixes = "getSupportedCodeFixes",
106109

107110
GetApplicableRefactors = "getApplicableRefactors",
@@ -533,6 +536,11 @@ namespace ts.server.protocol {
533536
arguments: CodeFixRequestArgs;
534537
}
535538

539+
export interface GetCombinedCodeFixRequest extends Request {
540+
command: CommandTypes.GetCombinedCodeFix;
541+
arguments: GetCombinedCodeFixRequestArgs;
542+
}
543+
536544
export interface ApplyCodeActionCommandRequest extends Request {
537545
command: CommandTypes.ApplyCodeActionCommand;
538546
arguments: ApplyCodeActionCommandRequestArgs;
@@ -585,6 +593,10 @@ namespace ts.server.protocol {
585593
errorCodes?: number[];
586594
}
587595

596+
export interface GetCombinedCodeFixRequestArgs extends FileRequestArgs {
597+
groupId: {};
598+
}
599+
588600
export interface ApplyCodeActionCommandRequestArgs {
589601
/** May also be an array of commands. */
590602
command: {};
@@ -1568,7 +1580,7 @@ namespace ts.server.protocol {
15681580

15691581
export interface CodeFixResponse extends Response {
15701582
/** The code actions that are available */
1571-
body?: CodeAction[];
1583+
body?: CodeFix[];
15721584
}
15731585

15741586
export interface CodeAction {
@@ -1580,6 +1592,16 @@ namespace ts.server.protocol {
15801592
commands?: {}[];
15811593
}
15821594

1595+
export interface CodeActionAll {
1596+
changes: FileCodeEdits[];
1597+
commands: {}[] | undefined;
1598+
}
1599+
1600+
export interface CodeFix extends CodeAction {
1601+
/** If present, one may call 'getAllCodeFixesInGroup' with this groupId. */
1602+
groupId: {} | undefined;
1603+
}
1604+
15831605
/**
15841606
* Format and format on key response message.
15851607
*/

0 commit comments

Comments
 (0)