decomp: add some basic renaming support for _disasm.gc files (#54)

This commit is contained in:
Tyler Wilding 2022-08-07 18:28:42 -04:00 committed by GitHub
parent ad788257ec
commit 95a7f4b125
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 315 additions and 122 deletions

View file

@ -16,6 +16,7 @@ import {
GameName,
getDirectoriesInDir,
} from "../utils/file-utils";
import { getDecompilerConfigDirectory } from "../utils/decomp-tools";
enum CastKind {
Label,
@ -45,78 +46,6 @@ class CastContext {
}
}
async function promptUserToSelectConfigDirectory(
projectRoot: vscode.Uri
): Promise<string | undefined> {
// Get all `.jsonc` files in ./decompiler/config
const dirs = await getDirectoriesInDir(
vscode.Uri.joinPath(projectRoot, "decompiler/config").fsPath
);
return await vscode.window.showQuickPick(dirs, {
title: "Config?",
});
}
async function getDecompilerConfigDirectory(
activeFile: vscode.Uri
): Promise<string | undefined> {
const projectRoot = getWorkspaceFolderByName("jak-project");
if (projectRoot === undefined) {
vscode.window.showErrorMessage(
"OpenGOAL - Unable to locate 'jak-project' workspace folder"
);
return undefined;
}
const config = getConfig();
const gameName = await determineGameFromPath(activeFile);
if (gameName == GameName.Jak1) {
if (
config.decompilerJak1ConfigDirectory === undefined ||
!existsSync(config.decompilerJak1ConfigDirectory)
) {
const selection = await promptUserToSelectConfigDirectory(projectRoot);
if (selection === undefined) {
vscode.window.showErrorMessage(
"OpenGOAL - Can't cast without knowing where to store them!"
);
return undefined;
}
await updateJak1DecompConfigDirectory(selection);
return vscode.Uri.joinPath(projectRoot, "decompiler/config/", selection)
.fsPath;
} else {
return vscode.Uri.joinPath(
projectRoot,
"decompiler/config/",
config.decompilerJak1ConfigDirectory
).fsPath;
}
} else if (gameName == GameName.Jak2) {
if (
config.decompilerJak2ConfigDirectory === undefined ||
!existsSync(config.decompilerJak2ConfigDirectory)
) {
const selection = await promptUserToSelectConfigDirectory(projectRoot);
if (selection === undefined) {
vscode.window.showErrorMessage(
"OpenGOAL - Can't cast without knowing where to store them!"
);
return undefined;
}
await updateJak2DecompConfigDirectory(selection);
return vscode.Uri.joinPath(projectRoot, "decompiler/config/", selection)
.fsPath;
} else {
return vscode.Uri.joinPath(
projectRoot,
"decompiler/config/",
config.decompilerJak2ConfigDirectory
).fsPath;
}
}
}
async function getOpNumber(line: string): Promise<number | undefined> {
const matches = [...line.matchAll(opNumRegex)];
if (matches.length == 1) {

View file

@ -8,9 +8,10 @@ import { PdfCustomProvider } from "./vendor/vscode-pdfviewer/pdfProvider";
import { switchFile } from "./utils/file-utils";
import { activateDecompTools } from "./decomp/decomp-tools";
import { getMainChannel, initContext } from "./context";
import { IRFoldingRangeProvider } from "./languages/ir2-folder";
import { IRFoldingRangeProvider } from "./languages/ir2/ir2-folder";
import { activateTypeCastTools } from "./decomp/type-caster";
import { IRInlayHintsProvider } from "./languages/ir2-inlay-hinter";
import { IRInlayHintsProvider } from "./languages/ir2/ir2-inlay-hinter";
import { OpenGOALDisasmRenameProvider } from "./languages/opengoal/disasm/opengoal-disasm-renamer";
export async function activate(context: vscode.ExtensionContext) {
try {
@ -60,6 +61,10 @@ export async function activate(context: vscode.ExtensionContext) {
{ scheme: "file", language: "opengoal-ir" },
new IRInlayHintsProvider()
);
vscode.languages.registerRenameProvider(
{ scheme: "file", language: "opengoal", pattern: "**/*_disasm.gc" },
new OpenGOALDisasmRenameProvider()
);
// Start the LSP
lsp.activate(context);

View file

@ -1,10 +1,7 @@
import { parse } from "comment-json";
import { existsSync, readFileSync } from "fs";
import path = require("path");
import * as vscode from "vscode";
import { getConfig } from "../config/config";
import { determineGameFromPath, GameName } from "../utils/file-utils";
import { getWorkspaceFolderByName } from "../utils/workspace";
import { getCastFileData } from "../../utils/decomp-tools";
import { getWorkspaceFolderByName } from "../../utils/workspace";
class HintCacheEntry {
version: number;
@ -28,7 +25,7 @@ async function getOpNumber(line: string): Promise<number | undefined> {
}
// NOTE - this is not in the LSP because i want to eventually tie commands to the hint
// to either jump to them or remove them (removing them is probably better)
// to either jump to them or remove them (removing them is probably more useful)
// Though, if i end up never doing this, this SHOULD be in the LSP, probably
// The LSP already knows the all-types file used for the file. But what would probably
@ -98,42 +95,6 @@ export class IRInlayHintsProvider implements vscode.InlayHintsProvider {
}
}
private async getCastFileData(
projectRoot: vscode.Uri,
document: vscode.TextDocument,
fileName: string
): Promise<any | undefined> {
const gameName = await determineGameFromPath(document.uri);
if (gameName === undefined) {
return undefined;
}
const config = getConfig();
let decompConfigPath = "";
if (gameName == GameName.Jak1) {
const path = config.decompilerJak1ConfigDirectory;
if (path === undefined) {
return undefined;
}
decompConfigPath = path;
} else if (gameName == GameName.Jak2) {
const path = config.decompilerJak2ConfigDirectory;
if (path === undefined) {
return undefined;
}
decompConfigPath = path;
}
const path = vscode.Uri.joinPath(
projectRoot,
`decompiler/config/${decompConfigPath}/${fileName}`
).fsPath;
if (!existsSync(path)) {
return undefined;
}
// TODO - would be performant to cache these files, requires listening to them as well though
return parse(readFileSync(path).toString(), undefined, true);
}
private async getAllPotentialStackValues(
stackCastData: any,
stackOffset: number
@ -344,19 +305,19 @@ export class IRInlayHintsProvider implements vscode.InlayHintsProvider {
return undefined;
}
const labelCastData = await this.getCastFileData(
const labelCastData = getCastFileData(
projectRoot,
document,
"label_types.jsonc"
);
const stackCastData = await this.getCastFileData(
const stackCastData = getCastFileData(
projectRoot,
document,
"stack_structures.jsonc"
);
const typeCastData = await this.getCastFileData(
const typeCastData = getCastFileData(
projectRoot,
document,
"type_casts.jsonc"

View file

@ -0,0 +1,166 @@
import { stringify } from "comment-json";
import { writeFileSync } from "fs";
import { join } from "path";
import * as vscode from "vscode";
import {
getCastFileData,
getDecompilerConfigDirectory,
} from "../../../utils/decomp-tools";
import { getWorkspaceFolderByName } from "../../../utils/workspace";
export class OpenGOALDisasmRenameProvider implements vscode.RenameProvider {
public async provideRenameEdits(
document: vscode.TextDocument,
position: vscode.Position,
newName: string,
token: vscode.CancellationToken
): Promise<vscode.WorkspaceEdit | undefined> {
const symbolRange = document.getWordRangeAtPosition(position, /[\w\-.]+/g);
if (symbolRange === undefined) {
return;
}
const symbol = document.getText(symbolRange);
// TODO - likely doesn't work on states!
// TODO - detect if its method/function args in a better way thatn just if it's on
// the same line as a defun/defmethod
const line = document.lineAt(position.line).text;
const isArgument = line.includes("defun") || line.includes("defmethod");
// If it's an argument, we have to figure out the index
let argumentIndex = undefined;
let argumentCount = undefined;
if (isArgument) {
const matches = [...line.matchAll(/(\([^\s(]*\s[^\s)]*\))/g)];
if (matches.length == 0) {
return;
}
let tempIdx = 0;
for (const match of matches[0]) {
const [argName, argType] = match
.toString()
.replace("(", "")
.replace(")", "")
.split(" ");
if (argName === symbol) {
argumentIndex = tempIdx;
argumentCount = matches.length;
break;
}
tempIdx++;
}
if (argumentIndex === undefined || argumentCount === undefined) {
return;
}
}
// Get the function this is related to (walk back until a defun/defmethod is found)
// TODO - probably could eventually replace this with an LSP integration eventually
// methods are annoying because in cast files they are referred to by the method id, not their name
let funcName = undefined;
for (let i = position.line; i > 0; i--) {
const currLine = document.lineAt(i).text;
const matches = [...currLine.matchAll(/(?:defun|defmethod)\s+([^\s]*)/g)];
if (matches.length == 1) {
// Functions are easy
if (currLine.includes("defun")) {
funcName = matches[0][1].toString();
break;
} else {
// methods are more difficult, for now we grab the info from the previous line
const prevLine = document.lineAt(i - 1).text;
const methodMatches = [
...prevLine.matchAll(
/;; definition for method (\d+) of type (.*)/g
),
];
if (matches.length == 1) {
funcName = `(method ${methodMatches[0][1]} ${methodMatches[0][2]})`;
}
break;
}
}
}
if (funcName === undefined) {
return;
}
// Update the var-names file
const projectRoot = getWorkspaceFolderByName("jak-project");
if (projectRoot === undefined) {
vscode.window.showErrorMessage(
"OpenGOAL - Unable to locate 'jak-project' workspace folder"
);
return;
}
const varNameData = getCastFileData(
projectRoot,
document,
"var_names.jsonc"
);
if (varNameData === undefined) {
return;
}
if (!(funcName in varNameData)) {
varNameData[funcName] = {};
}
if (isArgument) {
if (argumentIndex === undefined || argumentCount === undefined) {
return;
}
if ("args" in varNameData[funcName]) {
// We assume that all slots are filled up already, as this is required
varNameData[funcName].args[argumentIndex] = newName;
} else {
// Otherwise, we initialize it properly
// TODO - supporting `null` here would be nice
varNameData[funcName].args = [];
for (let i = 0; i < argumentCount; i++) {
if (i == argumentIndex) {
varNameData[funcName].args[i] = newName;
} else {
varNameData[funcName].args[i] = `arg${i}`;
}
}
}
} else {
// if "vars" is in
if ("vars" in varNameData[funcName]) {
// Check to see if the current symbol has already been renamed
let internalVar = undefined;
for (const [key, value] of Object.entries(varNameData[funcName].vars)) {
if (value === symbol) {
internalVar = key;
break;
}
}
if (internalVar !== undefined) {
varNameData[funcName].vars[internalVar] = newName;
} else {
varNameData[funcName].vars[symbol] = newName;
}
} else {
varNameData[funcName]["vars"] = {};
varNameData[funcName]["vars"][symbol] = newName;
}
}
// Write out cast file change
const configDir = await getDecompilerConfigDirectory(document.uri);
if (configDir === undefined) {
return;
}
const filePath = join(configDir, "var_names.jsonc");
writeFileSync(filePath, stringify(varNameData, null, 2));
// The actual renaming is done by the decompiler, which happens automatically if
// auto decompilation is enabled
//
// TODO - maybe it should pause here and await the file to be re-decompiled so
// make it consistent (you don't do another rename) before values have been updated.
return;
}
}

134
src/utils/decomp-tools.ts Normal file
View file

@ -0,0 +1,134 @@
import { parse } from "comment-json";
import { existsSync, readFileSync } from "fs";
import * as vscode from "vscode";
import {
getConfig,
updateJak1DecompConfigDirectory,
updateJak2DecompConfigDirectory,
} from "../config/config";
import {
determineGameFromPath,
GameName,
getDirectoriesInDir,
} from "./file-utils";
import { getWorkspaceFolderByName } from "./workspace";
export function getCastFileData(
projectRoot: vscode.Uri,
document: vscode.TextDocument,
fileName: string
): any | undefined {
const gameName = determineGameFromPath(document.uri);
if (gameName === undefined) {
return undefined;
}
const config = getConfig();
let decompConfigPath = "";
if (gameName == GameName.Jak1) {
const path = config.decompilerJak1ConfigDirectory;
if (path === undefined) {
return undefined;
}
decompConfigPath = path;
} else if (gameName == GameName.Jak2) {
const path = config.decompilerJak2ConfigDirectory;
if (path === undefined) {
return undefined;
}
decompConfigPath = path;
}
const path = vscode.Uri.joinPath(
projectRoot,
`decompiler/config/${decompConfigPath}/${fileName}`
).fsPath;
if (!existsSync(path)) {
return undefined;
}
// TODO - would be performant to cache these files, requires listening to them as well though
return parse(readFileSync(path).toString(), undefined, true);
}
async function promptUserToSelectConfigDirectory(
projectRoot: vscode.Uri
): Promise<string | undefined> {
// Get all `.jsonc` files in ./decompiler/config
const dirs = await getDirectoriesInDir(
vscode.Uri.joinPath(projectRoot, "decompiler/config").fsPath
);
return await vscode.window.showQuickPick(dirs, {
title: "Config?",
});
}
export async function getDecompilerConfigDirectory(
activeFile: vscode.Uri
): Promise<string | undefined> {
const projectRoot = getWorkspaceFolderByName("jak-project");
if (projectRoot === undefined) {
vscode.window.showErrorMessage(
"OpenGOAL - Unable to locate 'jak-project' workspace folder"
);
return undefined;
}
const config = getConfig();
const gameName = determineGameFromPath(activeFile);
if (gameName == GameName.Jak1) {
if (
config.decompilerJak1ConfigDirectory === undefined ||
!existsSync(
vscode.Uri.joinPath(
projectRoot,
"decompiler/config/",
config.decompilerJak1ConfigDirectory
).fsPath
)
) {
const selection = await promptUserToSelectConfigDirectory(projectRoot);
if (selection === undefined) {
vscode.window.showErrorMessage(
"OpenGOAL - Can't cast without knowing where to store them!"
);
return undefined;
}
await updateJak1DecompConfigDirectory(selection);
return vscode.Uri.joinPath(projectRoot, "decompiler/config/", selection)
.fsPath;
} else {
return vscode.Uri.joinPath(
projectRoot,
"decompiler/config/",
config.decompilerJak1ConfigDirectory
).fsPath;
}
} else if (gameName == GameName.Jak2) {
if (
config.decompilerJak2ConfigDirectory === undefined ||
!existsSync(
vscode.Uri.joinPath(
projectRoot,
"decompiler/config/",
config.decompilerJak2ConfigDirectory
).fsPath
)
) {
const selection = await promptUserToSelectConfigDirectory(projectRoot);
if (selection === undefined) {
vscode.window.showErrorMessage(
"OpenGOAL - Can't cast without knowing where to store them!"
);
return undefined;
}
await updateJak2DecompConfigDirectory(selection);
return vscode.Uri.joinPath(projectRoot, "decompiler/config/", selection)
.fsPath;
} else {
return vscode.Uri.joinPath(
projectRoot,
"decompiler/config/",
config.decompilerJak2ConfigDirectory
).fsPath;
}
}
}

View file

@ -39,9 +39,7 @@ export function openFile(filePath: string | undefined) {
vscode.window.showTextDocument(vscode.Uri.file(filePath));
}
export async function determineGameFromPath(
path: vscode.Uri
): Promise<GameName | undefined> {
export function determineGameFromPath(path: vscode.Uri): GameName | undefined {
if (path.fsPath.includes("jak1")) {
return GameName.Jak1;
} else if (path.fsPath.includes("jak2")) {