diff --git a/src/wave_editor.ts b/src/wave_editor.ts index 081a3ff..953842d 100644 --- a/src/wave_editor.ts +++ b/src/wave_editor.ts @@ -71,13 +71,14 @@ export async function addWave( const position = hover && now - hover.at <= HOVER_FRESH_MS ? hover.pos : editor.selection.active; - const wordRange = editor.document.getWordRangeAtPosition(position); - if (!wordRange) { + const expr = getExpressionAtPosition(editor.document, position); + if (!expr) { vscode.window.showInformationMessage('Hover a signal, then press your shortcut.'); return; } + + let tokenText = expr.text; - let tokenText = editor.document.getText(wordRange); const isVariable = await looksLikeVariable(editor, position); if (!isVariable) { const choice = await vscode.window.showWarningMessage( @@ -212,3 +213,174 @@ function flattenSymbols(items: vscode.DocumentSymbol[]): vscode.DocumentSymbol[] walk(items); return out; } + +/** + * Expand the hovered token into its full left-only member-access expression. + * + * Examples on: foo.bar[1][i].baz[2] + * hover "foo" => "foo" + * hover "bar" => "foo.bar" + * hover "[1]" => "foo.bar[1]" + * hover "[i]" => "foo.bar[1][i]" + * hover "baz" => "foo.bar[1][i].baz" + * hover "[2]" => "foo.bar[1][i].baz[2]" + */ +export function getExpressionAtPosition( + document: vscode.TextDocument, + position: vscode.Position +): { text: string; range: vscode.Range } | undefined { + const text = document.getText(); + const toff = document.offsetAt(position); + + const isIdentChar = (c: string) => /[A-Za-z0-9_$]/.test(c); + const isSpace = (c: string) => /\s/.test(c); + + // --- 1) Find the "segment" under the cursor: BRACKET GROUP [ ... ] first, then IDENT + let segStart = -1; + let segEnd = -1; + + // Try bracket group first (fixes the [1] case) + const group = findEnclosingBracketGroup(text, toff); + if (group) { + segStart = group.start; + segEnd = group.end; + } else { + // No bracket group → try word + const identRegex = /[A-Za-z_$][A-Za-z0-9_$]*/; + const word = + document.getWordRangeAtPosition(position, identRegex) ?? + (toff > 0 ? document.getWordRangeAtPosition(document.positionAt(toff - 1), identRegex) : undefined); + if (!word) return undefined; + segStart = document.offsetAt(word.start); + segEnd = document.offsetAt(word.end); + } + + // --- 2) Expand LEFT to include ".ident" and bracket chains + let left = segStart; + + const includePrevBracketGroup = (): boolean => { + if (left <= 0 || text[left - 1] !== ']') return false; + let depth = 1; + for (let k = left - 2; k >= 0; k--) { + const ch = text[k]; + if (ch === ']') depth++; + else if (ch === '[') { + depth--; + if (depth === 0) { + left = k; + return true; + } + } + } + return false; + }; + + const includePrevDotIdent = (): boolean => { + let p = left; + + // allow spaces before checking for '.' + while (p > 0 && isSpace(text[p - 1])) p--; + + // --- dot case + if (p > 0 && text[p - 1] === '.') { + p--; // on '.' + // skip spaces before dot + while (p > 0 && isSpace(text[p - 1])) p--; + + // consume any bracket groups before the dot + let q = p; + const consumePrevBracketGroupAt = (idx: number): number | null => { + if (idx <= 0 || text[idx - 1] !== ']') return null; + let d = 1; + for (let k = idx - 2; k >= 0; k--) { + const ch = text[k]; + if (ch === ']') d++; + else if (ch === '[') { + d--; + if (d === 0) return k; + } + } + return null; + }; + + while (true) { + while (q > 0 && isSpace(text[q - 1])) q--; + const nextQ = consumePrevBracketGroupAt(q); + if (nextQ === null) break; + q = nextQ; + } + + // now require identifier before them + while (q > 0 && isSpace(text[q - 1])) q--; + if (q <= 0 || !isIdentChar(text[q - 1])) return false; + + let k = q - 1; + while (k >= 0 && isIdentChar(text[k])) k--; + left = k + 1; + + // also include bracket groups attached even further left + while (includePrevBracketGroup()) {} + return true; + } + + // --- bare ident case: only if adjacent (no whitespace) + if (left > 0 && isIdentChar(text[left - 1])) { + let k = left - 1; + while (k >= 0 && isIdentChar(text[k])) k--; + const start = k + 1; + + // Stop before SV declaration keywords (logic, wire, etc.) + const token = text.slice(start, left); + const stopKeywords = new Set([ + 'logic','wire','reg','bit','byte','shortint','int','longint','integer','time', + 'signed','unsigned','const','var','local','static','automatic' + ]); + if (stopKeywords.has(token)) return false; + + left = start; + while (includePrevBracketGroup()) {} + return true; + } + + return false; + }; + + // include bracket groups and dot chains iteratively + while (includePrevBracketGroup()) {} + while (includePrevDotIdent()) {} + + const range = new vscode.Range(document.positionAt(left), document.positionAt(segEnd)); + const expr = text.slice(left, segEnd).trim(); + if (!expr) return undefined; + + return { text: expr, range }; +} + +/** Finds the [ ... ] group that contains `offset`, or just before it if cursor is on a ']'. */ +function findEnclosingBracketGroup(text: string, offset: number): { start: number; end: number } | undefined { + let i = offset; + if (i > 0 && text[i - 1] === ']') i--; // hovering just after a ']' counts + + for (let l = i; l >= 0; l--) { + const ch = text[l]; + if (ch === '[') { + let depth = 1; + for (let r = l + 1; r < text.length; r++) { + const cr = text[r]; + if (cr === '[') depth++; + else if (cr === ']') { + depth--; + if (depth === 0) { + if (i >= l && i <= r) { + return { start: l, end: r + 1 }; + } + break; + } + } + } + } else if (ch === '\n' || ch === '\r') { + break; + } + } + return undefined; +} \ No newline at end of file