Support addNameToNamelessParameter, refactor copilot code

Not done switching quick fix code to use CompositeCommand instead of
passing a followup to ApplyCodeActionCommand.
This commit is contained in:
Nathan Shively-Sanders
2023-08-25 14:16:43 -07:00
parent 2267ba86a6
commit 2aaf53bc41
4 changed files with 196 additions and 199 deletions

View File

@@ -18,80 +18,7 @@ import { DiagnosticsManager } from './diagnostics';
import FileConfigurationManager from './fileConfigurationManager';
import { applyCodeActionCommands, getEditForCodeAction } from './util/codeAction';
import { conditionalRegistration, requireSomeCapability } from './util/dependentRegistration';
type EditorChatReplacementCommand_args = {
readonly message: string;
readonly document: vscode.TextDocument;
readonly diagnostic: vscode.Diagnostic;
};
class EditorChatReplacementCommand implements Command {
public static readonly ID = '_typescript.quickFix.editorChatReplacement';
public readonly id = EditorChatReplacementCommand.ID;
constructor( private readonly client: ITypeScriptServiceClient, private readonly diagnosticManager: DiagnosticsManager) {
}
async execute({ message, document, diagnostic }: EditorChatReplacementCommand_args) {
this.diagnosticManager.deleteDiagnostic(document.uri, diagnostic);
await editorChat(this.client, document, diagnostic.range.start.line, message);
}
}
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() {
await editorChat(this.client, this.document, this.range.start.line, this.prompt);
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 });
}
}
function 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;
}
async function editorChat(client: ITypeScriptServiceClient, document: vscode.TextDocument, startLine: number, message: string) {
const filepath = client.toOpenTsFilePath(document);
if (!filepath) {
return;
}
const response = await client.execute('navtree', { file: filepath }, nulToken);
if (response.type !== 'response' || !response.body?.childItems) {
return;
}
const initialRange = findScopeEndLineFromNavTree(startLine, response.body.childItems);
if (!initialRange) {
return;
}
await vscode.commands.executeCommand('vscode.editorChat.start', { initialRange, message, autoSend: true });
}
import { ChatPanelFollowup, EditorChatFollowUp, EditorChatReplacementCommand1, CompositeCommand } from './util/copilot';
type ApplyCodeActionCommand_args = {
readonly document: vscode.TextDocument;
@@ -293,9 +220,11 @@ 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 EditorChatReplacementCommand(client, diagnosticsManager));
commandManager.register(new EditorChatReplacementCommand1(client, diagnosticsManager));
commandManager.register(new ChatPanelFollowup(client));
this.supportedCodeActionProvider = new SupportedCodeActionProvider(client);
}
@@ -393,24 +322,51 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
tsAction: Proto.CodeFixAction
): VsCodeCodeAction[] {
const actions: 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);
}
else if (aiQuickFixEnabled && tsAction.fixName === fixNames.inferFromUsage) {
const inferFromBody = new VsCodeCodeAction(tsAction, 'Copilot: Infer and add types', vscode.CodeActionKind.QuickFix);
inferFromBody.edit = new vscode.WorkspaceEdit();
inferFromBody.diagnostics = [diagnostic];
inferFromBody.command = {
command: EditorChatReplacementCommand.ID,
arguments: [<EditorChatReplacementCommand_args>{
message: 'Add types to this code. Add separate interfaces when possible. Do not change the code except for adding types.',
diagnostic,
document }],
title: ''
};
actions.push(inferFromBody);
if (vscode.workspace.getConfiguration('typescript').get('experimental.aiQuickFix')) {
if(tsAction.fixName === fixNames.classIncorrectlyImplementsInterface) {
followupAction = new EditorChatFollowUp('Implement the class using the interface', document, diagnostic.range, this.client);
}
else if (tsAction.fixName === fixNames.inferFromUsage) {
const inferFromBody = new VsCodeCodeAction(tsAction, 'Copilot: Infer and add types', vscode.CodeActionKind.QuickFix);
inferFromBody.edit = new vscode.WorkspaceEdit();
inferFromBody.diagnostics = [diagnostic];
inferFromBody.command = {
command: EditorChatReplacementCommand1.ID,
arguments: [<EditorChatReplacementCommand1.Args>{
message: 'Add types to this code. Add separate interfaces when possible. Do not change the code except for adding types.',
diagnostic,
document }],
title: ''
};
actions.push(inferFromBody);
}
else if (tsAction.fixName === fixNames.addNameToNamelessParameter) {
followupAction = new EditorChatFollowUp('Suggest a better name for this parameter', document, diagnostic.range, this.client);
const suggestName = new VsCodeCodeAction(tsAction, 'Add parameter name', vscode.CodeActionKind.QuickFix);
suggestName.edit = getEditForCodeAction(this.client, tsAction);
suggestName.command = {
command: CompositeCommand.ID,
arguments: [{
command: ApplyCodeActionCommand.ID,
arguments: [<ApplyCodeActionCommand_args>{ action: tsAction, diagnostic, document }],
title: ''
}, {
command: ChatPanelFollowup.ID,
arguments: [<ChatPanelFollowup.Args>{
prompt: `Suggest 5 normal-sounding, useful names for the parameter
\`\`\`
${document.getText(diagnostic.range)}
\`\`\` `,
range: diagnostic.range,
expand: 'navtree-function',
document }],
title: ''
}],
title: '',
}
return [suggestName]
}
}
const codeAction = new VsCodeCodeAction(tsAction, tsAction.description, vscode.CodeActionKind.QuickFix);
codeAction.edit = getEditForCodeAction(this.client, tsAction);

View File

@@ -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 { ChatPanelFollowup, EditorChatReplacementCommand2, CompositeCommand } from './util/copilot';
function toWorkspaceEdit(client: ITypeScriptServiceClient, edits: readonly Proto.FileCodeEdits[]): vscode.WorkspaceEdit {
const workspaceEdit = new vscode.WorkspaceEdit();
@@ -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;
@@ -396,6 +386,7 @@ 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) {
console.log("!!!!!!!!!!!!!!!!!!!!!!!!!!" + this.bonus?.command + "!!!!!!!!!!!!!!!!!!")
this.command = {
command: CompositeCommand.ID,
title: '',
@@ -467,92 +458,11 @@ class InferTypesAction extends vscode.CodeAction {
super(title, vscode.CodeActionKind.Refactor);
this.command = {
title,
command: EditorChatReplacementCommand.ID,
arguments: [<EditorChatReplacementCommand.Args>{ message: 'Add types to this code. Add separate interfaces when possible. Do not change the code except for adding types.', document, rangeOrSelection }]
command: EditorChatReplacementCommand2.ID,
arguments: [<EditorChatReplacementCommand2.Args>{ message: 'Add types to this code. Add separate interfaces when possible. Do not change the code except for adding types.', document, rangeOrSelection }]
};
}
}
class EditorChatReplacementCommand implements Command {
public static readonly ID = "_typescript.quickFix.editorChatReplacement";
public readonly id = EditorChatReplacementCommand.ID;
constructor(
private readonly client: ITypeScriptServiceClient,
) {
}
async execute({ message, document, rangeOrSelection }: EditorChatReplacementCommand.Args) {
// TODO: "this code" is not specific; might get better results with a more specific referent
// TODO: Doesn't work in JS files? Is this the span-finder's fault? Try falling back to startLine plus something.
// TODO: Need to emit jsdoc in JS files once it's working at all
// TODO: When there are "enough" types around, leave off the "Add separate interfaces when possible" because it's not helpful.
// (brainstorming: enough non-primitives, or evidence of type aliases in the same file, or imported)
await editorChat(this.client, document, rangeOrSelection.start.line, message)
}
}
namespace EditorChatReplacementCommand {
export interface Args {
readonly message: string;
readonly document: vscode.TextDocument;
readonly rangeOrSelection: vscode.Range | vscode.Selection;
}
}
function 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;
}
async function editorChat(client: ITypeScriptServiceClient, document: vscode.TextDocument, startLine: number, message: string) {
const filepath = client.toOpenTsFilePath(document);
if (!filepath) {
return;
}
const response = await client.execute('navtree', { file: filepath }, nulToken);
if (response.type !== 'response' || !response.body?.childItems) {
return;
}
const initialRange = findScopeEndLineFromNavTree(startLine, response.body.childItems);
if (!initialRange) {
return;
}
await vscode.commands.executeCommand('vscode.editorChat.start', { initialRange, message, autoSend: true });
}
namespace ChatPanelFollowup {
export interface Args {
readonly prompt: string;
readonly document: vscode.TextDocument;
readonly range: vscode.Range;
readonly expand: Expand;
}
// assuming there is an ast to walk, I'm convinced I can do a more consistent job than the navtree code.
export type Expand = 'none' | 'navtree-function' | 'statement' | 'ast-statement'
}
class ChatPanelFollowup implements Command {
public readonly id = ChatPanelFollowup.ID;
public static readonly ID: string = '_typescript.refactor.chatPanelFollowUp';
constructor(private readonly client: ITypeScriptServiceClient) {
}
async execute({ prompt, document, range, expand }: ChatPanelFollowup.Args) {
const filepath = this.client.toOpenTsFilePath(document);
if (!filepath) {
return;
}
const response = await this.client.execute('navtree', { file: filepath }, nulToken);
if (response.type !== 'response' || !response.body?.childItems) {
return;
}
const enclosingRange = expand === 'navtree-function' && findScopeEndLineFromNavTree(range.start.line, response.body.childItems) || range;
console.log(JSON.stringify(enclosingRange))
vscode.interactive.sendInteractiveRequestToProvider('copilot', { message: prompt, autoSend: true, initialRange: enclosingRange } as any)
}
}
type TsCodeAction = InlinedCodeAction | MoveToFileCodeAction | SelectCodeAction | InferTypesAction;
@@ -568,7 +478,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 EditorChatReplacementCommand(this.client));
commandManager.register(new EditorChatReplacementCommand2(this.client));
commandManager.register(new ChatPanelFollowup(this.client));
}
@@ -686,27 +596,28 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeActi
} else {
let bonus: vscode.Command | undefined
if (vscode.workspace.getConfiguration('typescript', null).get('experimental.aiQuickFix')) {
const actionPrefix = action.name.startsWith("constant_")
|| action.name.startsWith("function_")
|| action.name.startsWith("Extract to")
if (actionPrefix) {
const kind = action.name.startsWith("constant_") ? "expression"
: action.name.startsWith("function_") ? "function"
: action.name.startsWith("Extract to") ? "type"
: "code";
if (action.name.startsWith('constant_')
|| action.name.startsWith('function_')
|| action.name.startsWith('Extract to')
|| action.name.startsWith('Infer function return')) {
const kind = action.name.startsWith('constant_') ? 'expression'
: action.name.startsWith('function_') ? 'function'
: action.name.startsWith('Extract to') ? 'type'
: action.name.startsWith('Infer function return') ? 'type'
: 'code';
bonus = {
command: ChatPanelFollowup.ID,
arguments: [<ChatPanelFollowup.Args>{
prompt: `Suggest 5 names for the ${kind}
\`\`\`
${document.getText(rangeOrSelection)}.
\`\`\`
`,
\`\`\`
${document.getText(rangeOrSelection)}.
\`\`\` `,
range: rangeOrSelection,
expand: 'navtree-function',
document }],
title: ''
}
if (action.name.startsWith('Infer function return')) console.log(JSON.stringify(bonus))
}
}
codeAction = new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection, bonus);

View File

@@ -0,0 +1,129 @@
import * as vscode from 'vscode';
import { Command } from '../../commands/commandManager';
import { nulToken } from '../../utils/cancellation';
import { DiagnosticsManager } from '../diagnostics';
import type * as Proto from '../../tsServer/protocol/protocol';
import * as typeConverters from '../../typeConverters';
import { ITypeScriptServiceClient } from '../../typescriptService';
// TODO: quick fix version needs to delete the diagnostic (because maybe the followup interferes with it?)
// so it needs a diagnostic manager and a diagnostic. The refactor version doesn't need this
// (there is tiny bits of code overall so maybe there's a different way to write this)
export namespace EditorChatReplacementCommand1 {
export type Args = {
readonly message: string;
readonly document: vscode.TextDocument;
readonly diagnostic: vscode.Diagnostic;
};
}
export class EditorChatReplacementCommand1 implements Command {
public static readonly ID = '_typescript.quickFix.editorChatReplacement';
public readonly id = EditorChatReplacementCommand1.ID;
constructor( private readonly client: ITypeScriptServiceClient, private readonly diagnosticManager: DiagnosticsManager) {
}
async execute({ message, document, diagnostic }: EditorChatReplacementCommand1.Args) {
this.diagnosticManager.deleteDiagnostic(document.uri, diagnostic);
const initialRange = await findScopeEndLineFromNavTree(this.client, document, diagnostic.range.start.line);
await vscode.commands.executeCommand('vscode.editorChat.start', { initialRange, message, autoSend: true });
}
}
export class EditorChatReplacementCommand2 implements Command {
public static readonly ID = "_typescript.quickFix.editorChatReplacement";
public readonly id = EditorChatReplacementCommand2.ID;
constructor(
private readonly client: ITypeScriptServiceClient,
) {
}
async execute({ message, document, rangeOrSelection }: EditorChatReplacementCommand2.Args) {
// TODO: "this code" is not specific; might get better results with a more specific referent
// TODO: Doesn't work in JS files? Is this the span-finder's fault? Try falling back to startLine plus something.
// TODO: Need to emit jsdoc in JS files once it's working at all
// TODO: When there are "enough" types around, leave off the "Add separate interfaces when possible" because it's not helpful.
// (brainstorming: enough non-primitives, or evidence of type aliases in the same file, or imported)
const initialRange = await findScopeEndLineFromNavTree(this.client, document, rangeOrSelection.start.line)
await vscode.commands.executeCommand('vscode.editorChat.start', { initialRange, message, autoSend: true });
}
}
export namespace EditorChatReplacementCommand2 {
export interface Args {
readonly message: string;
readonly document: vscode.TextDocument;
readonly rangeOrSelection: vscode.Range | vscode.Selection;
}
}
export 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 initialRange = await findScopeEndLineFromNavTree(this.client, this.document, this.range.start.line);
await vscode.commands.executeCommand('vscode.editorChat.start', { initialRange, message: this.prompt, autoSend: true });
}
}
export function findScopeEndLineFromNavTreeWorker(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 findScopeEndLineFromNavTreeWorker(startLine, node.childItems);
}
}
return undefined;
}
export async function findScopeEndLineFromNavTree(client: ITypeScriptServiceClient, document: vscode.TextDocument, startLine: number) {
const filepath = client.toOpenTsFilePath(document);
if (!filepath) {
return;
}
const response = await client.execute('navtree', { file: filepath }, nulToken);
if (response.type !== 'response' || !response.body?.childItems) {
return;
}
return findScopeEndLineFromNavTreeWorker(startLine, response.body.childItems);
}
export namespace ChatPanelFollowup {
export interface Args {
readonly prompt: string;
readonly document: vscode.TextDocument;
readonly range: vscode.Range;
readonly expand: Expand;
}
// assuming there is an ast to walk, I'm convinced I can do a more consistent job than the navtree code.
export type Expand = 'none' | 'navtree-function' | 'statement' | 'ast-statement'
}
export class ChatPanelFollowup implements Command {
public readonly id = ChatPanelFollowup.ID;
public static readonly ID: string = '_typescript.refactor.chatPanelFollowUp';
constructor(private readonly client: ITypeScriptServiceClient) {
}
async execute({ prompt, document, range, expand }: ChatPanelFollowup.Args) {
console.log("-------------------------------" + prompt + "------------------------------")
const enclosingRange = expand === 'navtree-function' && findScopeEndLineFromNavTree(this.client, document, range.start.line) || range;
vscode.interactive.sendInteractiveRequestToProvider('copilot', { message: prompt, autoSend: true, initialRange: enclosingRange } as any)
}
}
export 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 ?? []));
}
}
}

View File

@@ -17,5 +17,6 @@ export const forgottenThisPropertyAccess = 'forgottenThisPropertyAccess';
export const removeUnnecessaryAwait = 'removeUnnecessaryAwait';
export const spelling = 'spelling';
export const inferFromUsage = 'inferFromUsage';
export const addNameToNamelessParameter = 'addNameToNamelessParameter';
export const unreachableCode = 'fixUnreachableCode';
export const unusedIdentifier = 'unusedIdentifier';