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>
13 KiB
Plan: Code blocks, language grammars and batch formatting
Approach
Fenced code becomes a card (uppercase language label, Copy, Wrap) whose text is colored by a new pure crate notescraft-syntax (L0, wraps the owner-approved syntect with the fancy-regex backend; also owns language suggestions and grammar fetch/validate). The same crate serves both text paths: rendered mode (code_card) and source mode (editor layout job). Large blocks highlight on one worker thread behind a content-hash cache, so the UI thread never waits. JSON detect/validate/format/minify is a pure helper in the blocks crate (next to splice) and commits through the existing Session::lint_commit, so every rewrite is one undo step. Batch format and lint is an engine state machine: a stepped dry run collects per-file change proposals without writing, the UI reviews them as diffs, Apply recomputes each accepted file from fresh text and commits it via lint_commit. Nothing here may panic on hostile input.
Architecture
- Render, rendered mode:
show_contentsfence branch (note_view.rs:~305,code_blockatnote_view/blocks.rs:29) -> newcode_card(header row with label + Copy/Wrap, then body rows) ->SyntaxCache::get(lang, body)returnsOption<Arc<Highlighted>>(token ranges per line) without blocking, spawns the worker on a miss -> rows painted with a newSpan::CodeTok(text, Color32)(monospace + token color; the existingSpan::Coloreduses the proportional font, so it cannot be reused). Todaycode_card(blocks.rs:56) paints the fence lines too; the new card hides them (label replaces the opening fence) while click-to-edit still maps to the same lines. - Render, source mode:
editor::highlight::layout_job(highlight.rs:88) treats a whole fence as one priority-3Sty::Codeassignment (inline::code_fences,inline.rs:18). Neweditor/src/fence_syntax.rssplits each fence body into sub-assignmentsSty::Syntax(TokenKind)(same priority, samecode_background) from the sameSyntaxCache; a miss paints today's plainSty::Codeand the worker's completion triggers a repaint. Fences over 2,000 lines or 256 KiB stay plain in source mode (documented cap; source mode has no visible-window knowledge). - Large blocks: the worker parses sequentially with a
ParseStatecheckpoint every 100 lines; rendered mode asks for the visible window plus margin, so a block over 1,000 lines highlights only what is on screen and scroll reuses cached chunks. Blocks up to 200 lines highlight in one job. Cache key = hash(language, body); an edit changes the key, so no stale colors and no invalidation code. Cache is bounded (entries and bytes); eviction by age. - Inline code chip:
egui::TextFormat.backgroundcannot round corners.text_row_rect(painting.rs:~159) paints a rounded rect (radius 6,radius_small()) behind eachSpan::Monorun using the existinghit_rect(painting.rs:~131), then the galley on top;fmt_spanstops setting a background forMono. Rows with inline math (flow.rs) keep the old flat background (documented gap). Source mode keeps its flatSty::Codebackground. A theme test asserts text/code_backgroundcontrast >= 4.5:1 for every built-in theme. - Grammar flow: picker ->
grammar.info/grammar.installengine commands ->syntax::fetch-> gates (https only, size cap, parses as grammar, sample highlights without error) ->craft_atomic::atomic_writeinto the app config dir ->AppSettings.grammarsentry. Failure at any gate saves nothing and changes no state; offline gives a data error. Removal deletes the file and the entry; blocks fall back to plain monospace. - JSON: chip validates on draw (cached by body hash, 1 MiB cap, serde_json depth limit stays on).
json.format_block/json.minify_block(path, block index) reuse theblock_edit::editpattern (engine/src/block_edit/mod.rs:79): pure fn overparse_blockstext_range->Edited->lint_commit. Invalid JSON returnsErr, text unchanged. Formatting is a small re-indenter over the validated text (notserde_json::to_string_pretty): serde_json withoutpreserve_order/arbitrary_precisionsorts keys and rewrites numbers such as1e5, which would change data. - Batch:
batch.start(scope, checks)queues note paths (scope note/folder/vault fromvault.list); UI callsbatch.step(budget)per frame (same cooperative pattern as the fast-open plan, no threads; progress + cancel; nothing written). Per note it recordsProposal { path, content_hash, status, changes[] }. Changes are byte-range replacements against the text read at dry-run time: JSON block format (labeled json, valid = proposed; unlabeled = needs_review) and lint fixes (notescraft_lint::fix, safe ones = proposed, the rest = needs_review). Unreadable, invalid JSON, read-only = Not edited with reason.batch.decidestores Accept/Skip per change;batch.applyre-reads each file, compares the content hash from the dry run (mismatch -> skipped and reported, never overwritten), applies only accepted non-overlapping changes in descending offset order, then onelint_commitper file (atomic viavault.write->craft_atomic, one undo step per file when the note is open). A write error is reported and the file stays intact.
Files
- Create
crates/syntax/(Cargo.toml,src/lib.rs,load.rs,highlight.rs,cache.rs,scopes.rs,suggest.rs,fetch.rs,validate.rs) — L0, no egui.scopes.rsmaps scopes to 8TokenKinds (keyword, string, number, comment, type, function, property, operator) by prefix rules, unknown -> none.crates/themes/src/syntax.rs—SyntaxColors(8 tokens), per-built-in dark/light values, optional[syntax]table in user themes with variant defaults.crates/editor/src/fence_syntax.rs— source-mode glue above.crates/blocks/src/json.rs—check_json,format_json_block,minify_json_block,looks_like_json, caps.crates/engine/src/grammars.rs,crates/engine/src/batch/{mod.rs,commands.rs,propose.rs}, JSON commands registered fromblock_edit/mod.rs(111 lines today).crates/ui-egui/src/views/note_view/blocks/code_card.rs(note_view/ is at the 12-file cap, so new note-view code goes in theblocks/subfolder, 4 files today);crates/ui-egui/src/widgets/lang_picker.rs;crates/ui-egui/src/views/batch_view/{mod.rs,review.rs}(views/ has 11 loose files).
- Modify
Cargo.toml— workspace depssyntect(default-features off; parsing, default-syntaxes, dump-load, regex-fancy; exact feature set fixed by the prototype),notescraft-syntax.xtask/src/layers.rs—("syntax", Class::Layer(0))in the L0 block ofTABLE.crates/editor/Cargo.toml,src/lib.rs,src/colors.rs:~11(EditorColors.syntax+ fallback),src/highlight.rs:28-50,88(Sty::Syntax);crates/blocks/Cargo.toml(+serde_json, already approved);crates/engine/Cargo.toml.crates/themes/src/{model.rs,builtin.rs,load.rs,lib.rs}—Theme.syntax;crates/ui-egui/src/design/theme.rsand the shell mapping that buildsEditorColors.crates/engine/src/prefs/app_settings.rs:~35—grammars: BTreeMap<String, GrammarInfo>(serde default);crates/engine/src/commands.rs:~25— chaingrammar_specs,batch::specs;crates/engine/src/vault/session.rs— batch state field.crates/ui-egui/src/views/note_view.rs(fence branch ~305; already 581 lines, so only the call site changes),note_view/blocks.rs(code_block/code_carddelegate),note_view/inlines.rs:24(Span::CodeTok),note_view/painting.rs(fmt_span, chip paint),views/mod.rs,src/text/views/*string tables (newcode_card.rstable file; folder has 11).ROADMAP.md,docs/ARCHITECTURE.md.
- Delete: none.
Data
- Registry:
https://git.marcospaulo.dev.br/masi/notescraft-registry/raw/branch/<default>/index.json(Gitea raw path; branch name to confirm):{ version, grammars: [{ id, name, format: "sublime-syntax", file, size, sha256, license, sample }] }. Only this host is accepted for registry entries; redirects are refused. - Installed:
<config dir>/grammars/<id>.sublime-syntax;AppSettings.grammars[id] = { file, size, license, installed_at }. - Dismissed JSON offers: list
"<path>#<body-hash>"in the opaqueuiblob (cap 500, insideMAX_UI_BYTES). - Per-block UI state (wrap, copy confirm): egui temp memory keyed by body hash, never persisted.
- Batch:
Proposal { path, content_hash, status: proposed|needs_review|not_edited|clean, reason?, changes: [{ id, kind: json|lint, range, before, after, rule?, decision }] }; summary counts for the four statuses.
Dependencies
syntect(fancy-regex backend, no onig): owner-approved.- Downloads: no HTTP client or hash crate exists in the workspace (
Cargo.lockhas none). Recommended: a boundedcurlsubprocess, the pattern already in the tree atcrates/acp/src/openai/transport.rs:67(andcrates/git/src/process.rsfor caps and deadlines):--disable --proto =https --proto-redir =https --max-redirs 0 --max-filesize <cap> --connect-timeout --max-time -sS --fail, stdout read with a hard byte cap in Rust, host allow-list checked in Rust before spawn. User links: https only, no credentials, no IP literals or localhost, size cap; no checksum exists for them, the other gates apply. No new crate for transport. - sha256 of registry files: needs owner approval for
sha2(pure Rust, RustCrypto, default features off). Until approved, the Grammar-source checksum requirement is not met; install still enforces size + parse + sample. - Owner decisions (2026-10-18):
sha2approved (pure Rust, default features off); boundedcurlsubprocess approved for downloads; missing TOML or TypeScript grammars are embedded as licensed.sublime-syntaxfiles once the prototype confirms the gap; source-mode highlighting cap of 2,000 lines or 256 KiB per fence accepted. - Nothing else new; diffs reuse
notescraft-git/diff_view, writes reusecraft-atomic.
Risks
- Spec criterion: built-in TOML and TypeScript. Prototype (2026-10-18, syntect 5, fancy-regex, no onig) confirmed: the bundled pack lacks TOML and TypeScript and has the other 15 checked languages; if confirmed, two licensed
.sublime-syntaxfiles are embedded withinclude_str!so they work offline. Owner decision on which grammars/licenses. - Grammar format: registry serves
.sublime-syntax; syntect reads it natively (no converter)..tmLanguageis rejected with a clear error. - First-use cost of loading the syntax dump: built lazily on the worker thread after window show; rows paint plain until ready; process-wide cache.
- Hostile input: per-line (10 KiB) and per-block caps in
highlight.rs,ParseStateerrors become plain text, JSON depth limit stays on, all offsets fromget/split_at_checked; fuzz-style test with random bytes and 1 MiB lines. - Worker thread leaks or floods: single thread, bounded queue (latest request wins), result delivered through the cache plus
ctx.request_repaint. - Batch Apply overwrites newer disk state: content-hash re-check immediately before writing; open dirty buffers are edited through the buffer path so the user's unsaved text is never discarded.
- Overlapping accepted changes (JSON block inside a lint fix range): later overlapping change is skipped and listed, never merged silently.
- Reserved folders: batch walks
vault.list(hidden entries skipped);.obsidian/and.notescraft/are never written. - Source-mode caps mean a 5,000-line fence is plain in source mode but colored in rendered mode; accepted.
Out of Scope (Defer)
- YAML/TOML formatters, autocomplete, LSP, running code, grammar editor, per-language indent setting (2 spaces fixed; answers the open question).
- Backup before Apply (per-file undo is the guarantee); folder context-menu entry for batch (command palette and Tools menu only).
- Lint rule set beyond
apply_all_safe; table alignment fixes. - Swatches and the link arrow (editor previews plan).
Test Strategy
- Unit:
blocks::json(valid/invalid/minify; key order and1e5preserved byte-for-byte; depth and size caps; hostile bytes);suggest_languagetypos/ties;scopes(each of the 8 kinds reachable, unknown ignored);highlightcaps and checkpoints (window result equals full-run result);validategates (http, oversize, non-grammar, bad sample); fence label rewrite is a char-boundary-safe edit; theme contrast >= 4.5:1 for chip and 4.5:1 for each syntax token oncode_backgroundin all built-ins. - Engine integration (headless registry, like the existing block_edit tests):
json.*is one undo step; invalid JSON leaves text identical; batch dry run on a temp vault writes nothing (fs compared), counts correct, unreadable file listed under Not edited; Apply skips a file changed after the dry run; cancel writes nothing;fetchagainst an injectable transport (no network in tests), offline error. - UI (kittest/snapshot): card shows uppercase label, Copy puts body without fences on the clipboard, Wrap reflows; unknown language renders plain; colored tokens for the 12 built-in languages; source-mode layout job colors a fence; 1,000+ line block draws only the window.
- Manual:
snapshotexample with JSON/YAML/Rust/diff blocks at 1440x900 and--scale 2in Mocha and Latte; real registry download, broken link, offline; batch on the owner's vault; release-build scroll of a 5,000-line block; note with 50 blocks opens within the 50 ms budget;cargo xtask ciandcargo xtask i18n.