M11.1: semantic-frontend protocol scaffolding (wire + capability)
First milestone of the M11 semantic-frontend arc (docs/semantic-frontend-protocol.md). Wire-format scaffolding only — no producer or consumer; mechanically identical to the M10.5 CRDT wire declaration, and non-breaking by the same slice-membership + per-session-filter argument. - PROTOCOL_VERSION 2 -> 3; SUPPORTED_PROTOCOL_VERSIONS [1,2,3]. v0.1 and v1.0 binaries keep connecting unchanged (membership, not strict equality). - semantic_render capability bit on FrontendCapabilities, InstanceCapabilities, NegotiatedCapabilities (#[serde(default)]; instance default false until the M11.2 projection seam). negotiate_capabilities AND-combines it and enforces the semantic_render => crdt_replica dependency as a CapabilityMismatch (a semantic session is also a text replica), never a silent degrade. PMACS_INSTANCE_SEMANTIC_RENDER env override added. - InstanceMessage SemanticFrame family: StyleSpans, Decorations, InlineAdornments, BlockAdornments, FoldState, ResourceOffer. FrontendEvent::Viewport. Supporting types: ByteRange, StyleSpan, Decoration/DecorationKind, InlineAdornment/AdornmentPlacement/ AdornmentContent, BlockAdornment, ResourceBody. All byte-anchored; no pixels cross the contract boundary. - Grid TUI (frontend.rs) and daemon apply_event drop the new family silently — the "declared, not yet wired" posture CrdtOp held between M10.5 and M10.8. Stale v1.0 version-pin tests updated to the v1.1 truth; negotiation matrix + postcard round-trips added. Lib + integration suites green on both the default and crdt feature flavors; clippy -D warnings clean on both. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d3fa63290a
commit
1c5169b8f8
28
CHANGELOG.md
28
CHANGELOG.md
|
|
@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
#### Semantic-frontend protocol scaffolding (M11.1)
|
||||||
|
|
||||||
|
First milestone of the M11 semantic-frontend arc (see
|
||||||
|
`docs/semantic-frontend-protocol.md`). Wire-format scaffolding only —
|
||||||
|
no producer or consumer yet; mechanically identical to the M10.5 CRDT
|
||||||
|
wire declaration.
|
||||||
|
|
||||||
|
- `PROTOCOL_VERSION` bumped 2 → 3; `SUPPORTED_PROTOCOL_VERSIONS` now
|
||||||
|
`[1, 2, 3]`. The slice-membership handshake keeps v0.1/v1.0 binaries
|
||||||
|
connecting to v1.1 binaries unchanged.
|
||||||
|
- New `semantic_render` capability bit on `FrontendCapabilities`,
|
||||||
|
`InstanceCapabilities`, and `NegotiatedCapabilities`
|
||||||
|
(`#[serde(default)]`; instance default `false` until the M11.2
|
||||||
|
projection seam lands). `negotiate_capabilities` AND-combines it and
|
||||||
|
enforces the `semantic_render ⇒ crdt_replica` dependency (a semantic
|
||||||
|
session is also a text replica) — rejected as a
|
||||||
|
`CapabilityMismatch`, never silently degraded.
|
||||||
|
- `InstanceMessage` gains the `SemanticFrame` family: `StyleSpans`,
|
||||||
|
`Decorations`, `InlineAdornments`, `BlockAdornments`, `FoldState`,
|
||||||
|
`ResourceOffer`; `FrontendEvent` gains `Viewport`. All anchored in
|
||||||
|
byte offsets — the instance never learns a pixel.
|
||||||
|
- `PMACS_INSTANCE_SEMANTIC_RENDER` env override mirrors the existing
|
||||||
|
per-capability test overrides.
|
||||||
|
|
||||||
## [1.0.0] --- 2026-05-18
|
## [1.0.0] --- 2026-05-18
|
||||||
|
|
||||||
First stable release. Builds on the 0.1.0 preview (M1–M6) with the
|
First stable release. Builds on the 0.1.0 preview (M1–M6) with the
|
||||||
|
|
|
||||||
|
|
@ -578,6 +578,11 @@ pub fn build_capabilities() -> FrontendCapabilities {
|
||||||
terminal_kind: std::env::var("TERM").ok(),
|
terminal_kind: std::env::var("TERM").ok(),
|
||||||
multi_frontend: cfg!(feature = "crdt"),
|
multi_frontend: cfg!(feature = "crdt"),
|
||||||
crdt_replica: cfg!(feature = "crdt"),
|
crdt_replica: cfg!(feature = "crdt"),
|
||||||
|
// T M11.1 — the v0.1/v1.0 TUI is a grid frontend, not a
|
||||||
|
// semantic (layout-local) renderer. It never consumes the
|
||||||
|
// SemanticFrame family. A future GPU/GUI frontend sets this
|
||||||
|
// true; the TUI stays false.
|
||||||
|
semantic_render: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -574,6 +574,9 @@ fn cleanup(socket_path: &Path, lock: LockHandle) {
|
||||||
/// anything else / absent → default `true`):
|
/// anything else / absent → default `true`):
|
||||||
/// - `PMACS_INSTANCE_MULTI_FRONTEND`
|
/// - `PMACS_INSTANCE_MULTI_FRONTEND`
|
||||||
/// - `PMACS_INSTANCE_CRDT_REPLICA`
|
/// - `PMACS_INSTANCE_CRDT_REPLICA`
|
||||||
|
/// - `PMACS_INSTANCE_SEMANTIC_RENDER` (T M11.1; default `false`
|
||||||
|
/// until the M11.2 projection seam lands, so this env var is the
|
||||||
|
/// only way to advertise the bit for negotiation tests)
|
||||||
///
|
///
|
||||||
/// Production daemons don't set these; tests do.
|
/// Production daemons don't set these; tests do.
|
||||||
fn instance_capabilities_with_env_override() -> InstanceCapabilities {
|
fn instance_capabilities_with_env_override() -> InstanceCapabilities {
|
||||||
|
|
@ -587,6 +590,7 @@ fn instance_capabilities_with_env_override() -> InstanceCapabilities {
|
||||||
InstanceCapabilities {
|
InstanceCapabilities {
|
||||||
multi_frontend: env_bool("PMACS_INSTANCE_MULTI_FRONTEND", defaults.multi_frontend),
|
multi_frontend: env_bool("PMACS_INSTANCE_MULTI_FRONTEND", defaults.multi_frontend),
|
||||||
crdt_replica: env_bool("PMACS_INSTANCE_CRDT_REPLICA", defaults.crdt_replica),
|
crdt_replica: env_bool("PMACS_INSTANCE_CRDT_REPLICA", defaults.crdt_replica),
|
||||||
|
semantic_render: env_bool("PMACS_INSTANCE_SEMANTIC_RENDER", defaults.semantic_render),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1753,7 +1757,18 @@ fn apply_event(
|
||||||
}
|
}
|
||||||
FrontendEvent::Paste { .. }
|
FrontendEvent::Paste { .. }
|
||||||
| FrontendEvent::FocusGained(_)
|
| FrontendEvent::FocusGained(_)
|
||||||
| FrontendEvent::FocusLost(_) => {
|
| FrontendEvent::FocusLost(_)
|
||||||
|
// T M11.1: the semantic-frontend viewport declaration. Its
|
||||||
|
// consumer is the instance-side projection seam
|
||||||
|
// (`SemanticRenderState`, M11.2), which scopes the
|
||||||
|
// SemanticFrame family to this byte range. M11.1 only
|
||||||
|
// declares the wire shape; no projection seam exists yet and
|
||||||
|
// the instance advertises `semantic_render: false`, so
|
||||||
|
// negotiation rejects any session that would emit this — it
|
||||||
|
// is unreachable in practice. Dropped silently until M11.2
|
||||||
|
// wires the consumer (same "declared, not yet wired" posture
|
||||||
|
// CrdtOp had between M10.5 and M10.8).
|
||||||
|
| FrontendEvent::Viewport { .. } => {
|
||||||
// v0.1: silently ignored. Future work surfaces these
|
// v0.1: silently ignored. Future work surfaces these
|
||||||
// through Lua hooks (paste-text-fn, focus-changed-hook).
|
// through Lua hooks (paste-text-fn, focus-changed-hook).
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -368,7 +368,21 @@ impl Frontend {
|
||||||
// byte position (consumed by BufferMirror's cursor tracker
|
// byte position (consumed by BufferMirror's cursor tracker
|
||||||
// in attach.rs) drives optimistic-apply. The legacy path
|
// in attach.rs) drives optimistic-apply. The legacy path
|
||||||
// here only needs grid; the byte variant drops silently.
|
// here only needs grid; the byte variant drops silently.
|
||||||
| InstanceMessage::CursorByte { .. } => {
|
| InstanceMessage::CursorByte { .. }
|
||||||
|
// T M11.1: the semantic-frontend projection family. This
|
||||||
|
// is the grid TUI — it advertises `semantic_render: false`,
|
||||||
|
// so a v3 daemon never sends these here (the per-session
|
||||||
|
// outgoing filter, M11.2, gates the family). If one
|
||||||
|
// arrives anyway it drops silently, same v0.1-ignored
|
||||||
|
// category as CrdtOp / PresenceUpdate. A semantic
|
||||||
|
// frontend (M11.5) consumes them via its own layout path,
|
||||||
|
// not this cell-grid path.
|
||||||
|
| InstanceMessage::StyleSpans { .. }
|
||||||
|
| InstanceMessage::Decorations { .. }
|
||||||
|
| InstanceMessage::InlineAdornments { .. }
|
||||||
|
| InstanceMessage::BlockAdornments { .. }
|
||||||
|
| InstanceMessage::FoldState { .. }
|
||||||
|
| InstanceMessage::ResourceOffer { .. } => {
|
||||||
// v0.1 TUI ignores these; v0.3 GUI consumes them.
|
// v0.1 TUI ignores these; v0.3 GUI consumes them.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -367,6 +367,7 @@ mod tests {
|
||||||
NegotiatedCapabilities {
|
NegotiatedCapabilities {
|
||||||
multi_frontend: true,
|
multi_frontend: true,
|
||||||
crdt_replica: false,
|
crdt_replica: false,
|
||||||
|
semantic_render: false,
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
@ -382,6 +383,7 @@ mod tests {
|
||||||
NegotiatedCapabilities {
|
NegotiatedCapabilities {
|
||||||
multi_frontend: false,
|
multi_frontend: false,
|
||||||
crdt_replica: false,
|
crdt_replica: false,
|
||||||
|
semantic_render: false,
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
@ -665,6 +667,7 @@ mod tests {
|
||||||
NegotiatedCapabilities {
|
NegotiatedCapabilities {
|
||||||
multi_frontend: true,
|
multi_frontend: true,
|
||||||
crdt_replica: true,
|
crdt_replica: true,
|
||||||
|
semantic_render: false,
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
@ -677,6 +680,7 @@ mod tests {
|
||||||
NegotiatedCapabilities {
|
NegotiatedCapabilities {
|
||||||
multi_frontend: true,
|
multi_frontend: true,
|
||||||
crdt_replica: false,
|
crdt_replica: false,
|
||||||
|
semantic_render: false,
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
664
src/protocol.rs
664
src/protocol.rs
|
|
@ -320,6 +320,38 @@ pub enum FrontendEvent {
|
||||||
/// loro's `import_updates` decodes.
|
/// loro's `import_updates` decodes.
|
||||||
op: crate::rope::CrdtOp,
|
op: crate::rope::CrdtOp,
|
||||||
},
|
},
|
||||||
|
/// T M11.1: the buffer byte range a semantic frontend currently
|
||||||
|
/// has on screen, in buffer coordinates. Replaces the
|
||||||
|
/// instance-derived grid viewport for `semantic_render` sessions:
|
||||||
|
/// the instance scopes its `StyleSpans` / `Decorations` / … to
|
||||||
|
/// this range rather than shipping a whole file's styling.
|
||||||
|
///
|
||||||
|
/// **No pixels.** This carries a byte range, never viewport pixel
|
||||||
|
/// size, DPI, font metrics, or glyph advances — the contract
|
||||||
|
/// boundary invariant from the semantic-frontend design note. The
|
||||||
|
/// frontend owns all visual-motion semantics and resolves
|
||||||
|
/// pixel→offset locally; there is deliberately no hit-test
|
||||||
|
/// request variant and no `SemanticResize`, both of which would
|
||||||
|
/// leak pixels across the boundary.
|
||||||
|
///
|
||||||
|
/// `generation` ties the declared range to a CRDT version so the
|
||||||
|
/// instance can ignore a viewport that races a not-yet-applied
|
||||||
|
/// edit (symmetric with `StyleSpans::generation`).
|
||||||
|
///
|
||||||
|
/// Only emitted by sessions that negotiated `semantic_render`;
|
||||||
|
/// a non-semantic session never sends it. M11.1 declares the
|
||||||
|
/// wire shape; the instance-side consumer is wired with the
|
||||||
|
/// projection seam in M11.2.
|
||||||
|
Viewport {
|
||||||
|
/// Which frontend's viewport this is.
|
||||||
|
frontend_id: FrontendId,
|
||||||
|
/// Which buffer the visible range indexes into.
|
||||||
|
buffer_id: crate::buffer::BufferId,
|
||||||
|
/// Half-open byte range currently on screen.
|
||||||
|
visible: ByteRange,
|
||||||
|
/// CRDT generation the frontend computed `visible` against.
|
||||||
|
generation: u64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FrontendEvent {
|
impl FrontendEvent {
|
||||||
|
|
@ -334,7 +366,8 @@ impl FrontendEvent {
|
||||||
| Self::FocusGained(frontend_id)
|
| Self::FocusGained(frontend_id)
|
||||||
| Self::FocusLost(frontend_id)
|
| Self::FocusLost(frontend_id)
|
||||||
| Self::Detach(frontend_id)
|
| Self::Detach(frontend_id)
|
||||||
| Self::CrdtOp { frontend_id, .. } => *frontend_id,
|
| Self::CrdtOp { frontend_id, .. }
|
||||||
|
| Self::Viewport { frontend_id, .. } => *frontend_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -581,6 +614,82 @@ pub enum InstanceMessage {
|
||||||
/// converts to `usize` for the loro API.
|
/// converts to `usize` for the loro API.
|
||||||
byte_pos: crate::rope::Position,
|
byte_pos: crate::rope::Position,
|
||||||
},
|
},
|
||||||
|
/// T M11.1 — syntax + face styling over the semantic frontend's
|
||||||
|
/// current viewport range. `generation` ties the spans to a CRDT
|
||||||
|
/// version so the frontend can discard styling that predates an
|
||||||
|
/// edit it has already applied optimistically. Ships **no text** —
|
||||||
|
/// the frontend holds the rope via the `crdt_replica` machinery
|
||||||
|
/// and interprets these spans over it. Diffs against the previous
|
||||||
|
/// frame the way `CellDelta` does today: changed spans only,
|
||||||
|
/// scoped to the range the frontend last declared via
|
||||||
|
/// `FrontendEvent::Viewport`.
|
||||||
|
///
|
||||||
|
/// Gated on negotiated `semantic_render`; never sent to a grid
|
||||||
|
/// session (the daemon's per-session outgoing filter — wired with
|
||||||
|
/// the producer in M11.2 — never emits it there, so postcard's
|
||||||
|
/// hard-error on unknown variants is mooted exactly as it is for
|
||||||
|
/// `CursorByte`).
|
||||||
|
StyleSpans {
|
||||||
|
/// Buffer these spans interpret.
|
||||||
|
buffer_id: crate::buffer::BufferId,
|
||||||
|
/// CRDT generation the spans were computed against.
|
||||||
|
generation: u64,
|
||||||
|
/// Styled byte runs, scoped to the declared viewport.
|
||||||
|
spans: Vec<StyleSpan>,
|
||||||
|
},
|
||||||
|
/// T M11.1 — diagnostics, search hits, current-line, and any
|
||||||
|
/// other "this region means something" overlay, as offset ranges
|
||||||
|
/// plus a kind. Peer selection is **not** here — it stays on the
|
||||||
|
/// existing `PresenceUpdate` path. Gated on `semantic_render`.
|
||||||
|
Decorations {
|
||||||
|
/// Buffer these decorations apply to.
|
||||||
|
buffer_id: crate::buffer::BufferId,
|
||||||
|
/// Decoration regions for the declared viewport.
|
||||||
|
decorations: Vec<Decoration>,
|
||||||
|
},
|
||||||
|
/// T M11.1 — inlay hints, blame, lens, virtual text. Anchored at
|
||||||
|
/// a single offset with a placement; occupies no document bytes —
|
||||||
|
/// the frontend interleaves it at layout time. Gated on
|
||||||
|
/// `semantic_render`.
|
||||||
|
InlineAdornments {
|
||||||
|
/// Buffer these adornments annotate.
|
||||||
|
buffer_id: crate::buffer::BufferId,
|
||||||
|
/// The adornment items for the declared viewport.
|
||||||
|
items: Vec<InlineAdornment>,
|
||||||
|
},
|
||||||
|
/// T M11.1 — diff zones, folded-region placeholders, anything
|
||||||
|
/// occupying its own vertical band. Anchored to the offset of the
|
||||||
|
/// line it precedes or replaces; the frontend allocates the
|
||||||
|
/// vertical space. Gated on `semantic_render`.
|
||||||
|
BlockAdornments {
|
||||||
|
/// Buffer these adornments annotate.
|
||||||
|
buffer_id: crate::buffer::BufferId,
|
||||||
|
/// The block items for the declared viewport.
|
||||||
|
items: Vec<BlockAdornment>,
|
||||||
|
},
|
||||||
|
/// T M11.1 — the instance's authoritative fold set as document
|
||||||
|
/// facts. Folding is an instance command-semantics concern (Lua
|
||||||
|
/// can fold); the visual collapse is a frontend layout concern —
|
||||||
|
/// the frontend renders the placeholder and adjusts its own
|
||||||
|
/// layout. Gated on `semantic_render`.
|
||||||
|
FoldState {
|
||||||
|
/// Buffer whose fold set this is.
|
||||||
|
buffer_id: crate::buffer::BufferId,
|
||||||
|
/// Folded byte ranges.
|
||||||
|
folds: Vec<ByteRange>,
|
||||||
|
},
|
||||||
|
/// T M11.1 — out-of-band content an adornment refers to (images,
|
||||||
|
/// blame avatars). Sent once, referenced by `handle`, so it is
|
||||||
|
/// not re-shipped per frame. Gated on `semantic_render`.
|
||||||
|
ResourceOffer {
|
||||||
|
/// Stable handle adornments reference via
|
||||||
|
/// [`AdornmentContent::Resource`].
|
||||||
|
handle: u64,
|
||||||
|
/// MIME type of `body`.
|
||||||
|
mime: String,
|
||||||
|
/// Inline bytes or a URI the frontend resolves itself.
|
||||||
|
body: ResourceBody,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flat selection state for the wire.
|
/// Flat selection state for the wire.
|
||||||
|
|
@ -604,6 +713,153 @@ pub struct SelectionSnapshot {
|
||||||
pub active: crate::rope::Position,
|
pub active: crate::rope::Position,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// T M11.1 — Semantic-frontend projection types
|
||||||
|
//
|
||||||
|
// The payloads of the `InstanceMessage::StyleSpans` … `ResourceOffer`
|
||||||
|
// family and `FrontendEvent::Viewport`. Everything is anchored in
|
||||||
|
// **byte offsets** (consistent with `CursorByte`): line/col is a
|
||||||
|
// frontend rendering concern, CRDT position is replica-internal. The
|
||||||
|
// instance never learns a pixel — see the contract boundary in
|
||||||
|
// `docs/semantic-frontend-protocol.md`.
|
||||||
|
//
|
||||||
|
// The variant/kind sets here are provisional and co-evolve within the
|
||||||
|
// M11 arc behind the `semantic_render` capability + protocol v3,
|
||||||
|
// exactly as the CRDT op shape evolved M10.5→M10.10 behind
|
||||||
|
// `crdt_replica`. They are not a wire-compat hazard for non-semantic
|
||||||
|
// sessions: the daemon's per-session outgoing filter (wired with the
|
||||||
|
// producer in M11.2) never emits the family to a session that didn't
|
||||||
|
// negotiate `semantic_render`, so postcard's hard-error on unknown
|
||||||
|
// variants is mooted exactly as it is for `CursorByte`.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Half-open byte range `[start, end)` into a buffer's rope, matching
|
||||||
|
/// the rope's own range convention.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct ByteRange {
|
||||||
|
/// Inclusive start byte offset.
|
||||||
|
pub start: u64,
|
||||||
|
/// Exclusive end byte offset.
|
||||||
|
pub end: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One run of buffer bytes carrying a resolved visual style. The
|
||||||
|
/// instance is the single syntax/face authority; the frontend lays
|
||||||
|
/// the style out locally over rope text it already holds. Reuses
|
||||||
|
/// [`crate::cell::Style`] so the grid and semantic projections share
|
||||||
|
/// one style vocabulary.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct StyleSpan {
|
||||||
|
/// Byte range this style covers.
|
||||||
|
pub range: ByteRange,
|
||||||
|
/// The resolved style (syntax highlight ∘ faces ∘ theme).
|
||||||
|
pub style: crate::cell::Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a [`Decoration`] region *means*. Provisional variant set (see
|
||||||
|
/// the module-section note above). Peer selection is deliberately
|
||||||
|
/// absent — it stays on the `PresenceUpdate` path.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum DecorationKind {
|
||||||
|
/// LSP diagnostic, error severity.
|
||||||
|
DiagnosticError,
|
||||||
|
/// LSP diagnostic, warning severity.
|
||||||
|
DiagnosticWarning,
|
||||||
|
/// LSP diagnostic, information severity.
|
||||||
|
DiagnosticInfo,
|
||||||
|
/// LSP diagnostic, hint severity.
|
||||||
|
DiagnosticHint,
|
||||||
|
/// The local selection region.
|
||||||
|
Selection,
|
||||||
|
/// A non-active search match.
|
||||||
|
SearchMatch,
|
||||||
|
/// The currently-focused search match.
|
||||||
|
SearchMatchActive,
|
||||||
|
/// The line containing the cursor.
|
||||||
|
CurrentLine,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A byte range tagged with what it means. The frontend decides how
|
||||||
|
/// to paint each [`DecorationKind`] (squiggle, highlight, gutter
|
||||||
|
/// mark) — the instance only states the fact.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct Decoration {
|
||||||
|
/// Byte range the decoration covers.
|
||||||
|
pub range: ByteRange,
|
||||||
|
/// What the region signifies.
|
||||||
|
pub kind: DecorationKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where an [`InlineAdornment`] sits relative to its anchor offset.
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum AdornmentPlacement {
|
||||||
|
/// On its own, before the line containing `at`.
|
||||||
|
BeforeLine,
|
||||||
|
/// At the end of the line containing `at`.
|
||||||
|
EndOfLine,
|
||||||
|
/// Inline, exactly at the byte offset `at`.
|
||||||
|
AtOffset,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adornment payload: either inline styled text, or a handle into a
|
||||||
|
/// previously-sent [`InstanceMessage::ResourceOffer`] so out-of-band
|
||||||
|
/// content (images, blame avatars) is shipped once, not per frame.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum AdornmentContent {
|
||||||
|
/// Styled virtual text.
|
||||||
|
Text {
|
||||||
|
/// The virtual text to display.
|
||||||
|
text: String,
|
||||||
|
/// Its style.
|
||||||
|
style: crate::cell::Style,
|
||||||
|
},
|
||||||
|
/// A handle into a `ResourceOffer`.
|
||||||
|
Resource {
|
||||||
|
/// The offered resource's handle.
|
||||||
|
handle: u64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Virtual text occupying no document bytes (inlay hints, blame,
|
||||||
|
/// lens). Anchored at a single offset; the frontend interleaves it
|
||||||
|
/// at layout time.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct InlineAdornment {
|
||||||
|
/// Buffer byte offset this adornment anchors to.
|
||||||
|
pub at: u64,
|
||||||
|
/// Placement relative to `at`.
|
||||||
|
pub placement: AdornmentPlacement,
|
||||||
|
/// What to render.
|
||||||
|
pub content: AdornmentContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Content occupying its own vertical band (diff zones, folded-region
|
||||||
|
/// placeholders). Anchored to the offset of the line it precedes or
|
||||||
|
/// replaces. `replaces` is `Some` when the band stands in for a
|
||||||
|
/// collapsed region (the frontend renders the placeholder instead of
|
||||||
|
/// that range), `None` for an additive band. The frontend allocates
|
||||||
|
/// the vertical space — the instance never dictates pixel height.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct BlockAdornment {
|
||||||
|
/// Buffer byte offset of the line this band precedes/replaces.
|
||||||
|
pub at: u64,
|
||||||
|
/// The byte range this band stands in for, if it replaces one.
|
||||||
|
pub replaces: Option<ByteRange>,
|
||||||
|
/// What to render in the band.
|
||||||
|
pub content: AdornmentContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The body of an [`InstanceMessage::ResourceOffer`] — inline bytes
|
||||||
|
/// for small payloads, or a URI the frontend resolves itself for
|
||||||
|
/// large or remote resources.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum ResourceBody {
|
||||||
|
/// The resource bytes, carried inline.
|
||||||
|
Inline(Vec<u8>),
|
||||||
|
/// A URI the frontend fetches/resolves on its own.
|
||||||
|
Uri(String),
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Attachment
|
// Attachment
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -1117,20 +1373,34 @@ impl AttachmentHandle {
|
||||||
/// accepted by v1.0 binaries; CRDT-only message variants
|
/// accepted by v1.0 binaries; CRDT-only message variants
|
||||||
/// (`InstanceMessage::CrdtOp`, `FrontendEvent::CrdtOp`) are filtered
|
/// (`InstanceMessage::CrdtOp`, `FrontendEvent::CrdtOp`) are filtered
|
||||||
/// per-session for v1 negotiated sessions.
|
/// per-session for v1 negotiated sessions.
|
||||||
pub const PROTOCOL_VERSION: u32 = 2;
|
///
|
||||||
|
/// T M11.1: bumped from 2 to 3. The v1.0 wire (version 2) remains
|
||||||
|
/// accepted; the semantic-frontend variant family
|
||||||
|
/// (`InstanceMessage::StyleSpans` … `ResourceOffer`,
|
||||||
|
/// `FrontendEvent::Viewport`) is filtered per-session for sessions
|
||||||
|
/// that did not negotiate `semantic_render`. Mechanically identical
|
||||||
|
/// to the M10.5 bump: the slice-membership handshake check (not
|
||||||
|
/// strict equality) means v0.1/v1.0 binaries keep connecting
|
||||||
|
/// unchanged, and the new variants simply existing in the enums is
|
||||||
|
/// not a wire-compat issue for non-semantic sessions because the
|
||||||
|
/// daemon never emits them to those sessions.
|
||||||
|
pub const PROTOCOL_VERSION: u32 = 3;
|
||||||
|
|
||||||
/// T M10.5: the set of protocol versions a v1.0 binary accepts on
|
/// T M10.5: the set of protocol versions a v1.0 binary accepts on
|
||||||
/// the wire. v0.1 binaries only accepted `[1]`; v1.0 binaries accept
|
/// the wire. v0.1 binaries only accepted `[1]`; v1.0 binaries accept
|
||||||
/// `[1, 2]` so the version asymmetry the §sec:m10-backward-compat
|
/// `[1, 2]` so the version asymmetry the §sec:m10-backward-compat
|
||||||
/// spec section describes is handled symmetrically on both sides.
|
/// spec section describes is handled symmetrically on both sides.
|
||||||
///
|
///
|
||||||
/// The handshake check is "is the peer's `protocol_version` present in
|
/// T M11.1: extended to `[1, 2, 3]`. v1.1 binaries accept the v0.1
|
||||||
|
/// (1), v1.0 (2), and semantic-frontend (3) wires. The check remains
|
||||||
|
/// slice membership — "is the peer's `protocol_version` present in
|
||||||
/// this slice?" — not strict equality on `PROTOCOL_VERSION`. The
|
/// this slice?" — not strict equality on `PROTOCOL_VERSION`. The
|
||||||
/// session's negotiated version (the peer's) is recorded for
|
/// session's negotiated version (the peer's) is recorded for
|
||||||
/// downstream filtering: v1 sessions don't receive
|
/// downstream filtering: v1 sessions don't receive
|
||||||
/// `InstanceMessage::CrdtOp` / `PresenceUpdate` messages even from
|
/// `InstanceMessage::CrdtOp` / `PresenceUpdate` messages even from
|
||||||
/// a v2 daemon.
|
/// a v3 daemon, and only sessions that negotiated `semantic_render`
|
||||||
pub const SUPPORTED_PROTOCOL_VERSIONS: &[u32] = &[1, 2];
|
/// receive the `SemanticFrame` variant family.
|
||||||
|
pub const SUPPORTED_PROTOCOL_VERSIONS: &[u32] = &[1, 2, 3];
|
||||||
|
|
||||||
/// T M10.5: predicate for the handshake check. Returns `true` if
|
/// T M10.5: predicate for the handshake check. Returns `true` if
|
||||||
/// `peer_version` is in [`SUPPORTED_PROTOCOL_VERSIONS`].
|
/// `peer_version` is in [`SUPPORTED_PROTOCOL_VERSIONS`].
|
||||||
|
|
@ -1234,6 +1504,23 @@ pub struct InstanceCapabilities {
|
||||||
/// broadcast routing for CRDT ops wires up in this milestone.
|
/// broadcast routing for CRDT ops wires up in this milestone.
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub crdt_replica: bool,
|
pub crdt_replica: bool,
|
||||||
|
/// T M11.1: instance can produce the semantic-frontend variant
|
||||||
|
/// family (`InstanceMessage::StyleSpans` … `ResourceOffer`) and
|
||||||
|
/// consume `FrontendEvent::Viewport`.
|
||||||
|
///
|
||||||
|
/// Default is `false` — unlike `crdt_replica`, this does *not*
|
||||||
|
/// track the `crdt` Cargo feature. M11.1 declares the bit
|
||||||
|
/// position and the negotiation mechanics; the instance-side
|
||||||
|
/// projection seam (`SemanticRenderState`, the producer) is
|
||||||
|
/// M11.2. Advertising `true` before the producer exists would be
|
||||||
|
/// wire-protocol false advertising — the M10.5→M10.7 "bits false
|
||||||
|
/// until the path is wired" discipline, applied to M11. The
|
||||||
|
/// default flips to `cfg!(feature = "crdt")` when M11.2 lands the
|
||||||
|
/// projection seam (semantic sessions are also text replicas, so
|
||||||
|
/// the dependency on `crdt_replica` makes the feature gate the
|
||||||
|
/// natural ceiling).
|
||||||
|
#[serde(default)]
|
||||||
|
pub semantic_render: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clippy in non-CRDT builds notes that `cfg!(feature = "crdt")`
|
// Clippy in non-CRDT builds notes that `cfg!(feature = "crdt")`
|
||||||
|
|
@ -1255,9 +1542,19 @@ impl Default for InstanceCapabilities {
|
||||||
// is also a CRDT replica; gating both on the same feature
|
// is also a CRDT replica; gating both on the same feature
|
||||||
// keeps the daemon's advertised capabilities consistent
|
// keeps the daemon's advertised capabilities consistent
|
||||||
// with what it can actually do.
|
// with what it can actually do.
|
||||||
|
//
|
||||||
|
// T M11.1 — `semantic_render` defaults to `false`
|
||||||
|
// unconditionally (not gated on the `crdt` feature like the
|
||||||
|
// two bits above). There is no projection-seam producer yet;
|
||||||
|
// the producer and the feature-tracking default flip are
|
||||||
|
// M11.2 scope. Until then a frontend declaring
|
||||||
|
// `semantic_render: true` gets `Goodbye(CapabilityMismatch)`,
|
||||||
|
// exactly as `multi_frontend`/`crdt_replica` did between
|
||||||
|
// M10.5 and the M10.8 Day-4 flip.
|
||||||
Self {
|
Self {
|
||||||
multi_frontend: cfg!(feature = "crdt"),
|
multi_frontend: cfg!(feature = "crdt"),
|
||||||
crdt_replica: cfg!(feature = "crdt"),
|
crdt_replica: cfg!(feature = "crdt"),
|
||||||
|
semantic_render: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1322,6 +1619,20 @@ pub struct FrontendCapabilities {
|
||||||
/// negotiation; M10.5 declares the bit position.
|
/// negotiation; M10.5 declares the bit position.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub crdt_replica: bool,
|
pub crdt_replica: bool,
|
||||||
|
/// T M11.1: frontend is a semantic (layout-local) renderer — it
|
||||||
|
/// consumes the `InstanceMessage::StyleSpans` … `ResourceOffer`
|
||||||
|
/// family and emits `FrontendEvent::Viewport`. false for v0.1 and
|
||||||
|
/// v1.0 grid/TUI frontends; a future GPU/GUI frontend opts in.
|
||||||
|
///
|
||||||
|
/// A semantic frontend is *required* to also be a text replica:
|
||||||
|
/// the semantic frame ships no text, so the frontend must hold
|
||||||
|
/// the rope locally via the `crdt_replica` machinery. This
|
||||||
|
/// dependency is enforced in [`negotiate_capabilities`], not just
|
||||||
|
/// documented — declaring `semantic_render: true` without
|
||||||
|
/// `crdt_replica: true` is a capability mismatch, never a silent
|
||||||
|
/// degrade.
|
||||||
|
#[serde(default)]
|
||||||
|
pub semantic_render: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// T M10.7 — the negotiated capability bits for one attached session.
|
/// T M10.7 — the negotiated capability bits for one attached session.
|
||||||
|
|
@ -1349,11 +1660,21 @@ pub struct NegotiatedCapabilities {
|
||||||
/// `crdt_replica = true`. The daemon's outgoing-message filter for
|
/// `crdt_replica = true`. The daemon's outgoing-message filter for
|
||||||
/// `CrdtOp` consults this in M10.8.
|
/// `CrdtOp` consults this in M10.8.
|
||||||
pub crdt_replica: bool,
|
pub crdt_replica: bool,
|
||||||
|
/// T M11.1 — session uses the semantic projection: it
|
||||||
|
/// produces/consumes the `InstanceMessage::StyleSpans` …
|
||||||
|
/// `ResourceOffer` family and `FrontendEvent::Viewport`. True iff
|
||||||
|
/// both sides declared `semantic_render = true` *and* the session
|
||||||
|
/// also negotiated `crdt_replica = true` (a semantic session is a
|
||||||
|
/// text replica; see [`negotiate_capabilities`]). The daemon's
|
||||||
|
/// per-session outgoing filter gates the entire semantic family
|
||||||
|
/// on this bit — wired with the producer in M11.2.
|
||||||
|
pub semantic_render: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// T M10.7 — pure-function capability negotiation.
|
/// T M10.7 — pure-function capability negotiation.
|
||||||
///
|
///
|
||||||
/// For each negotiated bit (`multi_frontend`, `crdt_replica`):
|
/// For each negotiated bit (`multi_frontend`, `crdt_replica`,
|
||||||
|
/// `semantic_render`):
|
||||||
///
|
///
|
||||||
/// | Frontend wants | Instance has | Result |
|
/// | Frontend wants | Instance has | Result |
|
||||||
/// |----------------|--------------|--------|
|
/// |----------------|--------------|--------|
|
||||||
|
|
@ -1366,7 +1687,23 @@ pub struct NegotiatedCapabilities {
|
||||||
/// (returns `Err`). Otherwise the negotiated bits are returned as
|
/// (returns `Err`). Otherwise the negotiated bits are returned as
|
||||||
/// [`NegotiatedCapabilities`]. The `Err` form gathers ALL missing
|
/// [`NegotiatedCapabilities`]. The `Err` form gathers ALL missing
|
||||||
/// bits into one `CapabilityMismatch` — one round-trip carries the
|
/// bits into one `CapabilityMismatch` — one round-trip carries the
|
||||||
/// complete picture rather than serial rejections.
|
/// complete picture rather than serial rejections. Missing bits are
|
||||||
|
/// ordered `multi_frontend`, `crdt_replica`, `semantic_render` for
|
||||||
|
/// deterministic wire output.
|
||||||
|
///
|
||||||
|
/// # T M11.1 — the `semantic_render ⇒ crdt_replica` dependency
|
||||||
|
///
|
||||||
|
/// A semantic-render session ships no text on the semantic frame;
|
||||||
|
/// the frontend holds the rope locally via the `crdt_replica`
|
||||||
|
/// machinery (`BufferSnapshot` to bootstrap, `CrdtOp` to stay live).
|
||||||
|
/// So `semantic_render` is only coherent on a session that also
|
||||||
|
/// negotiated `crdt_replica`. When the AND-rule would yield
|
||||||
|
/// `semantic_render = true` but the session did not also negotiate
|
||||||
|
/// `crdt_replica = true`, this function rejects with
|
||||||
|
/// `"semantic_render"` in `missing` rather than silently degrading
|
||||||
|
/// the session to a text-only replica. The rejected identifier is
|
||||||
|
/// `"semantic_render"` (the capability whose precondition is unmet),
|
||||||
|
/// not `"crdt_replica"`.
|
||||||
///
|
///
|
||||||
/// # Wire-format stability
|
/// # Wire-format stability
|
||||||
///
|
///
|
||||||
|
|
@ -1396,10 +1733,31 @@ pub fn negotiate_capabilities(
|
||||||
}
|
}
|
||||||
(a, b) => a && b,
|
(a, b) => a && b,
|
||||||
};
|
};
|
||||||
|
let semantic_render = match (frontend.semantic_render, instance.semantic_render) {
|
||||||
|
(true, false) => {
|
||||||
|
missing.push("semantic_render".to_string());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
(a, b) => a && b,
|
||||||
|
};
|
||||||
|
// T M11.1 — dependency rule. A semantic session is a text replica
|
||||||
|
// (the semantic frame carries no text). If both sides declared
|
||||||
|
// `semantic_render` but the session did not also negotiate
|
||||||
|
// `crdt_replica`, reject rather than silently degrade. Guard
|
||||||
|
// against a duplicate push: the only path where `semantic_render`
|
||||||
|
// is already in `missing` is the `(true, false)` arm above, which
|
||||||
|
// also sets the local `semantic_render` to false, so the
|
||||||
|
// condition below cannot re-fire for that case — but the explicit
|
||||||
|
// membership check keeps this robust against future reordering.
|
||||||
|
if semantic_render && !crdt_replica && !missing.iter().any(|m| m == "semantic_render") {
|
||||||
|
missing.push("semantic_render".to_string());
|
||||||
|
}
|
||||||
|
let semantic_render = semantic_render && crdt_replica;
|
||||||
if missing.is_empty() {
|
if missing.is_empty() {
|
||||||
Ok(NegotiatedCapabilities {
|
Ok(NegotiatedCapabilities {
|
||||||
multi_frontend,
|
multi_frontend,
|
||||||
crdt_replica,
|
crdt_replica,
|
||||||
|
semantic_render,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(GoodbyeReason::CapabilityMismatch { missing })
|
Err(GoodbyeReason::CapabilityMismatch { missing })
|
||||||
|
|
@ -2577,23 +2935,27 @@ mod tests {
|
||||||
// --- M5.5a handshake & postcard round-trips ---
|
// --- M5.5a handshake & postcard round-trips ---
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn protocol_version_is_two_for_v10() {
|
fn protocol_version_is_three_for_v11() {
|
||||||
// Pin the value: T M10.5 bumped from 1 to 2. The v1.0 wire
|
// Pin the value: T M10.5 bumped 1→2 (v1.0 wire: CrdtOp /
|
||||||
// adds CrdtOp / PresenceUpdate variants; the v1.0 binary
|
// PresenceUpdate). T M11.1 bumped 2→3 (v1.1 wire: the
|
||||||
// serves both v1 and v2 sessions per §sec:m10-backward-compat.
|
// SemanticFrame family + FrontendEvent::Viewport). The v1.1
|
||||||
assert_eq!(PROTOCOL_VERSION, 2);
|
// binary serves v1, v2, and v3 sessions — the slice-membership
|
||||||
|
// handshake makes the relaxation symmetric, exactly as M10.5
|
||||||
|
// did for §sec:m10-backward-compat.
|
||||||
|
assert_eq!(PROTOCOL_VERSION, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn supported_protocol_versions_includes_one_and_two() {
|
fn supported_protocol_versions_includes_one_two_three() {
|
||||||
// T M10.5: v1.0 binaries accept both wire versions during the
|
// T M10.5: v1.0 binaries accept v1+v2. T M11.1: v1.1 binaries
|
||||||
// handshake. v0.1 binaries (with their strict-equality check)
|
// accept v1+v2+v3. The check is slice membership, not strict
|
||||||
// accepted only v1; this is the symmetric relaxation that
|
// equality, so v0.1/v1.0 binaries keep connecting to v1.1
|
||||||
// makes §sec:m10-backward-compat hold once both binaries ship.
|
// binaries unchanged. v4+ is rejected until the next bump.
|
||||||
assert!(is_supported_protocol_version(1));
|
assert!(is_supported_protocol_version(1));
|
||||||
assert!(is_supported_protocol_version(2));
|
assert!(is_supported_protocol_version(2));
|
||||||
|
assert!(is_supported_protocol_version(3));
|
||||||
assert!(!is_supported_protocol_version(0));
|
assert!(!is_supported_protocol_version(0));
|
||||||
assert!(!is_supported_protocol_version(3));
|
assert!(!is_supported_protocol_version(4));
|
||||||
assert!(!is_supported_protocol_version(u32::MAX));
|
assert!(!is_supported_protocol_version(u32::MAX));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2629,6 +2991,7 @@ mod tests {
|
||||||
terminal_kind: Some("xterm-256color".into()),
|
terminal_kind: Some("xterm-256color".into()),
|
||||||
multi_frontend: false,
|
multi_frontend: false,
|
||||||
crdt_replica: false,
|
crdt_replica: false,
|
||||||
|
semantic_render: false,
|
||||||
},
|
},
|
||||||
initial_size: CellSize::new(50, 200),
|
initial_size: CellSize::new(50, 200),
|
||||||
};
|
};
|
||||||
|
|
@ -2945,10 +3308,11 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn m10_5_handshake_matrix_versions_outside_range_rejected() {
|
fn m10_5_handshake_matrix_versions_outside_range_rejected() {
|
||||||
// v1 daemon's strict-equality behavior is documented at the
|
// v1 daemon's strict-equality behavior is documented at the
|
||||||
// v0.1 code level (different binary); v2 daemon's range check
|
// v0.1 code level (different binary); the v1.1 daemon's range
|
||||||
// rejects v3+ until v0.2 ships.
|
// check accepts v1/v2/v3 (T M11.1 added v3) and rejects v4+
|
||||||
|
// until the next protocol bump.
|
||||||
assert!(!is_supported_protocol_version(0));
|
assert!(!is_supported_protocol_version(0));
|
||||||
assert!(!is_supported_protocol_version(3));
|
assert!(!is_supported_protocol_version(4));
|
||||||
assert!(!is_supported_protocol_version(u32::MAX));
|
assert!(!is_supported_protocol_version(u32::MAX));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3157,6 +3521,36 @@ mod tests {
|
||||||
InstanceCapabilities {
|
InstanceCapabilities {
|
||||||
multi_frontend,
|
multi_frontend,
|
||||||
crdt_replica,
|
crdt_replica,
|
||||||
|
..InstanceCapabilities::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// T M11.1 — caps builder that also sets `semantic_render`, for
|
||||||
|
/// the semantic-negotiation matrix. The 2-arg `inst_caps` keeps
|
||||||
|
/// `semantic_render` at its `Default` (`false`) so the existing
|
||||||
|
/// M10.7 matrix tests are untouched.
|
||||||
|
fn inst_caps_s(
|
||||||
|
multi_frontend: bool,
|
||||||
|
crdt_replica: bool,
|
||||||
|
semantic_render: bool,
|
||||||
|
) -> InstanceCapabilities {
|
||||||
|
InstanceCapabilities {
|
||||||
|
multi_frontend,
|
||||||
|
crdt_replica,
|
||||||
|
semantic_render,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn front_caps_s(
|
||||||
|
multi_frontend: bool,
|
||||||
|
crdt_replica: bool,
|
||||||
|
semantic_render: bool,
|
||||||
|
) -> FrontendCapabilities {
|
||||||
|
FrontendCapabilities {
|
||||||
|
multi_frontend,
|
||||||
|
crdt_replica,
|
||||||
|
semantic_render,
|
||||||
|
..FrontendCapabilities::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3287,4 +3681,232 @@ mod tests {
|
||||||
other => panic!("expected CapabilityMismatch, got {other:?}"),
|
other => panic!("expected CapabilityMismatch, got {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// T M11.1 — semantic_render negotiation matrix + the
|
||||||
|
// semantic_render ⇒ crdt_replica dependency rule.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negotiate_semantic_render_both_sides_with_crdt() {
|
||||||
|
// The only success shape: both sides want semantic_render AND
|
||||||
|
// the session also negotiates crdt_replica (the text-replica
|
||||||
|
// dependency). semantic_render true implies crdt_replica true.
|
||||||
|
let res = negotiate_capabilities(
|
||||||
|
&front_caps_s(true, true, true),
|
||||||
|
&inst_caps_s(true, true, true),
|
||||||
|
)
|
||||||
|
.expect("ok");
|
||||||
|
assert!(res.crdt_replica);
|
||||||
|
assert!(res.semantic_render);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negotiate_semantic_render_frontend_silent() {
|
||||||
|
// Instance offers semantic_render; frontend doesn't ask. The
|
||||||
|
// subset (no semantic projection) is accepted, no error —
|
||||||
|
// identical posture to the multi_frontend/crdt_replica
|
||||||
|
// "frontend silent" case.
|
||||||
|
let res = negotiate_capabilities(
|
||||||
|
&front_caps_s(false, false, false),
|
||||||
|
&inst_caps_s(true, true, true),
|
||||||
|
)
|
||||||
|
.expect("ok");
|
||||||
|
assert!(!res.semantic_render);
|
||||||
|
assert!(!res.crdt_replica);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negotiate_semantic_render_frontend_wants_instance_lacks() {
|
||||||
|
// Frontend wants crdt+semantic; instance has crdt but not the
|
||||||
|
// semantic projection (the M11.1 reality until M11.2 flips
|
||||||
|
// the instance default). Only semantic_render is missing.
|
||||||
|
let err = negotiate_capabilities(
|
||||||
|
&front_caps_s(false, true, true),
|
||||||
|
&inst_caps_s(false, true, false),
|
||||||
|
)
|
||||||
|
.expect_err("should mismatch");
|
||||||
|
match err {
|
||||||
|
GoodbyeReason::CapabilityMismatch { missing } => {
|
||||||
|
assert_eq!(missing, vec!["semantic_render".to_string()]);
|
||||||
|
}
|
||||||
|
other => panic!("expected CapabilityMismatch, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negotiate_semantic_render_requires_crdt_replica_dependency() {
|
||||||
|
// Both sides declare semantic_render, but the frontend did
|
||||||
|
// NOT request crdt_replica. The AND-rule alone would yield
|
||||||
|
// semantic_render=true; the dependency rule rejects instead
|
||||||
|
// of silently degrading to a text-only replica. The rejected
|
||||||
|
// identifier is "semantic_render" (the capability whose
|
||||||
|
// precondition is unmet), not "crdt_replica".
|
||||||
|
let err = negotiate_capabilities(
|
||||||
|
&front_caps_s(false, false, true),
|
||||||
|
&inst_caps_s(false, true, true),
|
||||||
|
)
|
||||||
|
.expect_err("should mismatch");
|
||||||
|
match err {
|
||||||
|
GoodbyeReason::CapabilityMismatch { missing } => {
|
||||||
|
assert_eq!(missing, vec!["semantic_render".to_string()]);
|
||||||
|
}
|
||||||
|
other => panic!("expected CapabilityMismatch, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negotiate_semantic_render_dependency_orders_after_crdt_replica() {
|
||||||
|
// Frontend wants crdt+semantic; instance has the semantic
|
||||||
|
// projection but lacks crdt. crdt_replica fails the AND-rule
|
||||||
|
// (true,false) → "crdt_replica"; the dependency rule then
|
||||||
|
// appends "semantic_render". Deterministic order:
|
||||||
|
// [crdt_replica, semantic_render]. No duplicate semantic_render.
|
||||||
|
let err = negotiate_capabilities(
|
||||||
|
&front_caps_s(false, true, true),
|
||||||
|
&inst_caps_s(false, false, true),
|
||||||
|
)
|
||||||
|
.expect_err("should mismatch");
|
||||||
|
match err {
|
||||||
|
GoodbyeReason::CapabilityMismatch { missing } => {
|
||||||
|
assert_eq!(
|
||||||
|
missing,
|
||||||
|
vec!["crdt_replica".to_string(), "semantic_render".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => panic!("expected CapabilityMismatch, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negotiate_ok_semantic_render_always_implies_crdt_replica() {
|
||||||
|
// Invariant: every successful negotiation with
|
||||||
|
// semantic_render=true also has crdt_replica=true. Exhaust
|
||||||
|
// the 2³ declared-bit combinations on each side that the
|
||||||
|
// helpers can express; any Ok with semantic_render must carry
|
||||||
|
// crdt_replica.
|
||||||
|
for fc in [false, true] {
|
||||||
|
for fr in [false, true] {
|
||||||
|
for fs in [false, true] {
|
||||||
|
for ic in [false, true] {
|
||||||
|
for ir in [false, true] {
|
||||||
|
for is in [false, true] {
|
||||||
|
if let Ok(neg) = negotiate_capabilities(
|
||||||
|
&front_caps_s(fc, fr, fs),
|
||||||
|
&inst_caps_s(ic, ir, is),
|
||||||
|
) && neg.semantic_render
|
||||||
|
{
|
||||||
|
assert!(
|
||||||
|
neg.crdt_replica,
|
||||||
|
"semantic_render without crdt_replica leaked through \
|
||||||
|
negotiation: front=({fc},{fr},{fs}) inst=({ic},{ir},{is})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negotiate_two_arg_helpers_default_semantic_render_false() {
|
||||||
|
// Regression: the M10.7 matrix uses the 2-arg helpers, which
|
||||||
|
// must keep semantic_render at its Default (false) so adding
|
||||||
|
// the bit did not perturb existing negotiation outcomes.
|
||||||
|
let res =
|
||||||
|
negotiate_capabilities(&front_caps(true, true), &inst_caps(true, true)).expect("ok");
|
||||||
|
assert!(res.multi_frontend);
|
||||||
|
assert!(res.crdt_replica);
|
||||||
|
assert!(!res.semantic_render);
|
||||||
|
}
|
||||||
|
|
||||||
|
// T M11.1 — postcard round-trips for the SemanticFrame family and
|
||||||
|
// FrontendEvent::Viewport. Mirrors the M10.x variant round-trip
|
||||||
|
// tests: encode → decode → structural equality.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn semantic_frame_family_round_trips_through_postcard() {
|
||||||
|
let bid = crate::buffer::BufferId::next();
|
||||||
|
let msgs = vec![
|
||||||
|
InstanceMessage::StyleSpans {
|
||||||
|
buffer_id: bid,
|
||||||
|
generation: 7,
|
||||||
|
spans: vec![StyleSpan {
|
||||||
|
range: ByteRange { start: 0, end: 12 },
|
||||||
|
style: crate::cell::Style::default(),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
InstanceMessage::Decorations {
|
||||||
|
buffer_id: bid,
|
||||||
|
decorations: vec![
|
||||||
|
Decoration {
|
||||||
|
range: ByteRange { start: 3, end: 9 },
|
||||||
|
kind: DecorationKind::DiagnosticError,
|
||||||
|
},
|
||||||
|
Decoration {
|
||||||
|
range: ByteRange { start: 20, end: 20 },
|
||||||
|
kind: DecorationKind::CurrentLine,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
InstanceMessage::InlineAdornments {
|
||||||
|
buffer_id: bid,
|
||||||
|
items: vec![InlineAdornment {
|
||||||
|
at: 42,
|
||||||
|
placement: AdornmentPlacement::EndOfLine,
|
||||||
|
content: AdornmentContent::Text {
|
||||||
|
text: "→ i32".to_string(),
|
||||||
|
style: crate::cell::Style::default(),
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
InstanceMessage::BlockAdornments {
|
||||||
|
buffer_id: bid,
|
||||||
|
items: vec![BlockAdornment {
|
||||||
|
at: 64,
|
||||||
|
replaces: Some(ByteRange { start: 64, end: 256 }),
|
||||||
|
content: AdornmentContent::Resource { handle: 1 },
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
InstanceMessage::FoldState {
|
||||||
|
buffer_id: bid,
|
||||||
|
folds: vec![ByteRange { start: 100, end: 400 }],
|
||||||
|
},
|
||||||
|
InstanceMessage::ResourceOffer {
|
||||||
|
handle: 1,
|
||||||
|
mime: "image/png".to_string(),
|
||||||
|
body: ResourceBody::Inline(vec![0x89, b'P', b'N', b'G']),
|
||||||
|
},
|
||||||
|
InstanceMessage::ResourceOffer {
|
||||||
|
handle: 2,
|
||||||
|
mime: "image/svg+xml".to_string(),
|
||||||
|
body: ResourceBody::Uri("file:///tmp/blame.svg".to_string()),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for msg in msgs {
|
||||||
|
let bytes = postcard::to_allocvec(&msg).expect("encode");
|
||||||
|
let decoded: InstanceMessage = postcard::from_bytes(&bytes).expect("decode");
|
||||||
|
assert_eq!(msg, decoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frontend_event_viewport_round_trips_through_postcard() {
|
||||||
|
let ev = FrontendEvent::Viewport {
|
||||||
|
frontend_id: FrontendId(4),
|
||||||
|
buffer_id: crate::buffer::BufferId::next(),
|
||||||
|
visible: ByteRange {
|
||||||
|
start: 1_024,
|
||||||
|
end: 4_096,
|
||||||
|
},
|
||||||
|
generation: 99,
|
||||||
|
};
|
||||||
|
let bytes = postcard::to_allocvec(&ev).expect("encode");
|
||||||
|
let decoded: FrontendEvent = postcard::from_bytes(&bytes).expect("decode");
|
||||||
|
assert_eq!(ev, decoded);
|
||||||
|
// The contract-boundary invariant, asserted structurally:
|
||||||
|
// frontend_id() must resolve for the new variant (it is part
|
||||||
|
// of the per-frontend routing alternation).
|
||||||
|
assert_eq!(decoded.frontend_id(), FrontendId(4));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,7 @@ mod tests {
|
||||||
terminal_kind: Some("xterm-256color".into()),
|
terminal_kind: Some("xterm-256color".into()),
|
||||||
multi_frontend: false,
|
multi_frontend: false,
|
||||||
crdt_replica: false,
|
crdt_replica: false,
|
||||||
|
semantic_render: false,
|
||||||
},
|
},
|
||||||
initial_size: crate::cell::CellSize::new(24, 80),
|
initial_size: crate::cell::CellSize::new(24, 80),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,7 @@ pub fn build_default_caps() -> FrontendCapabilities {
|
||||||
terminal_kind: Some("test".into()),
|
terminal_kind: Some("test".into()),
|
||||||
multi_frontend: false,
|
multi_frontend: false,
|
||||||
crdt_replica: false,
|
crdt_replica: false,
|
||||||
|
semantic_render: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,7 @@ impl Observer {
|
||||||
terminal_kind: Some("test-observer".into()),
|
terminal_kind: Some("test-observer".into()),
|
||||||
multi_frontend: true,
|
multi_frontend: true,
|
||||||
crdt_replica: true,
|
crdt_replica: true,
|
||||||
|
semantic_render: false,
|
||||||
};
|
};
|
||||||
let req = AttachRequest {
|
let req = AttachRequest {
|
||||||
protocol_version: PROTOCOL_VERSION,
|
protocol_version: PROTOCOL_VERSION,
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,7 @@ fn build_default_caps() -> FrontendCapabilities {
|
||||||
terminal_kind: Some("perf-gate".into()),
|
terminal_kind: Some("perf-gate".into()),
|
||||||
multi_frontend: false,
|
multi_frontend: false,
|
||||||
crdt_replica: false,
|
crdt_replica: false,
|
||||||
|
semantic_render: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue