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
impl DocumentSession
Sourcepub fn open(path: impl Into<PathBuf>) -> Result<Self, SessionError>
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.
Sourcepub fn open_with_schema(
path: impl Into<PathBuf>,
schema: Schema,
) -> Result<Self, SessionError>
pub fn open_with_schema( path: impl Into<PathBuf>, schema: Schema, ) -> Result<Self, SessionError>
Open a prov document from disk carrying the workspace schema.
Sourcepub fn open_managed(
path: impl Into<PathBuf>,
schema: Option<Schema>,
derived: Vec<String>,
) -> Result<Self, SessionError>
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_names — id,
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?
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}Sourcepub fn open_with(
path: impl Into<PathBuf>,
body_format: BodyFormat,
schema: Option<Schema>,
) -> Result<Self, SessionError>
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.
Sourcepub fn from_text(
path: impl Into<PathBuf>,
text: &str,
body_format: BodyFormat,
schema: Option<Schema>,
) -> Result<Self, SessionError>
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.
pub fn path(&self) -> &Path
Sourcepub fn metadata(&self) -> &Model<ProvBackend>
pub fn metadata(&self) -> &Model<ProvBackend>
The metadata editor (its rows are what a metadata pane renders).
pub fn metadata_mut(&mut self) -> &mut Model<ProvBackend>
Sourcepub fn meta(&self) -> &Value
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?
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}Sourcepub fn cursor_path(&self) -> Option<Vec<Seg>>
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.
Sourcepub fn body_format(&self) -> ContentFormat
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.
Sourcepub fn body_links(&self) -> Result<Vec<BodyLink>, SessionError>
pub fn body_links(&self) -> Result<Vec<BodyLink>, SessionError>
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.
Sourcepub fn body_link_at_caret(&self) -> Result<Option<BodyLink>, SessionError>
pub fn body_link_at_caret(&self) -> Result<Option<BodyLink>, SessionError>
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.
pub fn body_mut(&mut self) -> &mut Doc
Sourcepub fn heading_at_caret(&self) -> Option<Heading>
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.
Sourcepub fn locator_at_caret(&self) -> Option<String>
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.
Sourcepub fn set_candidates(&mut self, candidates: HashMap<String, Vec<Choice>>)
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.
Sourcepub fn set_metadata(&mut self, path: &[Seg], value: Value)
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).
Sourcepub fn apply_findings(&mut self, findings: &[Finding])
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.
Sourcepub fn findings(&self) -> &[Finding]
pub fn findings(&self) -> &[Finding]
Every finding apply_findings was last given.
Sourcepub fn meta_findings(&self) -> impl Iterator<Item = &Finding>
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.
Sourcepub fn meta_finding_at(&self, path: &[Seg]) -> Option<&Finding>
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.
Sourcepub fn sync_history(&mut self)
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.
Sourcepub fn journal(&self) -> &[Region]
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.
Sourcepub fn can_undo(&self) -> bool
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.
Sourcepub fn undo(&mut self) -> bool
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.
Sourcepub fn redo(&mut self) -> bool
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.
Sourcepub fn reassemble(&mut self) -> Result<String, SessionError>
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.
Sourcepub fn save(&mut self) -> Result<(), SessionError>
pub fn save(&mut self) -> Result<(), SessionError>
Write the reassembled document (metadata edits + body edits) to disk.