edamame 0.1.6

A Terminal Markdown editor that stays rendered while you edit
Documentation
# Hybrid editing model

> Part of the edamame contributor deep-dives. Index and project-wide conventions: [`AGENTS.md`](../../AGENTS.md). Sibling docs live in [`docs/dev/`](.).


- **The rope is always pure `\n`; `Buffer::line_ending` records the on-disk convention.** Every path that ingests document text normalizes CRLF away first (`buffer::normalize_newlines`): `Buffer::load_file` / `from_str` / `reload`, the filesystem watcher (`handle_file_changed`, *before* it hashes, diffs or stores — or a CRLF file spuriously diffs as wholly-changed and the own-write echo filter never matches), the `--diff` sides (`difftool::read_side`), the OS clipboard read (`clipboard_text`) and every bracketed paste (`paste_text`). Every *egress* re-encodes to the buffer's convention: saves stream through `write_lines` (`\n`→`\r\n` chunk by chunk, never materializing a translated `String`), and the *external* clipboard copy widens through `encode_newlines` while the kill-ring stays `\n`-only. So downstream code — cursor math, wrap, diff, `(?m)$` anchoring — may assume no `\r`; a bare `strip_suffix('\n')` is enough. **`reload` takes the ending as a parameter rather than re-detecting it** — its bytes are already normalized upstream, so detection would always report `Lf` and silently flip a CRLF document. A new disk-read or clipboard path owes the same normalize-in / encode-out pair; a lone `\r` (classic-Mac, essentially extinct) is deliberately left intact.
- **Virtual blocks for blank lines.** `ParsedDoc::build` synthesises a one-byte block for every blank line, so the cursor lands on each as its own block and the blank survives in `RenderedView` when the surrounding block goes raw. The extended ranges (`build_extended_ranges`) are built *after* the virtual blanks are pushed; never derive cursor-mapping ranges that absorb blank-line bytes into adjacent blocks (the removed `parse_offsets::covering_ranges` did).
- **`per_block_own` vs. extended ranges.** `ParsedDoc` tracks per-block *own* rendered line counts (for `RenderedView`'s raw-replacement region) *and* extended covering ranges (for cursor lookup). Mixing them collapses gap blank lines when the cursor enters the previous block.
- **Loose lists stay one block; their spacing comes from item spans.** One `Block::List`, whose `ListItem`s each carry the source-line `span` the parser recorded (marker line through last content line, never the blank lines pulldown-cmark's item range absorbs). The renderer emits one blank `Line` per source line between consecutive items' spans, so spacing is kept while numbering comes from pulldown-cmark, and a blank inside an item (in a fence, between its blocks) is inside that item's span and never counts. The item renders the blank lines between its own blocks instead, one blank row per line (less `ListItem::hidden`, its link reference definitions), as a quote and a footnote definition do (`renderer::push_gap_rows`), and indents its later blocks to its text column; only a nested list takes the 4-cell nesting indent. Paragraphs and raw HTML lay out behind the indent themselves; every block that renders flush (a table, code, math, a rule, a heading, a quote) is rendered inset by the indent and then prefixed with it, its row origins shifted, as a quote does behind its bar (`render_under_indent`). The row keeps its line style, so a quote's wash or a code surface still fills to the edge, while the indent takes the background around the item (`Renderer::surface`: the page, or an enclosing quote's wash), and `line_render::leading_bar_prefix` repaints indent and bar on a wrapped row. An image stays flush, since its overlay paints from the view's left edge. This holds at any depth, so a loose list nested in an item or a footnote keeps its spacing too (it once rendered tight, leaving the blank between its items the one blank line in a container with no row). Don't reintroduce `split_lists_on_blank_lines` — fragmenting forced per-group `start` re-derivation and block/range surgery for no rendering benefit.
- **Every rendered row records where it came from, and no consumer re-derives it.** The renderer writes each row into a `RowSink` together with its `RowOrigin` (`markdown::row_origin`), at the moment it decides the row's layout: the source lines it shows (relative to its top-level block, so the render cache stays position-independent) and how its columns relate to theirs — `Chrome`, or `Content` of kind `Inline`, `Verbatim`, `TableRow` or `Flow`. Container renderers wrap their children's origins exactly as they wrap their lines (a quote bar shifts `rendered_col`); `ParsedDoc::build` gives the rows it adds itself (virtual blanks, the phantom last line) theirs at the same push, and `ParsedDoc::row_origins` is 1:1 with `lines` (a `debug_assert` after every block and at the end of `build`). `tests/row_provenance.rs` checks every origin against the rendered output itself, over a fixed corpus and a generator.
  - **Every *row* question goes through `document::row_map`** — the cursor's row (`cursor_rendered_line_idx`, `RenderedView`'s reveal row, both through `row_for_pos`), the gutter (`state_source_lines`), the clicked row (`mouse_ops::coord`), the selection overlay's row (`rendered_view::paint`) and `revealed_raw_row_count`'s line. A line shows on the first row that reaches it (`row_for_line`), so a line rendering no row (a link reference definition inside a container, a bare `-`) shares the next line's, and a line past every row lands on the last row that shows one; a row belongs to the first line it shows (`line_for_row`), and a row no line owns (a table's top border, an unclosed fence's placeholder) to the line above. The cursor's row also weighs its column (`row_for_pos`): where a line's first row is chrome and a later row shows its chars (a marker on a row of its own, `- - a`), the cursor goes on the row that places its column, which is where a click on that char puts it back. The math-preview band is the one adjustment the origins can't carry, since it is cursor state: `row_map` alone reads `latex_source_offset`, and a revealed diagram's rows (mermaid, `$$…$$`) map to the source lines they paint through `row_map::revealed_diagram_line` — the click, `revealed_raw_row_count` and `RenderedView` all ask it, and it answers `None` for a band row and for padding past the source (a reservation not yet shrunk to it), which the origins clamp to the last line. The reverse, a collapsed diagram whose source runs past its image's rows, ends on a row whose origin covers every line from its own to the end, so each line still has a row. Those rows paint their selection and yank flash themselves, against the raw text they show, as every revealed row does (see [search-replace.md](search-replace.md)); the overlay painter skips every image row. A table's rows go through `row_map::table_row`; only their columns map through `table_layout`'s pipe geometry.
  - **So does every *column* question** — the click (`rendered_to_raw_col`), the cursor indicator (`raw_to_rendered_col_near`), the overlays (`raw_to_rendered_col`) and the reveal gate (`reveals`). One `match` on the row's `ColOrigin`, never on block kind: `Verbatim` is one char for one past the prefix; `Inline` and `Flow` map through `InlineColMap`s built over each line's text past its content column (`InlineColMap::build_inline`, which pins a slice reading `2. a` or `# b` to the paragraph text it was and resolves references against the document's definitions, `ParsedDoc::ref_labels`; an ATX heading's closing sequence is left off its slice), one space per break, or failing that through one map over the slices joined by `\n` (an inline spanning a break, `*a⏎b*`). For a paragraph's row that join is over the whole paragraph, built once per parse and shared by its rows (`row_map::JoinedMap`, `ParsedDoc::with_joined_map`), and the row takes the part between the breaks around its lines: with reflow off the renderer cuts an inline at every break inside it (`renderer::split_at_breaks`), so `a *b⏎c* d` is two rows whose own lines alone read a literal `*`. A row's map is accepted only when it renders exactly the chars the row shows, and is cached per row on the parse with the row's chars and its block's position (`ParsedDoc::row_cache_or_init`). Where none fits, `raw_to_rendered_col` answers `None`: a selection skips the row's interior rather than painting off by N, a search match or `:s` preview shows at the one-for-one guess as wide as its text (`paint::Overlay`), and the indicator takes the one-for-one guess. A click on a chrome row lands on its line's content column, or, on a line that is all chrome (a fence, a setext underline), on its leaf's nearest one, so past any container prefix. A prefix (marker, bar, leader) lines up with the raw one **from the right**, both ways, so a task box or footnote leader is hit exactly whatever indent or number padding the two disagree on; spaces after a marker beyond what the rendered prefix has (`1.  foo`) are dropped from the alignment, so the marker still meets its glyph; indentation and a code row's pad cell stand for nothing and land on what follows. `RawPos` columns on a block's first line count from its range start, as every consumer slices the block (an indented code block's range starts past its indent).
  - **The round trip is the invariant** (`tests/row_provenance.rs`): a click on any char puts the cursor where the indicator then shows it, a click there puts it in the same place again, and every content char maps back to itself. It is checked twice: at `row_map`'s level over the corpus and the generator, where every inline row must also *have* an exact map, and end to end through `mouse_ops` and `RenderedView` (`clicking_where_the_cursor_shows_keeps_it_there`), which is what catches a cursor row and a click disagreeing across rows.
  - **The revealed cursor row paints the cursor's own line**, not the line its origin names (`editor::state::cursor_raw_line`). The two differ when the cursor's line renders no row and shares the next line's, so the view, `revealed_raw_row_count` and the click mapping must all use the cursor's line on that row.
  - **The gutter numbers a row with the first line it shows, and only the first row to reach a line numbers it**, so numbers ascend and never repeat: a marker on a row of its own and the fence label below it, a table row's wrap chunks and separators, an image's reserves all stay blank. It reads the document from `ParsedDoc::source`, not the live `Buffer` — it resolves parse-time byte ranges, and the table is memoised per parse.
  - **Its inputs are shared too:** `rendered_view::raw_text::cursor_block_pos` is the single derivation of the cursor's block and `RawPos` (line, column) that every cursor-row caller feeds `row_for_pos`: `cursor_rendered_line_idx`, `cursor_raw_line`, `EditorState::cursor_stacked_row`, the click, and `RenderedView` through `raw_block_cursor`, which adds the block's source. It walks the rope without copying, counts lines at `\n` as `raw_source_lines` splits them (not at every break `ropey` counts, such as U+2028), and clamps a cursor past the block's last line to that line's end. `RenderedView` keeps one branch of its own, for a stale parse, rebuilding the source and position from `cursor_block_line_range` so just-typed characters show before the reparse.
  - **A row revealed in place wraps as the painter wraps its raw line**, behind its `row_map::revealed_indent` (`wrap::revealed_rows_of_str`); so does the cursor's sub-row there (`cursor_sub_line_in_rendered`, gated by `cursor_row_shows_raw`). A flat wrap put the cursor's row one high on a line like `- ab abcdefghijk` at 12 cells.
- **Every row's wrap indent is stated, never read off its text** (`wrap::Indent`: a `lead` before the first row, a `hang` before each continuation). A rendered row's is `RowOrigin::hang`, set by the renderer beside `rendered_col`: content hangs where its text starts (past bars, markers, a footnote's leader, indent), a verbatim row also under its own indentation (`RowOrigin::hung_under_indent`), chrome behind its containers' prefixes only; `shifted` moves it with the prefix, and `ParsedDoc::row_indent` hands it out. A revealed raw line's is `row_map::revealed_indent`: its leaf's `SrcLines` content column in cells (a chrome line takes the leaf's nearest; a line in no leaf, none), plus a verbatim leaf's own indentation. Its `lead` pads the first row by however far the row showing the line started its content right of the line's raw prefix (a right-aligned ` 6.` beside `10.`, a 2-space nested item rendered at its 4-cell nesting), so the reveal doesn't move the text; a line with no prefix (a lazy continuation) is never padded. Every painter, row count, click and hit-test takes the indent from one of the two (vertical navigation from `row_map::source_line_layout`, which picks between them per line) — `compute_hanging_indent`, which guessed it from the text, hung a footnote one cell short (or not at all) and an escaped `1\. ` like a list item. Raw mode and the diff view's marked lines are the exceptions: flat, and behind the `- ` / `+ ` marker (`diff::layout::marker_indent`).
- **Prose reflow breaks the 1:1 the other way, and `EffectiveRows` is how the reveal survives it.** A soft break inside a paragraph is not semantic (CommonMark collapses it to a space), so with `config.editor.reflow` on (default) a paragraph's soft-break segments join into one flow wrapped to the viewport, at any depth — top-level, a list item's first or later paragraph, a quote's, a footnote's (`renderer::paragraph_reflows`). A paragraph with a hard break — top-level or inside emphasis or a link (`*a\⏎b*`) — keeps one row per source line instead, at any depth. A paragraph's *rendered* form is then **shorter** than its raw source (six source lines → two rows) — the reverse of every other block, where raw ≤ rendered. Its flow is one row, a `Flow` origin over the paragraph's lines behind whatever prefix its containers drew. Reflow rides in `RenderSettings`, so a toggle clears the render cache. A joined soft break renders as a space, except one between two CJK glyphs, which renders as nothing (the segment break rule of earlier CSS Text 3 drafts, as Firefox applies it; `markdown::segment_break`). The renderer drops those breaks from the flow's inlines (`renderer::collapse_soft_breaks`), and from a multi-line setext heading's, which joins onto one row the same way, and `InlineColMap` drops their rendered char by the same test on the same neighboring chars, so the row's column map stays in step; `tests/row_provenance.rs` fails on any disagreement.
  - **Any row over several lines stacks, reflow on or off** (`row_map::stacked_lines(block, row)`): an `Inline` or `Flow` content row whose origin spans more than one line — a reflowed paragraph's flow, a multi-line setext heading's text, a row holding a multi-line code span — and a reflowed paragraph's flow over one line too (the renderer's rule, `paragraph_reflows`). A reflowed paragraph is just the common case; the rest of this section says "paragraph" for any stacked row.
  - **The cursor's stack is `row_map::cursor_stack(block, pos)`**, the one gate `EffectiveRows`, the view, the click and the timer share. It is the cursor's row's `stacked_lines`, plus two cases. A line with no row of its own — a link reference definition inside a container, a bare list marker (`-` with the item's text below), a nested setext underline — shares a neighbor's row (`row_for_line`: the next row, or the last for a line past every row), and the stack is the union of that row's lines and the cursor's line, whether or not the row stacks by itself. The union is contiguous, since any line between the two has no row either. The exception is a marker row whose line goes on to show on the rows below it (`> [d]: /u⏎> - - a`): that line would show twice, raw and rendered, so the cursor's line stacks alone. This replaced revealing the cursor's line in place, which painted it over the row it shared. And on a setext underline with a rule row of its own, the heading's text row stacks, so every text line shows while the rule reveals as the underline (`RenderedView`'s setext arm now paints only rule rows and one-line text rows). A shared table row, which reveals cell by cell, and a diagram, whose rows show its lines one for one, never stack. `tests/row_provenance.rs`'s agreement check fails on any row-less line outside the known kinds (`renders_no_row`), so a new kind can't silently reveal over its neighbor.
  - **The reveal makes the paragraph *taller* while the cursor is in it** (its source lines shown stacked), so document height is reveal-dependent — the height-neutral padding the other reveals rely on can't express it. `editor::effective_rows::EffectiveRows` is a per-frame overlay answering the viewport's visual-row questions (`total_visual_rows`, `line_at_visual_row`) with the paragraph's one flow row replaced by its `M` raw lines, as a cheap delta over the base `VisualRowCache`. That row may be one of many in its block (an item among its siblings, a paragraph among a quote's), which stay rendered. `RowHit::Raw::raw_line` is the *block-relative* source line, so a nested paragraph's stack starts past its block's first line (`EffectiveRows::raw_lines`). It is the identity unless the cursor's row stacks in Rendered mode (`EditorState::cursor_stacked_row`, from `cursor_stack`), so the consumers routed through `EditorState::effective_rows` (viewport sizing, `rendered_cursor_visual_row`, `char_offset_at_visual_row`, the gutter, the reveal loop's start, `mouse_ops::coord`'s click and row walk) are unchanged elsewhere. `EditorState` memoises it per `(parse, width, revealed row and its stacked lines)`, so a frame's repeated queries build it once, and memoises `cursor_stacked_row` per `(parse, cursor offset)`, since the patch's key needs that lookup first and it walks the block's rows and a list's items. A miss copies the block's source only, never the document.
  - **A stacked line is the whole source line**, container prefix included (`> alpha`, `- alpha`, `  beta`), as every revealed row is; the quote's wash stays, its bar doesn't (settled 2026-10-06, against re-drawing the rendered chrome in front of the raw text). Columns are therefore the raw line's own. **Its wrap is the painter's**: `wrap::revealed_rows_of_str` behind the line's `row_map::revealed_indent`. `EffectiveRows`, the click's row walk and the cursor's sub-row all measure with it — a flat wrap disagrees with the painter on a line like `- ab abcdefghijk` at 12 cells, which put every click below it a row off.
  - **The stack is the paragraph's lines** (the flow row's origin), so trailing blank lines the block's byte range absorbs never stack: they own rows of their own.
  - **The height change is timer-driven** (reveal after `RAW_REVEAL_DELAY`, un-reveal on exit), so `EditorState::anchor_stacked_reveal` (from `App::prepare_viewport`, beside `sync_image_reveal`) reconciles it. The expansion is entirely *below* the paragraph's first row, so leaving `scroll` alone pins that row and everything above it and lets only the content below reflow (the image/mermaid feel); the toggle frame just re-runs `ensure_cursor_visible` (scrolls the minimum, usually nothing) rather than re-pinning the cursor's exact row.
  - **A stacked row and a mermaid diagram are *reveal-as-one-unit*** (`state_cursor_block`): the delay re-arms on every buffer-line move like any block, so **scrolling through one never dwells long enough to reveal it**; once a dwell reveals it, `cursor_reveal_latched` holds it revealed while the cursor stays inside (`latch_cursor_reveal` per frame), so moving between its lines never flashes it collapsed. The latch drops on leaving the block *or the paragraph* — `cursor_stacked_unit` tracks which stacked row the cursor is in, keyed on `(block, row)`, so a list's items are units of their own. Not on the stack's lines: a row-less line's stack differs from its row's own, and moving between the two must keep the latch. The `EffectiveRows` memo, by contrast, keys on the lines too, since they change the patch. **Exception: entering on a line other than the first** (upward move or click, from another block or a sibling paragraph) reveals and latches immediately — the collapsed flow row can't show the cursor on its true line, which would sit at the top and drop when the paragraph expands. A first-line (downward) entry keeps the delay, since its line *is* the flow row.
- **Jitter-suppression reveal.** `cursor_block_revealed()` returns false during a 120 ms `RAW_REVEAL_DELAY` after the cursor enters a new *buffer line* (not block); `RenderedView` keeps the block rendered and draws an inverted-cell cursor indicator meanwhile. `App::next_deadline` wakes the loop when the delay expires, so the reveal draws without a keypress.
- **Single-pass parse.** `ParsedDoc::build` gets blocks and top-level byte ranges (plus the line-start index and link-definition labels the pass has anyway) from one `parse_document` call, so blocks↔ranges stay 1:1 by construction — don't add a second ranges pass ([performance.md](performance.md#the-two-optimizations-and-why-they-must-not-be-undone)).
- **The AST carries source positions, block-relative.** The same pass records, for every leaf, a `SrcLines` (first line, and per line the char column where content starts past container prefixes, markers and stripped indent; `None` for chrome such as a fence or setext underline), and for every container and `ListItem` a line `span`. They come from pulldown-cmark's own event offsets (`parser::stream::EventStream`), never from re-parsing or sniffing text, and are relative to the top-level block's first line so `RenderCache` hits for a block that moved. Columns are `u32` and saturate: a line's length is untrusted. `SrcLines` has two storage forms (see [performance.md](performance.md#known-ceilings)); `SrcLines::new` always picks the canonical one, which keeps the derived `Eq` / `Hash` comparing content. Content starting inside a partly consumed tab records the tab's own column. `parser::BLOCK_PARSE_COUNT` (tests) pins that recording them still costs one block parse.
- **Block-level render memoization.** `EditorState` owns a `markdown::RenderCache` threaded into every `refresh_parsed`. The key is the `Block` *value* plus a `RenderSettings` fingerprint (theme address, viewport width, `table.row_striping`, big-H1, …); keying by AST rather than source bytes keeps live table-width drags and post-pass mutations correct. `Block::ImageBlock` is never cached (its row count tracks the decode cache). Eviction is by document membership per build. **A new `Renderer` knob that changes output must go into `RenderSettings`**, or stale hits paint with the old setting. Costs, the cache-worthy gate and the remaining ceilings are in [performance.md](performance.md).
- **One shared wrap, one shared painter.** `document::wrap` decides where rows break and the cells each char takes; `ui::line_render` paints those rows with the trailing-cell fill. Preview and Rendered both paint through `line_render`, and the cursor, click and scroll math all read `wrap`, which sits below `editor` so none of them reach up into `ui` for it. Change them, don't fork them.
- **Where a row may break is one predicate.** `wrap::is_break_after`: "anything non-alphanumeric", plus beside any wide glyph. Both neighbors matter, so it takes the slice and an index, not a lone `char`. Smart punctuation is on, so a rendered contraction carries `’` (U+2019) — a rule naming one owes the other.
  - **CJK breaks between glyphs** (`Joint::Glyph`, issue #58). CJK has no spaces between words, so without it a list marker, quote bar or `Note:` before a long CJK run went alone onto a row of its own. The joints, and which punctuation suppresses one, are the table wrap's — see [tables.md](tables.md).
  - **Refinements that remove breaks** (issue #36): a no-break char (`table_layout::is_no_break_char`) binds both neighbors; nothing breaks before closing punctuation or after opening punctuation unless whitespace follows (`no_break_before` / `no_break_after`); `"` / `'` / `’` aren't breaks when a word follows and none precedes (`’90s`); and `.` `,` `:` `'` `’` `_` don't break between two alphanumerics (`they’re`, `3.14`, `file.md`). **`/` is deliberately not exempted** — that would strip every break out of a URL path.
  - **Row-level rules in `visual_rows_of_chars`:** a break right after a one-letter word backs up to the preceding one (`ends_with_lone_word`) unless that word starts the row. A row never ends on whitespace alone: a word too long to fit after a leading pad (a code row's pad cell) hard-breaks instead of leaving a blank row. A window ending on a wide glyph with a space just past it fills the row and absorbs the space; an ASCII word ending at the edge does *not* — it goes down a row, because a reveal that re-wraps the row could land a click on the absorbed, cell-less space.
- **A row may never end mid-grapheme-cluster; the cluster gate outranks every break refinement.** The wrap reasons in `char`s, where a ZWJ looked like a fine break (a family emoji split across rows, a combining mark stranded from its base). `joints_of` builds the line's `table_layout::joints` once (`Joint::Inside` marks inside a cluster), consulted by both `is_break_after` and `snap_to_cluster_boundary`. Two things keep it affordable and terminating: an **all-ASCII fast path** returns `None`, and a snap that would empty the row **keeps the unsnapped break**. This fixes *breaking* inside a cluster, not *measuring* one — `char_cells` still sums a ZWJ sequence's members.
- **A break absorbs the single space that follows it, so `next_start` may exceed `row_end`.** A space at a row *start* reads as accidental indentation. `absorbed_next_start` applies to every arm, not just the hard break. The absorbed char belongs to no row, so both cursor paths know it: `sub_line_of_col` reports a column in `end..next_start` as the **next row's column 0**, and `render_line_reporting_cursor` rewrites a `cursor_col_override` landing there onto that row's first char (otherwise the cursor sits on a cell that doesn't exist). A *run* of spaces is never absorbed (interior whitespace is content — `visual_rows_preserves_interior_whitespace_across_wrap`), nor is a trailing one. Overlay painters intersect with `[row_start, row_end)`, so an absorbed char is never washed.
  - **`wrap::last_col_in_row` is the single clamp keeping a cursor on its own row**, shared by `state_cursor_visual::raw_col_for_visual_cells` and `mouse_ops::coord`'s `char_in_row_at_cell`, `raw_click_col`, `click_to_rendered_char_idx` and `rendered_click_to_line_col`. It measures against `end`, never `next_start`: `next_start - 1` clamps the cursor onto the absorbed space — a char with no cell, so Up stalls permanently and a click on a row's last cell lands a row low. A new screen-cell→char-column site owes it the call.
- **The cursor is a uniform block; color, not shape, signals context.** The cell at the insertion point is recolored while the character stays visible. Resolved in one place — `app::cursor_style::editor_cursor_style` — with views receiving the finished `Style`. The default handler colors by *view* mode (`status_mode_preview` / `_rendered` / `_raw`); under vim the cursor **mirrors the sub-mode chip**, reading the same `status_mode_vim_normal` / `_insert` / `_visual` fields (minus the chip's `BOLD`). RAW is surfaced only in INSERT; NORMAL/VISUAL keep their sub-mode color in every view. Modal inputs use `theme.cursor`. No bar/caret shape, no `CursorShape` enum.
  - Painted via `cursor_col_override`, never baked into the wrapped `Line`, so the wrap keys on glyph-free source text and stays in lockstep with `move_up_visual` / `move_down_visual` and the scroll math. `line_render::paint_row` recolors the resolved cell; the raw-reveal builders in `rendered_view/paint.rs` and `raw_view::raw_display_line` build lines without the cursor; `overlay_raw_cell` recolors a table cell in place. A block cursor on a selected cell wins over that wash.
- **Modal input cursors are blink-stable.** `ui::cursor::text_field_spans` always emits a one-cell slot — the character recolored when the blink is on, a plain cell when off — so the field never changes width. A new text-input modal must use it — through `cursor::scrolled_field_spans` when the value can outgrow the field.
- **Code body rows render behind one pad cell, which their origin records.** `render_code_block` paints each body line as `format!(" {:<width$}", …)` and records the row as `Verbatim`, with `renderer::CODE_PAD_COLS` as its `rendered_col` and the line's content column (past container prefixes and any indent pulldown-cmark stripped) as its `raw_col`, so no consumer knows the pad or the strip exists (issue #28 was three consumers each knowing it, until one didn't). `code_block_render_agrees_with_its_row_origins` pins the pad against both of `code_body_row`'s branches. Fence rows are `Chrome`: a selection or yank washes them whole, and a search match or `:s` preview leaves them alone (`paint::Overlay`). An unclosed fence's last line is code because the parser says so, not because its text fails a fence test.
- **A `Verbatim` row never de-renders, so "revealed ⇒ 1:1" is wrong on one.** A code body, frontmatter or raw HTML row looks the same raw, so `row_map::reveals` keeps it rendered (with its pad and highlighting) and paints only the indicator; fences, `$$` delimiters and every other row reveal. `cursor_block_revealed()` deliberately doesn't know about it — it answers a block-level, time-based question for four other consumers. `RenderedView`'s reveal gate and `mouse_ops::coord` (the revealed-row shortcut *and* `revealed_raw_row_count`) both ask `reveals`, or a click mapped against raw text on a row the view never revealed lands on the wrong character.
- **NBSP padding in code blocks.** Blank lines inside fenced blocks use U+00A0, working around a ratatui `WordWrapper` (`trim: false`) bug where an all-whitespace line yields an extra visual row. Don't simplify back to spaces.
- **Word-group undo merging.** `History::record` merges contiguous single alphanumeric inserts into the previous delta; cursor moves break the group. Delta-based, not snapshot-based.
- **Visual line navigation.** `move_up_visual` / `move_down_visual` and `line_render::render_line` must use the same wrap algorithm (`visual_rows_of_chars` / `sub_line_of_col`) over the same text behind the same indent, or the cursor lands in a different column than where it appears. Navigation steps whole buffer lines, so it lays each out as `row_map::source_line_layout` says the view paints it: which leading chars aren't painted, and the indent behind which the rest wraps (see [input.md](input.md)).
- **`Action` is the full surface.** Every action lives in `config/keymap.rs::Action`; unimplemented variants are no-ops in `edit_ops` so keybindings stay stable while a feature is in flight.
- **Clipboard is feature-gated.** `arboard` sits behind the default-on `clipboard` feature; disabled, copy/cut/paste use the in-process kill-ring, which is also what tests assert against. Wayland needs `wayland-data-control` for read access.