Split Code

This commit is contained in:
brice.boisson
2025-10-22 22:21:04 +02:00
parent d88c95e3fe
commit 0d91be6664
4 changed files with 390 additions and 442 deletions

39
src/eda_connect.ts Normal file
View File

@@ -0,0 +1,39 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { promises as fsp } from 'fs';
export async function sendAndAwait(
sharedDir: string,
command: string,
timeoutMs: number
): Promise<string | null> {
const cfg = vscode.workspace.getConfiguration('modelsimWave');
const keep = cfg.get<boolean>('debugKeepResults') === true;
const id = `${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
const resultsPath = path.join(sharedDir, `modelsim_results_${id}.txt`);
const commandsPath = path.join(sharedDir, 'modelsim_commands.txt');
const line = `${id}|${resultsPath}|${command}\n`;
await fsp.appendFile(commandsPath, line, 'utf8');
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const buf = await fsp.readFile(resultsPath);
const txt = buf.toString('utf8');
if (!keep) {
try { await fsp.unlink(resultsPath); } catch {}
}
return txt;
} catch {
await sleep(200);
}
}
return null;
}
function sleep(ms: number) {
return new Promise(r => setTimeout(r, ms));
}

View File

@@ -2,14 +2,22 @@ import * as vscode from 'vscode';
import * as path from 'path'; import * as path from 'path';
import { promises as fsp } from 'fs'; import { promises as fsp } from 'fs';
import { MyWebviewViewProvider } from './wave_action';
import { sendAndAwait } from './eda_connect';
import { requestModuleTree, addWave } from './wave_editor';
// Tracks the most recent hover position per document, with a timestamp. // Tracks the most recent hover position per document, with a timestamp.
const lastHoverByDoc = new Map<string, { pos: vscode.Position; at: number }>(); export const lastHoverByDoc = new Map<string, { pos: vscode.Position; at: number }>();
const HOVER_FRESH_MS = 2000; export const HOVER_FRESH_MS = 2000;
// Hold MANY instance paths per module name, e.g. queue_slot -> ["/test/...[0]", "/test/...[1]", ...] // Hold MANY instance paths per module name, e.g. queue_slot -> ["/test/...[0]", "/test/...[1]", ...]
let moduleTree: Record<string, { module: string[], last: number }> = {};
let waveMode = false; const svSelectors: vscode.DocumentSelector = [
{ language: 'systemverilog', scheme: 'file' },
{ language: 'verilog', scheme: 'file' }
];
export let waveMode = false;
let statusItem: vscode.StatusBarItem; let statusItem: vscode.StatusBarItem;
function toggleWaveMode() { function toggleWaveMode() {
@@ -20,149 +28,6 @@ function toggleWaveMode() {
: 'Edit Mode — click to switch to Wave Debug Mode'; : 'Edit Mode — click to switch to Wave Debug Mode';
} }
// vscode.commands.registerCommand("myext.ctrlW", async () => {
// if (waveMode) {
// await sendToModelSim("add wave " + getSelectedSignal());
// } else {
// await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
// }
// });
function getNonce() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 16; i++) result += chars.charAt(Math.floor(Math.random() * chars.length));
return result;
}
class MyWebviewViewProvider implements vscode.WebviewViewProvider {
constructor(private readonly ctx: vscode.ExtensionContext) {}
resolveWebviewView(webviewView: vscode.WebviewView): void | Thenable<void> {
// Normal webview options go here (retainContextWhenHidden is NOT set here)
webviewView.webview.options = {
enableScripts: true,
// If you serve local files, set allowed roots:
// localResourceRoots: [vscode.Uri.joinPath(this.ctx.extensionUri, 'media')]
};
webviewView.webview.onDidReceiveMessage(async (msg) => {
if ((msg?.type ?? '').toLowerCase() !== 'wheel') return;
// Example: latest-only (cancel previous) using an AbortController
try {
if (waveMode && !!msg.ctrl) {
const cfg = vscode.workspace.getConfiguration('modelsimWave');
const sharedDir = cfg.get<string>('sharedDir') || '.';
const timeoutMs = cfg.get<number>('timeoutMs') ?? 5000;
console.log(msg.xPercent100);
if (msg.direction === 'zoom-in') {
const cmd = (`zoom_in_at ` + msg.xPercent100).replace(/\/+/g, '/');
const resp = await sendAndAwait(sharedDir, cmd, timeoutMs);
} else {
const cmd = (`zoom_out_at ` + msg.xPercent100).replace(/\/+/g, '/');
const resp = await sendAndAwait(sharedDir, cmd, timeoutMs);
}
}
} catch (e) {
if ((e as any).name !== 'AbortError') console.error(e);
}
});
// Paint immediately with a small shell
webviewView.webview.html = this.getHtml(webviewView.webview);
// (Optional) Load data after first paint and send it to the webview
// void this.init(w
// ebviewView.webview);
}
private getHtml(webview: vscode.Webview): string {
// You can also reference local resources if needed:
// const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.ctx.extensionUri, 'media', 'main.js'));
// const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(this.ctx.extensionUri, 'media', 'main.css'));
const nonce = getNonce();
return /* html */ `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="
default-src 'none';
img-src ${webview.cspSource} https:;
style-src ${webview.cspSource} 'unsafe-inline';
script-src 'nonce-${nonce}';
">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Panel</title>
<style>
body { font-family: var(--vscode-font-family); margin: 0; padding: 12px; }
.muted { opacity: 0.7; }
</style>
</head>
<body>
<h2>My Panel</h2>
<p class="muted">This webview is retained when hidden. Re-open should be instant.</p>
<button id="btn">Click me</button>
<pre id="log"></pre>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
const root = document.getElementById('root') || document.documentElement;
// Normalize wheel deltas to pixels
function normalizeWheel(e) {
let f = 1;
if (e.deltaMode === 1) f = 16; // lines → px
else if (e.deltaMode === 2) f = window.innerHeight; // pages → px
return { dx: e.deltaX * f, dy: e.deltaY * f };
}
// Helper to clamp 0..1
const clamp01 = (v) => Math.min(1, Math.max(0, v));
root.addEventListener('wheel', (e) => {
const { dx, dy } = normalizeWheel(e);
// ctrl-like on mac can be metaKey for some gestures; include both if you want
const ctrlLike = e.ctrlKey || e.metaKey;
// Position of cursor relative to the panel (0..1 from left)
const rect = root.getBoundingClientRect();
const xPct = clamp01((e.clientX - rect.left) / Math.max(1, rect.width));
// Wheel direction: negative dy usually = "zoom in", positive = "zoom out"
// Flip this if you prefer the opposite behavior.
const direction = dy < 0 ? 'zoom-in' : 'zoom-out';
vscode.postMessage({
type: 'wheel',
ctrl: !!ctrlLike,
deltaX: dx,
deltaY: dy,
xPercent: xPct, // 0..1
xPercent100: Math.round(xPct * 100), // 0..100 (convenience)
direction // "zoom-in" | "zoom-out"
});
// If you want to consume the scroll, uncomment:
// e.preventDefault();
}, { passive: false });
</script>s
</body>
</html>`;
}
// Example async init after first paint
// private async init(webview: vscode.Webview) {
// const data = await Promise.resolve({ message: "hello" });
// webview.postMessage({ type: 'init', data });
// }
}
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1000); statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1000);
statusItem.command = 'modelsim.toggleWaveMode'; // click to toggle statusItem.command = 'modelsim.toggleWaveMode'; // click to toggle
@@ -171,125 +36,10 @@ export function activate(context: vscode.ExtensionContext) {
toggleWaveMode(); toggleWaveMode();
}); });
const cmd = vscode.commands.registerCommand('modelsim.addWaveUnderCursor', async (args?: { mode?: string }) => { const cmd = vscode.commands.registerCommand('modelsim.addWaveUnderCursor', async (args?: { mode?: string }) => {
if (waveMode) { addWave(args?.mode ?? 'auto');
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const cfg = vscode.workspace.getConfiguration('modelsimWave');
const sharedDir = cfg.get<string>('sharedDir') || '.';
const topScope = cfg.get<string>('topScope') || 'sim:/test/'; // keep in sync with package.json
const timeoutMs = cfg.get<number>('timeoutMs') ?? 5000;
// Prefer last hover over caret
const docKey = editor.document.uri.toString();
const hover = lastHoverByDoc.get(docKey);
const now = Date.now();
const position =
hover && now - hover.at <= HOVER_FRESH_MS ? hover.pos : editor.selection.active;
const wordRange = editor.document.getWordRangeAtPosition(position);
if (!wordRange) {
vscode.window.showInformationMessage('Hover a signal, then press your shortcut.');
return;
}
const tokenText = editor.document.getText(wordRange);
const isVariable = await looksLikeVariable(editor, position);
if (!isVariable) {
const choice = await vscode.window.showWarningMessage(
`"${tokenText}" doesnt look like a variable here. Add anyway?`,
'Add',
'Cancel'
);
if (choice !== 'Add') return;
}
// Load module tree once per session
if (Object.keys(moduleTree).length === 0) {
moduleTree = await requestModuleTree(sharedDir, topScope, timeoutMs);
console.log("Lenght: ", Object.keys(moduleTree).length);
if (Object.keys(moduleTree).length === 0) {
vscode.window.showErrorMessage('Failed to load module hierarchy from ModelSim.');
return;
}
}
// Get module name from file name (language-independent)
const modName = path.basename(editor.document.fileName, path.extname(editor.document.fileName));
let instancePaths =
moduleTree[modName].module ?? moduleTree[modName.toLowerCase()].module;
if (!instancePaths || instancePaths.length === 0) {
console.log(
`No instance paths found for module "${modName}". ` +
`Ensure your poller uses "find instances" and the design is loaded.`
);
return;
}
// Add the token for every instance of this module (dedupe paths just in case)
instancePaths = Array.from(new Set(instancePaths)).map(p => p.replace(/\/+/g, '/'));
const mode = args?.mode ?? 'auto';
let instance_num = moduleTree[modName].last ?? moduleTree[modName.toLowerCase()].last;
if (instancePaths.length > 1 && mode === 'set') {
const input_num = await vscode.window.showInputBox({
title: 'Enter a number',
prompt: 'This number will be used by the action.',
placeHolder: moduleTree[modName].last !== undefined ? `Press Enter to reuse ${moduleTree[modName].last}` : 'e.g. 42',
value: moduleTree[modName].last !== undefined ? String(moduleTree[modName].last) : '',
ignoreFocusOut: true,
validateInput: (value) => {
if (value.trim() === '') return null; // allow Enter to reuse last
const n = Number(value);
if (!Number.isInteger(n)) return 'Please enter an integer.';
if (n < 0) return 'Please enter a non-negative integer.';
// optionally cap it
if (n > 10000) return 'Thats too large (max 10000).';
return null;
},
});
if (input_num === undefined) {
// User hit Esc — do nothing
return;
}
// Empty input means reuse last (if any)
instance_num = (input_num.trim() === '' || Number(input_num) >= moduleTree[modName].module.length) && moduleTree[modName].last !== undefined ? moduleTree[modName].last : Number(input_num);
moduleTree[modName].last = instance_num;
}
let ok = true;
// for (const p of instancePaths) {
const cmd = `quietly add wave -noupdate \{${instancePaths[instance_num]}/${tokenText}\}`.replace(/\/+/g, '/');
console.log(cmd);
const res = await sendAndAwait(sharedDir, cmd, timeoutMs);
ok = ok && !!res;
ok = ok && !!(await sendAndAwait(sharedDir, 'update', timeoutMs));
if (ok) {
vscode.window.showInformationMessage(
`Added "${tokenText}" for ${instancePaths.length} instance(s) of ${modName}.`
);
} else {
vscode.window.showWarningMessage('No response from ModelSim (timeout or error).');
}
} else {
await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
}
}); });
// Record last hover (no UI needed) // Record last hover (no UI needed)
const svSelectors: vscode.DocumentSelector = [
{ language: 'systemverilog', scheme: 'file' },
{ language: 'verilog', scheme: 'file' }
];
const hoverRecorder = vscode.languages.registerHoverProvider(svSelectors, { const hoverRecorder = vscode.languages.registerHoverProvider(svSelectors, {
provideHover(doc, position) { provideHover(doc, position) {
lastHoverByDoc.set(doc.uri.toString(), { pos: position, at: Date.now() }); lastHoverByDoc.set(doc.uri.toString(), { pos: position, at: Date.now() });
@@ -333,184 +83,5 @@ export function deactivate() {
statusItem?.dispose(); statusItem?.dispose();
} }
// ───────────────────────────────────────── Symbol utilities
async function looksLikeVariable(
editor: vscode.TextEditor,
position: vscode.Position
): Promise<boolean> {
try {
const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
'vscode.executeDocumentSymbolProvider',
editor.document.uri
);
if (!symbols || symbols.length === 0) return true;
const flat = flattenSymbols(symbols);
const hit = flat
.filter(s => s.range.contains(position))
.sort((a, b) => a.range.end.compareTo(b.range.end))
.pop();
if (!hit) return true;
const variableKinds = new Set([
vscode.SymbolKind.Variable,
vscode.SymbolKind.Field,
vscode.SymbolKind.Constant,
vscode.SymbolKind.Property,
vscode.SymbolKind.EnumMember
]);
return variableKinds.has(hit.kind);
} catch {
return true;
}
}
function flattenSymbols(items: vscode.DocumentSymbol[]): vscode.DocumentSymbol[] {
const out: vscode.DocumentSymbol[] = [];
const walk = (arr: vscode.DocumentSymbol[]) => {
for (const s of arr) {
out.push(s);
if (s.children?.length) walk(s.children);
}
};
walk(items);
return out;
}
function parseModuleNameFromDoc(doc: vscode.TextDocument): string | null {
const m = doc.getText().match(/\bmodule\s+([A-Za-z_][A-Za-z0-9_$]*)\b/);
return m ? m[1] : null;
}
// ───────────────────────────────────────── ModelSim command protocol
async function requestModuleTree(
sharedDir: string,
topScope: string,
timeoutMs: number
): Promise<Record<string, { module: string[], last: number }>> {
const tree: Record<string, string[]> = {};
const payload = `get_module_tree ${topScope}`;
console.log("Requesting ModuleTree");
vscode.window.showInformationMessage('Requesting ModuleTree');
const res = await sendAndAwait(sharedDir, payload, timeoutMs);
const text = (await res) ?? "";
console.log("Module Hierarchy: ", text)
const rx = /\{([^}]+?)\s+\(([^)]+)\)\}/g; // {<path> (<type>)}
const moduleTree: Record<string, { module: string[], last: number }> = {};
let m: RegExpExecArray | null;
while ((m = rx.exec(text))) {
const rawPath = m[1].trim(); // e.g. "/test/u_queue/u_queue_slot[0]"
const modType = m[2].trim(); // e.g. "queue_slot"
if (rawPath.startsWith("/std") || rawPath.startsWith("/std::")) continue;
console.log("RawPath: ", rawPath);
if (!(rawPath === `/${topScope}` || rawPath.startsWith(`/${topScope}/`))) continue;
console.log("RawPath: ", rawPath);
// normalize and keep indices
// const normPath =
// "/" +
// rawPath
// .replace(/^\//, "")
// .split("/")
// .map(seg => (seg.startsWith("u_") ? seg : "u_" + seg))
// .join("/");
((moduleTree[modType] ??= { module: [], last: 0 }).module).push(rawPath);
}
console.log(moduleTree);
return moduleTree;
// if (!res) {
// vscode.window.showErrorMessage('No response from ModelSim (timeout).');
// return tree;
// }
// const preview = res.slice(0, 400).replace(/\r?\n/g, '\\n');
// console.log(`[modelsim-wave] module-tree raw preview: ${preview}`);
// if (res.startsWith('ERROR:')) {
// vscode.window.showErrorMessage(`ModelSim error: ${res}`);
// return tree;
// }
// const lines = res.replace(/\r/g, '').split('\n').map(l => l.trim()).filter(Boolean);
// const push = (mod: string, p: string) => {
// // normalize paths: strip sim:/, ensure single leading slash, collapse slashes
// let path = p.replace(/^sim:\//i, '/').replace(/\/+/g, '/');
// if (!path.startsWith('/')) path = `/${path}`;
// (tree[mod] ??= []).push(path);
// };
// for (const line of lines) {
// let m: RegExpExecArray | null;
// // 1) "<module> /path" or "<module> sim:/path"
// m = /^([A-Za-z_][A-Za-z0-9_$\.]*)\s+(?:sim:\/)?(\/.*)$/.exec(line);
// if (m) { push(m[1], m[2]); continue; }
// // 2) "{/path (module)}" → raw `find instances` formatted line
// m = /^\{(?:sim:\/)?(\/[^ ]*) \(([^)]+)\)\}$/.exec(line);
// if (m) { push(m[2], m[1]); continue; }
// // 3) "lib.module /path"
// m = /^(\S+)\.([A-Za-z_][A-Za-z0-9_$\.]*)\s+(?:sim:\/)?(\/.*)$/.exec(line);
// if (m) { push(m[2], m[3]); continue; }
// // 4) Fallback: "<token> <maybe-path>"
// m = /^(\S+)\s+(?:sim:\/)?(\/.*)$/.exec(line);
// if (m) { push(m[1], m[2]); continue; }
// // If nothing matched, keep going; some lines may be comments.
// }
// if (Object.keys(tree).length === 0) {
// vscode.window.showWarningMessage(
// 'ModelSim replied, but no modules were parsed. Check “Log (Extension Host)” for the raw preview and verify Top Scope.'
// );
// }
// return tree;
}
async function sendAndAwait(
sharedDir: string,
command: string,
timeoutMs: number
): Promise<string | null> {
const cfg = vscode.workspace.getConfiguration('modelsimWave');
const keep = cfg.get<boolean>('debugKeepResults') === true;
const id = `${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
const resultsPath = path.join(sharedDir, `modelsim_results_${id}.txt`);
const commandsPath = path.join(sharedDir, 'modelsim_commands.txt');
const line = `${id}|${resultsPath}|${command}\n`;
await fsp.appendFile(commandsPath, line, 'utf8');
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const buf = await fsp.readFile(resultsPath);
const txt = buf.toString('utf8');
if (!keep) {
try { await fsp.unlink(resultsPath); } catch {}
}
return txt;
} catch {
await sleep(200);
}
}
return null;
}
function sleep(ms: number) {
return new Promise(r => setTimeout(r, ms));
}

138
src/wave_action.ts Normal file
View File

@@ -0,0 +1,138 @@
import * as vscode from 'vscode';
import { waveMode } from './extension';
import { sendAndAwait } from './eda_connect';
export class MyWebviewViewProvider implements vscode.WebviewViewProvider {
constructor(private readonly ctx: vscode.ExtensionContext) {}
resolveWebviewView(webviewView: vscode.WebviewView): void | Thenable<void> {
// Normal webview options go here (retainContextWhenHidden is NOT set here)
webviewView.webview.options = {
enableScripts: true,
// If you serve local files, set allowed roots:
// localResourceRoots: [vscode.Uri.joinPath(this.ctx.extensionUri, 'media')]
};
webviewView.webview.onDidReceiveMessage(async (msg) => {
if ((msg?.type ?? '').toLowerCase() !== 'wheel') return;
// Example: latest-only (cancel previous) using an AbortController
try {
if (waveMode && !!msg.ctrl) {
const cfg = vscode.workspace.getConfiguration('modelsimWave');
const sharedDir = cfg.get<string>('sharedDir') || '.';
const timeoutMs = cfg.get<number>('timeoutMs') ?? 5000;
console.log(msg.xPercent100);
if (msg.direction === 'zoom-in') {
const cmd = (`zoom_in_at ` + msg.xPercent100).replace(/\/+/g, '/');
const resp = await sendAndAwait(sharedDir, cmd, timeoutMs);
} else {
const cmd = (`zoom_out_at ` + msg.xPercent100).replace(/\/+/g, '/');
const resp = await sendAndAwait(sharedDir, cmd, timeoutMs);
}
}
} catch (e) {
if ((e as any).name !== 'AbortError') console.error(e);
}
});
// Paint immediately with a small shell
webviewView.webview.html = this.getHtml(webviewView.webview);
// (Optional) Load data after first paint and send it to the webview
// void this.init(w
// ebviewView.webview);
}
private getHtml(webview: vscode.Webview): string {
// You can also reference local resources if needed:
// const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.ctx.extensionUri, 'media', 'main.js'));
// const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(this.ctx.extensionUri, 'media', 'main.css'));
const nonce = getNonce();
return /* html */ `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="
default-src 'none';
img-src ${webview.cspSource} https:;
style-src ${webview.cspSource} 'unsafe-inline';
script-src 'nonce-${nonce}';
">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Panel</title>
<style>
body { font-family: var(--vscode-font-family); margin: 0; padding: 12px; }
.muted { opacity: 0.7; }
</style>
</head>
<body>
<h2>My Panel</h2>
<p class="muted">This webview is retained when hidden. Re-open should be instant.</p>
<button id="btn">Click me</button>
<pre id="log"></pre>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
const root = document.getElementById('root') || document.documentElement;
// Normalize wheel deltas to pixels
function normalizeWheel(e) {
let f = 1;
if (e.deltaMode === 1) f = 16; // lines → px
else if (e.deltaMode === 2) f = window.innerHeight; // pages → px
return { dx: e.deltaX * f, dy: e.deltaY * f };
}
// Helper to clamp 0..1
const clamp01 = (v) => Math.min(1, Math.max(0, v));
root.addEventListener('wheel', (e) => {
const { dx, dy } = normalizeWheel(e);
// ctrl-like on mac can be metaKey for some gestures; include both if you want
const ctrlLike = e.ctrlKey || e.metaKey;
// Position of cursor relative to the panel (0..1 from left)
const rect = root.getBoundingClientRect();
const xPct = clamp01((e.clientX - rect.left) / Math.max(1, rect.width));
// Wheel direction: negative dy usually = "zoom in", positive = "zoom out"
// Flip this if you prefer the opposite behavior.
const direction = dy < 0 ? 'zoom-in' : 'zoom-out';
vscode.postMessage({
type: 'wheel',
ctrl: !!ctrlLike,
deltaX: dx,
deltaY: dy,
xPercent: xPct, // 0..1
xPercent100: Math.round(xPct * 100), // 0..100 (convenience)
direction // "zoom-in" | "zoom-out"
});
// If you want to consume the scroll, uncomment:
// e.preventDefault();
}, { passive: false });
</script>s
</body>
</html>`;
}
// Example async init after first paint
// private async init(webview: vscode.Webview) {
// const data = await Promise.resolve({ message: "hello" });
// webview.postMessage({ type: 'init', data });
// }
}
function getNonce() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 16; i++) result += chars.charAt(Math.floor(Math.random() * chars.length));
return result;
}

200
src/wave_editor.ts Normal file
View File

@@ -0,0 +1,200 @@
import * as vscode from 'vscode';
import * as path from 'path';
import { waveMode, lastHoverByDoc, HOVER_FRESH_MS } from './extension';
import { sendAndAwait } from './eda_connect';
export let moduleTree: Record<string, { module: string[], last: number }> = {};
export async function requestModuleTree(
sharedDir: string,
topScope: string,
timeoutMs: number
): Promise<Record<string, { module: string[], last: number }>> {
const tree: Record<string, string[]> = {};
const payload = `get_module_tree ${topScope}`;
console.log("Requesting ModuleTree");
vscode.window.showInformationMessage('Requesting ModuleTree');
const res = await sendAndAwait(sharedDir, payload, timeoutMs);
const text = (await res) ?? "";
console.log("Module Hierarchy: ", text)
const rx = /\{([^}]+?)\s+\(([^)]+)\)\}/g; // {<path> (<type>)}
const moduleTree: Record<string, { module: string[], last: number }> = {};
let m: RegExpExecArray | null;
while ((m = rx.exec(text))) {
const rawPath = m[1].trim(); // e.g. "/test/u_queue/u_queue_slot[0]"
const modType = m[2].trim(); // e.g. "queue_slot"
if (rawPath.startsWith("/std") || rawPath.startsWith("/std::")) continue;
console.log("RawPath: ", rawPath);
if (!(rawPath === `/${topScope}` || rawPath.startsWith(`/${topScope}/`))) continue;
console.log("RawPath: ", rawPath);
((moduleTree[modType] ??= { module: [], last: 0 }).module).push(rawPath);
}
console.log(moduleTree);
return moduleTree;
}
export async function addWave(
mode: String
) {
if (waveMode) {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const cfg = vscode.workspace.getConfiguration('modelsimWave');
const sharedDir = cfg.get<string>('sharedDir') || '.';
const topScope = cfg.get<string>('topScope') || 'sim:/test/'; // keep in sync with package.json
const timeoutMs = cfg.get<number>('timeoutMs') ?? 5000;
// Prefer last hover over caret
const docKey = editor.document.uri.toString();
const hover = lastHoverByDoc.get(docKey);
const now = Date.now();
const position =
hover && now - hover.at <= HOVER_FRESH_MS ? hover.pos : editor.selection.active;
const wordRange = editor.document.getWordRangeAtPosition(position);
if (!wordRange) {
vscode.window.showInformationMessage('Hover a signal, then press your shortcut.');
return;
}
const tokenText = editor.document.getText(wordRange);
const isVariable = await looksLikeVariable(editor, position);
if (!isVariable) {
const choice = await vscode.window.showWarningMessage(
`"${tokenText}" doesnt look like a variable here. Add anyway?`,
'Add',
'Cancel'
);
if (choice !== 'Add') return;
}
// Load module tree once per session
if (Object.keys(moduleTree).length === 0) {
moduleTree = await requestModuleTree(sharedDir, topScope, timeoutMs);
console.log("Lenght: ", Object.keys(moduleTree).length);
if (Object.keys(moduleTree).length === 0) {
vscode.window.showErrorMessage('Failed to load module hierarchy from ModelSim.');
return;
}
}
// Get module name from file name (language-independent)
const modName = path.basename(editor.document.fileName, path.extname(editor.document.fileName));
let instancePaths =
moduleTree[modName].module ?? moduleTree[modName.toLowerCase()].module;
if (!instancePaths || instancePaths.length === 0) {
console.log(
`No instance paths found for module "${modName}". ` +
`Ensure your poller uses "find instances" and the design is loaded.`
);
return;
}
// Add the token for every instance of this module (dedupe paths just in case)
instancePaths = Array.from(new Set(instancePaths)).map(p => p.replace(/\/+/g, '/'));
let instance_num = moduleTree[modName].last ?? moduleTree[modName.toLowerCase()].last;
if (instancePaths.length > 1 && mode === 'set') {
const input_num = await vscode.window.showInputBox({
title: 'Enter a number',
prompt: 'This number will be used by the action.',
placeHolder: moduleTree[modName].last !== undefined ? `Press Enter to reuse ${moduleTree[modName].last}` : 'e.g. 42',
value: moduleTree[modName].last !== undefined ? String(moduleTree[modName].last) : '',
ignoreFocusOut: true,
validateInput: (value) => {
if (value.trim() === '') return null; // allow Enter to reuse last
const n = Number(value);
if (!Number.isInteger(n)) return 'Please enter an integer.';
if (n < 0) return 'Please enter a non-negative integer.';
// optionally cap it
if (n > 10000) return 'Thats too large (max 10000).';
return null;
},
});
if (input_num === undefined) {
// User hit Esc — do nothing
return;
}
// Empty input means reuse last (if any)
instance_num = (input_num.trim() === '' || Number(input_num) >= moduleTree[modName].module.length) && moduleTree[modName].last !== undefined ? moduleTree[modName].last : Number(input_num);
moduleTree[modName].last = instance_num;
}
let ok = true;
// for (const p of instancePaths) {
const cmd = `quietly add wave -noupdate \{${instancePaths[instance_num]}/${tokenText}\}`.replace(/\/+/g, '/');
console.log(cmd);
const res = await sendAndAwait(sharedDir, cmd, timeoutMs);
ok = ok && !!res;
ok = ok && !!(await sendAndAwait(sharedDir, 'update', timeoutMs));
if (ok) {
vscode.window.showInformationMessage(
`Added "${tokenText}" for ${instancePaths.length} instance(s) of ${modName}.`
);
} else {
vscode.window.showWarningMessage('No response from ModelSim (timeout or error).');
}
} else {
await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
}
}
async function looksLikeVariable(
editor: vscode.TextEditor,
position: vscode.Position
): Promise<boolean> {
try {
const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
'vscode.executeDocumentSymbolProvider',
editor.document.uri
);
if (!symbols || symbols.length === 0) return true;
const flat = flattenSymbols(symbols);
const hit = flat
.filter(s => s.range.contains(position))
.sort((a, b) => a.range.end.compareTo(b.range.end))
.pop();
if (!hit) return true;
const variableKinds = new Set([
vscode.SymbolKind.Variable,
vscode.SymbolKind.Field,
vscode.SymbolKind.Constant,
vscode.SymbolKind.Property,
vscode.SymbolKind.EnumMember
]);
return variableKinds.has(hit.kind);
} catch {
return true;
}
}
function flattenSymbols(items: vscode.DocumentSymbol[]): vscode.DocumentSymbol[] {
const out: vscode.DocumentSymbol[] = [];
const walk = (arr: vscode.DocumentSymbol[]) => {
for (const s of arr) {
out.push(s);
if (s.children?.length) walk(s.children);
}
};
walk(items);
return out;
}