From 2aaf53bc415a930548c0bd2ecabe3927aa149d08 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Fri, 25 Aug 2023 14:16:43 -0700 Subject: [PATCH] Support addNameToNamelessParameter, refactor copilot code Not done switching quick fix code to use CompositeCommand instead of passing a followup to ApplyCodeActionCommand. --- .../src/languageFeatures/quickFix.ts | 140 ++++++------------ .../src/languageFeatures/refactor.ts | 125 +++------------- .../src/languageFeatures/util/copilot.ts | 129 ++++++++++++++++ .../src/tsServer/protocol/fixNames.ts | 1 + 4 files changed, 196 insertions(+), 199 deletions(-) create mode 100644 extensions/typescript-language-features/src/languageFeatures/util/copilot.ts diff --git a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts index 6ba8fa1a483..ab033714a8e 100644 --- a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts +++ b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts @@ -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{ - 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: [{ + 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: [{ action: tsAction, diagnostic, document }], + title: '' + }, { + command: ChatPanelFollowup.ID, + arguments: [{ + 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); diff --git a/extensions/typescript-language-features/src/languageFeatures/refactor.ts b/extensions/typescript-language-features/src/languageFeatures/refactor.ts index cdebdc712ed..51db7b5bd1b 100644 --- a/extensions/typescript-language-features/src/languageFeatures/refactor.ts +++ b/extensions/typescript-language-features/src/languageFeatures/refactor.ts @@ -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 { - 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: [{ 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: [{ 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{ 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); diff --git a/extensions/typescript-language-features/src/languageFeatures/util/copilot.ts b/extensions/typescript-language-features/src/languageFeatures/util/copilot.ts new file mode 100644 index 00000000000..e0087d10d66 --- /dev/null +++ b/extensions/typescript-language-features/src/languageFeatures/util/copilot.ts @@ -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 { + for (const command of commands) { + await vscode.commands.executeCommand(command.command, ...(command.arguments ?? [])); + } + } +} + diff --git a/extensions/typescript-language-features/src/tsServer/protocol/fixNames.ts b/extensions/typescript-language-features/src/tsServer/protocol/fixNames.ts index 4430e73ac2c..c456dceaab3 100644 --- a/extensions/typescript-language-features/src/tsServer/protocol/fixNames.ts +++ b/extensions/typescript-language-features/src/tsServer/protocol/fixNames.ts @@ -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';