Docs

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,
}