Editor previews (colour swatches, arrows, embeds), the 18 obsidian-basics tasks (find and replace with regex, back/forward, auto-pair, footnotes, folding, search operators, split view, PDF/audio/ video embeds, spelling, drag and drop, pinned tabs, settings) and round 2: ```query blocks, stacked tabs, pop-out windows, tasks ticked inside embeds, text around an embed, embeds that follow edits, block menu shortcut. New crates: notescraft-regex (linear-time engine) and notescraft-syntax. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
12 KiB
Plan: Editor previews: colors, external links, fenced diagrams and trees
Approach
Pure scanners in a new editor/src/preview/ module (no egui, bounded, linear) detect colors and tree blocks; ui-egui only draws. Rendered note mode is the one path that can host previews. Neither text path can put a widget inside a text run, so a swatch or arrow reserves space with one pad character in the row's galley and is painted over the pad's measured rect (the existing char-based hit_rect). Source mode (the TextEdit buffer) gets no swatches or arrows: a pad char there would change the buffer. Mermaid already renders inline in rendered mode; this plan adds the Source/Diagram toggle with per-block memory and the fallback chips. Previews never produce a NoteEdit, so saved bytes are exactly what the user wrote.
Architecture
- Already there (verified): inline Mermaid in rendered mode.
show_contents(note_view.rs:~305) asksdiagram_view::fence_ranges(diagram_view.rs:140) for mermaid fences and calls thefenceclosure, wired inshell/app/window/views.rs:50-66(Views.inline: BTreeMap<(path, range.start), DiagramView>) and, for default callers,center.rs:149andnote_view.rs:192.show_diagram(diagram_view.rs:42) has a header with SVG copy and a Show/Hide source button that appends a read-onlyui.code(source)below the diagram; parse errors showpaint::show_error(message + read-only source); parse caps live innotescraft_diagrams::parse(MAX_INPUT_BYTES,MAX_NODES,MAX_EDGES, errorsTooLarge,LimitExceeded,NoHeader,NotLayoutable). Source mode shows a diagram only for whole.mmdfiles (FileWidget::Diagram), not inline fences. - New for diagrams: (1) a two-state
Mode { Diagram, Source }per block, default Diagram, replacing the offset key with a key of (path, hash of source, nth duplicate) so edits above a block do not mis-key it; held inViewsfor as long as the note is open. (2) Thefenceclosure returnsbool(rendered or not). In Source mode it draws only a small header row (label + Diagram/Source toggle) and returnsfalse;show_contentsthen falls through to the ordinarycode_block, so the original text is shown and editing uses the existing click-to-edit path (open_on_click,edit::request), no new edit code. Three closure call sites change (center.rs:149,note_view.rs:192,views.rs:50) plus theBodyHooks/Rendererssignatures. (3) Fallback: onNoHeader/NotLayoutablethe block is forced to Source with an "unsupported" chip; on other parse errors an "error" chip with the first message line; onTooLarge/LimitExceededa size message.show_errorloses its duplicate source dump (the code card shows it). No parse is attempted past the caps (the crate already refuses), and the parse result stays cached per source hash (update_cache). - Colors:
spans_for_line(inlines.rs:53) askseditor::preview::detect_colors(text, limits)for plain-text andSpan::Monoruns and splits the span, insertingSpan::Swatch { rgb, alpha }(one pad char,U+2003).build_job/text_row_rect(painting.rs:112,159) record the pad's char index; after layout,hit_rectgives its rect and the painter draws a 12 pt rounded rect (radius 3, one border, 3:1 against the surface, alpha composited over the surface). Detection result is cached per line text hash in egui temp memory. - Pad character and selection:
select.rsbuilds copy text and source alignment fromgalley.text()(select.rs:~249,295) andedit::CaretAt::Rendered { plain }gets the galley text; the pad must not reach any of them. OnePADconstant inpainting.rs; selection copy filters it,locateand the caret char index skip it. A test selects across a swatch and asserts clipboard text and caret byte offset equal the source. Fallback if this proves fragile: reserve space withextra_letter_spacingon the value's last char and paint the swatch after the value (changes where it sits, never the text). - Rows with inline math take the flow renderer (
flow.rs, per-word galleys); swatches and arrows are not drawn there (documented gap; the text still reads correctly). - Plain-text hex vs tags: a candidate is skipped when it falls in a
Tagitem range (#abc,#123) or lacks a non-alphanumeric boundary on both sides. Named colors (16 basic CSS names, default off) are only detected inside inline code, whole word, per spec. - Fenced colors:
code_cardrows (code-blocks plan) call the same detector when the fence language is stylesheet, markup, JSON or settings (css scss less html xml svg json yaml toml ini); other languages get none. - Settings:
EditorSettings(engine/src/prefs/app_settings.rs:130, which already holdsblocks.{grip,toolbar,slash_preview}) gainspreviews { colors: true, named_colors: false }, persisted by the existingsettings.setpath. The shell publishes them each frame into egui temp memory exactly likeblock_menu::publish(widgets/block_menu.rs:144) and views read them with apreviews::settings(ctx)getter, so no signature threading. Off skips detection entirely. - Context menu: swatch rect gets
ui.interact(.., Sense::click())andresponse.context_menuthroughwidgets/context_menu.rs: color preview + hex, Copy color, Copy as rgb (ctx.copy_text), Color previews switch, Named colors switch (each callssettings.set). Menu never touches note text. - Links:
Span::Link { text, dest }already carriesdest; external =deststarts withhttp://orhttps://(computed, no new field).build_jobappends one pad char inside the link's char range (so the arrow is clickable and uses the same link run), the painter draws a 16 pt up-right arrow in the link color over the pad rect (vector strokes, no font dependency). Wikilinks and vault-relative links get nothing;link_target/opener unchanged, so opening behavior is exactly as before. - Trees:
editor::preview::detect_tree(label, body): label empty,textortree; at least 2 lines, at least 2 of them with a tree glyph (│ ├ └ ─) followed by a name; box-drawing tables fail (no name column after the glyph run / closing corners).note_view/blocks/tree_card.rsdraws the TREE label header, Copy (verbatim source viactx.copy_text) and per row: prefix glyphs in monospace (already aligned), a folder or file icon fromdesign/icons.rs(name ends in/or has children = folder), then the name. Non-qualifying fences stay ordinary code cards. - Caps:
MAX_COLORS_PER_NOTE2,000, lines longer than 4 KiB are not scanned, tree body 64 KiB / 2,000 lines; above the caps detection stops silently.
Files
- Create
crates/editor/src/preview/{mod.rs,color.rs,named.rs,tree.rs}—Detected { range, rgb, alpha },detect_colors,NAMED_COLORS,detect_tree; unit tests inline. (editor/srchas 8 loose files, so a folder.)crates/ui-egui/src/views/note_view/blocks/tree_card.rs(note_view/ is at the 12-loose-file cap; new code goes inblocks/).crates/ui-egui/src/widgets/color_menu.rs;crates/ui-egui/src/widgets/previews.rs(settings getter/publisher and the arrow/swatch painters, kept out ofpainting.rs).
- Modify
crates/editor/src/lib.rs:15-26—pub mod preview;.crates/engine/src/prefs/app_settings.rs:~130—previewsinEditorSettings; the settings validation foreditor.previews.*keys.crates/ui-egui/src/views/note_view/inlines.rs:24,53—Span::Swatch, spans split; external flag helper.crates/ui-egui/src/views/note_view/painting.rs:56,112,159— pad char, swatch/arrow paint after layout, secondary-click hit rect.crates/ui-egui/src/views/note_view/select.rs:~249,295andedit.rscaret mapping — ignorePAD.crates/ui-egui/src/views/note_view.rs:~305and theRenderers/BodyHookstypes (:~215-245) — fence closure returnsbool; tree dispatch beforecode_block.crates/ui-egui/src/views/center.rs:149,src/shell/app/window/views.rs:50-66and:212(whole-file diagram unchanged),views/diagram_view.rs:42(Mode, toggle header, chips),views/diagram_view/paint.rs:9(show_errorwithout source dump).crates/ui-egui/src/shell/app/editor/blocks.rs:~54— publish preview settings next toblock_menu::publish.- String tables:
src/text/views/diagram_view.rs,src/text/views/note_view.rs(+ menu labels). ROADMAP.md,docs/ARCHITECTURE.md.
- Delete: none.
Data
- No schema or disk change. Settings keys
editor.previews.colors(default true) andeditor.previews.named_colors(default false) inconfig.tomlvia the existing app-settings store (survives restart). - Per-block diagram mode:
HashMap<(path, source_hash, nth), Mode>inViews, dropped when the note closes, never written. - Detection output
Detected { range: Range<usize> (char boundaries), rgb: [u8;3], alpha: u8 }; per-line cache in egui temp memory keyed by line hash, bounded. - Fenced-language allow-list and named-color table are
*.constants.rs-style consts inpreview/.
Dependencies
- None new (spec constraint). Parsing is hand-written (hex 3/4/6/8,
rgb[a],hsl[a]with range-checked args); diagrams use the existingnotescraft-diagrams; clipboard usesctx.copy_text; link opening is untouched.
Risks
- Pad char leaks into copy, selection or caret mapping (the largest risk): single
PADconstant, filtered in the three places that readgalley.text(), covered by a round-trip test; fallback described above. - Source mode has no swatches or arrows. Spec stories say "the editor"; owner confirms Visual (rendered) mode is enough for v1.
- Byte/char mismatch with multibyte text: scanners use
getandis_char_boundary, swatch positions are char counts (whathit_recttakes); fixtures with accents and emoji. - False positives:
#123and#abctags/issue numbers (skipped viaTagitems and boundary rule), prose "red" (default off, inline code only), CSS-like text in non-style fences (allow-list). - Hostile input: caps above; one linear scan per line, cached; 1,000-color note budget < 10 ms per frame checked by a timing test in release.
- Right-click collides with the selection pointer handling in
select::row: the menu attaches only inside a swatch rect; elsewhere the existing path is unchanged. - Fence closure signature change touches three callers; a compile-checked change, existing diagram tests (
diagram_view.rs:~180) updated for the key change. - Tree detection false positives (box tables, ASCII art): require tree glyph + name on at least 2 lines; ambiguous means plain code card; test asserts Copy equals source bytes.
- Contrast: swatch border 3:1 in both themes asserted by a theme test over all built-ins.
Out of Scope (Defer)
- Color picker/editing; swatches and arrows in source mode and in math-containing rows; previews outside the editor.
- Full CSS named-color set (basic 16 only, answers the open question); arrow on bare, non-link addresses (no arrow).
- Persisting Source/Diagram across sessions; converting unsupported Mermaid kinds.
- Language-labeled tree fences (only empty,
text,treelabels qualify).
Test Strategy
- Unit (
editor/preview): hex 3/4/6/8 accept and reject;rgb/rgba/hsl/hslaranges and alpha, out-of-range -> none; named colors whole-word and gated;detect_treeaccepts (with and without label) and rejects tables/art; caps; boundary safety on multibyte fixtures; hostile 1 MB lines. - UI (kittest): spans include
Swatchbefore detected values, none for#123tags or when previews are off; fencedcssgets swatches,rustdoes not; external link has the arrow, wikilink and in-vault link do not, click still returns the sameclicked_link; select-across-swatch copy equals source; mermaid: valid -> Diagram, toggle -> original text editable and remembered across redraws and after an edit above the block, garbage -> Source + "error" chip, unknown kind -> "unsupported", over-limit -> message; tree card renders and Copy equals source;NoteViewResponse.editisNonefor every preview interaction, and a save after previews is byte-identical. - Engine: settings round-trip and defaults for
editor.previews.*. - Manual:
snapshotexample with a fixture note (colors, links, mermaid, tree) at 1440x900 and--scale 2in Mocha and Latte; right-click menu; restart honors switches; 1,000-color note stays smooth in a release build;cargo xtask ciandcargo xtask i18n.
Owner decision (2026-10-18): swatches and link arrows in rendered mode only (pad character plus overlay); source mode and rows with inline math get none.