mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user