Skip to content

Commit 19ea189

Browse files
author
Andy
authored
Support a "getCombinedCodeFix" service (#20338)
* Support a "getCombinedCodeFix" service * Rename things * Code review * Rename things * Update API baselines * CodeActionAll -> CombinedCodeActions * Take a `scope` parameter instead of `fileName` for flexibility * Renames and bugfixes * Make API changes internal * Code review * Update comment
1 parent 4902d85 commit 19ea189

File tree

119 files changed

+2881
-1728
lines changed

Some content is hidden

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

119 files changed

+2881
-1728
lines changed

src/compiler/core.ts

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

229229
export function zipWith<T, U, V>(arrayA: ReadonlyArray<T>, arrayB: ReadonlyArray<U>, callback: (a: T, b: U, index: number) => V): V[] {
230230
const result: V[] = [];
231-
Debug.assert(arrayA.length === arrayB.length);
231+
Debug.assertEqual(arrayA.length, arrayB.length);
232232
for (let i = 0; i < arrayA.length; i++) {
233233
result.push(callback(arrayA[i], arrayB[i], i));
234234
}
235235
return result;
236236
}
237237

238+
export function zipToIterator<T, U>(arrayA: ReadonlyArray<T>, arrayB: ReadonlyArray<U>): Iterator<[T, U]> {
239+
Debug.assertEqual(arrayA.length, arrayB.length);
240+
let i = 0;
241+
return {
242+
next() {
243+
if (i === arrayA.length) {
244+
return { value: undefined as never, done: true };
245+
}
246+
i++;
247+
return { value: [arrayA[i - 1], arrayB[i - 1]], done: false };
248+
}
249+
};
250+
}
251+
238252
export function zipToMap<T>(keys: ReadonlyArray<string>, values: ReadonlyArray<T>): Map<T> {
239253
Debug.assert(keys.length === values.length);
240254
const map = createMap<T>();
@@ -1385,7 +1399,6 @@ namespace ts {
13851399
this.set(key, values = [value]);
13861400
}
13871401
return values;
1388-
13891402
}
13901403
function multiMapRemove<T>(this: MultiMap<T>, key: string, value: T) {
13911404
const values = this.get(key);

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
}

src/harness/fourslash.ts

+32-9
Original file line numberDiff line numberDiff line change
@@ -2376,7 +2376,7 @@ Actual: ${stringify(fullActual)}`);
23762376
*/
23772377
public getAndApplyCodeActions(errorCode?: number, index?: number) {
23782378
const fileName = this.activeFile.fileName;
2379-
this.applyCodeActions(this.getCodeFixActions(fileName, errorCode), index);
2379+
this.applyCodeActions(this.getCodeFixes(fileName, errorCode), index);
23802380
}
23812381

23822382
public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) {
@@ -2429,6 +2429,17 @@ Actual: ${stringify(fullActual)}`);
24292429
this.verifyRangeIs(expectedText, includeWhiteSpace);
24302430
}
24312431

2432+
public verifyCodeFixAll(options: FourSlashInterface.VerifyCodeFixAllOptions): void {
2433+
const { fixId, newFileContent } = options;
2434+
const fixIds = ts.mapDefined(this.getCodeFixes(this.activeFile.fileName), a => a.fixId);
2435+
ts.Debug.assert(ts.contains(fixIds, fixId), "No available code fix has that group id.", () => `Expected '${fixId}'. Available action ids: ${fixIds}`);
2436+
const { changes, commands } = this.languageService.getCombinedCodeFix({ type: "file", fileName: this.activeFile.fileName }, fixId, this.formatCodeSettings);
2437+
assert.deepEqual(commands, options.commands);
2438+
assert(changes.every(c => c.fileName === this.activeFile.fileName), "TODO: support testing codefixes that touch multiple files");
2439+
this.applyChanges(changes);
2440+
this.verifyCurrentFileContent(newFileContent);
2441+
}
2442+
24322443
/**
24332444
* Applies fixes for the errors in fileName and compares the results to
24342445
* expectedContents after all fixes have been applied.
@@ -2441,7 +2452,7 @@ Actual: ${stringify(fullActual)}`);
24412452
public verifyFileAfterCodeFix(expectedContents: string, fileName?: string) {
24422453
fileName = fileName ? fileName : this.activeFile.fileName;
24432454

2444-
this.applyCodeActions(this.getCodeFixActions(fileName));
2455+
this.applyCodeActions(this.getCodeFixes(fileName));
24452456

24462457
const actualContents: string = this.getFileContent(fileName);
24472458
if (this.removeWhitespace(actualContents) !== this.removeWhitespace(expectedContents)) {
@@ -2451,7 +2462,7 @@ Actual: ${stringify(fullActual)}`);
24512462

24522463
public verifyCodeFix(options: FourSlashInterface.VerifyCodeFixOptions) {
24532464
const fileName = this.activeFile.fileName;
2454-
const actions = this.getCodeFixActions(fileName, options.errorCode);
2465+
const actions = this.getCodeFixes(fileName, options.errorCode);
24552466
let index = options.index;
24562467
if (index === undefined) {
24572468
if (!(actions && actions.length === 1)) {
@@ -2490,7 +2501,7 @@ Actual: ${stringify(fullActual)}`);
24902501
* Rerieves a codefix satisfying the parameters, or undefined if no such codefix is found.
24912502
* @param fileName Path to file where error should be retrieved from.
24922503
*/
2493-
private getCodeFixActions(fileName: string, errorCode?: number): ts.CodeAction[] {
2504+
private getCodeFixes(fileName: string, errorCode?: number): ts.CodeFixAction[] {
24942505
const diagnosticsForCodeFix = this.getDiagnostics(fileName).map(diagnostic => ({
24952506
start: diagnostic.start,
24962507
length: diagnostic.length,
@@ -2506,7 +2517,7 @@ Actual: ${stringify(fullActual)}`);
25062517
});
25072518
}
25082519

2509-
private applyCodeActions(actions: ts.CodeAction[], index?: number): void {
2520+
private applyCodeActions(actions: ReadonlyArray<ts.CodeAction>, index?: number): void {
25102521
if (index === undefined) {
25112522
if (!(actions && actions.length === 1)) {
25122523
this.raiseError(`Should find exactly one codefix, but ${actions ? actions.length : "none"} found. ${actions ? actions.map(a => `${Harness.IO.newLine()} "${a.description}"`) : ""}`);
@@ -2519,8 +2530,10 @@ Actual: ${stringify(fullActual)}`);
25192530
}
25202531
}
25212532

2522-
const changes = actions[index].changes;
2533+
this.applyChanges(actions[index].changes);
2534+
}
25232535

2536+
private applyChanges(changes: ReadonlyArray<ts.FileTextChanges>): void {
25242537
for (const change of changes) {
25252538
this.applyEdits(change.fileName, change.textChanges, /*isFormattingEdit*/ false);
25262539
}
@@ -2532,7 +2545,7 @@ Actual: ${stringify(fullActual)}`);
25322545
this.raiseError("Exactly one range should be specified in the testfile.");
25332546
}
25342547

2535-
const codeFixes = this.getCodeFixActions(this.activeFile.fileName, errorCode);
2548+
const codeFixes = this.getCodeFixes(this.activeFile.fileName, errorCode);
25362549

25372550
if (codeFixes.length === 0) {
25382551
if (expectedTextArray.length !== 0) {
@@ -2871,7 +2884,7 @@ Actual: ${stringify(fullActual)}`);
28712884
}
28722885

28732886
public verifyCodeFixAvailable(negative: boolean, info: FourSlashInterface.VerifyCodeFixAvailableOptions[] | undefined) {
2874-
const codeFixes = this.getCodeFixActions(this.activeFile.fileName);
2887+
const codeFixes = this.getCodeFixes(this.activeFile.fileName);
28752888

28762889
if (negative) {
28772890
if (codeFixes.length) {
@@ -3038,7 +3051,7 @@ Actual: ${stringify(fullActual)}`);
30383051
}
30393052

30403053
public printAvailableCodeFixes() {
3041-
const codeFixes = this.getCodeFixActions(this.activeFile.fileName);
3054+
const codeFixes = this.getCodeFixes(this.activeFile.fileName);
30423055
Harness.IO.log(stringify(codeFixes));
30433056
}
30443057

@@ -4149,6 +4162,10 @@ namespace FourSlashInterface {
41494162
this.state.verifyRangeAfterCodeFix(expectedText, includeWhiteSpace, errorCode, index);
41504163
}
41514164

4165+
public codeFixAll(options: VerifyCodeFixAllOptions): void {
4166+
this.state.verifyCodeFixAll(options);
4167+
}
4168+
41524169
public fileAfterApplyingRefactorAtMarker(markerName: string, expectedContent: string, refactorNameToApply: string, actionName: string, formattingOptions?: ts.FormatCodeSettings): void {
41534170
this.state.verifyFileAfterApplyingRefactorAtMarker(markerName, expectedContent, refactorNameToApply, actionName, formattingOptions);
41544171
}
@@ -4584,6 +4601,12 @@ namespace FourSlashInterface {
45844601
commands?: ts.CodeActionCommand[];
45854602
}
45864603

4604+
export interface VerifyCodeFixAllOptions {
4605+
fixId: string;
4606+
newFileContent: string;
4607+
commands: ReadonlyArray<{}>;
4608+
}
4609+
45874610
export interface VerifyRefactorOptions {
45884611
name: string;
45894612
actionName: string;

src/harness/harness.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ namespace assert {
3636
export function isFalse(expr: boolean, msg = "Expected value to be false."): void {
3737
assert(!expr, msg);
3838
}
39-
export function equal<T>(a: T, b: T, msg = "Expected values to be equal."): void {
40-
assert(a === b, msg);
39+
export function equal<T>(a: T, b: T, msg?: string): void {
40+
assert(a === b, msg || (() => `Expected to be equal:\nExpected:\n${JSON.stringify(a)}\nActual:\n${JSON.stringify(b)}`));
4141
}
4242
export function notEqual<T>(a: T, b: T, msg = "Expected values to not be equal."): void {
4343
assert(a !== b, msg);

src/harness/harnessLanguageService.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -521,9 +521,10 @@ namespace Harness.LanguageService {
521521
getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): ts.TextSpan {
522522
return unwrapJSONCallResult(this.shim.getSpanOfEnclosingComment(fileName, position, onlyMultiLine));
523523
}
524-
getCodeFixesAtPosition(): ts.CodeAction[] {
524+
getCodeFixesAtPosition(): never {
525525
throw new Error("Not supported on the shim.");
526526
}
527+
getCombinedCodeFix = ts.notImplemented;
527528
applyCodeActionCommand = ts.notImplemented;
528529
getCodeFixDiagnostics(): ts.Diagnostic[] {
529530
throw new Error("Not supported on the shim.");

src/server/client.ts

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

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

@@ -553,15 +553,18 @@ namespace ts.server {
553553
return notImplemented();
554554
}
555555

556-
getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: number[]): CodeAction[] {
556+
getCodeFixesAtPosition(file: string, start: number, end: number, errorCodes: ReadonlyArray<number>): ReadonlyArray<CodeFixAction> {
557557
const args: protocol.CodeFixRequestArgs = { ...this.createFileRangeRequestArgs(file, start, end), errorCodes };
558558

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

562-
return response.body.map(entry => this.convertCodeActions(entry, file));
562+
// TODO: GH#20538 shouldn't need cast
563+
return (response.body as ReadonlyArray<protocol.CodeFixAction>).map(({ description, changes, fixId }) => ({ description, changes: this.convertChanges(changes, file), fixId }));
563564
}
564565

566+
getCombinedCodeFix = notImplemented;
567+
565568
applyCodeActionCommand = notImplemented;
566569

567570
private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs {
@@ -638,14 +641,11 @@ namespace ts.server {
638641
});
639642
}
640643

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

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

src/server/protocol.ts

+52-3
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,14 @@ namespace ts.server.protocol {
100100
BreakpointStatement = "breakpointStatement",
101101
CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects",
102102
GetCodeFixes = "getCodeFixes",
103-
ApplyCodeActionCommand = "applyCodeActionCommand",
104103
/* @internal */
105104
GetCodeFixesFull = "getCodeFixes-full",
105+
// TODO: GH#20538
106+
/* @internal */
107+
GetCombinedCodeFix = "getCombinedCodeFix",
108+
/* @internal */
109+
GetCombinedCodeFixFull = "getCombinedCodeFix-full",
110+
ApplyCodeActionCommand = "applyCodeActionCommand",
106111
GetSupportedCodeFixes = "getSupportedCodeFixes",
107112

108113
GetApplicableRefactors = "getApplicableRefactors",
@@ -552,6 +557,19 @@ namespace ts.server.protocol {
552557
arguments: CodeFixRequestArgs;
553558
}
554559

560+
// TODO: GH#20538
561+
/* @internal */
562+
export interface GetCombinedCodeFixRequest extends Request {
563+
command: CommandTypes.GetCombinedCodeFix;
564+
arguments: GetCombinedCodeFixRequestArgs;
565+
}
566+
567+
// TODO: GH#20538
568+
/* @internal */
569+
export interface GetCombinedCodeFixResponse extends Response {
570+
body: CombinedCodeActions;
571+
}
572+
555573
export interface ApplyCodeActionCommandRequest extends Request {
556574
command: CommandTypes.ApplyCodeActionCommand;
557575
arguments: ApplyCodeActionCommandRequestArgs;
@@ -601,7 +619,21 @@ namespace ts.server.protocol {
601619
/**
602620
* Errorcodes we want to get the fixes for.
603621
*/
604-
errorCodes?: number[];
622+
errorCodes?: ReadonlyArray<number>;
623+
}
624+
625+
// TODO: GH#20538
626+
/* @internal */
627+
export interface GetCombinedCodeFixRequestArgs {
628+
scope: GetCombinedCodeFixScope;
629+
fixId: {};
630+
}
631+
632+
// TODO: GH#20538
633+
/* @internal */
634+
export interface GetCombinedCodeFixScope {
635+
type: "file";
636+
args: FileRequestArgs;
605637
}
606638

607639
export interface ApplyCodeActionCommandRequestArgs {
@@ -1587,7 +1619,7 @@ namespace ts.server.protocol {
15871619

15881620
export interface CodeFixResponse extends Response {
15891621
/** The code actions that are available */
1590-
body?: CodeAction[];
1622+
body?: CodeAction[]; // TODO: GH#20538 CodeFixAction[]
15911623
}
15921624

15931625
export interface CodeAction {
@@ -1599,6 +1631,23 @@ namespace ts.server.protocol {
15991631
commands?: {}[];
16001632
}
16011633

1634+
// TODO: GH#20538
1635+
/* @internal */
1636+
export interface CombinedCodeActions {
1637+
changes: ReadonlyArray<FileCodeEdits>;
1638+
commands?: ReadonlyArray<{}>;
1639+
}
1640+
1641+
// TODO: GH#20538
1642+
/* @internal */
1643+
export interface CodeFixAction extends CodeAction {
1644+
/**
1645+
* If present, one may call 'getCombinedCodeFix' with this fixId.
1646+
* This may be omitted to indicate that the code fix can't be applied in a group.
1647+
*/
1648+
fixId?: {};
1649+
}
1650+
16021651
/**
16031652
* Format and format on key response message.
16041653
*/

0 commit comments

Comments
 (0)