mirror of
https://github.com/open-goal/opengoal-vscode.git
synced 2024-10-19 20:47:37 -04:00
decomp: add some basic renaming support for _disasm.gc files (#54)
This commit is contained in:
parent
ad788257ec
commit
95a7f4b125
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
166
src/languages/opengoal/disasm/opengoal-disasm-renamer.ts
Normal file
166
src/languages/opengoal/disasm/opengoal-disasm-renamer.ts
Normal 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
134
src/utils/decomp-tools.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")) {
|
||||
|
|
Loading…
Reference in a new issue