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 * 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}" 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"); |  | ||||||
|     } |  | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   // 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
									
								
							
							
						
						
									
										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