Skip to main content

TextDocument

Struct TextDocument 

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

A rich text document.

Owns the backend (database, event hub, undo/redo manager) and provides document-level operations. All cursor-based editing goes through TextCursor, obtained via cursor() or cursor_at().

Internally uses Arc<Mutex<...>> so that multiple TextCursors can coexist and edit concurrently. Cloning a TextDocument creates a new handle to the same underlying document (like Qt’s implicit sharing).

Implementations§

Source§

impl TextDocument

This impl block contains no public items.

Test-only accessor for the underlying rope-backed store. Not part of the stable public API.

Source§

impl TextDocument

Source

pub fn new() -> Self

Create a new, empty document.

§Panics

Panics if the database context cannot be created (e.g. filesystem error). Use TextDocument::try_new for a fallible alternative.

Source

pub fn try_new() -> Result<Self>

Create a new, empty document, returning an error on failure.

Source

pub fn set_plain_text(&self, text: &str) -> Result<()>

Replace the entire document with plain text. Clears undo history.

Source

pub fn to_plain_text(&self) -> Result<String>

Export the entire document as plain text, in reading order.

This is the human-readable view: prose only. Embedded objects (a table) contribute their content but not the U+FFFC anchor the document holds where they sit — which is what you want for a cat-style export, and is why the crate’s fast path bails the moment a table exists.

Do not compute offsets from this string. It is deliberately not character-for-character the text a search runs against: that text carries the object anchors, so a position taken here is short by two characters per preceding table. For an addressable view — one whose offsets find_all and replace_text agree with — use djot_to_plain_text, which is pinned to match the document’s own search text exactly.

The two are allowed to differ in that one respect and no other; in particular they agree on order. They did not always: this export used to hoist every blockquote’s prose to the end of the document ("> a0\n\na" came back as "a\na0"), because it concatenated frames in creation order instead of sorting all blocks by document_position. See plain_text_order_tests.

Source

pub fn set_markdown( &self, markdown: &str, ) -> Result<Operation<MarkdownImportResult>>

Replace the entire document with Markdown. Clears undo history.

This is a long operation. Returns a typed Operation handle.

Source

pub fn to_markdown(&self) -> Result<String>

Export the entire document as Markdown.

Source

pub fn set_djot(&self, djot: &str) -> Result<Operation<DjotImportResult>>

Replace the entire document with djot markup. Clears undo history.

This is a long operation. Returns a typed Operation handle.

Source

pub fn set_djot_with_options( &self, djot: &str, options: DjotImportOptions, ) -> Result<Operation<DjotImportResult>>

Replace the entire document with djot markup, selecting which optional block attributes (alignment, line height, direction, non-breakable lines, background color) are applied via options. Clears undo history.

This is a long operation. Returns a typed Operation handle.

Source

pub fn set_djot_sync(&self, djot: &str) -> Result<DjotImportResult>

Replace the entire document with djot markup, synchronously, on the calling thread. Clears undo history.

This is the right call for loading a document’s initial content — the case where the caller is going to block for the result anyway. set_djot starts a long operation: it spawns a thread, and the caller then blocks in Operation::wait until that thread publishes. That round trip is pure overhead when there is no frame loop to keep responsive, and it does not shrink with the input — an empty document costs the same thread spawn and hand-off as a full one. Loading N documents in a loop paid it N times.

Prefer set_djot when the import is genuinely long and the caller must stay responsive (it reports progress and can be cancelled); prefer this when the caller just wants the content in.

Observationally equivalent to set_djot(..).wait() — same import, same DocumentReset, same cache/block bookkeeping — except that, having no operation, it emits no LongOperation* events and cannot be cancelled.

Source

pub fn set_djot_sync_with_options( &self, djot: &str, options: DjotImportOptions, ) -> Result<DjotImportResult>

As set_djot_sync, selecting which optional block attributes are applied via options.

Source

pub fn to_djot(&self) -> Result<String>

Export the entire document as djot markup.

Source

pub fn to_djot_with_options(&self, options: DjotExportOptions) -> Result<String>

Export the entire document as djot markup, selecting which optional block attributes (alignment, line height, direction, non-breakable lines, background color) are emitted via options.

Source

pub fn set_html(&self, html: &str) -> Result<Operation<HtmlImportResult>>

Replace the entire document with HTML. Clears undo history.

This is a long operation. Returns a typed Operation handle.

Source

pub fn to_html(&self) -> Result<String>

Export the entire document as HTML.

Source

pub fn to_latex( &self, document_class: &str, include_preamble: bool, ) -> Result<String>

Export the entire document as LaTeX.

Source

pub fn to_docx(&self, output_path: &str) -> Result<Operation<DocxExportResult>>

Export the entire document as DOCX to a file path.

This is a long operation. Returns a typed Operation handle.

Source

pub fn to_docx_with_options( &self, output_path: &str, options: DocxExportOptions, ) -> Result<Operation<DocxExportResult>>

As to_docx, but with page geometry + base typography overrides — a manuscript style (page size, margins, body font, line spacing, first-line indent, alignment, and an optional page-number header). Per-block RTL is emitted automatically from each block’s own direction, independent of these options.

Source

pub fn to_epub(&self, output_path: &str) -> Result<Operation<EpubExportResult>>

Export the entire document as an EPUB 3 file to a file path.

This is a long operation. Returns a typed Operation handle.

Source

pub fn to_epub_with_options( &self, output_path: &str, options: EpubExportOptions, ) -> Result<Operation<EpubExportResult>>

As to_epub, but with book-level metadata (title, author, language, reading direction). The document is split into chapters at the shallowest heading level present (e.g. every top-level # Chapter heading) — see EpubExportOptions for details.

Source

pub fn to_pdf( &self, output_path: &str, options: PdfExportOptions, ) -> Result<Operation<PdfExportResult>>

Export the entire document as a PDF file, using the given options (page geometry, typography, embedded font bytes, base language/direction).

This is a long operation. Returns a typed Operation handle.

Requires the pdf cargo feature on text-document (which forwards to frontend’s and document_io’s own pdf features). If it was not enabled at compile time, this returns Err(DocumentError::Unsupported(..)) immediately rather than attempting the export — no #[cfg] is needed at the call site either way.

Source

pub fn to_pdf_with_options( &self, _output_path: &str, _options: PdfExportOptions, ) -> Result<Operation<PdfExportResult>>

As to_pdf, when the pdf cargo feature was not enabled at compile time — returns DocumentError::Unsupported immediately, without starting an operation or touching the backend at all.

Source

pub fn clear(&self) -> Result<()>

Clear all document content and reset to an empty state.

Source

pub fn cursor(&self) -> TextCursor

Create a cursor at position 0.

Source

pub fn cursor_at(&self, position: usize) -> TextCursor

Create a cursor at the given position. If position falls inside an extended grapheme cluster (decomposed accents, ZWJ emoji, skin-tone sequences, flag pairs), the cursor snaps forward to the end of the containing cluster so subsequent NextCharacter/PreviousCharacter round-trips remain identity.

Source

pub fn stats(&self) -> DocumentStats

Get document statistics. O(1) — reads cached values.

Source

pub fn character_count(&self) -> usize

Get the total character count. O(1) — reads cached value.

Source

pub fn block_count(&self) -> usize

Get the number of blocks (paragraphs). O(1) — reads cached value.

Source

pub fn is_empty(&self) -> bool

Returns true if the document has no text content.

Source

pub fn text_at(&self, position: usize, length: usize) -> Result<String>

Get text at a position for a given length.

Source

pub fn find_element_at_position( &self, position: usize, ) -> Option<(u64, usize, usize)>

Find the inline segment containing position and return its stable element id (synthesized from (block_id, byte_start) via common::format_runs::synth_element_id) together with the segment’s absolute start position and the character offset of position within the segment. Used by accessibility layers to convert a document-absolute character position into the (element_id, character_index_in_run) coordinate space AccessKit’s TextPosition expects.

Returns None when the position is outside the document. Returns the element at position position - 1 when position falls exactly on an element boundary, matching the “cursor belongs to the preceding element at a boundary” convention used throughout text-document.

Source

pub fn block_at(&self, position: usize) -> Result<BlockInfo>

Get info about the block at a position. O(log n).

Source

pub fn block_format_at(&self, position: usize) -> Result<BlockFormat>

Get the block format at a position.

Source

pub fn flow(&self) -> Vec<FlowElement>

Walk the main frame’s visual flow in document order.

Returns the top-level flow elements — blocks, tables, and sub-frames — in the order defined by the main frame’s child_order. Table cell contents are NOT included here; access them through TextTableCell::blocks().

This is the primary entry point for layout initialization.

Source

pub fn block_by_id(&self, block_id: usize) -> Option<TextBlock>

Get a read-only handle to a block by its entity ID.

Entity IDs are stable across insertions and deletions. Returns None if no block with this ID exists.

Source

pub fn snapshot_block_at_position( &self, position: usize, ) -> Option<BlockSnapshot>

Build a single BlockSnapshot for the block at the given position.

This is O(k) where k = format runs + image anchors in that block, compared to snapshot_flow() which is O(n) over the entire document. Use for incremental layout updates after single-block edits.

Source

pub fn snapshot_block_at_position_without_highlights( &self, position: usize, ) -> Option<BlockSnapshot>

Like snapshot_block_at_position but with no highlights applied — base fragments and empty paint_highlights, regardless of the active sessions. Used by the incremental relayout path of a view that has opted out of highlights.

Source

pub fn snapshot_block_at_position_masked( &self, position: usize, mask: &HighlightMask, ) -> Option<BlockSnapshot>

Like snapshot_block_at_position but rendering only the sessions mask admits — the per-view incremental path (two panes over one document can carry different find sessions). all() = the plain method; none() = the without-highlights method.

Source

pub fn block_at_position(&self, position: usize) -> Option<TextBlock>

Get a read-only handle to the block containing the given character position. Returns None if position is out of range.

Source

pub fn block_by_number(&self, block_number: usize) -> Option<TextBlock>

Get a read-only handle to a block by its 0-indexed global block number.

O(n): requires scanning all blocks sorted by document_position to find the nth one. Prefer block_at_position() or block_by_id() in performance-sensitive paths.

Source

pub fn blocks(&self) -> Vec<TextBlock>

All blocks in the document, sorted by document_position. O(n).

Returns blocks from all frames, including those inside table cells. This is the efficient way to iterate all blocks — avoids the O(n^2) cost of calling block_by_number(i) in a loop.

Source

pub fn blocks_in_range(&self, position: usize, length: usize) -> Vec<TextBlock>

All blocks whose character range intersects [position, position + length).

O(n): scans all blocks once. Returns them sorted by document_position. A block intersects if its range [block.position, block.position + block.length) overlaps the query range. An empty query range (length == 0) returns the block containing that position, if any.

Source

pub fn snapshot_flow(&self) -> FlowSnapshot

Snapshot the entire main flow in a single lock acquisition.

Returns a FlowSnapshot containing snapshots for every element in the flow.

Source

pub fn snapshot_flow_without_highlights(&self) -> FlowSnapshot

Snapshot the entire main flow with no highlights applied — base fragments and empty paint_highlights on every block, regardless of the active sessions.

This is the per-view opt-out: a read-only viewer that should stay free of search / spell / syntax highlighting pulls this snapshot instead of snapshot_flow. Because suppression happens at build time, it works for metric-affecting sessions too (whose highlights are otherwise merged into fragments irreversibly).

Source

pub fn snapshot_flow_masked(&self, mask: &HighlightMask) -> FlowSnapshot

Snapshot the entire main flow rendering only the sessions mask admits.

The generalization of the plain / without-highlights pair: all() shows every session, none() shows none, and only([...]) shows a chosen set — which is how two panes over one shared document carry different find sessions. The effective HighlighterKind is resolved once here, at the snapshot root, and threaded down, so a view showing only paint-only sessions never pays the reshape path for a metric session it does not show.

Source

pub fn snapshot_flow_masked_no_paint( &self, mask: &HighlightMask, ) -> FlowSnapshot

Snapshot the main flow like snapshot_flow_masked, but without computing the paint-only overlay (paint_highlights is empty on every block). Fragments are identical — metric sessions still split them — so a consumer that reads only the fragments and their geometry gets the exact same tree, minus the extract_paint_spans work.

This is the accessibility path’s snapshot: the AT tree reads fragments, never the paint overlay, so paying to compute a per-block paint span for each of a spell-checker’s tens of thousands of ranges is pure waste (it dominated the a11y rebuild on a large mis-dictionaried document). Render and layout keep using snapshot_flow_masked, which they must — they draw the overlay.

Source

pub fn find( &self, query: &str, from: usize, options: &FindOptions, ) -> Result<Option<FindMatch>>

Find the next (or previous) occurrence. Returns None if not found.

Source

pub fn find_all( &self, query: &str, options: &FindOptions, ) -> Result<Vec<FindMatch>>

Find all occurrences.

Source

pub fn replace_text( &self, query: &str, replacement: &str, replace_all: bool, options: &ReplaceOptions, ) -> Result<usize>

Replace occurrences. Returns the number of replacements. Undoable.

options carries both how to find the text and — via crate::ReplaceOptions::format_policy — what the replacement wears where it overwrites formatted prose. The default drops the formatting under the replaced range, which is fine for plain text and destructive for a rename that lands on a partly-bold name; pass a different policy when that matters.

Source

pub fn replace_ranges( &self, ranges: &[ReplaceRange], options: &ReplaceOptions, ) -> Result<usize>

Replace an explicit set of ranges, each with its own replacement text. Undoable as one action, however many ranges it touches.

replace_text can only put the same string at every match. This is for the case where the caller decides per occurrence — a reviewed bulk rename where some occurrences are unticked, or one that preserves the case it found (AURÉLIENAURÉLIAN, not aurélian).

Do not build the ranges with a separate find_all call. The document can move between the two, and the ranges then address text that is no longer there — which does not fail, it rewrites the wrong words. Use find_and_replace, which does both under one lock.

Ranges that straddle a block boundary, or that overlap one another, are skipped; the returned count reflects only what was actually applied.

Source

pub fn find_and_replace( &self, query: &str, options: &ReplaceOptions, decide: impl FnMut(&str, usize) -> Option<String>, ) -> Result<usize>

Find every match of query and let decide choose what each becomes — atomically.

decide is handed the matched text and the index of the match, and returns the replacement, or None to leave that occurrence alone. So a rename that preserves case and skips the occurrences a writer unticked is one call:

doc.find_and_replace("Aurélien", &ReplaceOptions::new(FindOptions::default()), |matched, i| {
    if excluded.contains(&i) {
        return None; // the writer unticked this one
    }
    Some(if matched.chars().all(char::is_uppercase) { "AURÉLIAN".into() } else { "Aurélian".into() })
})?;

The scan and the splice happen under one lock, which is the whole point. Calling find_all and then replace_ranges would drop the lock in between, and the document can be edited there — after which every range addresses text that has moved. That does not raise an error; it silently rewrites the wrong words. The document mutex is not reentrant, so composing the two public methods cannot close the gap; only doing both inside one can.

Source

pub fn add_resource( &self, resource_type: ResourceType, name: &str, mime_type: &str, data: &[u8], ) -> Result<()>

Add a resource (image, stylesheet) to the document.

Source

pub fn resource(&self, name: &str) -> Result<Option<Vec<u8>>>

Get a resource by name. Returns None if not found.

Uses an internal cache to avoid scanning all resources on repeated lookups.

Source

pub fn undo(&self) -> Result<()>

Undo the last operation.

Source

pub fn redo(&self) -> Result<()>

Redo the last undone operation.

Source

pub fn can_undo(&self) -> bool

Returns true if there are operations that can be undone.

Source

pub fn can_redo(&self) -> bool

Returns true if there are operations that can be redone.

Source

pub fn clear_undo_redo(&self)

Clear all undo/redo history.

Source

pub fn is_modified(&self) -> bool

Returns true if the document has been modified since creation or last reset.

Source

pub fn set_modified(&self, modified: bool)

Set or clear the modified flag.

Source

pub fn title(&self) -> String

Get the document title.

Source

pub fn set_title(&self, title: &str) -> Result<()>

Set the document title.

Source

pub fn text_direction(&self) -> TextDirection

Get the text direction.

Source

pub fn set_text_direction(&self, direction: TextDirection) -> Result<()>

Set the text direction.

Source

pub fn default_wrap_mode(&self) -> WrapMode

Get the default wrap mode.

Source

pub fn set_default_wrap_mode(&self, mode: WrapMode) -> Result<()>

Set the default wrap mode.

Source

pub fn default_language(&self) -> String

Get the document-wide default language (ISO 639-1 code, e.g. “en”). This is the fallback hyphenation language for blocks that don’t set their own language. Defaults to "en" when never set.

Source

pub fn set_default_language(&self, language: &str) -> Result<()>

Set the document-wide default language (ISO 639-1 code). Blocks without an explicit language inherit this for hyphenation.

Source

pub fn on_change<F>(&self, callback: F) -> Subscription
where F: Fn(DocumentEvent) + Send + Sync + 'static,

Subscribe to document events via callback.

Callbacks are invoked outside the document lock (after the editing operation completes and the lock is released). It is safe to call TextDocument or TextCursor methods from within the callback without risk of deadlock. However, keep callbacks lightweight — they run synchronously on the calling thread and block the caller until they return.

Drop the returned Subscription to unsubscribe.

§Breaking change (v0.0.6)

The callback bound changed from Send to Send + Sync in v0.0.6 to support Arc-based dispatch. Callbacks that capture non-Sync types (e.g., Rc<T>, Cell<T>) must be wrapped in a Mutex.

Source

pub fn poll_events(&self) -> Vec<DocumentEvent>

Return events accumulated since the last poll_events() call.

This delivery path is independent of callback dispatch via on_change — using both simultaneously is safe and each path sees every event exactly once.

Source

pub fn set_syntax_highlighter( &self, highlighter: Option<Arc<dyn SyntaxHighlighter>>, )

Attach a single syntax highlighter to this document — the classic, one-highlighter entry point.

Immediately re-highlights the entire document. Replaces the one highlighter this method manages, and only that one: a spell-checker or find layer registered independently via add_syntax_session / add_range_session is left untouched. Pass None to remove it.

This is a convenience over the session registry — it owns exactly one “shim” session. A host that wants to manage several layers uses the session methods directly.

Source

pub fn add_syntax_session( &self, highlighter: Arc<dyn SyntaxHighlighter>, ) -> SessionId

Register a syntax session — a SyntaxHighlighter callback with its own per-block state cascade — and return its crate::SessionId.

Unlike set_syntax_highlighter, this adds rather than replaces: a document can carry a syntax highlighter and a spell-checker at once, each a session, merged in registration order (a later session’s field wins). Sessions remain visible only in views whose HighlightMask admits them.

Source

pub fn add_range_session(&self) -> SessionId

Register an empty range session — absolute-offset ranges set with set_session_ranges, the shape used for search and (later) an externally-driven spell-checker. Returns its crate::SessionId.

A view’s own find session is a range session it alone admits; that is how two panes over one document highlight different queries.

Source

pub fn set_session_ranges( &self, id: SessionId, ranges: Vec<RangeHighlight>, ) -> bool

Replace the ranges of a range session (absolute char offsets, the space FindMatch reports in). Returns false if id is not a range session.

Fires a highlight-changed event so live views showing this session re-snapshot — the only signal there is, since the ranges do not mutate the document.

Source

pub fn remove_session(&self, id: SessionId) -> bool

Retire a session (of either kind). Returns whether it existed.

Source

pub fn rehighlight(&self)

Re-highlight the entire document.

Call this when the highlighter’s rules change (e.g., new keywords were added, spellcheck dictionary updated).

Source

pub fn rehighlight_block(&self, block_id: usize)

Re-highlight a single block and cascade to subsequent blocks if the block state changes.

Source§

impl TextDocument

Source

pub fn append_line(&self, text: &str) -> Result<usize>

Append text as a new block at the end, returning the document’s new block count.

The append half of a streaming buffer. Costs the rope insert and one entity write, whatever the buffer already holds — against ~15.9 ms per line at 10 000 lines through the ordinary editing path (docs/streaming-baseline.md).

The returned count is what a scrollback cap is checked against, so a caller never needs a separate count call — which matters, because block_count walks the whole document:

let count = doc.append_line(line)?;
if count > CAP {
    doc.truncate_front(count - CAP)?;
}

text is taken as a single line: it must not contain \n, since a block is one line by construction here (embedded newlines would desynchronize the rope from the block index). Returns DocumentError::InvalidArgument if it does.

Not undoable, by design — see the module docs.

Source

pub fn append_lines<I, S>(&self, lines: I) -> Result<usize>
where I: IntoIterator<Item = S>, S: AsRef<str>,

Append several lines in one go, returning the document’s new block count.

Equivalent to append_line per line, but pays the document-count write and the throwaway-stack clear once for the batch rather than per line. A view draining a channel once per frame should prefer this.

No line may contain \n.

Not undoable, by design — see the module docs.

Source

pub fn truncate_front(&self, n: usize) -> Result<usize>

Drop the first n blocks, returning how many were actually removed.

The eviction half of a streaming buffer. Returns less than n when the document holds fewer blocks; a document is never emptied completely — one block always remains, since an empty document is not a valid state here (it is created with one block, and the rest of the API assumes at least one exists).

Not undoable, by design — see the module docs.

Trait Implementations§

Source§

impl Clone for TextDocument

Source§

fn clone(&self) -> TextDocument

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Default for TextDocument

Source§

fn default() -> Self

Returns the “default value” for a type. 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> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. 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> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
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.