doc-overlay
Source: crates/lvcs-doc/src/actor/output.rs.
/// Set the anchors a workspace watches in this document (design §8,
/// decision 82), replacing any set `watcher` sent before; an empty set ends
/// the watch and is not answered. File and override documents take it: the
/// anchors are into `/content`. A stacked workspace watches the document
/// that holds each range in its base view, which may be an override of a
/// workspace it stacks on. The document answers a non-empty set at once
/// with [`AnchorsResolved`] at its current heads, then again just before
/// every render. A watch lasts for the document actor's incarnation.
///
/// Watches are durable context, so the journaled state covers them
/// (decision 32): [`DocFingerprint::digest`] covers the watch map, ordered
/// by watcher and then override id, beside the heads, and replay rebuilds
/// the map from the incarnation's journaled `WatchAnchors`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WatchAnchors {
pub watcher: WorkspaceId,
pub anchors: BTreeMap<OverrideId, RangeAnchor>,
}
/// One watcher's anchors resolved at `heads`, the document's heads after a
/// change (or when the watch arrived). Emitted once per watcher just before
/// the [`DocOutput::Rendered`] for the same heads, so the workspace composes
/// each render with spans of the same version. A span is `None` while the
/// document lacks the anchor's heads (they may not have synced yet), or
/// when the anchor names characters not visible, in order, at them;
/// [`AnchorSpan::Collapsed`] means the base deleted the whole range, and
/// the override has lost its anchor (design §8, decision 41): its `at` is
/// the gap where the range was, where the view keeps the override's text
/// (decision 92). `lines` names each span as a reader is shown it, from
/// the text at `heads` (`lines::name`; a collapsed span names the line its
/// gap is in), keyed as `spans` is, and is `None` where the span is
/// (decision 165).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AnchorsResolved {
pub watcher: WorkspaceId,
#[serde(with = "crate::cbor::heads")]
pub heads: Vec<ChangeHash>,
pub spans: BTreeMap<OverrideId, Option<AnchorSpan>>,
pub lines: BTreeMap<OverrideId, Option<LineName>>,
}
/// Anchor a new claim for a workspace (design §8).
///
/// Claims never combine or overlap (decisions 71, 83, 95, 99, 100, 156, and
/// 160): the part of an edit inside an existing claim goes to that
/// override, and the workspace sends one `Claim` for the parts of an edit
/// in each maximal run of unclaimed characters, claimed together as one
/// range; adjacent overrides stay separate and the view composes them.
/// `at` is the heads of the document that holds the claimed characters in
/// the claimant's view (decision 89), and `start..end` are code points of
/// its `/content` there: the characters the parts replace, or an empty
/// range (`start == end`) for an insertion at a gap.
///
/// The document snaps the claim to whole lines of `/content` at `at`, in
/// every text document, `file` and `override` alike, and in every language
/// (decisions 156 and 160); a line is its characters and its trailing
/// newline (decision 95), and nothing is parsed. A range snaps from the
/// start of the line holding its first character to the end of the line
/// holding its last. An insertion snaps, in decision 95's order, to the
/// line ending just before the gap (whose newline, or the text's last
/// character, is just before it), else to the line holding the character
/// after it: so a gap inside a line claims that line, a gap at a line's
/// start the line before it, the gap at the text's end the last line, and
/// the gap at 0 the first line. It then clips the snapped range to
/// the run of unclaimed characters (none of `watcher`'s watched anchors in
/// this document covers them at `at`, decision 83's resolution) that
/// contains the range or, for an insertion, borders the gap (the run
/// holding the character before it, else the one holding the character
/// after it); an insertion tries those choices in order until one leaves a
/// non-empty range. A watched anchor that resolves to
/// [`AnchorSpan::Collapsed`] at `at` covers no characters, but its gap is a
/// boundary (decision 100): a run never holds one strictly inside it, so a
/// snapped range that would straddle the gap is clipped to the side holding
/// the claimed range, or for an insertion to the side the run bordering the
/// gap is on, and no new claim holds a collapsed override's gap strictly
/// inside it. It answers with exactly one [`Claimed`]. A claim whose lines
/// touch or overlap another claim of the same workspace is clipped beside
/// it, never merged into it; readers are shown the touching or
/// overlapping claims of one participant in one workspace and one document
/// as one line range (decisions 156 and 160).
///
/// `run_start..run_end` is the claimant's view run (decision 101's M3
/// deferral, frozen at the M3 freeze): the maximal stretch of the
/// claimant's view that comes from this document and holds `start..end`,
/// in code points of `/content` at `at`, bounded by the claimant's own
/// overrides and by those of every workspace it stacks on. The snapped
/// range is also clipped to it, so a claim of text a stacked workspace
/// inherits from a lower override never reaches past what the claimant
/// sees from that override: characters outside the run count as claimed,
/// so a range reaching past it is refused `OutOfBounds` like one that
/// overlaps a watched claim, and an insertion borders the run only from
/// inside it. A run past the text, or ending before it starts, refuses the
/// claim `OutOfBounds`. `M3.workspace` sends it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Claim {
pub watcher: WorkspaceId,
/// Chosen by the workspace actor, increasing per workspace; echoed by
/// [`Claimed`].
pub claim: u64,
#[serde(with = "crate::cbor::heads")]
pub at: Vec<ChangeHash>,
pub start: usize,
pub end: usize,
pub run_start: usize,
pub run_end: usize,
}
/// The answer to a [`Claim`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Claimed {
pub watcher: WorkspaceId,
pub claim: u64,
pub outcome: ClaimOutcome,
}
/// What a [`Claim`] produced.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ClaimOutcome {
/// The snapped and clipped range, anchored at the claim's `at`: the
/// anchor's [`RangeAnchor::heads`] are the claimed document's heads at
/// claim time, and `start..end` is the range in code points of
/// `/content` there (decision 87), so the workspace can cut the new
/// override's init text and place the edit in it without waiting for
/// a resolution at later heads. It overlaps none of the watcher's
/// claims.
Anchored {
anchor: RangeAnchor,
start: usize,
end: usize,
},
/// `UnknownBase` when the document lacks `at`, `OutOfBounds` when
/// `start..end` is past the text there, overlaps a watched claim,
/// reaches outside the view run (or the run is past the text), or
/// holds a collapsed watched claim's gap strictly inside it,
/// `Failed` when nothing is left to anchor (no anchor is empty): an
/// empty text, or an insertion with claimed characters (or an end of
/// the text) on both sides of the gap; also `Failed` when the document
/// faulted answering the claim or was closed.
Refused(RejectReason),
}
/// Read this document's body at `heads` (design §8). The workspace actor
/// reads an override at its init heads for the claim-time base text and
/// executable bit a merge-back merges against (decision 85), a base
/// document at a claim's heads, with `within` the claim's anchor, for who
/// changed the range since, and a whited-out base document at its
/// whiteout's heads for who changed the file since (decision 86). `req` is
/// chosen by the workspace actor, as for an edit it routes. File and
/// override documents take it. Answered with exactly one [`ReadAtDone`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReadAt {
pub req: RequestId,
#[serde(with = "crate::cbor::heads")]
pub heads: Vec<ChangeHash>,
/// Limit the answer's `authors` to changes inside this range.
pub within: Option<RangeAnchor>,
/// Name the window from these heads to `heads` in the answer's
/// `window` (decision 193): the changes in `heads`' history and not in
/// these heads' history, an empty set naming every change up to
/// `heads`. `None` for no window.
#[serde(with = "crate::cbor::opt_heads")]
pub since: Option<Vec<ChangeHash>>,
}
/// The answer to a [`ReadAt`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReadAtDone {
pub req: RequestId,
pub outcome: ReadAtOutcome,
}
/// What a [`ReadAt`] found.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReadAtOutcome {
/// The file at `heads`, and the Automerge actors of the changes the
/// document holds beyond `heads`; with `within`, only those that
/// inserted a character inside its range or deleted one of its
/// characters (a character is inside when it lies between the anchor's
/// first and last characters in list order, decision 83; the range's
/// characters are those visible inside it at the anchor's heads and
/// those inserted inside it since). The daemon maps them to
/// participants (see `Conflict.authors` in `lvcs-ws`). The two flags
/// read every change beyond `heads`, `within` or not, for the
/// protected-path gate (decision 122): `unattributed` is set when one
/// names no [`AuthorId`] (an actor id that is not 16 bytes), and
/// `bundled` when one's record names a merge bundle (decision 125).
/// `window`, with `since`, maps the author of each change in the
/// window to the lines of `body` it touched (decision 193): those
/// holding a character it inserted that is visible at `heads`, and
/// the line where a character it deleted was, as `lines::names` names
/// them, without `near`; a binary or deleted body names no lines, and
/// a change whose actor id names no [`AuthorId`] is left out. It is
/// empty without `since`. `derived` is `/meta/derived` at `heads`
/// (row 180). `head_authors` are the authors of the document's head
/// changes beyond `heads`, those no later change depends on (decision
/// 314).
Read {
body: FileBody,
executable: bool,
derived: bool,
authors: BTreeSet<AuthorId>,
unattributed: bool,
bundled: bool,
window: BTreeMap<AuthorId, Vec<LineName>>,
head_authors: BTreeSet<AuthorId>,
},
/// `UnknownBase` when the document lacks `heads`, `since`, or
/// `within`'s heads;
/// `Failed` when `within` names characters not visible, in order, at
/// its heads, or the document faulted answering or was closed.
Refused(RejectReason),
}
/// A merge bundle's id (design §8, decisions 48 and 57; `BundleId::of`):
/// the `HASH` of the canonical CBOR array (workspace id, merging
/// participant key, the workspace's frontier id, the base's visible
/// frontier id), each a byte string, so it never depends on the changes
/// that carry it. Written in `ID_ENCODING`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct BundleId(#[serde(with = "crate::cbor::fixed")] pub [u8; 32]);
/// One merge-back change of a `WorkspaceMerged` bundle (decision 48), the
/// only way a workspace's work reaches a base document. Committed on
/// `origin.author`'s own branch at `base`, like an [`Edit`] or the matching
/// [`DiskChange`], except that the commit's record ([`ChangeMessage`],
/// its Automerge `message`) names `bundle`, so every peer knows the change
/// belongs to the bundle before the merge record arrives; no other change's
/// record names a bundle (decision 125). Answered like an `Edit`:
/// [`DocOutput::Changed`] with `req`, or [`DocOutput::Rejected`]
/// (`UnknownBase` for a `base` the document lacks, `OutOfBounds` for text
/// splices that don't apply there), then rendered. It always commits, so a
/// change that writes what `base` already holds records nothing and answers
/// with the document's heads unchanged. File and override documents take
/// it. `req` is chosen by the workspace actor, as for an edit it routes.
/// A `derived` of `Mark` makes a text or recreate change one whole-text
/// replacement at `base` that puts `/meta/derived` true, never a three-way
/// merge; `Unmark` puts it false; `Keep` leaves it (row 180).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BundleEdit {
pub bundle: BundleId,
pub req: RequestId,
pub origin: Origin,
#[serde(with = "crate::cbor::heads")]
pub base: Vec<ChangeHash>,
pub change: BundleChange,
/// How the change treats `/meta/derived` ([`DerivedFlag`]).
pub derived: DerivedFlag,
}
/// What a [`BundleEdit`] writes.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum BundleChange {
/// Splices of `/content` at `base`, sorted and disjoint, applied like
/// an [`Edit`]'s.
Text(Vec<Splice>),
/// Recreate a deleted file with this text (a workspace-local file
/// whose base document is deleted, at genesis or after history,
/// decision 98): at `base`, replace the whole `/content` with it, and
/// delete and re-put `/meta/format` (`text`) and `/meta/deleted`
/// (false) (decision 76), so the file's old content, which removal
/// left in `/content`, is never kept beside the new text.
Recreate(String),
/// Replace the file with these bytes (a binary whole-document
/// override), like [`DiskChange::Binary`].
Binary(#[serde(with = "crate::cbor::bytes")] Vec<u8>),
/// Set the executable bit, like [`DiskChange::Executable`].
Executable(bool),
/// Delete the file (a merged whiteout), like [`DiskChange::Removed`].
Removed,
}