Steps to reproduce
- Open the ZenUML editor (rewrite/web-foundation branch).
- Type
A->B: @.
- Observe the autocomplete popup.
Expected: No annotation completions (@Actor, @Database, etc.) — the cursor is inside a message label (Content node), which is not a participant-declaration slot.
Actual: The full annotation list (@Actor, @BoundaryParticipant, @CloudFrontParticipant, etc.) is offered.
Root cause
zenumlAutocomplete.ts line 258:
if ((zone === 'head' || zone === 'top') && (typed.startsWith('@') || context.explicit)) {
options.push(...annotationCompletions())
}
For A->B: @ the parser produces the ancestor path LineContent → Content → AsyncMessage → Statement → Program. None of those nodes are Head or StatementBraceBlock, so resolveZone returns 'top'. The annotation gate then fires because zone === 'top' and typed.startsWith('@') are both true.
The 'top' zone was introduced to mean "the document top level where both participant declarations and message statements are syntactically valid start positions" (ADR 0002). It does not mean "a declaration slot". A message label (Content) is neither a declaration position nor a statement start — it is the free-text body of an async message — but the zone-only gate cannot distinguish these cases.
Fix sketch
Add a helper isInsideMessageContent(state, pos) that walks the syntax tree up from pos and returns true if any ancestor is Content or AsyncMessage:
function isInsideMessageContent(state: EditorState, pos: number): boolean {
for (let n: SyntaxNode | null = syntaxTree(state).resolveInner(pos, -1); n; n = n.parent) {
if (n.name === 'Content' || n.name === 'AsyncMessage') return true
}
return false
}
Then gate annotations out of that position:
// line 258 — add the !isInsideMessageContent guard
if ((zone === 'head' || zone === 'top') && !isInsideMessageContent(state, pos) &&
(typed.startsWith('@') || context.explicit)) {
options.push(...annotationCompletions())
}
Location: /web/src/editor/zenumlAutocomplete.ts line 258.
Found via the 100-case browser-test campaign (catalog-extended.spec.ts, case K7); adversarially verified.
Steps to reproduce
A->B: @.Expected: No annotation completions (
@Actor,@Database, etc.) — the cursor is inside a message label (Contentnode), which is not a participant-declaration slot.Actual: The full annotation list (
@Actor,@BoundaryParticipant,@CloudFrontParticipant, etc.) is offered.Root cause
zenumlAutocomplete.tsline 258:For
A->B: @the parser produces the ancestor pathLineContent → Content → AsyncMessage → Statement → Program. None of those nodes areHeadorStatementBraceBlock, soresolveZonereturns'top'. The annotation gate then fires becausezone === 'top'andtyped.startsWith('@')are both true.The
'top'zone was introduced to mean "the document top level where both participant declarations and message statements are syntactically valid start positions" (ADR 0002). It does not mean "a declaration slot". A message label (Content) is neither a declaration position nor a statement start — it is the free-text body of an async message — but the zone-only gate cannot distinguish these cases.Fix sketch
Add a helper
isInsideMessageContent(state, pos)that walks the syntax tree up fromposand returnstrueif any ancestor isContentorAsyncMessage:Then gate annotations out of that position:
Location:
/web/src/editor/zenumlAutocomplete.tsline 258.Found via the 100-case browser-test campaign (catalog-extended.spec.ts, case K7); adversarially verified.