Skip to main content

WorkspaceView

Struct WorkspaceView 

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

One workspace, opened for reading, with everything an editor resolves once.

Implementations§

Source§

impl WorkspaceView

Source

pub fn discover(from: &Path) -> Result<Option<Self>, SessionError>

Find the workspace from belongs to and open it for reading.

from may be a file or a directory; a file’s directory is where the walk up starts. Ok(None) when no ancestor holds a root document, which is the ordinary state of a markdown file that is simply a markdown file — not an error, and a frontend should carry on without a workspace rather than refuse to open it.

A directory holding two root candidates and no index/readme to break the tie is an error: prov will not guess which is the root, and neither should this.

Examples found in repository?
examples/inspect.rs (line 9)
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( root_dir: impl Into<PathBuf>, root_doc: impl Into<PathBuf>, config: WorkspaceConfig, ) -> Result<Self, SessionError>

Open a workspace whose root and config are already known — the path a caller that did its own discovery takes, and what discover calls.

Source

pub fn root_dir(&self) -> &Path

The workspace root directory — the absolute path every relative one here is joined to.

Examples found in repository?
examples/inspect.rs (line 11)
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 root_document(&self) -> PathBuf

The root document, absolute.

Source

pub fn config_document(&self) -> Option<PathBuf>

The config document the root points at, absolute, when there is one. A workspace that keeps all its policy in the root’s prov: block has none.

Source

pub fn config(&self) -> &WorkspaceConfig

The effective config — defaults, overlaid by the root’s prov: block, overlaid by the config document.

Source

pub fn facets(&self) -> &Facets

The classification this workspace’s vocabulary implies. See crate::facets for why nothing acts on it.

Examples found in repository?
examples/inspect.rs (line 16)
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 vocabularies(&self) -> &Vocabularies

The vocabularies the controlled fields point at, one per declaration. A vocabulary that failed to load is simply absent.

Source

pub fn field_scopes(&self) -> &FieldScopes

Which region of the tree each scoped field declaration governs — what schema_for asks to know which declaration of a field reaches a document. A workspace with no scoped declarations has an empty one that always falls back to the workspace-wide declaration.

Source

pub fn prov(&self) -> &Workspace<StdFs, NoIdentity, FileIndex>

prov’s read surface, for a frontend that wants more of it than this view exposes — a tree, a census, a title index.

Source

pub fn content_schema(&self) -> Schema

The schema a content document in this workspace is edited under when no particular document is in hand: the workspace-wide declaration of each field, and nothing for a field declared only under indexes.

For a document you have, schema_for is the real answer — it is this, with each scoped field resolved to the declaration that governs that document.

Source

pub fn schema_for(&self, path: &Path) -> Schema

The schema path is edited under: the config-document schema for the document that is this workspace’s config, and for everything else the content schema with each field governed by whichever of its declarations reaches this document — status under Tasks gets the task terms, under Proposals the proposal terms, and a document under neither gets no status rule at all.

A fact about which document this is, not a preference. prov.yaml is a document whose keys are policy, and editing it under the content schema would offer term pickers for fields it does not have and none for the ones it does.

Examples found in repository?
examples/inspect.rs (line 18)
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_document( &self, path: impl AsRef<Path>, ) -> Result<DocumentSession, SessionError>

Open a document in this workspace as a DocumentSession, under the schema schema_for picks.

The schema and nothing else — no keys are made read-only and no rows are sunk, because both of those are the frontend’s to decide (see crate::facets). Most editors want at least the first, which is three lines rather than one:

let facets = view.facets();
let schema = Some(view.schema_for(&path));
let mut session = DocumentSession::open_managed(&path, schema, facets.managed_key_names())?;
session.metadata_mut().set_demoted(facets.structural_keys(session.meta()));
Source

pub fn candidates_map( &self, doc: &Path, ) -> Result<HashMap<String, Vec<Choice>>, SessionError>

Every relation’s candidate list for a document being edited at doc, in the shape ProvBackend::set_candidates takes.

One walk, whatever the relation. Every content document in the workspace is a candidate for every relation — prov’s relations are not typed by what they may point at — so the list is built once and shared across the entries. The map is keyed by relation so that the backend can answer without knowing any of this, and so that a narrowing added later (a relation that may only point at an index, say) changes one function and nothing downstream.

Source

pub fn candidates_for( &self, doc: &Path, relation: &str, ) -> Result<Vec<Choice>, SessionError>

What a picker on relation, in the document at doc, should offer: every other content document in the workspace, spelled as a link this workspace would write.

Each Choice carries the three things a picker draws and commits:

  • value is exactly what reference_to would write for that target — markdown or wikilink, by path or by id, root-relative or document-relative, labelled with the target’s own title. So choosing a candidate writes a link in the workspace’s own style, and a document that chooses one is indistinguishable from a document whose link was typed by hand correctly.
  • label is the target’s title, which is what a reader is looking for and what flower’s filter matches against.
  • detail is the workspace-relative path, which is what tells two documents with the same title apart.

doc itself is left out: a document contains or is contained by other documents, and prov’s check has a finding for the one that points at itself.

relation is read for nothing today and is in the signature anyway, because which relation is asking is the only axis this could ever be narrowed along, and a caller that has already written the argument does not have to be found again when it is.

§What it costs

A spanning walk from the workspace root (reachable_documents_from) — the same population prov check counts — plus one read per document for its title. Proportional to the workspace, not to the document. This is a per-open cost and must not be put behind a keystroke; see ProvBackend::set_candidates for where the answer is cached.

Source

pub fn resolve(&self, doc: &Path, link: &impl AnyLink) -> Destination

Resolve a link written in the document at doc (absolute or workspace-relative).

Path targets and id: handles resolve; a nominal ([[My File]]) target does not, because resolving one needs a title index over the whole workspace and that is a scan an editor should not do behind a keystroke. Use resolve_nominal to pay for it deliberately.

Generic over AnyLink, which is what lets a link written in the prose body resolve through exactly this code: where a link sits is the one thing resolution never asks about.

Examples found in repository?
examples/inspect.rs (line 38)
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 resolve_nominal( &self, doc: &Path, link: &impl AnyLink, ) -> Result<Destination, SessionError>

resolve, also resolving nominal targets against a title index built by walking the workspace.

Separate because of what it costs: the index is a scan from the root, and a [[My File]] link is the only kind that needs one. A frontend that follows links from the keyboard should call resolve first and fall back to this only when it comes back Unresolvable.

Source

pub fn reference_to( &self, from: &Path, to: &Path, locator: Option<&str>, ) -> Result<String, SessionError>

The link text to write from the document at from to the one at to, optionally landing on locator inside it — “a link to there”, in this workspace’s own spelling.

The inverse of resolve, and the only thing in this crate that produces link syntax rather than consuming it. It is still a read: nothing is written, nothing is registered, and the caller decides what to do with the string. Retargeting an existing link is prov’s mutate layer and is still out of scope (see the module docs); handing a reader the text of a link is not.

Everything about the spelling is prov’s reference_style — markdown or wikilink, by path or by id, root-relative or document-relative, labelled or bare. A workspace that addresses by id gets one only if the target is already registered: minting an id would be a write, so an unregistered target degrades to a path link, which is exactly what format_reference does with None.

The label is the target’s own title, falling back to prov’s path_to_title — the same two steps every prov verb that authors a link takes. A target that cannot be read falls back with it rather than failing: a link to a document that is not there yet is a reasonable thing to want to write.

Source

pub fn findings_for(&self, doc: &Path) -> Result<Vec<Finding>, SessionError>

What prov’s integrity check says about doc, placed where an editor can draw it.

§What it costs

Workspace::check is reachability-bounded: it walks from the document it is given and reports on what that walk reaches. Starting it at the document itself is therefore the cheap per-document check — for a leaf note with no contents it loads one document and censuses its links, which is the price of a save. It is not free for every document: run on an index, it walks the subtree under it, and run on the workspace root it walks the workspace. A frontend that wants this after every keystroke should not have it; after a save, which is what provui-tui does, it is proportional to what the document contains.

The bound is also why the answer is narrower than prov check on the whole workspace: a finding lodged against this document by a walk that started somewhere else — a parent reporting that this document does not link back — is not reachable from here and does not appear. What does appear is everything this document declares.

Findings about other documents the walk reached are filtered out: subject is prov’s own answer to “which file would a repair open”, and a broken link in a.md pointing at b.md belongs to a.md.

Every inbound reference to target, walked from the workspace root.

prov keeps no stored backlink index — this is the census inverted, so it is always fresh and always a walk. Worth it on demand (“what points at this?”), not on every frame.

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.