Split TS' AI-backed code actions into separate entries (#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 <matb@microsoft.com>
This commit is contained in:
Nathan Shively-Sanders
2024-01-31 14:46:39 -08:00
committed by GitHub
parent 3246d63177
commit 5e6ec068b2
5 changed files with 84 additions and 119 deletions

View File

@@ -10,7 +10,8 @@
"enabledApiProposals": [
"workspaceTrust",
"multiDocumentHighlightProvider",
"mappedEditsProvider"
"mappedEditsProvider",
"codeActionAI"
],
"capabilities": {
"virtualWorkspaces": {
@@ -147,59 +148,6 @@
"title": "%configuration.typescript%",
"order": 20,
"properties": {
"typescript.experimental.aiCodeActions": {
"type": "object",
"default": {},
"description": "%typescript.experimental.aiCodeActions%",
"scope": "resource",
"properties": {
"classIncorrectlyImplementsInterface": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.classIncorrectlyImplementsInterface%"
},
"classDoesntImplementInheritedAbstractMember": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.classDoesntImplementInheritedAbstractMember%"
},
"missingFunctionDeclaration": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.missingFunctionDeclaration%"
},
"inferAndAddTypes": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.inferAndAddTypes%"
},
"addNameToNamelessParameter": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.addNameToNamelessParameter%"
},
"extractConstant": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.extractConstant%"
},
"extractFunction": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.extractFunction%"
},
"extractType": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.extractType%"
},
"extractInterface": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiCodeActions.extractInterface%"
}
}
},
"typescript.tsdk": {
"type": "string",
"markdownDescription": "%typescript.tsdk.desc%",

View File

@@ -8,16 +8,6 @@
"configuration.suggest.completeFunctionCalls": "Complete functions with their parameter signature.",
"configuration.suggest.includeAutomaticOptionalChainCompletions": "Enable/disable showing completions on potentially undefined values that insert an optional chain call. Requires strict null checks to be enabled.",
"configuration.suggest.includeCompletionsForImportStatements": "Enable/disable auto-import-style completions on partially-typed import statements.",
"typescript.experimental.aiCodeActions": "Enable/disable AI-assisted code actions. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.classIncorrectlyImplementsInterface": "Enable/disable AI assistance for Class Incorrectly Implements Interface quickfix. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.classDoesntImplementInheritedAbstractMember": "Enable/disable AI assistance for Class Doesn't Implement Inherited Abstract Member quickfix. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.missingFunctionDeclaration": "Enable/disable AI assistance for Missing Function Declaration quickfix. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.inferAndAddTypes": "Enable/disable AI assistance for Infer and Add Types refactor. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.addNameToNamelessParameter": "Enable/disable AI assistance for Add Name to Nameless Parameter quickfix. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.extractConstant": "Enable/disable AI assistance for Extract Constant refactor. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.extractFunction": "Enable/disable AI assistance for Extract Function refactor. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.extractType": "Enable/disable AI assistance for Extract Type refactor. Requires an extension providing AI chat functionality.",
"typescript.experimental.aiCodeActions.extractInterface": "Enable/disable AI assistance for Extract Interface refactor. Requires an extension providing AI chat functionality.",
"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.",
"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.",
"typescript.enablePromptUseWorkspaceTsdk": "Enables prompting of users to use the TypeScript version configured in the workspace for Intellisense.",

View File

@@ -147,12 +147,19 @@ class VsCodeFixAllCodeAction extends VsCodeCodeAction {
class CodeActionSet {
private readonly _actions = new Set<VsCodeCodeAction>();
private readonly _fixAllActions = new Map<{}, VsCodeCodeAction>();
private readonly _aiActions = new Set<VsCodeCodeAction>();
public get values(): Iterable<VsCodeCodeAction> {
return this._actions;
public *values(): Iterable<VsCodeCodeAction> {
yield* this._actions;
yield* this._aiActions;
}
public addAction(action: VsCodeCodeAction) {
if (action.isAI) {
// there are no separate fixAllActions for AI, and no duplicates, so return immediately
this._aiActions.add(action);
return;
}
for (const existing of this._actions) {
if (action.tsAction.fixName === existing.tsAction.fixName && equals(action.edit, existing.edit)) {
this._actions.delete(existing);
@@ -261,7 +268,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
}
}
const allActions = Array.from(results.values);
const allActions = Array.from(results.values());
for (const action of allActions) {
action.isPreferred = isPreferredFix(action, allActions);
}
@@ -321,29 +328,41 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
action: Proto.CodeFixAction
): VsCodeCodeAction[] {
const actions: VsCodeCodeAction[] = [];
let message: string | undefined;
let expand: Expand | undefined;
let title = action.description;
if (vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions')) {
if (action.fixName === fixNames.classIncorrectlyImplementsInterface && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.classIncorrectlyImplementsInterface')) {
const codeAction = new VsCodeCodeAction(action, action.description, vscode.CodeActionKind.QuickFix);
codeAction.edit = getEditForCodeAction(this.client, action);
codeAction.diagnostics = [diagnostic];
codeAction.command = {
command: ApplyCodeActionCommand.ID,
arguments: [{ action, diagnostic, document } satisfies ApplyCodeActionCommand_args],
title: ''
};
actions.push(codeAction);
const copilot = vscode.extensions.getExtension('github.copilot-chat');
if (copilot?.isActive) {
let message: string | undefined;
let expand: Expand | undefined;
let title = action.description;
if (action.fixName === fixNames.classIncorrectlyImplementsInterface) {
title += ' with Copilot';
message = `Implement the stubbed-out class members for ${document.getText(diagnostic.range)} with a useful implementation.`;
expand = { kind: 'code-action', action };
}
else if (action.fixName === fixNames.fixClassDoesntImplementInheritedAbstractMember && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.classDoesntImplementInheritedAbstractMember')) {
else if (action.fixName === fixNames.fixClassDoesntImplementInheritedAbstractMember) {
title += ' with Copilot';
message = `Implement the stubbed-out class members for ${document.getText(diagnostic.range)} with a useful implementation.`;
expand = { kind: 'code-action', action };
}
else if (action.fixName === fixNames.fixMissingFunctionDeclaration && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.missingFunctionDeclaration')) {
title += `Implement missing function declaration '${document.getText(diagnostic.range)}' using Copilot`;
else if (action.fixName === fixNames.fixMissingFunctionDeclaration) {
title = `Implement missing function declaration '${document.getText(diagnostic.range)}' using Copilot`;
message = `Provide a reasonable implementation of the function ${document.getText(diagnostic.range)} given its type and the context it's called in.`;
expand = { kind: 'code-action', action };
}
else if (action.fixName === fixNames.inferFromUsage && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.inferAndAddTypes')) {
else if (action.fixName === fixNames.inferFromUsage) {
const inferFromBody = new VsCodeCodeAction(action, 'Infer types using Copilot', vscode.CodeActionKind.QuickFix);
inferFromBody.edit = new vscode.WorkspaceEdit();
inferFromBody.diagnostics = [diagnostic];
inferFromBody.isAI = true;
inferFromBody.command = {
command: EditorChatFollowUp.ID,
arguments: [{
@@ -356,7 +375,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
};
actions.push(inferFromBody);
}
else if (action.fixName === fixNames.addNameToNamelessParameter && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.addNameToNamelessParameter')) {
else if (action.fixName === fixNames.addNameToNamelessParameter) {
const newText = action.changes.map(change => change.textChanges.map(textChange => textChange.newText).join('')).join('');
title = 'Add meaningful parameter name with Copilot';
message = `Rename the parameter ${newText} with a more meaningful name.`;
@@ -365,32 +384,33 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
pos: diagnostic.range.start
};
}
}
const codeAction = new VsCodeCodeAction(action, title, vscode.CodeActionKind.QuickFix);
codeAction.edit = getEditForCodeAction(this.client, action);
codeAction.diagnostics = [diagnostic];
codeAction.command = {
command: ApplyCodeActionCommand.ID,
arguments: [{ action: action, diagnostic, document } satisfies ApplyCodeActionCommand_args],
title: ''
};
if (expand && message !== undefined) {
codeAction.command = {
command: CompositeCommand.ID,
title: '',
arguments: [codeAction.command, {
command: EditorChatFollowUp.ID,
if (expand && message !== undefined) {
const aiCodeAction = new VsCodeCodeAction(action, title, vscode.CodeActionKind.QuickFix);
aiCodeAction.edit = getEditForCodeAction(this.client, action);
aiCodeAction.edit?.insert(document.uri, diagnostic.range.start, '');
aiCodeAction.diagnostics = [diagnostic];
aiCodeAction.isAI = true;
aiCodeAction.command = {
command: CompositeCommand.ID,
title: '',
arguments: [{
message,
expand,
document,
action: { type: 'quickfix', quickfix: action }
} satisfies EditorChatFollowUp_Args],
}],
};
command: ApplyCodeActionCommand.ID,
arguments: [{ action, diagnostic, document } satisfies ApplyCodeActionCommand_args],
title: ''
}, {
command: EditorChatFollowUp.ID,
title: '',
arguments: [{
message,
expand,
document,
action: { type: 'quickfix', quickfix: action }
} satisfies EditorChatFollowUp_Args],
}],
};
actions.push(aiCodeAction);
}
}
actions.push(codeAction);
return actions;
}

View File

@@ -352,6 +352,9 @@ class InlinedCodeAction extends vscode.CodeAction {
const title = copilotRename ? action.description + ' and suggest a name with Copilot.' : action.description;
super(title, InlinedCodeAction.getKind(action));
if (copilotRename) {
this.isAI = true;
}
if (action.notApplicableReason) {
this.disabled = { reason: action.notApplicableReason };
}
@@ -613,36 +616,39 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeActi
yield new SelectCodeAction(refactor, document, rangeOrSelection);
} else {
for (const action of refactor.actions) {
yield this.refactorActionToCodeAction(document, refactor, action, rangeOrSelection, refactor.actions);
for (const codeAction of this.refactorActionToCodeActions(document, refactor, action, rangeOrSelection, refactor.actions)) {
yield codeAction;
}
}
}
}
}
private refactorActionToCodeAction(
private refactorActionToCodeActions(
document: vscode.TextDocument,
refactor: Proto.ApplicableRefactorInfo,
action: Proto.RefactorActionInfo,
rangeOrSelection: vscode.Range | vscode.Selection,
allActions: readonly Proto.RefactorActionInfo[],
): TsCodeAction {
let codeAction: TsCodeAction;
): TsCodeAction[] {
const codeActions: TsCodeAction[] = [];
if (action.name === 'Move to file') {
codeAction = new MoveToFileCodeAction(document, action, rangeOrSelection);
codeActions.push(new MoveToFileCodeAction(document, action, rangeOrSelection));
} else {
let copilotRename: ((info: Proto.RefactorEditInfo) => vscode.Command) | undefined;
if (vscode.workspace.getConfiguration('typescript', null).get('experimental.aiCodeActions')) {
if (Extract_Constant.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractConstant')
|| Extract_Function.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractFunction')
|| Extract_Type.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractType')
|| Extract_Interface.matches(action) && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.extractInterface')
codeActions.push(new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection, undefined));
const copilot = vscode.extensions.getExtension('github.copilot-chat');
if (copilot?.isActive) {
if (Extract_Constant.matches(action)
|| Extract_Function.matches(action)
|| Extract_Type.matches(action)
|| Extract_Interface.matches(action)
) {
const newName = Extract_Constant.matches(action) ? 'newLocal'
: Extract_Function.matches(action) ? 'newFunction'
: Extract_Type.matches(action) ? 'NewType'
: Extract_Interface.matches(action) ? 'NewInterface'
: '';
copilotRename = info => ({
const copilotRename: ((info: Proto.RefactorEditInfo) => vscode.Command) = info => ({
title: '',
command: EditorChatFollowUp.ID,
arguments: [{
@@ -658,14 +664,14 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeActi
document,
} satisfies EditorChatFollowUp_Args]
});
codeActions.push(new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection, copilotRename));
}
}
codeAction = new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection, copilotRename);
}
codeAction.isPreferred = TypeScriptRefactorProvider.isPreferred(action, allActions);
return codeAction;
for (const codeAction of codeActions) {
codeAction.isPreferred = TypeScriptRefactorProvider.isPreferred(action, allActions);
}
return codeActions;
}
private shouldTrigger(context: vscode.CodeActionContext, rangeOrSelection: vscode.Range | vscode.Selection) {

View File

@@ -11,8 +11,9 @@
"include": [
"src/**/*",
"../../src/vscode-dts/vscode.d.ts",
"../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts",
"../../src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts",
"../../src/vscode-dts/vscode.proposed.codeActionAI.d.ts",
"../../src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts",
"../../src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts",
"../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts",
]
}