Skip to main content

Editor

Struct Editor 

Source
pub struct Editor { /* private fields */ }
Expand description

A span-splice editor over a document: applies lossless, in-place edits and reparses after each one, so node addressing stays valid as the document evolves. Every op is addressed by a locator — a dot-separated index path ("0.3.1") or a selector that must match exactly one node (heading("Status")). A failed edit leaves the document unchanged.

Implementations§

Source§

impl Editor

Source

pub fn new(input: &[u8], format: Format) -> Result<Self, Error>

Create an editor over a private copy of input, parsed as format with default options.

Source

pub fn new_str(input: &str, format: Format) -> Result<Self, Error>

Source

pub fn new_ext( input: &[u8], format: Format, extensions: MarkdownExtensions, ) -> Result<Self, Error>

Like Editor::new, plus Markdown extensions to enable (ignored for other formats). The editor reparses with these after every edit, so a directive-bearing document stays parseable — needed before Editor::filter can match directive[...] selectors.

Source

pub fn replace(&mut self, locator: &str, text: &str) -> Result<(), Error>

Replace the whole source of the located node with text.

Source

pub fn replace_content( &mut self, locator: &str, text: &str, ) -> Result<(), Error>

Replace the interior (between-delimiters content) of the located container.

Source

pub fn insert_before(&mut self, locator: &str, text: &str) -> Result<(), Error>

Insert text immediately before the located node.

Source

pub fn insert_after(&mut self, locator: &str, text: &str) -> Result<(), Error>

Insert text immediately after the located node.

Source

pub fn insert_child( &mut self, locator: &str, index: usize, text: &str, ) -> Result<(), Error>

Insert text as the index-th child of the located container (an index at or past the child count appends).

Source

pub fn delete(&mut self, locator: &str) -> Result<(), Error>

Delete the located node (removes exactly its span; no whitespace cleanup).

Source

pub fn delete_smart(&mut self, locator: &str) -> Result<(), Error>

Delete the located node, tidying surrounding blank lines for a whole-line (block) node; an inline node degrades to the exact delete.

Source

pub fn unwrap_node(&mut self, locator: &str) -> Result<(), Error>

Unwrap the located node: replace it with its interior (drop the wrapper, keep the children) — e.g. peel a :::vis{...} container. A node with no interior (a leaf, or an empty container) is removed.

Source

pub fn filter( &mut self, drop: &str, keep: Option<&str>, unwrap_kept: bool, ) -> Result<(), Error>

Prune the document in place: remove every node matching the drop selector except those also matching keep (None spares nothing), then — if unwrap_kept — unwrap the survivors. Read the result with Editor::source.

Source

pub fn source(&mut self) -> Result<Vec<u8>, Error>

The editor’s current (edited) source bytes.

Source

pub fn source_str(&mut self) -> Result<String, Error>

The editor’s current source bytes as a UTF-8 string.

Source

pub fn ast_json(&mut self) -> Result<Vec<u8>, Error>

Encode the editor’s current tree as pretty-printed JSON — the live counterpart of Document::ast_json, for inspecting between edits.

Source

pub fn query(&mut self, selector: &str) -> Result<Vec<QueryMatch>, Error>

Resolve a selector against the editor’s current tree — the live counterpart of Document::query.

Source

pub fn edit_range( &mut self, start: usize, end: usize, text: &str, ) -> Result<Change, Error>

Splice [start, end) of the current source with text, reparse, and return the Change the edit produced — the offset-addressed primitive a caret editor is built on: a keystroke is edit_range(c, c, "x"), backspace edit_range(c - 1, c, ""), a selection replace edit_range(a, b, s). start <= end <= source length, else Error::InvalidArgument. A reparse-breaking edit is rolled back and returns Error::EditConflict, leaving the document untouched.

Source

pub fn last_change(&mut self) -> Option<Change>

The byte effect of the last successful edit — including the locator ops (Editor::replace, Editor::delete_smart, …), so any edit can re-anchor a caret without re-diffing. None before the first successful edit. (A multi-splice op such as Editor::filter reports only its final splice.)

Source

pub fn undo(&mut self) -> Result<Option<Change>, Error>

Undo the last edit step, restoring the previous source and reparsing. Returns the Change the undo produced (current → restored) so a caret can re-anchor, or None when there’s nothing to undo. History accrues across every successful edit that funnels through the splice primitive.

Source

pub fn redo(&mut self) -> Result<Option<Change>, Error>

Redo the most recently undone edit step; the inverse of Editor::undo. Returns None when the redo stack is empty (nothing undone, or a fresh edit has invalidated it).

Source

pub fn coalesce_last_undo(&mut self) -> Result<(), Error>

Fold the most recent edit into the undo step before it, so a caret editor can coalesce a run of keystrokes into a single undo. Call right after an edit_range that continues a run (same kind, no intervening caret move); a no-op unless there are at least two steps to merge.

Source

pub fn revision(&mut self) -> u64

A monotonic change token, bumped once per successful mutation of the document (every edit and every undo/redo). Never decreases and never repeats for the life of the editor; the initial parse is revision 0. Equal revision means a byte-identical document, so it can key a cache instead of hand-tracking “did anything change?”.

Source

pub fn dirty_range(&mut self) -> Option<Range<usize>>

The cumulative dirty byte range since the last Editor::clear_dirty (or since the editor was created) — the union of every mutation’s byte effect over that window, in current source coordinates — or None when the document is clean relative to the last clear.

The incremental-rebuild companion to Editor::revision: revision says whether a cached view (glyph rows, syntax spans) needs rebuilding, this says which bytes changed, so a consumer rebuilds only the affected part instead of the whole document. A single conservative interval: it always covers every changed byte and may over-cover the gap between edits to disjoint regions, but never under-covers.

It reports where bytes differ — exact, because twig splices losslessly and never reflows untouched bytes — not where the parse differs. An edit can reinterpret bytes outside the range (opening a code fence, a # promoting a paragraph to a heading), so a consumer rebuilding structure from it should widen the range to the enclosing block(s) itself (e.g. via Editor::node_at on each end). Typical loop: on a repaint, if Editor::revision moved, read this range, rebuild the rows it (widened) covers, then call Editor::clear_dirty.

Source

pub fn clear_dirty(&mut self)

Acknowledge the current dirty range: mark the document clean so a later Editor::dirty_range reports only mutations made after this call. Call it once you’ve consumed the range (rebuilt the affected view). Leaves the document, Editor::revision, and Editor::last_change untouched.

Source

pub fn set_caret_blob(&mut self, blob: &[u8]) -> Result<(), Error>

Attach an opaque, caller-owned blob (e.g. a serialized caret/selection) to the editor’s current document state. Twig copies the bytes and never interprets them; it only carries them through the undo history so Editor::undo/Editor::redo hand back the caret matching the restored source (via Editor::caret_blob). Set it with the pre-edit caret before an edit so the retired undo step captures it. An empty blob clears the current caret.

Source

pub fn caret_blob(&mut self) -> Result<Vec<u8>, Error>

The opaque caret blob for the editor’s current document state (see Editor::set_caret_blob). After Editor::undo/Editor::redo this is the restored state’s caret; after an edit it is empty until set again. Returns an owned copy, so it outlives the next edit.

Source

pub fn nodes(&mut self) -> Result<Vec<FlatNode>, Error>

Snapshot the current tree as a flat FlatNode array (the JSON-free read path for a renderer), indexed so nodes[i].id == NodeId(i). Walk it via the parent/first_child/next_sibling links; the root is the node whose parent is None.

Source

pub fn child_spans( &mut self, node: Option<NodeId>, ) -> Result<Vec<QueryMatch>, Error>

The direct children of node as QueryMatches (id, span, kind) — None enumerates the document root’s children (the top-level blocks). The cheap top-level enumeration an incremental renderer walks to decide which blocks changed, without marshalling the whole arena; pair it with Editor::subtree to then re-marshal only those that did. A childless node yields an empty vec.

Source

pub fn subtree(&mut self, node: NodeId) -> Result<Vec<FlatNode>, Error>

Snapshot the subtree rooted at node as a self-contained FlatNode array with local ids: array[0] is the root, every link is an index into the returned vec (or None), and spans stay absolute. The incremental-render companion to Editor::nodes — re-marshal one edited block’s subtree instead of the whole document. The root’s parent and next_sibling are None, so a walk from index 0 stays inside the subtree. Error::InvalidArgument if node is out of range.

Source

pub fn node_at(&mut self, offset: usize) -> Result<Option<QueryMatch>, Error>

The deepest node whose span contains byte offset (with offset equal to the source length treated as inside the root) — mouse hit-testing and cursor context. Ok(None) if no node covers the offset; Error::InvalidArgument if offset exceeds the source length.

Source

pub fn ancestors_at(&mut self, offset: usize) -> Result<Vec<QueryMatch>, Error>

The chain of nodes containing byte offset, root-first down to the deepest (the node Editor::node_at returns) — the ancestor path for a breadcrumb or context-scoped edit. Empty if no node covers the offset.

Source

pub fn wrap_range( &mut self, start: usize, end: usize, kind: InlineKind, ) -> Result<Change, Error>

Wrap [start, end) with kind’s delimiters — the unconditional half of the inline toolbar (always adds a mark; *word***word** stacks). Error::UnsupportedFormat if the document’s format can’t spell kind (e.g. a Markdown InlineKind::Mark); Error::InvalidArgument for a bad range; Error::EditConflict if the result doesn’t reparse.

Source

pub fn toggle_inline( &mut self, start: usize, end: usize, kind: InlineKind, ) -> Result<Change, Error>

Toggle kind over [start, end): remove the mark if the range already is a node of kind (its whole span or its rendered interior), else wrap it — a rich editor’s Cmd-B. Same error rules as Editor::wrap_range.

Source

pub fn set_block( &mut self, offset: usize, kind: BlockKind, ) -> Result<Change, Error>

Convert the innermost heading/paragraph covering byte offset to kind, rewriting its leading marker while keeping its inline content (the toolbar’s H1…H6 / Body switch). Djot and Markdown only, else Error::UnsupportedFormat; Error::NotFound if no heading/paragraph covers offset; Error::InvalidArgument for a heading level outside 1–6.

Source

pub fn toggle_block_container( &mut self, start: usize, end: usize, kind: BlockContainerKind, ) -> Result<Change, Error>

Toggle a block container over the blocks [start, end) covers — the toolbar’s Quote / Bulleted list / Numbered list buttons. Djot and Markdown only, else Error::UnsupportedFormat; Error::NotFound if the range covers no block; Error::InvalidArgument for a bad range.

The range widens to whole lines of the blocks it touches (you cannot quote half a paragraph), and the prefix lands at column 0, so a container wraps the outermost structure on those lines.

Whether this adds or removes is decided from the AST — the ancestors of start — not by looking for a > in the source. It removes the container only when the range covers every block that container holds, and then only one level (> > a> a). A partly covered container nests instead, since removing it would drag its uncovered siblings out with it: selecting the first paragraph of > a\n>\n> b\n gives > > a\n>\n> b\n. Toggling one list kind while inside the other converts in place (- a1. a) rather than nesting.

Each covered block becomes one item, so an ordered list numbers a multi-block range 1., 2., 3.… Removing a list inserts a blank line between items that lacked one, keeping them separate blocks (a tight - a\n- b\n stripped bare would be a single two-line paragraph).

Source

pub fn renumber_ordered_lists(&mut self, offset: usize) -> Result<(), Error>

Renumber the ordered list at byte offset so its markers run 1, 2, 3, …, each nesting level restarting at 1 — the numbering a caret editor keeps as items are inserted, deleted, and nested, where a raw splice leaves the source numbers stale (1. 2. 2. 3.). Djot and Markdown; the display of an ordered list is renumbered by any CommonMark renderer regardless, so this is source hygiene, not a render fix.

Error::NotFound when offset is not inside an ordered list. When the numbering is already sequential this is a no-op that still returns Ok — the source is left byte-for-byte unchanged. The Change is not returned because a no-op has none; re-read Editor::source_str for the result.

Source

pub fn table_insert_row( &mut self, offset: usize, below: bool, ) -> Result<(), Error>

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

Source

pub fn table_delete_row(&mut self, offset: usize) -> Result<(), Error>

Delete the caret’s row. Error::NotEditable for the header row or the last remaining body row.

Source

pub fn table_insert_column( &mut self, offset: usize, right: bool, ) -> Result<(), Error>

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

Source

pub fn table_delete_column(&mut self, offset: usize) -> Result<(), Error>

Delete the caret’s column. Error::NotEditable when it is the only one.

Source

pub fn table_set_alignment( &mut self, offset: usize, alignment: Alignment, ) -> Result<(), Error>

Set the caret’s column to alignment.

Source

pub fn table_move_row(&mut self, offset: usize, down: bool) -> Result<(), Error>

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

Source

pub fn table_move_column( &mut self, offset: usize, right: bool, ) -> Result<(), Error>

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

Link [start, end) to destination[text](destination). Djot and Markdown only, else Error::UnsupportedFormat; Error::InvalidArgument for a bad range or a destination containing a newline (neither format can carry one, and quietly rewriting the URL would be worse than refusing).

An existing link covering the range has its destination replaced and its text kept, so re-linking fixes a URL instead of nesting [[t](a)](b); to unlink, use Editor::unwrap_node.

A range inside an existing autolink (<https://x.dev>) re-points it the same way, but there is no text to keep — an autolink’s text is its destination — so the node is replaced whole, respelled canonically for the new destination. This covers a caret and any selection the autolink contains, including one covering it exactly: an autolink’s URL is not editable text, so no part of it can host a [, and “link half this URL” has no spelling. A caret inside both an autolink and a link ([<https://x.dev>](d)) re-points the link, whose text is separable from its destination and so survives.

A selection starting or ending strictly inside an autolink without being contained by it — running from ordinary text into the middle of a URL — is refused with [Status::NotEditable]: half of it is real text, so there is nothing to re-point, and any splice would rewrite the URL. A selection that contains an autolink whole is unaffected — it splices at the edges and wraps as usual.

A link with no text — an empty range, or re-pointing an existing [](old) — is spelled canonically for the destination given, never as [](destination): a childless link has nothing to render, so consumers fall back to showing the destination and a caret has nowhere to sit. A destination the format can autolink (an absolute URL or an email, by that format’s own rules) yields <destination>; anything else yields [destination](destination), the destination doubling as the text so it stays visible and editable. Which destinations autolink is not the caller’s to guess — <foo> is raw HTML in Markdown, a relative path goes literal in both, and the formats disagree (<mailto:a@b.dev> is a url in Markdown, an email in Djot), so each is asked its own parser.

The destination is escaped for the format, so a ) or a space in it cannot break the markup — and the two formats genuinely differ: Markdown ends a destination at the first space ([t](a b) is not a link at all) so whitespace moves it into the <…> form, while Djot takes spaces literally and would read <a b> as the URL itself.

Source

pub fn insert_image( &mut self, start: usize, end: usize, destination: &str, ) -> Result<Change, Error>

Spell [start, end) as an image pointing at destination![alt](destination), the selected source becoming the alt text.

The destination is escaped exactly as insert_link escapes one, because it is the same grammar production: Markdown moves a destination holding whitespace into the <…> form, Djot leaves it bare because <…> there would read as the URL itself. That is the reason this exists rather than being a format! at the call site — ![](my file.png) is not an image in Markdown at all, and no caller can fix that without reproducing twig’s per-format escape table.

Two ways it is simpler than a link. An empty range stays empty: ![](destination) is a perfectly good image, where the childless [](destination) that insert_link works to avoid has nothing to render or put a caret in. And there is no autolink or re-point reasoning — an image has no bare-URL spelling, and re-pointing an existing one is a read of its destination plus an insert, above this op.

Returns Error::InvalidArgument for a destination holding a newline and Error::UnsupportedFormat for a parse-only format (XML, HTML).

Source

pub fn insert_literal( &mut self, offset: usize, text: &str, ) -> Result<Change, Error>

Insert text at offset as a literal run: every byte the format reads as markup is backslash-escaped so the run reparses as exactly text — a typed *, # or ` stays that character rather than opening emphasis, a heading or a code span. This is the inverse of serialization (which writes an already-parsed run verbatim): it is what a WYSIWYG surface calls so that keyboard input can never mint markup, leaving formatting to explicit commands.

The escaping is positional and per-format, and neither is the caller’s to reproduce: inline specials (*, `, [, <…) are escaped anywhere on the line, while block markers (#, >, -…) are escaped only where offset sits in its line’s leading whitespace — so an inserted “5 - 3” keeps its - but “- item” at column zero does not become a bullet. An embedded newline in text re-enters that line-start zone.

Two constructs a byte-alphabet cannot reach are left as typed: a GFM bare-URL autolink (https://x.com, with no delimiter to escape) and an ordered-list marker (1., special only after a digit run). Returns Error::UnsupportedFormat for a parse-only format (XML, HTML) and Error::InvalidArgument when offset is past the source.

Source

pub fn insert_line_break(&mut self, offset: usize) -> Result<Change, Error>

Insert a hard line break inside a table cell at offset, spelled the format’s way (<br> for Markdown). A table row is one source line, so the ordinary newline-based hard break can’t appear there; the spliced <br> reparses as a semantic hard_break node — not opaque raw HTML — so the break reads back as structure. Like the other gestures it leans on the splice+reparse+rollback backstop: a break that would no longer parse as the same table yields Error::EditConflict and changes nothing.

Returns Error::UnsupportedFormat for a format with no in-cell break spelling — djot (no idiomatic in-cell break), HTML and XML (parse-only); Error::NotFound when offset is not inside a table cell (only the in-cell gesture is spelled today); and Error::InvalidArgument when offset is past the source.

Trait Implementations§

Source§

impl Debug for Editor

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Drop for Editor

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

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.