pmacs/src/statusline.rs

1175 lines
42 KiB
Rust

//! Lua statusline provider registry and borrow-released evaluator.
//!
//! The registry is editor-global, while evaluation is scoped to one frontend's
//! visible windows (grid) or one declared active buffer (semantic). Callers
//! must interpret [`StatuslineEvaluationOutcome::Invalidated`] as an
//! authoritative empty replacement and [`StatuslineEvaluationOutcome::NoMessage`]
//! as no publication at all.
use std::cell::RefCell;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
use std::rc::Rc;
use mlua::{Function, Lua, Value};
use pmacs_protocol::{
FrontendId, MAX_STATUSLINE_FACE_BYTES, MAX_STATUSLINE_PROVIDER_NAME_BYTES,
MAX_STATUSLINE_PROVIDERS, MAX_STATUSLINE_SEGMENT_BYTES, is_modeline_face_name,
};
use crate::buffer::BufferId;
use crate::command::SourceLocation;
use crate::editor_core::EditorCore;
use crate::lua_bindings::BufferIdLua;
use crate::window::WindowId;
/// Cheaply cloneable, single-threaded statusline registry handle.
pub type SharedStatuslineRegistry = Rc<RefCell<StatuslineRegistry>>;
/// Stable, monotonic provider identity. IDs are never reused.
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct StatuslineProviderId(u64);
impl StatuslineProviderId {
/// Numeric representation for diagnostics and Lua introspection.
#[must_use]
pub fn raw(self) -> u64 {
self.0
}
}
impl fmt::Display for StatuslineProviderId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "statusline-provider:{}", self.0)
}
}
/// Side of the modeline populated by a provider.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum StatuslineSide {
/// After the protected buffer-identity group.
Left,
/// Before the protected diagnostic/cursor/scroll group.
Right,
}
impl StatuslineSide {
/// Lua/API spelling.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Left => "left",
Self::Right => "right",
}
}
}
/// Fresh metadata returned by registry introspection.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StatuslineProviderMetadata {
/// Stable registration handle.
pub id: StatuslineProviderId,
/// Non-unique display/debug label.
pub name: String,
/// Compositor side.
pub side: StatuslineSide,
/// Survival priority; visual ordering is side-dependent.
pub priority: i32,
/// Static `ui.modeline` face name.
pub face: String,
/// Whether evaluation currently invokes the callback.
pub enabled: bool,
/// Registration callsite used for error attribution.
pub source: SourceLocation,
}
#[derive(Clone)]
struct StatuslineProviderDefinition {
metadata: StatuslineProviderMetadata,
callback: Function,
}
/// Registration validation/capacity failure. Failed registration never mutates
/// providers, IDs, or epochs.
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
pub enum StatuslineRegistryError {
#[error("statusline provider name must not be empty")]
/// The display label was empty.
EmptyName,
#[error("statusline provider name contains a control character")]
/// The display label contained a control scalar.
NameControl,
#[error("statusline provider name exceeds {MAX_STATUSLINE_PROVIDER_NAME_BYTES} bytes")]
/// The display label exceeded the shared byte limit.
NameTooLong,
#[error("statusline face must be ui.modeline or a ui.modeline.* child")]
/// The face was outside the reserved modeline namespace.
InvalidFace,
#[error("statusline face contains a control character")]
/// The face contained a control scalar.
FaceControl,
#[error("statusline face exceeds {MAX_STATUSLINE_FACE_BYTES} bytes")]
/// The face exceeded the shared byte limit.
FaceTooLong,
#[error("at most {MAX_STATUSLINE_PROVIDERS} statusline providers may be registered")]
/// The live-provider structural limit was reached.
ProviderLimit,
}
/// Full identity of one callback invocation and its failure latch.
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub struct StatuslineContext {
/// Frontend being rendered.
pub frontend_id: FrontendId,
/// Visible window being rendered.
pub window_id: WindowId,
/// Buffer displayed by that window.
pub buffer_id: BufferId,
/// Whether this is the frontend's focused window.
pub active: bool,
}
/// One successful, non-empty provider result. Vectors containing these are
/// already in the exact Q#SL4 display/survival order.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EvaluatedStatuslineSegment {
/// Registration that produced the segment.
pub provider_id: StatuslineProviderId,
/// Sanitized, non-empty one-line UTF-8 text.
pub text: String,
/// Static face copied from the registration.
pub face: String,
}
/// Complete custom output for one window context.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StatuslineWindowSegments {
/// Context to which both vectors belong.
pub context: StatuslineContext,
/// Left segments in priority-descending/id-ascending order.
pub left: Vec<EvaluatedStatuslineSegment>,
/// Right segments in priority-ascending/id-ascending order.
pub right: Vec<EvaluatedStatuslineSegment>,
}
/// Why phase 1 intentionally produced no message.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum StatuslineNoMessageReason {
/// The target frontend, view, or one of its layout windows no longer exists.
ContextUnavailable,
/// A layout window points at a buffer that has been removed.
BufferUnavailable,
/// The semantic frontend's daemon window does not match its declared
/// viewport. `BufferSnapshot` already cleared the remote mirror.
DeclaredBufferMismatch,
/// A core borrow was unexpectedly held before evaluation could begin.
CoreBorrowConflict,
}
/// Publication decision from one three-phase evaluation.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum StatuslineEvaluationOutcome {
/// Valid owned results. Empty vectors are authoritative truth.
Ready(Vec<StatuslineWindowSegments>),
/// A callback changed registry/layout context after phase 1. Callers MUST
/// publish empty left/right vectors for these old contexts (subject to
/// their normal payload baseline) and MUST discard all evaluated text.
Invalidated {
/// Phase-1 contexts whose previous published output must be cleared.
authoritative_empty: Vec<StatuslineContext>,
},
/// Phase 1 was already stale. Callers MUST NOT publish a segment message.
NoMessage(StatuslineNoMessageReason),
}
/// A newly armed provider failure. The evaluator also appends it to the
/// canonical `*errors*` buffer; returning it keeps tests/telemetry explicit.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StatuslineProviderFailure {
/// Registration that failed.
pub provider_id: StatuslineProviderId,
/// Display label captured from the registration.
pub provider_name: String,
/// Registration callsite.
pub source: SourceLocation,
/// Exact per-window latch key.
pub context: StatuslineContext,
/// Callback/return-validation error text.
pub message: String,
}
/// Evaluator result including only first-in-consecutive-run failures.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StatuslineEvaluation {
/// Publication decision.
pub outcome: StatuslineEvaluationOutcome,
/// Newly armed failures already appended to `*errors*`.
pub new_failures: Vec<StatuslineProviderFailure>,
}
/// Target shape for the shared evaluator.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum StatuslineEvaluationTarget {
/// Every visible leaf in the frontend's layout, in layout order.
Grid {
/// Frontend whose entire visible layout is evaluated.
frontend_id: FrontendId,
},
/// The frontend's **primary document window**, iff it still displays
/// the declared semantic viewport buffer, **plus its visible side
/// window** when one exists (bottom-panel Q#BP8 / A2A-2).
///
/// Two contexts, not one: the document result feeds the semantic
/// `StatuslineSegments` wire, while the side result paints in the
/// panel's own mode line. Unprojected document splits run no
/// callbacks, and a derived-hidden side (Q#BP2b) is omitted because
/// it has no mode line to paint this frame.
///
/// The document context is captured **first**; consumers must still
/// select by window identity rather than position, since only one of
/// the two may reach the single semantic statusline slot.
///
/// `active` on each context reports **actual focus**, so a document
/// provider truthfully observes `active = false` while a panel owns
/// focus (Q#BP14, parent acceptance 42).
Semantic {
/// Frontend whose document (and visible side) window is evaluated.
frontend_id: FrontendId,
/// Buffer declared by the semantic viewport.
declared_buffer: BufferId,
},
}
/// Registry state. Provider callbacks are never invoked while this is borrowed.
pub struct StatuslineRegistry {
next_id: u64,
layout_epoch: u64,
face_set_epoch: u64,
providers: BTreeMap<StatuslineProviderId, StatuslineProviderDefinition>,
enabled_face_counts: HashMap<String, usize>,
failure_latches: HashSet<(StatuslineProviderId, StatuslineContext)>,
}
impl Default for StatuslineRegistry {
fn default() -> Self {
Self::new()
}
}
impl StatuslineRegistry {
/// Construct an empty registry with epochs at zero.
#[must_use]
pub fn new() -> Self {
Self {
next_id: 1,
layout_epoch: 0,
face_set_epoch: 0,
providers: BTreeMap::new(),
enabled_face_counts: HashMap::new(),
failure_latches: HashSet::new(),
}
}
/// Current monotonic ordering/context guard epoch.
#[must_use]
pub fn layout_epoch(&self) -> u64 {
self.layout_epoch
}
/// Current monotonic enabled-face-inventory epoch.
#[must_use]
pub fn face_set_epoch(&self) -> u64 {
self.face_set_epoch
}
/// Number of live registrations, including disabled ones.
#[must_use]
pub fn len(&self) -> usize {
self.providers.len()
}
/// Whether there are no live registrations.
#[must_use]
pub fn is_empty(&self) -> bool {
self.providers.is_empty()
}
/// Fresh metadata in stable registration order.
#[must_use]
pub fn providers(&self) -> Vec<StatuslineProviderMetadata> {
self.providers
.values()
.map(|definition| definition.metadata.clone())
.collect()
}
/// Sorted/deduplicated enabled face inventory for `ThemeFacts` expansion.
#[must_use]
pub fn enabled_face_names(&self) -> Vec<String> {
let mut faces: Vec<_> = self.enabled_face_counts.keys().cloned().collect();
faces.sort_unstable();
faces
}
/// Register one enabled provider after validating the shared structural
/// limits. Lua-specific strict type/table validation happens in the binding.
pub fn register(
&mut self,
name: String,
side: StatuslineSide,
priority: i32,
face: String,
callback: Function,
source: SourceLocation,
) -> Result<StatuslineProviderId, StatuslineRegistryError> {
validate_name(&name)?;
validate_face(&face)?;
if self.providers.len() >= MAX_STATUSLINE_PROVIDERS {
return Err(StatuslineRegistryError::ProviderLimit);
}
let id = StatuslineProviderId(self.next_id);
self.next_id = self
.next_id
.checked_add(1)
.expect("statusline provider id exhausted");
let face_set_changed = self.add_enabled_face(&face);
let metadata = StatuslineProviderMetadata {
id,
name,
side,
priority,
face,
enabled: true,
source,
};
self.providers
.insert(id, StatuslineProviderDefinition { metadata, callback });
self.bump_layout_epoch();
if face_set_changed {
self.bump_face_set_epoch();
}
Ok(id)
}
/// Idempotently remove a provider and all its failure latches.
pub fn unregister(&mut self, id: StatuslineProviderId) -> bool {
let Some(definition) = self.providers.remove(&id) else {
return false;
};
self.failure_latches.retain(|(provider, _)| *provider != id);
self.bump_layout_epoch();
if definition.metadata.enabled && self.remove_enabled_face(&definition.metadata.face) {
self.bump_face_set_epoch();
}
true
}
/// Change survival priority. No-op and stale writes do not advance epochs.
pub fn set_priority(&mut self, id: StatuslineProviderId, priority: i32) -> bool {
let Some(definition) = self.providers.get_mut(&id) else {
return false;
};
if definition.metadata.priority != priority {
definition.metadata.priority = priority;
self.bump_layout_epoch();
}
true
}
/// Enable/disable a provider. Disabling clears all its failure latches.
pub fn set_enabled(&mut self, id: StatuslineProviderId, enabled: bool) -> bool {
let Some(definition) = self.providers.get_mut(&id) else {
return false;
};
if definition.metadata.enabled == enabled {
return true;
}
definition.metadata.enabled = enabled;
let face = definition.metadata.face.clone();
if !enabled {
self.failure_latches.retain(|(provider, _)| *provider != id);
}
self.bump_layout_epoch();
let face_set_changed = if enabled {
self.add_enabled_face(&face)
} else {
self.remove_enabled_face(&face)
};
if face_set_changed {
self.bump_face_set_epoch();
}
true
}
/// Drop every failure latch for a detached frontend.
pub fn detach_frontend(&mut self, frontend_id: FrontendId) {
self.failure_latches
.retain(|(_, context)| context.frontend_id != frontend_id);
}
/// Defense-in-depth stale-window/buffer/focus latch sweep.
pub fn retain_live_contexts(&mut self, live: &HashSet<StatuslineContext>) {
self.failure_latches
.retain(|(_, context)| live.contains(context));
}
fn snapshot_enabled(&self) -> (u64, Vec<StatuslineProviderDefinition>) {
let mut definitions: Vec<_> = self
.providers
.values()
.filter(|definition| definition.metadata.enabled)
.cloned()
.collect();
definitions.sort_by(|a, b| match (a.metadata.side, b.metadata.side) {
(StatuslineSide::Left, StatuslineSide::Right) => std::cmp::Ordering::Less,
(StatuslineSide::Right, StatuslineSide::Left) => std::cmp::Ordering::Greater,
(StatuslineSide::Left, StatuslineSide::Left) => b
.metadata
.priority
.cmp(&a.metadata.priority)
.then_with(|| a.metadata.id.cmp(&b.metadata.id)),
(StatuslineSide::Right, StatuslineSide::Right) => a
.metadata
.priority
.cmp(&b.metadata.priority)
.then_with(|| a.metadata.id.cmp(&b.metadata.id)),
});
(self.layout_epoch, definitions)
}
fn note_success(&mut self, id: StatuslineProviderId, context: StatuslineContext) {
self.failure_latches.remove(&(id, context));
}
fn note_failure(&mut self, id: StatuslineProviderId, context: StatuslineContext) -> bool {
if self.failure_latches.contains(&(id, context)) {
return false;
}
if self
.providers
.get(&id)
.is_some_and(|definition| definition.metadata.enabled)
{
self.failure_latches.insert((id, context));
}
true
}
fn add_enabled_face(&mut self, face: &str) -> bool {
let count = self.enabled_face_counts.entry(face.to_owned()).or_default();
let changed = *count == 0;
*count += 1;
changed
}
fn remove_enabled_face(&mut self, face: &str) -> bool {
let Some(count) = self.enabled_face_counts.get_mut(face) else {
return false;
};
*count -= 1;
if *count == 0 {
self.enabled_face_counts.remove(face);
true
} else {
false
}
}
fn bump_layout_epoch(&mut self) {
self.layout_epoch = self
.layout_epoch
.checked_add(1)
.expect("statusline layout epoch exhausted");
}
fn bump_face_set_epoch(&mut self) {
self.face_set_epoch = self
.face_set_epoch
.checked_add(1)
.expect("statusline face-set epoch exhausted");
}
}
fn validate_name(name: &str) -> Result<(), StatuslineRegistryError> {
if name.is_empty() {
return Err(StatuslineRegistryError::EmptyName);
}
if name.len() > MAX_STATUSLINE_PROVIDER_NAME_BYTES {
return Err(StatuslineRegistryError::NameTooLong);
}
if name.chars().any(char::is_control) {
return Err(StatuslineRegistryError::NameControl);
}
Ok(())
}
fn validate_face(face: &str) -> Result<(), StatuslineRegistryError> {
if face.len() > MAX_STATUSLINE_FACE_BYTES {
return Err(StatuslineRegistryError::FaceTooLong);
}
if face.chars().any(char::is_control) {
return Err(StatuslineRegistryError::FaceControl);
}
if !is_modeline_face_name(face) {
return Err(StatuslineRegistryError::InvalidFace);
}
Ok(())
}
/// Evaluate one target without holding a core or registry borrow across Lua.
///
/// Three phases are enforced internally: capture contexts, snapshot definitions
/// and invoke them, then compare epoch and contexts before publication.
pub fn evaluate_statusline(
lua: &Lua,
core: &Rc<RefCell<EditorCore>>,
registry: &SharedStatuslineRegistry,
target: StatuslineEvaluationTarget,
) -> StatuslineEvaluation {
let initial_contexts = match capture_target_contexts(core, target) {
Ok(contexts) => contexts,
Err(reason) => {
return StatuslineEvaluation {
outcome: StatuslineEvaluationOutcome::NoMessage(reason),
new_failures: Vec::new(),
};
}
};
let (layout_epoch, definitions) = registry.borrow().snapshot_enabled();
let mut windows = initial_contexts
.iter()
.copied()
.map(|context| StatuslineWindowSegments {
context,
left: Vec::new(),
right: Vec::new(),
})
.collect::<Vec<_>>();
let mut new_failures = Vec::new();
for window in &mut windows {
for definition in &definitions {
match call_provider(lua, definition, window.context) {
Ok(Some(text)) => {
registry
.borrow_mut()
.note_success(definition.metadata.id, window.context);
let segment = EvaluatedStatuslineSegment {
provider_id: definition.metadata.id,
text,
face: definition.metadata.face.clone(),
};
match definition.metadata.side {
StatuslineSide::Left => window.left.push(segment),
StatuslineSide::Right => window.right.push(segment),
}
}
Ok(None) => registry
.borrow_mut()
.note_success(definition.metadata.id, window.context),
Err(message) => {
if registry
.borrow_mut()
.note_failure(definition.metadata.id, window.context)
{
let failure = StatuslineProviderFailure {
provider_id: definition.metadata.id,
provider_name: definition.metadata.name.clone(),
source: definition.metadata.source.clone(),
context: window.context,
message,
};
crate::lua_bindings::log_statusline_provider_error(lua, &failure);
new_failures.push(failure);
}
}
}
}
}
let final_contexts = capture_target_contexts(core, target).ok();
let live_contexts = capture_all_live_contexts(core);
let final_epoch = registry.borrow().layout_epoch();
if let Some(live) = live_contexts {
registry.borrow_mut().retain_live_contexts(&live);
}
let outcome = if final_epoch == layout_epoch
&& final_contexts.as_deref() == Some(initial_contexts.as_slice())
{
StatuslineEvaluationOutcome::Ready(windows)
} else {
StatuslineEvaluationOutcome::Invalidated {
authoritative_empty: initial_contexts,
}
};
StatuslineEvaluation {
outcome,
new_failures,
}
}
fn capture_target_contexts(
core: &Rc<RefCell<EditorCore>>,
target: StatuslineEvaluationTarget,
) -> Result<Vec<StatuslineContext>, StatuslineNoMessageReason> {
let core = core
.try_borrow()
.map_err(|_| StatuslineNoMessageReason::CoreBorrowConflict)?;
let buffers = core
.registry
.try_borrow()
.map_err(|_| StatuslineNoMessageReason::CoreBorrowConflict)?;
match target {
StatuslineEvaluationTarget::Grid { frontend_id } => {
let view = core
.views
.get(&frontend_id)
.ok_or(StatuslineNoMessageReason::ContextUnavailable)?;
let mut contexts = Vec::new();
for window_id in view.layout.iter_ids() {
let window = core
.windows
.get(&window_id)
.ok_or(StatuslineNoMessageReason::ContextUnavailable)?;
if buffers.get(window.buffer_id).is_err() {
return Err(StatuslineNoMessageReason::BufferUnavailable);
}
contexts.push(StatuslineContext {
frontend_id,
window_id,
buffer_id: window.buffer_id,
active: window_id == view.active,
});
}
Ok(contexts)
}
StatuslineEvaluationTarget::Semantic {
frontend_id,
declared_buffer,
} => {
let view = core
.views
.get(&frontend_id)
.ok_or(StatuslineNoMessageReason::ContextUnavailable)?;
// Bottom-panel §1.3 #12 — Projection. This LOOKUP resolves
// the primary document window: with a panel focused,
// `view.active` would name the panel and the declared-buffer
// check would clear the document's statusline.
//
// `active` is NOT rerouted with it (Q#BP14/parent 42): it
// reports ACTUAL focus, so a document provider truthfully
// observes `active = false` while the panel owns focus.
let window_id = core
.primary_document_window(frontend_id)
.ok_or(StatuslineNoMessageReason::ContextUnavailable)?;
let window = core
.windows
.get(&window_id)
.ok_or(StatuslineNoMessageReason::ContextUnavailable)?;
if buffers.get(window.buffer_id).is_err() {
return Err(StatuslineNoMessageReason::BufferUnavailable);
}
if window.buffer_id != declared_buffer {
return Err(StatuslineNoMessageReason::DeclaredBufferMismatch);
}
let mut contexts = vec![StatuslineContext {
frontend_id,
window_id: window.id,
buffer_id: window.buffer_id,
active: window.id == view.active,
}];
// Bottom-panel Q#BP8 / A2A-2 — the semantic fan-out is the
// primary document PLUS the frontend's visible side window,
// and nothing else: unprojected document splits run no
// callbacks. The document result feeds the semantic
// `StatuslineSegments`; the side result paints in the panel's
// own mode line.
//
// A derived-hidden side window is omitted (Q#BP2b): it has no
// mode line to paint this frame, so evaluating providers for
// it would invoke callbacks for a surface nobody can see.
if !view.panel_hidden {
for side_id in view.layout.iter_ids() {
if side_id == window.id {
continue;
}
let Some(side) = core.windows.get(&side_id) else {
return Err(StatuslineNoMessageReason::ContextUnavailable);
};
if !side.is_side() {
continue;
}
if buffers.get(side.buffer_id).is_err() {
return Err(StatuslineNoMessageReason::BufferUnavailable);
}
contexts.push(StatuslineContext {
frontend_id,
window_id: side.id,
buffer_id: side.buffer_id,
// Same rule as the document context: ACTUAL focus.
active: side.id == view.active,
});
}
}
Ok(contexts)
}
}
}
fn capture_all_live_contexts(core: &Rc<RefCell<EditorCore>>) -> Option<HashSet<StatuslineContext>> {
let core = core.try_borrow().ok()?;
let buffers = core.registry.try_borrow().ok()?;
let mut live = HashSet::new();
for (&frontend_id, view) in &core.views {
for window_id in view.layout.iter_ids() {
if let Some(window) = core.windows.get(&window_id)
&& buffers.get(window.buffer_id).is_ok()
{
live.insert(StatuslineContext {
frontend_id,
window_id,
buffer_id: window.buffer_id,
active: window_id == view.active,
});
}
}
}
Some(live)
}
fn call_provider(
lua: &Lua,
definition: &StatuslineProviderDefinition,
context: StatuslineContext,
) -> Result<Option<String>, String> {
let table = lua.create_table().map_err(|error| error.to_string())?;
table
.raw_set("frontend", context.frontend_id.0)
.map_err(|error| error.to_string())?;
table
.raw_set("window", context.window_id.raw())
.map_err(|error| error.to_string())?;
table
.raw_set("buffer", BufferIdLua(context.buffer_id))
.map_err(|error| error.to_string())?;
table
.raw_set("active", context.active)
.map_err(|error| error.to_string())?;
let value: Value = definition
.callback
.call(table)
.map_err(|error| error.to_string())?;
let bytes = match value {
Value::Nil => return Ok(None),
Value::String(value) => value
.to_str()
.map_err(|_| "callback returned a string that is not valid UTF-8".to_owned())?
.as_bytes()
.to_vec(),
other => {
return Err(format!(
"callback must return a string or nil, got {}",
other.type_name()
));
}
};
let text = String::from_utf8(bytes).expect("mlua to_str validated UTF-8");
let text = sanitize_provider_text(&text);
if text.len() > MAX_STATUSLINE_SEGMENT_BYTES {
return Err(format!(
"callback result exceeds {MAX_STATUSLINE_SEGMENT_BYTES} bytes after sanitation"
));
}
if text.is_empty() {
Ok(None)
} else {
Ok(Some(text))
}
}
/// Apply the shared one-line provider-output policy.
#[must_use]
pub fn sanitize_provider_text(text: &str) -> String {
let mut output = String::with_capacity(text.len());
for ch in text.chars() {
if ch == '\n' {
break;
}
if ch.is_control() {
output.push(' ');
} else {
output.push(ch);
}
}
output
}
/// Flatten a provider failure into one durable `*errors*` entry while
/// retaining multi-line traceback content.
#[must_use]
pub(crate) fn sanitize_provider_error_text(text: &str) -> String {
text.chars()
.map(|ch| if ch.is_control() { ' ' } else { ch })
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::buffer_registry::BufferRegistry;
use crate::lua::LuaHost;
use crate::lua_bindings::{SharedRegistry, statusline_registry};
fn harness() -> (LuaHost, Rc<RefCell<EditorCore>>, SharedStatuslineRegistry) {
let buffers: SharedRegistry = Rc::new(RefCell::new(BufferRegistry::new()));
let core = Rc::new(RefCell::new(EditorCore::new(buffers.clone())));
let host = LuaHost::with_registry(buffers).expect("Lua host");
let statusline = statusline_registry(host.lua()).expect("statusline registry");
(host, core, statusline)
}
#[test]
fn provider_output_and_error_log_have_distinct_newline_policies() {
let multiline = "boom\nstack\ttrace\r\0";
assert_eq!(sanitize_provider_text(multiline), "boom");
assert_eq!(
sanitize_provider_error_text(multiline),
"boom stack trace "
);
let (host, core, registry) = harness();
host.lua()
.load(
r"
pmacs.statusline.register {
name='trace', side='left',
fn=function() error('boom\nstack detail') end,
}
",
)
.exec()
.unwrap();
let evaluation = evaluate_statusline(
host.lua(),
&core,
&registry,
StatuslineEvaluationTarget::Grid {
frontend_id: FrontendId::LOCAL,
},
);
assert_eq!(evaluation.new_failures.len(), 1);
let errors = host.errors_buffer_text();
assert!(
errors.contains("boom stack detail") && errors.contains("stack traceback"),
"flattened provider traceback must retain every line: {errors:?}"
);
assert_eq!(
errors.lines().count(),
1,
"one failure run must remain one durable error entry"
);
}
#[test]
fn registry_epochs_track_layout_and_distinct_enabled_faces() {
let lua = Lua::new();
let callback = lua.create_function(|_, ()| Ok(())).unwrap();
let mut registry = StatuslineRegistry::new();
let first = registry
.register(
"a".to_owned(),
StatuslineSide::Left,
0,
"ui.modeline.same".to_owned(),
callback.clone(),
SourceLocation::default(),
)
.unwrap();
let second = registry
.register(
"b".to_owned(),
StatuslineSide::Right,
0,
"ui.modeline.same".to_owned(),
callback,
SourceLocation::default(),
)
.unwrap();
assert_eq!((registry.layout_epoch(), registry.face_set_epoch()), (2, 1));
assert!(registry.set_priority(first, 0));
assert_eq!((registry.layout_epoch(), registry.face_set_epoch()), (2, 1));
assert!(registry.set_priority(first, 1));
assert_eq!((registry.layout_epoch(), registry.face_set_epoch()), (3, 1));
assert!(registry.set_enabled(first, false));
assert_eq!((registry.layout_epoch(), registry.face_set_epoch()), (4, 1));
assert!(registry.set_enabled(second, false));
assert_eq!((registry.layout_epoch(), registry.face_set_epoch()), (5, 2));
assert!(registry.unregister(first));
assert_eq!((registry.layout_epoch(), registry.face_set_epoch()), (6, 2));
assert!(!registry.unregister(first));
}
#[test]
fn lua_registration_is_strict_raw_and_lifecycle_is_introspectable() {
let (host, _, registry) = harness();
host.lua()
.load(
r#"
local touched = false
local mt = {
__index = function() touched = true; error("no __index") end,
__pairs = function() touched = true; error("no __pairs") end,
}
local spec = setmetatable({
name = "mine", side = "left", priority = 3,
face = "ui.modeline.mine", fn = function() return "ok" end,
}, mt)
H = pmacs.statusline.register(spec)
P = pmacs.statusline.providers()
assert(not touched)
assert(#P == 1 and P[1].handle == H and P[1].name == "mine")
assert(pmacs.statusline.set_priority(H, -4))
assert(pmacs.statusline.set_enabled(H, false))
assert(pmacs.statusline.unregister(H))
assert(not pmacs.statusline.unregister(H))
assert(not pmacs.statusline.set_enabled(H, true))
"#,
)
.exec()
.unwrap();
assert!(registry.borrow().is_empty());
for script in [
"pmacs.statusline.register{name='x',side='left',fn=function()end,wat=1}",
"pmacs.statusline.register{name='x',side='middle',fn=function()end}",
"pmacs.statusline.register{name='x',side='left',priority=1.5,fn=function()end}",
"pmacs.statusline.register{name='x',side='left',face='ui.statusline',fn=function()end}",
"pmacs.statusline.register{name='x',side='left',fn=true}",
] {
assert!(host.lua().load(script).exec().is_err(), "accepted {script}");
assert!(registry.borrow().is_empty());
}
}
#[test]
fn lua_registration_enforces_shared_capacity_without_partial_mutation() {
let (host, _, registry) = harness();
host.lua()
.load(
r#"
for i = 1, 64 do
pmacs.statusline.register {
name = "p" .. i,
side = "left",
fn = function() return nil end,
}
end
"#,
)
.exec()
.unwrap();
let epochs = {
let registry = registry.borrow();
(registry.layout_epoch(), registry.face_set_epoch())
};
let error = host
.lua()
.load("pmacs.statusline.register{name='overflow',side='right',fn=function()end}")
.exec()
.unwrap_err()
.to_string();
assert!(error.contains("64"), "{error}");
let registry = registry.borrow();
assert_eq!(registry.len(), MAX_STATUSLINE_PROVIDERS);
assert_eq!((registry.layout_epoch(), registry.face_set_epoch()), epochs);
}
#[test]
fn evaluation_orders_sanitizes_and_exposes_owned_context() {
let (host, core, registry) = harness();
host.lua()
.load(
r"
A = pmacs.statusline.register{name='a',side='left',priority=0,fn=function(ctx)
assert(type(ctx.frontend)=='number' and type(ctx.window)=='number')
assert(ctx.buffer and ctx.active == true)
return 'a\rX\nignored'
end}
B = pmacs.statusline.register{name='b',side='left',priority=9,fn=function() return 'b' end}
C = pmacs.statusline.register{name='c',side='right',priority=9,fn=function() return 'c' end}
D = pmacs.statusline.register{name='d',side='right',priority=-2,fn=function() return 'd' end}
",
)
.exec()
.unwrap();
let evaluation = evaluate_statusline(
host.lua(),
&core,
&registry,
StatuslineEvaluationTarget::Grid {
frontend_id: FrontendId::LOCAL,
},
);
let StatuslineEvaluationOutcome::Ready(windows) = evaluation.outcome else {
panic!("expected ready outcome");
};
assert_eq!(windows.len(), 1);
assert_eq!(
windows[0]
.left
.iter()
.map(|segment| segment.text.as_str())
.collect::<Vec<_>>(),
["b", "a X"]
);
assert_eq!(
windows[0]
.right
.iter()
.map(|segment| segment.text.as_str())
.collect::<Vec<_>>(),
["d", "c"]
);
}
#[test]
fn invalid_results_are_isolated_omitted_and_latched() {
let (host, core, registry) = harness();
host.lua()
.load(
r"
pmacs.statusline.register{name='good-a',side='left',priority=2,fn=function() return 'a' end}
pmacs.statusline.register{name='number',side='left',priority=1,fn=function() return 42 end}
pmacs.statusline.register{name='good-b',side='left',priority=0,fn=function() return 'b' end}
pmacs.statusline.register{name='bytes',side='right',fn=function() return string.char(255) end}
pmacs.statusline.register{name='huge',side='right',fn=function() return string.rep('x', 1025) end}
",
)
.exec()
.unwrap();
let target = StatuslineEvaluationTarget::Grid {
frontend_id: FrontendId::LOCAL,
};
let first = evaluate_statusline(host.lua(), &core, &registry, target);
assert_eq!(first.new_failures.len(), 3);
let StatuslineEvaluationOutcome::Ready(windows) = first.outcome else {
panic!("expected ready output despite isolated failures");
};
assert_eq!(
windows[0]
.left
.iter()
.map(|segment| segment.text.as_str())
.collect::<Vec<_>>(),
["a", "b"]
);
assert!(windows[0].right.is_empty());
let repeated = evaluate_statusline(host.lua(), &core, &registry, target);
assert!(repeated.new_failures.is_empty());
}
#[test]
fn reentrant_unregister_is_invalidated_but_initial_mismatch_is_no_message() {
let (host, core, registry) = harness();
host.lua()
.load(
r"
H = pmacs.statusline.register{name='self',side='left',fn=function()
pmacs.statusline.unregister(H)
return 'stale'
end}
",
)
.exec()
.unwrap();
let active_buffer = core.borrow().active_buffer_id();
let evaluation = evaluate_statusline(
host.lua(),
&core,
&registry,
StatuslineEvaluationTarget::Semantic {
frontend_id: FrontendId::LOCAL,
declared_buffer: active_buffer,
},
);
let StatuslineEvaluationOutcome::Invalidated {
authoritative_empty,
} = evaluation.outcome
else {
panic!("expected invalidated outcome");
};
assert_eq!(authoritative_empty.len(), 1);
let other_buffer = core.borrow().registry.borrow_mut().create("other");
let mismatch = evaluate_statusline(
host.lua(),
&core,
&registry,
StatuslineEvaluationTarget::Semantic {
frontend_id: FrontendId::LOCAL,
declared_buffer: other_buffer,
},
);
assert_eq!(
mismatch.outcome,
StatuslineEvaluationOutcome::NoMessage(
StatuslineNoMessageReason::DeclaredBufferMismatch
)
);
}
#[test]
fn editor_bootstrap_registers_the_pure_lsp_provider() {
let state = crate::editor::EditorState::new();
let registry = statusline_registry(state.lua_host.lua()).unwrap();
let providers = registry.borrow().providers();
let lsp = providers
.iter()
.find(|provider| provider.name == "lsp")
.expect("builtin LSP provider");
assert_eq!(lsp.side, StatuslineSide::Right);
assert_eq!(lsp.priority, 0);
assert_eq!(lsp.face, "ui.modeline.lsp");
assert!(lsp.enabled);
}
#[test]
fn failure_latch_success_and_detach_each_rearm() {
let (host, core, registry) = harness();
host.lua()
.load(
r#"
MODE = "fail"
pmacs.statusline.register {
name = "bad", side = "left",
fn = function()
if MODE == "fail" then error("boom") end
return nil
end,
}
"#,
)
.exec()
.unwrap();
let target = StatuslineEvaluationTarget::Grid {
frontend_id: FrontendId::LOCAL,
};
let first = evaluate_statusline(host.lua(), &core, &registry, target);
assert_eq!(first.new_failures.len(), 1);
let second = evaluate_statusline(host.lua(), &core, &registry, target);
assert!(second.new_failures.is_empty());
host.lua().load("MODE = 'ok'").exec().unwrap();
let success = evaluate_statusline(host.lua(), &core, &registry, target);
assert!(success.new_failures.is_empty());
host.lua().load("MODE = 'fail'").exec().unwrap();
let rearmed = evaluate_statusline(host.lua(), &core, &registry, target);
assert_eq!(rearmed.new_failures.len(), 1);
registry.borrow_mut().detach_frontend(FrontendId::LOCAL);
let detached = evaluate_statusline(host.lua(), &core, &registry, target);
assert_eq!(detached.new_failures.len(), 1);
}
}