Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split TS' AI-backed code actions into separate entries #201140

Merged
merged 14 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 2 additions & 54 deletions extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"enabledApiProposals": [
"workspaceTrust",
"multiDocumentHighlightProvider",
"mappedEditsProvider"
"mappedEditsProvider",
"codeActionAI"
],
"capabilities": {
"virtualWorkspaces": {
Expand Down Expand Up @@ -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%",
Expand Down
10 changes: 0 additions & 10 deletions extensions/typescript-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like a few of these messages could be marked to be localized

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I'm working with the JS/TS team to share resources with an eventual VS version of these code actions, which will make it more complex, because the title will likely be loaded from a json file or something.

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: [{
Expand All @@ -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.`;
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down Expand Up @@ -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: [{
Expand All @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions extensions/typescript-language-features/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
}