diff --git a/Cargo.lock b/Cargo.lock index 9e307e3..7f4c9da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1249,6 +1249,7 @@ dependencies = [ "loro", "mlua", "nix 0.29.0", + "pmacs-protocol", "portable-pty", "postcard", "proptest", @@ -1272,6 +1273,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "pmacs-protocol" +version = "1.0.0" +dependencies = [ + "postcard", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "portable-pty" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 02294ef..5c4e2b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,19 @@ +[workspace] +# Session 1 of the pmacs-gpu arc: `pmacs-protocol` is the wire-types +# crate the post-v1.0 frontend (`pmacs-gpu`) will consume directly. The +# root `pmacs` package stays a workspace member (no file moves); the +# new crate lives under `pmacs-protocol/`. See +# `docs/pmacs-gpu-design.md`. +members = [".", "pmacs-protocol"] + +[workspace.dependencies] +# Shared between `pmacs` and `pmacs-protocol`. Pinned here so both +# crates use byte-identical postcard / serde versions — the wire +# format depends on it. +serde = { version = "1", features = ["derive"] } +postcard = { version = "1", features = ["use-std"] } +thiserror = "2" + [package] name = "pmacs" version = "1.0.0" @@ -60,7 +76,7 @@ crdt = ["dep:loro"] [dependencies] crossterm = "0.28" -thiserror = "2" +thiserror = { workspace = true } unicode-width = "0.2" # Work-stealing deque, MPMC channels, and parking primitives for the # M3 worker pool (spec §6.3). The umbrella crate re-exports @@ -70,8 +86,15 @@ crossbeam = "0.8" # trait, `rmp-serde` is the MessagePack codec the spec calls out by # name; in-process and out-of-process workers must look identical to # Lua, which means even the in-process bus encodes through MessagePack. -serde = { version = "1", features = ["derive"] } +serde = { workspace = true } rmp-serde = "1" +# Wire-types crate (session 1 of pmacs-gpu arc). Owns the +# `InstanceMessage` / `FrontendEvent` / capability / `SemanticFrame` +# family, plus the cell/buffer/rope wire types they reference. The +# `pmacs` crate re-exports through `crate::protocol`, `crate::cell`, +# `crate::buffer`, and `crate::rope` so existing internal imports keep +# working unchanged. +pmacs-protocol = { version = "1.0.0", path = "pmacs-protocol" } # Wire format for the M5 frontend ↔ instance protocol (T M5.5b). # Length-prefix framing wraps postcard-encoded payloads. Chosen for # compactness on the cell-stream traffic (60 Hz cell deltas dominate @@ -81,7 +104,7 @@ rmp-serde = "1" # `src/protocol.rs::Hello`. The worker-protocol encoding (§5.5 spec) # remains MessagePack via `rmp-serde`; different subsystems with # different requirements (compactness vs schema evolution). -postcard = { version = "1", features = ["use-std"] } +postcard = { workspace = true } # Signal handling for the M5.5 daemon (T M5.5e). Provides a safe # wrapper for installing handlers that set an AtomicBool flag, which # our accept loop polls between iterations. Used for SIGTERM/SIGINT diff --git a/pmacs-protocol/Cargo.toml b/pmacs-protocol/Cargo.toml new file mode 100644 index 0000000..bcad954 --- /dev/null +++ b/pmacs-protocol/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "pmacs-protocol" +version = "1.0.0" +edition = "2024" +rust-version = "1.95" +description = "Wire types for the pmacs daemon ↔ frontend protocol (the SemanticFrame family, capabilities, attach handshake)" +license = "MIT OR Apache-2.0" +authors = ["Pmacs contributors"] +readme = "../README.md" +repository = "https://git.levineuwirth.org/neuwirth/pmacs" +homepage = "https://levineuwirth.org/essays/pmacs" +keywords = ["editor", "emacs", "protocol", "ipc"] +categories = ["text-editors", "data-structures"] + +[lints.rust] +unsafe_code = "forbid" +missing_docs = "warn" + +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } +# Allows: same set as the root `pmacs` crate, mirrored so wire types +# moved here don't trip lints the originals didn't. +module_name_repetitions = "allow" +must_use_candidate = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +cast_possible_truncation = "allow" +cast_sign_loss = "allow" +cast_precision_loss = "allow" +similar_names = "allow" +multiple_crate_versions = "allow" + +[features] +# Mirrors the `crdt` feature on the parent `pmacs` crate. When enabled, +# the `InstanceMessage::CrdtOp` / `FrontendEvent::CrdtOp` variants exist +# and the wire-level `CrdtOp` struct is in scope. When disabled, those +# variants are compiled out so v0.1-era binaries built without `crdt` +# match the pre-v1.0 wire surface byte-for-byte. +crdt = [] + +[dependencies] +serde = { workspace = true } +postcard = { workspace = true } +thiserror = { workspace = true } diff --git a/pmacs-protocol/src/ids.rs b/pmacs-protocol/src/ids.rs new file mode 100644 index 0000000..f4b2d0c --- /dev/null +++ b/pmacs-protocol/src/ids.rs @@ -0,0 +1,80 @@ +//! Identity and range types — moved from `pmacs::buffer`, +//! `pmacs::protocol`, and `pmacs::rope` in session 1 of the +//! `pmacs-gpu` arc. The originals re-export these names so internal +//! `pmacs` imports (`crate::buffer::BufferId`, `crate::rope::Position`, +//! etc.) keep working unchanged. + +use std::sync::atomic::{AtomicU64, Ordering}; + +/// Opaque, per-process identifier for a buffer. +/// +/// The internal representation is private (R22): callers cannot reach +/// for `.0`; construction goes through [`BufferId::next`]. +/// +/// T M10.5: `Serialize` / `Deserialize` derived so `BufferId` can be +/// the routing key on `InstanceMessage::CrdtOp` / `FrontendEvent::CrdtOp`. +/// The serialized form is the bare `u64` (transparent newtype). +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)] +pub struct BufferId(u64); + +impl BufferId { + /// Allocate a fresh [`BufferId`] from the process-wide counter. + /// + /// Threading: any thread. + #[must_use] + pub fn next() -> Self { + static COUNTER: AtomicU64 = AtomicU64::new(1); + Self(COUNTER.fetch_add(1, Ordering::Relaxed)) + } + + /// Inspect the raw value. Useful for logging and FFI. + #[must_use] + pub const fn raw(self) -> u64 { + self.0 + } + + /// Rebuild an ID from a raw value for crate-internal references that + /// persist an already-issued buffer identity in generated text. + /// + /// Was `pub(crate)` before the session-1 crate split; promoted to + /// `pub` to remain reachable from `pmacs` after the move. Not + /// stable API for external consumers — external callers should + /// either round-trip via `serde` or accept that the constructor + /// may change. + #[must_use] + pub const fn from_raw(raw: u64) -> Self { + Self(raw) + } +} + +/// Opaque identifier for a frontend attached to an instance. +/// +/// Every input event carries a `FrontendId`. v0.1 uses one ID per +/// instance ([`FrontendId::LOCAL`]); v0.3 generalizes to multi-frontend +/// (multi-window, multi-user) without a protocol break. +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)] +pub struct FrontendId(pub u64); + +impl FrontendId { + /// The single frontend used in v0.1's local-attach mode. + /// + /// Future multi-frontend deployments allocate IDs from a counter + /// starting after this value; the constant is reserved. + pub const LOCAL: FrontendId = FrontendId(1); +} + +/// Byte offset into a rope. Buffer-wide; cursor / selection / span +/// anchors all use this type. Type alias rather than newtype so +/// arithmetic on offsets (slice ranges, byte deltas) doesn't need +/// conversions. +pub type Position = u64; + +/// 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, +} diff --git a/pmacs-protocol/src/lib.rs b/pmacs-protocol/src/lib.rs new file mode 100644 index 0000000..68eb20b --- /dev/null +++ b/pmacs-protocol/src/lib.rs @@ -0,0 +1,43 @@ +//! Wire types for the pmacs daemon ↔ frontend protocol. +//! +//! Session 1 of the pmacs-gpu arc — see `docs/pmacs-gpu-design.md` in +//! the workspace root. This crate owns every type that appears on the +//! `InstanceMessage` / `FrontendEvent` wire so a future `pmacs-gpu` +//! frontend can depend on it directly without pulling in the `pmacs` +//! main crate (and its Lua / tree-sitter / process-supervisor surface). +//! +//! What lives here: +//! - The `SemanticFrame` family: `StyleSpans`, `Decorations`, +//! `InlineAdornments`, `BlockAdornments`, `FoldState`, +//! `ResourceOffer`, `FileStyleSummary`. +//! - The grid-rendering family: `CellDelta`, plus `Cell`, `Glyph`, +//! `Style`, `Color`, `UnderlineStyle`, `CellCoord`, `CellSize`, +//! `DiffSpan`, `Attachment`. +//! - Identity types: `BufferId`, `FrontendId`, `Position`, `ByteRange`. +//! - The full message envelopes: `InstanceMessage`, `FrontendEvent`, +//! `GoodbyeReason`, capability structs, `PresenceUpdate`, etc. +//! - The optional `CrdtOp` wire variant (feature-gated on `crdt`). +//! +//! What does NOT live here: +//! - `crate::cell::CellGrid` and `crate::cell::diff()` (rendering +//! helpers, not wire types — stay in the `pmacs` crate). +//! - `Buffer` / `BufferRegistry` / `Rope` / `Edit` / `Range` +//! (instance-side editor machinery). +//! - `AttachTarget` and the attach-CLI binding error types +//! (`pmacs`-binary-only logic; `pmacs-gpu` builds its own attach +//! client). +//! - Lua / tree-sitter / process-supervisor everything. +//! +//! The `pmacs` crate re-exports back through its existing module paths +//! (`crate::cell::Style`, `crate::buffer::BufferId`, etc.) so internal +//! pmacs code doesn't churn its imports. New consumers +//! (`pmacs-gpu`, debug tools, future ports) depend on this crate +//! directly. + +pub mod ids; + +pub use ids::{BufferId, ByteRange, FrontendId, Position}; + +// Cell wire types, the SemanticFrame family, top-level message +// envelopes, and the feature-gated `CrdtOp` follow in subsequent +// commits within this PR. diff --git a/src/buffer.rs b/src/buffer.rs index 5dfd44c..a145981 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -27,7 +27,6 @@ //! observe `&Buffer` while the buffer's own `&mut self` is held. use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; use crate::file_io::FileMeta; use crate::rope::{Edit, Position, Range, Rope, RopeError}; @@ -37,40 +36,12 @@ use crate::view::{InterceptContext, View}; // Identifiers // --------------------------------------------------------------------------- -/// Opaque, per-process identifier for a buffer. -/// -/// The internal representation is private (R22): callers cannot reach for -/// `.0`; construction goes through [`BufferId::next`]. -/// -/// T M10.5: `Serialize` / `Deserialize` derived so `BufferId` can be the -/// routing key on `InstanceMessage::CrdtOp` / `FrontendEvent::CrdtOp`. -/// The serialized form is the bare `u64` (transparent newtype). -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)] -pub struct BufferId(u64); - -impl BufferId { - /// Allocate a fresh [`BufferId`] from the process-wide counter. - /// - /// Threading: any thread. - #[must_use] - pub fn next() -> Self { - static COUNTER: AtomicU64 = AtomicU64::new(1); - Self(COUNTER.fetch_add(1, Ordering::Relaxed)) - } - - /// Inspect the raw value. Useful for logging and FFI. - #[must_use] - pub const fn raw(self) -> u64 { - self.0 - } - - /// Rebuild an ID from a raw value for crate-internal references that - /// persist an already-issued buffer identity in generated text. - #[must_use] - pub(crate) const fn from_raw(raw: u64) -> Self { - Self(raw) - } -} +/// Re-export of `pmacs_protocol::BufferId` (moved there in session 1 +/// of the `pmacs-gpu` arc — see `docs/pmacs-gpu-design.md`). Existing +/// `crate::buffer::BufferId` import paths continue to resolve through +/// this re-export; new consumers (`pmacs-gpu`, debug tools) should +/// depend on `pmacs-protocol` directly. +pub use pmacs_protocol::BufferId; /// Opaque, per-buffer identifier for an attached view. /// diff --git a/src/protocol.rs b/src/protocol.rs index 122260e..7710364 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -44,24 +44,16 @@ use crate::cell::{Cell, CellCoord, CellSize, DiffSpan}; use std::path::PathBuf; // --------------------------------------------------------------------------- -// Frontend identity +// Frontend identity, byte ranges — re-exports from `pmacs-protocol` // --------------------------------------------------------------------------- -/// Opaque identifier for a frontend attached to an instance. -/// -/// Every input event carries a `FrontendId`. v0.1 uses one ID per -/// instance ([`FrontendId::LOCAL`]); v0.3 generalizes to multi-frontend -/// (multi-window, multi-user) without a protocol break. -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)] -pub struct FrontendId(pub u64); - -impl FrontendId { - /// The single frontend used in v0.1's local-attach mode. - /// - /// Future multi-frontend deployments allocate IDs from a counter - /// starting after this value; the constant is reserved. - pub const LOCAL: FrontendId = FrontendId(1); -} +// Session 1 of the `pmacs-gpu` arc moved the wire-types subset of +// this module into the `pmacs-protocol` crate. See +// `docs/pmacs-gpu-design.md`. Existing `crate::protocol::FrontendId` +// / `crate::protocol::ByteRange` imports resolve through these +// re-exports; new consumers (`pmacs-gpu`, debug tools) should depend +// on `pmacs-protocol` directly. +pub use pmacs_protocol::{ByteRange, FrontendId}; // --------------------------------------------------------------------------- // Key encoding @@ -793,15 +785,8 @@ pub struct SelectionSnapshot { // 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, -} +// `ByteRange` moved to `pmacs-protocol` (see the re-export near the +// top of this file). /// One run of buffer bytes carrying a resolved visual style. The /// instance is the single syntax/face authority; the frontend lays diff --git a/src/rope.rs b/src/rope.rs index d25aa16..86af642 100644 --- a/src/rope.rs +++ b/src/rope.rs @@ -43,11 +43,11 @@ const MAX_CHILDREN: usize = 8; // Public types // --------------------------------------------------------------------------- -/// Byte offset into a [`Rope`]. -/// -/// Not a codepoint index, not a grapheme index. Grapheme awareness is a -/// view-layer concern. -pub type Position = u64; +// `Position` is re-exported from `pmacs-protocol` (session 1 of the +// `pmacs-gpu` arc — see `docs/pmacs-gpu-design.md`). The type alias +// is a `u64` byte offset into a [`Rope`]: not a codepoint index, not +// a grapheme index. Grapheme awareness is a view-layer concern. +pub use pmacs_protocol::Position; /// A persistent rope of bytes. ///