feat(gpu): relative + hybrid line numbers over protocol v14 (sub-arc 3, GPU half)

Carry the line-number mode to the GPU so it renders relative/hybrid, not
just on/off. The v13 wire carried `LineNumbers { enabled: bool }`
(off/absolute only); v14 carries the full mode.

- Protocol: `LineNumberMode {Off, Absolute, Relative, Hybrid}` moves into
  pmacs-protocol (with `number_for`/`is_on`) so the wire, daemon, and both
  frontends share ONE enum and ONE number rule (Q#UX7); `pmacs` re-exports
  it as `crate:🪟:LineNumberMode`. `LineNumbers.enabled: bool` →
  `mode: LineNumberMode`. PROTOCOL_VERSION 13 → 14, SUPPORTED → [6..14],
  daemon-gated `< 14` (a v13 peer gets no LineNumbers, like the v10
  SearchPrompt bump).
- Producer (`line_numbers_msg`): ships the window's mode (cached-suppress
  on the mode now, seeded to Off).
- GPU: `line_numbers` field becomes the mode; `refresh_gutter_buffer`
  computes each number via `mode.number_for(line, cursor_line)` against the
  GPU's own cursor line (`cursor_line()` off `current_line_starts`). The
  buffer rebuilds every render, so relative numbers track the cursor for
  free. Gutter width unchanged (sized by line count → stable).

Tests: GPU headless render proves relative ≠ absolute with the cursor on
line 2; producer test asserts the mode ships; protocol version pins → 14.
fmt + clippy --all-targets clean both flavors + gpu; 1446 lib + 12 protocol
+ 55 pmacs-gpu tests pass. Needs a GPU eyeball.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
This commit is contained in:
Levi Neuwirth 2026-07-07 09:59:27 -04:00
parent 9ae381f740
commit 40ebdd8d7e
7 changed files with 190 additions and 105 deletions

View File

@ -36,8 +36,8 @@ use loro::{ContainerTrait, ExportMode};
use pmacs_protocol::{
AdornmentContent, AdornmentPlacement, BufferId, ByteRange, CrdtOp, Decoration, DecorationKind,
DecorationSegment, FrontendId, InlineAdornment, InstanceMessage, InstanceSignal,
Key as ProtocolKey, MenuPromptRow, Modifiers, PointerKind, SelectionSnapshot, StyleSegment,
StyleSpan,
Key as ProtocolKey, LineNumberMode, MenuPromptRow, Modifiers, PointerKind, SelectionSnapshot,
StyleSegment, StyleSpan,
cell::{Color as CellColor, Style as CellStyle},
};
use wgpu::MultisampleState;
@ -650,10 +650,12 @@ struct State {
/// Minimap vertex bytes cached by [`MinimapCacheKey`] —
/// rebuilding rescanned every line shape per frame.
minimap_cache: Option<(MinimapCacheKey, Vec<u8>)>,
/// Line-number gutter toggle (UX gutter arc, GPU side). Frontend-local
/// (Q#UX5), set from the `--line-numbers` flag. Off ⇒ zero coordinate
/// change: `gutter_width_px()` is 0 and every shift site is a no-op.
line_numbers: bool,
/// Line-number gutter mode (UX gutter arc, GPU side). Shipped by the
/// daemon over `InstanceMessage::LineNumbers` (protocol v14). `Off` ⇒
/// zero coordinate change: `gutter_width_px()` is 0 and every shift site
/// is a no-op. Relative/Hybrid are rendered locally against the GPU's
/// own cursor line.
line_numbers: LineNumberMode,
/// Shaped right-aligned line numbers, one per visible code line — its
/// own text layer over the code, aligned row-for-row (same line height).
gutter_buffer: Buffer,
@ -1882,7 +1884,7 @@ impl State {
mb_text_renderer,
mb_bg_vertex_buffer: ReusableVertexBuffer::new(),
minimap_cache: None,
line_numbers: false,
line_numbers: LineNumberMode::Off,
gutter_buffer,
gutter_text_renderer,
}
@ -2532,12 +2534,12 @@ impl State {
self.request_redraw();
None
}
// UX gutter (protocol v13): the daemon owns the per-window
// line-number toggle (`M-x window.toggle-line-numbers`); apply
// it to our local gutter state and repaint on change.
InstanceMessage::LineNumbers { enabled, .. } => {
if self.line_numbers != enabled {
self.line_numbers = enabled;
// UX gutter (protocol v14): the daemon owns the per-window
// line-number mode; apply it to our local gutter state and
// repaint on change.
InstanceMessage::LineNumbers { mode, .. } => {
if self.line_numbers != mode {
self.line_numbers = mode;
self.request_redraw();
}
None
@ -2812,7 +2814,7 @@ impl State {
/// disabled (UX gutter arc, Q#UX3): `digits * advance + gap`. Mirrors
/// the TUI's `Window::gutter_width`; the unit here is pixels.
fn gutter_width_px(&self) -> f32 {
if !self.line_numbers {
if !self.line_numbers.is_on() {
return 0.0;
}
let lines = self.current_line_starts.len().max(1);
@ -2826,24 +2828,43 @@ impl State {
TEXT_LEFT + self.gutter_width_px()
}
/// The GPU's own cursor's 0-based buffer line, or `0` when there's no
/// own cursor in the displayed buffer (relative/hybrid then count from
/// the top — a rare transient). Derived from the whole-buffer line
/// table, so it's independent of the shaped slice.
fn cursor_line(&self) -> usize {
let byte = match self.own_cursor.as_ref() {
Some(c) if Some(c.buffer_id) == self.current_buffer_id => c.byte,
_ => 0,
};
self.current_line_starts
.partition_point(|&start| start <= byte)
.saturating_sub(1)
}
/// Reshape the gutter buffer to the right-aligned line numbers for the
/// currently-shaped code lines (UX gutter arc). One number per code
/// line starting at `shaped_top`, so the two buffers align row-for-row
/// at the same `top` and line height. No-op when the gutter is off.
fn refresh_gutter_buffer(&mut self) {
use std::fmt::Write as _;
if !self.line_numbers {
if !self.line_numbers.is_on() {
return;
}
let digits = decimal_digits(self.current_line_starts.len().max(1)) as usize;
let first = self.shaped_top;
// Relative/Hybrid measure distance from the cursor's buffer line;
// Absolute ignores it. Rebuilt every render, so it tracks the cursor.
let cursor_line = self.cursor_line();
let mode = self.line_numbers;
let n = self.buffer.lines.len();
let mut text = String::new();
for i in 0..n {
if i > 0 {
text.push('\n');
}
let _ = write!(text, "{:>digits$}", first + i + 1);
let num = mode.number_for(first + i, cursor_line).unwrap_or(0);
let _ = write!(text, "{num:>digits$}");
}
self.gutter_buffer.set_text(
&mut self.font_system,
@ -4020,7 +4041,7 @@ impl State {
// main-text clip-left. Computed here as locals — calling `self.*`
// inside the `prepare` args would conflict with its `&mut` borrows.
let text_left = self.text_left();
let gutter_clip_left = if self.line_numbers {
let gutter_clip_left = if self.line_numbers.is_on() {
text_left.floor() as i32
} else {
0
@ -4087,7 +4108,7 @@ impl State {
// UX gutter: prepare the line-number layer in the reserved left
// strip (empty when off → renders nothing). Same `top` + line
// height as the code, so numbers align row-for-row.
let gutter_areas: Vec<TextArea> = if self.line_numbers {
let gutter_areas: Vec<TextArea> = if self.line_numbers.is_on() {
vec![TextArea {
buffer: &self.gutter_buffer,
left: TEXT_LEFT,
@ -4360,7 +4381,7 @@ impl State {
vstart: u64,
vend: u64,
) {
if !self.line_numbers {
if !self.line_numbers.is_on() {
return;
}
let slice_len = vend - vstart;
@ -7415,7 +7436,7 @@ mod tests {
let off_px = off.render_offscreen();
let mut on = State::new_headless(400, 300, "alpha\nbeta\ngamma\ndelta\n")
.expect("adapter was just available");
on.line_numbers = true;
on.line_numbers = LineNumberMode::Absolute;
let on_px = on.render_offscreen();
assert_eq!(off_px.len(), on_px.len());
let differing = off_px.iter().zip(&on_px).filter(|(a, b)| a != b).count();
@ -7434,14 +7455,14 @@ mod tests {
let Some(mut plain) = headless_or_skip(400, 300, text) else {
return;
};
plain.line_numbers = true;
plain.line_numbers = LineNumberMode::Absolute;
plain.current_buffer_id = Some(BufferId::next());
plain.view_range = (0, text.len() as u64);
let plain_px = plain.render_offscreen();
let mut with_diag =
State::new_headless(400, 300, text).expect("adapter was just available");
with_diag.line_numbers = true;
with_diag.line_numbers = LineNumberMode::Absolute;
with_diag.current_buffer_id = Some(BufferId::next());
with_diag.view_range = (0, text.len() as u64);
with_diag.current_decorations.push(Decoration {
@ -7461,4 +7482,39 @@ mod tests {
"the diagnostic sign bar should add ink ({differing} bytes differ)"
);
}
#[test]
fn headless_relative_mode_renders_differently_from_absolute() {
// Sub-arc 3: with the cursor on line 2, relative numbering
// (2,1,0,1,2) must differ from absolute (1,2,3,4,5) — proving the
// GPU renders the mode against its own cursor line.
let text = "alpha\nbeta\ngamma\ndelta\nepsilon\n";
let bid = BufferId::next();
let cursor = OwnCursor {
buffer_id: bid,
byte: 13, // inside "gamma" (buffer line 2)
};
let Some(mut abs) = headless_or_skip(400, 300, text) else {
return;
};
abs.current_buffer_id = Some(bid);
abs.view_range = (0, text.len() as u64);
abs.own_cursor = Some(cursor);
abs.line_numbers = LineNumberMode::Absolute;
let abs_px = abs.render_offscreen();
let mut rel = State::new_headless(400, 300, text).expect("adapter was just available");
rel.current_buffer_id = Some(bid);
rel.view_range = (0, text.len() as u64);
rel.own_cursor = Some(cursor);
rel.line_numbers = LineNumberMode::Relative;
let rel_px = rel.render_offscreen();
assert_eq!(abs_px.len(), rel_px.len());
let differing = abs_px.iter().zip(&rel_px).filter(|(a, b)| a != b).count();
assert!(
differing > 20,
"relative numbering must differ from absolute ({differing} bytes differ)"
);
}
}

View File

@ -49,8 +49,9 @@ pub use message::{
AdornmentContent, AdornmentPlacement, AttachRequest, BlockAdornment, CursorState, Decoration,
DecorationKind, DecorationSegment, FrontendCapabilities, FrontendEvent, GoodbyeReason, Hello,
InlineAdornment, InstanceCapabilities, InstanceIdentity, InstanceMessage, InstanceSignal, Key,
KeyEvent, MenuPromptRow, Modifiers, MouseButton, MouseEvent, MouseKind, NegotiatedCapabilities,
PROTOCOL_VERSION, PointerKind, ResourceBody, SUPPORTED_PROTOCOL_VERSIONS, SelectionSnapshot,
StyleSegment, StyleSpan, is_supported_protocol_version, negotiate_capabilities,
KeyEvent, LineNumberMode, MenuPromptRow, Modifiers, MouseButton, MouseEvent, MouseKind,
NegotiatedCapabilities, PROTOCOL_VERSION, PointerKind, ResourceBody,
SUPPORTED_PROTOCOL_VERSIONS, SelectionSnapshot, StyleSegment, StyleSpan,
is_supported_protocol_version, negotiate_capabilities,
};
pub use transport::{MAX_FRAME_BYTES, TransportError, read_message, write_message};

View File

@ -905,21 +905,68 @@ pub enum InstanceMessage {
/// Total candidate count (the window is a slice of this).
total: u32,
},
/// UX gutter (protocol v13) — the per-window line-number gutter mode
/// for the frontend's active window. A semantic frontend renders line
/// numbers *locally* (it owns the text), but the on/off toggle lives
/// daemon-side (`M-x window.toggle-line-numbers`), so the daemon ships
/// the mode. Additive + daemon-gated `>= 13` — an older peer would
/// hard-error decoding it, so it stays off wires negotiated below 13.
/// UX gutter — the per-window line-number gutter *mode* for the
/// frontend's active window. A semantic frontend renders line numbers
/// *locally* (it owns the text + its own cursor line, so it can draw
/// relative/hybrid without a round trip), but the mode toggle lives
/// daemon-side, so the daemon ships which mode. Bumped 13 → 14: the
/// v13 shape carried a bare `enabled: bool` (off/absolute only); v14
/// carries the full [`LineNumberMode`] so relative/hybrid can ride the
/// same message. Daemon-gated `>= 14` — a v13 peer negotiates v13 and
/// receives no `LineNumbers` (its gutter stays off) rather than
/// mis-decoding the wider shape.
LineNumbers {
/// Buffer the active window shows (routing/consistency; the mode
/// is a window property, not a buffer one).
buffer_id: crate::BufferId,
/// Whether the line-number gutter is enabled for that window.
enabled: bool,
/// The line-number gutter mode for that window.
mode: LineNumberMode,
},
}
/// Line-number gutter mode for a window (UX gutter arc). Shared across the
/// wire, the daemon, and both frontends so the *number rule* — what value
/// each line shows — is identical everywhere (Q#UX7). `pmacs` re-exports
/// this as `crate::window::LineNumberMode`.
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineNumberMode {
/// No gutter; zero layout change (the default, Emacs tradition).
#[default]
Off,
/// Absolute 1-based line numbers.
Absolute,
/// Distance from the cursor line (the cursor line shows `0`).
Relative,
/// Like `Relative`, but the cursor line shows its absolute 1-based
/// number instead of `0` (Vim `number` + `relativenumber`).
Hybrid,
}
impl LineNumberMode {
/// Whether this mode reserves a gutter at all (everything but `Off`).
#[must_use]
pub fn is_on(self) -> bool {
!matches!(self, Self::Off)
}
/// The displayed number for a 0-based buffer `line` given the cursor's
/// 0-based buffer line, or `None` in `Off`. `Relative`/`Hybrid` depend
/// on `cursor_line`; `Absolute` ignores it.
#[must_use]
pub fn number_for(self, line: usize, cursor_line: usize) -> Option<usize> {
match self {
Self::Off => None,
Self::Absolute => Some(line + 1),
Self::Relative => Some(line.abs_diff(cursor_line)),
Self::Hybrid => Some(if line == cursor_line {
line + 1
} else {
line.abs_diff(cursor_line)
}),
}
}
}
/// One row of an open menu on the wire ([`InstanceMessage::MenuPrompt`]).
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct MenuPromptRow {
@ -1199,7 +1246,13 @@ pub enum ResourceBody {
/// Daemon-gated `< 13`; a v12 peer negotiates v12 and receives no
/// `LineNumbers` (its gutter simply stays off), like every prior additive
/// bump.
pub const PROTOCOL_VERSION: u32 = 13;
///
/// UX gutter modes: bumped 13 → 14 — `LineNumbers` swapped its `enabled:
/// bool` for a [`LineNumberMode`] enum so relative/hybrid ride the same
/// message. Encoding change to that variant; daemon-gated `< 14` (a v13
/// peer negotiates v13 and receives no `LineNumbers` rather than
/// mis-decoding the wider shape), same shape as the v10 `SearchPrompt` bump.
pub const PROTOCOL_VERSION: u32 = 14;
/// 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
@ -1256,7 +1309,7 @@ pub const PROTOCOL_VERSION: u32 = 13;
/// Q#MB1: extended to `[6, 7, 8, 9, 10, 11, 12]`.
/// `InstanceMessage::MinibufferPrompt` is additive and daemon-gated per
/// session, so the ladder resumes again.
pub const SUPPORTED_PROTOCOL_VERSIONS: &[u32] = &[6, 7, 8, 9, 10, 11, 12, 13];
pub const SUPPORTED_PROTOCOL_VERSIONS: &[u32] = &[6, 7, 8, 9, 10, 11, 12, 13, 14];
/// T M10.5: predicate for the handshake check. Returns `true` if
/// `peer_version` is in [`SUPPORTED_PROTOCOL_VERSIONS`].

View File

@ -1050,11 +1050,12 @@ fn dispatcher_loop(
let peer_knows_minibuffer_prompt = session_registry
.session_state(*fid)
.is_some_and(|s| s.negotiated_protocol_version >= 12);
// UX gutter — `LineNumbers` is a v13 additive variant; a
// v12 peer keeps its gutter off rather than mis-decoding it.
// UX gutter — `LineNumbers` carries a `LineNumberMode` since
// v14 (was `enabled: bool` in v13); a peer below 14 keeps
// its gutter off rather than mis-decoding the wider shape.
let peer_knows_line_numbers = session_registry
.session_state(*fid)
.is_some_and(|s| s.negotiated_protocol_version >= 13);
.is_some_and(|s| s.negotiated_protocol_version >= 14);
for msg in &messages {
if !peer_knows_status_facts
&& matches!(msg, InstanceMessage::StatusFacts { .. })

View File

@ -1683,7 +1683,7 @@ mod tests {
// --- M5.5a handshake & postcard round-trips ---
#[test]
fn protocol_version_is_thirteen_for_line_numbers() {
fn protocol_version_is_fourteen_for_line_number_modes() {
// Pin the value: T M10.5 bumped 1→2 (v1.0 wire: CrdtOp /
// PresenceUpdate). T M11.1 bumped 2→3 (v1.1 wire: the
// SemanticFrame family + FrontendEvent::Viewport). T M11.6
@ -1702,8 +1702,10 @@ mod tests {
// `MenuPrompt`, all additive; the message daemon-gated). Q#MB1
// bumped 11→12 (`InstanceMessage::MinibufferPrompt`, additive +
// daemon-gated). UX gutter bumped 12→13
// (`InstanceMessage::LineNumbers`, additive + daemon-gated).
assert_eq!(PROTOCOL_VERSION, 13);
// (`InstanceMessage::LineNumbers`, additive + daemon-gated). UX
// gutter modes bumped 13→14 (`LineNumbers` swapped `enabled: bool`
// for a `LineNumberMode` enum — encoding change, still daemon-gated).
assert_eq!(PROTOCOL_VERSION, 14);
}
#[test]
@ -1715,8 +1717,8 @@ mod tests {
// Q#M4 / Q#S1 / Q#SR5 / Q#RX6 / Q#CM1 / Q#MB1 / UX gutter: the
// ladder resumes above that floor — v7 (`TripleDown`), v8
// (`StatusFacts`), v9 + v10 (`SearchPrompt` + regex/invalid), v11
// (the context menu), v12 (the GUI minibuffer), v13 (`LineNumbers`)
// all interoperate, so v6 through v13 talk.
// (the context menu), v12 (the GUI minibuffer), v13 (`LineNumbers`),
// v14 (`LineNumberMode`) all interoperate, so v6 through v14 talk.
assert!(is_supported_protocol_version(6));
assert!(is_supported_protocol_version(7));
assert!(is_supported_protocol_version(8));
@ -1725,10 +1727,11 @@ mod tests {
assert!(is_supported_protocol_version(11));
assert!(is_supported_protocol_version(12));
assert!(is_supported_protocol_version(13));
for rejected in [0, 1, 2, 3, 4, 5, 14, u32::MAX] {
assert!(is_supported_protocol_version(14));
for rejected in [0, 1, 2, 3, 4, 5, 15, u32::MAX] {
assert!(
!is_supported_protocol_version(rejected),
"v{rejected} must be rejected by a v13 binary"
"v{rejected} must be rejected by a v14 binary"
);
}
}

View File

@ -157,11 +157,11 @@ pub struct SemanticRenderState {
/// `(name, modified, diag_errors, diag_warnings)` last emitted as
/// `StatusFacts` (Q#S1) — cached-compare suppression.
last_status: HashMap<BufferId, (String, bool, u32, u32)>,
/// Last-emitted line-number gutter enabled-flag (UX gutter arc,
/// protocol v13) — cached-compare suppression. Seeded to `Some(false)`
/// (the frontend's default) so an off gutter never emits. Per-frontend
/// (one value), since this state carries one frontend's `frontend_id`.
last_line_numbers: Option<bool>,
/// Last-emitted line-number gutter mode (UX gutter arc, protocol v14) —
/// cached-compare suppression. Seeded to `Some(Off)` (the frontend's
/// default) so an off gutter never emits. Per-frontend (one value),
/// since this state carries one frontend's `frontend_id`.
last_line_numbers: Option<crate::window::LineNumberMode>,
/// Last emitted `SearchPrompt` payload per buffer, for
/// cached-compare suppression (see [`SearchPromptFacts`]).
last_search_prompt: HashMap<BufferId, SearchPromptFacts>,
@ -249,7 +249,7 @@ impl SemanticRenderState {
// window never emits `LineNumbers`, so the common case adds no
// traffic and the first frame is unchanged. Only an actual
// toggle-on (or later toggle-off) ships a message.
last_line_numbers: Some(false),
last_line_numbers: Some(crate::window::LineNumberMode::Off),
last_style_gate: HashMap::new(),
diag_line_cache: HashMap::new(),
}
@ -698,26 +698,26 @@ impl SemanticRenderState {
}
/// The `LineNumbers` message for this frame, or `None` when the gutter
/// mode hasn't changed (UX gutter arc, protocol v13). The toggle lives
/// on this frontend's active window (`M-x window.toggle-line-numbers`);
/// a semantic frontend renders the gutter locally but the daemon owns
/// the on/off state, so it ships the mode. The daemon's write loop
/// keeps the variant off wires negotiated `< 13`.
/// mode hasn't changed (UX gutter arc, protocol v14). The mode lives on
/// this frontend's active window; a semantic frontend renders the gutter
/// locally (it owns the text + its cursor line, so relative/hybrid need
/// no round trip) but the daemon owns the mode, so it ships which one.
/// The daemon's write loop keeps the variant off wires negotiated `< 14`.
fn line_numbers_msg(
&mut self,
state: &EditorState,
buffer_id: BufferId,
) -> Option<InstanceMessage> {
let enabled = {
let mode = {
let core = state.core.borrow();
core.active_window_for(self.frontend_id)
.is_some_and(|w| w.line_numbers != crate::window::LineNumberMode::Off)
.map_or(crate::window::LineNumberMode::Off, |w| w.line_numbers)
};
if self.last_line_numbers == Some(enabled) {
if self.last_line_numbers == Some(mode) {
return None;
}
self.last_line_numbers = Some(enabled);
Some(InstanceMessage::LineNumbers { buffer_id, enabled })
self.last_line_numbers = Some(mode);
Some(InstanceMessage::LineNumbers { buffer_id, mode })
}
/// The `InlineAdornments` message for this frame, or `None` when
@ -1748,14 +1748,19 @@ mod tests {
"off gutter must not emit LineNumbers"
);
// Toggle the active window on → next frame emits enabled = true.
// Toggle the active window on → next frame emits the mode.
state.core.borrow_mut().active_window_mut().line_numbers =
crate::window::LineNumberMode::Absolute;
let on = s.render_frame(&state);
assert!(
on.iter()
.any(|m| matches!(m, InstanceMessage::LineNumbers { enabled: true, .. })),
"toggling the gutter on must emit LineNumbers {{ enabled: true }}"
on.iter().any(|m| matches!(
m,
InstanceMessage::LineNumbers {
mode: crate::window::LineNumberMode::Absolute,
..
}
)),
"toggling the gutter on must emit LineNumbers with the mode"
);
// No further change → suppressed.

View File

@ -130,46 +130,12 @@ pub struct Selection {
}
/// Line-number display mode for a window's left gutter (UX gutter arc).
/// `Off` reserves no gutter at all — text starts at column 0, and every
/// coordinate is unchanged (the default, matching the Emacs tradition).
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub enum LineNumberMode {
/// No gutter; zero layout change.
#[default]
Off,
/// Absolute 1-based line numbers, right-aligned in the gutter.
Absolute,
/// Distance from the cursor line (the cursor line shows `0`).
Relative,
/// Like `Relative`, but the cursor line shows its absolute 1-based
/// number instead of `0` (Vim `number` + `relativenumber`).
Hybrid,
}
impl LineNumberMode {
/// Whether this mode reserves a gutter at all (everything but `Off`).
#[must_use]
pub fn is_on(self) -> bool {
!matches!(self, Self::Off)
}
/// The displayed 0-or-1-based number for a buffer `line` given the
/// cursor's buffer line, or `None` in `Off`. `Relative`/`Hybrid` depend
/// on `cursor_line`; `Absolute` ignores it.
#[must_use]
pub fn number_for(self, line: usize, cursor_line: usize) -> Option<usize> {
match self {
Self::Off => None,
Self::Absolute => Some(line + 1),
Self::Relative => Some(line.abs_diff(cursor_line)),
Self::Hybrid => Some(if line == cursor_line {
line + 1
} else {
line.abs_diff(cursor_line)
}),
}
}
}
///
/// Defined in `pmacs-protocol` so the wire, the daemon, and both frontends
/// share one enum and one number rule ([`LineNumberMode::number_for`],
/// Q#UX7); re-exported here so `crate::window::LineNumberMode` stays the
/// in-crate path.
pub use pmacs_protocol::LineNumberMode;
/// Cells of horizontal padding the line-number gutter adds around the
/// digit field: a leading and a trailing blank, so `gutter_w = digits +