Switch entirely to inline+action-based expansion

Except for a couple that still rely only on navtree-based expansion.
Lots of cleanup and standardisation.
This commit is contained in:
Nathan Shively-Sanders
2023-09-07 10:13:01 -07:00
parent a0a2cb4de0
commit 9c5b16a444
3 changed files with 101 additions and 152 deletions
@@ -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<VsCodeCode
commandManager.register(new CompositeCommand());
commandManager.register(new ApplyCodeActionCommand(client, diagnosticsManager, telemetryReporter));
commandManager.register(new ApplyFixAllCodeAction(client, telemetryReporter));
commandManager.register(new EditorChatReplacementCommand1(client, diagnosticsManager));
commandManager.register(new EditorChatReplacementCommand2(client));
commandManager.register(new ChatPanelFollowup(client));
this.supportedCodeActionProvider = new SupportedCodeActionProvider(client);
@@ -319,75 +319,74 @@ class TypeScriptQuickFixProvider implements vscode.CodeActionProvider<VsCodeCode
private getFixesForTsCodeAction(
document: vscode.TextDocument,
diagnostic: vscode.Diagnostic,
tsAction: Proto.CodeFixAction
action: Proto.CodeFixAction
): VsCodeCodeAction[] {
const actions: VsCodeCodeAction[] = [];
let followupAction: Command | undefined;
let codeAction = new VsCodeCodeAction(action, action.description, vscode.CodeActionKind.QuickFix);
codeAction.edit = getEditForCodeAction(this.client, action);
codeAction.diagnostics = [diagnostic];
codeAction.command = {
command: ApplyCodeActionCommand.ID,
arguments: [<ApplyCodeActionCommand_args>{ 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: [<EditorChatReplacementCommand1.Args>{
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.',
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: [<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: '',
arguments: [codeAction.command, {
command: EditorChatReplacementCommand2.ID,
title: '',
arguments: [<EditorChatReplacementCommand2.Args>{
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: [<ApplyCodeActionCommand_args>{ action: tsAction, diagnostic, document, followupAction }],
title: ''
};
actions.push(codeAction);
return actions;
}
@@ -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: [<EditorChatReplacementCommand2.Args>{ message: 'Add types to this code. Add separate interfaces when possible. Do not change the code except for adding types.', document, range: rangeOrSelection }]
arguments: [<EditorChatReplacementCommand2.Args>{
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<TsCodeActi
if (action.name === 'Move to file') {
codeAction = new MoveToFileCodeAction(document, action, rangeOrSelection);
} else {
let bonus: vscode.Command | undefined
let airename: ((rename: Proto.RefactorEditInfo) => 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: [<ChatPanelFollowup.Args>{
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: [<EditorChatReplacementCommand2.Args>{
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);
@@ -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<vscode.Range> {
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)
}