First version
This commit is contained in:
75
package.json
Normal file
75
package.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"name": "modelsim-wave-ext",
|
||||||
|
"displayName": "ModelSim Wave Extension",
|
||||||
|
"description": "Send add wave commands to ModelSim from VS Code",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"engines": { "vscode": "^1.70.0" },
|
||||||
|
"activationEvents": [
|
||||||
|
"onCommand:modelsim.addWaveUnderCursor",
|
||||||
|
"onStartupFinished",
|
||||||
|
"onView:myCustomPanel"
|
||||||
|
],
|
||||||
|
"main": "./out/extension.js",
|
||||||
|
"contributes": {
|
||||||
|
"commands": [
|
||||||
|
{ "command": "modelsim.addWaveUnderCursor", "title": "Add Wave for Word Under Cursor" },
|
||||||
|
{ "command": "modelsim.toggleWaveMode", "title": "Toggle Wave Debug Mode" }
|
||||||
|
],
|
||||||
|
"keybindings": [
|
||||||
|
{ "command": "modelsim.addWaveUnderCursor", "key": "ctrl+w" },
|
||||||
|
{ "command": "modelsim.toggleWaveMode", "key": "ctrl+alt+m" },
|
||||||
|
{ "command": "modelsim.zoomInWave", "key": "ctrl+alt+u" }
|
||||||
|
],
|
||||||
|
"viewsContainers": {
|
||||||
|
"panel": [
|
||||||
|
{
|
||||||
|
"id": "myPanelContainer",
|
||||||
|
"title": "My Tools",
|
||||||
|
"icon": "media/tools.svg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"myPanelContainer": [
|
||||||
|
{
|
||||||
|
"type": "webview",
|
||||||
|
"id": "myCustomPanel",
|
||||||
|
"name": "My Panel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"title": "ModelSim Wave",
|
||||||
|
"properties": {
|
||||||
|
"modelsimWave.sharedDir": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ".",
|
||||||
|
"description": "Folder shared between VS Code and ModelSim."
|
||||||
|
},
|
||||||
|
"modelsimWave.topScope": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "sim:/tb_top/",
|
||||||
|
"description": "Simulation top scope (e.g., sim:/top_tb)."
|
||||||
|
},
|
||||||
|
"modelsimWave.timeoutMs": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 5000,
|
||||||
|
"description": "Timeout waiting for ModelSim response (ms)."
|
||||||
|
},
|
||||||
|
"modelsimWave.debugKeepResults": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Keep ModelSim result files for debugging."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p ."
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"@types/vscode": "^1.70.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
482
src/extension.ts
Normal file
482
src/extension.ts
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { promises as fsp } from 'fs';
|
||||||
|
|
||||||
|
// Tracks the most recent hover position per document, with a timestamp.
|
||||||
|
const lastHoverByDoc = new Map<string, { pos: vscode.Position; at: number }>();
|
||||||
|
const HOVER_FRESH_MS = 2000;
|
||||||
|
|
||||||
|
// Hold MANY instance paths per module name, e.g. queue_slot -> ["/test/...[0]", "/test/...[1]", ...]
|
||||||
|
let moduleTree: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
let waveMode = false;
|
||||||
|
let statusItem: vscode.StatusBarItem;
|
||||||
|
|
||||||
|
function toggleWaveMode() {
|
||||||
|
waveMode = !waveMode;
|
||||||
|
statusItem.text = waveMode ? '🌊 Wave Debug Mode' : '✏️ Edit Mode';
|
||||||
|
statusItem.tooltip = waveMode
|
||||||
|
? 'Wave Debug Mode — click to switch to Edit 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) {
|
||||||
|
statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1000);
|
||||||
|
statusItem.command = 'modelsim.toggleWaveMode'; // click to toggle
|
||||||
|
|
||||||
|
const cmd_2 = vscode.commands.registerCommand('modelsim.toggleWaveMode', async () => {
|
||||||
|
toggleWaveMode();
|
||||||
|
});
|
||||||
|
const cmd = vscode.commands.registerCommand('modelsim.addWaveUnderCursor', async () => {
|
||||||
|
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}" doesn’t 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] ?? moduleTree[modName.toLowerCase()];
|
||||||
|
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 ok = true;
|
||||||
|
for (const p of instancePaths) {
|
||||||
|
const cmd = `quietly add wave -noupdate ${p}/${tokenText}`.replace(/\/+/g, '/');
|
||||||
|
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)
|
||||||
|
const svSelectors: vscode.DocumentSelector = [
|
||||||
|
{ language: 'systemverilog', scheme: 'file' },
|
||||||
|
{ language: 'verilog', scheme: 'file' }
|
||||||
|
];
|
||||||
|
const hoverRecorder = vscode.languages.registerHoverProvider(svSelectors, {
|
||||||
|
provideHover(doc, position) {
|
||||||
|
lastHoverByDoc.set(doc.uri.toString(), { pos: position, at: Date.now() });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const zoom = vscode.commands.registerCommand('modelsim.zoomInWave', async () => {
|
||||||
|
if (waveMode) {
|
||||||
|
const cfg = vscode.workspace.getConfiguration('modelsimWave');
|
||||||
|
const sharedDir = cfg.get<string>('sharedDir') || '.';
|
||||||
|
const timeoutMs = cfg.get<number>('timeoutMs') ?? 5000;
|
||||||
|
const cmd = `zoom_in_at 10`.replace(/\/+/g, '/');
|
||||||
|
const res = await sendAndAwait(sharedDir, cmd, timeoutMs);
|
||||||
|
if (!res) {
|
||||||
|
vscode.window.showWarningMessage('No response from ModelSim (timeout or error).');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const provider = new MyWebviewViewProvider(context);
|
||||||
|
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.window.registerWebviewViewProvider(
|
||||||
|
'myCustomPanel',
|
||||||
|
provider,
|
||||||
|
{ webviewOptions: { retainContextWhenHidden: true } } // 👈 the important bit
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
context.subscriptions.push(cmd, hoverRecorder, statusItem, cmd_2, zoom);
|
||||||
|
statusItem.text = waveMode ? '🌊 Wave Debug Mode' : '✏️ Edit Mode';
|
||||||
|
statusItem.tooltip = waveMode
|
||||||
|
? 'Wave Debug Mode — click to switch to Edit Mode'
|
||||||
|
: 'Edit Mode — click to switch to Wave Debug Mode';
|
||||||
|
statusItem.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deactivate() {
|
||||||
|
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, string[]>> {
|
||||||
|
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, string[]> = {};
|
||||||
|
|
||||||
|
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] ??= []).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));
|
||||||
|
}
|
||||||
104
test/back.do
Normal file
104
test/back.do
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# modelsim_poll.tcl — robust, logged, per-request protocol
|
||||||
|
|
||||||
|
# === CONFIG: set to your absolute shared folder ===
|
||||||
|
set shared_dir "/home/brice/Code/modelsim_wave_ext/test/tmp"
|
||||||
|
|
||||||
|
# === Derived paths ===
|
||||||
|
set commands_file [file join $shared_dir "modelsim_commands.txt"]
|
||||||
|
|
||||||
|
# Minimal logger
|
||||||
|
proc elog {msg} {
|
||||||
|
puts "[clock format [clock seconds] -format {%H:%M:%S}] [info nameofexecutable]: $msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Return lines: "<module> /test/...", one per INSTANCE (not unique by module)
|
||||||
|
proc extract_modules_from_find {} {
|
||||||
|
set result [find instances -r /*]
|
||||||
|
|
||||||
|
elog $result
|
||||||
|
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
proc getTimeAtPercent {percent} {
|
||||||
|
lassign [wave zoom range] start end
|
||||||
|
lassign $start sVal sUnit
|
||||||
|
lassign $end eVal eUnit
|
||||||
|
# (Usually sUnit == eUnit; if not, convert eVal to sUnit as needed.)
|
||||||
|
set t [expr {$sVal + ($eVal - $sVal) * $percent / 100.0}]
|
||||||
|
return "$t $sUnit"
|
||||||
|
}
|
||||||
|
|
||||||
|
proc zoomAtPercent {percent {factor 1.1}} {
|
||||||
|
set time [getTimeAtPercent $percent]
|
||||||
|
lassign [wave zoom range] start end
|
||||||
|
lassign $start sVal sUnit
|
||||||
|
lassign $end eVal eUnit
|
||||||
|
set new [expr {$eVal + 1}]
|
||||||
|
wave seetime "${new}${eUnit}" -at 0
|
||||||
|
wave seetime $time -at 50
|
||||||
|
wave zoom in $factor
|
||||||
|
lassign [wave zoom range] start end
|
||||||
|
lassign $start sVal sUnit
|
||||||
|
lassign $end eVal eUnit
|
||||||
|
set new [expr {$eVal + 1}]
|
||||||
|
wave seetime "${new}${eUnit}" -at 0
|
||||||
|
wave seetime $time -at $percent
|
||||||
|
}
|
||||||
|
|
||||||
|
proc handle_command {payload} {
|
||||||
|
if {[regexp {^get_module_tree\s+(.+)$} $payload -> top]} {
|
||||||
|
return [extract_modules_from_find]
|
||||||
|
} elseif {[regexp {^zoom_in_at\s+(.+)$} $payload -> percent]} {
|
||||||
|
return [zoomAtPercent $percent]
|
||||||
|
} elseif {[regexp {^zoom_out_at\s+(.+)$} $payload -> percent]} {
|
||||||
|
return [zoomAtPercent $percent 0.9]
|
||||||
|
} else {
|
||||||
|
if {[catch {eval $payload} out]} { return "ERROR: $out" }
|
||||||
|
return $out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proc poll_commands {} {
|
||||||
|
global commands_file
|
||||||
|
if {[file exists $commands_file]} {
|
||||||
|
set fid [open $commands_file r]
|
||||||
|
# Handle both \n and \r\n
|
||||||
|
set content [string map {\r ""} [read $fid]]
|
||||||
|
close $fid
|
||||||
|
file delete -force $commands_file
|
||||||
|
|
||||||
|
foreach raw [split $content "\n"] {
|
||||||
|
set line [string trim $raw]
|
||||||
|
if {$line eq ""} { continue }
|
||||||
|
|
||||||
|
# Expect: <id>|<result_path>|<payload>
|
||||||
|
if {![regexp {^(\S+)\|(\S+)\|(.*)$} $line -> id result_path payload]} {
|
||||||
|
elog "WARN: bad command line: $line"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
elog "CMD id=$id -> $payload"
|
||||||
|
set out [handle_command $payload]
|
||||||
|
|
||||||
|
# Ensure directory exists
|
||||||
|
set dir [file dirname $result_path]
|
||||||
|
if {![file isdirectory $dir]} {
|
||||||
|
catch { file mkdir $dir }
|
||||||
|
}
|
||||||
|
|
||||||
|
if {[catch { set rf [open $result_path w] } err]} {
|
||||||
|
elog "ERROR: cannot open result file '$result_path': $err"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
puts $rf $out
|
||||||
|
close $rf
|
||||||
|
elog "WROTE result -> $result_path (len=[string length $out])"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
after 200 poll_commands
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start
|
||||||
|
elog "Poller starting. shared_dir=$shared_dir pwd=[pwd]"
|
||||||
|
poll_commands
|
||||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"outDir": "out",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": ["node", "vscode"]
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user