Pretty good ranges based on RefactorInfo

Results are not pretty good yet. They are still below average.
This commit is contained in:
Nathan Shively-Sanders
2023-09-06 12:54:07 -07:00
parent 63c84d3aab
commit a0a2cb4de0
2 changed files with 115 additions and 44 deletions

View File

@@ -346,6 +346,7 @@ class InlinedCodeAction extends vscode.CodeAction {
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));
@@ -391,9 +392,10 @@ class InlinedCodeAction extends vscode.CodeAction {
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',
arguments: [[
this.document.uri,
@@ -459,7 +461,7 @@ 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, 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, range: rangeOrSelection }]
};
}
}
@@ -595,19 +597,26 @@ class TypeScriptRefactorProvider implements vscode.CodeActionProvider<TsCodeActi
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 kind = Extract_Constant.matches(action) ? 'variable'
: Extract_Function.matches(action) ? 'function'
: Extract_Type.matches(action) ? 'type'
: Extract_Interface.matches(action) ? 'type'
: action.name.startsWith('Infer function return') ? 'return type'
// 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'
// : '';
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 = {
bonus = undefined /*{
command: ChatPanelFollowup.ID,
arguments: [<ChatPanelFollowup.Args>{
prompt: `Suggest 5 ${kind} names for the code below:
@@ -618,11 +627,25 @@ ${document.getText(rangeOrSelection)}.
expand: 'navtree-function',
document }],
title: ''
}
if (action.name.startsWith('Infer function return')) console.log(JSON.stringify(bonus))
}*/
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.`,
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);
codeAction = new InlinedCodeAction(this.client, document, refactor, action, rangeOrSelection, bonus, airename);
}
codeAction.isPreferred = TypeScriptRefactorProvider.isPreferred(action, allActions);

View File

@@ -17,7 +17,7 @@ export namespace EditorChatReplacementCommand1 {
};
}
export class EditorChatReplacementCommand1 implements Command {
public static readonly ID = '_typescript.quickFix.editorChatReplacement';
public static readonly ID = '_typescript.quickFix.editorChatReplacement1';
public readonly id = EditorChatReplacementCommand1.ID;
constructor( private readonly client: ITypeScriptServiceClient, private readonly diagnosticManager: DiagnosticsManager) {
@@ -30,19 +30,27 @@ export class EditorChatReplacementCommand1 implements Command {
}
}
export class EditorChatReplacementCommand2 implements Command {
public static readonly ID = "_typescript.quickFix.editorChatReplacement";
public static readonly ID = '_typescript.quickFix.editorChatReplacement2';
public readonly id = EditorChatReplacementCommand2.ID;
constructor(
private readonly client: ITypeScriptServiceClient,
) {
}
async execute({ message, document, rangeOrSelection }: EditorChatReplacementCommand2.Args) {
async execute({ message, document, range: range, expand, marker, refactor }: 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)
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;
if (initialRange) {
console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + message
+ `\nWith context(${expand}): ` + document.getText().slice(document.offsetAt(initialRange.start), document.offsetAt(initialRange.end))
+ "\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
}
await vscode.commands.executeCommand('vscode.editorChat.start', { initialRange, message, autoSend: true });
}
}
@@ -50,11 +58,13 @@ export namespace EditorChatReplacementCommand2 {
export interface Args {
readonly message: string;
readonly document: vscode.TextDocument;
readonly rangeOrSelection: vscode.Range | vscode.Selection;
readonly range: vscode.Range;
readonly expand: Expand;
readonly marker?: string;
readonly refactor?: Proto.RefactorEditInfo;
}
}
export class EditorChatFollowUp implements Command {
id: string = '_typescript.quickFix.editorChatFollowUp';
@@ -68,39 +78,16 @@ export class EditorChatFollowUp implements Command {
}
}
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;
readonly marker?: string;
readonly refactor?: Proto.RefactorEditInfo;
}
// 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;
@@ -109,9 +96,11 @@ export class ChatPanelFollowup implements Command {
constructor(private readonly client: ITypeScriptServiceClient) {
}
async execute({ prompt, document, range, expand }: ChatPanelFollowup.Args) {
async execute({ prompt, document, range, expand, marker }: ChatPanelFollowup.Args) {
console.log("-------------------------------" + prompt + "------------------------------")
const enclosingRange = expand === 'navtree-function' && findScopeEndLineFromNavTree(this.client, document, range.start.line) || range;
const enclosingRange = expand === 'navtree-function' ? await findScopeEndLineFromNavTree(this.client, document, range.start.line)
: expand === 'identifier' ? findScopeEndMarker(document, range.start, marker!)
: range;
vscode.interactive.sendInteractiveRequestToProvider('copilot', { message: prompt, autoSend: true, initialRange: enclosingRange } as any)
}
}
@@ -127,3 +116,62 @@ export class CompositeCommand implements Command {
}
}
export type Expand = 'none' | 'navtree-function' | 'identifier' | 'refactor-info' | 'statement' | 'ast-statement'
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;
}
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);
}
function findScopeEndMarker(document: vscode.TextDocument, start: vscode.Position, marker: string): vscode.Range {
const text = document.getText();
const offset = text.indexOf(marker, text.indexOf(marker)) + marker.length
// TODO: Expand to the end of whatever marker is. (OR, chain to findScopeEndLineFromnavTree)
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
}
}
}
const text = document.getText()
let startIndex = text.indexOf(firstChange.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)
}