Skip to content

Commit 5e6ec06

Browse files
sandersnmjbvz
andauthored
Split TS' AI-backed code actions into separate entries (microsoft#201140)
* Split TS' AI-backed code actions into separate entries Lets the user decide whether to add AI to their code action, which shows intent, which is good for us to learn whether people actually want this. Related: this should be unflagged for insiders. To do this, do I just delete the flags? * Stop appending a duplicate message in missingFunctionDeclaration * Fix: quickfix was still showing Copilot-only It's a workaround--I'm not sure of the right way to do this. * Update to use `isAI` * Put AI code actions after others. * Add isAI to rest of code actions * Remove flags for TS AI code actions * Check for copilot-chat instead of copilot It's possible to have copilot installed without copilot-chat. * Fix file casing --------- Co-authored-by: Matt Bierner <[email protected]>
1 parent 3246d63 commit 5e6ec06

File tree

5 files changed

+84
-119
lines changed

5 files changed

+84
-119
lines changed

extensions/typescript-language-features/package.json

Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"enabledApiProposals": [
1111
"workspaceTrust",
1212
"multiDocumentHighlightProvider",
13-
"mappedEditsProvider"
13+
"mappedEditsProvider",
14+
"codeActionAI"
1415
],
1516
"capabilities": {
1617
"virtualWorkspaces": {
@@ -147,59 +148,6 @@
147148
"title": "%configuration.typescript%",
148149
"order": 20,
149150
"properties": {
150-
"typescript.experimental.aiCodeActions": {
151-
"type": "object",
152-
"default": {},
153-
"description": "%typescript.experimental.aiCodeActions%",
154-
"scope": "resource",
155-
"properties": {
156-
"classIncorrectlyImplementsInterface": {
157-
"type": "boolean",
158-
"default": false,
159-
"description": "%typescript.experimental.aiCodeActions.classIncorrectlyImplementsInterface%"
160-
},
161-
"classDoesntImplementInheritedAbstractMember": {
162-
"type": "boolean",
163-
"default": false,
164-
"description": "%typescript.experimental.aiCodeActions.classDoesntImplementInheritedAbstractMember%"
165-
},
166-
"missingFunctionDeclaration": {
167-
"type": "boolean",
168-
"default": false,
169-
"description": "%typescript.experimental.aiCodeActions.missingFunctionDeclaration%"
170-
},
171-
"inferAndAddTypes": {
172-
"type": "boolean",
173-
"default": false,
174-
"description": "%typescript.experimental.aiCodeActions.inferAndAddTypes%"
175-
},
176-
"addNameToNamelessParameter": {
177-
"type": "boolean",
178-
"default": false,
179-
"description": "%typescript.experimental.aiCodeActions.addNameToNamelessParameter%"
180-
},
181-
"extractConstant": {
182-
"type": "boolean",
183-
"default": false,
184-
"description": "%typescript.experimental.aiCodeActions.extractConstant%"
185-
},
186-
"extractFunction": {
187-
"type": "boolean",
188-
"default": false,
189-
"description": "%typescript.experimental.aiCodeActions.extractFunction%"
190-
},
191-
"extractType": {
192-
"type": "boolean",
193-
"default": false,
194-
"description": "%typescript.experimental.aiCodeActions.extractType%"
195-
},
196-
"extractInterface": {
197-
"type": "boolean",
198-
"default": false,
199-
"description": "%typescript.experimental.aiCodeActions.extractInterface%"
200-
}
201-
}
202-
},
203151
"typescript.tsdk": {
204152
"type": "string",
205153
"markdownDescription": "%typescript.tsdk.desc%",

extensions/typescript-language-features/package.nls.json

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,6 @@
88
"configuration.suggest.completeFunctionCalls": "Complete functions with their parameter signature.",
99
"configuration.suggest.includeAutomaticOptionalChainCompletions": "Enable/disable showing completions on potentially undefined values that insert an optional chain call. Requires strict null checks to be enabled.",
1010
"configuration.suggest.includeCompletionsForImportStatements": "Enable/disable auto-import-style completions on partially-typed import statements.",
11-
"typescript.experimental.aiCodeActions": "Enable/disable AI-assisted code actions. Requires an extension providing AI chat functionality.",
12-
"typescript.experimental.aiCodeActions.classIncorrectlyImplementsInterface": "Enable/disable AI assistance for Class Incorrectly Implements Interface quickfix. Requires an extension providing AI chat functionality.",
13-
"typescript.experimental.aiCodeActions.classDoesntImplementInheritedAbstractMember": "Enable/disable AI assistance for Class Doesn't Implement Inherited Abstract Member quickfix. Requires an extension providing AI chat functionality.",
14-
"typescript.experimental.aiCodeActions.missingFunctionDeclaration": "Enable/disable AI assistance for Missing Function Declaration quickfix. Requires an extension providing AI chat functionality.",
15-
"typescript.experimental.aiCodeActions.inferAndAddTypes": "Enable/disable AI assistance for Infer and Add Types refactor. Requires an extension providing AI chat functionality.",
16-
"typescript.experimental.aiCodeActions.addNameToNamelessParameter": "Enable/disable AI assistance for Add Name to Nameless Parameter quickfix. Requires an extension providing AI chat functionality.",
17-
"typescript.experimental.aiCodeActions.extractConstant": "Enable/disable AI assistance for Extract Constant refactor. Requires an extension providing AI chat functionality.",
18-
"typescript.experimental.aiCodeActions.extractFunction": "Enable/disable AI assistance for Extract Function refactor. Requires an extension providing AI chat functionality.",
19-
"typescript.experimental.aiCodeActions.extractType": "Enable/disable AI assistance for Extract Type refactor. Requires an extension providing AI chat functionality.",
20-
"typescript.experimental.aiCodeActions.extractInterface": "Enable/disable AI assistance for Extract Interface refactor. Requires an extension providing AI chat functionality.",
2111
"typescript.tsdk.desc": "Specifies the folder path to the tsserver and `lib*.d.ts` files under a TypeScript install to use for IntelliSense, for example: `./node_modules/typescript/lib`.\n\n- When specified as a user setting, the TypeScript version from `typescript.tsdk` automatically replaces the built-in TypeScript version.\n- When specified as a workspace setting, `typescript.tsdk` allows you to switch to use that workspace version of TypeScript for IntelliSense with the `TypeScript: Select TypeScript version` command.\n\nSee the [TypeScript documentation](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-newer-typescript-versions) for more detail about managing TypeScript versions.",
2212
"typescript.disableAutomaticTypeAcquisition": "Disables [automatic type acquisition](https://code.visualstudio.com/docs/nodejs/working-with-javascript#_typings-and-automatic-type-acquisition). Automatic type acquisition fetches `@types` packages from npm to improve IntelliSense for external libraries.",
2313
"typescript.enablePromptUseWorkspaceTsdk": "Enables prompting of users to use the TypeScript version configured in the workspace for Intellisense.",

extensions/typescript-language-features/src/languageFeatures/quickFix.ts

Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,19 @@ class VsCodeFixAllCodeAction extends VsCodeCodeAction {
147147
class CodeActionSet {
148148
private readonly _actions = new Set<VsCodeCodeAction>();
149149
private readonly _fixAllActions = new Map<{}, VsCodeCodeAction>();
150+
private readonly _aiActions = new Set<VsCodeCodeAction>();
150151

151-
public get values(): Iterable<VsCodeCodeAction> {
152-
return this._actions;
152+
public *values(): Iterable<VsCodeCodeAction> {
153+
yield* this._actions;
154+
yield* this._aiActions;
153155
}
154156

155157
public addAction(action: VsCodeCodeAction) {
158+
if (action.isAI) {
159+
// there are no separate fixAllActions for AI, and no duplicates, so return immediately
160+
this._aiActions.add(action);
161+
return;
162+
}
156163
for (const existing of this._actions) {
157164
if (action.tsAction.fixName === existing.tsAction.fixName && equals(action.edit, existing.edit)) {
158165
this._actions.delete(existing);
@@ -261,7 +268,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
261268
}
262269
}
263270

264-
const allActions = Array.from(results.values);
271+
const allActions = Array.from(results.values());
265272
for (const action of allActions) {
266273
action.isPreferred = isPreferredFix(action, allActions);
267274
}
@@ -321,29 +328,41 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
321328
action: Proto.CodeFixAction
322329
): VsCodeCodeAction[] {
323330
const actions: VsCodeCodeAction[] = [];
324-
let message: string | undefined;
325-
let expand: Expand | undefined;
326-
let title = action.description;
327-
if (vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions')) {
328-
if (action.fixName === fixNames.classIncorrectlyImplementsInterface && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.classIncorrectlyImplementsInterface')) {
331+
const codeAction = new VsCodeCodeAction(action, action.description, vscode.CodeActionKind.QuickFix);
332+
codeAction.edit = getEditForCodeAction(this.client, action);
333+
codeAction.diagnostics = [diagnostic];
334+
codeAction.command = {
335+
command: ApplyCodeActionCommand.ID,
336+
arguments: [{ action, diagnostic, document } satisfies ApplyCodeActionCommand_args],
337+
title: ''
338+
};
339+
actions.push(codeAction);
340+
341+
const copilot = vscode.extensions.getExtension('github.copilot-chat');
342+
if (copilot?.isActive) {
343+
let message: string | undefined;
344+
let expand: Expand | undefined;
345+
let title = action.description;
346+
if (action.fixName === fixNames.classIncorrectlyImplementsInterface) {
329347
title += ' with Copilot';
330348
message = `Implement the stubbed-out class members for ${document.getText(diagnostic.range)} with a useful implementation.`;
331349
expand = { kind: 'code-action', action };
332350
}
333-
else if (action.fixName === fixNames.fixClassDoesntImplementInheritedAbstractMember && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.classDoesntImplementInheritedAbstractMember')) {
351+
else if (action.fixName === fixNames.fixClassDoesntImplementInheritedAbstractMember) {
334352
title += ' with Copilot';
335353
message = `Implement the stubbed-out class members for ${document.getText(diagnostic.range)} with a useful implementation.`;
336354
expand = { kind: 'code-action', action };
337355
}
338-
else if (action.fixName === fixNames.fixMissingFunctionDeclaration && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.missingFunctionDeclaration')) {
339-
title += `Implement missing function declaration '${document.getText(diagnostic.range)}' using Copilot`;
356+
else if (action.fixName === fixNames.fixMissingFunctionDeclaration) {
357+
title = `Implement missing function declaration '${document.getText(diagnostic.range)}' using Copilot`;
340358
message = `Provide a reasonable implementation of the function ${document.getText(diagnostic.range)} given its type and the context it's called in.`;
341359
expand = { kind: 'code-action', action };
342360
}
343-
else if (action.fixName === fixNames.inferFromUsage && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.inferAndAddTypes')) {
361+
else if (action.fixName === fixNames.inferFromUsage) {
344362
const inferFromBody = new VsCodeCodeAction(action, 'Infer types using Copilot', vscode.CodeActionKind.QuickFix);
345363
inferFromBody.edit = new vscode.WorkspaceEdit();
346364
inferFromBody.diagnostics = [diagnostic];
365+
inferFromBody.isAI = true;
347366
inferFromBody.command = {
348367
command: EditorChatFollowUp.ID,
349368
arguments: [{
@@ -356,7 +375,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
356375
};
357376
actions.push(inferFromBody);
358377
}
359-
else if (action.fixName === fixNames.addNameToNamelessParameter && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.addNameToNamelessParameter')) {
378+
else if (action.fixName === fixNames.addNameToNamelessParameter) {
360379
const newText = action.changes.map(change => change.textChanges.map(textChange => textChange.newText).join('')).join('');
361380
title = 'Add meaningful parameter name with Copilot';
362381
message = `Rename the parameter ${newText} with a more meaningful name.`;
@@ -365,32 +384,33 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
365384
pos: diagnostic.range.start
366385
};
367386
}
368-
}
369-
const codeAction = new VsCodeCodeAction(action, title, vscode.CodeActionKind.QuickFix);
370-
codeAction.edit = getEditForCodeAction(this.client, action);
371-
codeAction.diagnostics = [diagnostic];
372-
codeAction.command = {
373-
command: ApplyCodeActionCommand.ID,
374-
arguments: [{ action: action, diagnostic, document } satisfies ApplyCodeActionCommand_args],
375-
title: ''
376-
};
377-
if (expand && message !== undefined) {
378-
codeAction.command = {
379-
command: CompositeCommand.ID,
380-
title: '',
381-
arguments: [codeAction.command, {
382-
command: EditorChatFollowUp.ID,
387+
if (expand && message !== undefined) {
388+
const aiCodeAction = new VsCodeCodeAction(action, title, vscode.CodeActionKind.QuickFix);
389+
aiCodeAction.edit = getEditForCodeAction(this.client, action);
390+
aiCodeAction.edit?.insert(document.uri, diagnostic.range.start, '');
391+
aiCodeAction.diagnostics = [diagnostic];
392+
aiCodeAction.isAI = true;
393+
aiCodeAction.command = {
394+
command: CompositeCommand.ID,
383395
title: '',
384396
arguments: [{
385-
message,
386-
expand,
387-
document,
388-
action: { type: 'quickfix', quickfix: action }
389-
} satisfies EditorChatFollowUp_Args],
390-
}],
391-
};
397+
command: ApplyCodeActionCommand.ID,
398+
arguments: [{ action, diagnostic, document } satisfies ApplyCodeActionCommand_args],
399+
title: ''
400+
}, {
401+
command: EditorChatFollowUp.ID,
402+
title: '',
403+
arguments: [{
404+
message,
405+
expand,
406+
document,
407+
action: { type: 'quickfix', quickfix: action }
408+
} satisfies EditorChatFollowUp_Args],
409+
}],
410+
};
411+
actions.push(aiCodeAction);
412+
}
392413
}
393-
actions.push(codeAction);
394414
return actions;
395415
}
396416

extensions/typescript-language-features/src/languageFeatures/refactor.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,9 @@ class InlinedCodeAction extends vscode.CodeAction {
352352
const title = copilotRename ? action.description + ' and suggest a name with Copilot.' : action.description;
353353
super(title, InlinedCodeAction.getKind(action));
354354

355+
if (copilotRename) {
356+
this.isAI = true;
357+
}
355358
if (action.notApplicableReason) {
356359
this.disabled = { reason: action.notApplicableReason };
357360
}
@@ -613,36 +616,39 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeActi
613616
yield new SelectCodeAction(refactor, document, rangeOrSelection);
614617
} else {
615618
for (const action of refactor.actions) {
616-
yield this.refactorActionToCodeAction(document, refactor, action, rangeOrSelection, refactor.actions);
619+
for (const codeAction of this.refactorActionToCodeActions(document, refactor, action, rangeOrSelection, refactor.actions)) {
620+
yield codeAction;
621+
}
617622
}
618623
}
619624
}
620625
}
621626

622-
private refactorActionToCodeAction(
627+
private refactorActionToCodeActions(
623628
document: vscode.TextDocument,
624629
refactor: Proto.ApplicableRefactorInfo,
625630
action: Proto.RefactorActionInfo,
626631
rangeOrSelection: vscode.Range | vscode.Selection,
627632
allActions: readonly Proto.RefactorActionInfo[],
628-
): TsCodeAction {
629-
let codeAction: TsCodeAction;
633+
): TsCodeAction[] {
634+
const codeActions: TsCodeAction[] = [];
630635
if (action.name === 'Move to file') {
631-
codeAction = new MoveToFileCodeAction(document, action, rangeOrSelection);
636+
codeActions.push(new MoveToFileCodeAction(document, action, rangeOrSelection));
632637
} else {
633-
let copilotRename: ((info: Proto.RefactorEditInfo) => vscode.Command) | undefined;
634-
if (vscode.workspace.getConfiguration('typescript', null).get('experimental.aiCodeActions')) {
635-
if (Extract_Constant.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractConstant')
636-
|| Extract_Function.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractFunction')
637-
|| Extract_Type.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractType')
638-
|| Extract_Interface.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractInterface')
638+
codeActions.push(new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection, undefined));
639+
const copilot = vscode.extensions.getExtension('github.copilot-chat');
640+
if (copilot?.isActive) {
641+
if (Extract_Constant.matches(action)
642+
|| Extract_Function.matches(action)
643+
|| Extract_Type.matches(action)
644+
|| Extract_Interface.matches(action)
639645
) {
640646
const newName = Extract_Constant.matches(action) ? 'newLocal'
641647
: Extract_Function.matches(action) ? 'newFunction'
642648
: Extract_Type.matches(action) ? 'NewType'
643649
: Extract_Interface.matches(action) ? 'NewInterface'
644650
: '';
645-
copilotRename = info => ({
651+
const copilotRename: ((info: Proto.RefactorEditInfo) => vscode.Command) = info => ({
646652
title: '',
647653
command: EditorChatFollowUp.ID,
648654
arguments: [{
@@ -658,14 +664,14 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeActi
658664
document,
659665
} satisfies EditorChatFollowUp_Args]
660666
});
667+
codeActions.push(new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection, copilotRename));
661668
}
662-
663669
}
664-
codeAction = new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection, copilotRename);
665670
}
666-
667-
codeAction.isPreferred = TypeScriptRefactorProvider.isPreferred(action, allActions);
668-
return codeAction;
671+
for (const codeAction of codeActions) {
672+
codeAction.isPreferred = TypeScriptRefactorProvider.isPreferred(action, allActions);
673+
}
674+
return codeActions;
669675
}
670676

671677
private shouldTrigger(context: vscode.CodeActionContext, rangeOrSelection: vscode.Range | vscode.Selection) {

extensions/typescript-language-features/tsconfig.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
"include": [
1212
"src/**/*",
1313
"../../src/vscode-dts/vscode.d.ts",
14-
"../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts",
15-
"../../src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts",
14+
"../../src/vscode-dts/vscode.proposed.codeActionAI.d.ts",
1615
"../../src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts",
16+
"../../src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts",
17+
"../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts",
1718
]
1819
}

0 commit comments

Comments
 (0)