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

Restore copilot fixes/refactorings #194092

Merged
merged 1 commit into from
Sep 26, 2023
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
57 changes: 52 additions & 5 deletions extensions/typescript-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,58 @@
"title": "%configuration.typescript%",
"order": 20,
"properties": {
"typescript.experimental.aiQuickFix": {
"type": "boolean",
"default": false,
"description": "%typescript.experimental.aiQuickFix%",
"scope": "resource"
"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",
Expand Down
11 changes: 10 additions & 1 deletion extensions/typescript-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@
"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.aiQuickFix": "Enable/disable AI-assisted quick fixes. Requires an extension providing AI chat functionality.",
"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 @@ -18,6 +18,7 @@ import { DiagnosticsManager } from './diagnostics';
import FileConfigurationManager from './fileConfigurationManager';
import { applyCodeActionCommands, getEditForCodeAction } from './util/codeAction';
import { conditionalRegistration, requireSomeCapability } from './util/dependentRegistration';
import { Expand, EditorChatFollowUp_Args, CompositeCommand, EditorChatFollowUp } from './util/copilot';

type ApplyCodeActionCommand_args = {
readonly document: vscode.TextDocument;
Expand All @@ -26,42 +27,6 @@ type ApplyCodeActionCommand_args = {
readonly followupAction?: Command;
};

class EditorChatFollowUp implements Command {

id: string = '_typescript.quickFix.editorChatFollowUp';

constructor(private readonly prompt: string, private readonly document: vscode.TextDocument, private readonly range: vscode.Range, private readonly client: ITypeScriptServiceClient) {
}

async execute() {
const findScopeEndLineFromNavTree = (startLine: number, navigationTree: Proto.NavigationTree[]): vscode.Range | undefined => {
for (const node of navigationTree) {
const range = typeConverters.Range.fromTextSpan(node.spans[0]);
if (startLine === range.start.line) {
return range;
} else if (startLine > range.start.line && startLine <= range.end.line && node.childItems) {
return findScopeEndLineFromNavTree(startLine, node.childItems);
}
}
return undefined;
};
const filepath = this.client.toOpenTsFilePath(this.document);
if (!filepath) {
return;
}
const response = await this.client.execute('navtree', { file: filepath }, nulToken);
if (response.type !== 'response' || !response.body?.childItems) {
return;
}
const startLine = this.range.start.line;
const enclosingRange = findScopeEndLineFromNavTree(startLine, response.body.childItems);
if (!enclosingRange) {
return;
}
await vscode.commands.executeCommand('vscode.editorChat.start', { initialRange: enclosingRange, message: this.prompt, autoSend: true });
}
}

class ApplyCodeActionCommand implements Command {
public static readonly ID = '_typescript.applyCodeActionCommand';
public readonly id = ApplyCodeActionCommand.ID;
Expand Down Expand Up @@ -255,8 +220,10 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
private readonly diagnosticsManager: DiagnosticsManager,
telemetryReporter: TelemetryReporter
) {
commandManager.register(new CompositeCommand());
commandManager.register(new ApplyCodeActionCommand(client, diagnosticsManager, telemetryReporter));
commandManager.register(new ApplyFixAllCodeAction(client, telemetryReporter));
commandManager.register(new EditorChatFollowUp(client));

this.supportedCodeActionProvider = new SupportedCodeActionProvider(client);
}
Expand Down Expand Up @@ -340,42 +307,89 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
}

for (const tsCodeFix of response.body) {
this.addAllFixesForTsCodeAction(results, document, file, diagnostic, tsCodeFix as Proto.CodeFixAction);
for (const action of this.getFixesForTsCodeAction(document, diagnostic, tsCodeFix)) {
results.addAction(action);
}
this.addFixAllForTsCodeAction(results, document.uri, file, diagnostic, tsCodeFix as Proto.CodeFixAction);
}
return results;
}

private addAllFixesForTsCodeAction(
results: CodeActionSet,
document: vscode.TextDocument,
file: string,
diagnostic: vscode.Diagnostic,
tsAction: Proto.CodeFixAction
): CodeActionSet {
results.addAction(this.getSingleFixForTsCodeAction(document, diagnostic, tsAction));
this.addFixAllForTsCodeAction(results, document.uri, file, diagnostic, tsAction as Proto.CodeFixAction);
return results;
}

private getSingleFixForTsCodeAction(
private getFixesForTsCodeAction(
document: vscode.TextDocument,
diagnostic: vscode.Diagnostic,
tsAction: Proto.CodeFixAction
): VsCodeCodeAction {
const aiQuickFixEnabled = vscode.workspace.getConfiguration('typescript').get('experimental.aiQuickFix');
let followupAction: Command | undefined;
if (aiQuickFixEnabled && tsAction.fixName === fixNames.classIncorrectlyImplementsInterface) {
followupAction = new EditorChatFollowUp('Implement the class using the interface', document, diagnostic.range, this.client);
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')) {
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')) {
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`;
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')) {
const inferFromBody = new VsCodeCodeAction(action, 'Infer types using Copilot', vscode.CodeActionKind.QuickFix);
inferFromBody.edit = new vscode.WorkspaceEdit();
inferFromBody.diagnostics = [diagnostic];
inferFromBody.command = {
command: EditorChatFollowUp.ID,
arguments: [<EditorChatFollowUp_Args>{
message: 'Add types to this code. Add separate interfaces when possible. Do not change the code except for adding types.',
expand: { kind: 'navtree-function', pos: diagnostic.range.start },
document
}],
title: ''
};
actions.push(inferFromBody);
}
else if (action.fixName === fixNames.addNameToNamelessParameter && vscode.workspace.getConfiguration('typescript').get('experimental.aiCodeActions.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 = {
kind: 'navtree-function',
pos: diagnostic.range.start
};
}
}
const codeAction = new VsCodeCodeAction(tsAction, tsAction.description, vscode.CodeActionKind.QuickFix);
codeAction.edit = getEditForCodeAction(this.client, tsAction);
const codeAction = new VsCodeCodeAction(action, title, vscode.CodeActionKind.QuickFix);
codeAction.edit = getEditForCodeAction(this.client, action);
codeAction.diagnostics = [diagnostic];
codeAction.command = {
command: ApplyCodeActionCommand.ID,
arguments: [<ApplyCodeActionCommand_args>{ action: tsAction, diagnostic, document, followupAction }],
arguments: [<ApplyCodeActionCommand_args>{ action: action, diagnostic, document }],
title: ''
};
return codeAction;
if (expand && message !== undefined) {
codeAction.command = {
command: CompositeCommand.ID,
title: '',
arguments: [codeAction.command, {
command: EditorChatFollowUp.ID,
title: '',
arguments: [<EditorChatFollowUp_Args>{
message,
expand,
document
}],
}],
};
}
actions.push(codeAction);
return actions;
}

private addFixAllForTsCodeAction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { coalesce } from '../utils/arrays';
import { nulToken } from '../utils/cancellation';
import FormattingOptionsManager from './fileConfigurationManager';
import { conditionalRegistration, requireSomeCapability } from './util/dependentRegistration';
import { EditorChatFollowUp, EditorChatFollowUp_Args, CompositeCommand } from './util/copilot';

function toWorkspaceEdit(client: ITypeScriptServiceClient, edits: readonly Proto.FileCodeEdits[]): vscode.WorkspaceEdit {
const workspaceEdit = new vscode.WorkspaceEdit();
Expand All @@ -34,17 +35,6 @@ function toWorkspaceEdit(client: ITypeScriptServiceClient, edits: readonly Proto
}


class CompositeCommand implements Command {
public static readonly ID = '_typescript.compositeCommand';
public readonly id = CompositeCommand.ID;

public async execute(...commands: vscode.Command[]): Promise<void> {
for (const command of commands) {
await vscode.commands.executeCommand(command.command, ...(command.arguments ?? []));
}
}
}

namespace DidApplyRefactoringCommand {
export interface Args {
readonly action: string;
Expand Down Expand Up @@ -355,15 +345,17 @@ class InlinedCodeAction extends vscode.CodeAction {
public readonly refactor: Proto.ApplicableRefactorInfo,
public readonly action: Proto.RefactorActionInfo,
public readonly range: vscode.Range,
public readonly copilotRename?: (info: Proto.RefactorEditInfo) => vscode.Command,
) {
super(action.description, InlinedCodeAction.getKind(action));
const title = copilotRename ? action.description + ' and suggest a name with Copilot.' : action.description;
super(title, InlinedCodeAction.getKind(action));

if (action.notApplicableReason) {
this.disabled = { reason: action.notApplicableReason };
}

this.command = {
title: action.description,
title,
command: DidApplyRefactoringCommand.ID,
arguments: [<DidApplyRefactoringCommand.Args>{ action: action.name }],
};
Expand Down Expand Up @@ -395,18 +387,21 @@ class InlinedCodeAction extends vscode.CodeAction {
if (response.body.renameLocation) {
// Disable renames in interactive playground https://github.com/microsoft/vscode/issues/75137
if (this.document.uri.scheme !== fileSchemes.walkThroughSnippet) {
if (this.copilotRename && this.command) {
this.command.title = 'Copilot: ' + this.command.title;
}
this.command = {
command: CompositeCommand.ID,
title: '',
arguments: coalesce([
this.command,
{
this.copilotRename ? this.copilotRename(response.body) : {
command: 'editor.action.rename',
arguments: [[
this.document.uri,
typeConverters.Position.fromLocation(response.body.renameLocation)
]]
}
},
])
};
}
Expand Down Expand Up @@ -456,7 +451,6 @@ class SelectCodeAction extends vscode.CodeAction {
};
}
}

type TsCodeAction = InlinedCodeAction | MoveToFileCodeAction | SelectCodeAction;

class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeAction> {
Expand All @@ -471,6 +465,7 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeActi
commandManager.register(new CompositeCommand());
commandManager.register(new SelectRefactorCommand(this.client));
commandManager.register(new MoveToFileRefactorCommand(this.client, didApplyRefactoringCommand));
commandManager.register(new EditorChatFollowUp(this.client));
}

public static readonly metadata: vscode.CodeActionProviderMetadata = {
Expand Down Expand Up @@ -582,7 +577,36 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeActi
if (action.name === 'Move to file') {
codeAction = new MoveToFileCodeAction(document, action, rangeOrSelection);
} else {
codeAction = new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection);
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')) {
const newName = Extract_Constant.matches(action) ? 'newLocal'
: Extract_Function.matches(action) ? 'newFunction'
: Extract_Type.matches(action) ? 'NewType'
: Extract_Interface.matches(action) ? 'NewInterface'
: '';
copilotRename = info => ({
title: '',
command: EditorChatFollowUp.ID,
arguments: [<EditorChatFollowUp_Args>{
message: `Rename ${newName} to a better name based on usage.`,
expand: Extract_Constant.matches(action) ? {
kind: 'navtree-function',
pos: typeConverters.Position.fromLocation(info.renameLocation!),
} : {
kind: 'refactor-info',
refactor: info,
},
document,
}]
});
}

}
codeAction = new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection, copilotRename);
}

codeAction.isPreferred = TypeScriptRefactorProvider.isPreferred(action, allActions);
Expand Down