First version
This commit is contained in:
		
							
								
								
									
										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)); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user