From 0d91be6664230079fecb2fbb280023e482b43cd7 Mon Sep 17 00:00:00 2001 From: "brice.boisson" Date: Wed, 22 Oct 2025 22:21:04 +0200 Subject: [PATCH] Split Code --- src/eda_connect.ts | 39 ++++ src/extension.ts | 455 ++------------------------------------------- src/wave_action.ts | 138 ++++++++++++++ src/wave_editor.ts | 200 ++++++++++++++++++++ 4 files changed, 390 insertions(+), 442 deletions(-) create mode 100644 src/eda_connect.ts create mode 100644 src/wave_action.ts create mode 100644 src/wave_editor.ts diff --git a/src/eda_connect.ts b/src/eda_connect.ts new file mode 100644 index 0000000..7464716 --- /dev/null +++ b/src/eda_connect.ts @@ -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 { + const cfg = vscode.workspace.getConfiguration('modelsimWave'); + const keep = cfg.get('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)); +} diff --git a/src/extension.ts b/src/extension.ts index 44577c1..5e17630 100644 --- a/src/extension.ts +++ b/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(); -const HOVER_FRESH_MS = 2000; +export const lastHoverByDoc = new Map(); +export const HOVER_FRESH_MS = 2000; // Hold MANY instance paths per module name, e.g. queue_slot -> ["/test/...[0]", "/test/...[1]", ...] -let moduleTree: Record = {}; -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 { - // 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('sharedDir') || '.'; - const timeoutMs = cfg.get('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 */ ` - - - - - -My Panel - - - -

My Panel

-

This webview is retained when hidden. Re-open should be instant.

- -

-
-s
-
-`;
-  }
-
-  // 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('sharedDir') || '.';
-      const topScope = cfg.get('topScope') || 'sim:/test/'; // keep in sync with package.json
-      const timeoutMs = cfg.get('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 {
-  try {
-    const symbols = await vscode.commands.executeCommand(
-      '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> {
-  const tree: Record = {};
-  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; // { ()}
-  const moduleTree: Record = {};
-
-  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) " /path"   or   " 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: " "
-  //   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 {
-  const cfg = vscode.workspace.getConfiguration('modelsimWave');
-  const keep = cfg.get('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));
-}
diff --git a/src/wave_action.ts b/src/wave_action.ts
new file mode 100644
index 0000000..671dcf7
--- /dev/null
+++ b/src/wave_action.ts
@@ -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 {
+    // 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('sharedDir') || '.';
+          const timeoutMs = cfg.get('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 */ `
+
+
+
+
+
+My Panel
+
+
+
+  

My Panel

+

This webview is retained when hidden. Re-open should be instant.

+ +

+
+s
+
+`;
+  }
+
+  // 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;
+}
diff --git a/src/wave_editor.ts b/src/wave_editor.ts
new file mode 100644
index 0000000..b4338ed
--- /dev/null
+++ b/src/wave_editor.ts
@@ -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 = {};
+
+export async function requestModuleTree(
+  sharedDir: string,
+  topScope: string,
+  timeoutMs: number
+): Promise> {
+  const tree: Record = {};
+  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; // { ()}
+  const moduleTree: Record = {};
+
+  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('sharedDir') || '.';
+    const topScope = cfg.get('topScope') || 'sim:/test/'; // keep in sync with package.json
+    const timeoutMs = cfg.get('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 {
+  try {
+    const symbols = await vscode.commands.executeCommand(
+      '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;
+}