From 74870eae8303241f2cf87c84940e68677c95b664 Mon Sep 17 00:00:00 2001 From: Joe Crop Date: Wed, 3 Jun 2026 13:35:49 -0700 Subject: [PATCH 1/7] Add member/port/package auto-completion (#82) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements context-aware completion: typing a trigger character suggests the relevant members. - `var.` -> fields of the struct/union type of `var` (the #82 request) - `obj.` -> properties/methods of the class type of `obj` - `.` -> ports of the module being instantiated (in a port-connection slot) - `pkg::` -> members declared in package `pkg` All four share one mechanism: detect the context, resolve a container symbol name, then list the symbols whose `container` is that name. Members are found by re-parsing the container's defining file on demand at a high maxDepth, because the default index depth (1) does not capture members of types nested in a package/module. Reuses DefinitionProvider.moduleFromPort for the port context and the `¤` exact-match workspace query to locate defining files. New SystemVerilogCompletionItemProvider (registered with '.'/':' triggers), replacing the unused src/wip/CompletionItemProvider.ts. Adds unit tests for the pure context/type-name helpers and provider tests via executeCompletionItemProvider, plus verilog-examples/member_completion.sv. Closes #82 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + src/extension.ts | 5 + src/providers/CompletionItemProvider.ts | 217 ++++++++++++++++++++++++ src/test/completion.test.ts | 84 +++++++++ src/wip/CompletionItemProvider.ts | 173 ------------------- verilog-examples/member_completion.sv | 35 ++++ 6 files changed, 342 insertions(+), 173 deletions(-) create mode 100644 src/providers/CompletionItemProvider.ts create mode 100644 src/test/completion.test.ts delete mode 100644 src/wip/CompletionItemProvider.ts create mode 100644 verilog-examples/member_completion.sv diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d903b..8d44914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ More details can be found on the [Releases](https://github.com/eirikpre/VSCode-S * Fixed syntax highlighting breaking on lines following a port connection that contains nested parentheses, such as `.data(func(a, b))` ([#188](https://github.com/eirikpre/VSCode-SystemVerilog/issues/188)) by @joecrop * Fixed code folding of `` `ifdef ``/`` `else ``/`` `endif `` blocks: an `` `ifdef `` now folds to its matching `` `endif `` with correct nesting instead of overrunning ([#142](https://github.com/eirikpre/VSCode-SystemVerilog/issues/142)) by @joecrop * Fixed go-to-definition and hover failing for modules whose ANSI header contains a package import or a comment before the parameter list, e.g. `module foo import pkg::*; #(...)` ([#189](https://github.com/eirikpre/VSCode-SystemVerilog/issues/189)) by @joecrop +* Added member auto-completion: typing `.` suggests struct/union fields, class members, and module ports, and `::` suggests package members ([#82](https://github.com/eirikpre/VSCode-SystemVerilog/issues/82)) by @joecrop ### [0.14.0] diff --git a/src/extension.ts b/src/extension.ts index a8a3eaa..bdce468 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,6 +12,7 @@ import { SystemVerilogModuleInstantiator } from './providers/ModuleInstantiator' import { SystemVerilogIndexer } from './indexer'; import { IndexerClient } from './utils/indexer-client'; import { applyIconPreference } from './file-icons'; +import { SystemVerilogCompletionItemProvider } from './providers/CompletionItemProvider'; // The LSP's client let client: LanguageClient; @@ -121,6 +122,7 @@ export function activate(context: ExtensionContext) { const formatProvider = new SystemVerilogFormatProvider(outputChannel); const moduleInstantiator = new SystemVerilogModuleInstantiator(formatProvider, symProvider); const referenceProvider = new SystemVerilogReferenceProvider(defProvider); + const completionProvider = new SystemVerilogCompletionItemProvider(indexer); context.subscriptions.push(statusBar); context.subscriptions.push(languages.registerDocumentSymbolProvider(selector, docProvider)); @@ -130,6 +132,9 @@ export function activate(context: ExtensionContext) { context.subscriptions.push(languages.registerDocumentRangeFormattingEditProvider(selector, formatProvider)); context.subscriptions.push(languages.registerDocumentFormattingEditProvider(selector, formatProvider)); context.subscriptions.push(languages.registerReferenceProvider(selector, referenceProvider)); + // Member/port/package completion. Trigger on '.' (members, ports) and ':' + // (the provider only acts on a full '::' for package scope). + context.subscriptions.push(languages.registerCompletionItemProvider(selector, completionProvider, '.', ':')); const buildHandler = () => { indexer.build_index(); diff --git a/src/providers/CompletionItemProvider.ts b/src/providers/CompletionItemProvider.ts new file mode 100644 index 0000000..448d588 --- /dev/null +++ b/src/providers/CompletionItemProvider.ts @@ -0,0 +1,217 @@ +import { CompletionItemProvider, CompletionItem, CompletionItemKind, TextDocument, Position, Range, CancellationToken, CompletionList, SnippetString, workspace, commands, SymbolInformation } from 'vscode'; // prettier-ignore +import { SystemVerilogIndexer } from '../indexer'; +import { SymbolWire } from '../wire-types'; +import { moduleFromPort } from './DefinitionProvider'; + +// Members of types nested inside a package/module live below the default +// indexing depth (maxDepthForPrecision === 1), so completions re-parse the +// defining file at a generous depth to surface them. +const DEEP = 32; + +// See test/SymbolKind_icons.png for an overview of the icons. +export function getCompletionItemKind(name: string): CompletionItemKind { + switch (name) { + case 'parameter': + case 'localparam': + return CompletionItemKind.Property; + case 'package': + case 'import': + return CompletionItemKind.File; + case 'wire': + case 'reg': + case 'logic': + case 'bit': + case 'byte': + case 'int': + case 'integer': + return CompletionItemKind.Variable; + case 'string': + return CompletionItemKind.Text; + case 'class': + return CompletionItemKind.Class; + case 'task': + case 'function': + return CompletionItemKind.Method; + case 'interface': + return CompletionItemKind.Interface; + case 'event': + return CompletionItemKind.Event; + case 'struct': + case 'union': + return CompletionItemKind.Struct; + case 'enum': + return CompletionItemKind.Enum; + case 'typedef': + return CompletionItemKind.TypeParameter; + default: + return CompletionItemKind.Field; + } +} + +export type CompletionCtx = { kind: 'package' | 'member' | 'port'; base: string }; + +/** + Classify what the user is completing from the text before the cursor. Pure + (string-only) so it can be unit tested. Returns null when no member/port/ + package completion applies. + - `pkg::` -> package members + - `ident.` -> struct/union/class members of `ident` + - `(`/`,`/`.` -> a port connection slot inside an instantiation +*/ +export function detectContext(linePrefix: string): CompletionCtx | null { + // Package scope. Match `::` specifically; a lone `:` (also a trigger char) + // matches nothing and yields no completion. + const pkg = linePrefix.match(/([a-zA-Z_]\w*)\s*::\s*$/); + if (pkg) { + return { kind: 'package', base: pkg[1] }; + } + // Member access: a dot immediately after an identifier. + const member = linePrefix.match(/([a-zA-Z_]\w*)\s*\.\s*$/); + if (member) { + return { kind: 'member', base: member[1] }; + } + // Port connection slot: a dot that is NOT preceded by an identifier + // (start of line, or after `(` or `,`). The enclosing module is resolved + // separately via moduleFromPort. + if (/(?:^|[(,])\s*\.\s*$/.test(linePrefix)) { + return { kind: 'port', base: '' }; + } + return null; +} + +/** + Reduce a declared type string to the bare type name used as the `container` + of its members: drop packed dimensions and a `pkg::` scope. Pure. +*/ +export function normalizeTypeName(type: string): string { + if (!type) { + return ''; + } + let t = type.replace(/\[[^\]]*\]/g, ' ').trim(); + const scope = t.lastIndexOf('::'); + if (scope !== -1) { + t = t.slice(scope + 2); + } + return t.trim().split(/\s+/).pop() || ''; +} + +export class SystemVerilogCompletionItemProvider implements CompletionItemProvider { + private indexer: SystemVerilogIndexer; + + constructor(indexer: SystemVerilogIndexer) { + this.indexer = indexer; + } + + public async provideCompletionItems( + document: TextDocument, + position: Position, + _token: CancellationToken + ): Promise { + try { + const linePrefix = document.lineAt(position.line).text.slice(0, position.character); + const ctx = detectContext(linePrefix); + if (!ctx) { + return []; + } + + if (ctx.kind === 'package') { + return this.completeMembers(ctx.base, DEEP, false); + } + + if (ctx.kind === 'member') { + const typeName = await this.resolveType(document, ctx.base); + if (!typeName) { + return []; + } + return this.completeMembers(typeName, DEEP, false); + } + + // Port connection: resolve the instantiated module from the + // surrounding text, then offer its ports (depth 0 == header ports + // only, so internal nets of the module are not suggested). + const moduleName = moduleFromPort(document, new Range(position, position)); + if (!moduleName) { + return []; + } + return this.completeMembers(moduleName, 0, true); + } catch { + // Completion must never throw; surface no suggestions instead. + return []; + } + } + + /** + Resolve `name`'s declaration in the current document to its declared + type string. Re-parses the current file at depth so locals nested in + modules/classes are found; falls back to a workspace name lookup. + */ + private async resolveType(document: TextDocument, name: string): Promise { + const local = await this.indexer.client.parseText({ + path: document.uri.fsPath, + text: document.getText(), + precision: 'full_no_references', + maxDepth: DEEP + }); + const hit = local.find((s) => s.name === name && s.type && s.type !== 'potential_reference'); + if (hit) { + return normalizeTypeName(hit.type); + } + const ws = await this.indexer.client.queryByName(name, { excludeTypes: ['potential_reference'], limit: 50 }); + const wsHit = ws.find((s) => s.type && s.type !== 'potential_reference'); + return wsHit ? normalizeTypeName(wsHit.type) : undefined; + } + + /** + List the children of a container symbol (struct/union/class/package/ + module) as completion items. Finds the container's defining file via an + exact-match workspace query, re-parses it at `maxDepth`, and keeps the + symbols whose `container` is the container name. + */ + private async completeMembers(containerName: string, maxDepth: number, asPort: boolean): Promise { + if (!containerName) { + return []; + } + const wsSyms = + (await commands.executeCommand( + 'vscode.executeWorkspaceSymbolProvider', + `¤${containerName}` + )) || []; + + const seenFiles = new Set(); + const items = new Map(); + for (const ws of wsSyms) { + const fsPath = ws.location.uri.fsPath; + if (seenFiles.has(fsPath)) { + continue; + } + seenFiles.add(fsPath); + // eslint-disable-next-line no-await-in-loop + const doc = await workspace.openTextDocument(ws.location.uri); + // eslint-disable-next-line no-await-in-loop + const wireSyms = await this.indexer.client.parseText({ + path: fsPath, + text: doc.getText(), + precision: 'full_no_references', + maxDepth + }); + for (const s of wireSyms) { + if (s.container === containerName && s.type !== 'potential_reference' && !items.has(s.name)) { + items.set(s.name, this.buildItem(s, asPort)); + } + } + } + return [...items.values()]; + } + + private buildItem(sym: SymbolWire, asPort: boolean): CompletionItem { + const item = new CompletionItem(sym.name, getCompletionItemKind(sym.type)); + if (sym.type) { + item.detail = sym.type; + } + if (asPort) { + // The triggering `.` is already typed; insert `name(${cursor})`. + item.insertText = new SnippetString(`${sym.name}($1)`); + } + return item; + } +} diff --git a/src/test/completion.test.ts b/src/test/completion.test.ts new file mode 100644 index 0000000..30a2e70 --- /dev/null +++ b/src/test/completion.test.ts @@ -0,0 +1,84 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { detectContext, normalizeTypeName } from '../providers/CompletionItemProvider'; + +const examplesFolderLocation = '../../verilog-examples'; + +// --- Pure-logic unit tests (no indexing needed) ----------------------------- + +suite('Completion Context Tests', () => { + test('test #1: detectContext classifies package / member / port / none', () => { + assert.deepStrictEqual(detectContext(' x = mc_pkg::'), { kind: 'package', base: 'mc_pkg' }); + assert.deepStrictEqual(detectContext(' alias_a = s.'), { kind: 'member', base: 's' }); + assert.deepStrictEqual(detectContext(' mc_sub u_sub (.'), { kind: 'port', base: '' }); + assert.deepStrictEqual(detectContext(' .'), { kind: 'port', base: '' }); + // A lone ':' (also a trigger char) must not be treated as package scope. + assert.strictEqual(detectContext(' x = mc_pkg:'), null); + // Plain text with no trigger. + assert.strictEqual(detectContext(' logic foo'), null); + }); + + test('test #2: normalizeTypeName strips dims and package scope', () => { + assert.strictEqual(normalizeTypeName('mc_struct_t'), 'mc_struct_t'); + assert.strictEqual(normalizeTypeName('mc_pkg::mc_struct_t'), 'mc_struct_t'); + assert.strictEqual(normalizeTypeName('logic [7:0]'), 'logic'); + assert.strictEqual(normalizeTypeName(''), ''); + }); +}); + +// --- Provider integration tests (need the workspace index) ------------------ + +suite('Completion Provider Tests', () => { + const uri = vscode.Uri.file(path.join(__dirname, examplesFolderLocation, 'member_completion.sv')); + const waitFor = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay)); + + async function labelsAt(after: string): Promise { + const doc = await vscode.workspace.openTextDocument(uri); + const offset = doc.getText().indexOf(after) + after.length; + const position = doc.positionAt(offset); + const list = (await vscode.commands.executeCommand( + 'vscode.executeCompletionItemProvider', + uri, + position + )) as vscode.CompletionList; + return (list?.items || []).map((i) => (typeof i.label === 'string' ? i.label : i.label.label)); + } + + suiteSetup(async () => { + await vscode.commands.executeCommand('systemverilog.build_index'); + // Wait until the container types from this fixture are indexed. + const deadline = Date.now() + 30000; + while (Date.now() < deadline) { + const syms = (await vscode.commands.executeCommand( + 'vscode.executeWorkspaceSymbolProvider', + '¤mc_struct_t' + )) as vscode.SymbolInformation[]; + if (syms && syms.length > 0) break; + await waitFor(200); + } + }); + + test('test #1: struct members after "s." (#82)', async () => { + const labels = await labelsAt('alias_a = s.'); + assert.ok(labels.includes('alpha'), 'expected alpha; got ' + labels.join(',')); + assert.ok(labels.includes('beta'), 'expected beta; got ' + labels.join(',')); + }); + + test('test #2: package members after "mc_pkg::"', async () => { + const labels = await labelsAt('X = mc_pkg::'); + assert.ok(labels.includes('MC_PARAM'), 'expected MC_PARAM; got ' + labels.join(',')); + assert.ok(labels.includes('mc_struct_t'), 'expected mc_struct_t; got ' + labels.join(',')); + }); + + test('test #3: class members after "c."', async () => { + const labels = await labelsAt('alias_g = c.'); + assert.ok(labels.includes('gamma'), 'expected gamma; got ' + labels.join(',')); + }); + + test('test #4: module ports after "(." in an instantiation', async () => { + const labels = await labelsAt('u_sub (.'); + assert.ok(labels.includes('clk_in'), 'expected clk_in; got ' + labels.join(',')); + assert.ok(labels.includes('data_out'), 'expected data_out; got ' + labels.join(',')); + }); +}); diff --git a/src/wip/CompletionItemProvider.ts b/src/wip/CompletionItemProvider.ts deleted file mode 100644 index 81f530e..0000000 --- a/src/wip/CompletionItemProvider.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { CompletionItemProvider, CompletionItem, TextDocument, Position, CancellationToken, CompletionContext, commands, CompletionItemKind } from 'vscode'; // prettier-ignore -import { SystemVerilogIndexer } from '../indexer'; -import { SystemVerilogSymbol } from '../symbol'; - -// See test/SymbolKind_icons.png for an overview of the icons -export function getCompletionItemKind(name: String): CompletionItemKind { - switch (name) { - case 'parameter': - case 'localparam': - return CompletionItemKind.Property; - case 'package': - case 'import': - return CompletionItemKind.File; - case 'wire': - case 'reg': - case 'logic': - return CompletionItemKind.Variable; - case 'string': - return CompletionItemKind.Text; - case 'class': - return CompletionItemKind.Class; - case 'task': - return CompletionItemKind.Method; - case 'function': - return CompletionItemKind.Function; - case 'interface': - return CompletionItemKind.Interface; - case 'event': - return CompletionItemKind.Event; - case 'struct': - return CompletionItemKind.Struct; - case 'program': - case 'module': - default: - return CompletionItemKind.Reference; - } -} - -export class SystemVerilogCompletionItemProvider implements CompletionItemProvider { - private indexer: SystemVerilogIndexer; - private globals: CompletionItem[]; - private known_types: CompletionItem[]; - - constructor(indexer: SystemVerilogIndexer) { - this.indexer = indexer; - - // See CompletionItemKind for overview - this.globals = [ - new CompletionItem('begin', CompletionItemKind.Module), - new CompletionItem('end', CompletionItemKind.Module), - new CompletionItem('parameter', CompletionItemKind.Constant), - new CompletionItem('localparam', CompletionItemKind.Constant), - new CompletionItem('logic', CompletionItemKind.Variable) - ]; - - this.known_types = [ - new CompletionItem('input', CompletionItemKind.Interface), - new CompletionItem('output', CompletionItemKind.Interface) - ]; - } - - //Entrypoint for getting completion items - provideCompletionItems( - document: TextDocument, - position: Position, - token: CancellationToken, - context: CompletionContext - ): Promise { - return new Promise((resolve) => { - let completionItems: CompletionItem[] = new Array(); - completionItems = completionItems.concat(this.globals); - // var lookupRange = document.getWordRangeAtPosition(position); - // var lookupTerm = document.getText(lookupRange); - - // get all DocumentSymbolproviders and step to each of them - return commands - .executeCommand('vscode.executeDocumentSymbolProvider', document.uri) - .then((symbols: SystemVerilogSymbol[]) => { - symbols.forEach((value: SystemVerilogSymbol) => { - console.log(value.containerName); - completionItems.push(this.constructModuleItem(value)); - }); - }) - .then((_) => { - return resolve(completionItems); - }); - - // this.indexer.provideWorkspaceSymbols(lookupTerm, token, false).then((symbols: SystemVerilogSymbol[]) => { - // symbols.forEach((value: SystemVerilogSymbol) => { - // if(value.kind = SymbolKind.Module){ - // completionItems.push(this.constructModuleItem(value)); - - // } - // }, completionItems); - // resolve(completionItems); - // }); - // }); - }); - } - - // Contruct completion item for all system verilog module items - constructModuleItem(symbol: SystemVerilogSymbol): CompletionItem { - let completionItem = new CompletionItem(symbol.name, getCompletionItemKind(symbol.containerName)); - return completionItem; - } - - // resolveCompletionItem(item:CompletionItem, token:CancellationToken): CompletionItem { - - // var descMarkdownString = new MarkdownString(); - // descMarkdownString.appendCodeblock(this.indexer.modules[item.label+item.insertText].toString(), "systemverilog"); - // item.documentation = descMarkdownString; - - // item.insertText = new SnippetString(this.createModuleInsertionText(item)); - // return item; - // }; - - // createModuleInsertionText(item: CompletionItem) : string { - - // var rawText = this.indexer.modules[item.label+item.insertText]; - // var text = rawText.replace(/\/\*[\s\S]*?\*\/|([\\:]|^)\/\/.*$/gm, ''); - // var hasParameters = 0; - // var parameters = []; - // var insertText = ""; - // var tabstopCnt = 2; - - // if (text.indexOf("#") > -1){ - // hasParameters = 1; - // parameters = text.slice(text.indexOf("(") + 1, text.indexOf(")")).split(","); - // } - // var signals = text.slice(text.lastIndexOf("(") + 1, text.lastIndexOf(")")).split(","); - - // // Extracting list of parameters - // if(hasParameters ){ - // for(var i = 0; i < parameters.length; i++){ - // var splitParameter = parameters[i].trim().replace(/\[(.*?)\]/,"").split(/ +/); - // var p = splitParameter[splitParameter.length - 1]; - // parameters[i] = p; - // } - // } - - // // Extracting list of signals - // for(var i = 0; i < signals.length; i++){ - // var splitSignal = signals[i].trim().replace(/\[(.*?)\]/,"").split(/ +/); - // var s = splitSignal[splitSignal.length - 1]; - // signals[i] = s; - // } - - // // Creatign the insertText based on the module name parameters and signals - // insertText = item.label; - // if (hasParameters){ - // insertText = insertText + + hasParameters ? " #(\n" : ""; - // for(var i = 0; i < parameters.length; i++ ){ - // if (i != 0){ - // insertText = insertText + ",\n"; - // } - // insertText = insertText + "\t." + parameters[i] + "($" + tabstopCnt.toString() + ")"; - // tabstopCnt++; - // } - // insertText = insertText + "\n)"; - // } - - // insertText = insertText + " $1 (\n"; - // for(var i = 0; i < signals.length; i++ ){ - // if (i != 0){ - // insertText = insertText + ",\n"; - // } - // insertText = insertText + "\t." + signals[i] + "($" + tabstopCnt.toString() + ")"; - // tabstopCnt++; - // } - // insertText = insertText + "\n);"; - // return insertText; - // }; -} diff --git a/verilog-examples/member_completion.sv b/verilog-examples/member_completion.sv new file mode 100644 index 0000000..b55c12f --- /dev/null +++ b/verilog-examples/member_completion.sv @@ -0,0 +1,35 @@ +// Fixtures for member/port/package auto-completion tests (issue #82). +// Self-contained: the container types are defined and instantiated here so the +// completion provider can resolve them within this one file. +package mc_pkg; + parameter MC_PARAM = 1; + typedef struct packed { + logic alpha; + logic beta; + } mc_struct_t; +endpackage + +class mc_class; + int gamma; +endclass + +module mc_sub ( + input logic clk_in, + output logic data_out +); +endmodule + +module mc_top; + import mc_pkg::*; + mc_struct_t s; + mc_class c; + + logic alias_a; + int alias_g; + localparam X = mc_pkg::MC_PARAM; + + assign alias_a = s.alpha; + assign alias_g = c.gamma; + + mc_sub u_sub (.clk_in(alias_a), .data_out(alias_a)); +endmodule From 650ce7e2157a51eb6ce9ba72bfdcb4a662fe4315 Mon Sep 17 00:00:00 2001 From: Joe Crop Date: Wed, 3 Jun 2026 13:50:42 -0700 Subject: [PATCH 2/7] docs: highlight member completion in README; bump to 0.15.0 Add the new member/port/package auto-completion (#82) to the README feature list and Examples section, and bump the version to 0.15.0 for this feature release (moving the #82 entry to a new [0.15.0] CHANGELOG section). Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 5 ++++- README.md | 10 ++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d44914..39ef190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ More details can be found on the [Releases](https://github.com/eirikpre/VSCode-S - 💡 Back-end Language Server for Systemverilog - 💡 Complete syntax highlighting +### [0.15.0] + +* Added member auto-completion: typing `.` suggests struct/union fields, class members, and module ports, and `::` suggests package members ([#82](https://github.com/eirikpre/VSCode-SystemVerilog/issues/82)) by @joecrop + ### [0.14.2] * Fixed syntax highlighting of parameterized module instantiations (`mod #(...) u_mod (...)`): the module name, instance name and port connections are highlighted again, instead of being shadowed by the class-instance rule by @joecrop @@ -24,7 +28,6 @@ More details can be found on the [Releases](https://github.com/eirikpre/VSCode-S * Fixed syntax highlighting breaking on lines following a port connection that contains nested parentheses, such as `.data(func(a, b))` ([#188](https://github.com/eirikpre/VSCode-SystemVerilog/issues/188)) by @joecrop * Fixed code folding of `` `ifdef ``/`` `else ``/`` `endif `` blocks: an `` `ifdef `` now folds to its matching `` `endif `` with correct nesting instead of overrunning ([#142](https://github.com/eirikpre/VSCode-SystemVerilog/issues/142)) by @joecrop * Fixed go-to-definition and hover failing for modules whose ANSI header contains a package import or a comment before the parameter list, e.g. `module foo import pkg::*; #(...)` ([#189](https://github.com/eirikpre/VSCode-SystemVerilog/issues/189)) by @joecrop -* Added member auto-completion: typing `.` suggests struct/union fields, class members, and module ports, and `::` suggests package members ([#82](https://github.com/eirikpre/VSCode-SystemVerilog/issues/82)) by @joecrop ### [0.14.0] diff --git a/README.md b/README.md index 4c16415..07290a5 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This VS Code extension provides features to read, navigate and write SystemVeril - Go to symbol in workspace folder (indexed modules/interfaces/programs/classes/packages) (`Ctrl+T`) - Go to definition (_works for module/interface/program/class/package names and for ports too!_) (`Ctrl+LeftClick`) - Find references (_works for module/interface/program/class/package names and for ports too!_) (`Ctrl+LeftClick`) +- Member auto-completion for struct/union fields, class members, package members (`pkg::`) and module ports — automatically after `.` / `::`, or with `Ctrl+Space` - Quick-start on already indexed workspaces - Code snippets for many common blocks - Instantiate module from already indexed module @@ -39,6 +40,15 @@ This VS Code extension provides features to read, navigate and write SystemVeril ![Module Instantiation Example](resources/moduleInit_demo.gif) +### Member Auto-Completion + +Type `.` after a variable or `::` after a package name to get context-aware member suggestions: + +- `my_struct.` → fields of the struct/union type +- `my_object.` → properties and methods of the class type +- `my_package::` → members declared in the package (parameters, typedefs, functions, …) +- inside a module instantiation, `.` → the instantiated module's ports + ## Recommendations - If you have netlists in your workspace you can exclude them in the settings with `systemverilog.excludeIndexing`, e.g.: `**/syn/**` diff --git a/package-lock.json b/package-lock.json index 9639a85..65deed8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "systemverilog", - "version": "0.14.2", + "version": "0.15.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "systemverilog", - "version": "0.14.2", + "version": "0.15.0", "license": "MIT", "dependencies": { "antlr4ts": "^0.5.0-alpha.4", diff --git a/package.json b/package.json index 746b78c..6aefbc3 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "systemverilog", "displayName": "SystemVerilog - Language Support", "description": "Language support for Verilog and SystemVerilog.", - "version": "0.14.2", + "version": "0.15.0", "publisher": "eirikpre", "author": { "name": "Eirik Prestegårdshus", From e051f151880eb698acbdea2980be64bb25f46134 Mon Sep 17 00:00:00 2001 From: Joe Crop Date: Wed, 3 Jun 2026 13:56:56 -0700 Subject: [PATCH 3/7] perf: cache completion parses; show status-bar progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completion latency came from re-parsing files on every invocation. Add a per-file parse cache keyed by `fsPath|maxDepth` and invalidated by the document version, so repeat completions (and same-file struct/package lookups) reuse the parse instead of round-tripping to the indexer worker. Also show a transient status-bar spinner ("$(sync~spin) SystemVerilog: resolving members…") while the lookup runs, so the user gets feedback during the (now rarer) slow path. The message clears automatically when the work settles. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/providers/CompletionItemProvider.ts | 94 ++++++++++++++++--------- 1 file changed, 59 insertions(+), 35 deletions(-) diff --git a/src/providers/CompletionItemProvider.ts b/src/providers/CompletionItemProvider.ts index 448d588..713a6d0 100644 --- a/src/providers/CompletionItemProvider.ts +++ b/src/providers/CompletionItemProvider.ts @@ -1,4 +1,4 @@ -import { CompletionItemProvider, CompletionItem, CompletionItemKind, TextDocument, Position, Range, CancellationToken, CompletionList, SnippetString, workspace, commands, SymbolInformation } from 'vscode'; // prettier-ignore +import { CompletionItemProvider, CompletionItem, CompletionItemKind, TextDocument, Position, Range, CancellationToken, CompletionList, SnippetString, workspace, window, commands, SymbolInformation } from 'vscode'; // prettier-ignore import { SystemVerilogIndexer } from '../indexer'; import { SymbolWire } from '../wire-types'; import { moduleFromPort } from './DefinitionProvider'; @@ -98,6 +98,11 @@ export function normalizeTypeName(type: string): string { export class SystemVerilogCompletionItemProvider implements CompletionItemProvider { private indexer: SystemVerilogIndexer; + // Per-file parse cache keyed by `fsPath|maxDepth`, invalidated by the + // document version. Avoids re-parsing the same (unchanged) file on every + // completion — the bulk of the latency. One entry per (file, depth). + private parseCache = new Map(); + constructor(indexer: SystemVerilogIndexer) { this.indexer = indexer; } @@ -107,37 +112,66 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid position: Position, _token: CancellationToken ): Promise { + let ctx: CompletionCtx | null = null; try { const linePrefix = document.lineAt(position.line).text.slice(0, position.character); - const ctx = detectContext(linePrefix); - if (!ctx) { - return []; - } + ctx = detectContext(linePrefix); + } catch { + return []; + } + if (!ctx) { + return []; + } - if (ctx.kind === 'package') { - return this.completeMembers(ctx.base, DEEP, false); - } + // Show a transient status-bar spinner while the (potentially slow) + // member resolution runs; it clears automatically when the work + // settles. Completion must never throw — fall back to no suggestions. + const work = this.resolve(ctx, document, position).catch(() => [] as CompletionItem[]); + window.setStatusBarMessage('$(sync~spin) SystemVerilog: resolving members…', work); + return work; + } - if (ctx.kind === 'member') { - const typeName = await this.resolveType(document, ctx.base); - if (!typeName) { - return []; - } - return this.completeMembers(typeName, DEEP, false); - } + private async resolve(ctx: CompletionCtx, document: TextDocument, position: Position): Promise { + if (ctx.kind === 'package') { + return this.completeMembers(ctx.base, DEEP, false); + } - // Port connection: resolve the instantiated module from the - // surrounding text, then offer its ports (depth 0 == header ports - // only, so internal nets of the module are not suggested). - const moduleName = moduleFromPort(document, new Range(position, position)); - if (!moduleName) { + if (ctx.kind === 'member') { + const typeName = await this.resolveType(document, ctx.base); + if (!typeName) { return []; } - return this.completeMembers(moduleName, 0, true); - } catch { - // Completion must never throw; surface no suggestions instead. + return this.completeMembers(typeName, DEEP, false); + } + + // Port connection: resolve the instantiated module from the surrounding + // text, then offer its ports (depth 0 == header ports only, so internal + // nets of the module are not suggested). + const moduleName = moduleFromPort(document, new Range(position, position)); + if (!moduleName) { return []; } + return this.completeMembers(moduleName, 0, true); + } + + /** + Parse a document at `maxDepth`, reusing a cached result while the + document version is unchanged. + */ + private async parseDoc(doc: TextDocument, maxDepth: number): Promise { + const key = `${doc.uri.fsPath}|${maxDepth}`; + const cached = this.parseCache.get(key); + if (cached && cached.version === doc.version) { + return cached.syms; + } + const syms = await this.indexer.client.parseText({ + path: doc.uri.fsPath, + text: doc.getText(), + precision: 'full_no_references', + maxDepth + }); + this.parseCache.set(key, { version: doc.version, syms }); + return syms; } /** @@ -146,12 +180,7 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid modules/classes are found; falls back to a workspace name lookup. */ private async resolveType(document: TextDocument, name: string): Promise { - const local = await this.indexer.client.parseText({ - path: document.uri.fsPath, - text: document.getText(), - precision: 'full_no_references', - maxDepth: DEEP - }); + const local = await this.parseDoc(document, DEEP); const hit = local.find((s) => s.name === name && s.type && s.type !== 'potential_reference'); if (hit) { return normalizeTypeName(hit.type); @@ -188,12 +217,7 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid // eslint-disable-next-line no-await-in-loop const doc = await workspace.openTextDocument(ws.location.uri); // eslint-disable-next-line no-await-in-loop - const wireSyms = await this.indexer.client.parseText({ - path: fsPath, - text: doc.getText(), - precision: 'full_no_references', - maxDepth - }); + const wireSyms = await this.parseDoc(doc, maxDepth); for (const s of wireSyms) { if (s.container === containerName && s.type !== 'potential_reference' && !items.has(s.name)) { items.set(s.name, this.buildItem(s, asPort)); From 08eca1bc365274dfd8679352107d2fbba7b7e1c3 Mon Sep 17 00:00:00 2001 From: Joe Crop Date: Wed, 3 Jun 2026 14:17:44 -0700 Subject: [PATCH 4/7] Complete enum values in comparisons and case labels (#82) Extend member completion to enum values: typing `fsm_state ==`/`!=` or a case-item label inside `case (fsm_state)` now suggests the enum's values. - parser-core: index enum values as members of the enum type (getEnumValues), so they flow through the same container-based lookup as struct fields. - symbol-kinds: map `enum_value` to SymbolKind.EnumMember. - CompletionItemProvider: detect `expr ==`/`!=`/`===`/`!==` (a new `value` context) and case-item label positions (enclosingCaseExpr scans back to the enclosing `case (expr)` selector), then list that type's members. Note: this is the enum idiom; for packed structs the fields are reached with `.` (already supported), not via `==`/case. Updates the fixture to a typedef enum and adds parser + provider tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 2 +- src/parser-core.ts | 42 +++++++++++++++-- src/providers/CompletionItemProvider.ts | 61 ++++++++++++++++++++++--- src/symbol-kinds.ts | 3 ++ src/test/completion.test.ts | 33 ++++++++++++- src/test/parser.test.ts | 11 +++++ verilog-examples/member_completion.sv | 21 ++++++++- 7 files changed, 158 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39ef190..b41e67c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ More details can be found on the [Releases](https://github.com/eirikpre/VSCode-S ### [0.15.0] -* Added member auto-completion: typing `.` suggests struct/union fields, class members, and module ports, and `::` suggests package members ([#82](https://github.com/eirikpre/VSCode-SystemVerilog/issues/82)) by @joecrop +* Added member auto-completion: `.` suggests struct/union fields, class members, and module ports; `::` suggests package members; and enum values are suggested in `==`/`!=` comparisons and `case`-item labels ([#82](https://github.com/eirikpre/VSCode-SystemVerilog/issues/82)) by @joecrop ### [0.14.2] diff --git a/src/parser-core.ts b/src/parser-core.ts index d26cb47..c945667 100644 --- a/src/parser-core.ts +++ b/src/parser-core.ts @@ -397,10 +397,16 @@ export class SystemVerilogParser { ); } if (match.groups!.body) { - subBlocks.push({ - match, - bodyOffset: match.index! + offset + match[0].indexOf(match.groups!.body) - }); + const bodyOffset = match.index! + offset + match[0].indexOf(match.groups!.body); + if (type === 'typedef' && /\benum\b/.test(match[0]) && precision.includes('full')) { + // An enum body is a comma-separated value list, not + // declarations, so extract the values directly as + // members of the enum type (so they can be completed + // in `==`/case contexts). + symbols.push(...this.getEnumValues(source, match.groups!.body, bodyOffset, name)); + } else { + subBlocks.push({ match, bodyOffset }); + } } } } @@ -442,6 +448,34 @@ export class SystemVerilogParser { } } + // Extract the value identifiers from an enum body (the text between the + // braces), e.g. `RED, GREEN = 2, BLUE[1:0]` -> RED, GREEN, BLUE. Each is + // emitted as a member of the enum type `parent`. + private getEnumValues(source: ParseSource, text: string, offset: number, parent: string): SymbolWire[] { + const out: SymbolWire[] = []; + const re = /(?:^|,)\s*([a-zA-Z_]\w*)/g; + // eslint-disable-next-line no-constant-condition + while (true) { + const m = re.exec(text); + if (m == null) break; + const nameStart = m.index + m[0].indexOf(m[1]); + const start = source.positionAt(nameStart + offset); + const end = source.positionAt(nameStart + m[1].length + offset); + out.push({ + name: m[1], + type: 'enum_value', + kind: getSymbolKindInt('enum_value'), + container: parent, + file: source.fsPath, + sl: start.line, + sc: start.character, + el: end.line, + ec: end.character + }); + } + return out; + } + private getPorts(source: ParseSource, text: string, offset: number, parent: string): SymbolWire[] { const out: SymbolWire[] = []; const re = new RegExp(this.r_ports.source, this.r_ports.flags); diff --git a/src/providers/CompletionItemProvider.ts b/src/providers/CompletionItemProvider.ts index 713a6d0..d96a2be 100644 --- a/src/providers/CompletionItemProvider.ts +++ b/src/providers/CompletionItemProvider.ts @@ -41,6 +41,8 @@ export function getCompletionItemKind(name: string): CompletionItemKind { return CompletionItemKind.Struct; case 'enum': return CompletionItemKind.Enum; + case 'enum_value': + return CompletionItemKind.EnumMember; case 'typedef': return CompletionItemKind.TypeParameter; default: @@ -48,15 +50,16 @@ export function getCompletionItemKind(name: string): CompletionItemKind { } } -export type CompletionCtx = { kind: 'package' | 'member' | 'port'; base: string }; +export type CompletionCtx = { kind: 'package' | 'member' | 'port' | 'value'; base: string }; /** Classify what the user is completing from the text before the cursor. Pure (string-only) so it can be unit tested. Returns null when no member/port/ - package completion applies. - - `pkg::` -> package members - - `ident.` -> struct/union/class members of `ident` - - `(`/`,`/`.` -> a port connection slot inside an instantiation + package/value completion applies. + - `pkg::` -> package members + - `ident.` -> struct/union/class members of `ident` + - `ident ==` -> values of `ident`'s type (e.g. enum values) + - `(`/`,`/`.` -> a port connection slot inside an instantiation */ export function detectContext(linePrefix: string): CompletionCtx | null { // Package scope. Match `::` specifically; a lone `:` (also a trigger char) @@ -70,6 +73,12 @@ export function detectContext(linePrefix: string): CompletionCtx | null { if (member) { return { kind: 'member', base: member[1] }; } + // Comparison against a value of the operand's type (typically an enum): + // `expr ==`/`!=`/`===`/`!==`. The right-hand side may be partially typed. + const cmp = linePrefix.match(/([a-zA-Z_]\w*)\s*(?:===|!==|==|!=)\s*[a-zA-Z_]?\w*$/); + if (cmp) { + return { kind: 'value', base: cmp[1] }; + } // Port connection slot: a dot that is NOT preceded by an identifier // (start of line, or after `(` or `,`). The enclosing module is resolved // separately via moduleFromPort. @@ -116,6 +125,15 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid try { const linePrefix = document.lineAt(position.line).text.slice(0, position.character); ctx = detectContext(linePrefix); + if (!ctx && /^\s*\w*$/.test(linePrefix)) { + // A bare identifier at the start of a line may be a case-item + // label; offer the values of the enclosing `case (expr)` + // selector's type (typically an enum). + const caseVar = this.enclosingCaseExpr(document, position.line); + if (caseVar) { + ctx = { kind: 'value', base: caseVar }; + } + } } catch { return []; } @@ -136,7 +154,9 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid return this.completeMembers(ctx.base, DEEP, false); } - if (ctx.kind === 'member') { + if (ctx.kind === 'member' || ctx.kind === 'value') { + // Resolve the operand's type, then list that type's members + // (struct/class fields for `.`, enum values for `==`/case). const typeName = await this.resolveType(document, ctx.base); if (!typeName) { return []; @@ -154,6 +174,29 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid return this.completeMembers(moduleName, 0, true); } + /** + If `line` is inside a `case (expr) … endcase`, return the case selector + identifier. Scans upward, skipping already-closed case blocks, so nested + cases resolve to the innermost enclosing one. Bounded for performance. + */ + private enclosingCaseExpr(document: TextDocument, line: number): string | undefined { + let closed = 0; + for (let ln = line - 1; ln >= 0 && line - ln < 200; ln -= 1) { + const text = document.lineAt(ln).text; + if (/\bendcase\b/.test(text)) { + closed += 1; + } + const m = text.match(/\bcase[xz]?\s*\(\s*([a-zA-Z_]\w*)/); + if (m) { + if (closed === 0) { + return m[1]; + } + closed -= 1; + } + } + return undefined; + } + /** Parse a document at `maxDepth`, reusing a cached result while the document version is unchanged. @@ -229,7 +272,11 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid private buildItem(sym: SymbolWire, asPort: boolean): CompletionItem { const item = new CompletionItem(sym.name, getCompletionItemKind(sym.type)); - if (sym.type) { + // For enum values the raw type is the internal 'enum_value' marker, so + // show the enum type (the container) instead; otherwise show the type. + if (sym.type === 'enum_value') { + item.detail = sym.container || 'enum'; + } else if (sym.type) { item.detail = sym.type; } if (asPort) { diff --git a/src/symbol-kinds.ts b/src/symbol-kinds.ts index de591bd..ed55375 100644 --- a/src/symbol-kinds.ts +++ b/src/symbol-kinds.ts @@ -83,6 +83,9 @@ export function getSymbolKindInt(name: string): number { case 'enum': case 'Enum': return SymbolKind.Enum; + case 'enum_value': + case 'EnumMember': + return SymbolKind.EnumMember; case 'modport': case 'Null': return SymbolKind.Null; diff --git a/src/test/completion.test.ts b/src/test/completion.test.ts index 30a2e70..cc43da2 100644 --- a/src/test/completion.test.ts +++ b/src/test/completion.test.ts @@ -13,6 +13,9 @@ suite('Completion Context Tests', () => { assert.deepStrictEqual(detectContext(' alias_a = s.'), { kind: 'member', base: 's' }); assert.deepStrictEqual(detectContext(' mc_sub u_sub (.'), { kind: 'port', base: '' }); assert.deepStrictEqual(detectContext(' .'), { kind: 'port', base: '' }); + // Comparison against a value (e.g. an enum value). + assert.deepStrictEqual(detectContext(' if (fsm_state == '), { kind: 'value', base: 'fsm_state' }); + assert.deepStrictEqual(detectContext(' x = a != b'), { kind: 'value', base: 'a' }); // A lone ':' (also a trigger char) must not be treated as package scope. assert.strictEqual(detectContext(' x = mc_pkg:'), null); // Plain text with no trigger. @@ -33,9 +36,8 @@ suite('Completion Provider Tests', () => { const uri = vscode.Uri.file(path.join(__dirname, examplesFolderLocation, 'member_completion.sv')); const waitFor = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay)); - async function labelsAt(after: string): Promise { + async function labelsAtOffset(offset: number): Promise { const doc = await vscode.workspace.openTextDocument(uri); - const offset = doc.getText().indexOf(after) + after.length; const position = doc.positionAt(offset); const list = (await vscode.commands.executeCommand( 'vscode.executeCompletionItemProvider', @@ -45,6 +47,18 @@ suite('Completion Provider Tests', () => { return (list?.items || []).map((i) => (typeof i.label === 'string' ? i.label : i.label.label)); } + // Position the cursor immediately after `after`. + async function labelsAt(after: string): Promise { + const doc = await vscode.workspace.openTextDocument(uri); + return labelsAtOffset(doc.getText().indexOf(after) + after.length); + } + + // Position the cursor immediately before `before`. + async function labelsBefore(before: string): Promise { + const doc = await vscode.workspace.openTextDocument(uri); + return labelsAtOffset(doc.getText().indexOf(before)); + } + suiteSetup(async () => { await vscode.commands.executeCommand('systemverilog.build_index'); // Wait until the container types from this fixture are indexed. @@ -81,4 +95,19 @@ suite('Completion Provider Tests', () => { assert.ok(labels.includes('clk_in'), 'expected clk_in; got ' + labels.join(',')); assert.ok(labels.includes('data_out'), 'expected data_out; got ' + labels.join(',')); }); + + test('test #5: enum values after "==" comparison', async () => { + const labels = await labelsAt('fsm_state == '); + assert.ok(labels.includes('RED'), 'expected RED; got ' + labels.join(',')); + assert.ok(labels.includes('GREEN'), 'expected GREEN; got ' + labels.join(',')); + assert.ok(labels.includes('BLUE'), 'expected BLUE; got ' + labels.join(',')); + }); + + test('test #6: enum values as case-item labels', async () => { + // Cursor at the start of the "RED:" label line (prefix is whitespace). + const labels = await labelsBefore('RED:'); + assert.ok(labels.includes('RED'), 'expected RED; got ' + labels.join(',')); + assert.ok(labels.includes('GREEN'), 'expected GREEN; got ' + labels.join(',')); + assert.ok(labels.includes('BLUE'), 'expected BLUE; got ' + labels.join(',')); + }); }); diff --git a/src/test/parser.test.ts b/src/test/parser.test.ts index 3208dc3..0179267 100644 --- a/src/test/parser.test.ts +++ b/src/test/parser.test.ts @@ -67,4 +67,15 @@ suite('Parser Tests', () => { 'port y under foo' ); }); + + test('test #6: enum values are indexed as members of the enum type (#82)', () => { + const text = 'package p;\n typedef enum logic [1:0] { RED, GREEN = 2, BLUE } color_e;\nendpackage\n'; + const syms = symbols(text); + const values = syms.filter((s) => s.container === 'color_e').map((s) => s.name); + assert.deepStrictEqual(values, ['RED', 'GREEN', 'BLUE']); + assert.ok( + syms.every((s) => s.type !== 'enum_value' || s.container === 'color_e'), + 'enum values are contained by the enum type' + ); + }); }); diff --git a/verilog-examples/member_completion.sv b/verilog-examples/member_completion.sv index b55c12f..ab321e8 100644 --- a/verilog-examples/member_completion.sv +++ b/verilog-examples/member_completion.sv @@ -1,4 +1,4 @@ -// Fixtures for member/port/package auto-completion tests (issue #82). +// Fixtures for member/port/package/value auto-completion tests (issue #82). // Self-contained: the container types are defined and instantiated here so the // completion provider can resolve them within this one file. package mc_pkg; @@ -7,6 +7,11 @@ package mc_pkg; logic alpha; logic beta; } mc_struct_t; + typedef enum logic [1:0] { + RED, + GREEN, + BLUE + } mc_color_e; endpackage class mc_class; @@ -23,6 +28,7 @@ module mc_top; import mc_pkg::*; mc_struct_t s; mc_class c; + mc_color_e fsm_state; logic alias_a; int alias_g; @@ -32,4 +38,17 @@ module mc_top; assign alias_g = c.gamma; mc_sub u_sub (.clk_in(alias_a), .data_out(alias_a)); + + always_comb begin + int i = 0; + // case-item labels are enum values of fsm_state's type + case (fsm_state) + RED: i = 1; + GREEN: i = 2; + default: i = 0; + endcase + + // comparison against an enum value + if (fsm_state == BLUE) i = 3; + end endmodule From 1dc0cd3b18b49f90c15712add347a1022d05fb9e Mon Sep 17 00:00:00 2001 From: Joe Crop Date: Wed, 3 Jun 2026 14:31:04 -0700 Subject: [PATCH 5/7] Scope member completion to the in-scope definition (#82) Completion looked up a type/container by name across the whole workspace and merged the members of every same-named definition, so a type name reused in several local-scoped files (e.g. a per-file `state_e`) produced a union of all their members. Resolve with SystemVerilog scope instead: 1. a definition in the current file wins (local scope); 2. otherwise, when the name is defined in multiple files, prefer the definition whose package is imported by the current file (via the file's `import pkg::*` statements), falling back to a single match rather than the union of every same-named definition. Adds scope_a.sv / scope_b.sv (same type name, different values) and a test asserting the other file's values are not offered. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/providers/CompletionItemProvider.ts | 77 ++++++++++++++++++++----- src/test/completion.test.ts | 18 ++++++ verilog-examples/scope_a.sv | 8 +++ verilog-examples/scope_b.sv | 5 ++ 4 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 verilog-examples/scope_a.sv create mode 100644 verilog-examples/scope_b.sv diff --git a/src/providers/CompletionItemProvider.ts b/src/providers/CompletionItemProvider.ts index d96a2be..6fb68b4 100644 --- a/src/providers/CompletionItemProvider.ts +++ b/src/providers/CompletionItemProvider.ts @@ -151,7 +151,7 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid private async resolve(ctx: CompletionCtx, document: TextDocument, position: Position): Promise { if (ctx.kind === 'package') { - return this.completeMembers(ctx.base, DEEP, false); + return this.completeMembers(ctx.base, DEEP, false, document); } if (ctx.kind === 'member' || ctx.kind === 'value') { @@ -161,7 +161,7 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid if (!typeName) { return []; } - return this.completeMembers(typeName, DEEP, false); + return this.completeMembers(typeName, DEEP, false, document); } // Port connection: resolve the instantiated module from the surrounding @@ -171,7 +171,7 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid if (!moduleName) { return []; } - return this.completeMembers(moduleName, 0, true); + return this.completeMembers(moduleName, 0, true, document); } /** @@ -235,23 +235,52 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid /** List the children of a container symbol (struct/union/class/package/ - module) as completion items. Finds the container's defining file via an - exact-match workspace query, re-parses it at `maxDepth`, and keeps the - symbols whose `container` is the container name. + module) as completion items, respecting SystemVerilog scope so that + unrelated same-named definitions in other files are not merged in: + 1. a definition in the current file wins (local scope); + 2. otherwise resolve via the workspace, but when the name is defined in + more than one file, prefer the definition whose package is imported + by the current file, falling back to a single (first) match rather + than the union of every same-named definition. */ - private async completeMembers(containerName: string, maxDepth: number, asPort: boolean): Promise { + private async completeMembers( + containerName: string, + maxDepth: number, + asPort: boolean, + document: TextDocument + ): Promise { if (!containerName) { return []; } + const matches = (s: SymbolWire) => s.container === containerName && s.type !== 'potential_reference'; + + // 1) Local definition in the current file takes precedence. + const localMembers = (await this.parseDoc(document, maxDepth)).filter(matches); + if (localMembers.length > 0) { + return this.toItems(localMembers, asPort); + } + + // 2) Resolve via the workspace, scoped to the in-scope definition. const wsSyms = (await commands.executeCommand( 'vscode.executeWorkspaceSymbolProvider', `¤${containerName}` )) || []; + if (wsSyms.length === 0) { + return []; + } + let chosen = wsSyms; + if (wsSyms.length > 1) { + const imported = this.importedScopes(document); + const scoped = wsSyms.filter((s) => s.containerName && imported.has(s.containerName)); + // Prefer the imported-package definition(s); otherwise a single + // match, never the union of every same-named definition. + chosen = scoped.length > 0 ? scoped : [wsSyms[0]]; + } const seenFiles = new Set(); - const items = new Map(); - for (const ws of wsSyms) { + const members: SymbolWire[] = []; + for (const ws of chosen) { const fsPath = ws.location.uri.fsPath; if (seenFiles.has(fsPath)) { continue; @@ -260,16 +289,36 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid // eslint-disable-next-line no-await-in-loop const doc = await workspace.openTextDocument(ws.location.uri); // eslint-disable-next-line no-await-in-loop - const wireSyms = await this.parseDoc(doc, maxDepth); - for (const s of wireSyms) { - if (s.container === containerName && s.type !== 'potential_reference' && !items.has(s.name)) { - items.set(s.name, this.buildItem(s, asPort)); - } + members.push(...(await this.parseDoc(doc, maxDepth)).filter(matches)); + } + return this.toItems(members, asPort); + } + + private toItems(members: SymbolWire[], asPort: boolean): CompletionItem[] { + const items = new Map(); + for (const s of members) { + if (!items.has(s.name)) { + items.set(s.name, this.buildItem(s, asPort)); } } return [...items.values()]; } + // Package names brought into scope by `import pkg::*;` / `import pkg::name;` + // in the current document. + private importedScopes(document: TextDocument): Set { + const set = new Set(); + const re = /\bimport\s+([a-zA-Z_]\w*)\s*::/g; + const text = document.getText(); + // eslint-disable-next-line no-constant-condition + while (true) { + const m = re.exec(text); + if (m == null) break; + set.add(m[1]); + } + return set; + } + private buildItem(sym: SymbolWire, asPort: boolean): CompletionItem { const item = new CompletionItem(sym.name, getCompletionItemKind(sym.type)); // For enum values the raw type is the internal 'enum_value' marker, so diff --git a/src/test/completion.test.ts b/src/test/completion.test.ts index cc43da2..676fa01 100644 --- a/src/test/completion.test.ts +++ b/src/test/completion.test.ts @@ -110,4 +110,22 @@ suite('Completion Provider Tests', () => { assert.ok(labels.includes('GREEN'), 'expected GREEN; got ' + labels.join(',')); assert.ok(labels.includes('BLUE'), 'expected BLUE; got ' + labels.join(',')); }); + + test('test #7: same-named type in another file is not merged in (scope)', async () => { + // scope_a.sv and scope_b.sv both define `scope_color_e` with different + // values. Completing in scope_a must only offer scope_a's values. + const aUri = vscode.Uri.file(path.join(__dirname, examplesFolderLocation, 'scope_a.sv')); + const doc = await vscode.workspace.openTextDocument(aUri); + const offset = doc.getText().indexOf('a_state == ') + 'a_state == '.length; + const list = (await vscode.commands.executeCommand( + 'vscode.executeCompletionItemProvider', + aUri, + doc.positionAt(offset) + )) as vscode.CompletionList; + const labels = (list?.items || []).map((i) => (typeof i.label === 'string' ? i.label : i.label.label)); + assert.ok(labels.includes('A_RED'), 'expected A_RED; got ' + labels.join(',')); + assert.ok(labels.includes('A_GREEN'), 'expected A_GREEN; got ' + labels.join(',')); + assert.ok(!labels.includes('B_BLUE'), 'must NOT include scope_b values; got ' + labels.join(',')); + assert.ok(!labels.includes('B_YELLOW'), 'must NOT include scope_b values; got ' + labels.join(',')); + }); }); diff --git a/verilog-examples/scope_a.sv b/verilog-examples/scope_a.sv new file mode 100644 index 0000000..a41db0d --- /dev/null +++ b/verilog-examples/scope_a.sv @@ -0,0 +1,8 @@ +// Scope test (A): a locally-defined enum sharing a name with scope_b.sv. +module scope_a; + typedef enum logic [1:0] { A_RED, A_GREEN } scope_color_e; + scope_color_e a_state; + always_comb begin + if (a_state == A_RED) begin end + end +endmodule diff --git a/verilog-examples/scope_b.sv b/verilog-examples/scope_b.sv new file mode 100644 index 0000000..271d82c --- /dev/null +++ b/verilog-examples/scope_b.sv @@ -0,0 +1,5 @@ +// Scope test (B): a different enum with the SAME type name as scope_a.sv. +module scope_b; + typedef enum logic [1:0] { B_BLUE, B_YELLOW } scope_color_e; + scope_color_e b_state; +endmodule From ca63e4ad114ff6aa84141798bc438cc56f126a36 Mon Sep 17 00:00:00 2001 From: Joe Crop Date: Wed, 3 Jun 2026 14:59:26 -0700 Subject: [PATCH 6/7] Honor explicit pkg:: scope in member completion (#82) Extend scope resolution to respect an explicit package qualifier on the declared type: `pkg_x::kind_e v` now completes pkg_x's members even when a same-named type exists in another package or locally. resolveType now returns the type name plus any explicit package scope (typeScope helper), and completeMembers takes a preferScope: when present it skips the local-definition shortcut and narrows the workspace matches to that package (in addition to wildcard-imported packages). Adds pkg_x.sv/pkg_y.sv (same enum type name in different packages) and scope_explicit.sv with a test asserting only the referenced package's values are offered. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/providers/CompletionItemProvider.ts | 67 ++++++++++++++++++------- src/test/completion.test.ts | 17 +++++++ verilog-examples/pkg_x.sv | 4 ++ verilog-examples/pkg_y.sv | 3 ++ verilog-examples/scope_explicit.sv | 8 +++ 5 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 verilog-examples/pkg_x.sv create mode 100644 verilog-examples/pkg_y.sv create mode 100644 verilog-examples/scope_explicit.sv diff --git a/src/providers/CompletionItemProvider.ts b/src/providers/CompletionItemProvider.ts index 6fb68b4..a90e7f2 100644 --- a/src/providers/CompletionItemProvider.ts +++ b/src/providers/CompletionItemProvider.ts @@ -104,6 +104,25 @@ export function normalizeTypeName(type: string): string { return t.trim().split(/\s+/).pop() || ''; } +/** + Extract the explicit package scope from a declared type, if any: the package + immediately enclosing the type. `mypkg::state_e` -> `mypkg`, + `a::b::c` -> `b`, plain `state_e`/`logic [7:0]` -> undefined. Pure. +*/ +export function typeScope(type: string): string | undefined { + if (!type) { + return undefined; + } + const t = + type + .replace(/\[[^\]]*\]/g, ' ') + .trim() + .split(/\s+/) + .pop() || ''; + const parts = t.split('::'); + return parts.length >= 2 ? parts[parts.length - 2] : undefined; +} + export class SystemVerilogCompletionItemProvider implements CompletionItemProvider { private indexer: SystemVerilogIndexer; @@ -156,12 +175,13 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid if (ctx.kind === 'member' || ctx.kind === 'value') { // Resolve the operand's type, then list that type's members - // (struct/class fields for `.`, enum values for `==`/case). - const typeName = await this.resolveType(document, ctx.base); - if (!typeName) { + // (struct/class fields for `.`, enum values for `==`/case). An + // explicit `pkg::` scope on the type narrows the lookup precisely. + const resolved = await this.resolveType(document, ctx.base); + if (!resolved || !resolved.name) { return []; } - return this.completeMembers(typeName, DEEP, false, document); + return this.completeMembers(resolved.name, DEEP, false, document, resolved.scope); } // Port connection: resolve the instantiated module from the surrounding @@ -222,15 +242,19 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid type string. Re-parses the current file at depth so locals nested in modules/classes are found; falls back to a workspace name lookup. */ - private async resolveType(document: TextDocument, name: string): Promise { + private async resolveType( + document: TextDocument, + name: string + ): Promise<{ name: string; scope?: string } | undefined> { + const pick = (raw: string) => ({ name: normalizeTypeName(raw), scope: typeScope(raw) }); const local = await this.parseDoc(document, DEEP); const hit = local.find((s) => s.name === name && s.type && s.type !== 'potential_reference'); if (hit) { - return normalizeTypeName(hit.type); + return pick(hit.type); } const ws = await this.indexer.client.queryByName(name, { excludeTypes: ['potential_reference'], limit: 50 }); const wsHit = ws.find((s) => s.type && s.type !== 'potential_reference'); - return wsHit ? normalizeTypeName(wsHit.type) : undefined; + return wsHit ? pick(wsHit.type) : undefined; } /** @@ -247,17 +271,22 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid containerName: string, maxDepth: number, asPort: boolean, - document: TextDocument + document: TextDocument, + preferScope?: string ): Promise { if (!containerName) { return []; } const matches = (s: SymbolWire) => s.container === containerName && s.type !== 'potential_reference'; - // 1) Local definition in the current file takes precedence. - const localMembers = (await this.parseDoc(document, maxDepth)).filter(matches); - if (localMembers.length > 0) { - return this.toItems(localMembers, asPort); + // 1) A local definition wins — unless the reference was explicitly + // package-scoped (`pkg::Type`), which must resolve to that package + // even if a same-named type exists locally. + if (!preferScope) { + const localMembers = (await this.parseDoc(document, maxDepth)).filter(matches); + if (localMembers.length > 0) { + return this.toItems(localMembers, asPort); + } } // 2) Resolve via the workspace, scoped to the in-scope definition. @@ -270,11 +299,15 @@ export class SystemVerilogCompletionItemProvider implements CompletionItemProvid return []; } let chosen = wsSyms; - if (wsSyms.length > 1) { - const imported = this.importedScopes(document); - const scoped = wsSyms.filter((s) => s.containerName && imported.has(s.containerName)); - // Prefer the imported-package definition(s); otherwise a single - // match, never the union of every same-named definition. + if (wsSyms.length > 1 || preferScope) { + // Prefer a definition whose enclosing package is in scope: an + // explicit `pkg::` on the reference, or a wildcard-imported package. + const preferred = this.importedScopes(document); + if (preferScope) { + preferred.add(preferScope); + } + const scoped = wsSyms.filter((s) => s.containerName && preferred.has(s.containerName)); + // Otherwise a single match, never the union of every same-named def. chosen = scoped.length > 0 ? scoped : [wsSyms[0]]; } diff --git a/src/test/completion.test.ts b/src/test/completion.test.ts index 676fa01..f8773cf 100644 --- a/src/test/completion.test.ts +++ b/src/test/completion.test.ts @@ -128,4 +128,21 @@ suite('Completion Provider Tests', () => { assert.ok(!labels.includes('B_BLUE'), 'must NOT include scope_b values; got ' + labels.join(',')); assert.ok(!labels.includes('B_YELLOW'), 'must NOT include scope_b values; got ' + labels.join(',')); }); + + test('test #8: explicit pkg:: scope resolves to that package (scope)', async () => { + // `pkg_x::kind_e v` must complete pkg_x's values, not pkg_y's same-named enum. + const uriE = vscode.Uri.file(path.join(__dirname, examplesFolderLocation, 'scope_explicit.sv')); + const doc = await vscode.workspace.openTextDocument(uriE); + const offset = doc.getText().indexOf('v == ') + 'v == '.length; + const list = (await vscode.commands.executeCommand( + 'vscode.executeCompletionItemProvider', + uriE, + doc.positionAt(offset) + )) as vscode.CompletionList; + const labels = (list?.items || []).map((i) => (typeof i.label === 'string' ? i.label : i.label.label)); + assert.ok(labels.includes('X_ONE'), 'expected X_ONE; got ' + labels.join(',')); + assert.ok(labels.includes('X_TWO'), 'expected X_TWO; got ' + labels.join(',')); + assert.ok(!labels.includes('Y_ALPHA'), 'must NOT include pkg_y values; got ' + labels.join(',')); + assert.ok(!labels.includes('Y_BETA'), 'must NOT include pkg_y values; got ' + labels.join(',')); + }); }); diff --git a/verilog-examples/pkg_x.sv b/verilog-examples/pkg_x.sv new file mode 100644 index 0000000..b8af382 --- /dev/null +++ b/verilog-examples/pkg_x.sv @@ -0,0 +1,4 @@ +// Two packages in separate files define an enum with the SAME type name. +package pkg_x; + typedef enum logic [1:0] { X_ONE, X_TWO } kind_e; +endpackage diff --git a/verilog-examples/pkg_y.sv b/verilog-examples/pkg_y.sv new file mode 100644 index 0000000..18f1018 --- /dev/null +++ b/verilog-examples/pkg_y.sv @@ -0,0 +1,3 @@ +package pkg_y; + typedef enum logic [1:0] { Y_ALPHA, Y_BETA } kind_e; +endpackage diff --git a/verilog-examples/scope_explicit.sv b/verilog-examples/scope_explicit.sv new file mode 100644 index 0000000..9d49264 --- /dev/null +++ b/verilog-examples/scope_explicit.sv @@ -0,0 +1,8 @@ +// Explicit `pkg_x::kind_e` reference must resolve to pkg_x's definition, +// not pkg_y's same-named enum (and without a wildcard import). +module scope_explicit; + pkg_x::kind_e v; + always_comb begin + if (v == X_ONE) begin end + end +endmodule From 007de585d9cb1fc0327de017d01c9bfd92a0f9ef Mon Sep 17 00:00:00 2001 From: Joe Crop Date: Wed, 3 Jun 2026 15:10:06 -0700 Subject: [PATCH 7/7] Add completion_demo.sv hands-on example for member completion (#82) A self-contained example exercising every completion context with "TRY:" cursor hints: struct fields (pkt.), package members (demo_pkg::), class members (tr.), module ports (fifo instantiation), and enum values in `==` and case labels. Points to the scope_*/pkg_* fixtures for cross-file scope. Co-Authored-By: Claude Opus 4.8 (1M context) --- verilog-examples/completion_demo.sv | 109 ++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 verilog-examples/completion_demo.sv diff --git a/verilog-examples/completion_demo.sv b/verilog-examples/completion_demo.sv new file mode 100644 index 0000000..039d38f --- /dev/null +++ b/verilog-examples/completion_demo.sv @@ -0,0 +1,109 @@ +// ============================================================================= +// Member / port / package / enum auto-completion demo (issue #82) +// +// Open this file and try the positions marked "TRY:". Suggestions pop up +// automatically after `.` and `::`; otherwise press Ctrl+Space. Everything is +// self-contained in this file so the completions resolve locally. +// +// For the cross-file SCOPE handling, see scope_a.sv / scope_b.sv (same type +// name in different files) and pkg_x.sv / pkg_y.sv + scope_explicit.sv +// (explicit `pkg::Type` references). +// ============================================================================= + +package demo_pkg; + parameter WIDTH = 8; + + typedef enum logic [1:0] { + IDLE, + BUSY, + DONE + } state_e; + + typedef struct packed { + logic valid; + logic [7:0] data; + state_e state; + } packet_t; + + function automatic int double_it(int v); + return v * 2; + endfunction +endpackage + +// A class, to exercise object-member completion. +class Transaction; + int id; + string name; + rand bit [7:0] payload; + + function void show(); + endfunction +endclass + +// A submodule whose ports are completed at instantiation. +module fifo #( + parameter int DEPTH = 16 +) ( + input logic clk, + input logic rst_n, + input logic [7:0] wr_data, + output logic [7:0] rd_data, + output logic empty +); +endmodule + +module demo_top + import demo_pkg::*; +( + input logic clk, + input logic rst_n +); + + packet_t pkt; // struct variable + state_e cur_state; // enum variable (imported from demo_pkg) + Transaction tr; // class handle + + logic [7:0] data_bus; + logic fifo_empty; + + // ----- 1) STRUCT MEMBERS ------------------------------------------------- + // TRY: type `pkt.` -> valid, data, state + assign data_bus = pkt.data; + + // ----- 2) PACKAGE MEMBERS ------------------------------------------------ + // TRY: type `demo_pkg::` -> WIDTH, state_e, packet_t, double_it + localparam int W = demo_pkg::WIDTH; + + // ----- 3) CLASS MEMBERS -------------------------------------------------- + initial begin + tr = new(); + // TRY: type `tr.` -> id, name, payload, show + tr.id = 1; + end + + // ----- 4) MODULE PORTS (at instantiation) -------------------------------- + // TRY: on a new line inside the port list below, type `.` -> clk, rst_n, + // wr_data, rd_data, empty + fifo #(.DEPTH(8)) u_fifo ( + .clk (clk), + .rst_n (rst_n), + .wr_data (data_bus), + .rd_data (data_bus), + .empty (fifo_empty) + ); + + // ----- 5) ENUM VALUES in comparisons and case ---------------------------- + always_comb begin + // TRY: type `cur_state == ` -> IDLE, BUSY, DONE + if (cur_state == DONE) begin + end + + // TRY: on a new label line inside this case, type a letter -> IDLE, BUSY, DONE + case (cur_state) + IDLE: data_bus = 8'h00; + BUSY: data_bus = 8'hAA; + DONE: data_bus = 8'hFF; + endcase + end + +endmodule