Files
notescraft/scripts/waves/wave7/editor.md
T
Marcos Paulo c5ee2d00c9 Wave 7 setup: ui polish and editor prompts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 20:25:09 -03:00

4.1 KiB

Crate: crates/editor (notescraft-editor, layer L5). First cut of the markdown EDITOR, source mode. SPEC sections 6 items 2-3, 5, 11 (editor notes), 14 (editor risk). Pragmatic design (decision recorded): wrap egui's TextEdit::multiline (which already provides IME, selection, clipboard, undo, bidi) with a custom markdown layouter for styling; keep a String buffer (no ropey until a benchmark demands it). Allowed dependencies (all { workspace = true }): egui, notescraft-markdown, thiserror, serde (and egui dev-feature default_fonts for tests). Do NOT depend on notescraft-ui-egui (same layer): colours come in through an EditorColors struct of egui::Color32. Another agent edits ui-egui at the same time: do not touch it.

Build in SMALL steps: one file per tool call (<=250 lines), build + clippy after each, every module declared in lib.rs, no unwrap/expect/panic outside tests, no indexing (use get()).

Implement:

  1. colors.rs: EditorColors { text, muted, heading, link, link_unresolved, tag, code, code_background, highlight, accent, selection } plus EditorColors::fallback_dark() for tests.
  2. highlight.rs: layout_job(text: &str, colors: &EditorColors, font_size: f32, wrap_width: f32, resolver: &dyn Fn(&str) -> bool) -> egui::text::LayoutJob built from notescraft_markdown::parse(text) items: headings larger and bold by level (32/24/20/16/16/16 relative to base 16), wikilinks and links coloured resolved/unresolved, tags, ==highlight== with background, inline code and fences monospace on code_background, bold/italic/strikethrough markers dimmed, callout header lines accent, blockquote > dimmed, task markers, comments muted, frontmatter muted. Sections never overlap and always cover the whole string on char boundaries (assert this in tests with random input); never panic on any text; texts over 2 MiB fall back to a single plain section.
  3. editor.rs: EditorState { text: String, cursor: Option<usize>, version: u64 }, show_editor(ui, &mut EditorState, &EditorColors, resolver, opts: EditorOptions) -> EditorResponse { changed: bool, link_clicked: Option<String>, cursor_line_col: (usize, usize) } using TextEdit::multiline(...).layouter(...), frame-less, desired width 100%, font size from options, tab = 4 spaces, Cmd/Ctrl+Enter ignored, and these key behaviours implemented BEFORE the TextEdit sees the events (consume them from ui.input_mut): Enter continues lists (- , * , 1. , - [ ] , > ) with numbering increment and an empty item ends the list; Tab/Shift+Tab indent and outdent selected lines or the list item; [[, (, [, " ` ** auto-close pairs and typing the closer skips over it; Cmd/Ctrl+B and +I wrap the selection in ** / *; Cmd/Ctrl+K wraps in [[ ]] (or [text]()? only [[ ]]). Implement these as PURE functions over (text, selection) -> Option<Edit> in keys.rs (on_enter, on_tab, wrap_selection, auto_close) so they are unit tested without a UI, and apply Edit (range replace + new selection) through the TextEdit state.
  4. completion.rs: pure autocomplete: trigger_at(text, cursor) -> Option<Trigger::{Wikilink{query}, Heading{note, query}, Block{note, query}, Tag{query}, Callout{query}}> for [[, [[note#, [[note#^, #, > [!; apply(text, trigger, choice) -> Edit; and a show_popup(ui, anchor, items, selected) -> Option<usize> widget (max 8 rows). The caller supplies the candidate list; do NOT depend on the index crate.
  5. stats.rs: line_col(text, cursor), word_count.

Tests (headless egui, 2 frames at 900x700, default_fonts): layout_job coverage invariant on 200 random adversarial strings (deterministic generator, adversarial alphabet [ ] ! # ^ = % $ > - * \ ``` and é), every highlight kind produces the expected colour on a fixture, 3 MiB text fallback, show_editor runs without panic and reports changes when text is modified throughRawInput events; keys.rs tables: list continuation (bullet, numbered increment, task, nested, empty item ends, ordinary line), Tab/Shift+Tab, wrap_selection, auto_close and skip-over with multi-byte chars; completion triggers for each form including negative cases ([[closed,#mid-word,# heading`, code fences).