pub struct WorkspaceView { /* private fields */ }Expand description
One workspace, opened for reading, with everything an editor resolves once.
Implementations§
Source§impl WorkspaceView
impl WorkspaceView
Sourcepub fn discover(from: &Path) -> Result<Option<Self>, SessionError>
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?
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(
root_dir: impl Into<PathBuf>,
root_doc: impl Into<PathBuf>,
config: WorkspaceConfig,
) -> Result<Self, SessionError>
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.
Sourcepub fn root_dir(&self) -> &Path
pub fn root_dir(&self) -> &Path
The workspace root directory — the absolute path every relative one here is joined to.
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 root_document(&self) -> PathBuf
pub fn root_document(&self) -> PathBuf
The root document, absolute.
Sourcepub fn config_document(&self) -> Option<PathBuf>
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.
Sourcepub fn config(&self) -> &WorkspaceConfig
pub fn config(&self) -> &WorkspaceConfig
The effective config — defaults, overlaid by the root’s prov: block,
overlaid by the config document.
Sourcepub fn facets(&self) -> &Facets
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?
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 vocabularies(&self) -> &Vocabularies
pub fn vocabularies(&self) -> &Vocabularies
The vocabularies the controlled fields point at, one per declaration. A vocabulary that failed to load is simply absent.
Sourcepub fn field_scopes(&self) -> &FieldScopes
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.
Sourcepub fn prov(&self) -> &Workspace<StdFs, NoIdentity, FileIndex>
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.
Sourcepub fn content_schema(&self) -> Schema
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.
Sourcepub fn schema_for(&self, path: &Path) -> Schema
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?
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_document(
&self,
path: impl AsRef<Path>,
) -> Result<DocumentSession, SessionError>
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()));Sourcepub fn candidates_map(
&self,
doc: &Path,
) -> Result<HashMap<String, Vec<Choice>>, SessionError>
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.
Sourcepub fn candidates_for(
&self,
doc: &Path,
relation: &str,
) -> Result<Vec<Choice>, SessionError>
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:
valueis exactly whatreference_towould 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.labelis the target’s title, which is what a reader is looking for and what flower’s filter matches against.detailis 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.
Sourcepub fn resolve(&self, doc: &Path, link: &impl AnyLink) -> Destination
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?
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 resolve_nominal(
&self,
doc: &Path,
link: &impl AnyLink,
) -> Result<Destination, SessionError>
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.
Sourcepub fn reference_to(
&self,
from: &Path,
to: &Path,
locator: Option<&str>,
) -> Result<String, SessionError>
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.
Sourcepub fn findings_for(&self, doc: &Path) -> Result<Vec<Finding>, SessionError>
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.
Sourcepub fn backlinks_to(
&self,
target: impl AsRef<Path>,
) -> Result<Vec<Backlink>, SessionError>
pub fn backlinks_to( &self, target: impl AsRef<Path>, ) -> Result<Vec<Backlink>, SessionError>
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.