diff --git a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts index 030a0debeeb..a901d38c1e1 100644 --- a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts +++ b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts @@ -18,7 +18,7 @@ import { DiagnosticsManager } from './diagnostics'; import FileConfigurationManager from './fileConfigurationManager'; import { applyCodeActionCommands, getEditForCodeAction } from './util/codeAction'; import { conditionalRegistration, requireSomeCapability } from './util/dependentRegistration'; -import { ChatPanelFollowup, EditorChatFollowUp, EditorChatReplacementCommand1, CompositeCommand } from './util/copilot'; +import { ChatPanelFollowup, Expand, EditorChatReplacementCommand2, CompositeCommand } from './util/copilot'; type ApplyCodeActionCommand_args = { readonly document: vscode.TextDocument; @@ -223,7 +223,7 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider{ action: action, diagnostic, document }], + title: '' + }; + actions.push(codeAction); + 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); + let message: string | undefined + let expand: Expand | undefined + + if(action.fixName === fixNames.classIncorrectlyImplementsInterface) { + message = `Implement the stubbed-out class members for ${document.getText(diagnostic.range)} with a useful implementation.`; + expand = { kind: 'code-action', action } } - else if(tsAction.fixName === fixNames.fixClassDoesntImplementInheritedAbstractMember) { - // TODO: This range has the same problem as all the other followups - followupAction = new EditorChatFollowUp('Implement abstract class members with a useful implementation', document, diagnostic.range, this.client); + else if(action.fixName === fixNames.fixClassDoesntImplementInheritedAbstractMember) { + message = `Implement the stubbed-out class members for ${document.getText(diagnostic.range)} with a useful implementation.`; + expand = { kind: 'code-action', action }; } - else if (tsAction.fixName === fixNames.fixMissingFunctionDeclaration) { - let edits = getEditForCodeAction(this.client, tsAction) - // console.log(JSON.stringify(edits)) // need to generate a new range based on the length and lines of the new text - const range = !edits ? diagnostic.range : edits.entries()[0][1][0].range - followupAction = new EditorChatFollowUp( - `Implement the function based on the function call \`${document.getText(diagnostic.range)}\``, - document, range, this.client); + else if (action.fixName === fixNames.fixMissingFunctionDeclaration) { + 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 (tsAction.fixName === fixNames.inferFromUsage) { - const inferFromBody = new VsCodeCodeAction(tsAction, 'Copilot: Infer and add types', vscode.CodeActionKind.QuickFix); + else if (action.fixName === fixNames.inferFromUsage) { + const inferFromBody = new VsCodeCodeAction(action, 'Copilot: Infer and add types', vscode.CodeActionKind.QuickFix); inferFromBody.edit = new vscode.WorkspaceEdit(); inferFromBody.diagnostics = [diagnostic]; inferFromBody.command = { - command: EditorChatReplacementCommand1.ID, - arguments: [{ + command: EditorChatReplacementCommand2.ID, + arguments: [{ message: 'Add types to this code. Add separate interfaces when possible. Do not change the code except for adding types.', - diagnostic, - document }], + expand: { kind: 'navtree-function', pos: diagnostic.range.start }, + document + }], title: '' }; actions.push(inferFromBody); } - else if (tsAction.fixName === fixNames.addNameToNamelessParameter) { - const suggestName = new VsCodeCodeAction(tsAction, 'Add parameter name', vscode.CodeActionKind.QuickFix); - suggestName.edit = getEditForCodeAction(this.client, tsAction); - suggestName.command = { + else if (action.fixName === fixNames.addNameToNamelessParameter) { + const newText = action.changes.map(change => change.textChanges.map(textChange => textChange.newText).join('')).join('') + message = `Rename the parameter ${newText} with a more meaningful name.`, + expand = { + kind: 'navtree-function', + pos: diagnostic.range.start + }; + } + if (expand && message != null) { + codeAction.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: '', + arguments: [codeAction.command, { + command: EditorChatReplacementCommand2.ID, + title: '', + arguments: [{ + message, + expand, + document + }], + }], } - return [suggestName] } } - const codeAction = new VsCodeCodeAction(tsAction, tsAction.description, vscode.CodeActionKind.QuickFix); - codeAction.edit = getEditForCodeAction(this.client, tsAction); - codeAction.diagnostics = [diagnostic]; - codeAction.command = { - command: ApplyCodeActionCommand.ID, - arguments: [{ action: tsAction, diagnostic, document, followupAction }], - title: '' - }; - actions.push(codeAction); return actions; } diff --git a/extensions/typescript-language-features/src/languageFeatures/refactor.ts b/extensions/typescript-language-features/src/languageFeatures/refactor.ts index 53eb9272c6d..e78884c44e8 100644 --- a/extensions/typescript-language-features/src/languageFeatures/refactor.ts +++ b/extensions/typescript-language-features/src/languageFeatures/refactor.ts @@ -345,7 +345,6 @@ class InlinedCodeAction extends vscode.CodeAction { public readonly refactor: Proto.ApplicableRefactorInfo, public readonly action: Proto.RefactorActionInfo, public readonly range: vscode.Range, - public readonly bonus?: vscode.Command, public readonly airename?: (x: Proto.RefactorEditInfo) => vscode.Command, ) { super(action.description, InlinedCodeAction.getKind(action)); @@ -387,13 +386,11 @@ 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: '', arguments: coalesce([ // TODO: this.bonus should really get renameLocation as well (at least most of the time) - this.bonus, // TODO: This should actually go second. Maybe? this.command, this.airename ? this.airename(response.body) : { command: 'editor.action.rename', @@ -461,7 +458,14 @@ class InferTypesAction extends vscode.CodeAction { this.command = { title, 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, range: rangeOrSelection }] + arguments: [{ + message: 'Add types to this code. Add separate interfaces when possible. Do not change the code except for adding types.', + document, + expand: { + kind: 'none', + range: rangeOrSelection + } + }] }; } } @@ -596,56 +600,38 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider vscode.Command) | undefined if (vscode.workspace.getConfiguration('typescript', null).get('experimental.aiQuickFix')) { if (Extract_Constant.matches(action) || Extract_Function.matches(action) || Extract_Type.matches(action) || Extract_Interface.matches(action) - || action.name.startsWith('Infer function return')) { // TODO: There's no CodeActionKind for infer function return; maybe that's why it doesn't work - // const keyword = Extract_Constant.matches(action) ? 'const' - // : Extract_Function.matches(action) ? 'function' - // : Extract_Type.matches(action) ? 'type' - // : Extract_Interface.matches(action) ? 'interface' - // : action.name.startsWith('Infer function return') ? 'return' - // : ''; + || action.name.startsWith('Infer function return')) { const newName = Extract_Constant.matches(action) ? 'newLocal' : Extract_Function.matches(action) ? 'newFunction' : Extract_Type.matches(action) ? 'NewType' : Extract_Interface.matches(action) ? 'NewInterface' : action.name.startsWith('Infer function return') ? 'newReturnType' : ''; - bonus = undefined /*{ - command: ChatPanelFollowup.ID, - arguments: [{ - prompt: `Suggest 5 ${kind} names for the code below: -\`\`\` -${document.getText(rangeOrSelection)}. -\`\`\` `, - range: rangeOrSelection, - expand: 'navtree-function', - document }], - title: '' - }*/ airename = refactorInfo => ({ title: '', command: EditorChatReplacementCommand2.ID, arguments: [{ - expand: Extract_Constant.matches(action) ? 'navtree-function' : 'refactor-info', - refactor: refactorInfo, message: `Rename ${newName} to a better name based on usage.`, + expand: Extract_Constant.matches(action) ? { + kind: 'navtree-function', + pos: typeConverters.Position.fromLocation(refactorInfo.renameLocation!), + } : { + kind: 'refactor-info', + refactor: refactorInfo, + }, document, - // TODO: only start is used for everything but 'default'; refactor-info ignores it entirely - range: new vscode.Range( - typeConverters.Position.fromLocation(refactorInfo.renameLocation!), - typeConverters.Position.fromLocation({ ...refactorInfo.renameLocation!, offset: refactorInfo.renameLocation!.offset + 12 })) }] }); } } - codeAction = new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection, bonus, airename); + codeAction = new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection, airename); } codeAction.isPreferred = TypeScriptRefactorProvider.isPreferred(action, allActions); diff --git a/extensions/typescript-language-features/src/languageFeatures/util/copilot.ts b/extensions/typescript-language-features/src/languageFeatures/util/copilot.ts index eb274072aa5..1f7958e30a6 100644 --- a/extensions/typescript-language-features/src/languageFeatures/util/copilot.ts +++ b/extensions/typescript-language-features/src/languageFeatures/util/copilot.ts @@ -1,34 +1,10 @@ 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.editorChatReplacement1'; - 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.editorChatReplacement2'; public readonly id = EditorChatReplacementCommand2.ID; @@ -36,16 +12,17 @@ export class EditorChatReplacementCommand2 implements Command { private readonly client: ITypeScriptServiceClient, ) { } - async execute({ message, document, range: range, expand, marker, refactor }: EditorChatReplacementCommand2.Args) { + async execute({ message, document, expand }: 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 = expand === 'navtree-function' ? await findScopeEndLineFromNavTree(this.client, document, range.start.line) - : expand === 'identifier' ? findScopeEndMarker(document, range.start, marker!) - : expand === 'refactor-info' ? findRefactorScope(document, refactor!) - : range; + const initialRange = expand.kind === 'navtree-function' ? await findScopeEndLineFromNavTree(this.client, document, expand.pos.line) + : expand.kind === 'identifier' ? findScopeEndMarker(document, expand.range.start, expand.marker) + : expand.kind === 'refactor-info' ? await findEditScope(this.client, document, expand.refactor.edits.flatMap(e => e.textChanges)) + : expand.kind === 'code-action' ? await findEditScope(this.client, document, expand.action.changes.flatMap(c => c.textChanges)) + : expand.range; if (initialRange) { console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + message + `\nWith context(${expand}): ` + document.getText().slice(document.offsetAt(initialRange.start), document.offsetAt(initialRange.end)) @@ -58,23 +35,7 @@ export namespace EditorChatReplacementCommand2 { export interface Args { readonly message: string; readonly document: vscode.TextDocument; - readonly range: vscode.Range; readonly expand: Expand; - readonly marker?: string; - readonly refactor?: Proto.RefactorEditInfo; - } -} - -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 }); } } @@ -98,8 +59,8 @@ export class ChatPanelFollowup implements Command { async execute({ prompt, document, range, expand, marker }: ChatPanelFollowup.Args) { console.log("-------------------------------" + prompt + "------------------------------") - const enclosingRange = expand === 'navtree-function' ? await findScopeEndLineFromNavTree(this.client, document, range.start.line) - : expand === 'identifier' ? findScopeEndMarker(document, range.start, marker!) + const enclosingRange = expand.kind === 'navtree-function' ? await findScopeEndLineFromNavTree(this.client, document, range.start.line) + : expand.kind === 'identifier' ? findScopeEndMarker(document, range.start, marker!) : range; vscode.interactive.sendInteractiveRequestToProvider('copilot', { message: prompt, autoSend: true, initialRange: enclosingRange } as any) } @@ -116,7 +77,11 @@ export class CompositeCommand implements Command { } } -export type Expand = 'none' | 'navtree-function' | 'identifier' | 'refactor-info' | 'statement' | 'ast-statement' +export type Expand = { kind: 'none', readonly range: vscode.Range } + | { kind: "navtree-function", readonly pos: vscode.Position } + | { kind: 'refactor-info', readonly refactor: Proto.RefactorEditInfo } + | { kind: 'code-action', readonly action: Proto.CodeAction } + | { kind: "identifier", readonly range: vscode.Range, readonly marker: string }; function findScopeEndLineFromNavTreeWorker(startLine: number, navigationTree: Proto.NavigationTree[]): vscode.Range | undefined { for (const node of navigationTree) { @@ -149,29 +114,28 @@ function findScopeEndMarker(document: vscode.TextDocument, start: vscode.Positio return new vscode.Range(start, document.positionAt(offset)) } -function findRefactorScope(document: vscode.TextDocument, refactor: Proto.RefactorEditInfo): vscode.Range { - let first = typeConverters.Position.fromLocation(refactor.edits[0].textChanges[0].start) - let firstChange = refactor.edits[0].textChanges[0] - let lastChange = refactor.edits[0].textChanges[0] - let last = typeConverters.Position.fromLocation(refactor.edits[0].textChanges[0].start) - for (const edit of refactor.edits) { - for (const change of edit.textChanges) { - const start = typeConverters.Position.fromLocation(change.start) - const end = typeConverters.Position.fromLocation(change.end) - if (start.compareTo(first) < 0) { - first = start - firstChange = change - } - if (end.compareTo(last) > 0) { - last = end - lastChange = change - } +async function findEditScope(client: ITypeScriptServiceClient, document: vscode.TextDocument, edits: Proto.CodeEdit[]): Promise { + let first = typeConverters.Position.fromLocation(edits[0].start) + let firstEdit = edits[0] + let lastEdit = edits[0] + let last = typeConverters.Position.fromLocation(edits[0].start) + for (const edit of edits) { + const start = typeConverters.Position.fromLocation(edit.start) + const end = typeConverters.Position.fromLocation(edit.end) + if (start.compareTo(first) < 0) { + first = start + firstEdit = edit + } + if (end.compareTo(last) > 0) { + last = end + lastEdit = edit } } const text = document.getText() - let startIndex = text.indexOf(firstChange.newText) + let startIndex = text.indexOf(firstEdit.newText) let start = startIndex > -1 ? document.positionAt(startIndex) : first - let endIndex = text.lastIndexOf(lastChange.newText) - let end = endIndex > -1 ? document.positionAt(endIndex + lastChange.newText.length) : last - return new vscode.Range(start, end) + let endIndex = text.lastIndexOf(lastEdit.newText) + let end = endIndex > -1 ? document.positionAt(endIndex + lastEdit.newText.length) : last + const expandEnd = await findScopeEndLineFromNavTree(client, document, end.line) + return new vscode.Range(start, expandEnd?.end ?? end) }