Directives (4/4): complete names and argument values - #159
Open
wildthink wants to merge 2 commits into
Open
Conversation
Phase 3 of the directive seam. A self-contained call has no body, so until now it parsed, claimed its span, and then rendered as its own literal source — `@pagebreak` looked exactly like the text `@pagebreak`. This gives it something to draw. A directive returns a `DirectivePresentation` — an SF Symbol, replacement text, or an NSImage — and the styler collapses the source behind it. The mechanism is the one inline LaTeX already uses, not a new one: the characters stay in the storage, the first carries the image plus enough kern to occupy its width, the rest collapse to zero width via clear colour and the shrunk marker font. `MarkdownTextLayoutFragment` draws it. That is what keeps "markers shrink, they don't disappear" true here. Selection, find, copy, and undo all still see the real characters, and the caret entering the call reveals the source muted — the same flip every other construct does. Failure is visible rather than silent: `.literal`, or a symbol name the system doesn't know, leaves the source on screen instead of collapsing it to a gap the user can neither see nor fix. Rasterised glyphs are cached in an NSCache keyed by everything that determines the pixels — presentation re-runs for every visible directive on every keystroke, and rasterising text each time is the one part of this path expensive enough to matter. The parser is untouched: this is styling only, and the 4000-input corpus fingerprint is unchanged. `DirectiveScanner`'s diff is comment-only — it already emitted the geometry this needs, and its comments pointed forward to this change. DirectiveStylingTests asserted that a self-contained call renders as literal text with nothing collapsing it. That was Phase 1 stating its own limit, and it is exactly what this changes, so it becomes a collapse/reveal pair rather than being deleted. `FontDirective` and `ColorDirective` are both containers and draw no glyph, so the engine still ships no self-contained directive. Demo/ gains `@icon`, `@flag`, `@emoji`, and `@pagebreak` as embedder-side examples — curated data and print semantics are app concerns. `@flag` computes its glyph from regional-indicator scalars and carries no dataset. 463 tests pass, demo builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Phase 4, the last of the seam. Autocomplete for directive NAMES (`@fo`) and for their ARGUMENT VALUES (`@icon(sta`, `@flag(jap`), riding the seam the [[wiki-link]] picker already established: the engine detects the trigger, ranks the candidates, reports the anchor rect, and routes up/down/return/ escape. The embedder draws the list — no picker UI ships in the engine. Autocomplete cannot read the AST. Mid-typing, `@ico` and `@icon(sta` are exactly what the parser REJECTS — no body, no closing paren — which is correct for styling and useless for completion. So `DirectiveCompletionScanner` is a separate, forgiving backwards scan over the current line, bounded to 256 characters per caret move. It reuses the parser's boundary rule, so it can never offer a directive the parser would then refuse. The engine owns the candidates because it owns the registry. Values come from `MarkdownDirective.valueCompletions(for:prefix:)`, whose default already answers anything the declared schema can — closed keyword sets and booleans — so a directive implements it only when its domain is dynamic or too large to declare. That is what makes a `@flag`-style command clean rather than special: the schema was already there. A newly registered directive appears in the picker with no embedder change. The commit path is deliberately NOT `applyInlineReplacement` — that runs the wiki-link storage/display transform and stamps `.wikiLinkID`, neither of which means anything here. Pickers stay shut where they must: inside code spans and fenced blocks, on a selection rather than a caret, when not typing, mid-IME composition, in raw source mode, and after an email address. Changes to existing files are 47 lines across 3; detection, commit, scanning, and the completion types live in their own files. Demo gains a caret-anchored picker (~60 lines) serving both names and values through one context type. 488 tests pass, demo builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The last phase of the directive seam. Stacks on #158 (glyphs) — the diff below includes it; review that one first, and this is the ~1,200-line increment on top.
What it does
Autocomplete for directive names (
@fo) and for their argument values (@icon(sta,@flag(jap), riding the seam the[[wiki-link]]picker already established: the engine detects the trigger, ranks the candidates, reports the anchor rect, and routes ↑/↓/↵/Esc. The embedder draws the list — no picker UI ships in the engine.Why it can't use the AST
This is the one design point worth your attention. Mid-typing,
@icoand@icon(staare exactly what the parser REJECTS — no closing paren, no body. That's correct for styling and useless for completion.So
DirectiveCompletionScanneris a separate, forgiving backwards scan over the current line, bounded to 256 characters per caret move. It reuses the parser's boundary rule, so it can never offer a directive the parser would then refuse — the two agree on what could become a directive, and disagree only on whether it's finished yet.Where the candidates come from
The engine owns them because it owns the registry. Names come from the registered directives; values from
MarkdownDirective.valueCompletions(for:prefix:), whose default already answers anything the declared schema can — closed keyword sets and booleans.A directive implements it only when its domain is dynamic or too large to declare. That's what makes a
@flag-style command clean rather than special: the schema was already there. The demo's@flagoffers every ISO region matching on code or localised country name, carrying no dataset — codes fromLocale.Region, names from the user's locale.A newly registered directive appears in the picker with no embedder change.
Scope
Changes to existing files are 47 lines across 3:
NativeTextViewWrapper.swiftNativeTextViewCoordinator+TextDelegate.swiftNativeTextViewCoordinator.swiftEverything else is new files: the scanner, the completion types, detection/commit, and tests.
The commit path is deliberately not
applyInlineReplacement— that runs the wiki-link storage/display transform and stamps.wikiLinkID, neither of which means anything here.Where the picker stays shut
Inside code spans and fenced blocks, on a selection rather than a caret, when not typing, mid-IME composition, in raw source mode, and after an email address. Each has a test.
Verification
With this, #154 is the only directive work left outstanding and I'm happy to take it whenever suits you.
🤖 Generated with Claude Code