From ccc630d0ed636dbcf9929a83a1dc8486980e8997 Mon Sep 17 00:00:00 2001 From: DaanV2 <2393905+DaanV2@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:39:43 +0200 Subject: [PATCH 01/11] refactor: fixing a whole bunch --- .gitignore | 1 + src/assembly/content.ts | 20 +++----- src/assembly/order.ts | 37 ++++++++++++++ src/documents/tracker.ts | 2 +- src/extension.ts | 2 +- src/format/uris.ts | 3 +- src/test/assembly-order.test.ts | 43 +++++++++++++++++ src/test/source-match.test.ts | 46 ++++++++++++++++++ src/view/sourceMatch.ts | 21 ++++---- src/view/sourceMatchTarget.ts | 9 ++-- src/view/webview.ts | 51 +++++--------------- src/view/webviewHtml.ts | 85 +++++++++++++++------------------ 12 files changed, 206 insertions(+), 114 deletions(-) create mode 100644 src/assembly/order.ts create mode 100644 src/test/assembly-order.test.ts diff --git a/.gitignore b/.gitignore index 0b60dfa..a61689a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist node_modules .vscode-test/ *.vsix +*.log \ No newline at end of file diff --git a/src/assembly/content.ts b/src/assembly/content.ts index 0fa70bf..19cc199 100644 --- a/src/assembly/content.ts +++ b/src/assembly/content.ts @@ -3,12 +3,13 @@ import { CommandOptions, executeCommand } from "../commands/commands"; import { goFSPath, packageUri } from "../format"; import { getFunctions } from "../go/dependencies"; import { AssemblyBlock } from "./info"; +import { prioritizeAssemblyBlocks } from "./order"; import { logger } from "../logger/logger"; export async function getAsm( goUri: Uri, env?: NodeJS.ProcessEnv, - gcflags?: string + gcflags?: string, ): Promise { logger.info("getting assembly", { uri: goUri.toString() }); @@ -28,6 +29,7 @@ export async function getAsm( const popts = { location: ProgressLocation.Window, cancellable: true, + title: "compiling go for assembly", }; const info = await window.withProgress(popts, async (progress, token) => { @@ -37,7 +39,7 @@ export async function getAsm( "go", ["build", gcflags ?? "-gcflags=-S", packageDir], options, - token + token, ); if ((result.code ?? 0) > 0) { @@ -53,19 +55,10 @@ export async function getAsm( progress.report({ message: "parsing..." }); let info = AssemblyBlock.parse(result.stderr); - // Filter only the functions from the file, recording their declaration order. + // Keep package assembly available so the view can reveal other-file code. progress.report({ message: "filtering..." }); const funcs = await getFunctions(goUri); - info = info.flatMap((block) => { - const idx = funcs.findIndex((f) => block.header.includes(f)); - if (idx === -1) { - return []; - } - block.sortIndex = idx; - return [block]; - }); - - info.sort((a, b) => (a.sortIndex ?? 0) - (b.sortIndex ?? 0)); + info = prioritizeAssemblyBlocks(info, funcs); // Done progress.report({ message: "done" }); @@ -86,7 +79,6 @@ function printAssemblyInfo(packageDir: string, cwd: string | undefined) { return function (asm: AssemblyBlock): string { const header = asm.header.replaceAll(" ", "\n#\t"); - let lines = asm.data.map((e) => e.replaceAll(packageDir, "")); if (cwd) { diff --git a/src/assembly/order.ts b/src/assembly/order.ts new file mode 100644 index 0000000..0ec6371 --- /dev/null +++ b/src/assembly/order.ts @@ -0,0 +1,37 @@ +import { AssemblyBlock } from "./info"; + +export function prioritizeAssemblyBlocks( + blocks: AssemblyBlock[], + currentFileFunctions: string[], +): AssemblyBlock[] { + return blocks + .map((block, originalIndex) => ({ + block, + originalIndex, + currentFileIndex: currentFileFunctions.findIndex((name) => + block.header.includes(name), + ), + })) + .sort((left, right) => { + const leftIsCurrent = left.currentFileIndex !== -1; + const rightIsCurrent = right.currentFileIndex !== -1; + + if (leftIsCurrent !== rightIsCurrent) { + return leftIsCurrent ? -1 : 1; + } + + if ( + leftIsCurrent && + rightIsCurrent && + left.currentFileIndex !== right.currentFileIndex + ) { + return left.currentFileIndex - right.currentFileIndex; + } + + return left.originalIndex - right.originalIndex; + }) + .map(({ block, currentFileIndex }) => ({ + ...block, + sortIndex: currentFileIndex >= 0 ? currentFileIndex : undefined, + })); +} \ No newline at end of file diff --git a/src/documents/tracker.ts b/src/documents/tracker.ts index a01bf62..c820a66 100644 --- a/src/documents/tracker.ts +++ b/src/documents/tracker.ts @@ -33,7 +33,7 @@ export class DocumentTracker { const result: [Uri, AssemblyView][] = []; for (const entry of this._openFiles.entries()) { - if (entry[0].fsPath.includes(packageUri.fsPath)) { + if (entry[0].path.startsWith(packageUri.path)) { result.push(entry); } } diff --git a/src/extension.ts b/src/extension.ts index 3f5db08..32c12fd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -60,7 +60,7 @@ function init(context: vscode.ExtensionContext) { ), // Events vscode.workspace.onDidSaveTextDocument((e) => { - if (!e.uri.fsPath.endsWith(".go")) { + if (!e.uri.path.endsWith(".go")) { return; } logger.info("file saved, checking for assembly updates", { event: e }); diff --git a/src/format/uris.ts b/src/format/uris.ts index 29b63eb..d0c1a6f 100644 --- a/src/format/uris.ts +++ b/src/format/uris.ts @@ -1,4 +1,3 @@ -import path from "path"; import { Uri } from "vscode"; export function goFSPath(uri: Uri | string): string { @@ -16,5 +15,5 @@ export function packageUri(uri: Uri): Uri { } export function filename(uri: Uri): string { - return path.basename(uri.fsPath); + return uri.path.split("/").pop() ?? ""; } diff --git a/src/test/assembly-order.test.ts b/src/test/assembly-order.test.ts new file mode 100644 index 0000000..5968042 --- /dev/null +++ b/src/test/assembly-order.test.ts @@ -0,0 +1,43 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { prioritizeAssemblyBlocks } from "../assembly/order"; + +describe("prioritizeAssemblyBlocks", () => { + it("keeps current-file functions first without dropping other blocks", () => { + const blocks = [ + { header: "pkg.Helper STEXT", data: ["RET"] }, + { header: "pkg.CurrentOne STEXT", data: ["RET"] }, + { header: "pkg.Other STEXT", data: ["RET"] }, + { header: "pkg.CurrentTwo STEXT", data: ["RET"] }, + ]; + + const ordered = prioritizeAssemblyBlocks(blocks, [ + "CurrentOne", + "CurrentTwo", + ]); + + assert.deepStrictEqual( + ordered.map((block) => block.header), + [ + "pkg.CurrentOne STEXT", + "pkg.CurrentTwo STEXT", + "pkg.Helper STEXT", + "pkg.Other STEXT", + ], + ); + }); + + it("preserves original order when no current-file functions match", () => { + const blocks = [ + { header: "pkg.First STEXT", data: ["RET"] }, + { header: "pkg.Second STEXT", data: ["RET"] }, + ]; + + const ordered = prioritizeAssemblyBlocks(blocks, ["Missing"]); + + assert.deepStrictEqual( + ordered.map((block) => block.header), + ["pkg.First STEXT", "pkg.Second STEXT"], + ); + }); +}); \ No newline at end of file diff --git a/src/test/source-match.test.ts b/src/test/source-match.test.ts index 0a68b88..22e2d0e 100644 --- a/src/test/source-match.test.ts +++ b/src/test/source-match.test.ts @@ -51,4 +51,50 @@ describe("sourceMatch", () => { assert.strictEqual(matchesSourceFile("pkg/bar.go", target), false); }); + + // Windows-style paths from Go compiler output (e.g. C:/Users/...) + it("matches windows absolute path in asm output", () => { + const target: SourceFileMatchTarget = { + absolute: "c:/users/me/work/mod/pkg/foo.go", + relative: "pkg/foo.go", + basename: "foo.go", + }; + + assert.strictEqual(matchesSourceFile("C:\\Users\\me\\work\\mod\\pkg\\foo.go", target), true); + }); + + it("matches windows workspace-relative path in asm output", () => { + const target: SourceFileMatchTarget = { + absolute: "c:/users/me/work/mod/pkg/foo.go", + relative: "pkg/foo.go", + basename: "foo.go", + }; + + assert.strictEqual(matchesSourceFile(".\\pkg\\foo.go", target), true); + }); + + it("matches when asm has windows drive-letter path and target is unix-normalized", () => { + const target: SourceFileMatchTarget = { + absolute: "users/me/work/mod/pkg/foo.go", + relative: "pkg/foo.go", + basename: "foo.go", + }; + + // Go on Windows may emit absolute paths like C:/Users/... + assert.strictEqual(matchesSourceFile("C:/Users/me/work/mod/pkg/foo.go", target), true); + }); + + it("normalizes windows drive letter", () => { + assert.strictEqual( + normalizePathForCompare("C:\\Users\\Me\\Work\\pkg\\foo.go"), + "users/me/work/pkg/foo.go", + ); + }); + + it("normalizes unix absolute path", () => { + assert.strictEqual( + normalizePathForCompare("/home/me/work/pkg/foo.go"), + "home/me/work/pkg/foo.go", + ); + }); }); diff --git a/src/view/sourceMatch.ts b/src/view/sourceMatch.ts index 8fbfbe7..73ec0df 100644 --- a/src/view/sourceMatch.ts +++ b/src/view/sourceMatch.ts @@ -1,5 +1,3 @@ -import path from "path"; - export interface SourceFileMatchTarget { absolute: string; relative?: string; @@ -9,6 +7,7 @@ export interface SourceFileMatchTarget { export function normalizePathForCompare(file: string): string { return file .replace(/\\/g, "/") + .replace(/^[a-z]:/i, "") .replace(/^\.\//, "") .replace(/^\/+/, "") .toLowerCase(); @@ -19,22 +18,28 @@ export function matchesSourceFile( target: SourceFileMatchTarget, ): boolean { const source = normalizePathForCompare(sourceFile); + const absolute = normalizePathForCompare(target.absolute); + const relative = target.relative + ? normalizePathForCompare(target.relative) + : undefined; + const basename = normalizePathForCompare(target.basename); + if (!source) { return false; } - if (source === target.absolute || target.absolute.endsWith("/" + source)) { + if (source === absolute || absolute.endsWith("/" + source)) { return true; } if ( - target.relative && - (source === target.relative || - source.endsWith("/" + target.relative) || - target.relative.endsWith("/" + source)) + relative && + (source === relative || + source.endsWith("/" + relative) || + relative.endsWith("/" + source)) ) { return true; } - return path.posix.basename(source) === target.basename; + return (source.split("/").pop() ?? "") === basename; } diff --git a/src/view/sourceMatchTarget.ts b/src/view/sourceMatchTarget.ts index 97f7090..08beacc 100644 --- a/src/view/sourceMatchTarget.ts +++ b/src/view/sourceMatchTarget.ts @@ -1,13 +1,14 @@ -import path from "path"; import { Uri, workspace } from "vscode"; import { normalizePathForCompare, SourceFileMatchTarget } from "./sourceMatch"; export function createSourceMatchTarget(fileUri: Uri): SourceFileMatchTarget { - const absolute = normalizePathForCompare(fileUri.fsPath); - const basename = normalizePathForCompare(path.basename(fileUri.fsPath)); + const absolute = normalizePathForCompare(fileUri.path); + const basename = normalizePathForCompare(fileUri.path.split("/").pop() ?? ""); const ws = workspace.getWorkspaceFolder(fileUri); const relative = ws - ? normalizePathForCompare(path.relative(ws.uri.fsPath, fileUri.fsPath)) + ? normalizePathForCompare( + fileUri.path.substring(ws.uri.path.length).replace(/^\//, ""), + ) : undefined; return { diff --git a/src/view/webview.ts b/src/view/webview.ts index c8ad439..454250a 100644 --- a/src/view/webview.ts +++ b/src/view/webview.ts @@ -7,7 +7,6 @@ import { ViewColumn, WebviewPanel, window, - workspace, } from "vscode"; import { getAsm } from "../assembly"; import { filename } from "../format"; @@ -17,12 +16,9 @@ import { createSourceMatchTarget } from "./sourceMatchTarget"; import { matchesSourceFile, SourceFileMatchTarget } from "./sourceMatch"; import { getHtml } from "./webviewHtml"; -function findSourceEditor( - srcFile: string, - sourceFileUri: Uri, -) { +function findSourceEditor(srcFile: string, sourceFileUri: Uri) { const primaryEditor = window.visibleTextEditors.find( - (e) => e.document.uri.fsPath === sourceFileUri.fsPath, + (e) => e.document.uri.toString() === sourceFileUri.toString(), ); return window.visibleTextEditors.find( @@ -60,11 +56,13 @@ export class AssemblyView implements Disposable { ViewColumn.Beside, { enableScripts: true }, ); - this.panel.webview.html = getHtml( - "loading...", - "", - new Map(), - this.sourceMatchTarget, + this.panel.webview.html = "loading..."; + getHtml("loading...", "", new Map(), this.sourceMatchTarget).then( + (html) => { + if (this.panel.webview.html === "loading...") { + this.panel.webview.html = html; + } + }, ); // Handle messages from the webview (ASM hover → source highlight) @@ -77,7 +75,7 @@ export class AssemblyView implements Disposable { // Handle cursor changes in source editor (source → ASM highlight) this._disposables.push( window.onDidChangeTextEditorSelection((e) => { - if (e.textEditor.document.uri.fsPath === this.fileUri.fsPath) { + if (e.textEditor.document.uri.toString() === this.fileUri.toString()) { this._syncFromSource(e.textEditor.selection.active.line + 1); } }), @@ -103,21 +101,21 @@ export class AssemblyView implements Disposable { const asm = await getAsm( this.fileUri, this.envManager.getEnvVars(), - this.envManager.getGcFlags() + this.envManager.getGcFlags(), ); const { lineToSource, sourceToLines } = buildLineMaps( asm, this.sourceMatchTarget, ); this._sourceToLines = sourceToLines; - this.panel.webview.html = getHtml( + this.panel.webview.html = await getHtml( asm, this.filename, lineToSource, this.sourceMatchTarget, ); } catch (err: any) { - this.panel.webview.html = getHtml( + this.panel.webview.html = await getHtml( `got an error: ${JSON.stringify({ ...err }, undefined, 2)}`, this.filename, new Map(), @@ -139,30 +137,7 @@ export class AssemblyView implements Disposable { } } else if (m["type"] === "hoverEnd") { this._clearSourceHighlight(); - } else if (m["type"] === "export") { - const content = m["content"]; - if (typeof content === "string") { - this._exportAssembly(content); - } - } - } - - private async _exportAssembly(content: string) { - const defaultUri = Uri.file(this.filename.replace(/\.go$/, ".s")); - const uri = await window.showSaveDialog({ - defaultUri, - filters: { - "Assembly files": ["s"], - "Text files": ["txt"], - "All files": ["*"], - }, - saveLabel: "Export Assembly", - }); - if (!uri) { - return; } - await workspace.fs.writeFile(uri, new TextEncoder().encode(content)); - window.showInformationMessage(`Assembly exported to ${uri.fsPath}`); } private _highlightSourceLine(srcFile: string, srcLine: number) { diff --git a/src/view/webviewHtml.ts b/src/view/webviewHtml.ts index cd7684c..a297ae8 100644 --- a/src/view/webviewHtml.ts +++ b/src/view/webviewHtml.ts @@ -1,5 +1,7 @@ +import { logger } from "../logger/logger"; import { SourceRef } from "./lineMaps"; import { SourceFileMatchTarget, matchesSourceFile } from "./sourceMatch"; +import { ProgressLocation, window } from "vscode"; interface RenderBlock { header: string; @@ -16,11 +18,12 @@ function escapeHtml(text: string): string { function syntaxHighlight(escaped: string): string { return escaped - .replace(/(0x[0-9a-f]+)/g, '$1') + .replace(/(0x[0-9a-fA-F]+)/g, '$1') .replace( - /\b(AX|AL|BX|CX|DX|SI|DI|R[0-9]+|SP|SB|BP)\b/g, + /\b(AX|AL|BX|BL|CX|CL|DX|DL|SI|DI|R[0-9]+|SP|SB|BP|FP|PC|TLS)\b/g, '$1', ) + .replace(/(\$-?\d+)/g, '$1') .replace(/\b([A-Z]{3,})\b/g, '$1') .replace(/\(([^)]+\.go:\d+)\)/g, '($1)') .replace(/(#.*$|\/\/.*$)/g, '$1'); @@ -94,7 +97,10 @@ function renderBodyContent(content: RenderBlock[] | string[]): string { .join("\n"); } -function renderHtml(filename: string, content: RenderBlock[] | string[]): string { +function renderHtml( + filename: string, + content: RenderBlock[] | string[], +): string { const bodyContent = renderBodyContent(content); return ` @@ -103,8 +109,9 @@ function renderHtml(filename: string, content: RenderBlock[] | string[]): string