From bcf8098b8f8ae0e0c3562c42b3cc82bdc68bb371 Mon Sep 17 00:00:00 2001 From: Tyler Wilding Date: Fri, 26 Jan 2024 14:13:50 -0500 Subject: [PATCH] decomp: add option to auto-format decompiler results (#336) --- package.json | 18 +++-- src/config/config.ts | 13 ++++ src/decomp/decomp-tools.ts | 106 ++++++++++++++++++++++++++- src/languages/ir2/ir2-completions.ts | 6 +- 4 files changed, 135 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 829d64c..7494f10 100644 --- a/package.json +++ b/package.json @@ -240,6 +240,14 @@ "default": null, "description": "File path to the decompiler executable" }, + "opengoal.formatterPath": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "File path to the formatter executable for when invoking directly (not via LSP)" + }, "opengoal.typeSearcherPath": { "type": [ "string", @@ -262,6 +270,11 @@ "type": "string", "default": "ntsc_v1", "description": "Config version to use for decompiling Jak 3 related files" + }, + "opengoal.formatDecompilationOutput": { + "type": "boolean", + "default": false, + "description": "Whether or not the results of the decompiler should be auto-formatted" } } }, @@ -427,11 +440,6 @@ "command": "opengoal.decomp.openManPage", "group": "z_commands" } - ], - "editor/title": [ - { - "when": "resourceScheme == opengoalBatchRename" - } ] }, "languages": [ diff --git a/src/config/config.ts b/src/config/config.ts index 92d94cf..38bed2e 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -13,10 +13,14 @@ export function getConfig() { opengoalLspPath: configOptions.get("opengoalLspPath"), opengoalLspLogPath: configOptions.get("opengoalLspLogPath"), opengoalLspLogVerbose: configOptions.get("opengoalLspLogVerbose"), + formatDecompilationOutput: configOptions.get( + "formatDecompilationOutput", + ), eeManPagePath: configOptions.get("eeManPagePath"), vuManPagePath: configOptions.get("vuManPagePath"), decompilerPath: configOptions.get("decompilerPath"), + formatterPath: configOptions.get("formatterPath"), typeSearcherPath: configOptions.get("typeSearcherPath"), jak1DecompConfigVersion: configOptions.get( "decompilerJak1ConfigVersion", @@ -83,6 +87,15 @@ export async function updateDecompilerPath(path: string) { ); } +export async function updateFormatterPath(path: string) { + const userConfig = vscode.workspace.getConfiguration(); + await userConfig.update( + "opengoal.formatterPath", + path, + vscode.ConfigurationTarget.Global, + ); +} + export async function updateTypeSearcherPath(path: string) { const userConfig = vscode.workspace.getConfiguration(); await userConfig.update( diff --git a/src/decomp/decomp-tools.ts b/src/decomp/decomp-tools.ts index 1ba5b8a..75d6ae9 100644 --- a/src/decomp/decomp-tools.ts +++ b/src/decomp/decomp-tools.ts @@ -4,7 +4,11 @@ import * as vscode from "vscode"; import { determineGameFromPath, GameName } from "../utils/file-utils"; import { open_in_pdf } from "./man-page"; import * as util from "util"; -import { getConfig, updateDecompilerPath } from "../config/config"; +import { + getConfig, + updateDecompilerPath, + updateFormatterPath, +} from "../config/config"; import * as path from "path"; import { getExtensionContext, getProjectRoot } from "../context"; import { @@ -36,6 +40,7 @@ enum DecompStatus { Idle, Running, Errored, + Formatting, } function updateStatus(status: DecompStatus, metadata?: any) { @@ -71,6 +76,20 @@ function updateStatus(status: DecompStatus, metadata?: any) { decompStatusItem.tooltip = "Decompiling..."; decompStatusItem.command = undefined; break; + case DecompStatus.Formatting: + if (metadata.objectNames.length > 0) { + if (metadata.objectNames.length <= 5) { + subText = metadata.objectNames.join(", "); + } else { + subText = `${metadata.objectNames.slice(0, 5).join(", ")}, and ${ + metadata.objectNames.length - 5 + } more`; + } + } + decompStatusItem.text = `$(loading~spin) Formatting - ${subText} - [ ${metadata.decompConfig} ]`; + decompStatusItem.tooltip = "Formatting..."; + decompStatusItem.command = undefined; + break; default: break; } @@ -85,6 +104,15 @@ function defaultDecompPath() { } } +function defaultFormatterPath() { + const platform = process.platform; + if (platform == "win32") { + return "out/build/Release/bin/formatter.exe"; + } else { + return "build/tools/formatter"; + } +} + function getDecompilerConfig(gameName: GameName): string | undefined { let decompConfigPath = undefined; if (gameName == GameName.Jak1) { @@ -159,6 +187,39 @@ async function checkDecompilerPath(): Promise { return decompilerPath; } +async function checkFormatterPath(): Promise { + let formatterPath = getConfig().formatterPath; + + // Look for the decompiler if the path isn't set or the file is now missing + if (formatterPath !== undefined && existsSync(formatterPath)) { + return formatterPath; + } + + const potentialPath = vscode.Uri.joinPath( + getProjectRoot(), + defaultFormatterPath(), + ); + if (existsSync(potentialPath.fsPath)) { + formatterPath = potentialPath.fsPath; + } else { + // Ask the user to find it cause we have no idea + const path = await vscode.window.showOpenDialog({ + canSelectMany: false, + openLabel: "Select Formatter", + title: "Provide the formatter executable's path", + }); + if (path === undefined || path.length == 0) { + vscode.window.showErrorMessage( + "OpenGOAL - Aborting formatting, you didn't provide a path to the executable", + ); + return undefined; + } + formatterPath = path[0].fsPath; + } + updateFormatterPath(formatterPath); + return formatterPath; +} + async function decompFiles( gameName: GameName, fileNames: string[], @@ -216,6 +277,49 @@ async function decompFiles( `DECOMP ERROR:\nSTDOUT:\n${error.stdout}\nSTDERR:\n${error.stderr}`, ); } + + // Format results + if (getConfig().formatDecompilationOutput) { + const formatterPath = await checkFormatterPath(); + if (!formatterPath) { + return; + } + + updateStatus(DecompStatus.Formatting, { + objectNames: fileNames, + decompConfig: path.parse(decompConfig).name, + }); + + for (const name of fileNames) { + const filePath = path.join( + getProjectRoot()?.fsPath, + "decompiler_out", + gameName, + `${name}_disasm.gc`, + ); + + const formatterArgs = ["--write", "--file", filePath]; + try { + const { stdout, stderr } = await execFileAsync( + formatterPath, + formatterArgs, + { + encoding: "utf8", + cwd: getProjectRoot()?.fsPath, + timeout: 20000, + }, + ); + channel.append(stdout.toString()); + channel.append(stderr.toString()); + } catch (error: any) { + updateStatus(DecompStatus.Errored); + channel.append( + `DECOMP ERROR:\nSTDOUT:\n${error.stdout}\nSTDERR:\n${error.stderr}`, + ); + } + } + updateStatus(DecompStatus.Idle); + } } async function getValidObjectNames(gameName: string) { diff --git a/src/languages/ir2/ir2-completions.ts b/src/languages/ir2/ir2-completions.ts index ff42c68..a450d8f 100644 --- a/src/languages/ir2/ir2-completions.ts +++ b/src/languages/ir2/ir2-completions.ts @@ -45,19 +45,21 @@ export class IRCompletionItemProvider implements vscode.CompletionItemProvider { // ! - mutated (if it's involved in a set) // ? - optional (can't easily determine this and is frankly rare) let paramFound = false; + let paramPrinted = false; for (let i = 1; i < funcBody.length; i++) { const line = funcBody[i]; if (line.includes(`(set! (-> ${arg.name}`)) { docstring += ` @param! ${arg.name} something\n`; paramFound = true; + paramPrinted = true; break; } else if (line.includes(arg.name)) { paramFound = true; } } - if (paramFound) { + if (paramFound && !paramPrinted) { docstring += ` @param ${arg.name} something\n`; - } else { + } else if (!paramPrinted) { docstring += ` @param_ ${arg.name} something\n`; } }