diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d903b..b41e67c 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: `.` 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] * 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 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", 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/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 new file mode 100644 index 0000000..a90e7f2 --- /dev/null +++ b/src/providers/CompletionItemProvider.ts @@ -0,0 +1,370 @@ +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'; + +// 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 'enum_value': + return CompletionItemKind.EnumMember; + case 'typedef': + return CompletionItemKind.TypeParameter; + default: + return CompletionItemKind.Field; + } +} + +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/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) + // 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] }; + } + // 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. + 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() || ''; +} + +/** + 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; + + // 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; + } + + public async provideCompletionItems( + document: TextDocument, + position: Position, + _token: CancellationToken + ): Promise { + let ctx: CompletionCtx | null = null; + 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 []; + } + if (!ctx) { + return []; + } + + // 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; + } + + private async resolve(ctx: CompletionCtx, document: TextDocument, position: Position): Promise { + if (ctx.kind === 'package') { + return this.completeMembers(ctx.base, DEEP, false, document); + } + + 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). 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(resolved.name, DEEP, false, document, resolved.scope); + } + + // 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, document); + } + + /** + 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. + */ + 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; + } + + /** + 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<{ 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 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 ? pick(wsHit.type) : undefined; + } + + /** + List the children of a container symbol (struct/union/class/package/ + 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, + document: TextDocument, + preferScope?: string + ): Promise { + if (!containerName) { + return []; + } + const matches = (s: SymbolWire) => s.container === containerName && s.type !== 'potential_reference'; + + // 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. + const wsSyms = + (await commands.executeCommand( + 'vscode.executeWorkspaceSymbolProvider', + `¤${containerName}` + )) || []; + if (wsSyms.length === 0) { + return []; + } + let chosen = wsSyms; + 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]]; + } + + const seenFiles = new Set(); + const members: SymbolWire[] = []; + for (const ws of chosen) { + 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 + 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 + // 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) { + // The triggering `.` is already typed; insert `name(${cursor})`. + item.insertText = new SnippetString(`${sym.name}($1)`); + } + return item; + } +} 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 new file mode 100644 index 0000000..f8773cf --- /dev/null +++ b/src/test/completion.test.ts @@ -0,0 +1,148 @@ +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: '' }); + // 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. + 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 labelsAtOffset(offset: number): Promise { + const doc = await vscode.workspace.openTextDocument(uri); + 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)); + } + + // 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. + 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(',')); + }); + + 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(',')); + }); + + 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(',')); + }); + + 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/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/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/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 diff --git a/verilog-examples/member_completion.sv b/verilog-examples/member_completion.sv new file mode 100644 index 0000000..ab321e8 --- /dev/null +++ b/verilog-examples/member_completion.sv @@ -0,0 +1,54 @@ +// 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; + parameter MC_PARAM = 1; + typedef struct packed { + logic alpha; + logic beta; + } mc_struct_t; + typedef enum logic [1:0] { + RED, + GREEN, + BLUE + } mc_color_e; +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; + mc_color_e fsm_state; + + 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)); + + 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 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_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 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