Summary
When a user types a reserved keyword (e.g. if, opt, while) immediately after group or @Actor , the autocomplete popup offers block keywords including "Conditional (alt) block". This violates the context-aware rule that the declaration name slot must never offer keywords.
Steps to reproduce
- Open the ZenUML editor (empty doc).
- Type
group (group keyword + space).
- Type
if.
- Observe the autocomplete popup.
Expected: No block keywords offered (the popup should be empty or offer only participant names, since if is an invalid name anyway).
Actual: "Conditional (alt) block" (if) appears in the popup.
The same bug fires for @Actor if, <<stereo>> if, and with any other reserved keyword (opt, par, while, try, new, ref, …).
Root cause
isNamingDeclaration (web/src/editor/zenumlAutocomplete.ts:91) detects "cursor is in a declaration name slot" by walking up the syntax tree looking for a Group or Participant ancestor. This works correctly when the typed text is an Identifier — the parser absorbs it into Group > Name / Participant > Name.
However, Name { !name Identifier } (web/src/editor/grammar/zenuml.grammar:160) only accepts Identifier tokens. Reserved keywords tokenize as dedicated terminals (IfKeyword, WhileKeyword, etc.) and cannot satisfy Name. When group if is parsed, the IfKeyword token falls outside the Group subtree (parsed as a sibling or via error recovery). isNamingDeclaration's ancestor walk finds no Group or Participant and returns false. The guard at line 272 never fires. Zone resolves to 'top' (top-level document), so keywordsForZone('top') (= HEAD_KEYWORDS ∪ BLOCK_KEYWORDS) populates the popup.
Commit 2b22053 already generalized the naming guard to include GroupKeyword, but it only helps when the parser successfully places the cursor token inside the Group node — which keyword tokens never are.
Fix sketch
Replace the tree-containment check in isNamingDeclaration (or add a complementary text-based guard) that detects "cursor is on the same line as, and immediately follows, a declaration marker (group, @Annotation, <<stereo>>) with no intervening newline or brace." A regex similar to the existing atMessageEndpoint approach would work:
// In isNamingDeclaration or in the keyword-gate branch:
const before = state.doc.sliceString(Math.max(0, pos - 60), pos)
// After `group `, `@Annotation `, or `<<Stereotype>> ` with no newline:
if (/(?:^|\n)\s*(?:group|@\w+|<<[^>]+>>)\s+\w*$/.test(before)) return true
This must NOT suppress block keywords on the next line: group\nif is a legitimate top-level if statement and should still offer if. The same-line anchor in the regex ensures that.
Affected file
web/src/editor/zenumlAutocomplete.ts — isNamingDeclaration function (~line 91) and the keyword-gate guard at line 272.
Found via the 100-case browser-test campaign (catalog-extended.spec.ts, case P9); adversarially verified.
Summary
When a user types a reserved keyword (e.g.
if,opt,while) immediately aftergroupor@Actor, the autocomplete popup offers block keywords including "Conditional (alt) block". This violates the context-aware rule that the declaration name slot must never offer keywords.Steps to reproduce
group(group keyword + space).if.Expected: No block keywords offered (the popup should be empty or offer only participant names, since
ifis an invalid name anyway).Actual: "Conditional (alt) block" (
if) appears in the popup.The same bug fires for
@Actor if,<<stereo>> if, and with any other reserved keyword (opt,par,while,try,new,ref, …).Root cause
isNamingDeclaration(web/src/editor/zenumlAutocomplete.ts:91) detects "cursor is in a declaration name slot" by walking up the syntax tree looking for aGrouporParticipantancestor. This works correctly when the typed text is anIdentifier— the parser absorbs it intoGroup > Name/Participant > Name.However,
Name { !name Identifier }(web/src/editor/grammar/zenuml.grammar:160) only acceptsIdentifiertokens. Reserved keywords tokenize as dedicated terminals (IfKeyword,WhileKeyword, etc.) and cannot satisfyName. Whengroup ifis parsed, theIfKeywordtoken falls outside theGroupsubtree (parsed as a sibling or via error recovery).isNamingDeclaration's ancestor walk finds noGrouporParticipantand returnsfalse. The guard at line 272 never fires. Zone resolves to'top'(top-level document), sokeywordsForZone('top')(= HEAD_KEYWORDS ∪ BLOCK_KEYWORDS) populates the popup.Commit
2b22053already generalized the naming guard to includeGroupKeyword, but it only helps when the parser successfully places the cursor token inside theGroupnode — which keyword tokens never are.Fix sketch
Replace the tree-containment check in
isNamingDeclaration(or add a complementary text-based guard) that detects "cursor is on the same line as, and immediately follows, a declaration marker (group,@Annotation,<<stereo>>) with no intervening newline or brace." A regex similar to the existingatMessageEndpointapproach would work:This must NOT suppress block keywords on the next line:
group\nifis a legitimate top-levelifstatement and should still offerif. The same-line anchor in the regex ensures that.Affected file
web/src/editor/zenumlAutocomplete.ts—isNamingDeclarationfunction (~line 91) and the keyword-gate guard at line 272.Found via the 100-case browser-test campaign (catalog-extended.spec.ts, case P9); adversarially verified.