Skip to main content

DocumentSession

Struct DocumentSession 

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

One open prov document: a metadata editor and a body editor over the same file, reconciled on save.

Implementations§

Source§

impl DocumentSession

Source

pub fn open(path: impl Into<PathBuf>) -> Result<Self, SessionError>

Open a prov document from disk, parsing the body in the grammar its extension declares, with no schema.

Source

pub fn open_with_schema( path: impl Into<PathBuf>, schema: Schema, ) -> Result<Self, SessionError>

Open a prov document from disk carrying the workspace schema.

Source

pub fn open_managed( path: impl Into<PathBuf>, schema: Option<Schema>, derived: Vec<String>, ) -> Result<Self, SessionError>

Open a prov document declaring the keys the workspace maintains, so the metadata model draws their rows and declines every edit to them.

derived is Facets::managed_key_namesid, content_hash, and whatever the workspace named as its updated stamp. It is a separate entry point rather than something open_with_schema does for you because declining an edit is a policy, and a repair tool that means to rewrite a stale id is as legitimate a frontend as an editor that must not. This crate hands over the list and lets the frontend decide (see crate::facets); most editors want it, and this is the one line that says so.

The set has to arrive here rather than being applied afterwards: flower takes it before it builds its first row list.

Examples found in repository?
examples/inspect.rs (line 20)
7fn main() {
8    let path = std::path::PathBuf::from(std::env::args().nth(1).expect("a document"));
9    let view = WorkspaceView::discover(&path).expect("discovery");
10    match &view {
11        Some(v) => println!("workspace: {}", v.root_dir().display()),
12        None => println!("workspace: none"),
13    }
14    let facets = view
15        .as_ref()
16        .map(|v| v.facets().clone())
17        .unwrap_or_default();
18    let schema = view.as_ref().map(|v| v.schema_for(&path));
19    let session =
20        DocumentSession::open_managed(&path, schema, facets.managed_key_names()).expect("open");
21
22    println!("\nkeys:");
23    for (key, facet) in facets.classify(session.meta()) {
24        let flags = [
25            facet.structural().then_some("structural"),
26            facet.managed().then_some("managed"),
27        ]
28        .into_iter()
29        .flatten()
30        .collect::<Vec<_>>()
31        .join(",");
32        println!("  {key:<14} {:<9} {flags}", facet.kind());
33    }
34
35    println!("\nlinks:");
36    for link in links_in(session.meta(), &facets) {
37        let landing = match &view {
38            Some(v) => v.resolve(&path, &link),
39            None => resolve_without_workspace(&path, &link),
40        };
41        let mark = if matches!(landing, Destination::Document { exists: true, .. }) {
42            "→"
43        } else {
44            "·"
45        };
46        println!(
47            "  {mark} {:<10} {:<22} {}",
48            link.relation.name,
49            link.display(),
50            landing.describe()
51        );
52    }
53}
Source

pub fn open_with( path: impl Into<PathBuf>, body_format: BodyFormat, schema: Option<Schema>, ) -> Result<Self, SessionError>

Open a prov document from disk, parsing the body as body_format, with an optional workspace schema.

Source

pub fn from_text( path: impl Into<PathBuf>, text: &str, body_format: BodyFormat, schema: Option<Schema>, ) -> Result<Self, SessionError>

Build a session from in-memory text. The path still drives prov’s carrier/format detection (extension for a config doc, content sniffing for a fenced block). schema governs the metadata model when present.

Source

pub fn path(&self) -> &Path

Source

pub fn has_body(&self) -> bool

Whether the document has an editable prose body.

Source

pub fn metadata(&self) -> &Model<ProvBackend>

The metadata editor (its rows are what a metadata pane renders).

Source

pub fn metadata_mut(&mut self) -> &mut Model<ProvBackend>

Source

pub fn meta(&self) -> &Value

The metadata value tree — what crate::links and crate::facets ask their questions of.

The model’s own copy, not a reparse: it is rebuilt on every edit, so this is current and free.

Examples found in repository?
examples/inspect.rs (line 23)
7fn main() {
8    let path = std::path::PathBuf::from(std::env::args().nth(1).expect("a document"));
9    let view = WorkspaceView::discover(&path).expect("discovery");
10    match &view {
11        Some(v) => println!("workspace: {}", v.root_dir().display()),
12        None => println!("workspace: none"),
13    }
14    let facets = view
15        .as_ref()
16        .map(|v| v.facets().clone())
17        .unwrap_or_default();
18    let schema = view.as_ref().map(|v| v.schema_for(&path));
19    let session =
20        DocumentSession::open_managed(&path, schema, facets.managed_key_names()).expect("open");
21
22    println!("\nkeys:");
23    for (key, facet) in facets.classify(session.meta()) {
24        let flags = [
25            facet.structural().then_some("structural"),
26            facet.managed().then_some("managed"),
27        ]
28        .into_iter()
29        .flatten()
30        .collect::<Vec<_>>()
31        .join(",");
32        println!("  {key:<14} {:<9} {flags}", facet.kind());
33    }
34
35    println!("\nlinks:");
36    for link in links_in(session.meta(), &facets) {
37        let landing = match &view {
38            Some(v) => v.resolve(&path, &link),
39            None => resolve_without_workspace(&path, &link),
40        };
41        let mark = if matches!(landing, Destination::Document { exists: true, .. }) {
42            "→"
43        } else {
44            "·"
45        };
46        println!(
47            "  {mark} {:<10} {:<22} {}",
48            link.relation.name,
49            link.display(),
50            landing.describe()
51        );
52    }
53}
Source

pub fn cursor_path(&self) -> Option<Vec<Seg>>

The metadata path the cursor is on, whichever projection the model is showing.

flower has two — a flat row list and a page stack — and asks the question a different way in each. A frontend that wants “the row under the cursor” should not have to know which one it set, least of all a frontend that switches between them; getting it wrong reads as a link that follows the wrong document rather than as an error.

Source

pub fn body(&self) -> &Doc

The body editor.

Source

pub fn body_format(&self) -> ContentFormat

The grammar the body is written in, as prov spells it.

leaf’s Format is twig’s, which is the wider list — it also names XML and AsciiDoc, which prov has no content format for. Anything outside prov’s three reads as Markdown, which is the same fallback DocumentSession::open applies on the way in, so the answer here is the format the body was actually parsed under rather than a second guess at it.

Every link the prose body declares, as it stands.

Parsed on each call rather than cached: the body is a live buffer, and a cached span list is one edit away from pointing at the wrong bytes. Following a link is a keystroke, not a frame, so one twig parse of one document’s prose is the right price for an answer that is never stale.

The body link the caret is standing inside, if any — the body pane’s half of “the row under the cursor, is that a link?”.

leaf keeps the caret as a byte offset into the same buffer the spans are measured in (leaf_core::Doc::caret), so the two meet without a conversion.

Source

pub fn body_mut(&mut self) -> &mut Doc

Source

pub fn heading_at_caret(&self) -> Option<Heading>

The heading the caret is under — the nearest one at or above it, and None when the caret sits above the document’s first heading (or there are none).

“At or above” is the rule every table of contents and every anchor implementation uses: a caret three paragraphs into a section is in that section, and the heading that opened it is the thing a reader would name to point at where they are.

Parsed through twig directly — prov::twig is the same copy prov and leaf are both built on, so this is the tree leaf is already holding rather than a second one with its own opinions. It is parsed again here because leaf’s own Doc::nodes is private: Doc exposes locate (a fragment to a landing) and link_destination_at_caret (a caret to a link) but nothing that hands back the node array, and nothing that answers the caret-to-heading question. The parse is one document’s prose on a keystroke, which is the same price body_links pays and for the same reason.

Source

pub fn locator_at_caret(&self) -> Option<String>

The #locator naming where the caret is — prov::link::slug of the heading above it.

prov’s slug rather than a local one, because prov is what has to read it back: the fragment this writes is the fragment prov check resolves and the fragment leaf’s Doc::locate lands, and locate’s third reading — a heading’s own words, slugged — is the one that applies to Markdown, where there are no ids to name at all.

Source

pub fn set_candidates(&mut self, candidates: HashMap<String, Vec<Choice>>)

Hand the metadata backend the candidate lists a reference field’s picker should offer, per relation — see ProvBackend::set_candidates for what it costs and WorkspaceView::candidates_map for where a list comes from.

Through the session rather than through metadata_mut().backend_mut() because it is the same kind of out-of-band fact as the schema and the findings: something only a host with a workspace can know, handed to the one document that cannot work it out.

Source

pub fn set_metadata(&mut self, path: &[Seg], value: Value)

Programmatically set the metadata value at path — the flat, by-path edit a UI/FFI issues (vs. driving the selection).

Source

pub fn apply_findings(&mut self, findings: &[Finding])

Take on a set of findings: wash the body ones under the text they are about, and hold the rest for the host to read.

The highlight list is owned by this call. leaf’s set_highlights replaces the whole set rather than adding to it — deliberately, so the host and the document can never disagree about what is on screen — so there is no way to “clear the finding highlights and keep the others”. A session whose findings are being applied is a session whose body highlights are the findings; a host that also wants search hits or annotations in the body composes its own list and calls leaf directly instead of calling this.

Each highlight’s id is the finding’s kind, which is what leaf hands back when a reader activates one, and its marker is "finding" — the name is opaque to leaf, and a frontend reads it as whatever glyph it draws in the margin.

The metadata half is owned the same way, and by the same argument: flower’s set_annotations replaces the whole set rather than adding to it, so the rows a session’s findings are applied to carry those findings and nothing else. A host with annotations of its own composes the list and calls the model directly.

A Site::Meta finding becomes an Annotation at the same path — so the row contents[2] was narrowed to is the row that gets the marker — and a Site::Document one becomes an annotation at the empty path, which is flower’s spelling for “the document”. That is deliberately not a row: nothing draws the root, so a finding about the file rather than about anything written in it stays the host’s to report, which is what findings is for. Site::Body findings go to leaf and nowhere else.

The severity map is total in one direction only: this crate draws two levels and flower draws three, so nothing here ever produces Severity::Info. prov has no severity at all (see crate::findings), and inventing a third here would be inventing it twice.

Source

pub fn findings(&self) -> &[Finding]

Every finding apply_findings was last given.

Source

pub fn meta_findings(&self) -> impl Iterator<Item = &Finding>

The findings that sit in the metadata.

apply_findings has already handed these to the model as annotations, so a widget over it draws them; this is the same half as prov reported it, for a host that wants the kind or the severity rather than the sentence.

Source

pub fn meta_finding_at(&self, path: &[Seg]) -> Option<&Finding>

The finding sitting at metadata path, if there is one.

Exact, not inherited: a finding on contents does not answer for contents[2]. flower’s annotation_at is the other question and inherits from the nearest annotated ancestor.

Source

pub fn sync_history(&mut self)

Notice whatever either editor has just done, and record which one did it. The host calls this once per event-loop iteration, after dispatching the event and before reading the next.

§Why it is polled rather than pushed

Neither editor has an edit entry point the session could wrap. A keystroke reaches leaf through leaf_ratatui::handle_key and flower through flower_ratatui::handle_key, both of which take the editor directly, and a host holding body_mut and metadata_mut can edit through either without passing through anything of this crate’s. What both editors do expose is a counter that moves on every change and on nothing else — Doc::revision and Model::edit_seq — so the session reads those instead of asking the host to remember to tell it. A host that forgets to call this loses undo; it cannot get the order wrong, which is the failure worth designing against.

§The known limit

leaf coalesces keystrokes into steps on its own schedule. Typing a word moves the revision once per character, and twig may hold the whole word as a single undo step. So a Region::Body journal entry is not a leaf step, and a count of entries is not a count of undos: what undo does is take one leaf step, never a keystroke, and then drop whatever further Body entries leaf has nothing left to answer for before it reaches the next Meta one. That is what keeps the ordering exact — body, then metadata, then body undoes in that order — while leaving the granularity to the editor that owns the bytes, which is the only component that can decide it.

flower has no such coalescing: one commit is one step.

A fresh edit in either region clears the redo journal, the way a fresh edit clears either editor’s own.

Source

pub fn journal(&self) -> &[Region]

The steps recorded so far, oldest first — what sync_history has seen. For a frontend drawing a history, and for a test asserting the order.

Source

pub fn can_undo(&self) -> bool

Whether there is a step to take back. See undo for why this is not !journal().is_empty().

A hint, in the direction hints should err: it can say yes where the body entries left are all coalesced away, because leaf’s own can_undo is a step counter rather than its history — see undo. It never says no while there is something to take back, which is the half a greyed-out menu item needs to be right about.

Source

pub fn can_redo(&self) -> bool

Whether there is an undone step to put back.

Source

pub fn undo(&mut self) -> bool

Take back the most recent step, in whichever editor made it.

The journal says which editor, and that editor’s own undo says what — Doc::undo for the body, Model::undo for the metadata. Neither is reimplemented here and neither is second-guessed: flower replays an inverse op through the same Backend::apply the edit went through, so a workspace-maintained key refuses its undo exactly as it refuses its edit, and a refusal here is a refusal that leaves the journal as it was.

Entries leaf has nothing to answer for are dropped, not pressed. Because leaf coalesces (see sync_history), eight Body entries may face one leaf step: the first undo spends the step and the next one walks past the remaining seven to the Meta entry underneath. Without that, a reader would press the key seven times for nothing before the metadata edit came back.

true when something was undone.

Source

pub fn redo(&mut self) -> bool

Put back the most recently undone step, in the editor that made it — the mirror of undo, exhaustion-skipping and refusals included.

true when something was redone.

Source

pub fn dirty(&self) -> bool

true if the metadata or the body has unsaved edits.

Source

pub fn reassemble(&mut self) -> Result<String, SessionError>

Reconcile the body edits into the document and return the full reassembled text — exactly the bytes save writes. Does not touch disk.

Source

pub fn save(&mut self) -> Result<(), SessionError>

Write the reassembled document (metadata edits + body edits) to disk.

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> Same for T

Source§

type Output = T

Should always be Self
Source§

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

Source§

type Error = !

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

fn try_from(value: U) -> Result<T, !>

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.