session 1 commit 1/4: workspace + identity types moved to pmacs-protocol

Workspace skeleton: root Cargo.toml becomes a workspace with members
[".", "pmacs-protocol"]; [workspace.dependencies] pins serde,
postcard, thiserror so both crates use byte-identical versions (the
wire format depends on it). pmacs main package keeps its existing
shape (no file moves); it just gains pmacs-protocol as a path
dependency.

Identity types moved: BufferId (from buffer.rs), FrontendId + ByteRange
(from protocol.rs), Position type alias (from rope.rs). All four are
self-contained — no custom-type dependencies — so the first stage of
the move can land atomically without dragging cell/message types along.

src/buffer.rs / src/protocol.rs / src/rope.rs each gain a 'pub use
pmacs_protocol::...' re-export for the moved names, so existing
internal imports (crate::buffer::BufferId, crate::rope::Position, etc.)
continue to resolve unchanged. New consumers (pmacs-gpu, debug tools)
will depend on pmacs-protocol directly.

One visibility change: BufferId::from_raw was pub(crate); promoted to
pub with a doc note that it's not stable API for external consumers.
The (crate) restriction was advisory only — external deserialization
already worked via the derived Deserialize, so making it pub doesn't
widen the actual surface, just makes it honest.

Lib gate: 1314 passed, no regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-05-20 09:11:33 -04:00
parent f7cea2ec3a
commit 14341d958e
8 changed files with 226 additions and 68 deletions

10
Cargo.lock generated
View File

@ -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"

View File

@ -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

46
pmacs-protocol/Cargo.toml Normal file
View File

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

80
pmacs-protocol/src/ids.rs Normal file
View File

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

43
pmacs-protocol/src/lib.rs Normal file
View File

@ -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.

View File

@ -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.
///

View File

@ -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

View File

@ -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.
///