From 040e941857427301e2670cfe6752cd942579c167 Mon Sep 17 00:00:00 2001 From: Tyler Wilding Date: Wed, 4 Jan 2023 20:28:02 -0500 Subject: [PATCH] decomp: pre-populated list of possible types (#181) --- .gitignore | 1 + package.json | 8 ++ src/config/config.ts | 10 ++ src/context.ts | 16 +++ src/decomp/decomp-tools.ts | 96 ++++++----------- src/decomp/type-caster.ts | 214 ++++++++++++++++++++++++++++++++++--- src/utils/file-utils.ts | 4 +- 7 files changed, 269 insertions(+), 80 deletions(-) diff --git a/.gitignore b/.gitignore index 28c3761..d2598ed 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ out/ *.exe *.bin lsp-metadata.json +*-types.json diff --git a/package.json b/package.json index c57f442..a75043a 100644 --- a/package.json +++ b/package.json @@ -222,6 +222,14 @@ "default": null, "description": "File path to the decompiler executable" }, + "opengoal.typeSearcherPath": { + "type": [ + "string", + "null" + ], + "default": null, + "description": "File path to the type searcher executable" + }, "opengoal.decompilerJak1Config": { "type": [ "string", diff --git a/src/config/config.ts b/src/config/config.ts index 45e7575..1017f49 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -14,6 +14,7 @@ export function getConfig() { eeManPagePath: configOptions.get("eeManPagePath"), vuManPagePath: configOptions.get("vuManPagePath"), decompilerPath: configOptions.get("decompilerPath"), + typeSearcherPath: configOptions.get("typeSearcherPath"), jak1DecompConfig: configOptions.get("decompilerJak1Config"), jak2DecompConfig: configOptions.get("decompilerJak2Config"), decompilerJak1ConfigDirectory: configOptions.get( @@ -75,6 +76,15 @@ export async function updateDecompilerPath(path: string) { ); } +export async function updateTypeSearcherPath(path: string) { + const userConfig = vscode.workspace.getConfiguration(); + await userConfig.update( + "opengoal.typeSearcherPath", + path, + vscode.ConfigurationTarget.Global + ); +} + export async function updateJak1DecompConfig(config: string) { const userConfig = vscode.workspace.getConfiguration(); await userConfig.update( diff --git a/src/context.ts b/src/context.ts index 0825615..9e7750f 100644 --- a/src/context.ts +++ b/src/context.ts @@ -2,10 +2,12 @@ import * as vscode from "vscode"; import { RecentFiles } from "./RecentFiles"; +import { getWorkspaceFolderByName } from "./utils/workspace"; const channel = vscode.window.createOutputChannel("OpenGOAL"); let extensionContext: vscode.ExtensionContext; let recentFiles: RecentFiles; +let projectRoot: vscode.Uri | undefined = undefined; export function initContext(extContext: vscode.ExtensionContext) { extensionContext = extContext; @@ -27,3 +29,17 @@ export function getExtensionContext() { export function getMainChannel() { return channel; } + +export function getProjectRoot(): vscode.Uri { + if (projectRoot === undefined) { + projectRoot = getWorkspaceFolderByName("jak-project"); + // if it's still undefined, throw an error + if (projectRoot === undefined) { + vscode.window.showErrorMessage( + "OpenGOAL - Unable to locate 'jak-project' workspace folder" + ); + throw new Error("unable to locate 'jak-project' workspace folder"); + } + } + return projectRoot; +} diff --git a/src/decomp/decomp-tools.ts b/src/decomp/decomp-tools.ts index cb9a767..1f08351 100644 --- a/src/decomp/decomp-tools.ts +++ b/src/decomp/decomp-tools.ts @@ -1,7 +1,7 @@ import { exec, execFile } from "child_process"; import { existsSync, promises as fs } from "fs"; import * as vscode from "vscode"; -import { determineGameFromPath, GameName, openFile } from "../utils/file-utils"; +import { determineGameFromPath, GameName } from "../utils/file-utils"; import { open_in_pdf } from "./man-page"; import * as util from "util"; import { @@ -12,22 +12,19 @@ import { } from "../config/config"; import * as path from "path"; import * as glob from "glob"; -import { getExtensionContext, getRecentFiles } from "../context"; +import { getExtensionContext, getProjectRoot } from "../context"; import { getFileNamesFromUris, getUrisFromTabs, - getWorkspaceFolderByName, truncateFileNameEndings, } from "../utils/workspace"; import { activateDecompTypeSearcher } from "./type-searcher/type-searcher"; +import { updateTypeCastSuggestions } from "./type-caster"; const globAsync = util.promisify(glob); const execFileAsync = util.promisify(execFile); const execAsync = util.promisify(exec); -// Put some of this stuff into the context -let projectRoot: vscode.Uri | undefined = undefined; - let channel: vscode.OutputChannel; let fsWatcher: vscode.FileSystemWatcher | undefined; @@ -102,27 +99,19 @@ async function promptUserToSelectConfig( async function getDecompilerConfig( gameName: GameName ): Promise { - if (projectRoot === undefined) { - projectRoot = getWorkspaceFolderByName("jak-project"); - if (projectRoot === undefined) { - vscode.window.showErrorMessage( - "OpenGOAL - Unable to locate 'jak-project' workspace folder" - ); - return undefined; - } - } - const config = getConfig(); if (gameName == GameName.Jak1) { const decompConfig = config.jak1DecompConfig; if ( decompConfig === undefined || !existsSync( - vscode.Uri.joinPath(projectRoot, `decompiler/config/${decompConfig}`) - .fsPath + vscode.Uri.joinPath( + getProjectRoot(), + `decompiler/config/${decompConfig}` + ).fsPath ) ) { - const config = await promptUserToSelectConfig(projectRoot); + const config = await promptUserToSelectConfig(getProjectRoot()); if (config === undefined) { return; } else { @@ -137,11 +126,13 @@ async function getDecompilerConfig( if ( decompConfig === undefined || !existsSync( - vscode.Uri.joinPath(projectRoot, `decompiler/config/${decompConfig}`) - .fsPath + vscode.Uri.joinPath( + getProjectRoot(), + `decompiler/config/${decompConfig}` + ).fsPath ) ) { - const config = await promptUserToSelectConfig(projectRoot); + const config = await promptUserToSelectConfig(getProjectRoot()); if (config === undefined) { return; } else { @@ -156,16 +147,6 @@ async function getDecompilerConfig( } async function checkDecompilerPath(): Promise { - if (projectRoot === undefined) { - projectRoot = getWorkspaceFolderByName("jak-project"); - if (projectRoot === undefined) { - vscode.window.showErrorMessage( - "OpenGOAL - Unable to locate 'jak-project' workspace folder" - ); - return undefined; - } - } - let decompilerPath = getConfig().decompilerPath; // Look for the decompiler if the path isn't set or the file is now missing @@ -173,7 +154,10 @@ async function checkDecompilerPath(): Promise { return decompilerPath; } - const potentialPath = vscode.Uri.joinPath(projectRoot, defaultDecompPath()); + const potentialPath = vscode.Uri.joinPath( + getProjectRoot(), + defaultDecompPath() + ); if (existsSync(potentialPath.fsPath)) { decompilerPath = potentialPath.fsPath; } else { @@ -221,7 +205,7 @@ async function decompFiles(decompConfig: string, fileNames: string[]) { ], { encoding: "utf8", - cwd: projectRoot?.fsPath, + cwd: getProjectRoot()?.fsPath, timeout: 20000, } ); @@ -239,16 +223,9 @@ async function decompFiles(decompConfig: string, fileNames: string[]) { } async function getValidObjectNames(gameName: string) { - if (projectRoot === undefined) { - projectRoot = getWorkspaceFolderByName("jak-project"); - if (projectRoot === undefined) { - return undefined; - } - } - // Look for the `all_objs.json` file const objsPath = path.join( - projectRoot.fsPath, + getProjectRoot().fsPath, "goal_src", gameName, "build", @@ -415,7 +392,14 @@ function toggleAutoDecompilation() { fsWatcher = vscode.workspace.createFileSystemWatcher( "**/decompiler/config/**/*.{jsonc,json,gc}" ); - fsWatcher.onDidChange(() => decompAllActiveFiles()); + fsWatcher.onDidChange((uri: vscode.Uri) => { + decompAllActiveFiles(); + // Also update list of types for that game + const gameName = determineGameFromPath(uri); + if (gameName !== undefined) { + updateTypeCastSuggestions(gameName); + } + }); fsWatcher.onDidCreate(() => decompAllActiveFiles()); fsWatcher.onDidDelete(() => decompAllActiveFiles()); } else { @@ -434,16 +418,6 @@ async function updateSourceFile() { return; } - if (projectRoot === undefined) { - projectRoot = getWorkspaceFolderByName("jak-project"); - if (projectRoot === undefined) { - vscode.window.showErrorMessage( - "OpenGOAL - Unable to locate 'jak-project' workspace folder" - ); - return undefined; - } - } - let fileName = path.basename(editor.document.fileName); let disasmFilePath = ""; if (fileName.match(/.*_ir2\.asm/)) { @@ -468,7 +442,7 @@ async function updateSourceFile() { `python ./scripts/gsrc/update-from-decomp.py --game ${gameName} --file ${fileName}`, { encoding: "utf8", - cwd: projectRoot?.fsPath, + cwd: getProjectRoot()?.fsPath, timeout: 20000, } ); @@ -486,16 +460,6 @@ async function updateReferenceTest() { return; } - if (projectRoot === undefined) { - projectRoot = getWorkspaceFolderByName("jak-project"); - if (projectRoot === undefined) { - vscode.window.showErrorMessage( - "OpenGOAL - Unable to locate 'jak-project' workspace folder" - ); - return undefined; - } - } - // TODO - duplication with above let fileName = path.basename(editor.document.fileName); @@ -518,7 +482,7 @@ async function updateReferenceTest() { gameName = "jak2"; } const folderToSearch = vscode.Uri.joinPath( - projectRoot, + getProjectRoot(), `goal_src/${gameName}` ); const files = await globAsync(`**/${fileName}.gc`, { @@ -530,7 +494,7 @@ async function updateReferenceTest() { } const refTestPath = vscode.Uri.joinPath( - projectRoot, + getProjectRoot(), `test/decompiler/reference/${gameName}/${files[0].replace( ".gc", "_REF.gc" diff --git a/src/decomp/type-caster.ts b/src/decomp/type-caster.ts index 0acd9a4..c589d4c 100644 --- a/src/decomp/type-caster.ts +++ b/src/decomp/type-caster.ts @@ -1,10 +1,16 @@ -import { getExtensionContext } from "../context"; +import { getExtensionContext, getProjectRoot } from "../context"; import * as vscode from "vscode"; import { basename, join } from "path"; -import { readFileSync, writeFileSync } from "fs"; +import { fstat, readFileSync, writeFileSync } from "fs"; import { parse, stringify } from "comment-json"; import { getFuncNameFromSelection } from "../languages/ir2/ir2-utils"; import { getDecompilerConfigDirectory } from "./utils"; +import { determineGameFromPath, GameName } from "../utils/file-utils"; +import { getConfig, updateTypeSearcherPath } from "../config/config"; +import { existsSync } from "fs"; +import * as util from "util"; +import { execFile } from "child_process"; +const execFileAsync = util.promisify(execFile); enum CastKind { Label, @@ -33,6 +39,81 @@ class CastContext { } } +const typeCastSuggestions = new Map(); +const recentLabelCasts = new Map(); +const recentTypeCasts = new Map(); +const recentStackCasts = new Map(); + +function defaultTypeSearcherPath() { + const platform = process.platform; + if (platform == "win32") { + return "out/build/Release/bin/type_searcher.exe"; + } else { + return "build/tools/type_searcher"; + } +} + +async function checkTypeSearcherPath(): Promise { + let typeSearcherPath = getConfig().typeSearcherPath; + + // Look for the decompiler if the path isn't set or the file is now missing + if (typeSearcherPath !== undefined && existsSync(typeSearcherPath)) { + return typeSearcherPath; + } + + const potentialPath = vscode.Uri.joinPath( + getProjectRoot(), + defaultTypeSearcherPath() + ); + if (existsSync(potentialPath.fsPath)) { + typeSearcherPath = potentialPath.fsPath; + } else { + // Ask the user to find it cause we have no idea + const path = await vscode.window.showOpenDialog({ + canSelectMany: false, + openLabel: "Select Type Searcher", + title: "Provide the type searcher executable's path", + }); + if (path === undefined || path.length == 0) { + return undefined; + } + typeSearcherPath = path[0].fsPath; + } + updateTypeSearcherPath(typeSearcherPath); + return typeSearcherPath; +} + +export async function updateTypeCastSuggestions(gameName: GameName) { + const typeSearcherPath = await checkTypeSearcherPath(); + if (!typeSearcherPath) { + return; + } + + try { + const jsonPath = vscode.Uri.joinPath( + getExtensionContext().extensionUri, + `${gameName.toString()}-types.json` + ).fsPath; + await execFileAsync( + typeSearcherPath, + [`--game`, gameName.toString(), `--output-path`, jsonPath, `--all`], + { + encoding: "utf8", + cwd: getProjectRoot().fsPath, + timeout: 20000, + } + ); + if (existsSync(jsonPath)) { + const result = readFileSync(jsonPath, { encoding: "utf-8" }); + typeCastSuggestions.set(gameName, JSON.parse(result)); + } + } catch (error: any) { + vscode.window.showErrorMessage( + "Couldn't get a list of all types to use for casting suggestions" + ); + } +} + async function getOpNumber(line: string): Promise { const matches = [...line.matchAll(opNumRegex)]; if (matches.length == 1) { @@ -109,6 +190,51 @@ async function validActiveFile(editor: vscode.TextEditor): Promise { return true; } +function generateCastSelectionItems( + fullList: string[] | undefined, + recentList: string[] | undefined +): vscode.QuickPickItem[] { + const items: vscode.QuickPickItem[] = []; + if (recentList !== undefined && recentList.length > 0) { + items.push({ + label: "Recent Casts", + kind: vscode.QuickPickItemKind.Separator, + }); + for (const name of recentList) { + items.push({ + label: name, + }); + } + } + if (fullList !== undefined && fullList.length > 0) { + items.push({ + label: "All Types", + kind: vscode.QuickPickItemKind.Separator, + }); + for (const name of fullList) { + items.push({ + label: name, + }); + } + } + return items; +} + +async function initTypeCastSuggestions(gameName: GameName | undefined) { + if (gameName !== undefined && typeCastSuggestions.size === 0) { + await updateTypeCastSuggestions(gameName); + if (recentLabelCasts.get(gameName) === undefined) { + recentLabelCasts.set(gameName, []); + } + if (recentTypeCasts.get(gameName) === undefined) { + recentTypeCasts.set(gameName, []); + } + if (recentStackCasts.get(gameName) === undefined) { + recentStackCasts.set(gameName, []); + } + } +} + async function labelCastSelection() { const editor = vscode.window.activeTextEditor; if (editor === undefined || !validActiveFile(editor)) { @@ -126,14 +252,34 @@ async function labelCastSelection() { } // Get what we should cast to - const castToType = await vscode.window.showInputBox({ - title: "Cast to Type?", - }); + const gameName = determineGameFromPath(editor.document.uri); + await initTypeCastSuggestions(gameName); + if (gameName === undefined) { + await vscode.window.showErrorMessage("Couldn't determine game version"); + return; + } + + const items = generateCastSelectionItems( + typeCastSuggestions.get(gameName), + recentLabelCasts.get(gameName) + ); + let castToType; + if (items.length > 0) { + castToType = ( + await vscode.window.showQuickPick(items, { + title: "Cast to Type?", + }) + )?.label; + } else { + castToType = await vscode.window.showInputBox({ + title: "Cast to Type?", + }); + } + if (castToType === undefined || castToType.trim() === "") { await vscode.window.showErrorMessage("Can't cast if no type is provided"); return; } - // If the label is a pointer, ask for a size let pointerSize = undefined; if (castToType.includes("pointer")) { @@ -159,6 +305,7 @@ async function labelCastSelection() { lastCastKind = CastKind.Label; lastLabelCastType = castToType; lastLabelCastSize = pointerSize; + recentLabelCasts.get(gameName)?.unshift(castToType); } async function getStackOffset(line: string): Promise { @@ -217,9 +364,30 @@ async function stackCastSelection() { } // Get what we should cast to - const castToType = await vscode.window.showInputBox({ - title: "Cast to Type?", - }); + const gameName = determineGameFromPath(editor.document.uri); + await initTypeCastSuggestions(gameName); + if (gameName === undefined) { + await vscode.window.showErrorMessage("Couldn't determine game version"); + return; + } + + const items = generateCastSelectionItems( + typeCastSuggestions.get(gameName), + recentStackCasts.get(gameName) + ); + let castToType; + if (items.length > 0) { + castToType = ( + await vscode.window.showQuickPick(items, { + title: "Cast to Type?", + }) + )?.label; + } else { + castToType = await vscode.window.showInputBox({ + title: "Cast to Type?", + }); + } + if (castToType === undefined || castToType.trim() === "") { await vscode.window.showErrorMessage("Can't cast if no type is provided"); return; @@ -230,6 +398,7 @@ async function stackCastSelection() { lastCastKind = CastKind.Stack; lastStackCastType = castToType; + recentStackCasts.get(gameName)?.unshift(castToType); } function getRegisters( @@ -341,9 +510,29 @@ async function typeCastSelection() { } // Get what we should cast to - const castToType = await vscode.window.showInputBox({ - title: "Cast to Type?", - }); + const gameName = determineGameFromPath(editor.document.uri); + await initTypeCastSuggestions(gameName); + if (gameName === undefined) { + await vscode.window.showErrorMessage("Couldn't determine game version"); + return; + } + + const items = generateCastSelectionItems( + typeCastSuggestions.get(gameName), + recentTypeCasts.get(gameName) + ); + let castToType; + if (items.length > 0) { + castToType = ( + await vscode.window.showQuickPick(items, { + title: "Cast to Type?", + }) + )?.label; + } else { + castToType = await vscode.window.showInputBox({ + title: "Cast to Type?", + }); + } if (castToType === undefined || castToType.trim() === "") { await vscode.window.showErrorMessage("Can't cast if no type is provided"); return; @@ -361,6 +550,7 @@ async function typeCastSelection() { lastCastKind = CastKind.TypeCast; lastTypeCastRegister = registerSelection; lastTypeCastType = castToType; + recentTypeCasts.get(gameName)?.unshift(castToType); } // Execute the same cast as last time (same type, same register) just on a different selection diff --git a/src/utils/file-utils.ts b/src/utils/file-utils.ts index 16bb2b1..b34aa21 100644 --- a/src/utils/file-utils.ts +++ b/src/utils/file-utils.ts @@ -4,8 +4,8 @@ import { promises as fs } from "fs"; import { getRecentFiles } from "../context"; export enum GameName { - Jak1, - Jak2, + Jak1 = "jak1", + Jak2 = "jak2", } const fileSwitchingAssoc = {