decomp: pre-populated list of possible types (#181)

This commit is contained in:
Tyler Wilding 2023-01-04 20:28:02 -05:00 committed by GitHub
parent 08851637e2
commit 040e941857
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 269 additions and 80 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ out/
*.exe
*.bin
lsp-metadata.json
*-types.json

View file

@ -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",

View file

@ -14,6 +14,7 @@ export function getConfig() {
eeManPagePath: configOptions.get<string>("eeManPagePath"),
vuManPagePath: configOptions.get<string>("vuManPagePath"),
decompilerPath: configOptions.get<string>("decompilerPath"),
typeSearcherPath: configOptions.get<string>("typeSearcherPath"),
jak1DecompConfig: configOptions.get<string>("decompilerJak1Config"),
jak2DecompConfig: configOptions.get<string>("decompilerJak2Config"),
decompilerJak1ConfigDirectory: configOptions.get<string>(
@ -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(

View file

@ -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;
}

View file

@ -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<string | undefined> {
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<string | undefined> {
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<string | undefined> {
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"

View file

@ -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<GameName, string[]>();
const recentLabelCasts = new Map<GameName, string[]>();
const recentTypeCasts = new Map<GameName, string[]>();
const recentStackCasts = new Map<GameName, string[]>();
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<string | undefined> {
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<number | undefined> {
const matches = [...line.matchAll(opNumRegex)];
if (matches.length == 1) {
@ -109,6 +190,51 @@ async function validActiveFile(editor: vscode.TextEditor): Promise<boolean> {
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({
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<number | undefined> {
@ -217,9 +364,30 @@ async function stackCastSelection() {
}
// Get what we should cast to
const castToType = await vscode.window.showInputBox({
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({
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

View file

@ -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 = {