Skip to main content

Doc

Struct Doc 

Source
pub struct Doc {
Show 13 fields pub format: Format, pub path: PathBuf, pub source: String, pub caret: usize, pub anchor: Option<usize>, pub dirty: bool, pub status: Option<String>, pub view: View, pub vmap: VisualMap, pub scroll: usize, pub body_origin: (u16, u16), pub body_height: u16, pub drawn_caret: Option<usize>, /* private fields */
}

Fields§

§format: Format§path: PathBuf§source: String

Current source, refreshed from the editor after every successful edit.

§caret: usize

The caret, as a byte offset into source (always on a char boundary).

§anchor: Option<usize>

The selection’s fixed end, if a selection is active; the moving end is the caret. None means no selection.

§dirty: bool§status: Option<String>§view: View§vmap: VisualMap

The rendered map for the WYSIWYG view; empty in the source view. Movement and clicks read it to stay in visible space.

§scroll: usize§body_origin: (u16, u16)§body_height: u16§drawn_caret: Option<usize>

The caret as of the last frame drawn, or None before the first.

Scrolling is the viewport’s business, not the caret’s: the view follows the caret when the caret moves, but a wheel that doesn’t touch the caret has to be free to scroll away from it — otherwise the view is pinned to the caret and stops dead at the edge of the document you can see. Comparing against this is what tells the two apart, and it catches a caret set by any route, including a frontend assigning the field itself.

Implementations§

Source§

impl Doc

Source

pub fn open(path: PathBuf) -> Result<Self>

Examples found in repository?
examples/bench.rs (line 121)
34fn main() {
35    for kb in [10usize, 100, 1000] {
36        let src = body(kb * 1024);
37        println!("=== {} KB ===", src.len() / 1024);
38
39        let mut ed = Editor::new_str(&src, Format::Markdown).unwrap();
40        let nodes = ed.nodes().unwrap();
41        let map = wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None);
42        println!("  ({} AST nodes, {} map rows)", nodes.len(), map.rows.len());
43
44        println!("  -- per edit (unavoidable today) --");
45        time("twig edit_range (reparse)", 5, || {
46            ed.edit_range(src.len() / 2, src.len() / 2, "x").is_ok()
47        });
48        time("twig nodes() FFI marshal", 5, || ed.nodes().unwrap().len());
49        time("wysiwyg::build", 5, || {
50            wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None)
51                .rows
52                .len()
53        });
54        {
55            // The incremental path with a warm cache and nothing changed: the
56            // floor cost the block cache adds even on a pure repaint — hash every
57            // block, clone every reused row, recollect stops. No subtree is
58            // marshalled (every block hits). The real keystroke win shows up in
59            // "Doc::insert + rebuild" below, which re-marshals only the edited
60            // block and reuses the rest.
61            let mut cache = wysiwyg::BlockCache::default();
62            let top = ed.child_spans(None).unwrap();
63            let _ = wysiwyg::build_cached(
64                &top,
65                &src,
66                None,
67                false,
68                &HashMap::new(),
69                None,
70                &mut cache,
71                |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
72            );
73            time("wysiwyg::build_cached (all reused)", 5, || {
74                let top = ed.child_spans(None).unwrap();
75                wysiwyg::build_cached(
76                    &top,
77                    &src,
78                    None,
79                    false,
80                    &HashMap::new(),
81                    None,
82                    &mut cache,
83                    |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
84                )
85                .rows
86                .len()
87            });
88        }
89
90        println!("  -- claimed hot, actually noise --");
91        time("twig source_str() (full copy)", 5, || {
92            ed.source_str().unwrap().len()
93        });
94        let clean = src.clone();
95        time("dirty compare (full cmp)", 5, || src == clean);
96
97        println!("  -- what the GUI adds on a cache miss --");
98        time("clone every row's glyphs", 5, || {
99            map.rows
100                .iter()
101                .map(|r| r.glyphs.clone())
102                .collect::<Vec<_>>()
103                .len()
104        });
105        time("hash every glyph (cache key?)", 5, || {
106            let mut n = 0u64;
107            for r in &map.rows {
108                let mut h = std::collections::hash_map::DefaultHasher::new();
109                for g in &r.glyphs {
110                    g.ch.hash(&mut h);
111                }
112                n ^= h.finish();
113            }
114            n
115        });
116
117        println!("  -- the whole path, as a frontend calls it --");
118        let mut p = std::env::temp_dir();
119        p.push(format!("leaf_bench_{kb}.md"));
120        std::fs::write(&p, &src).unwrap();
121        let mut d = Doc::open(p).unwrap();
122        d.view = View::Wysiwyg;
123        d.place_caret(src.len() / 2, false);
124        d.build_visual_unwrapped();
125        time("build_visual (cached: a repaint)", 200, || {
126            d.build_visual_unwrapped()
127        });
128        time("Doc::insert + rebuild (a keystroke)", 5, || {
129            d.insert("x");
130            d.build_visual_unwrapped();
131        });
132        println!();
133    }
134}
Source

pub fn from_source(source: String, format: Format) -> Result<Self>

Build a document from an in-memory string, the format named explicitly — the portable, filesystem-free counterpart to Doc::open (which reads a path and sniffs the format from its extension). A wasm or FFI host, which has no path to read, uses this: it hands over bytes it fetched however it could, and later persists Doc::source however it can (a browser download, localStorage, a backend PUT) and calls Doc::mark_saved.

No file backs the result, so it starts untitled (Doc::is_untitled is true) exactly like a Doc::blank that has been given content.

Source

pub fn blank() -> Result<Self>

An untitled, empty document — the + button and a leaf launched with no file argument. Nothing on disk backs it until a Doc::save_as.

It is Markdown, because a format has to be chosen before a name exists to read one from: detect_format reads the extension and an untitled document has neither. Markdown is what leaf’s own files are, what its block markers are already written for (insert_block_prefix), and the extension a Save As will overwhelmingly pick — a wrong guess here would mean typing djot into a buffer parsing it as Markdown. Note that Save As doesn’t revisit this: see Doc::save_as.

Source

pub fn is_untitled(&self) -> bool

Whether this document has no file behind it yet — a Doc::blank that has never been saved. The question a ⌘S handler asks to know it should open a Save As picker instead (Doc::save won’t guess a name), and the header asks to know the name it shows is a placeholder.

Source

pub fn toggle_view(&mut self)

Source

pub fn markup_mode(&self) -> MarkupMode

The current markup-exposure preference (see MarkupMode).

Source

pub fn set_markup_mode(&mut self, mode: MarkupMode)

Set the markup-exposure preference. Both of its axes take effect at once: the editing one on the next insert, and the rendering one on the next build — which is why this drops the cached visual map and the per-block render cache, exactly as set_line_flow does.

Source

pub fn line_flow(&self) -> LineFlow

The current soft-break flow preference (see LineFlow).

Source

pub fn set_line_flow(&mut self, mode: LineFlow)

Set the soft-break flow preference. The mode changes how every block lays out, so a change drops the cached visual map and the per-block render cache, forcing the next build_visual to rebuild under the new flow.

Source

pub fn view_name(&self) -> &'static str

Source

pub fn build_visual(&mut self, width: usize)

Rebuild the WYSIWYG visual map for the current tree at width columns (called by the renderer each frame it’s in the WYSIWYG view). Build the WYSIWYG map, wrapped at width display columns.

Cheap to call every frame, which is what both frontends do: the map is a pure function of the document and the wrap width, so a call that would rebuild the same map returns the one already built. Only an edit (or a resize) pays.

That isn’t a micro-optimisation. A frontend repaints for reasons that have nothing to do with the text — a blinking caret, a scroll, a focus change — and rebuilding here is O(document): 23 ms on a 1 MB file, of which 5 ms is marshalling twig’s AST across the C ABI. Paid twice a second by the GUI’s blink timer, that was 14% of a core spent redrawing an unchanged document. (cargo run --release -p leaf-core --example bench for the numbers.)

Source

pub fn build_visual_unwrapped(&mut self)

Build the WYSIWYG map with each block as a single unwrapped row — for a frontend (the GUI) that wraps at its own proportional pixel width rather than a fixed character column.

Examples found in repository?
examples/bench.rs (line 124)
34fn main() {
35    for kb in [10usize, 100, 1000] {
36        let src = body(kb * 1024);
37        println!("=== {} KB ===", src.len() / 1024);
38
39        let mut ed = Editor::new_str(&src, Format::Markdown).unwrap();
40        let nodes = ed.nodes().unwrap();
41        let map = wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None);
42        println!("  ({} AST nodes, {} map rows)", nodes.len(), map.rows.len());
43
44        println!("  -- per edit (unavoidable today) --");
45        time("twig edit_range (reparse)", 5, || {
46            ed.edit_range(src.len() / 2, src.len() / 2, "x").is_ok()
47        });
48        time("twig nodes() FFI marshal", 5, || ed.nodes().unwrap().len());
49        time("wysiwyg::build", 5, || {
50            wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None)
51                .rows
52                .len()
53        });
54        {
55            // The incremental path with a warm cache and nothing changed: the
56            // floor cost the block cache adds even on a pure repaint — hash every
57            // block, clone every reused row, recollect stops. No subtree is
58            // marshalled (every block hits). The real keystroke win shows up in
59            // "Doc::insert + rebuild" below, which re-marshals only the edited
60            // block and reuses the rest.
61            let mut cache = wysiwyg::BlockCache::default();
62            let top = ed.child_spans(None).unwrap();
63            let _ = wysiwyg::build_cached(
64                &top,
65                &src,
66                None,
67                false,
68                &HashMap::new(),
69                None,
70                &mut cache,
71                |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
72            );
73            time("wysiwyg::build_cached (all reused)", 5, || {
74                let top = ed.child_spans(None).unwrap();
75                wysiwyg::build_cached(
76                    &top,
77                    &src,
78                    None,
79                    false,
80                    &HashMap::new(),
81                    None,
82                    &mut cache,
83                    |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
84                )
85                .rows
86                .len()
87            });
88        }
89
90        println!("  -- claimed hot, actually noise --");
91        time("twig source_str() (full copy)", 5, || {
92            ed.source_str().unwrap().len()
93        });
94        let clean = src.clone();
95        time("dirty compare (full cmp)", 5, || src == clean);
96
97        println!("  -- what the GUI adds on a cache miss --");
98        time("clone every row's glyphs", 5, || {
99            map.rows
100                .iter()
101                .map(|r| r.glyphs.clone())
102                .collect::<Vec<_>>()
103                .len()
104        });
105        time("hash every glyph (cache key?)", 5, || {
106            let mut n = 0u64;
107            for r in &map.rows {
108                let mut h = std::collections::hash_map::DefaultHasher::new();
109                for g in &r.glyphs {
110                    g.ch.hash(&mut h);
111                }
112                n ^= h.finish();
113            }
114            n
115        });
116
117        println!("  -- the whole path, as a frontend calls it --");
118        let mut p = std::env::temp_dir();
119        p.push(format!("leaf_bench_{kb}.md"));
120        std::fs::write(&p, &src).unwrap();
121        let mut d = Doc::open(p).unwrap();
122        d.view = View::Wysiwyg;
123        d.place_caret(src.len() / 2, false);
124        d.build_visual_unwrapped();
125        time("build_visual (cached: a repaint)", 200, || {
126            d.build_visual_unwrapped()
127        });
128        time("Doc::insert + rebuild (a keystroke)", 5, || {
129            d.insert("x");
130            d.build_visual_unwrapped();
131        });
132        println!();
133    }
134}
Source

pub fn set_media_rows(&mut self, rows: HashMap<String, usize>)

Tell the model how many visual rows each block image should reserve, keyed by the image’s destination. A terminal frontend calls this once it has decoded and measured its pictures — core does no image I/O, so this is the only way it learns a height — and the next Doc::build_visual lays each placeholder out that tall (the label row plus blank filler rows the frontend paints the raster over). A destination left out of the map falls back to the bare one-row placeholder, which is also what a frontend that can’t draw pictures (or lays them out in its own units, like the GUI) gets by never calling this.

Cheap to call every frame with the same map: only a change invalidates the built map (and the block-row cache, since a height isn’t part of a block’s bytes and so wouldn’t otherwise re-render it). Steady state is a no-op, so a frontend can just hand over its current measurements each frame.

Source

pub fn revision(&self) -> u64

The revision the document’s text is at — bumped by every edit, undo, redo, and reload, and by nothing else. A frontend caches against this to tell a repaint that needs new work from one that doesn’t.

It counts edits, not distinct texts: typing x and deleting it again lands on the same text two revisions later. Work is only ever rebuilt needlessly, never wrongly reused.

Source

pub fn format_name(&self) -> &'static str

Source

pub fn authorable(&self) -> bool

Whether this document’s format offers any door in — false only for a wholly parse-only format (XML, AsciiDoc), where every gesture refuses and a frontend may as well open the file read-only.

This is a much weaker claim than the name suggests, and driving per-button state from it is exactly the mistake to avoid: HTML answers true because it spells the inline marks with a tag pair (<strong>, <em>, <code>) while a heading, a quote, a list, a task box, a link and a code fence all remain unspellable there. Ask capabilities — or supports — per control.

Source

pub fn supports(&self, gesture: Gesture) -> bool

Whether this document’s format can spell gesture, which is twig’s own answer rather than a copy of it: Format::supports reads the same Syntax table the Editor method consults before refusing.

It is a fact about the format, not about the caret. true does not promise the gesture succeeds where it is standing — a link over a table border still fails — only that it will not fail with UnsupportedFormat. Gray out on false; don’t read true as “this will work here”.

Source

pub fn capabilities(&self) -> Capabilities

Every control’s enabled state in one read — what a toolbar builds itself from when a document opens or its format changes. See Capabilities.

Source

pub fn file_name(&self) -> String

The name to show for this document. An untitled one has no file to name it, and both frontends put this straight on screen — an empty path renders as an empty header, so it says so instead.

Source

pub fn selection(&self) -> Option<(usize, usize)>

The selection as an ordered [start, end) byte range, or None when the caret and anchor coincide (an empty selection is no selection).

Source

pub fn selected_text(&self) -> Option<&str>

The selected text, or None when there’s no selection — the source slice a copy/cut hands to the system clipboard.

Source

pub fn breadcrumb(&mut self) -> String

The AST breadcrumb at the caret (root → deepest), e.g. doc › para › strong. Read live from twig via ancestors_at.

Source

pub fn edit(&mut self, start: usize, end: usize, text: &str)

Replace the byte range [start, end) with text, re-anchoring the caret after it. The public form of the internal splice — a pixel frontend that hit-tests to a byte offset (or an IME that hands back an explicit range) edits through this, the same twig edit_range the caret ops use.

Source

pub fn insert(&mut self, text: &str)

Insert typed text at the caret, replacing the selection if there is one. A single typed character coalesces with the run of typing before it; a newline or a multi-character insert is its own undo step.

Typed input only — clipboard text goes through paste.

Examples found in repository?
examples/bench.rs (line 129)
34fn main() {
35    for kb in [10usize, 100, 1000] {
36        let src = body(kb * 1024);
37        println!("=== {} KB ===", src.len() / 1024);
38
39        let mut ed = Editor::new_str(&src, Format::Markdown).unwrap();
40        let nodes = ed.nodes().unwrap();
41        let map = wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None);
42        println!("  ({} AST nodes, {} map rows)", nodes.len(), map.rows.len());
43
44        println!("  -- per edit (unavoidable today) --");
45        time("twig edit_range (reparse)", 5, || {
46            ed.edit_range(src.len() / 2, src.len() / 2, "x").is_ok()
47        });
48        time("twig nodes() FFI marshal", 5, || ed.nodes().unwrap().len());
49        time("wysiwyg::build", 5, || {
50            wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None)
51                .rows
52                .len()
53        });
54        {
55            // The incremental path with a warm cache and nothing changed: the
56            // floor cost the block cache adds even on a pure repaint — hash every
57            // block, clone every reused row, recollect stops. No subtree is
58            // marshalled (every block hits). The real keystroke win shows up in
59            // "Doc::insert + rebuild" below, which re-marshals only the edited
60            // block and reuses the rest.
61            let mut cache = wysiwyg::BlockCache::default();
62            let top = ed.child_spans(None).unwrap();
63            let _ = wysiwyg::build_cached(
64                &top,
65                &src,
66                None,
67                false,
68                &HashMap::new(),
69                None,
70                &mut cache,
71                |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
72            );
73            time("wysiwyg::build_cached (all reused)", 5, || {
74                let top = ed.child_spans(None).unwrap();
75                wysiwyg::build_cached(
76                    &top,
77                    &src,
78                    None,
79                    false,
80                    &HashMap::new(),
81                    None,
82                    &mut cache,
83                    |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
84                )
85                .rows
86                .len()
87            });
88        }
89
90        println!("  -- claimed hot, actually noise --");
91        time("twig source_str() (full copy)", 5, || {
92            ed.source_str().unwrap().len()
93        });
94        let clean = src.clone();
95        time("dirty compare (full cmp)", 5, || src == clean);
96
97        println!("  -- what the GUI adds on a cache miss --");
98        time("clone every row's glyphs", 5, || {
99            map.rows
100                .iter()
101                .map(|r| r.glyphs.clone())
102                .collect::<Vec<_>>()
103                .len()
104        });
105        time("hash every glyph (cache key?)", 5, || {
106            let mut n = 0u64;
107            for r in &map.rows {
108                let mut h = std::collections::hash_map::DefaultHasher::new();
109                for g in &r.glyphs {
110                    g.ch.hash(&mut h);
111                }
112                n ^= h.finish();
113            }
114            n
115        });
116
117        println!("  -- the whole path, as a frontend calls it --");
118        let mut p = std::env::temp_dir();
119        p.push(format!("leaf_bench_{kb}.md"));
120        std::fs::write(&p, &src).unwrap();
121        let mut d = Doc::open(p).unwrap();
122        d.view = View::Wysiwyg;
123        d.place_caret(src.len() / 2, false);
124        d.build_visual_unwrapped();
125        time("build_visual (cached: a repaint)", 200, || {
126            d.build_visual_unwrapped()
127        });
128        time("Doc::insert + rebuild (a keystroke)", 5, || {
129            d.insert("x");
130            d.build_visual_unwrapped();
131        });
132        println!();
133    }
134}
Source

pub fn paste(&mut self, text: &str)

Insert clipboard text at the caret, replacing the selection if there is one — always its own undo step, whatever its length.

Provenance is the whole point, and only the caller has it. insert reads a lone character as a keystroke and folds it into the run around it, which is right for typing and wrong for a one-character paste: that paste would vanish mid-run on an undo it was never part of, and the characters the user actually typed would go with it. Length can’t tell the two apart — ⌘V of x and typing x are the same string — so the door the caller comes through is what says which happened.

Source

pub fn edit_composing(&mut self, start: usize, end: usize, text: &str)

Replace [start, end) with text as one step of an IME composition — the same splice as edit, but marked so the run of steps folds into a single undo.

A composition is one act of writing. Typing かんじ and picking 感じ is a dozen calls here, each replacing the last one’s provisional bytes, and an undo step per call means undoing a word means pressing ⌘Z until the reading unspools backwards through kana — the intermediate states were never text the user wrote. Only the frontend knows a call is provisional (the bytes look like any other edit), so the door the caller comes through is what says so, exactly as it is for paste versus insert.

Pair with end_composition, or the next composition folds into this one.

Source

pub fn end_composition(&mut self)

Close the open composition run, so the next one is its own undo step. Call when the IME commits or withdraws a composition.

Only clears a composition run: a frontend that reports an end it never began (some IMEs unmark unprompted) would otherwise split the run of typing around it into two undo steps for no reason the user can see.

Source

pub fn selection_html(&mut self) -> Option<String>

The selection rendered as HTML, for the clipboard’s text/html flavor — what lets a paste into Docs/Mail/Slack keep its formatting. None when nothing is selected, or when the selection doesn’t render (the caller still has selected_text, which is what to publish as text/plain either way).

The fragment is a source substring, and that is the honest limit here. It’s parsed standalone, so a selection whose meaning depends on its surroundings converts as what it literally says rather than what it looks like on screen: half a list item is a paragraph, a row torn out of a table is the text of a row, the ** of a bold run selected without its closing ** is two asterisks. Every one of those still renders — there’s no error to report — it just renders as the fragment and not as the document. Widening the range to whole blocks would publish text the user didn’t select, which is a worse lie than a fragment being a fragment; the plain flavor has the same substring, so the two flavors at least agree.

Source

pub fn paste_html(&mut self, html: &str) -> bool

Paste the clipboard’s text/html flavor, converting it to this document’s format first. Its own undo step, like any paste.

Returns whether it landed. false means the HTML didn’t convert to anything worth pasting — the caller should fall back to the plain flavor rather than treat it as an error. The html module has the full list of what that covers: a table twig won’t build, markup it doesn’t recognise, an empty result.

Source

pub fn indent(&mut self)

Indent the selected lines — or the caret’s line, with no selection — by one level (Tab).

Source

pub fn outdent(&mut self)

Take one indent level back off the selected lines, or the caret’s line (Shift+Tab). A line with no indentation is left exactly as it is.

A line with less than a full level gives back what it has rather than refusing: outdent’s job is to walk a line left, and real documents — hand written, or reflowed by some other editor — are full of indentation that was never a clean multiple of anything. Refusing there would strand the line at a depth Shift+Tab couldn’t undo.

Source

pub fn newline(&mut self)

The Enter key.

In source view it’s a literal newline. In WYSIWYG it’s AST-aware: a bare \n is only a markdown soft break (same paragraph), so the block the caret is in decides what actually gets written.

  • paragraph → twig’s Editor::split_block, which parts the block at the caret and reopens its container
  • list item → likewise: the next item, its indent, quote prefix and [ ] box all reproduced by twig — except an empty item, which exits the list
  • block quote → likewise: a new paragraph inside the quote
  • heading → a new paragraph, not another heading
  • code block → a literal newline (stay in the block)
  • blank line → a literal newline (one Backspace undoes it)
  • LineFlow::Preserve → a single soft break, which renders as a visible line

Where split_block is used it replaces markup leaf used to spell by hand, and it is better at it: it drops the whitespace the caret was sitting in front of instead of stranding it at the head of the second half, and it knows continuations leaf’s marker scan never covered — a checklist item continues as an unchecked checklist item rather than a plain bullet.

The exceptions above are exceptions because split_block is either wrong there or refuses: parting a fence yields two fences with the code split between them, parting a heading yields a second heading where every editor gives a paragraph, and a blank line, an empty item, a setext heading and a table all report an error rather than a split.

Source

pub fn backspace(&mut self)

Source

pub fn delete_forward(&mut self)

Source

pub fn delete_word_back(&mut self)

Delete from the caret back to the start of the previous word (⌥⌫ / Ctrl+⌫). Deletes the selection instead when one is active.

Source

pub fn delete_word_forward(&mut self)

Delete from the caret forward to the end of the next word (⌥⌦ / Ctrl+Del). Deletes the selection instead when one is active.

Source

pub fn delete_to_line_start(&mut self)

Delete from the caret back to the start of its line (⌘⌫). Deletes the selection instead when one is active, as every other delete here does.

The line is the view’s own — the one Home and End work on, so in WYSIWYG a soft-wrapped row is a line. It is not Home’s target, though: Home stops at the first character and this takes the indentation with it, the way Cocoa’s deleteToBeginningOfLine: does. Stopping at the text would leave an indent behind that nothing can then ask to delete, where a caret left at column 0 is one press of Home away from either.

Source

pub fn delete_to_line_end(&mut self)

Kill from the caret to the end of its line (^K). Deletes the selection instead when one is active.

At the end of the line it does nothing, rather than pulling the line below up into this one. Joining has no meaning to give it in both views at once: a WYSIWYG line ends at a soft wrap as often as at a newline, and there is nothing there to delete, while the newline a source line ends with is only half of the blank line that separates two paragraphs — deleting one leaves a soft break, which is not the join it looks like. The views agreeing is worth more than emacs’ second press, and Delete is already the key that joins.

Source

pub fn toggle(&mut self, kind: InlineKind)

Toggle an inline mark over the selection (Bold / Italic / Code / …). Keeps the toggled region selected so a second press cleanly reverses it.

Source

pub fn set_block(&mut self, kind: BlockKind)

Convert the block at the caret to a heading level or paragraph.

Source

pub fn current_heading_level(&mut self) -> Option<u32>

The heading level of the text block at the caret, or None when that block is not a heading.

Source

pub fn active_inline_marks(&mut self) -> InlineMarks

The inline marks in force at the caret (or over the selection) — what a toolbar draws lit, and the block-level Doc::current_heading_level’s inline counterpart. Cheap enough to call every frame: one twig ancestors_at query per caret (two with a selection), each walking root → deepest node at one offset. It never snapshots the tree the way current_heading_level does, and the returned set is a Copy bitset, so the only allocation is twig’s own small ancestor Vec.

A selection reports a mark only when the mark covers all of it. That’s what every real toolbar means by an active button — Bold lit over a half-bold selection would claim a press turns bold off, when Doc::toggle hands the range to twig and gets the whole thing bolded. Whole-coverage is asked as “is the same mark node standing over both the first and the last character?”: inline nodes are contiguous, so one node covering both ends covers every byte between them. Two touching runs (**a****b**) are two nodes, and correctly light nothing.

At a bare caret a mark is active when the caret stands inside the mark’s span — span.start <= caret < span.end, delimiters included, which is what makes the boundaries behave. In a **bold** b the offsets from the opening * (2) through the last byte of the closing ** (9) are all bold, so the WYSIWYG caret both before b and after d (the delimiters are hidden, and those offsets are 4 and 8) reports bold — matching where typing would actually land inside the marked run. The offset one past the mark (10) is the text after it and reports nothing, at the end of the buffer exactly as in the middle.

Source

pub fn toggle_heading(&mut self, level: u32)

Toggle a heading at the caret: if the block is already this heading level, revert it to a paragraph; otherwise convert it to this heading level. This gives the heading commands the same toggle feel as bold/italic/code — re-applying a heading a line already has turns it back into body text.

Source

pub fn toggle_blockquote(&mut self)

Toggle a block quote around the selection, or around the block at the caret — the toolbar’s Quote button.

Source

pub fn toggle_list(&mut self, ordered: bool)

Toggle a numbered (ordered) or bulleted list over the selection, or over the block at the caret — one op with the kind as a flag, the way toggle_heading takes its level, so a frontend needs no twig type to name the two buttons.

Pressing the other list’s button while in a list converts in place rather than nesting, so the pair reads as one three-state control (bulleted / numbered / neither) rather than two independent wrappers.

Source

pub fn task_checked_at_caret(&mut self) -> Option<bool>

Whether the list item at the caret carries a checkbox, and which way it faces — Some(true) ticked, Some(false) empty, None for a plain list item or no item at all. What a toolbar reads to light its checkbox button.

Source

pub fn task_checked_at(&mut self, offset: usize) -> Option<bool>

task_checked_at_caret for an arbitrary offset — what a frontend asks before deciding a click landed on a box.

Source

pub fn toggle_task_checked(&mut self)

Tick or untick the task item at the caret (the checkbox’s keyboard half). A no-op with a reported reason when the caret is in no task item — minting a box here is toggle_task_item’s job.

Source

pub fn toggle_task_at(&mut self, offset: usize)

Tick or untick the task item covering offset — what a click on a rendered checkbox is. Separate from the caret form because a click carries its own offset and must not first move the caret there: ticking a box three paragraphs away should not take the cursor with it.

Source

pub fn toggle_task_item(&mut self)

Give the list item at the caret a checkbox, or take its checkbox away — the gesture that converts between a plain bullet and a task. A new box arrives unticked.

Source

pub fn caret_in_table(&mut self) -> bool

Whether the caret is inside a table — what a frontend asks to enable or disable its table controls.

An HTML <table> still answers true: the caret really is in a table, and the reason the grid controls stay dark there is Capabilities::table, which is a fact about the document’s format rather than about the caret. A frontend needs both.

Source

pub fn table_insert_row(&mut self, below: bool)

Insert an empty row below (below) or above the caret’s row.

Source

pub fn table_delete_row(&mut self)

Delete the caret’s row (not the header, not the last body row).

Source

pub fn table_insert_column(&mut self, right: bool)

Insert an empty column right (right) or left of the caret’s column.

Source

pub fn table_delete_column(&mut self)

Delete the caret’s column (unless it is the only one).

Source

pub fn table_set_alignment(&mut self, alignment: Alignment)

Set the caret’s column to alignment.

Source

pub fn table_move_row(&mut self, down: bool)

Move the caret’s row one place down (down) or up, within the body rows.

Source

pub fn table_move_column(&mut self, right: bool)

Move the caret’s column one place right (right) or left.

Link the selection to destination — the toolbar’s Link button. With no selection it acts at the caret, which re-points a link the caret is already standing in (twig replaces an existing link’s destination and keeps its text) and otherwise spells a link that has no text of its own: an autolink (<https://x.dev>) where the destination is one, and [destination](destination) where it isn’t.

destination reaches twig raw. Escaping it is format knowledge and the two formats genuinely disagree — Markdown ends a destination at the first space and moves it into <…>, djot reads that <…> as part of the URL itself — so the side holding the document is the side that gets to spell it. A destination twig can’t carry at all (one with a newline) comes back as an error rather than a quietly rewritten URL.

Source

pub fn insert_image(&mut self, destination: &str, alt: &str)

Insert a block-level image at the caret: ![alt](destination). Any selection becomes the alt text (so “select a caption, insert image” labels it); with no selection, alt is used — empty for none. The caret lands just past the inserted image.

Both halves go through twig (insert_literal for the alt text, insert_image for the image), so neither is spelled here. That used to be a format!, and it was wrong the first time an app inserted a real filename: Markdown ends a destination at the first space, so ![](my photo.png) is not an image at all — and the fix is per-format, since moving into the <…> form is exactly wrong for Djot, where <…> becomes the URL itself.

Source

pub fn insert_media(&mut self, kind: MediaKind, destination: &str, alt: &str)

Insert a block-level image, video, or audio at the caret. The image case is insert_image; video and audio are spelled as HTML elements, which is the only spelling Markdown and Djot have for them:

<video src="clip.mp4" controls>alt</video>
<audio src="take.mp3" controls>alt</audio>

HTML rather than a ::video{…} directive deliberately. A directive means something only to an app that knows the vocabulary, so the document would read as literal punctuation everywhere else; <video> is what every other renderer already understands, and what leaf’s own reader picks back up through html_elements promotion (see [parse_extensions]).

The one-line spelling needs twig ≥ 2.5.1, which widened CommonMark’s HTML-block tag list to cover <video>/<audio>/<picture> under html_elements. Before that only the multi-line form parsed as a block at all, and this wrote three lines to work around it.

controls is always written: a player with no transport is a still frame the reader can’t do anything with. Any selection becomes the element’s fallback text, exactly as it becomes an image’s alt.

The same verbatim-insertion caveat as insert_image applies, and bites harder here: a " in destination closes the attribute. A frontend taking these from a file picker is fine; one taking them from free text should keep them tame.

Source

pub fn insert_thematic_break(&mut self)

Insert a thematic break at the caret — the toolbar’s Horizontal Rule button. Spelling and placement are both twig’s; leaf used to write --- itself, which was the Markdown spelling in a djot document too.

A rule is a block, so insert_thematic_break alone has nowhere to put one mid-paragraph and lands it after the caret’s whole block. To get a rule at the caret — the paragraph parted in two around it, which is what a rule button is understood to do — the paragraph is first divided with split_block and the rule then aimed at the first half. Aiming it at the offset split_block returns puts the rule after the second half instead, which is a rule in the right document and the wrong place.

Only a plain paragraph is split. Everywhere else the rule simply lands after the block, which is both twig’s own answer and the better one: splitting a fenced code block would leave two fences with a rule between them, and splitting a list item would mint an item nobody asked for on the way to a rule that lands after the list regardless. A table and a setext heading refuse the split outright, so they take the same path by themselves.

The destination of the link under the caret — what a Link prompt shows so ⌘K on an existing link edits its URL instead of asking for it again. None when the caret stands in no link.

An autolink carries no separate destination: its text is the URL, so that’s what comes back for one.

The destination of the link at off. link_destination_at_caret for a place the caret isn’t.

The offset form exists for the same reason footnote_at’s does: a frontend drawing a piece of the document somewhere else — a footnote’s text in a popover, say — has rows and runs but no caret in them, and still needs to know which of those runs a reader can follow.

Source

pub fn locate(&mut self, id: &str) -> Option<Landing>

Where the locator id lands in this document — the #v2 half of a chapter.dj#v2, resolved to the block it names. None when nothing here answers to it.

The other end of a link, and the reason this exists: without it a destination has only file granularity, so following a citation into a chapter drops the reader at the top of it to hunt for the verse. Which is also why it is a document query rather than a caret one — the document being asked is usually not the one the reader is in.

Three readings, tried in order, because the same #some-heading is written three ways across the formats leaf opens:

  1. A declared id, exactly as written: djot’s {#v1} on a block, and the auto-ids djot mints for its headings. The only exact answer, so it goes first — a document that says {#v1} has settled the question.
  2. A declared id, slugged. djot spells a heading’s auto-id Some-Heading-Here; nearly every tool that writes a link to one spells it #some-heading-here. Comparing slugs is what lets a link authored anywhere land on a djot heading.
  3. A heading’s text, slugged. Markdown has no ids at all — twig mints none and {#custom} is literal text in a Markdown heading — so for the format most vaults are written in, the heading’s own words are the only thing a fragment can name. This is the rule every Markdown renderer already follows, which is what makes #a-heading mean in diaryx what it means on the web.

Ties go to the earliest match, then to the widest: a duplicated id is the document’s mistake and the first one is the answer every anchor implementation gives, while preferring the wider span picks the section over the heading that opens it — more for a peek to show, same place to land.

Source

pub fn insert_footnote(&mut self)

Write a footnote at the caret — the toolbar’s Footnote button, and the one gesture in the footnote story that authors rather than follows.

Both halves go in as one twig edit: the [^1] where the caret is, and the [^1]: definition at the end of the document. Half a footnote is not a footnote — a bare reference with nothing defining it renders as literal brackets — so a single button that wrote only the reference would leave the author to hand-spell the other half in a document that had just stopped showing them what the first half meant. One edit also means one undo takes both back.

The definition’s body is left empty and the caret lands in it, which is the whole point of pressing the button: nobody wants a reference to a note they have not written yet. Getting back to where they were writing is footnote_definition_at_caret — the same return leg a reader following a reference already uses, so the author is left standing on the near end of a round trip that works.

A selection collapses to its end rather than being replaced: a reference annotates the words before it, so “select the claim, add a footnote” should mark that claim, not consume it.

Source

pub fn footnote_at_caret(&mut self) -> Option<FootnoteRef>

The footnote reference under the caret, resolved to the note it names. footnote_at at the caret’s offset.

Source

pub fn footnote_at(&mut self, off: usize) -> Option<FootnoteRef>

The footnote reference at off, resolved to the note it names — what a frontend shows when a reader activates a [^1].

A reference is not a link node, so link_destination_at_caret does not (and should not) answer for one: a link names a destination to leave for, a reference names a note that is already in this document. Following one is a move within the page, which is why this hands back an offset rather than something to open.

Offset-based rather than caret-only because the gesture that wants this most is the one that must not move the caret: a pointer hovering a [1] asks what note it names without disturbing where the reader was typing. The caret is just the offset a click already placed — footnote_at_caret passes it.

None when off stands in no reference. A reference whose note the document never defines is not None — it answers with the label it looked for and no text, which is what lets a frontend say so instead of silently doing nothing.

Source

pub fn footnote_definition_at_caret(&mut self) -> Option<FootnoteDef>

The footnote definition the caret stands in, and where the reference that names it is. footnote_definition_at at the caret’s offset.

Source

pub fn footnote_definition_at(&mut self, off: usize) -> Option<FootnoteDef>

The footnote definition spanning off, and where the reference that names it is — the return leg of footnote_at.

The mirror image, deliberately: the same gesture that takes a reader from [1] down to the note takes them from the note back up to [1], so following a footnote is a round trip rather than a fall. It needs no memory of how the reader arrived — the document says where the reference is — which is what makes it work for a reader who scrolled to the notes themselves, and what keeps it right after an edit moves either end.

None when off stands in no definition. A definition nothing cites is not None, for FootnoteRef’s reason in reverse: it answers with its label and no offset, so a frontend can say “nothing refers to this” rather than offer a jump that goes nowhere.

Source

pub fn image_destination_at_caret(&mut self) -> Option<String>

The destination of the image under the caret — what an image prompt shows so editing an existing image starts from its current URL instead of blank, the image analogue of link_destination_at_caret. None when the caret stands in no image. A caret resting just after a block image (its trailing stop) is still “in” it — the half-open span test excludes that offset, which is the intended precision: past the image is past it.

Source

pub fn code_language_at_caret(&mut self) -> Option<String>

The language of the fenced code block the caret stands in — what a language prompt shows so editing it starts from the current value rather than blank. None when the caret is in no code block, or in one whose fence carries no language (or an indented block, which has no fence).

Source

pub fn caret_in_fenced_code(&mut self) -> bool

Whether the caret stands in a fenced code block — the one a language prompt could edit. A frontend gates its “set language” affordance on this (an indented block, which can’t carry a language, reports false).

Source

pub fn set_code_language(&mut self, lang: &str)

Set (or clear, with "") the language of the fenced code block the caret is in — the prompt’s confirm. A no-op when the caret is in no fenced block, and a reported error for a language the format’s fence cannot carry.

twig rewrites the info string, so the fence’s own width — measured against a body neither side touches — is kept, and a language holding a space, a line end or the fence character is refused rather than written out to reparse as something else. Leaf used to splice over the info span itself and trim() the input, which handled the one bad case it had thought of.

Source

pub fn undo(&mut self)

Undo the last edit step (⌘Z / ^Z), putting the caret and selection back where they were when that step began.

Source

pub fn redo(&mut self)

Redo the last undone edit step (⇧⌘Z / ^Y), putting the caret and selection back where that step originally left them.

Source

pub fn save(&mut self)

Source

pub fn save_as(&mut self, path: PathBuf)

Save As: write the document to path and move it there — self.path becomes path, and every later Doc::save writes the new file. That’s what Save As means; a copy would leave the user editing a document whose name is no longer where their keystrokes go.

The move only happens if the bytes actually landed. A failed write leaves the path, dirty, and the disk watermark exactly as they were, with the same save failed: … status a failed Doc::save sets — the document must never come away believing it was saved.

An existing path is overwritten, and the caller is the one that knows whether to ask first: a Save As picker has already run that prompt, and a second confirmation from down here would be the same question twice.

format does not follow the new extension. The buffer is parsed as the format it was opened with, and re-reading it as another one is a conversion — a different, lossy operation that would throw away the undo history — not a rename. So notes.md saved as notes.dj holds Markdown in a .dj file, and format_name() keeps honestly saying markdown until it’s reopened.

Source

pub fn mark_saved(&mut self)

Re-base the document’s saved watermark to the current bytes: clears dirty, records source as the new clean state (so undoing back to here clears the flag again), and re-stamps the on-disk hash.

Doc::save/Doc::save_as call this after a write lands. It is also the hook a filesystem-free host calls itself once it has persisted Doc::source its own way (a browser download, localStorage, a backend PUT) — which is why it is public and touches no filesystem: the bytes are already where that host wants them, and this just tells the model they are safe.

Source

pub fn disk_state(&self) -> DiskState

What the file looks like now against the bytes leaf last read or wrote.

Reads the file and hashes it (see disk_hash for why it isn’t an mtime), so this is a filesystem round-trip, not a per-frame question — ask it when a window regains focus, on a timer, or before a save.

This only reports the file. Whether the document also has unsaved edits is dirty, and the interesting case is the conjunction: dirty plus DiskState::Changed means a save overwrites someone’s work and a Doc::reload discards the user’s. leaf-core deliberately won’t choose — it has no way to ask — so it hands a frontend both halves and lets it put the question to the person who can answer it.

Source

pub fn reload(&mut self)

Re-read the file and replace the document with what’s there — the other answer to a DiskState::Changed.

Discards unsaved changes and the undo history, unconditionally. It doesn’t check dirty first: a frontend that wants to protect unsaved work asks (dirty + Doc::disk_state) before calling this, and one reloading a clean document shouldn’t have to argue with a guard. The history goes because twig’s undo stack belongs to the buffer, and these are different bytes — replaying a step recorded against the old ones onto them would corrupt the document, and nothing here can honestly rebase it.

The caret keeps its byte offset, clamped to the new length; the selection is dropped. Anything cleverer would be a lie: leaf doesn’t know how the file changed, so it can’t know where the caret “still” is. Clamping keeps it where the user left it in the common case (a change further down the file, or none in the text they’re sitting in), and never puts it somewhere invalid. A selection has two such offsets and no such excuse — silently reinterpreting one over changed bytes would arm the next keystroke to delete something the user never selected.

Nothing is touched unless the whole reload succeeds; a failure leaves the document alone with a status.

Source

pub fn place_caret(&mut self, offset: usize, extend: bool)

Place the caret at byte offset (clamped to a char boundary), extending the selection when extend is set. The public form of move_to, for a frontend that hit-tests pixels straight to a source offset.

Examples found in repository?
examples/bench.rs (line 123)
34fn main() {
35    for kb in [10usize, 100, 1000] {
36        let src = body(kb * 1024);
37        println!("=== {} KB ===", src.len() / 1024);
38
39        let mut ed = Editor::new_str(&src, Format::Markdown).unwrap();
40        let nodes = ed.nodes().unwrap();
41        let map = wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None);
42        println!("  ({} AST nodes, {} map rows)", nodes.len(), map.rows.len());
43
44        println!("  -- per edit (unavoidable today) --");
45        time("twig edit_range (reparse)", 5, || {
46            ed.edit_range(src.len() / 2, src.len() / 2, "x").is_ok()
47        });
48        time("twig nodes() FFI marshal", 5, || ed.nodes().unwrap().len());
49        time("wysiwyg::build", 5, || {
50            wysiwyg::build(&nodes, &src, None, false, &HashMap::new(), None)
51                .rows
52                .len()
53        });
54        {
55            // The incremental path with a warm cache and nothing changed: the
56            // floor cost the block cache adds even on a pure repaint — hash every
57            // block, clone every reused row, recollect stops. No subtree is
58            // marshalled (every block hits). The real keystroke win shows up in
59            // "Doc::insert + rebuild" below, which re-marshals only the edited
60            // block and reuses the rest.
61            let mut cache = wysiwyg::BlockCache::default();
62            let top = ed.child_spans(None).unwrap();
63            let _ = wysiwyg::build_cached(
64                &top,
65                &src,
66                None,
67                false,
68                &HashMap::new(),
69                None,
70                &mut cache,
71                |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
72            );
73            time("wysiwyg::build_cached (all reused)", 5, || {
74                let top = ed.child_spans(None).unwrap();
75                wysiwyg::build_cached(
76                    &top,
77                    &src,
78                    None,
79                    false,
80                    &HashMap::new(),
81                    None,
82                    &mut cache,
83                    |id| ed.subtree(twig::NodeId(id)).unwrap_or_default(),
84                )
85                .rows
86                .len()
87            });
88        }
89
90        println!("  -- claimed hot, actually noise --");
91        time("twig source_str() (full copy)", 5, || {
92            ed.source_str().unwrap().len()
93        });
94        let clean = src.clone();
95        time("dirty compare (full cmp)", 5, || src == clean);
96
97        println!("  -- what the GUI adds on a cache miss --");
98        time("clone every row's glyphs", 5, || {
99            map.rows
100                .iter()
101                .map(|r| r.glyphs.clone())
102                .collect::<Vec<_>>()
103                .len()
104        });
105        time("hash every glyph (cache key?)", 5, || {
106            let mut n = 0u64;
107            for r in &map.rows {
108                let mut h = std::collections::hash_map::DefaultHasher::new();
109                for g in &r.glyphs {
110                    g.ch.hash(&mut h);
111                }
112                n ^= h.finish();
113            }
114            n
115        });
116
117        println!("  -- the whole path, as a frontend calls it --");
118        let mut p = std::env::temp_dir();
119        p.push(format!("leaf_bench_{kb}.md"));
120        std::fs::write(&p, &src).unwrap();
121        let mut d = Doc::open(p).unwrap();
122        d.view = View::Wysiwyg;
123        d.place_caret(src.len() / 2, false);
124        d.build_visual_unwrapped();
125        time("build_visual (cached: a repaint)", 200, || {
126            d.build_visual_unwrapped()
127        });
128        time("Doc::insert + rebuild (a keystroke)", 5, || {
129            d.insert("x");
130            d.build_visual_unwrapped();
131        });
132        println!();
133    }
134}
Source

pub fn select_all(&mut self)

Select the whole document (⌘A / Ctrl+A) — everything reachable in the active view, so in WYSIWYG it starts below hidden frontmatter (copy won’t grab the metadata) while the source view still selects the literal whole.

Source

pub fn select_word_at(&mut self, offset: usize)

Select the word (or whitespace / punctuation run) at offset — the double-click gesture. Anchors on the run’s start with the caret at its end so a following Shift-motion extends from the far edge.

Source

pub fn select_block_at(&mut self, offset: usize)

Select the whole enclosing text block (paragraph, heading, list item’s text…) at offset — the triple-click gesture. Reads the range straight from the AST (twig’s content_span), so it selects the entire logical paragraph even when that paragraph soft-wraps across several visual rows — where a visual-row-based select breaks down, because one source offset at a wrap boundary belongs to two rows at once.

Source

pub fn move_left(&mut self, extend: bool)

Source

pub fn move_right(&mut self, extend: bool)

Source

pub fn move_word_left(&mut self, extend: bool)

Move to the start of the previous word (⌥← / Ctrl+←).

Source

pub fn move_word_right(&mut self, extend: bool)

Move to the end of the next word (⌥→ / Ctrl+→).

Source

pub fn move_up(&mut self, extend: bool)

Source

pub fn move_down(&mut self, extend: bool)

Source

pub fn move_home(&mut self, extend: bool)

Home: to the first character on the line, or to column 0 when the caret is already on it — the two-press toggle every editor spells this way. The indentation is somewhere the caret has to be able to reach and almost never where a reader is headed, so it costs the second press.

Source

pub fn move_end(&mut self, extend: bool)

End: to the end of the line.

Source

pub fn cell_hop(&mut self, forward: bool) -> bool

Hop to the next (Tab) or previous (Shift+Tab) table cell, landing with the cell’s whole content selected (see Self::select_cell). Returns false when the caret isn’t in a table, or is already in the last/first cell — the frontend then does whatever Tab normally does (indent), so Tab keeps its meaning everywhere else.

Source

pub fn cell_move_vertical(&mut self, down: bool) -> bool

Move the caret to the cell directly above (down == false) or below in the same column, landing with the cell’s whole content selected (see Self::select_cell). Returns false at the grid’s top/bottom edge (or when the caret isn’t in a table), so the frontend can fall through — the vertical counterpart of [cell_hop].

A ragged row that is short a column clamps to its last cell, so Down never falls out of the table over a gap the row above happened to have.

Source

pub fn cell_tab(&mut self, forward: bool) -> bool

Tab / Shift+Tab inside a table. Tab steps to the next cell, appending a fresh row and entering it when it runs off the last one; Shift+Tab steps back and simply stays put at the very first cell. false when the caret isn’t in a table.

Source

pub fn cell_return(&mut self) -> bool

Return inside a table: drop to the cell below in the same column, appending a new row when the caret is already in the last one. false when the caret isn’t in a table, so the frontend inserts a newline.

Source

pub fn cell_line_break(&mut self) -> bool

Shift+Return inside a table: insert a hard line break within the current cell, via twig’s insert_line_break. false when the caret isn’t in a table, so the frontend inserts an ordinary line break.

A table row is a single source line, so the newline-spelled hard break can’t live in a cell. twig spells the in-cell break the format’s way (<br> for Markdown) and reparses it as a semantic hard_break, so the break round-trips as structure the renderer reads back as a line — not the opaque raw HTML the old raw-splice left behind.

Djot has no idiomatic in-cell break, so twig refuses it (UnsupportedFormat) rather than emit a <br> that any other djot reader would render as the literal text <br>. The gesture is still consumed there — returning false would let the frontend insert a real newline, which splits the one-line row — it just leaves the cell unchanged and says so on the status line. A rollback (EditConflict) is swallowed the same.

Which formats refuse is Capabilities::cell_line_break, and the two have to be read together: djot is not the only false, and naming it in the message was already a guess that HTML — which spells the break as its own <br> — would have made wrong.

Source

pub fn move_doc_start(&mut self, extend: bool)

Move the caret to the very start of the document (⌘↑ on macOS, Ctrl+Home on Windows/Linux).

Source

pub fn move_doc_end(&mut self, extend: bool)

Move the caret to the very end of the document (⌘↓ on macOS, Ctrl+End on Windows/Linux).

Source

pub fn click(&mut self, row: usize, col: usize, extend: bool)

Point the caret at the body cell (row, col) the mouse landed on — col being a cell of the terminal grid, which is what a display column is. A click on the far cell of a wide character lands at that character’s start; the mapping’s own doc-comments carry the rule.

Source

pub fn follow_caret(&mut self, caret_row: usize, height: usize, rows: usize)

Settle scroll for a frame about to be drawn: follow the caret onto the screen if it has moved since the last frame, and never scroll past the last of rows.

Only if it has moved — that’s the whole point. Revealing the caret on every frame ties the viewport to it, and a scroll wheel that fights the caret for the viewport loses: the view snaps back the instant it tries to pass the caret’s row, so the document can’t be scrolled beyond what’s already on screen. A caret move is the frontend’s cue to follow; a scroll with the caret sitting still is the reader’s cue to leave it alone.

Source

pub fn caret_pos(&self) -> (usize, usize)

The caret’s screen position (row, col) in the active view’s grid, with col a display column: the cell to draw the caret in, which on a line of 你好 or emoji is not the count of characters before it.

Auto Trait Implementations§

§

impl !Send for Doc

§

impl !Sync for Doc

§

impl Freeze for Doc

§

impl RefUnwindSafe for Doc

§

impl Unpin for Doc

§

impl UnsafeUnpin for Doc

§

impl UnwindSafe for Doc

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.