Split Code
This commit is contained in:
		
							
								
								
									
										39
									
								
								src/eda_connect.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/eda_connect.ts
									
									
									
									
									
										Normal 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)); | ||||
| } | ||||
							
								
								
									
										455
									
								
								src/extension.ts
									
									
									
									
									
								
							
							
						
						
									
										455
									
								
								src/extension.ts
									
									
									
									
									
								
							| @@ -2,14 +2,22 @@ import * as vscode from 'vscode'; | ||||
| import * as path from 'path'; | ||||
| 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. | ||||
| const lastHoverByDoc = new Map<string, { pos: vscode.Position; at: number }>(); | ||||
| const HOVER_FRESH_MS = 2000; | ||||
| export const lastHoverByDoc = new Map<string, { pos: vscode.Position; at: number }>(); | ||||
| export const HOVER_FRESH_MS = 2000; | ||||
|  | ||||
| // 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; | ||||
|  | ||||
| function toggleWaveMode() { | ||||
| @@ -20,149 +28,6 @@ function toggleWaveMode() { | ||||
|     : '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 | ||||
| @@ -171,125 +36,10 @@ export function activate(context: vscode.ExtensionContext) { | ||||
|     toggleWaveMode(); | ||||
|   }); | ||||
|   const cmd = vscode.commands.registerCommand('modelsim.addWaveUnderCursor', async (args?: { 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}" 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].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 'That’s 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"); | ||||
|     } | ||||
|     addWave(args?.mode ?? 'auto'); | ||||
|   }); | ||||
|  | ||||
|   // 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() }); | ||||
| @@ -333,184 +83,5 @@ 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, { 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
									
								
							
							
						
						
									
										138
									
								
								src/wave_action.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										200
									
								
								src/wave_editor.ts
									
									
									
									
									
										Normal 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}" 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].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 'That’s 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; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user