Skip to main content

leaf_ffi/
lib.rs

1//! leaf-ffi — the Swift / C-ABI frontend binding for leaf.
2//!
3//! This is the native-Apple analogue of `leaf-wasm`: it takes `leaf-core`'s
4//! frontend-neutral [`Doc`] — the byte-offset caret model and the AST→glyph
5//! [`VisualMap`] — and exposes it across a C ABI (via UniFFI) in the shape an
6//! AppKit/SwiftUI renderer wants. Core stays the single source of truth for the
7//! text, the caret math, and the offset⇄position mapping; the Swift side only
8//! paints glyphs and forwards key/mouse events back in, exactly as the TUI, gpui,
9//! and wasm frontends do.
10//!
11//! ## The boundary is style *runs*, not glyphs
12//!
13//! [`Doc::build_visual`] resolves the document to rows of per-character glyphs,
14//! each tagged with a semantic [`Role`] and the author's emphasis. Sending one
15//! object per character would make every keystroke O(document) in boundary
16//! crossings. Instead [`LeafDoc::view`] coalesces each row's glyphs into maximal
17//! **runs** of identical style and ships those — a handful of records per line.
18//! The Swift renderer maps each run's `role` to a font/size/weight and its
19//! emphasis flags to traits, the native counterpart of the TUI's `to_ratatui`
20//! and the web's CSS class.
21//!
22//! ## Core owns the grid; Swift owns the pixels
23//!
24//! Core lays a row out in whole character *columns* (a terminal-cell measure),
25//! and every offset⇄position method speaks that grid. It deliberately does *not*
26//! dictate presentation. So a native renderer is *proportional* — body text in a
27//! real family, headings by **size** and weight, code in a monospace panel — and
28//! never multiplies `col × cell_width`. It lets `NSLayoutManager` / Core Text
29//! shape each row, places the caret at [`DocView::caret_ch`] (a UTF-16 offset,
30//! which is exactly what `NSAttributedString` and `NSTextView` count in), and
31//! hit-tests a click through `characterIndex(for:)`, feeding the resulting
32//! row + UTF-16 offset back through [`LeafDoc::click_ch`]. Core measures nothing
33//! in pixels; Swift positions nothing in the model.
34//!
35//! ## Threading
36//!
37//! A UniFFI object is handed to Swift as a reference-counted handle whose methods
38//! take `&self`, so the [`Doc`] lives behind a [`Mutex`]. Every call locks, edits
39//! or reads, and returns a fresh [`DocView`] — one boundary crossing both mutates
40//! and repaints, same as the wasm frontend. Drive it from the main thread.
41
42use std::sync::{Arc, Mutex};
43
44use leaf_core::style::{Baseline, Role, Style as LStyle};
45use leaf_core::wysiwyg::text_width;
46use leaf_core::{
47    Alignment, BlockKind, Capabilities as CoreCapabilities, ColorScheme, Doc, Format, InlineKind,
48    LineFlow as CoreLineFlow, MarkupMode as CoreMarkupMode, MediaKind as CoreMediaKind, View,
49    VisualMap,
50};
51use unicode_segmentation::UnicodeSegmentation;
52
53uniffi::setup_scaffolding!();
54
55/// A parse failure constructing a document — the only fallible entry point. Every
56/// other method is infallible (it operates on an already-parsed model), so they
57/// return a [`DocView`] directly.
58#[derive(Debug, thiserror::Error, uniffi::Error)]
59pub enum LeafError {
60    /// The `format` string handed to [`LeafDoc::new`] wasn't one leaf understands.
61    #[error("unknown format: {name}")]
62    UnknownFormat { name: String },
63    /// `leaf-core` failed to parse `source` as the requested format.
64    #[error("parse error: {message}")]
65    Parse { message: String },
66}
67
68/// One maximal span of same-styled glyphs on a visual row — the unit the Swift
69/// renderer turns into a single styled attributed-string run.
70#[derive(uniffi::Record)]
71pub struct Run {
72    /// The run's text, glyphs concatenated in column order.
73    pub text: String,
74    /// The glyph's semantic role as a renderer class id: `body`, `h1`…`h6`,
75    /// `code`, `link`, `mark`, `list`, `quote`, `rule`.
76    pub role: String,
77    pub bold: bool,
78    pub italic: bool,
79    pub underline: bool,
80    pub strike: bool,
81    /// Raised off the baseline and drawn smaller — a footnote reference's `[1]`,
82    /// or an author's `^x^`. Mutually exclusive with [`sub`](Self::sub); core's
83    /// `Baseline` is one value, and these are its two non-default cases flattened
84    /// to the flag shape the rest of this record is spelled in.
85    pub sup: bool,
86    /// Lowered off the baseline and drawn smaller — an author's `~x~`.
87    pub sub: bool,
88    /// The byte offset in the source this run's first glyph came from.
89    ///
90    /// What a run *means*, as opposed to how it looks: a `link` role says a span
91    /// is drawn as a link but not where it points, and the only way back to that
92    /// is the source. A frontend drawing part of the document somewhere the caret
93    /// isn't — a footnote's text in a popover — pairs this with
94    /// [`LeafDoc::link_destination_at`] or [`LeafDoc::footnote_at`] to make those
95    /// runs followable.
96    ///
97    /// The alternative was for a frontend to count its way along the row's text
98    /// and ask [`LeafDoc::offset_for_pos`], which means converting between three
99    /// units that only agree on ASCII: this is a byte offset, the run's text is
100    /// characters, and a row's column is a *display* cell (a wide CJK glyph is
101    /// two). Handing the offset over is exact, O(1), and needs none of that.
102    ///
103    /// `0` for the runs of the source view, whose rows are split from raw text
104    /// rather than laid out from glyphs.
105    pub src: u32,
106    /// Whether this run lies inside the active selection — so the renderer can
107    /// paint a selection background without re-deriving it from offsets.
108    pub sel: bool,
109}
110
111/// Where a locator lands — what [`LeafDoc::locate`] answers with, and the FFI
112/// mirror of [`leaf_core::Landing`].
113///
114/// A span rather than an offset because the two things a host does with a
115/// locator want different halves of it: following one puts a caret at `start`,
116/// while peeking at one draws the rows between `start` and `end`. Only the first
117/// can be recovered from an offset alone.
118#[derive(uniffi::Record)]
119pub struct LandingView {
120    /// The first byte of the block the locator names — where a caret goes.
121    pub start: u32,
122    /// One past its last byte, so the pair maps through `pos_for_offset` to the
123    /// rendered rows the block occupies, the way [`FootnoteView`]'s pair does.
124    pub end: u32,
125}
126
127impl From<leaf_core::Landing> for LandingView {
128    fn from(l: leaf_core::Landing) -> Self {
129        LandingView {
130            start: l.start as u32,
131            end: l.end as u32,
132        }
133    }
134}
135
136/// A footnote reference and the note it names — what [`LeafDoc::footnote_at`]
137/// answers with. The FFI mirror of [`leaf_core::FootnoteRef`].
138///
139/// A reference whose definition the document is missing still comes back, with
140/// its `label` and no `text`: that a `[^99]` names nothing is a thing to tell
141/// the reader, and it is not the same as the caret standing on no reference at
142/// all (which is `None`).
143#[derive(uniffi::Record)]
144pub struct FootnoteView {
145    /// The reference's label — the `1` of `[^1]`, without the `^` or brackets.
146    pub label: String,
147    /// The note's body as source text, or `None` when nothing defines it.
148    pub text: Option<String>,
149    /// The byte offset the note's body starts at, for a "go to note" that moves
150    /// the caret there. `None` alongside a `None` `text`.
151    pub offset: Option<u32>,
152    /// Where the body ends, exclusive. With `offset` this bounds the note, so a
153    /// frontend can map the pair through `pos_for_offset` to the *rendered rows*
154    /// it occupies and draw those — the note with its markup resolved, rather
155    /// than the asterisks and backticks `text` carries. `None` alongside a
156    /// `None` `offset`.
157    pub end: Option<u32>,
158}
159
160impl From<leaf_core::FootnoteRef> for FootnoteView {
161    fn from(f: leaf_core::FootnoteRef) -> Self {
162        FootnoteView {
163            label: f.label,
164            text: f.text,
165            offset: f.offset.map(|o| o as u32),
166            end: f.end.map(|o| o as u32),
167        }
168    }
169}
170
171/// A footnote definition and the reference that sends a reader to it — what
172/// [`LeafDoc::footnote_definition_at_caret`] answers with, and the FFI mirror of
173/// [`leaf_core::FootnoteDef`].
174///
175/// The other half of [`FootnoteView`]'s round trip: that one carries a reader
176/// down to the note, this one carries them back up. A definition nothing cites
177/// still comes back, with its `label` and no `offset`, for the reason an
178/// undefined reference does — "nothing refers to this note" is worth saying.
179#[derive(uniffi::Record)]
180pub struct FootnoteDefView {
181    /// The definition's label — the `1` of `[^1]: …`, spelled exactly as
182    /// [`FootnoteView::label`] spells the same footnote's.
183    pub label: String,
184    /// The byte offset the first reference starts at, for a "back to reference"
185    /// that moves the caret there. `None` for a note nothing refers to.
186    pub offset: Option<u32>,
187}
188
189impl From<leaf_core::FootnoteDef> for FootnoteDefView {
190    fn from(f: leaf_core::FootnoteDef) -> Self {
191        FootnoteDefView {
192            label: f.label,
193            offset: f.offset.map(|o| o as u32),
194        }
195    }
196}
197
198/// One visual line: its styled runs plus the row-level flags a frontend draws
199/// chrome from.
200#[derive(uniffi::Record)]
201pub struct Row {
202    pub runs: Vec<Run>,
203    /// Drawn but holds no caret (a table rule, a block-gap blank line): the
204    /// renderer skips it for click/caret math. See [`leaf_core::VRow`].
205    pub decoration: bool,
206    /// A fenced/indented code-block line — the renderer draws a tinted, bordered
207    /// panel around each maximal run of these.
208    pub code: bool,
209    /// A fenced block's language, carried on the block's first code row only.
210    pub code_lang: Option<String>,
211    /// A `:::name{.class}` directive-container line — the renderer draws a
212    /// tinted panel around each maximal run of these, the `code` recipe. See
213    /// [`leaf_core::VRow::directive`].
214    pub directive: bool,
215    /// A directive container's space-joined `.class` attrs, carried on the
216    /// block's first row only. See [`leaf_core::VRow::directive_label`].
217    pub directive_label: Option<String>,
218    /// The heading level (1–6) if this row belongs to a heading block, else
219    /// `None`. A proportional renderer sizes the *whole* row from this so an
220    /// inline `` `code` `` run inside a heading still reads at the heading's size.
221    pub heading: Option<u8>,
222    /// What this row divides, on the blank rows a block boundary is drawn with
223    /// and `None` everywhere else — so `boundary != nil` is exactly "this row is
224    /// a drawn block boundary". A frontend spaces a boundary by the pair it
225    /// falls between (the margin above a heading is wider than the one between
226    /// two paragraphs); the *height* is the frontend's, the *kind* is core's.
227    /// See [`leaf_core::Boundary`].
228    pub boundary: Option<Boundary>,
229}
230
231/// What a drawn block boundary separates. The FFI mirror of
232/// [`leaf_core::Boundary`].
233#[derive(uniffi::Record)]
234pub struct Boundary {
235    pub above: BlockClass,
236    pub below: BlockClass,
237}
238
239/// The block kinds core tells apart — the vocabulary a [`Boundary`] is spelled
240/// in. The FFI mirror of [`leaf_core::BlockClass`]; `Other` covers every kind
241/// core doesn't separate out, so a frontend's `match` stays exhaustive as the
242/// list grows.
243#[derive(uniffi::Enum)]
244pub enum BlockClass {
245    Paragraph,
246    Heading,
247    /// A whole list. Core draws no boundary row *between* two items of one list,
248    /// tight or loose, so an `ListItem`↔`ListItem` pair never reaches a frontend.
249    List,
250    ListItem,
251    Quote,
252    Code,
253    Table,
254    Media,
255    Directive,
256    Rule,
257    Footnote,
258    Other,
259}
260
261impl From<leaf_core::BlockClass> for BlockClass {
262    fn from(k: leaf_core::BlockClass) -> Self {
263        use leaf_core::BlockClass as K;
264        match k {
265            K::Paragraph => BlockClass::Paragraph,
266            K::Heading => BlockClass::Heading,
267            K::List => BlockClass::List,
268            K::ListItem => BlockClass::ListItem,
269            K::Quote => BlockClass::Quote,
270            K::Code => BlockClass::Code,
271            K::Table => BlockClass::Table,
272            K::Media => BlockClass::Media,
273            K::Directive => BlockClass::Directive,
274            K::Rule => BlockClass::Rule,
275            K::Footnote => BlockClass::Footnote,
276            K::Other => BlockClass::Other,
277        }
278    }
279}
280
281/// One *visual line* of a table cell: its styled runs and the source offsets
282/// bounding it. A cell is usually one line, but an in-cell hard break (an inline
283/// `<br>`) splits it into several — each its own line here, so the frontend
284/// shapes and caret-maps them independently (the byte↔UTF-16 offset math a cell
285/// needs holds within a line, which carries no break). The runs are *unwrapped*:
286/// column width — and any soft wrap within it — is the frontend's to decide.
287#[derive(uniffi::Record)]
288pub struct TableCellLineView {
289    pub runs: Vec<Run>,
290    /// The source offsets bounding this line's content — the caret home at its
291    /// start and the stop just past its end.
292    pub start: u32,
293    pub end: u32,
294}
295
296/// One cell of a table's structural grid: its content as one or more visual
297/// lines, the column alignment its text honours, and the source range the whole
298/// cell occupies (where a click or the caret lands).
299#[derive(uniffi::Record)]
300pub struct TableCellView {
301    /// The cell's lines, in order — one unless an in-cell `<br>` splits it.
302    pub lines: Vec<TableCellLineView>,
303    /// `"left"`, `"right"`, `"center"`, or `"default"`.
304    pub align: String,
305    /// The source offsets bounding the cell's content — the caret anchors a
306    /// click in the cell resolves to.
307    pub start: u32,
308    pub end: u32,
309}
310
311/// One row of a table's structural grid; a header row draws bold and is ruled
312/// off from the body below it.
313#[derive(uniffi::Record)]
314pub struct TableRowView {
315    pub head: bool,
316    pub cells: Vec<TableCellView>,
317}
318
319/// A table described *structurally* rather than as the monospace box-glyph
320/// picture that spells it in [`DocView::rows`]. A proportional renderer draws its
321/// own grid from this — columns sized to content, real borders — and SKIPS the
322/// picture rows in `[start_row, end_row)`. The two describe the same cells at the
323/// same source offsets, so the caret lands identically either way. See
324/// [`leaf_core::TableInfo`].
325#[derive(uniffi::Record)]
326pub struct TableView {
327    /// The [`DocView::rows`] indices the box-drawn picture occupies — the rows a
328    /// grid-drawing frontend skips.
329    pub start_row: u32,
330    pub end_row: u32,
331    pub grid: Vec<TableRowView>,
332}
333
334/// A leaf directive (`::name{…}`) — a standalone block with no body, drawn in
335/// [`DocView::rows`] as a one-row `⧉ name` placeholder. A frontend that knows
336/// the host app's vocabulary reads this and paints the real thing over the rows
337/// in `[start_row, end_row)` — a web view for diaryx's `::embed{src=…}`, say —
338/// exactly as a grid-drawing one replaces a [`TableView`]'s picture rows. One
339/// that doesn't just paints the placeholder, which is already framed by the
340/// directive panel chrome.
341///
342/// Core resolves nothing here and neither does this layer: the vocabulary
343/// belongs to the app. See [`leaf_core::DirectiveInfo`].
344#[derive(uniffi::Record)]
345pub struct DirectiveView {
346    /// The [`DocView::rows`] indices the placeholder occupies.
347    pub start_row: u32,
348    pub end_row: u32,
349    /// The directive's type (`embed`, `toc`, `vis`), no leading colons.
350    pub name: String,
351    /// Its `[label]` text, or empty — what the placeholder row shows.
352    pub label: String,
353    /// Its `{…}` attributes in source order. A bare attribute (`{public}`) has an
354    /// empty value, which a consumer reads as a flag.
355    pub attrs: Vec<DirectiveAttr>,
356}
357
358/// One `{key=value}` attribute of a [`DirectiveView`]. A record rather than a
359/// tuple because UniFFI has no tuple type; an absent value flattens to `""`,
360/// since a bare attribute is a flag and the distinction from `key=""` has no
361/// consumer on this side.
362#[derive(uniffi::Record)]
363pub struct DirectiveAttr {
364    pub key: String,
365    pub value: String,
366}
367
368/// What a block-level media placeholder is, so Swift knows which view to build
369/// over the rows core reserved: an `NSImageView`/`UIImageView`, or an
370/// `AVPlayerView` with or without a picture to show. The peer of
371/// [`leaf_core::MediaKind`].
372#[derive(uniffi::Enum)]
373pub enum MediaKind {
374    Image,
375    Video,
376    Audio,
377}
378
379/// One `<source>` alternative of a block media element — a candidate URL plus
380/// whichever of the two things HTML picks a `<source>` by: a media query
381/// (`<picture>`) or a MIME type (`<video>`/`<audio>`).
382///
383/// Unlike the web frontend, which hands the whole list to the browser and lets
384/// it choose, a native renderer usually wants [`MediaView::src`] — already
385/// resolved for the current appearance — and reaches in here only to pick a
386/// codec `AVFoundation` can actually play.
387#[derive(uniffi::Record)]
388pub struct MediaSourceView {
389    /// The `media="…"` query, or empty for an unconditional source.
390    pub media: String,
391    /// The candidate URL (a `<picture>` `srcset` or a `<video>`/`<audio>` `src`).
392    pub src: String,
393    /// The `type="…"` MIME (`"video/webm"`), or empty when none is declared.
394    pub mime: String,
395}
396
397/// One block-level image, video, or audio: which rows core reserved for it and
398/// what to build there. The peer of [`leaf_core::MediaInfo`], and the media
399/// analogue of [`DirectiveView`] — a frontend **skips the rows in
400/// `start_row..end_row`** and lays its own view over them, rather than painting
401/// the `🖼`/`🎬`/`🔊` placeholder glyphs core put there for a surface that can't.
402#[derive(uniffi::Record)]
403pub struct MediaView {
404    /// The [`DocView::rows`] indices the placeholder occupies.
405    pub start_row: u32,
406    pub end_row: u32,
407    /// Which of the three this is — the view to build.
408    pub kind: MediaKind,
409    /// The URL to load, already resolved against the current appearance (see
410    /// [`LeafDoc::set_dark_appearance`]). A relative path resolves against the
411    /// document's own directory, which core does not know — the host does.
412    /// Empty only when a `<video>`/`<audio>` named neither a `src` nor a
413    /// `<source>`, which is a broken document.
414    pub src: String,
415    /// A `<video>`'s poster frame URL, or empty. An image destination, so it
416    /// loads exactly as an image `src` does — worth showing before the movie is
417    /// ready, or in place of one that won't play.
418    pub poster: String,
419    /// The alt / fallback text, for the view's accessibility label.
420    pub alt: String,
421    /// The `<source>` alternatives in document order; empty for a plain image.
422    pub sources: Vec<MediaSourceView>,
423}
424
425/// A per-destination measured height, the way Swift reports one back — the input
426/// half of the loop [`LeafDoc::set_media_rows`] closes.
427#[derive(uniffi::Record)]
428pub struct MediaHeight {
429    /// The media's `src` as it appeared in the document, keying it to a
430    /// [`MediaView`].
431    pub destination: String,
432    /// How many visual rows the laid-out view needs.
433    pub rows: u32,
434}
435
436/// A whole rendered frame: the rows to paint, where the caret sits, and the
437/// toolbar state — everything the Swift side needs for one repaint, in one value.
438/// Returned by every view-producing method.
439#[derive(uniffi::Record)]
440pub struct DocView {
441    pub rows: Vec<Row>,
442    /// Tables described structurally, for a frontend that draws its own grid
443    /// instead of painting the box-glyph rows. Empty in the source view. Each
444    /// names the `rows` span its picture occupies, to be skipped.
445    pub tables: Vec<TableView>,
446    /// Leaf directives (`::name{…}`) described structurally, for a frontend that
447    /// paints what the host app's vocabulary makes of them instead of the `⧉`
448    /// placeholder row. Empty in the source view, where the directive is the
449    /// literal text the caret is editing.
450    pub directives: Vec<DirectiveView>,
451    /// Block-level images, videos, and audio described structurally, for a
452    /// frontend that lays real views over the rows core reserved instead of
453    /// painting the placeholder glyphs. Empty in the source view, where the
454    /// `![](…)` or `<video>` markup is the literal text being edited.
455    pub media: Vec<MediaView>,
456    /// The caret's row: an index into [`Self::rows`].
457    pub caret_row: u32,
458    /// The caret's display *column* within its row — core's grid position. Kept
459    /// for callers reasoning in columns; a proportional renderer wants
460    /// [`Self::caret_ch`] instead.
461    pub caret_col: u32,
462    /// The caret's offset within its row's text in **UTF-16 code units** — what
463    /// `NSAttributedString`/`NSTextView` count to. This is `caret_col` mapped
464    /// through the row's grapheme widths, so it lands the caret correctly past
465    /// wide glyphs (CJK, emoji) where a column and a character index diverge.
466    pub caret_ch: u32,
467    /// The caret's **source byte offset** — the coordinate a table cell is keyed
468    /// by (`TableCellView::start`/`end`), so a frontend drawing its own grid can
469    /// find which cell the caret sits in without the picture-row indices.
470    pub caret_src: u32,
471    /// Whether a (non-empty) selection is active.
472    pub has_selection: bool,
473    /// The selection's *fixed* end (the caret is the moving end), as a row and a
474    /// UTF-16 offset — so the renderer can restore a native selection with the
475    /// same direction the model has. Equal to the caret when `has_selection` is
476    /// false.
477    pub anchor_row: u32,
478    pub anchor_ch: u32,
479    /// Whether the buffer differs from the last saved bytes — for a "● modified"
480    /// affordance.
481    pub dirty: bool,
482    /// `"wysiwyg"` or `"source"`, for a view-toggle affordance.
483    pub view: String,
484    /// The heading level at the caret, if any — a toolbar lights H1…H6 from it.
485    pub heading: Option<u32>,
486    /// The inline marks active at the caret (`bold`, `italic`, `code`, …) — the
487    /// toolbar lights the matching buttons.
488    pub active: Vec<String>,
489    /// The destination of the link the caret stands in, or `None` — the toolbar
490    /// lights its Link button from it and seeds an edit of that link with it.
491    ///
492    /// It rides the frame rather than being a query a toolbar makes for itself
493    /// because a toolbar only redraws when the *state* changes: walking the caret
494    /// out of a link changes no mark, no heading, and no dirty flag, so a Link
495    /// button reading this by a call of its own would keep a stale light on. Same
496    /// reason `heading` is here and not asked for.
497    ///
498    /// Only a *parsed* link answers ([`LeafDoc::link_destination_at_caret`]);
499    /// a wikilink is literal text with no node behind it, and has nothing to
500    /// repoint — see `LinkTarget.swift`.
501    pub link: Option<String>,
502}
503
504/// A visual position: a row index plus a UTF-16 offset within that row's text —
505/// the coordinate the geometry side (Core Text) draws from. Returned by
506/// [`LeafDoc::pos_for_offset`], the bridge from a source offset (what a
507/// `UITextPosition` wraps) to where it sits on screen.
508#[derive(uniffi::Record)]
509pub struct RowCol {
510    pub row: u32,
511    pub ch: u32,
512}
513
514/// The rows a source range covers, both ends **inclusive** — what a frontend
515/// slices out of a frame to draw a block somewhere other than where it sits: a
516/// footnote peek, a link peek, a landing flash. Returned by
517/// [`LeafDoc::row_range_for`].
518///
519/// Inclusive rather than half-open because the answer is "these rows", not "up
520/// to here": every caller wants `rows[first...last]`, and a `last` one past the
521/// end would be a second thing to get wrong at each of them. `last >= first`
522/// always, so the pair is never empty — a range with no visible byte still
523/// covers the row it opened on.
524#[derive(uniffi::Record)]
525pub struct RowRange {
526    pub first: u32,
527    pub last: u32,
528}
529
530/// Which formatting controls this document's format can spell — the toolbar's
531/// enabled state, one flag per button, from [`LeafDoc::capabilities`]. Mirrors
532/// [`leaf_core::Capabilities`], where the reasoning lives.
533///
534/// Its shape is a flat record of `Bool`s rather than a query taking a gesture
535/// because the Swift side wants exactly one crossing and a value it can hold in
536/// an `@Observable`: `let caps = doc.capabilities()`, then
537/// `.disabled(!caps.bold)` on each control.
538#[derive(uniffi::Record)]
539pub struct Capabilities {
540    pub bold: bool,
541    pub italic: bool,
542    pub code: bool,
543    pub mark: bool,
544    pub underline: bool,
545    pub strike: bool,
546    pub superscript: bool,
547    pub subscript: bool,
548    /// Both [`LeafDoc::set_heading`] and [`LeafDoc::set_paragraph`] — they are
549    /// the same gesture in core and stand or fall together.
550    pub heading: bool,
551    pub blockquote: bool,
552    pub bullet_list: bool,
553    pub ordered_list: bool,
554    /// [`LeafDoc::toggle_task_item`], [`LeafDoc::toggle_task_checked`] and
555    /// [`LeafDoc::toggle_task_at`] — including a *tap* on a rendered checkbox,
556    /// which should not be live where the box cannot be spelled.
557    pub task: bool,
558    pub link: bool,
559    /// [`LeafDoc::insert_image`] and [`LeafDoc::insert_media`] both.
560    pub image: bool,
561    pub thematic_break: bool,
562    /// [`LeafDoc::insert_footnote`]. Markdown and djot spell the pair; HTML does
563    /// not, so the button goes rather than dims into a refusal.
564    pub footnote: bool,
565    pub code_language: bool,
566    /// The grid controls. Gate them on this *and* [`LeafDoc::caret_in_table`]:
567    /// this asks whether the format's tables are editable, that whether the
568    /// caret is in one.
569    pub table: bool,
570    /// Shift+Return inside a cell — [`LeafDoc::cell_line_break`].
571    pub cell_line_break: bool,
572}
573
574impl From<CoreCapabilities> for Capabilities {
575    fn from(c: CoreCapabilities) -> Self {
576        Self {
577            bold: c.bold,
578            italic: c.italic,
579            code: c.code,
580            mark: c.mark,
581            underline: c.underline,
582            strike: c.strike,
583            superscript: c.superscript,
584            // `subscript` is a Swift keyword; uniffi escapes it in the generated
585            // binding (`caps.`subscript``), so the field keeps its real name
586            // here rather than wearing a suffix on both sides of the boundary.
587            subscript: c.subscript,
588            heading: c.heading,
589            blockquote: c.blockquote,
590            bullet_list: c.bullet_list,
591            ordered_list: c.ordered_list,
592            task: c.task,
593            link: c.link,
594            image: c.image,
595            thematic_break: c.thematic_break,
596            footnote: c.footnote,
597            code_language: c.code_language,
598            table: c.table,
599            cell_line_break: c.cell_line_break,
600        }
601    }
602}
603
604/// A table column's text alignment — the argument to
605/// [`LeafDoc::table_set_alignment`]. Mirrors twig's `Alignment`.
606#[derive(uniffi::Enum)]
607pub enum TableAlignment {
608    Default,
609    Left,
610    Right,
611    Center,
612}
613
614impl TableAlignment {
615    fn into_core(self) -> Alignment {
616        match self {
617            TableAlignment::Default => Alignment::Default,
618            TableAlignment::Left => Alignment::Left,
619            TableAlignment::Right => Alignment::Right,
620            TableAlignment::Center => Alignment::Center,
621        }
622    }
623}
624
625/// How much of the source markup the rich view exposes — the argument to
626/// [`LeafDoc::set_markup_mode`]. Mirrors [`leaf_core::MarkupMode`]; `None`
627/// is the default (the clean surface Diaryx ships, with typed syntax kept
628/// literal).
629///
630/// A single three-way ladder rather than a pair of toggles, because only three
631/// of the four combinations of its two axes — reveal the caret's delimiters,
632/// author markup from typing — are coherent. See [`leaf_core::MarkupMode`]
633/// for which one is left out and why.
634#[derive(uniffi::Enum)]
635pub enum MarkupMode {
636    None,
637    Shortcuts,
638    Full,
639}
640
641impl MarkupMode {
642    fn into_core(self) -> CoreMarkupMode {
643        match self {
644            MarkupMode::None => CoreMarkupMode::None,
645            MarkupMode::Shortcuts => CoreMarkupMode::Shortcuts,
646            MarkupMode::Full => CoreMarkupMode::Full,
647        }
648    }
649
650    fn from_core(mode: CoreMarkupMode) -> Self {
651        match mode {
652            CoreMarkupMode::None => MarkupMode::None,
653            CoreMarkupMode::Shortcuts => MarkupMode::Shortcuts,
654            CoreMarkupMode::Full => MarkupMode::Full,
655        }
656    }
657}
658
659/// How the rich view treats a soft break (a bare newline inside a paragraph) —
660/// the argument to [`LeafDoc::set_line_flow`]. Mirrors [`leaf_core::LineFlow`];
661/// `Fold` is the default (soft breaks reflow into the paragraph, as before).
662#[derive(uniffi::Enum)]
663pub enum LineFlow {
664    Fold,
665    Preserve,
666}
667
668impl LineFlow {
669    fn into_core(self) -> CoreLineFlow {
670        match self {
671            LineFlow::Fold => CoreLineFlow::Fold,
672            LineFlow::Preserve => CoreLineFlow::Preserve,
673        }
674    }
675
676    fn from_core(mode: CoreLineFlow) -> Self {
677        match mode {
678            CoreLineFlow::Fold => LineFlow::Fold,
679            CoreLineFlow::Preserve => LineFlow::Preserve,
680        }
681    }
682}
683
684/// A live leaf document bound for a native Apple frontend: `leaf_core::Doc` plus
685/// the wrap width the current viewport implies, behind a mutex. Constructed from
686/// an in-memory string and driven entirely through method calls — there is no
687/// filesystem behind it.
688#[derive(uniffi::Object)]
689pub struct LeafDoc {
690    inner: Mutex<Inner>,
691}
692
693/// The guarded state. Its methods assume the lock is held (they take `&mut
694/// self`); the [`LeafDoc`] exported wrappers acquire it, delegate, and return the
695/// resulting frame.
696struct Inner {
697    doc: Doc,
698    /// The wrap mode. `Some(cols)` wraps the map at that column budget (a terminal,
699    /// or a fixed-cell frontend); `None` builds it **unwrapped** — one row per block —
700    /// for a proportional GUI that wraps at its own pixel width. `build_visual`
701    /// caches on `(revision, width)`, so re-syncing when neither moved is free.
702    width: Option<usize>,
703    /// The host's current appearance, which a `<picture>`'s `prefers-color-scheme`
704    /// `<source>`s are matched against when resolving a block image's URL. Core
705    /// has no theme of its own, so this is AppKit/UIKit answering on its behalf;
706    /// defaults to light until the host calls
707    /// [`LeafDoc::set_dark_appearance`].
708    scheme: ColorScheme,
709}
710
711// SAFETY: `Doc` embeds a `twig::Editor`, which holds a `NonNull<TwigEditor>` and
712// is therefore `!Send`. UniFFI hands `LeafDoc` to Swift as a reference-counted
713// handle that must be `Send + Sync`, so `Inner` must be `Send`. This is sound
714// because:
715//   1. Every access goes through `LeafDoc::lock()` — the `Mutex` serializes all
716//      reads and mutations, so there is never concurrent access to the handle.
717//   2. twig's editor handle owns a plain heap allocation with no thread-affinity
718//      (no thread-locals, no per-thread state) — moving the pointer between
719//      threads is fine as long as use is serialized, which (1) guarantees.
720// The intended usage is still main-thread-driven; this impl only permits the
721// handle to cross threads safely, it does not invite concurrent use.
722unsafe impl Send for Inner {}
723
724impl Inner {
725    /// Rebuild the visual map at the current width. Cheap (cached) when nothing
726    /// changed; the guard that lets every movement/click method assume a fresh
727    /// grid regardless of call order.
728    fn sync(&mut self) {
729        match self.width {
730            Some(w) => self.doc.build_visual(w),
731            None => self.doc.build_visual_unwrapped(),
732        }
733    }
734
735    /// The plain text of visual row `row` in the active view — the string the
736    /// renderer concatenates its runs into. Backs the column⇄UTF-16 mapping.
737    fn row_text(&self, row: usize) -> String {
738        match self.doc.view {
739            View::Wysiwyg => self
740                .doc
741                .vmap
742                .rows
743                .get(row)
744                .map(|r| r.glyphs.iter().map(|g| g.ch).collect())
745                .unwrap_or_default(),
746            View::Source => self
747                .doc
748                .source
749                .split('\n')
750                .nth(row)
751                .unwrap_or("")
752                .to_string(),
753        }
754    }
755
756    /// The `(row, display-column)` a source offset sits at in the active view.
757    fn pos_of_offset(&self, off: usize) -> (usize, usize) {
758        match self.doc.view {
759            View::Wysiwyg => self.doc.vmap.pos_of_offset(off),
760            View::Source => {
761                let s = &self.doc.source;
762                // Walk back to a character boundary, not just into range. Every
763                // offset here arrives from a UI toolkit counting in its own
764                // units, so one landing mid-character is ordinary input — and
765                // slicing on it aborts the process across an FFI boundary that
766                // has no unwinding. `snap_stop` and `text_in_range` already do
767                // this; this was the one door left open.
768                let mut off = off.min(s.len());
769                while off > 0 && !s.is_char_boundary(off) {
770                    off -= 1;
771                }
772                let row = s[..off].bytes().filter(|&b| b == b'\n').count();
773                let line_start = s[..off].rfind('\n').map_or(0, |i| i + 1);
774                (row, text_width(&s[line_start..off]))
775            }
776        }
777    }
778
779    /// The inclusive row span a source range occupies in the active view.
780    ///
781    /// The rich view defers to [`leaf_core::wysiwyg::VisualMap::row_range_for`],
782    /// where the reasoning lives. The source view has no hidden bytes at all —
783    /// every byte is drawn on the line it is written on — so counting newlines
784    /// is the whole answer, and the last row is the one holding the range's last
785    /// byte rather than the one past it.
786    fn row_range_for(&self, start: usize, end: usize) -> (usize, usize) {
787        match self.doc.view {
788            View::Wysiwyg => self.doc.vmap.row_range_for(start..end),
789            View::Source => {
790                let first = self.pos_of_offset(start).0;
791                let last = self.pos_of_offset(end.max(start.saturating_add(1)) - 1).0;
792                (first, last.max(first))
793            }
794        }
795    }
796
797    /// The source offset under a click at row `row`, `ch` UTF-16 units in.
798    fn offset_at(&mut self, row: usize, ch: usize) -> usize {
799        self.sync();
800        let col = utf16_to_col(&self.row_text(row), ch);
801        self.doc.click(row, col, false);
802        self.doc.caret
803    }
804
805    // ── position mapping for UITextInput (non-mutating; caret untouched) ───────
806    // These branch by view exactly as `pos_of_offset` does, so the WYSIWYG map and
807    // the raw-source grid each answer in their own coordinates.
808
809    /// The source offset of display column `col` on visual `row` — the inverse of
810    /// [`Self::pos_of_offset`] in column space.
811    fn offset_of_col(&self, row: usize, col: usize) -> usize {
812        match self.doc.view {
813            View::Wysiwyg => self.doc.vmap.offset_of_pos(row, col),
814            View::Source => {
815                let line = self.row_text(row);
816                let (mut c, mut b) = (0usize, 0usize);
817                for g in line.graphemes(true) {
818                    if c >= col {
819                        break;
820                    }
821                    c += text_width(g);
822                    b += g.len();
823                }
824                self.source_line_start(row) + b
825            }
826        }
827    }
828
829    /// The byte offset where visual `row` begins in the source view.
830    fn source_line_start(&self, row: usize) -> usize {
831        self.doc
832            .source
833            .split('\n')
834            .take(row)
835            .map(|l| l.len() + 1)
836            .sum()
837    }
838
839    /// The next caret stop after `off`, or `None` at the end.
840    fn stop_after(&self, off: usize) -> Option<usize> {
841        match self.doc.view {
842            View::Wysiwyg => self.doc.vmap.stop_after(off),
843            View::Source => {
844                let s = &self.doc.source;
845                if off >= s.len() {
846                    None
847                } else {
848                    Some(
849                        s[off..]
850                            .grapheme_indices(true)
851                            .nth(1)
852                            .map_or(s.len(), |(i, _)| off + i),
853                    )
854                }
855            }
856        }
857    }
858
859    /// The previous caret stop before `off`, or `None` at the start.
860    fn stop_before(&self, off: usize) -> Option<usize> {
861        match self.doc.view {
862            View::Wysiwyg => self.doc.vmap.stop_before(off),
863            View::Source => {
864                let s = &self.doc.source;
865                let off = off.min(s.len());
866                if off == 0 {
867                    None
868                } else {
869                    s[..off].grapheme_indices(true).next_back().map(|(i, _)| i)
870                }
871            }
872        }
873    }
874
875    /// Snap `off` to a valid caret stop (WYSIWYG) / char boundary (source).
876    fn snap_stop(&self, off: usize) -> usize {
877        let s = &self.doc.source;
878        let mut off = off.min(s.len());
879        match self.doc.view {
880            View::Wysiwyg => self.doc.vmap.snap_to_stop(off),
881            View::Source => {
882                while off > 0 && !s.is_char_boundary(off) {
883                    off -= 1;
884                }
885                off
886            }
887        }
888    }
889
890    /// The navigable visual row above `row`, if any.
891    fn nav_above(&self, row: usize) -> Option<usize> {
892        match self.doc.view {
893            View::Wysiwyg => self.doc.vmap.navigable_above(row),
894            View::Source => (row > 0).then(|| row - 1),
895        }
896    }
897
898    /// The navigable visual row below `row`, if any.
899    fn nav_below(&self, row: usize) -> Option<usize> {
900        match self.doc.view {
901            View::Wysiwyg => self.doc.vmap.navigable_below(row),
902            View::Source => {
903                let n = self.doc.source.split('\n').count();
904                (row + 1 < n).then_some(row + 1)
905            }
906        }
907    }
908
909    /// Resolve the current document to a renderable frame of style runs. Called
910    /// for the first paint, on resize, and by every mutating wrapper so one
911    /// boundary crossing both edits and repaints.
912    fn view(&mut self) -> DocView {
913        self.sync();
914
915        let (ss, se) = self.doc.selection().unwrap_or((usize::MAX, usize::MAX));
916
917        // The two views speak different grids — the WYSIWYG map's resolved glyphs
918        // vs the raw source split on newlines — and `caret_pos` branches to match,
919        // so the rows must too or the caret lands on the wrong text.
920        let rows = match self.doc.view {
921            View::Wysiwyg => wysiwyg_rows(&self.doc.vmap, ss, se),
922            View::Source => source_rows(&self.doc.source, ss, se),
923        };
924        // Structural tables, for a proportional renderer that draws its own grid;
925        // none in the source view (the caret rides raw pipe text there).
926        let tables = match self.doc.view {
927            View::Wysiwyg => wysiwyg_tables(&self.doc.vmap, ss, se),
928            View::Source => Vec::new(),
929        };
930
931        // Leaf directives, on the same terms as the tables above: structural in
932        // the rich view, absent in the source view.
933        let directives = match self.doc.view {
934            View::Wysiwyg => wysiwyg_directives(&self.doc.vmap),
935            View::Source => Vec::new(),
936        };
937
938        // Block media, on the same terms again: only the rich view has
939        // placeholder rows to lay a view over.
940        let media = match self.doc.view {
941            View::Wysiwyg => wysiwyg_media(&self.doc.vmap, self.scheme),
942            View::Source => Vec::new(),
943        };
944
945        let (caret_row, caret_col) = self.doc.caret_pos();
946        // Map the caret's display column to a UTF-16 text offset so a native
947        // renderer can place it past wide glyphs (see [`DocView::caret_ch`]).
948        let caret_ch = col_to_utf16(&self.row_text(caret_row), caret_col);
949        // The selection's fixed (anchor) end, in the same row/UTF-16 terms.
950        let (has_selection, anchor_row, anchor_ch) = match self.doc.selection() {
951            Some(_) => {
952                let a = self.doc.anchor.unwrap_or(self.doc.caret);
953                let (ar, ac) = self.pos_of_offset(a);
954                (true, ar, col_to_utf16(&self.row_text(ar), ac))
955            }
956            None => (false, caret_row, caret_ch),
957        };
958        let heading = self.doc.current_heading_level();
959        let active = self
960            .doc
961            .active_inline_marks()
962            .iter()
963            .map(|k| mark_id(k).to_string())
964            .collect();
965        let link = self.doc.link_destination_at_caret();
966
967        DocView {
968            rows,
969            tables,
970            directives,
971            media,
972            caret_row: caret_row as u32,
973            caret_col: caret_col as u32,
974            caret_ch: caret_ch as u32,
975            caret_src: self.doc.caret.min(self.doc.source.len()) as u32,
976            has_selection,
977            anchor_row: anchor_row as u32,
978            anchor_ch: anchor_ch as u32,
979            dirty: self.doc.dirty,
980            view: self.doc.view_name().to_string(),
981            heading,
982            active,
983            link,
984        }
985    }
986}
987
988#[uniffi::export]
989impl LeafDoc {
990    /// Parse `source` as `format` (`"markdown"`/`"md"`, `"djot"`/`"dj"`,
991    /// `"html"`, `"xml"`) into a live, untitled document.
992    #[uniffi::constructor]
993    pub fn new(source: String, format: String) -> Result<Arc<Self>, LeafError> {
994        let format = match format.to_ascii_lowercase().as_str() {
995            "markdown" | "md" => Format::Markdown,
996            "djot" | "dj" => Format::Djot,
997            "html" | "htm" => Format::Html,
998            "xml" => Format::Xml,
999            other => {
1000                return Err(LeafError::UnknownFormat {
1001                    name: other.to_string(),
1002                });
1003            }
1004        };
1005        let doc = Doc::from_source(source, format).map_err(|e| LeafError::Parse {
1006            message: e.to_string(),
1007        })?;
1008        Ok(Arc::new(LeafDoc {
1009            inner: Mutex::new(Inner {
1010                doc,
1011                width: Some(80),
1012                scheme: ColorScheme::Light,
1013            }),
1014        }))
1015    }
1016
1017    /// Resolve the current document to a renderable frame — the first paint.
1018    pub fn view(&self) -> DocView {
1019        self.lock().view()
1020    }
1021
1022    /// Set the wrap width (in columns) the viewport implies and repaint. For a
1023    /// fixed-cell frontend (a terminal); a proportional GUI uses [`set_unwrapped`].
1024    pub fn set_width(&self, cols: u32) -> DocView {
1025        let mut g = self.lock();
1026        g.width = Some((cols as usize).max(1));
1027        g.view()
1028    }
1029
1030    /// Switch to **unwrapped** layout — one visual row per block, no column wrapping —
1031    /// and repaint. A proportional GUI calls this once at start-up, then wraps each
1032    /// row at its own pixel width (the caret/hit/selection geometry it derives from
1033    /// the pixel wrap; core still owns the caret model, in byte offsets). Idempotent
1034    /// and cheap to leave in place across edits.
1035    pub fn set_unwrapped(&self) -> DocView {
1036        let mut g = self.lock();
1037        g.width = None;
1038        g.view()
1039    }
1040
1041    /// Tell core whether the host is in a dark appearance, so a `<picture>`'s
1042    /// `prefers-color-scheme` `<source>`s resolve to the right banner. Call it
1043    /// from `viewDidChangeEffectiveAppearance` (AppKit) or
1044    /// `traitCollectionDidChange` (UIKit).
1045    ///
1046    /// Cheap to call repeatedly: resolving at the same appearance yields the same
1047    /// URLs, and a renderer keying its views by `src` tears nothing down.
1048    pub fn set_dark_appearance(&self, dark: bool) -> DocView {
1049        let mut g = self.lock();
1050        g.scheme = if dark {
1051            ColorScheme::Dark
1052        } else {
1053            ColorScheme::Light
1054        };
1055        g.view()
1056    }
1057
1058    /// Report how many visual rows each block media actually needs, measured from
1059    /// the views the renderer laid out, keyed by the media's `src`.
1060    ///
1061    /// Core does no I/O and can't know how tall a picture or a player is, so this
1062    /// is the only way a placeholder grows past its default single row. The loop
1063    /// is: lay out at the current reservation → measure → call this → repaint if
1064    /// it changed. Handing over the same measurements again is a no-op, so a
1065    /// renderer can report its current state each frame without diffing first.
1066    ///
1067    /// A frontend that lays media out in its own units and simply reserves the
1068    /// vertical space itself (the way the gpui GUI does with images) never needs
1069    /// to call this at all.
1070    pub fn set_media_rows(&self, heights: Vec<MediaHeight>) -> DocView {
1071        let mut g = self.lock();
1072        g.doc.set_media_rows(
1073            heights
1074                .into_iter()
1075                .map(|h| (h.destination, h.rows.max(1) as usize))
1076                .collect(),
1077        );
1078        g.view()
1079    }
1080
1081    /// Insert a block-level image, video, or audio at the caret. Any selection
1082    /// becomes the alt / fallback text. See [`leaf_core::Doc::insert_media`] for
1083    /// the markup each kind spells.
1084    pub fn insert_media(&self, kind: MediaKind, destination: String, alt: String) -> DocView {
1085        let mut g = self.lock();
1086        let kind = match kind {
1087            MediaKind::Image => CoreMediaKind::Image,
1088            MediaKind::Video => CoreMediaKind::Video,
1089            MediaKind::Audio => CoreMediaKind::Audio,
1090        };
1091        g.doc.insert_media(kind, &destination, &alt);
1092        g.view()
1093    }
1094
1095    /// Insert a thematic break (`---`) at the caret — the toolbar's Horizontal
1096    /// Rule button. See [`leaf_core::Doc::insert_thematic_break`] for how it
1097    /// handles a selection, a blank line, and the caret sitting mid-paragraph,
1098    /// mid-list, or inside a quote.
1099    pub fn insert_thematic_break(&self) -> DocView {
1100        let mut g = self.lock();
1101        g.doc.insert_thematic_break();
1102        g.view()
1103    }
1104
1105    /// The current source text — for a save (write to disk / iCloud / a document
1106    /// wrapper) or a source-view display.
1107    pub fn source(&self) -> String {
1108        self.lock().doc.source.clone()
1109    }
1110
1111    /// The selected text, if any — for a clipboard copy/cut.
1112    pub fn selected_text(&self) -> Option<String> {
1113        self.lock().doc.selected_text().map(str::to_string)
1114    }
1115
1116    /// Mark the buffer saved after the host persisted [`LeafDoc::source`] its own
1117    /// way — clears the dirty flag without touching a filesystem.
1118    pub fn mark_saved(&self) -> DocView {
1119        let mut g = self.lock();
1120        g.doc.mark_saved();
1121        g.view()
1122    }
1123
1124    // ── text input ───────────────────────────────────────────────────────────
1125
1126    pub fn insert(&self, text: String) -> DocView {
1127        let mut g = self.lock();
1128        g.doc.insert(&text);
1129        g.view()
1130    }
1131
1132    pub fn paste(&self, text: String) -> DocView {
1133        let mut g = self.lock();
1134        g.doc.paste(&text);
1135        g.view()
1136    }
1137
1138    pub fn newline(&self) -> DocView {
1139        let mut g = self.lock();
1140        g.doc.newline();
1141        g.view()
1142    }
1143
1144    /// Tab away from a table: indent the caret's line (or the selected lines) one
1145    /// level, nesting a list item under its sibling. The frontend calls this when
1146    /// [`LeafDoc::cell_tab`] declined because the caret isn't in a table.
1147    pub fn indent(&self) -> DocView {
1148        let mut g = self.lock();
1149        g.doc.indent();
1150        g.view()
1151    }
1152
1153    /// Shift+Tab away from a table: take one indent level back off the caret's
1154    /// line (or the selected lines), unnesting a list item. The mirror of
1155    /// [`LeafDoc::indent`].
1156    pub fn outdent(&self) -> DocView {
1157        let mut g = self.lock();
1158        g.doc.outdent();
1159        g.view()
1160    }
1161
1162    // ── table keys ────────────────────────────────────────────────────────────
1163    // Tab, Return, and Shift+Return take on table meanings when the caret is in
1164    // one. Each returns `Some(view)` when it acted as a table key and `None` when
1165    // the caret isn't in a table — the frontend then does the key's ordinary job
1166    // (indent, newline), so these keep their meaning everywhere else.
1167
1168    /// Tab (`forward`) / Shift+Tab hops to the next/previous cell; Tab past the
1169    /// last cell appends a fresh row and enters it.
1170    pub fn cell_tab(&self, forward: bool) -> Option<DocView> {
1171        let mut g = self.lock();
1172        g.sync();
1173        g.doc.cell_tab(forward).then(|| g.view())
1174    }
1175
1176    /// Return drops to the cell below in the same column, appending a row at the
1177    /// table's bottom.
1178    pub fn cell_return(&self) -> Option<DocView> {
1179        let mut g = self.lock();
1180        g.sync();
1181        g.doc.cell_return().then(|| g.view())
1182    }
1183
1184    /// Shift+Return inserts a hard line break *within* the current cell.
1185    pub fn cell_line_break(&self) -> Option<DocView> {
1186        let mut g = self.lock();
1187        g.sync();
1188        g.doc.cell_line_break().then(|| g.view())
1189    }
1190
1191    pub fn backspace(&self) -> DocView {
1192        let mut g = self.lock();
1193        g.doc.backspace();
1194        g.view()
1195    }
1196
1197    pub fn delete_forward(&self) -> DocView {
1198        let mut g = self.lock();
1199        g.doc.delete_forward();
1200        g.view()
1201    }
1202
1203    pub fn delete_word_back(&self) -> DocView {
1204        let mut g = self.lock();
1205        g.doc.delete_word_back();
1206        g.view()
1207    }
1208
1209    pub fn delete_word_forward(&self) -> DocView {
1210        let mut g = self.lock();
1211        g.doc.delete_word_forward();
1212        g.view()
1213    }
1214
1215    // ── caret movement ───────────────────────────────────────────────────────
1216    // Each syncs the grid first (movement reads the stop table / column layout),
1217    // moves, then repaints — `Inner::view` re-syncs but that's the cached no-op.
1218
1219    pub fn move_left(&self, extend: bool) -> DocView {
1220        let mut g = self.lock();
1221        g.sync();
1222        g.doc.move_left(extend);
1223        g.view()
1224    }
1225
1226    pub fn move_right(&self, extend: bool) -> DocView {
1227        let mut g = self.lock();
1228        g.sync();
1229        g.doc.move_right(extend);
1230        g.view()
1231    }
1232
1233    pub fn move_up(&self, extend: bool) -> DocView {
1234        let mut g = self.lock();
1235        g.sync();
1236        g.doc.move_up(extend);
1237        g.view()
1238    }
1239
1240    pub fn move_down(&self, extend: bool) -> DocView {
1241        let mut g = self.lock();
1242        g.sync();
1243        g.doc.move_down(extend);
1244        g.view()
1245    }
1246
1247    pub fn move_word_left(&self, extend: bool) -> DocView {
1248        let mut g = self.lock();
1249        g.sync();
1250        g.doc.move_word_left(extend);
1251        g.view()
1252    }
1253
1254    pub fn move_word_right(&self, extend: bool) -> DocView {
1255        let mut g = self.lock();
1256        g.sync();
1257        g.doc.move_word_right(extend);
1258        g.view()
1259    }
1260
1261    pub fn move_home(&self, extend: bool) -> DocView {
1262        let mut g = self.lock();
1263        g.sync();
1264        g.doc.move_home(extend);
1265        g.view()
1266    }
1267
1268    pub fn move_end(&self, extend: bool) -> DocView {
1269        let mut g = self.lock();
1270        g.sync();
1271        g.doc.move_end(extend);
1272        g.view()
1273    }
1274
1275    pub fn move_doc_start(&self, extend: bool) -> DocView {
1276        let mut g = self.lock();
1277        g.sync();
1278        g.doc.move_doc_start(extend);
1279        g.view()
1280    }
1281
1282    pub fn move_doc_end(&self, extend: bool) -> DocView {
1283        let mut g = self.lock();
1284        g.sync();
1285        g.doc.move_doc_end(extend);
1286        g.view()
1287    }
1288
1289    pub fn select_all(&self) -> DocView {
1290        let mut g = self.lock();
1291        g.doc.select_all();
1292        g.view()
1293    }
1294
1295    /// Place the caret from a click, in core's column grid: `row` indexes the
1296    /// visual [`Row`]s and `col` is the glyph column within it. Core clamps both
1297    /// to real caret stops. Prefer [`LeafDoc::click_ch`] from a proportional
1298    /// renderer.
1299    pub fn click(&self, row: u32, col: u32, extend: bool) -> DocView {
1300        let mut g = self.lock();
1301        g.sync();
1302        g.doc.click(row as usize, col as usize, extend);
1303        g.view()
1304    }
1305
1306    /// Place the caret from a click whose horizontal position is a **UTF-16
1307    /// offset** into the visual row's text — what `characterIndex(for:)` hands
1308    /// back. Converted to core's display column before clicking, so a proportional
1309    /// renderer never reasons about column widths itself.
1310    pub fn click_ch(&self, row: u32, ch: u32, extend: bool) -> DocView {
1311        let mut g = self.lock();
1312        g.sync();
1313        let col = utf16_to_col(&g.row_text(row as usize), ch as usize);
1314        g.doc.click(row as usize, col, extend);
1315        g.view()
1316    }
1317
1318    /// Select the word under a click (row, `ch`) — the double-click gesture.
1319    pub fn select_word_ch(&self, row: u32, ch: u32) -> DocView {
1320        let mut g = self.lock();
1321        let off = g.offset_at(row as usize, ch as usize);
1322        g.doc.select_word_at(off);
1323        g.view()
1324    }
1325
1326    /// Select the whole logical text block under a click (row, `ch`) — the
1327    /// triple-click gesture. Grabs the entire block even where it soft-wraps.
1328    pub fn select_block_ch(&self, row: u32, ch: u32) -> DocView {
1329        let mut g = self.lock();
1330        let off = g.offset_at(row as usize, ch as usize);
1331        g.doc.select_block_at(off);
1332        g.view()
1333    }
1334
1335    /// Mirror a native selection into the model: `[anchor, focus]` given as
1336    /// row + UTF-16 offset pairs. Each is resolved to a source offset the way a
1337    /// click is, then set as the selection's fixed and moving ends. A collapsed
1338    /// range (`anchor == focus`) just places the caret.
1339    pub fn set_selection(
1340        &self,
1341        anchor_row: u32,
1342        anchor_ch: u32,
1343        focus_row: u32,
1344        focus_ch: u32,
1345    ) -> DocView {
1346        let mut g = self.lock();
1347        let anchor = g.offset_at(anchor_row as usize, anchor_ch as usize);
1348        let focus = g.offset_at(focus_row as usize, focus_ch as usize);
1349        g.doc.place_caret(anchor, false);
1350        if anchor != focus {
1351            g.doc.place_caret(focus, true);
1352        }
1353        g.view()
1354    }
1355
1356    // ── rich clipboard (mirrors leaf-tui / leaf-gpui / leaf-wasm) ─────────────
1357
1358    /// The current selection rendered to HTML by twig — the rich flavor a copy
1359    /// writes alongside the plain [`LeafDoc::selected_text`]. `None` when nothing
1360    /// is selected.
1361    pub fn selection_html(&self) -> Option<String> {
1362        self.lock().doc.selection_html()
1363    }
1364
1365    /// Paste, preferring the clipboard's rich (`text/html`) flavor: twig parses
1366    /// `html` into the document's own markup and inserts it. Falls back to the
1367    /// plain `text` when there's no HTML or it doesn't parse.
1368    pub fn paste_rich(&self, html: Option<String>, text: String) -> DocView {
1369        let mut g = self.lock();
1370        let took = html.as_deref().is_some_and(|h| g.doc.paste_html(h));
1371        if !took {
1372            g.doc.paste(&text);
1373        }
1374        g.view()
1375    }
1376
1377    // ── formatting commands (mirror leaf-gpui's EditorCommand) ────────────────
1378
1379    pub fn toggle_bold(&self) -> DocView {
1380        let mut g = self.lock();
1381        g.doc.toggle(InlineKind::Strong);
1382        g.view()
1383    }
1384
1385    pub fn toggle_italic(&self) -> DocView {
1386        let mut g = self.lock();
1387        g.doc.toggle(InlineKind::Emph);
1388        g.view()
1389    }
1390
1391    pub fn toggle_code(&self) -> DocView {
1392        let mut g = self.lock();
1393        g.doc.toggle(InlineKind::Verbatim);
1394        g.view()
1395    }
1396
1397    pub fn toggle_mark(&self) -> DocView {
1398        let mut g = self.lock();
1399        g.doc.toggle(InlineKind::Mark);
1400        g.view()
1401    }
1402
1403    pub fn toggle_underline(&self) -> DocView {
1404        let mut g = self.lock();
1405        g.doc.toggle(InlineKind::Insert);
1406        g.view()
1407    }
1408
1409    pub fn toggle_strike(&self) -> DocView {
1410        let mut g = self.lock();
1411        g.doc.toggle(InlineKind::Delete);
1412        g.view()
1413    }
1414
1415    pub fn set_paragraph(&self) -> DocView {
1416        let mut g = self.lock();
1417        g.doc.set_block(BlockKind::Paragraph);
1418        g.view()
1419    }
1420
1421    /// Toggle the current block to a heading of `level` (1–6); toggling the
1422    /// active level off returns it to a paragraph, per core.
1423    pub fn set_heading(&self, level: u32) -> DocView {
1424        let mut g = self.lock();
1425        g.doc.toggle_heading(level);
1426        g.view()
1427    }
1428
1429    pub fn toggle_blockquote(&self) -> DocView {
1430        let mut g = self.lock();
1431        g.doc.toggle_blockquote();
1432        g.view()
1433    }
1434
1435    pub fn toggle_list(&self, ordered: bool) -> DocView {
1436        let mut g = self.lock();
1437        g.doc.toggle_list(ordered);
1438        g.view()
1439    }
1440
1441    /// Tick or untick the task item at the caret. See
1442    /// [`leaf_core::Doc::toggle_task_checked`].
1443    pub fn toggle_task_checked(&self) -> DocView {
1444        let mut g = self.lock();
1445        g.doc.toggle_task_checked();
1446        g.view()
1447    }
1448
1449    /// Tick or untick the task item covering `offset` — a tap on a rendered
1450    /// checkbox, which must not drag the caret across the document to get there.
1451    pub fn toggle_task_at(&self, offset: u64) -> DocView {
1452        let mut g = self.lock();
1453        g.doc.toggle_task_at(offset as usize);
1454        g.view()
1455    }
1456
1457    /// Give the list item at the caret a checkbox, or take its checkbox away.
1458    pub fn toggle_task_item(&self) -> DocView {
1459        let mut g = self.lock();
1460        g.doc.toggle_task_item();
1461        g.view()
1462    }
1463
1464    /// Whether the item at the caret has a box and which way it faces — `None`
1465    /// for a plain list item or no item at all. Drives a toolbar's checked state.
1466    pub fn task_checked_at_caret(&self) -> Option<bool> {
1467        let mut g = self.lock();
1468        g.doc.task_checked_at_caret()
1469    }
1470
1471    /// Which of the formatting commands above this document's format can
1472    /// actually spell — one flag per control, for building the toolbar.
1473    ///
1474    /// Read once when a document opens: the answer depends only on the format,
1475    /// so it cannot change under an edit. Every command refuses on its own
1476    /// regardless — the model is the authority, not the toolbar — so a frontend
1477    /// that ignores this stays correct, it just offers buttons whose only effect
1478    /// is a line in the status bar.
1479    ///
1480    /// Don't collapse it to one flag. An HTML document takes ⌘B, ⌘I and inline
1481    /// code (its marks are a tag pair) while refusing every heading, list, quote
1482    /// and link, and Markdown refuses the highlight djot spells — so a toolbar
1483    /// driven by [`Self::authorable`] alone would be wrong in both directions.
1484    pub fn capabilities(&self) -> Capabilities {
1485        self.lock().doc.capabilities().into()
1486    }
1487
1488    /// Whether this document's format offers *any* door in — `false` only for a
1489    /// wholly parse-only one (XML), where an app may as well open the file
1490    /// read-only and hide the formatting section outright. For anything finer,
1491    /// including whether to dim an individual button, use [`Self::capabilities`].
1492    pub fn authorable(&self) -> bool {
1493        self.lock().doc.authorable()
1494    }
1495
1496    // ── table editing ─────────────────────────────────────────────────────────
1497
1498    /// Whether the caret is inside a table — for enabling the table controls.
1499    /// Pair it with [`Capabilities::table`]: the caret is genuinely inside an
1500    /// HTML `<table>`, and the grid controls still cannot edit one.
1501    pub fn caret_in_table(&self) -> bool {
1502        self.lock().doc.caret_in_table()
1503    }
1504
1505    /// Insert an empty row below (`below`) or above the caret's row.
1506    pub fn table_insert_row(&self, below: bool) -> DocView {
1507        let mut g = self.lock();
1508        g.doc.table_insert_row(below);
1509        g.view()
1510    }
1511
1512    /// Delete the caret's row (not the header or the last body row).
1513    pub fn table_delete_row(&self) -> DocView {
1514        let mut g = self.lock();
1515        g.doc.table_delete_row();
1516        g.view()
1517    }
1518
1519    /// Insert an empty column right (`right`) or left of the caret's column.
1520    pub fn table_insert_column(&self, right: bool) -> DocView {
1521        let mut g = self.lock();
1522        g.doc.table_insert_column(right);
1523        g.view()
1524    }
1525
1526    /// Delete the caret's column (unless it is the only one).
1527    pub fn table_delete_column(&self) -> DocView {
1528        let mut g = self.lock();
1529        g.doc.table_delete_column();
1530        g.view()
1531    }
1532
1533    /// Set the caret's column to `alignment`.
1534    pub fn table_set_alignment(&self, alignment: TableAlignment) -> DocView {
1535        let mut g = self.lock();
1536        g.doc.table_set_alignment(alignment.into_core());
1537        g.view()
1538    }
1539
1540    /// Move the caret's row one place down (`down`) or up.
1541    pub fn table_move_row(&self, down: bool) -> DocView {
1542        let mut g = self.lock();
1543        g.doc.table_move_row(down);
1544        g.view()
1545    }
1546
1547    /// Move the caret's column one place right (`right`) or left.
1548    pub fn table_move_column(&self, right: bool) -> DocView {
1549        let mut g = self.lock();
1550        g.doc.table_move_column(right);
1551        g.view()
1552    }
1553
1554    pub fn insert_link(&self, destination: String) -> DocView {
1555        let mut g = self.lock();
1556        g.doc.insert_link(&destination);
1557        g.view()
1558    }
1559
1560    /// The destination of the link under the caret, if the caret is inside one —
1561    /// so a frontend can open it (⌘-click / "Open Link") or show it. `None` when the
1562    /// caret isn't on a link.
1563    pub fn link_destination_at_caret(&self) -> Option<String> {
1564        self.lock().doc.link_destination_at_caret()
1565    }
1566
1567    /// The destination of the link at byte offset `off` —
1568    /// [`link_destination_at_caret`](Self::link_destination_at_caret) for a place
1569    /// the caret isn't.
1570    ///
1571    /// What a frontend drawing part of the document *outside* the document asks:
1572    /// a footnote's text in a popover has link runs in it, and this is how those
1573    /// runs learn where they point, since a `Run` carries how a span looks and
1574    /// not what it means.
1575    pub fn link_destination_at(&self, off: u32) -> Option<String> {
1576        self.lock().doc.link_destination_at(off as usize)
1577    }
1578
1579    /// Where the locator `id` lands in this document — the `#v2` half of a
1580    /// `chapter.dj#v2`, resolved to the block it names. `None` when nothing here
1581    /// answers to it, which is a host's cue to open the document at its top
1582    /// rather than refuse to go.
1583    ///
1584    /// The query that gives a link finer granularity than the file. It reads an
1585    /// explicit `{#v1}`, a djot heading's minted id, or (for Markdown, which
1586    /// mints none) a heading's own words slugged — see [`leaf_core::Doc::locate`].
1587    ///
1588    /// Asked of *any* document, not only the open one: a host peeking at a
1589    /// citation builds a [`LeafDoc`] over the other file's bytes and asks this,
1590    /// which is what lets a hover show the verse instead of the filename.
1591    pub fn locate(&self, id: String) -> Option<LandingView> {
1592        self.lock().doc.locate(&id).map(LandingView::from)
1593    }
1594
1595    /// Write a footnote at the caret — the toolbar's Footnote button. Both the
1596    /// `[^1]` and the definition it needs go in as one edit (one undo takes both
1597    /// back), the label is the lowest number the document has free, and the caret
1598    /// is left **in the empty note** ready to type it. Gate the button on
1599    /// [`Capabilities::footnote`]; see [`leaf_core::Doc::insert_footnote`].
1600    pub fn insert_footnote(&self) -> DocView {
1601        let mut g = self.lock();
1602        g.doc.insert_footnote();
1603        g.view()
1604    }
1605
1606    /// The footnote reference under the caret, resolved to the note it names —
1607    /// so a frontend can show the note when a reader activates a `[1]`, instead
1608    /// of the nothing a reference click used to do. `None` when the caret isn't
1609    /// on a reference; see [`FootnoteView`] for the reference that resolved to
1610    /// no definition.
1611    pub fn footnote_at_caret(&self) -> Option<FootnoteView> {
1612        self.lock().doc.footnote_at_caret().map(FootnoteView::from)
1613    }
1614
1615    /// The footnote reference at byte offset `off`, resolved to the note it
1616    /// names — [`footnote_at_caret`](Self::footnote_at_caret) for a place the
1617    /// caret isn't.
1618    ///
1619    /// This is what a hover asks: a pointer resting on a `[1]` wants the note's
1620    /// text in a popover, and moving the caret to find out would yank the reader
1621    /// out of wherever they were typing.
1622    pub fn footnote_at(&self, off: u32) -> Option<FootnoteView> {
1623        self.lock()
1624            .doc
1625            .footnote_at(off as usize)
1626            .map(FootnoteView::from)
1627    }
1628
1629    /// The footnote definition the caret stands in, and where the reference that
1630    /// names it is — the return leg of [`footnote_at_caret`](Self::footnote_at_caret),
1631    /// so following a footnote is a round trip rather than a fall.
1632    ///
1633    /// `None` when the caret isn't in a definition, which is also how a frontend
1634    /// tells the two directions apart: the reference query answers up top, this
1635    /// one answers down in the notes, and never both at once.
1636    pub fn footnote_definition_at_caret(&self) -> Option<FootnoteDefView> {
1637        self.lock()
1638            .doc
1639            .footnote_definition_at_caret()
1640            .map(FootnoteDefView::from)
1641    }
1642
1643    pub fn undo(&self) -> DocView {
1644        let mut g = self.lock();
1645        g.doc.undo();
1646        g.view()
1647    }
1648
1649    pub fn redo(&self) -> DocView {
1650        let mut g = self.lock();
1651        g.doc.redo();
1652        g.view()
1653    }
1654
1655    /// Switch between the rendered WYSIWYG surface and the raw source.
1656    pub fn toggle_view(&self) -> DocView {
1657        let mut g = self.lock();
1658        g.doc.toggle_view();
1659        g.view()
1660    }
1661
1662    /// The current markup-exposure preference (see [`MarkupMode`]).
1663    pub fn markup_mode(&self) -> MarkupMode {
1664        MarkupMode::from_core(self.lock().doc.markup_mode())
1665    }
1666
1667    /// Set the markup-exposure preference. Returns a fresh view so a frontend
1668    /// can repaint — and under `Full` it must, because the returned view is the
1669    /// first one showing the caret's line raw. Diaryx leaves it at the `None`
1670    /// default.
1671    pub fn set_markup_mode(&self, mode: MarkupMode) -> DocView {
1672        let mut g = self.lock();
1673        g.doc.set_markup_mode(mode.into_core());
1674        g.view()
1675    }
1676
1677    /// The current soft-break flow preference (see [`LineFlow`]).
1678    pub fn line_flow(&self) -> LineFlow {
1679        LineFlow::from_core(self.lock().doc.line_flow())
1680    }
1681
1682    /// Set the soft-break flow preference. Returns a fresh view so a frontend
1683    /// can repaint: like the markup-exposure preference this one changes rendering
1684    /// immediately, laying preserved soft breaks out as their own rows.
1685    pub fn set_line_flow(&self, mode: LineFlow) -> DocView {
1686        let mut g = self.lock();
1687        g.doc.set_line_flow(mode.into_core());
1688        g.view()
1689    }
1690}
1691
1692// ── UITextInput support ──────────────────────────────────────────────────────
1693// A `UITextPosition` on the Swift side wraps a source byte offset; these are the
1694// offset↔geometry, stepping, and range-editing primitives the protocol needs.
1695// Queries never move the caret — they only read the (synced) visual map — so the
1696// system can probe positions freely while the model's selection stays put.
1697#[uniffi::export]
1698impl LeafDoc {
1699    /// The caret's source offset (the selection's moving end).
1700    pub fn caret_offset(&self) -> u32 {
1701        self.lock().doc.caret as u32
1702    }
1703
1704    /// The selection's fixed end (equals the caret when there's no selection).
1705    pub fn anchor_offset(&self) -> u32 {
1706        let g = self.lock();
1707        g.doc.anchor.unwrap_or(g.doc.caret) as u32
1708    }
1709
1710    /// The last caret stop in the document — `UITextInput.endOfDocument`.
1711    pub fn doc_end_offset(&self) -> u32 {
1712        let mut g = self.lock();
1713        g.sync();
1714        let end = g.doc.source.len();
1715        g.snap_stop(end) as u32
1716    }
1717
1718    /// Snap an arbitrary offset to the nearest valid caret stop.
1719    pub fn snap_offset(&self, off: u32) -> u32 {
1720        let mut g = self.lock();
1721        g.sync();
1722        g.snap_stop(off as usize) as u32
1723    }
1724
1725    /// Where a source offset sits on screen: its visual `(row, ch)`.
1726    pub fn pos_for_offset(&self, off: u32) -> RowCol {
1727        let mut g = self.lock();
1728        g.sync();
1729        let (row, col) = g.pos_of_offset(off as usize);
1730        let ch = col_to_utf16(&g.row_text(row), col);
1731        RowCol {
1732            row: row as u32,
1733            ch: ch as u32,
1734        }
1735    }
1736
1737    /// The rows a source range covers, inclusive — for drawing a block away
1738    /// from where it sits (a footnote peek, a link peek, a landing flash).
1739    ///
1740    /// Ask this rather than mapping `start` and `end - 1` through
1741    /// [`Self::pos_for_offset`]. That pair reads correctly and is wrong: a
1742    /// block's last byte is often *hidden* — a note or a paragraph ending in a
1743    /// link ends inside the link's destination — and `pos_for_offset` snaps a
1744    /// hidden offset forward to the next visible glyph, which for a trailing
1745    /// one is on the next block's row. A peek slicing that span drew the block
1746    /// after it too. `pos_for_offset`'s snap is right for a caret and wrong for
1747    /// a span; this is the question spans should be asking.
1748    pub fn row_range_for(&self, start: u32, end: u32) -> RowRange {
1749        let mut g = self.lock();
1750        g.sync();
1751        let (first, last) = g.row_range_for(start as usize, end as usize);
1752        RowRange {
1753            first: first as u32,
1754            last: last as u32,
1755        }
1756    }
1757
1758    /// The source offset at visual `(row, ch)` — the inverse of
1759    /// [`Self::pos_for_offset`], for hit-testing a point to a position.
1760    pub fn offset_for_pos(&self, row: u32, ch: u32) -> u32 {
1761        let mut g = self.lock();
1762        g.sync();
1763        let col = utf16_to_col(&g.row_text(row as usize), ch as usize);
1764        g.offset_of_col(row as usize, col) as u32
1765    }
1766
1767    /// Move `off` by `delta` caret stops (negative = left) — `position(from:offset:)`.
1768    pub fn step_offset(&self, off: u32, delta: i32) -> u32 {
1769        let mut g = self.lock();
1770        g.sync();
1771        let mut o = g.snap_stop(off as usize);
1772        if delta >= 0 {
1773            for _ in 0..delta {
1774                match g.stop_after(o) {
1775                    Some(n) => o = n,
1776                    None => break,
1777                }
1778            }
1779        } else {
1780            for _ in 0..(-delta) {
1781                match g.stop_before(o) {
1782                    Some(p) => o = p,
1783                    None => break,
1784                }
1785            }
1786        }
1787        o as u32
1788    }
1789
1790    /// The count of caret stops between two offsets (signed) — `offset(from:to:)`.
1791    pub fn distance_offset(&self, from: u32, to: u32) -> i32 {
1792        let mut g = self.lock();
1793        g.sync();
1794        let (from, to) = (from as usize, to as usize);
1795        let (mut a, b, sign) = if from <= to {
1796            (from, to, 1i32)
1797        } else {
1798            (to, from, -1i32)
1799        };
1800        a = g.snap_stop(a);
1801        let mut n = 0i32;
1802        while a < b {
1803            match g.stop_after(a) {
1804                Some(x) => {
1805                    a = x;
1806                    n += 1;
1807                }
1808                None => break,
1809            }
1810        }
1811        n * sign
1812    }
1813
1814    /// The offset one navigable row up/down from `off`, keeping its column —
1815    /// `position(from:in: .up/.down)`. `None` at the top/bottom edge.
1816    pub fn vertical_offset(&self, off: u32, down: bool) -> Option<u32> {
1817        let mut g = self.lock();
1818        g.sync();
1819        let (row, col) = g.pos_of_offset(off as usize);
1820        let target = if down {
1821            g.nav_below(row)
1822        } else {
1823            g.nav_above(row)
1824        };
1825        target.map(|r| g.offset_of_col(r, col) as u32)
1826    }
1827
1828    /// The visible text between two offsets — `text(in:)`. In the WYSIWYG
1829    /// view this is *not* the raw source slice: a hidden inline-mark
1830    /// delimiter (`**`, `` ` ``, `_`) contributes nothing, matching what
1831    /// `distance_offset`/`step_offset` already count in this same offset
1832    /// space — while a genuine block boundary the range spans (a paragraph
1833    /// gap, a table rule, …) contributes one inserted `'\n'` that
1834    /// `distance_offset`/`step_offset` do *not* count (a block boundary costs
1835    /// caret motion zero stops there, by design — see
1836    /// `the_caret_skips_the_gap_between_two_paragraphs` in `leaf-core`'s
1837    /// `doc.rs`). So the relationship is
1838    /// `text_in_range(a, b).chars().count() >= distance_offset(a, b)`, not
1839    /// strict equality: the two agree exactly when `(a, b)` spans no block
1840    /// boundary, and `text_in_range` is never shorter, only ever as long or
1841    /// longer, when it does. That inequality is still what
1842    /// `UITextInput`'s own word/line tokenizer needs (see
1843    /// [`leaf_core::wysiwyg::VisualMap::visible_text`] for why): it only reads
1844    /// this string to find a boundary and converts the result back to a
1845    /// position via `position(from:offset:)`, which walks stops — the
1846    /// inserted character is never hit as one, it only keeps the tokenizer
1847    /// from reading two paragraphs' last/first words as a single run of
1848    /// letters. The source view has nothing hidden to begin with, so there
1849    /// this is still exactly the raw slice.
1850    pub fn text_in_range(&self, from: u32, to: u32) -> String {
1851        let mut g = self.lock();
1852        g.sync();
1853        let len = g.doc.source.len();
1854        let (mut a, mut b) = ((from as usize).min(len), (to as usize).min(len));
1855        if a > b {
1856            std::mem::swap(&mut a, &mut b);
1857        }
1858        match g.doc.view {
1859            View::Wysiwyg => g.doc.vmap.visible_text(a, b),
1860            View::Source => {
1861                let s = &g.doc.source;
1862                while a > 0 && !s.is_char_boundary(a) {
1863                    a -= 1;
1864                }
1865                while b < s.len() && !s.is_char_boundary(b) {
1866                    b += 1;
1867                }
1868                s[a..b].to_string()
1869            }
1870        }
1871    }
1872
1873    /// Set the selection to `[anchor, focus]` by source offsets — the setter behind
1874    /// `UITextInput.selectedTextRange` and handle dragging.
1875    pub fn set_selection_offsets(&self, anchor: u32, focus: u32) -> DocView {
1876        let mut g = self.lock();
1877        g.doc.place_caret(anchor as usize, false);
1878        if focus != anchor {
1879            g.doc.place_caret(focus as usize, true);
1880        }
1881        g.view()
1882    }
1883
1884    /// Replace the source range `[from, to]` with `text` — `replace(_:withText:)`.
1885    pub fn replace_range(&self, from: u32, to: u32, text: String) -> DocView {
1886        let mut g = self.lock();
1887        g.doc.place_caret(from as usize, false);
1888        if to != from {
1889            g.doc.place_caret(to as usize, true);
1890        }
1891        g.doc.insert(&text);
1892        g.view()
1893    }
1894}
1895
1896impl LeafDoc {
1897    /// Acquire the guard, recovering from a poisoned lock: a panic in `leaf-core`
1898    /// under one call shouldn't wedge the whole document handle for the app.
1899    fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
1900        self.inner.lock().unwrap_or_else(|p| p.into_inner())
1901    }
1902}
1903
1904/// The UTF-16 offset into `text` of display column `col`. Walks grapheme clusters
1905/// exactly as core measures columns ([`text_width`] per cluster), so a wide
1906/// cluster advances the column by its cells while the offset advances by its
1907/// UTF-16 length; the two coincide only on plain ASCII.
1908fn col_to_utf16(text: &str, col: usize) -> usize {
1909    let mut c = 0usize;
1910    let mut u = 0usize;
1911    for g in text.graphemes(true) {
1912        if c >= col {
1913            break;
1914        }
1915        c += text_width(g);
1916        u += g.chars().map(char::len_utf16).sum::<usize>();
1917    }
1918    u
1919}
1920
1921/// The display column of the grapheme boundary at or before UTF-16 offset `off`
1922/// — the inverse of [`col_to_utf16`], turning a native click position back into
1923/// core's column. Core then clamps the column to a real caret stop.
1924fn utf16_to_col(text: &str, off: usize) -> usize {
1925    let mut c = 0usize;
1926    let mut u = 0usize;
1927    for g in text.graphemes(true) {
1928        if u >= off {
1929            break;
1930        }
1931        u += g.chars().map(char::len_utf16).sum::<usize>();
1932        c += text_width(g);
1933    }
1934    c
1935}
1936
1937/// The renderer class id for a semantic role. Heading level is folded into the
1938/// id (`h1`…`h6`) so a single style rule per level applies.
1939fn role_name(r: Role) -> String {
1940    match r {
1941        Role::Body => "body".into(),
1942        Role::Heading(level) => format!("h{}", level.clamp(1, 6)),
1943        Role::Code => "code".into(),
1944        Role::Link => "link".into(),
1945        Role::Mark => "mark".into(),
1946        Role::ListMarker => "list".into(),
1947        Role::QuoteGutter => "quote".into(),
1948        Role::Rule => "rule".into(),
1949        Role::Image => "image".into(),
1950        Role::Delimiter => "delimiter".into(),
1951    }
1952}
1953
1954/// The toolbar id for an inline mark — kept in sync with the Swift button ids.
1955fn mark_id(kind: InlineKind) -> &'static str {
1956    match kind {
1957        InlineKind::Strong => "bold",
1958        InlineKind::Emph => "italic",
1959        InlineKind::Verbatim => "code",
1960        InlineKind::Mark => "mark",
1961        InlineKind::Insert => "underline",
1962        InlineKind::Delete => "strike",
1963        InlineKind::Superscript => "superscript",
1964        InlineKind::Subscript => "subscript",
1965    }
1966}
1967
1968/// The WYSIWYG rows: each visual row's glyphs coalesced into maximal runs of
1969/// identical `(style, selected)`. A glyph is selected when its source byte lies
1970/// in `[ss, se)`.
1971fn wysiwyg_rows(vmap: &VisualMap, ss: usize, se: usize) -> Vec<Row> {
1972    vmap.rows
1973        .iter()
1974        .map(|vrow| {
1975            Row {
1976                runs: runs_of(&vrow.glyphs, ss, se),
1977                decoration: vrow.decoration,
1978                code: vrow.code,
1979                code_lang: vrow.code_lang.clone(),
1980                directive: vrow.directive,
1981                directive_label: vrow.directive_label.clone(),
1982                // Straight off the row, not scanned out of its glyphs: an empty
1983                // heading has none to scan, and a renderer sizing the line by a
1984                // glyph's role drew `# ` at body height until it had text.
1985                heading: vrow.heading,
1986                boundary: vrow.boundary.map(|b| Boundary {
1987                    above: b.above.into(),
1988                    below: b.below.into(),
1989                }),
1990            }
1991        })
1992        .collect()
1993}
1994
1995/// Coalesce `glyphs` into maximal runs of identical `(style, selected)` — the
1996/// shared body of a row's runs and a table cell's runs. A glyph is selected when
1997/// its source byte lies in `[ss, se)`.
1998/// Split a cell's flat glyphs into its visual lines at the in-cell break glyphs
1999/// (`\n`, from a `<br>`), each with the source range it spans. A line runs from
2000/// its first glyph's offset to the break that ends it (`cell_end` for the last);
2001/// an empty line — a leading/trailing break, or an empty cell — collapses to a
2002/// single caret home. The break glyphs themselves are dropped (they hold no
2003/// caret), exactly as the monospace picture drops them.
2004fn cell_lines(
2005    glyphs: &[leaf_core::Glyph],
2006    cell_start: usize,
2007    cell_end: usize,
2008    ss: usize,
2009    se: usize,
2010) -> Vec<TableCellLineView> {
2011    let mut lines = Vec::new();
2012    let mut seg: Vec<leaf_core::Glyph> = Vec::new();
2013    // The current line's start offset: the cell's for the first line, then the
2014    // first real glyph after each break (`None` until that glyph is seen).
2015    let mut line_start: Option<usize> = Some(cell_start);
2016    for g in glyphs {
2017        if g.ch == '\n' {
2018            let start = line_start.unwrap_or(g.src);
2019            lines.push(TableCellLineView {
2020                runs: runs_of(&seg, ss, se),
2021                start: start as u32,
2022                end: g.src as u32,
2023            });
2024            seg.clear();
2025            line_start = None;
2026        } else {
2027            if line_start.is_none() {
2028                line_start = Some(g.src);
2029            }
2030            seg.push(g.clone());
2031        }
2032    }
2033    lines.push(TableCellLineView {
2034        runs: runs_of(&seg, ss, se),
2035        start: line_start.unwrap_or(cell_end) as u32,
2036        end: cell_end as u32,
2037    });
2038    lines
2039}
2040
2041fn runs_of(glyphs: &[leaf_core::Glyph], ss: usize, se: usize) -> Vec<Run> {
2042    let mut runs: Vec<Run> = Vec::new();
2043    let mut buf = String::new();
2044    // The style/selection key the run is accumulating, and the source offset its
2045    // first glyph came from — carried alongside rather than re-derived, since a
2046    // run's glyphs are contiguous but its *text* has no offsets in it.
2047    let mut cur: Option<(LStyle, bool, usize)> = None;
2048    for g in glyphs {
2049        let key = (g.style, g.src >= ss && g.src < se);
2050        match cur {
2051            Some((style, sel, _)) if (style, sel) == key => buf.push(g.ch),
2052            _ => {
2053                if let Some((style, was_sel, src)) = cur.take() {
2054                    runs.push(make_run(std::mem::take(&mut buf), style, was_sel, src));
2055                }
2056                cur = Some((key.0, key.1, g.src));
2057                buf.push(g.ch);
2058            }
2059        }
2060    }
2061    if let Some((style, was_sel, src)) = cur {
2062        runs.push(make_run(buf, style, was_sel, src));
2063    }
2064    runs
2065}
2066
2067/// The leaf directives of a WYSIWYG frame — each with the `rows` span its
2068/// placeholder occupies (to be painted over) and the name/attributes a frontend
2069/// resolves it by. The peer of [`wysiwyg_tables`] for a block that renders as a
2070/// thing rather than as text.
2071fn wysiwyg_directives(vmap: &VisualMap) -> Vec<DirectiveView> {
2072    vmap.directives
2073        .iter()
2074        .map(|d| DirectiveView {
2075            start_row: d.rows_span.start as u32,
2076            end_row: d.rows_span.end as u32,
2077            name: d.name.clone(),
2078            label: d.label.clone(),
2079            attrs: d
2080                .attrs
2081                .iter()
2082                .map(|(k, v)| DirectiveAttr {
2083                    key: k.clone(),
2084                    value: v.clone().unwrap_or_default(),
2085                })
2086                .collect(),
2087        })
2088        .collect()
2089}
2090
2091/// The block media of a WYSIWYG frame — each with the `rows` span its
2092/// placeholder occupies (to be laid over) and what to build there. The peer of
2093/// [`wysiwyg_directives`], with each URL already resolved under `scheme`.
2094///
2095/// Resolving here rather than in Swift keeps the one piece of `<picture>` logic
2096/// core owns (`prefers-color-scheme` matching) in core. The `<source>` list
2097/// still crosses untouched, so a renderer can additionally pick by MIME — which
2098/// codecs AVFoundation has is not something core can know.
2099fn wysiwyg_media(vmap: &VisualMap, scheme: ColorScheme) -> Vec<MediaView> {
2100    vmap.media
2101        .iter()
2102        .map(|m| MediaView {
2103            start_row: m.rows_span.start as u32,
2104            end_row: m.rows_span.end as u32,
2105            kind: match m.kind {
2106                CoreMediaKind::Image => MediaKind::Image,
2107                CoreMediaKind::Video => MediaKind::Video,
2108                CoreMediaKind::Audio => MediaKind::Audio,
2109            },
2110            src: m.resolve(scheme).to_string(),
2111            poster: m.poster.clone(),
2112            alt: m.alt.clone(),
2113            sources: m
2114                .sources
2115                .iter()
2116                .map(|s| MediaSourceView {
2117                    media: s.media.clone(),
2118                    src: s.srcset.clone(),
2119                    mime: s.mime.clone(),
2120                })
2121                .collect(),
2122        })
2123        .collect()
2124}
2125
2126/// The structural tables of a WYSIWYG frame — each with the `rows` span its
2127/// box-glyph picture occupies (to be skipped) and its grid of styled cells.
2128fn wysiwyg_tables(vmap: &VisualMap, ss: usize, se: usize) -> Vec<TableView> {
2129    vmap.tables
2130        .iter()
2131        .map(|t| TableView {
2132            start_row: t.rows_span.start as u32,
2133            end_row: t.rows_span.end as u32,
2134            grid: t
2135                .grid
2136                .iter()
2137                .map(|row| TableRowView {
2138                    head: row.head,
2139                    cells: row
2140                        .cells
2141                        .iter()
2142                        .map(|cell| TableCellView {
2143                            lines: cell_lines(&cell.glyphs, cell.start, cell.end, ss, se),
2144                            align: align_name(cell.align),
2145                            start: cell.start as u32,
2146                            end: cell.end as u32,
2147                        })
2148                        .collect(),
2149                })
2150                .collect(),
2151        })
2152        .collect()
2153}
2154
2155/// The wire name for a cell's column alignment.
2156fn align_name(a: Alignment) -> String {
2157    match a {
2158        Alignment::Left => "left",
2159        Alignment::Right => "right",
2160        Alignment::Center => "center",
2161        Alignment::Default => "default",
2162    }
2163    .to_string()
2164}
2165
2166/// The source rows: the raw document split on `'\n'`, every line plain body text
2167/// with the `[ss, se)` selection carved out as its own run. Backs the source
2168/// view, whose caret rides raw byte offsets.
2169fn source_rows(source: &str, ss: usize, se: usize) -> Vec<Row> {
2170    let body = LStyle::default();
2171    let mut rows = Vec::new();
2172    let mut byte = 0usize;
2173
2174    for raw in source.split('\n') {
2175        let start = byte;
2176        let end = start + raw.len();
2177        // Selection overlap with this line, in line-local byte coordinates.
2178        let a = ss.clamp(start, end) - start;
2179        let b = se.clamp(start, end) - start;
2180
2181        // The source view's rows are split from raw text, so a run's offset is
2182        // simply where its slice starts — no glyphs to read one off.
2183        let mut runs = Vec::new();
2184        if a < b {
2185            if a > 0 {
2186                runs.push(make_run(raw[..a].to_string(), body, false, start));
2187            }
2188            runs.push(make_run(raw[a..b].to_string(), body, true, start + a));
2189            if b < raw.len() {
2190                runs.push(make_run(raw[b..].to_string(), body, false, start + b));
2191            }
2192        } else if !raw.is_empty() {
2193            runs.push(make_run(raw.to_string(), body, false, start));
2194        }
2195
2196        rows.push(Row {
2197            runs,
2198            decoration: false,
2199            code: false,
2200            code_lang: None,
2201            directive: false,
2202            directive_label: None,
2203            heading: None,  // source view is raw text — no resolved heading rows
2204            boundary: None, // …and no resolved block structure to divide
2205        });
2206        byte = end + 1; // skip the '\n' that `split` consumed
2207    }
2208    rows
2209}
2210
2211/// Build a [`Run`] from an accumulated string and the core style it was drawn
2212/// with — the one place role and emphasis flags cross into the view shape.
2213fn make_run(text: String, style: LStyle, sel: bool, src: usize) -> Run {
2214    Run {
2215        text,
2216        role: role_name(style.role),
2217        bold: style.bold,
2218        italic: style.italic,
2219        underline: style.underline,
2220        strike: style.strikethrough,
2221        sup: style.baseline == Baseline::Super,
2222        sub: style.baseline == Baseline::Sub,
2223        src: src as u32,
2224        sel,
2225    }
2226}
2227
2228#[cfg(test)]
2229mod tests {
2230    use super::*;
2231
2232    fn doc(src: &str) -> Arc<LeafDoc> {
2233        LeafDoc::new(src.to_string(), "markdown".to_string()).unwrap()
2234    }
2235
2236    #[test]
2237    fn a_footnote_definition_ending_the_file_is_itself_not_a_copy() {
2238        // No trailing newline: twig closes the last block on the virtual newline
2239        // it supplies at EOF, so the block's `span.end` is one past the source.
2240        // The definition and the `section` whose bytes contain it then both
2241        // overran, both keyed the block cache as *empty*, and the definition was
2242        // served the section's rows — this rendered the heading a second time.
2243        let src = "A claim[^1] worth checking.\n\n# A heading with a reference[^1] in it\n\n[^1]: The first note.\n[^note]: A note with a word for a label.";
2244        let d = LeafDoc::new(src.to_string(), "djot".to_string()).unwrap();
2245        let text: Vec<String> = d
2246            .view()
2247            .rows
2248            .iter()
2249            .map(|r| r.runs.iter().map(|x| x.text.as_str()).collect())
2250            .collect();
2251        assert_eq!(
2252            text.last().map(String::as_str),
2253            Some("[note] A note with a word for a label."),
2254            "the last definition should render itself: {text:?}"
2255        );
2256        assert_eq!(
2257            text.iter()
2258                .filter(|t| t.contains("A heading with a reference"))
2259                .count(),
2260            1,
2261            "the heading should render exactly once: {text:?}"
2262        );
2263    }
2264
2265    #[test]
2266    fn an_empty_heading_crosses_the_boundary_carrying_its_level() {
2267        // What the toolbar's H1 leaves on a blank line: a heading with no text
2268        // yet. The renderer sizes a row by this field, so a `nil` here is a line
2269        // (and a caret) drawn at body height that jumps to heading height on the
2270        // first keystroke — the level can't be scanned out of the runs, because
2271        // an empty heading has none.
2272        let d = doc("body\n\n# \n");
2273        let v = d.view();
2274        let head = v.rows.last().expect("the heading's row");
2275        assert!(
2276            head.runs.iter().all(|r| r.text.is_empty()),
2277            "the `# ` marker is hidden"
2278        );
2279        assert_eq!(head.heading, Some(1));
2280        assert_eq!(
2281            v.rows[0].heading, None,
2282            "the paragraph above is not a heading"
2283        );
2284    }
2285
2286    #[test]
2287    fn typing_into_a_heading_made_on_a_blank_line_keeps_the_caret_on_its_row() {
2288        // The reported bug at the boundary the Swift renderer reads: with a blank
2289        // line under it, the caret came back on a row two below the heading it
2290        // was actually in, and the view drew it there.
2291        let d = doc("one\n\ntwo\n\n\n\n");
2292        let _ = d.click(4, 0, false); // the first of the two blank lines
2293        let _ = d.set_heading(1);
2294        let mut v = d.view();
2295        for c in "title".chars() {
2296            v = d.insert(c.to_string());
2297        }
2298        assert_eq!(d.source(), "one\n\ntwo\n\n# title\n\n");
2299        assert_eq!(
2300            (v.caret_row, v.caret_ch),
2301            (4, 5),
2302            "the caret is on the heading's row"
2303        );
2304        assert_eq!(v.rows[4].heading, Some(1));
2305    }
2306
2307    #[test]
2308    fn a_video_crosses_the_boundary_as_media_with_the_rows_to_lay_it_over() {
2309        // What the Swift renderer actually consumes: a row span to cover, a kind
2310        // to build a view from, and a URL to load. A frontend that skipped the
2311        // span would paint core's `🎬` placeholder underneath its own player.
2312        let d = doc("<video src=\"clip.mp4\" poster=\"still.png\" controls></video>\n");
2313        let v = d.view();
2314        assert_eq!(v.media.len(), 1);
2315        let m = &v.media[0];
2316        assert!(matches!(m.kind, MediaKind::Video));
2317        assert_eq!(m.src, "clip.mp4");
2318        assert_eq!(m.poster, "still.png");
2319        assert!(
2320            m.end_row > m.start_row,
2321            "the span must cover at least its label row"
2322        );
2323    }
2324
2325    #[test]
2326    fn a_pictures_dark_source_resolves_by_appearance() {
2327        // The one piece of `<picture>` logic core owns, exercised across the
2328        // boundary: the same document resolves to a different URL depending on
2329        // what the host said its appearance was.
2330        let d = doc(
2331            "<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"d.svg\">\
2332             <img src=\"l.svg\" alt=\"banner\"></picture>\n",
2333        );
2334        assert_eq!(d.view().media[0].src, "l.svg", "light by default");
2335        assert_eq!(d.set_dark_appearance(true).media[0].src, "d.svg");
2336        assert_eq!(d.set_dark_appearance(false).media[0].src, "l.svg");
2337    }
2338
2339    #[test]
2340    fn tapping_below_a_trailing_picture_and_typing_keeps_it_a_picture() {
2341        // The whole gesture, across the boundary, in the order the Apple frontend
2342        // performs it: the layout clamps a point below the last row onto the
2343        // picture's row and asks for the position past its label glyphs; that
2344        // offset becomes the selection; then a character arrives. Before the two
2345        // halves of this fix, the offset was the stop *in front of* the picture
2346        // and the character dissolved it into a paragraph with an inline image —
2347        // the photo stopped being drawn, and nothing said so.
2348        let d = doc("hi\n\n![](p.png)\n");
2349        let v = d.set_unwrapped();
2350        let row = v.media[0].start_row;
2351        let label: u32 = v.rows[row as usize]
2352            .runs
2353            .iter()
2354            .map(|r| r.text.encode_utf16().count() as u32)
2355            .sum();
2356
2357        let off = d.offset_for_pos(row, label);
2358        assert_eq!(
2359            off,
2360            "hi\n\n![](p.png)".len() as u32,
2361            "the stop past the picture"
2362        );
2363
2364        d.set_selection_offsets(off, off);
2365        let after = d.insert("x".to_string());
2366        assert_eq!(d.source(), "hi\n\n![](p.png)\n\nx\n");
2367        assert_eq!(after.media.len(), 1, "still a picture, one paragraph up");
2368    }
2369
2370    #[test]
2371    fn backspace_from_that_same_tap_takes_the_picture_whole() {
2372        // The other half of the same gesture, and the one that cost this project's
2373        // own test vault a photo: tap under the picture, press Backspace. That
2374        // offset is the stop past the markup, so a byte-step deleted the closing
2375        // paren and left the literal text `![](p.png` where a photo had been.
2376        let d = doc("hi\n\n![](p.png)\n");
2377        d.set_unwrapped();
2378        let off = "hi\n\n![](p.png)".len() as u32;
2379        d.set_selection_offsets(off, off);
2380        let after = d.backspace();
2381        assert_eq!(d.source(), "hi\n");
2382        assert_eq!(after.media.len(), 0, "gone as a picture, not as bytes");
2383        let undone = d.undo();
2384        assert_eq!(d.source(), "hi\n\n![](p.png)\n");
2385        assert_eq!(
2386            undone.media.len(),
2387            1,
2388            "and one undo brings the picture back"
2389        );
2390    }
2391
2392    #[test]
2393    fn measured_heights_grow_the_reserved_span() {
2394        // The height loop: core reserves one row until the renderer measures the
2395        // real view and reports back, because core does no I/O and cannot know.
2396        let d = doc("![a cat](cat.png)\n");
2397        let before = &d.view().media[0];
2398        assert_eq!(
2399            before.end_row - before.start_row,
2400            1,
2401            "one row until measured"
2402        );
2403
2404        let after = d.set_media_rows(vec![MediaHeight {
2405            destination: "cat.png".to_string(),
2406            rows: 6,
2407        }]);
2408        let m = &after.media[0];
2409        assert_eq!(
2410            m.end_row - m.start_row,
2411            6,
2412            "the span grew to what was measured"
2413        );
2414    }
2415
2416    #[test]
2417    fn inserted_media_comes_straight_back_out_as_media() {
2418        // Round trip across the boundary, the pair that matters: what Swift asks
2419        // to insert, Swift sees on the very next frame.
2420        let d = doc("\n");
2421        let v = d.insert_media(
2422            MediaKind::Audio,
2423            "take.mp3".to_string(),
2424            "a take".to_string(),
2425        );
2426        assert_eq!(v.media.len(), 1);
2427        assert!(matches!(v.media[0].kind, MediaKind::Audio));
2428        assert_eq!(v.media[0].src, "take.mp3");
2429        assert_eq!(v.media[0].alt, "a take");
2430    }
2431
2432    #[test]
2433    fn the_source_view_publishes_no_media() {
2434        // In the source view the `<video>` markup is the literal text the caret
2435        // is editing — laying a player over it would cover what's being typed.
2436        let d = doc("<video src=\"clip.mp4\" controls></video>\n");
2437        assert_eq!(d.view().media.len(), 1);
2438        assert!(
2439            d.toggle_view().media.is_empty(),
2440            "no placeholders in the source view"
2441        );
2442    }
2443
2444    /// **A foreign caller's offset must never panic.** Every offset entering
2445    /// leaf comes from a UI toolkit that counts in its own units — UIKit hands
2446    /// back UTF-16 positions — so an offset landing mid-character is a normal
2447    /// thing to be handed, not a bug in the caller. Slicing on it aborts the
2448    /// process across the FFI boundary, where there is no unwinding to catch.
2449    ///
2450    /// Reproduces a real crash: `byte index 1236 is not a char boundary; it is
2451    /// inside '…'`.
2452    #[test]
2453    fn an_offset_inside_a_multibyte_char_does_not_panic() {
2454        let d = doc(
2455            "# April 02, 2026\n\nAn interesting thing AI said to me:\n\n> a person… who journals\n",
2456        );
2457        d.toggle_view(); // to the raw source view, where offsets index bytes directly
2458        let src = d.source();
2459        // The interior byte of the `…` — exactly the shape of the crash.
2460        let mid = src.find('…').expect("the ellipsis is in the fixture") + 1;
2461        assert!(
2462            !src.is_char_boundary(mid),
2463            "the fixture must be mid-character"
2464        );
2465
2466        // Every entry point that takes a raw source offset.
2467        let _ = d.pos_for_offset(mid as u32);
2468        let _ = d.vertical_offset(mid as u32, true);
2469        let _ = d.vertical_offset(mid as u32, false);
2470        let _ = d.snap_offset(mid as u32);
2471        let _ = d.step_offset(mid as u32, 1);
2472        let _ = d.step_offset(mid as u32, -1);
2473        let _ = d.distance_offset(0, mid as u32);
2474        let _ = d.text_in_range(0, mid as u32);
2475        let _ = d.set_selection_offsets(mid as u32, mid as u32);
2476        // And the caret must not come to rest inside the character either — a
2477        // mid-character caret is a later panic waiting for the next edit.
2478        let _ = d.replace_range(mid as u32, mid as u32, "x".to_string());
2479        assert!(
2480            d.source().is_char_boundary(d.caret_offset() as usize),
2481            "the caret must sit on a character boundary"
2482        );
2483    }
2484
2485    #[test]
2486    fn cell_lines_split_on_the_break_glyph_carrying_each_lines_source_range() {
2487        use leaf_core::Glyph;
2488        let g = |ch, src| Glyph {
2489            ch,
2490            style: LStyle::default(),
2491            src,
2492            stop: true,
2493        };
2494        // "a" at 10, a `<br>` at 11..15 (the break glyph), "b" at 15; cell 10..16.
2495        let glyphs = [g('a', 10), g('\n', 11), g('b', 15)];
2496        let lines = cell_lines(&glyphs, 10, 16, 0, 0);
2497        assert_eq!(lines.len(), 2, "one break makes two lines");
2498        assert_eq!(
2499            (lines[0].start, lines[0].end),
2500            (10, 11),
2501            "line 1 ends at the break"
2502        );
2503        assert_eq!(
2504            (lines[1].start, lines[1].end),
2505            (15, 16),
2506            "line 2 begins past it"
2507        );
2508        let text =
2509            |l: &TableCellLineView| l.runs.iter().map(|r| r.text.clone()).collect::<String>();
2510        assert_eq!(text(&lines[0]), "a");
2511        assert_eq!(text(&lines[1]), "b");
2512
2513        // A trailing break leaves an empty last line homed at the cell's end.
2514        let trailing = [g('a', 10), g('\n', 11)];
2515        let lines = cell_lines(&trailing, 10, 15, 0, 0);
2516        assert_eq!(lines.len(), 2);
2517        assert!(lines[1].runs.is_empty());
2518        assert_eq!((lines[1].start, lines[1].end), (15, 15));
2519
2520        // No break: one line spanning the whole cell.
2521        let plain = [g('P', 10), g('e', 11)];
2522        let lines = cell_lines(&plain, 10, 12, 0, 0);
2523        assert_eq!(lines.len(), 1);
2524        assert_eq!((lines[0].start, lines[0].end), (10, 12));
2525    }
2526
2527    fn row_text(v: &DocView, row: usize) -> String {
2528        v.rows[row].runs.iter().map(|r| r.text.clone()).collect()
2529    }
2530
2531    #[test]
2532    fn unwrapped_collapses_a_paragraph_to_one_row() {
2533        let d = doc("one two three four five six seven eight\n");
2534        let wrapped = d.set_width(10);
2535        let unwrapped = d.set_unwrapped();
2536        assert!(
2537            unwrapped.rows.len() < wrapped.rows.len(),
2538            "a narrow column wrap splits the paragraph; unwrapped keeps it whole"
2539        );
2540        assert!(
2541            (0..unwrapped.rows.len()).any(|i| row_text(&unwrapped, i).contains("eight")),
2542            "the whole paragraph, including its last word, sits on a single unwrapped row"
2543        );
2544    }
2545
2546    #[test]
2547    fn offsets_round_trip_when_unwrapped() {
2548        let d = doc("hello world\n");
2549        d.set_unwrapped();
2550        // offset -> (row, ch) -> offset is stable, so the pixel-wrapping frontend can
2551        // map between its visual lines and core's byte-offset caret model.
2552        let rc = d.pos_for_offset(6); // the 'w' of "world"
2553        assert_eq!(d.offset_for_pos(rc.row, rc.ch), 6);
2554    }
2555
2556    #[test]
2557    fn set_unwrapped_is_idempotent() {
2558        let d = doc("a paragraph of some length here\n");
2559        let first = d.set_unwrapped();
2560        let second = d.set_unwrapped();
2561        assert_eq!(first.rows.len(), second.rows.len());
2562    }
2563
2564    #[test]
2565    fn newline_on_last_list_item_before_a_blockquote_starts_a_new_item() {
2566        let src = "- one\n- two\n- three\n\n> quote\n";
2567        let d = doc(src);
2568        let off = (src.find("three").unwrap() + "three".len()) as u32; // end of "three" = 19
2569        d.set_selection_offsets(off, off);
2570        d.newline();
2571        let after = d.source();
2572        assert!(
2573            after.contains("- three\n- ") && after.contains("> quote"),
2574            "expected a new empty list item with the blockquote intact, got: {after:?}"
2575        );
2576    }
2577
2578    #[test]
2579    fn enter_on_an_empty_line_adds_one_newline_and_one_backspace_undoes_it() {
2580        let d = doc("hello\n");
2581        d.set_selection_offsets(5, 5);
2582        d.newline(); // paragraph "hello" → a paragraph break, caret on the empty line
2583        let after_para = d.source();
2584        let caret_para = d.caret_offset();
2585        d.newline(); // Enter on the empty line
2586        assert_eq!(
2587            d.source().len(),
2588            after_para.len() + 1,
2589            "an empty-line Enter adds a single newline, not another paragraph break"
2590        );
2591        d.backspace(); // a single Backspace restores the previous state
2592        assert_eq!(d.source(), after_para);
2593        assert_eq!(d.caret_offset(), caret_para);
2594    }
2595
2596    #[test]
2597    fn enter_in_a_nonempty_paragraph_still_opens_a_new_paragraph() {
2598        let d = doc("hello\n");
2599        d.set_selection_offsets(5, 5);
2600        let before = d.source().len();
2601        d.newline();
2602        assert_eq!(
2603            d.source().len(),
2604            before + 2,
2605            "a paragraph break is still \\n\\n"
2606        );
2607    }
2608
2609    #[test]
2610    fn link_destination_at_caret_reads_the_caret_link() {
2611        let d = doc("see [t](https://x.dev) ok\n");
2612        d.set_selection_offsets(5, 5); // caret on the link text "t"
2613        assert_eq!(
2614            d.link_destination_at_caret().as_deref(),
2615            Some("https://x.dev")
2616        );
2617        d.set_selection_offsets(0, 0); // caret on plain text
2618        assert_eq!(d.link_destination_at_caret(), None);
2619    }
2620
2621    #[test]
2622    fn the_frame_carries_the_caret_link_so_a_toolbar_can_light_and_seed_from_it() {
2623        // The reason it rides `DocView` rather than being asked for: stepping the
2624        // caret out of the link changes no other chrome fact on the frame, so a
2625        // toolbar that only redraws on a *changed* state would keep a stale light.
2626        let d = doc("see [t](https://x.dev) ok\n");
2627        d.set_selection_offsets(5, 5);
2628        let inside = d.view();
2629        assert_eq!(inside.link.as_deref(), Some("https://x.dev"));
2630        assert_eq!(inside.heading, None);
2631        assert!(inside.active.is_empty());
2632
2633        d.set_selection_offsets(0, 0);
2634        let outside = d.view();
2635        assert_eq!(outside.link, None);
2636        // Nothing else the frame reports moved with it.
2637        assert_eq!(outside.heading, inside.heading);
2638        assert_eq!(outside.active, inside.active);
2639    }
2640
2641    #[test]
2642    fn insert_footnote_crosses_and_leaves_the_caret_in_the_new_note() {
2643        // The button's round trip through the boundary: both halves written, and
2644        // a caret offset a host can type into without asking anything else.
2645        let d = doc("A claim and more.\n");
2646        d.set_selection_offsets(7, 7); // just past "A claim"
2647        d.insert_footnote();
2648        assert!(
2649            d.source().starts_with("A claim[^1] and more."),
2650            "{:?}",
2651            d.source()
2652        );
2653        assert!(d.source().contains("[^1]:"), "{:?}", d.source());
2654
2655        let note = d.footnote_at(9).expect("the reference just written");
2656        assert_eq!(note.label, "1");
2657        assert_eq!(
2658            d.caret_offset(),
2659            note.offset.expect("an empty note is still a place")
2660        );
2661        // …and the way back out is the same one a reader uses.
2662        assert_eq!(
2663            d.footnote_definition_at_caret().expect("in the note").label,
2664            "1"
2665        );
2666    }
2667
2668    #[test]
2669    fn capabilities_answer_for_footnotes_the_way_the_format_does() {
2670        assert!(
2671            doc("x\n").capabilities().footnote,
2672            "markdown spells the pair"
2673        );
2674        let html = LeafDoc::new("<p>x</p>\n".to_string(), "html".to_string()).unwrap();
2675        assert!(
2676            !html.capabilities().footnote,
2677            "html has no footnote of its own"
2678        );
2679    }
2680
2681    #[test]
2682    fn footnote_at_caret_crosses_with_its_note_and_its_offset() {
2683        let d = doc("A claim[^1] and more.\n\n[^1]: the note\n");
2684        d.set_selection_offsets(9, 9); // caret on the reference's label
2685        let f = d
2686            .footnote_at_caret()
2687            .expect("the caret stands in a reference");
2688        assert_eq!(f.label, "1");
2689        assert_eq!(f.text.as_deref(), Some("the note"));
2690        // The note's first word — a byte the caret can actually rest on. The
2691        // definition's `[^1]:` marker is decoration with no stop of its own.
2692        assert_eq!(f.offset, Some(29));
2693        assert_eq!(f.end, Some(37));
2694
2695        d.set_selection_offsets(0, 0); // caret on plain text
2696        assert!(d.footnote_at_caret().is_none());
2697    }
2698
2699    #[test]
2700    fn footnote_at_crosses_for_an_offset_without_moving_the_caret() {
2701        // What a hover needs: the note under the pointer, and the caret left
2702        // exactly where the reader put it.
2703        let d = doc("A claim[^1] and more.\n\n[^1]: the note\n");
2704        d.set_selection_offsets(0, 0);
2705        let f = d.footnote_at(9).expect("offset 9 stands in the reference");
2706        assert_eq!(f.label, "1");
2707        assert_eq!(f.text.as_deref(), Some("the note"));
2708        assert_eq!(d.caret_offset(), 0, "asking must not move the caret");
2709        assert!(d.footnote_at(2).is_none(), "offset 2 is prose");
2710    }
2711
2712    #[test]
2713    fn footnote_definition_at_caret_crosses_with_the_way_back() {
2714        let d = doc("A claim[^1] and more.\n\n[^1]: the note\n");
2715        d.set_selection_offsets(30, 30); // caret inside the note's body
2716        let f = d
2717            .footnote_definition_at_caret()
2718            .expect("the caret stands in a definition");
2719        assert_eq!(f.label, "1");
2720        assert_eq!(f.offset, Some(9), "the reference's label");
2721
2722        // Disjoint from the reference query, which is what lets one gesture mean
2723        // "down" up top and "back up" down here.
2724        d.set_selection_offsets(9, 9);
2725        assert!(d.footnote_definition_at_caret().is_none());
2726        assert!(d.footnote_at_caret().is_some());
2727    }
2728
2729    /// The contract a peek is built on: a note's offsets map to rows whose runs
2730    /// are the note *rendered* — emphasis as an italic run, `` `code` `` as a
2731    /// code run, a link as a link run — so a frontend draws it the way the
2732    /// document draws it instead of showing the reader raw asterisks.
2733    #[test]
2734    fn a_notes_offsets_map_to_its_rendered_rows() {
2735        let src = "Claim[^a].\n\n[^a]: see *emphasis* and `code` and [a link](https://x.dev).\n";
2736        let d = doc(src);
2737        let view = d.set_unwrapped();
2738        d.set_selection_offsets(6, 6); // the reference's label
2739
2740        let f = d.footnote_at_caret().expect("a reference");
2741        let start = d.pos_for_offset(f.offset.expect("a note"));
2742        let end = d.pos_for_offset(f.end.expect("a note") - 1);
2743        assert_eq!(
2744            start.row, end.row,
2745            "a one-paragraph note is one unwrapped row"
2746        );
2747
2748        let row = &view.rows[start.row as usize];
2749        let runs: Vec<(&str, &str, bool)> = row
2750            .runs
2751            .iter()
2752            .map(|r| (r.role.as_str(), r.text.as_str(), r.italic))
2753            .collect();
2754        assert!(runs.contains(&("body", "emphasis", true)), "got {runs:?}");
2755        assert!(
2756            runs.iter()
2757                .any(|(role, text, _)| *role == "code" && *text == "code"),
2758            "got {runs:?}"
2759        );
2760        assert!(
2761            runs.iter()
2762                .any(|(role, text, _)| *role == "link" && *text == "a link"),
2763            "got {runs:?}"
2764        );
2765
2766        // The rendered row carries no markup characters at all — which is the
2767        // whole point, and what `text` (source bytes) deliberately still does.
2768        let rendered: String = row.runs.iter().map(|r| r.text.as_str()).collect();
2769        assert!(
2770            !rendered.contains('*') && !rendered.contains('`'),
2771            "got {rendered:?}"
2772        );
2773        assert!(
2774            f.text.as_deref().unwrap().contains('*'),
2775            "the source answer keeps them"
2776        );
2777
2778        // `ch` is where the body starts within the row — past the `[a] ` marker,
2779        // so a frontend that wants the note without its label can slice there.
2780        assert_eq!(row.runs[0].role, "list");
2781        assert_eq!(start.ch as usize, row.runs[0].text.chars().count());
2782
2783        // And each run says where it came from, which is how a link run drawn in
2784        // a popover learns where it points. `Run` otherwise says how a span
2785        // looks, never what it means.
2786        let link = row
2787            .runs
2788            .iter()
2789            .find(|r| r.role == "link")
2790            .expect("a link run");
2791        assert_eq!(
2792            d.link_destination_at(link.src).as_deref(),
2793            Some("https://x.dev"),
2794            "the run at {} is the link",
2795            link.src
2796        );
2797    }
2798
2799    /// The peek bug, in the shape it was actually found in: three notes, each
2800    /// ending in a link, which is what a real citation block looks like.
2801    ///
2802    /// `a_notes_offsets_map_to_its_rendered_rows` above uses a note ending in a
2803    /// visible `.`, so its last byte has a row of its own and `end - 1` reads
2804    /// right. Take the full stop away — end the note *with* the link, as a
2805    /// citation does — and the last byte falls inside the hidden destination,
2806    /// where `pos_for_offset` snaps forward onto the next note's row. Hovering
2807    /// `[^2]` peeked notes 2 *and* 3.
2808    #[test]
2809    fn a_note_ending_in_a_link_covers_its_own_row_and_no_other() {
2810        let src = "A[^1] B[^2] C[^3].\n\n\
2811                   [^1]: https://en.wikipedia.org/wiki/Moravec%27s_paradox\n\n\
2812                   [^2]: [\"How to Get Startup Ideas,\" Nov 2012](https://www.paulgraham.com/startupideas.html)\n\n\
2813                   [^3]: [Alma 37:46](https://www.churchofjesuschrist.org/study/scriptures/bofm/alma/37?lang=eng&id=p46#p46)\n";
2814        let d = doc(src);
2815        let view = d.set_unwrapped();
2816
2817        // The caret in the [^2] reference, exactly as a hover resolves it.
2818        let off2 = src.find("[^2] C").unwrap() as u32 + 2;
2819        d.set_selection_offsets(off2, off2);
2820        let f = d.footnote_at_caret().expect("a reference");
2821        let (start, end) = (f.offset.expect("a note"), f.end.expect("a note"));
2822
2823        let span = d.row_range_for(start, end);
2824        assert_eq!(span.first, span.last, "one note is one unwrapped row");
2825
2826        // And what it draws is note 2 alone — the assertion the popover failed.
2827        let drawn: String = view.rows[span.first as usize]
2828            .runs
2829            .iter()
2830            .map(|r| r.text.as_str())
2831            .collect();
2832        assert!(drawn.contains("How to Get Startup Ideas"), "got {drawn:?}");
2833        assert!(
2834            !drawn.contains("Alma"),
2835            "note 3 leaked into the peek: {drawn:?}"
2836        );
2837
2838        // The old arithmetic, pinned as still wrong so nobody quietly restores
2839        // it: this is the failure `row_range_for` exists instead of.
2840        assert_ne!(
2841            d.pos_for_offset(end - 1).row,
2842            span.last,
2843            "the forward snap still leaves the note's row — that is the point",
2844        );
2845
2846        // Note 1 is a bare autolink, whose visible text *is* its URL, so it was
2847        // never affected and must not change.
2848        let off1 = src.find("[^1] B").unwrap() as u32 + 2;
2849        d.set_selection_offsets(off1, off1);
2850        let f1 = d.footnote_at_caret().expect("a reference");
2851        let one = d.row_range_for(f1.offset.unwrap(), f1.end.unwrap());
2852        assert_eq!(one.first, one.last);
2853        assert_ne!(one.first, span.first, "and it is a different note");
2854    }
2855
2856    /// A run's `src` is a byte offset core handed over, not something a frontend
2857    /// counted its way to — so multi-byte prose ahead of a link inside a note
2858    /// can't slide it.
2859    ///
2860    /// The offset is a *byte* offset while the run's text is characters and the
2861    /// row's columns are display cells; `src` is the only one of the three a
2862    /// frontend can use without converting between the other two.
2863    #[test]
2864    fn a_runs_source_offset_survives_multibyte_prose_ahead_of_it() {
2865        let src = "Claim[^a].\n\n[^a]: 日記 café [a link](https://x.dev).\n";
2866        let d = doc(src);
2867        let view = d.set_unwrapped();
2868        d.set_selection_offsets(6, 6);
2869
2870        let f = d.footnote_at_caret().expect("a reference");
2871        let start = d.pos_for_offset(f.offset.expect("a note"));
2872        let row = &view.rows[start.row as usize];
2873        let link = row
2874            .runs
2875            .iter()
2876            .find(|r| r.role == "link")
2877            .expect("a link run");
2878
2879        assert_eq!(
2880            d.link_destination_at(link.src).as_deref(),
2881            Some("https://x.dev")
2882        );
2883        assert_eq!(
2884            &src[link.src as usize..][.."a link".len()],
2885            "a link",
2886            "and it is a byte offset, not a character or column index"
2887        );
2888        // Which the character count is not: `日記 café ` is 9 characters and 13
2889        // bytes, so anything derived from the run text lands in the wrong place.
2890        let counted: usize = row
2891            .runs
2892            .iter()
2893            .take_while(|r| r.role != "link")
2894            .map(|r| r.text.chars().count())
2895            .sum();
2896        assert_ne!(counted, link.src as usize);
2897    }
2898
2899    /// The round trip through the API a frontend actually calls — which places
2900    /// carets, and so snaps them to real stops. Offsets that named the `[^`
2901    /// markers passed every test that assigned the caret directly and still
2902    /// dumped the reader in the paragraph above the note.
2903    #[test]
2904    fn following_a_footnote_and_coming_back_lands_on_real_caret_stops() {
2905        let d = doc("A claim[^1] and more.\n\n[^1]: the note\n");
2906        d.set_selection_offsets(9, 9);
2907
2908        let down = d
2909            .footnote_at_caret()
2910            .expect("a reference")
2911            .offset
2912            .expect("a note");
2913        d.set_selection_offsets(down, down);
2914        assert_eq!(
2915            d.caret_offset(),
2916            down,
2917            "the note is somewhere the caret fits"
2918        );
2919
2920        let up = d
2921            .footnote_definition_at_caret()
2922            .expect("arrived inside the definition")
2923            .offset
2924            .expect("a reference to return to");
2925        d.set_selection_offsets(up, up);
2926        assert_eq!(d.caret_offset(), up, "and so is the reference");
2927        assert_eq!(
2928            d.footnote_at_caret().expect("back on the reference").label,
2929            "1"
2930        );
2931    }
2932
2933    #[test]
2934    fn a_footnote_reference_crosses_the_ffi_raised() {
2935        // The whole point of the `sup` flag: without it a reference reaches
2936        // Swift as a run indistinguishable from a hyperlink's, which is why it
2937        // used to draw at body size.
2938        let d = doc("A claim[^1] and more.\n");
2939        let view = d.view();
2940        let runs: Vec<&Run> = view.rows.iter().flat_map(|r| &r.runs).collect();
2941        let chip = runs
2942            .iter()
2943            .find(|r| r.text.contains('1'))
2944            .expect("the reference's chip");
2945        assert!(chip.sup, "the reference should cross raised");
2946        assert!(!chip.sub);
2947        assert_eq!(
2948            chip.role, "link",
2949            "and still carrying the role every frontend paints"
2950        );
2951        // The prose it interrupts is a run of its own, on the normal baseline —
2952        // which is what proves the flag splits runs rather than bleeding.
2953        let prose = runs
2954            .iter()
2955            .find(|r| r.text.contains("claim"))
2956            .expect("the prose");
2957        assert!(!prose.sup && !prose.sub);
2958    }
2959
2960    #[test]
2961    fn text_in_range_hides_delimiters_like_the_screen_does() {
2962        // "a **bold** c\n": 0:'a' 1:' ' 2:'*' 3:'*' 4:'b' 5:'o' 6:'l' 7:'d'
2963        // 8:'*' 9:'*' 10:' ' 11:'c' 12:'\n'. Bytes 8..10 are the closing `**`
2964        // — hidden, no glyph — and bytes 2..4 the opening `**`, likewise
2965        // hidden. `caret_steps_over_hidden_delimiters` in leaf-core already
2966        // pins that one Right from 7 (just past the 'd') lands on 10 (the
2967        // space before 'c'), skipping 8/9 entirely — so the *visible* text
2968        // transiting [7, 10) is exactly "d": the closing `**` contributes
2969        // nothing, matching what's drawn on screen.
2970        let d = doc("a **bold** c\n");
2971        assert_eq!(d.text_in_range(7, 10), "d");
2972        assert_eq!(
2973            d.text_in_range(7, 10).chars().count() as i32,
2974            d.distance_offset(7, 10),
2975            "text(in:).count() must equal offset(from:to:) — the UITextInput invariant this bug broke"
2976        );
2977
2978        // Plain text with no hidden delimiter in range: unchanged, still the
2979        // raw slice, proving the fix doesn't regress the common case.
2980        assert_eq!(d.text_in_range(0, 1), "a");
2981        assert_eq!(d.text_in_range(11, 12), "c");
2982        assert_eq!(
2983            d.text_in_range(0, 1).chars().count() as i32,
2984            d.distance_offset(0, 1)
2985        );
2986    }
2987
2988    #[test]
2989    fn text_in_range_matches_distance_offset_across_marked_up_and_plain_spans() {
2990        // The general invariant, straddling bold/italic/code spans and not:
2991        // for any pair of offsets, the visible text `text_in_range` returns
2992        // must have exactly as many `chars()` as `distance_offset` reports
2993        // stops between them — otherwise iOS's word tokenizer (which fetches
2994        // a text window, finds a boundary by indexing into *that string*, and
2995        // converts the index back to a position via `position(from:offset:)`)
2996        // resolves the boundary at the wrong offset.
2997        let d = doc("a **bold** _em_ and `code` here\n");
2998        let len = d.source().len() as u32;
2999        let mut pairs = Vec::new();
3000        let mut a = 0u32;
3001        while a < len {
3002            let mut b = a + 1;
3003            while b <= len {
3004                pairs.push((a, b));
3005                b += 3; // sample rather than an O(n^2) sweep
3006            }
3007            a += 1;
3008        }
3009        for (a, b) in pairs {
3010            let text = d.text_in_range(a, b);
3011            let dist = d.distance_offset(a, b).abs();
3012            assert_eq!(
3013                text.chars().count() as i32,
3014                dist,
3015                "text_in_range({a}, {b}) = {text:?} has {} chars, but distance_offset says {dist}",
3016                text.chars().count()
3017            );
3018        }
3019    }
3020
3021    #[test]
3022    fn text_in_range_separates_paragraphs_so_words_dont_merge_across_the_gap() {
3023        // Regression: double-tapping the last word on a line immediately
3024        // followed by a paragraph break selected past the break into the
3025        // next paragraph — and kept compounding across further trivial
3026        // paragraphs in a row — because `text_in_range` returned the two
3027        // paragraphs' text with nothing between them: "hello" then "hello"
3028        // read back as one merged "hellohello" run of letters, no different
3029        // from the raw source concatenation, and iOS's word tokenizer duly
3030        // selected the whole run as a single word.
3031        let d = doc("hello\n\nhello\n\nhello\n");
3032        let src = d.source();
3033        assert_eq!(
3034            src.find("hello").unwrap(),
3035            0,
3036            "paragraph 1 at the very start"
3037        );
3038        let p2 = src[5..].find("hello").unwrap() + 5; // 7: paragraph 2's "hello"
3039
3040        // A window straddling the tail of paragraph 1 ("lo") and the head of
3041        // paragraph 2 ("he").
3042        let text = d.text_in_range(3, p2 as u32 + 2);
3043        assert_ne!(
3044            text, "lohe",
3045            "the two paragraphs' words must not read as merged"
3046        );
3047        assert!(
3048            text.chars().any(|c| !c.is_alphanumeric()),
3049            "a non-letter must separate the two paragraphs' words: got {text:?}"
3050        );
3051        assert_eq!(
3052            text, "lo\nhe",
3053            "exactly one separator opens the second paragraph's head"
3054        );
3055
3056        // A window that is nothing but the bare gap itself (no glyph on the
3057        // left, since it starts exactly at the end of paragraph 1's own last
3058        // row) must still carry the break — this is the case a naive
3059        // "insert a separator only between two real hits" fix undercounts,
3060        // since there is no earlier hit to anchor it to.
3061        let gap_only = d.text_in_range(5, p2 as u32);
3062        assert!(
3063            gap_only.chars().count() as i32 >= d.distance_offset(5, p2 as u32),
3064            "text_in_range must never be shorter than distance_offset: {gap_only:?}"
3065        );
3066
3067        // The invariant the two existing tests above assert (strict
3068        // equality) no longer holds once the range spans a paragraph
3069        // boundary — see `text_in_range`'s doc comment — but it must never
3070        // *undercount* relative to `distance_offset`, which is what would let
3071        // a tokenizer's `position(from:offset:)` walk past where the text it
3072        // was handed actually put a boundary.
3073        for (a, b) in [(0u32, src.len() as u32), (3, p2 as u32 + 2), (5, p2 as u32)] {
3074            let text = d.text_in_range(a, b);
3075            let dist = d.distance_offset(a, b);
3076            assert!(
3077                text.chars().count() as i32 >= dist,
3078                "text_in_range({a}, {b}) = {text:?} ({} chars) is shorter than distance_offset {dist}",
3079                text.chars().count()
3080            );
3081        }
3082
3083        // Caret motion itself is untouched by any of this: from the very end
3084        // of paragraph 1's row, a paragraph gap still costs exactly one
3085        // Right press to reach the start of paragraph 2 — matching
3086        // leaf-core's `the_caret_skips_the_gap_between_two_paragraphs`.
3087        assert_eq!(
3088            d.distance_offset(5, p2 as u32),
3089            1,
3090            "one Right crosses the whole gap"
3091        );
3092    }
3093}