feat: wire major modes through key dispatch

Store a detected major mode on each buffer and expose it through Lua.
Resolve mode-scoped bindings in dispatch, describe-key, and help links,
including exact encoded mode context after entering the help buffer.

Initialize modes once at buffer load, preserve explicit overrides and
clears across switches, and publish the mode through a per-window
statusline provider. Add daemon acceptance for the complete mode lifecycle.
This commit is contained in:
Levi Neuwirth 2026-07-21 20:25:48 -04:00
parent a548850423
commit 99cd7ec240
8 changed files with 1031 additions and 75 deletions

View File

@ -228,7 +228,7 @@ local function resolve_active_language(buf)
return pmacs.parse.language_from_shebang(buf)
end
local function attach_for_active_buffer()
local function attach_for_active_buffer(initialize_mode)
local buf = pmacs.window.buffer()
if not buf then return end
local key = tostring(buf)
@ -245,6 +245,14 @@ local function attach_for_active_buffer()
-- never gives a wrong-grammar tree.
local lang = pmacs.parse._has_view(buf) and parse_lang_by_buffer[key]
or resolve_active_language(buf)
-- The detected language is also the initial major-mode name. Do this
-- before grammar gating: a language supplied only by an LSP filetype or
-- shebang mapping is still a valid mode even when no parser is bundled.
-- Only after-load initializes it; after-switch must preserve explicit
-- overrides and explicit nil clears.
if initialize_mode and lang and pmacs.buffer.major_mode(buf) == nil then
pmacs.buffer.set_major_mode(buf, lang)
end
if not lang or not pmacs.parse._has_language(lang) then return end
pmacs.parse._dispatch(buf, lang)
-- T M4.3: install the syntax-highlight overlay for this buffer.
@ -266,7 +274,7 @@ end
pmacs.hook.add("buffer.after-load", function()
-- Best-effort: a missing grammar / re-entry / stale buffer
-- mustn't poison the rest of the after-load chain.
local ok, err = pcall(attach_for_active_buffer)
local ok, err = pcall(function() attach_for_active_buffer(true) end)
if not ok and pmacs.error then
pmacs.error("syntax.after-load: " .. tostring(err))
end
@ -284,13 +292,29 @@ pmacs.hook.add("buffer.after-switch", function()
local buf = pmacs.window.buffer()
if not buf then return end
highlighted_buffers[tostring(buf)] = nil
attach_for_active_buffer()
attach_for_active_buffer(false)
end)
if not ok and pmacs.error then
pmacs.error("syntax.after-switch: " .. tostring(err))
end
end)
-- Major mode is window-local presentation state because each split may show
-- a different buffer. The provider therefore reads ctx.buffer rather than
-- the focused buffer. Empty text omits the segment without tripping the
-- statusline failure latch.
pmacs.statusline.register {
name = "mode",
side = "left",
priority = 0,
face = "ui.modeline",
fn = function(ctx)
local mode = pmacs.buffer.major_mode(ctx.buffer)
if mode == nil then return "" end
return "(" .. mode .. ")"
end,
}
local function reparse_active_buffer_after_edit()
local buf = pmacs.window.buffer()
if not buf then return end

View File

@ -159,6 +159,8 @@ pub struct Buffer {
id: BufferId,
rope: Rope,
name: String,
/// The buffer's single active major mode, if one has been selected.
major_mode: Option<String>,
is_modified: bool,
/// When set, every content mutation is rejected before touching the
/// rope, CRDT, history, revision, modified bit, marks, or views.
@ -245,6 +247,7 @@ impl Buffer {
id,
rope,
name: name.into(),
major_mode: None,
is_modified: false,
read_only: false,
revision: 0,
@ -451,6 +454,20 @@ impl Buffer {
self.name = name.into();
}
/// This buffer's active major mode, if any.
///
/// The returned name borrows the buffer-owned mode string so key
/// dispatch can resolve mode bindings without cloning on its hot path.
#[must_use]
pub fn major_mode(&self) -> Option<&str> {
self.major_mode.as_deref()
}
/// Replace this buffer's active major mode, or clear it with `None`.
pub fn set_major_mode(&mut self, major_mode: Option<String>) {
self.major_mode = major_mode;
}
/// Whether the buffer has been modified since the last save / load.
#[must_use]
pub fn is_modified(&self) -> bool {
@ -1869,6 +1886,18 @@ mod tests {
out
}
#[test]
fn major_mode_is_buffer_owned_and_replaceable() {
let mut buf = Buffer::new(BufferId::next(), "*mode-test*");
assert_eq!(buf.major_mode(), None);
buf.set_major_mode(Some("rust".to_owned()));
assert_eq!(buf.major_mode(), Some("rust"));
buf.set_major_mode(None);
assert_eq!(buf.major_mode(), None);
}
dual_mode_test!(
read_only_rejects_direct_skip_history_mutations,
|make, make_bytes| {

View File

@ -797,16 +797,21 @@ impl EditorState {
return;
}
// Buffer-scope keybindings need the active buffer id passed
// through the dispatcher (otherwise `keymap_stack::resolve`
// skips the buffer-local map entirely and every "scope =
// buffer" binding falls through to global). The id is read
// outside the keymap borrow so a single-buffer focus check
// doesn't collide with the stack lookup below.
let active_buffer = Some(self.core.borrow().active_buffer_id());
// Buffer- and mode-scope keybindings resolve against the active
// buffer. Keep its mode borrowed from the registry only while the
// pure keymap lookup runs: `Option::as_slice` provides the required
// zero-or-one borrowed slice without allocating or cloning. Both
// RefCell borrows end with this block, before any Lua command runs.
let active_buffer = self.core.borrow().active_buffer_id();
let action = {
let registry = self.lua_host.registry().borrow();
let active_mode = registry
.get(active_buffer)
.ok()
.and_then(|buffer| buffer.major_mode());
let stack = self.lua_host.keymaps().borrow();
self.dispatcher.dispatch(chord, &stack, active_buffer, &[])
self.dispatcher
.dispatch(chord, &stack, Some(active_buffer), active_mode.as_slice())
};
// Snapshot the active buffer's edit revision before the command
@ -3722,6 +3727,34 @@ mod tests {
}
}
#[test]
fn dispatch_uses_active_buffer_major_mode_and_releases_borrows() {
let mut s = fresh_with(b"");
let buffer_id = s.core.borrow().active_buffer_id();
s.lua_host
.registry()
.borrow_mut()
.get_mut(buffer_id)
.unwrap()
.set_major_mode(Some("dispatch-test".to_owned()));
s.lua_host
.keymaps()
.borrow_mut()
.bind_mode(
"dispatch-test",
&crate::key::parse_sequence("C-b").unwrap(),
"editor.list-buffers",
crate::command::SourceLocation::default(),
)
.unwrap();
// `editor.list-buffers` mutably borrows the buffer registry. Reaching
// the resulting buffer therefore proves dispatch released both its
// registry and keymap borrows before invoking the mode-bound command.
s.dispatch_key(FrontendId::LOCAL, ctrl('b'));
assert_eq!(s.core.borrow().active_buffer_name(), "*buffer-list*");
}
#[test]
fn cx_cb_invokes_list_buffers() {
// Regression for the user-reported "C-x C-b stalls" bug. After

View File

@ -13,6 +13,7 @@
//! * `[command: cursor.left]` --- navigate to that command's help.
//! * `[key: C-x C-s]` --- describe the chord.
//! * `[key: s @buffer:3]` --- describe a buffer-local chord.
//! * `[key: g @mode:rust]` --- describe a mode-scoped chord.
//! * `[buffer: *errors*]` --- describe a buffer by name.
//! * `[mode: normal]`, `[hook: buffer.before-save]`, `[view: *help*]`.
//!
@ -84,21 +85,20 @@ pub fn render_command(
/// Render help for a chord sequence. Returns the help buffer id if
/// the sequence resolves to a binding, [`None`] otherwise.
///
/// `active_buffer` is the buffer scope to consult when resolving
/// the chord sequence. Pass `Some(id)` to surface buffer-local
/// bindings (matching what `dispatch_key` would see) and `None`
/// for global-only resolution. Buffer-scope keys (e.g.,
/// `pmacs-magit.stage` bound to `s` on the magit buffer) are
/// invisible without this, which is the M8.7 describe-key gap.
/// `active_buffer` and `active_modes` are the exact scope context to
/// consult when resolving the chord sequence. Pass the active buffer
/// and its zero-or-one major-mode slice to match dispatch, or no
/// context for global-only resolution.
pub fn render_key(
registry: &mut BufferRegistry,
commands: &CommandRegistry,
keymaps: &KeymapStack,
active_buffer: Option<BufferId>,
active_modes: &[&str],
sequence: &str,
) -> RenderResult {
let chords = parse_sequence(sequence).ok()?;
let resolution = keymaps.resolve(&chords, active_buffer, &[]);
let resolution = keymaps.resolve(&chords, active_buffer, active_modes);
let StackResolution::Bound(rb) = resolution else {
return None;
};
@ -150,7 +150,7 @@ pub fn render_mode(
let mut text = String::new();
let _ = writeln!(text, "Mode: {name}");
let _ = writeln!(text);
write_mode_bindings(&mut text, map);
write_mode_bindings(&mut text, name, map);
Some(replace_help_buffer(registry, &text))
}
@ -245,6 +245,16 @@ fn write_command_bindings(
scope.render()
);
}
Scope::Mode(name) => {
let encoded_name = encode_mode_target(name);
let _ = writeln!(
out,
" [key: {} @mode:{}] ({})",
display_sequence(seq),
encoded_name,
scope.render()
);
}
_ => {
let _ = writeln!(
out,
@ -258,33 +268,84 @@ fn write_command_bindings(
}
}
fn parse_key_target(registry: &BufferRegistry, target: &str) -> Option<(String, Option<BufferId>)> {
let Some((sequence, raw)) = target.rsplit_once(" @buffer:") else {
return Some((target.to_owned(), None));
};
let Ok(raw) = raw.trim().parse::<u64>() else {
return None;
};
let id = BufferId::from_raw(raw);
if registry.contains(id) {
Some((sequence.trim().to_owned(), Some(id)))
} else {
None
}
fn is_mode_target_unreserved(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'~' | b'-')
}
fn write_mode_bindings(out: &mut String, map: &Keymap) {
fn encode_mode_target(mode: &str) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let mut encoded = String::with_capacity(mode.len());
for byte in mode.bytes() {
if is_mode_target_unreserved(byte) {
encoded.push(char::from(byte));
} else {
encoded.push('%');
encoded.push(char::from(HEX[usize::from(byte >> 4)]));
encoded.push(char::from(HEX[usize::from(byte & 0x0F)]));
}
}
encoded
}
fn decode_mode_target(encoded: &str) -> Option<String> {
let encoded = encoded.as_bytes();
let mut decoded = Vec::with_capacity(encoded.len());
let mut index = 0;
while index < encoded.len() {
let byte = encoded[index];
if byte == b'%' {
let high = *encoded.get(index + 1)?;
let low = *encoded.get(index + 2)?;
let high = char::from(high).to_digit(16)?;
let low = char::from(low).to_digit(16)?;
decoded.push(u8::try_from((high << 4) | low).ok()?);
index += 3;
} else {
if !is_mode_target_unreserved(byte) {
return None;
}
decoded.push(byte);
index += 1;
}
}
String::from_utf8(decoded).ok()
}
fn parse_key_target(
registry: &BufferRegistry,
target: &str,
) -> Option<(String, Option<BufferId>, Option<String>)> {
if let Some((sequence, raw)) = target.rsplit_once(" @buffer:") {
let raw = raw.trim().parse::<u64>().ok()?;
let id = BufferId::from_raw(raw);
return registry
.contains(id)
.then(|| (sequence.trim().to_owned(), Some(id), None));
}
if let Some((sequence, encoded_mode)) = target.rsplit_once(" @mode:") {
let mode = decode_mode_target(encoded_mode)?;
return Some((sequence.trim().to_owned(), None, Some(mode)));
}
Some((target.to_owned(), None, None))
}
fn write_mode_bindings(out: &mut String, mode: &str, map: &Keymap) {
let entries: Vec<_> = map.iter().collect();
if entries.is_empty() {
let _ = writeln!(out, "(empty mode keymap)");
return;
}
let _ = writeln!(out, "Bindings:");
let encoded_mode = encode_mode_target(mode);
for (seq, binding) in entries {
let _ = writeln!(
out,
" [key: {}] -> [command: {}]",
" [key: {} @mode:{}] -> [command: {}]",
display_sequence(&seq),
encoded_mode,
binding.command
);
}
@ -430,8 +491,17 @@ pub fn follow_link_at(
match link.kind.as_str() {
"command" => render_command(registry, commands, keymaps, &link.target),
"key" => {
let (sequence, active_buffer) = parse_key_target(registry, &link.target)?;
render_key(registry, commands, keymaps, active_buffer, &sequence)
let (sequence, active_buffer, mode) = parse_key_target(registry, &link.target)?;
let mode = mode.as_deref();
let active_modes = mode.as_slice();
render_key(
registry,
commands,
keymaps,
active_buffer,
active_modes,
&sequence,
)
}
"buffer" => {
let id = registry.find_by_name(&link.target)?;
@ -542,7 +612,7 @@ mod tests {
},
)
.unwrap();
let (id, _) = render_key(&mut reg, &cmds, &kms, None, "C-x C-s").unwrap();
let (id, _) = render_key(&mut reg, &cmds, &kms, None, &[], "C-x C-s").unwrap();
let body = read_buffer_text(reg.get(id).unwrap());
assert!(body.contains("Key: C-x C-s"));
assert!(body.contains("[command: save]"));
@ -554,7 +624,38 @@ mod tests {
let mut reg = BufferRegistry::new();
let cmds = CommandRegistry::new();
let kms = KeymapStack::new();
assert!(render_key(&mut reg, &cmds, &kms, None, "C-q").is_none());
assert!(render_key(&mut reg, &cmds, &kms, None, &[], "C-q").is_none());
}
#[test]
fn render_key_uses_explicit_mode_context_before_global() {
let lua = Lua::new();
let mut reg = BufferRegistry::new();
let mut cmds = CommandRegistry::new();
let mut kms = KeymapStack::new();
cmds.define(make_command(&lua, "rust.save", "Rust save."))
.unwrap();
cmds.define(make_command(&lua, "global.save", "Global save."))
.unwrap();
kms.bind_global(
&parse_sequence("C-s").unwrap(),
"global.save",
SourceLocation::default(),
)
.unwrap();
kms.bind_mode(
"rust",
&parse_sequence("C-s").unwrap(),
"rust.save",
SourceLocation::default(),
)
.unwrap();
render_key(&mut reg, &cmds, &kms, None, &["rust"], "C-s").unwrap();
let body = read_help(&reg);
assert!(body.contains("Scope: mode:rust"), "{body}");
assert!(body.contains("[command: rust.save]"), "{body}");
assert!(!body.contains("[command: global.save]"), "{body}");
}
#[test]
@ -643,7 +744,7 @@ mod tests {
let _ = render_mode(&mut reg, &kms, "demo").unwrap();
let body = read_help(&reg);
assert!(body.contains("Mode: demo"));
assert!(body.contains("[key: C-x]"));
assert!(body.contains("[key: C-x @mode:demo]"));
assert!(body.contains("[command: x]"));
}
@ -678,6 +779,26 @@ mod tests {
assert_eq!(span.target, "C-x C-s");
}
#[test]
fn mode_key_target_codec_is_strict_and_round_trips_utf8() {
for mode in ["rust", "", " rust ", "]", "%", "", "x @buffer:1"] {
let encoded = encode_mode_target(mode);
assert_eq!(decode_mode_target(&encoded).as_deref(), Some(mode));
}
assert_eq!(encode_mode_target("rust"), "rust");
for malformed in ["%", "%0", "%GG", "%FF", "raw space", ""] {
assert!(
decode_mode_target(malformed).is_none(),
"accepted malformed mode target {malformed:?}"
);
}
let reg = BufferRegistry::new();
let (_, _, mode) = parse_key_target(&reg, "s @mode:").unwrap();
assert_eq!(mode.as_deref(), Some(""));
assert!(parse_key_target(&reg, "s @mode:%FF").is_none());
}
#[test]
fn link_at_off_link_returns_none() {
let text = "Plain text with no [command: foo] here.\n";
@ -752,4 +873,82 @@ mod tests {
assert!(body.contains("Scope: buffer"), "{body}");
assert!(body.contains("[command: pmacs-magit.stage]"), "{body}");
}
#[test]
fn follow_mode_key_link_preserves_mode_after_help_buffer_activation() {
let lua = Lua::new();
let mut reg = BufferRegistry::new();
let mut cmds = CommandRegistry::new();
let mut kms = KeymapStack::new();
let hooks = HookRegistry::new();
cmds.define(make_command(&lua, "rust.action", "Mode action."))
.unwrap();
cmds.define(make_command(&lua, "global.action", "Global fallback."))
.unwrap();
kms.bind_mode(
"rust",
&parse_sequence("s").unwrap(),
"rust.action",
SourceLocation::default(),
)
.unwrap();
kms.bind_global(
&parse_sequence("s").unwrap(),
"global.action",
SourceLocation::default(),
)
.unwrap();
render_command(&mut reg, &cmds, &kms, "rust.action").unwrap();
let body = read_help(&reg);
assert!(
body.contains("[key: s @mode:rust]"),
"mode key link must carry its mode scope: {body}"
);
let cursor = body.find("s @mode").unwrap() as u64;
follow_link_at(&mut reg, &cmds, &kms, &hooks, cursor).unwrap();
let body = read_help(&reg);
assert!(body.contains("Scope: mode:rust"), "{body}");
assert!(body.contains("[command: rust.action]"), "{body}");
assert!(!body.contains("[command: global.action]"), "{body}");
}
#[test]
fn follow_mode_key_link_round_trips_reserved_utf8_mode_exactly() {
let lua = Lua::new();
let mut reg = BufferRegistry::new();
let mut cmds = CommandRegistry::new();
let mut kms = KeymapStack::new();
let hooks = HookRegistry::new();
let mode = " rust]雪% @buffer:7 ";
cmds.define(make_command(&lua, "exact.mode", "Exact mode action."))
.unwrap();
cmds.define(make_command(&lua, "global.fallback", "Global fallback."))
.unwrap();
kms.bind_mode(
mode,
&parse_sequence("x").unwrap(),
"exact.mode",
SourceLocation::default(),
)
.unwrap();
kms.bind_global(
&parse_sequence("x").unwrap(),
"global.fallback",
SourceLocation::default(),
)
.unwrap();
render_command(&mut reg, &cmds, &kms, "exact.mode").unwrap();
let body = read_help(&reg);
let encoded = encode_mode_target(mode);
let link = format!("[key: x @mode:{encoded}]");
assert!(body.contains(&link), "encoded mode link missing: {body}");
let cursor = body.find("@mode:").unwrap() as u64;
follow_link_at(&mut reg, &cmds, &kms, &hooks, cursor).unwrap();
let body = read_help(&reg);
assert!(body.contains(&format!("Scope: mode:{mode}")), "{body}");
assert!(body.contains("[command: exact.mode]"), "{body}");
assert!(!body.contains("[command: global.fallback]"), "{body}");
}
}

View File

@ -6,9 +6,8 @@
//!
//! 1. **Buffer-local**: bindings that apply only when a specific
//! buffer is the active one. Most-specific scope.
//! 2. **Mode**: bindings that apply when a mode is active. Modes
//! aren't a real concept until T M2.5+ but the stack accepts them
//! today so the resolver doesn't grow a dimension when we add them.
//! 2. **Mode**: bindings that apply when the active buffer's major mode
//! matches the mode keymap.
//! 3. **Global**: the universal fallback. Last-resort scope.
//!
//! [`KeymapStack::resolve`] walks them in order and returns the
@ -38,7 +37,7 @@ use crate::keymap_tree::{Binding, Keymap, KeymapError, Resolution};
pub enum Scope {
/// Buffer-local --- specific to one [`BufferId`].
Buffer(BufferId),
/// Mode-active --- one of the active mode keymaps.
/// Mode-active --- the active major mode's keymap.
Mode(String),
/// The global fallback.
Global,
@ -91,10 +90,8 @@ pub enum StackResolution {
pub struct KeymapStack {
/// The global keymap (always consulted last).
pub global: Keymap,
/// Per-mode keymaps. Mode activation order is preserved by `Vec`;
/// the resolver consults them after buffer-local but before
/// global. The top of the vector is the most recently activated
/// mode and wins ties.
/// Per-mode keymaps. Registration order is preserved by `Vec`; resolution
/// follows the borrowed mode names supplied to [`Self::resolve`].
pub modes: Vec<(String, Keymap)>,
/// Per-buffer keymaps. Buffer-local always beats mode and global.
pub buffers: HashMap<BufferId, Keymap>,
@ -227,9 +224,8 @@ impl KeymapStack {
/// Resolve `sequence` in scope priority order.
///
/// `active_buffer` is the [`BufferId`] currently in focus (if any).
/// `active_modes` lists the active mode names in
/// most-recent-first order; the first match in that order wins
/// among modes.
/// `active_modes` borrows active mode names in priority order; the first
/// match in that order wins among modes.
///
/// Resolution semantics: the resolver returns the *most-specific*
/// complete binding it finds. If no scope has a complete match
@ -240,7 +236,7 @@ impl KeymapStack {
&self,
sequence: &[Chord],
active_buffer: Option<BufferId>,
active_modes: &[String],
active_modes: &[&str],
) -> StackResolution {
let mut any_pending = false;
@ -262,12 +258,12 @@ impl KeymapStack {
// 2) Modes --- ordered by `active_modes`.
for mode_name in active_modes {
if let Some((_, map)) = self.modes.iter().find(|(n, _)| n == mode_name) {
if let Some((_, map)) = self.modes.iter().find(|(n, _)| n == *mode_name) {
match map.lookup(sequence) {
Resolution::Bound(b) => {
return StackResolution::Bound(ResolvedBinding {
binding: b,
scope: Scope::Mode(mode_name.clone()),
scope: Scope::Mode((*mode_name).to_owned()),
});
}
Resolution::Pending => any_pending = true,
@ -378,7 +374,7 @@ impl KeyDispatcher {
chord: Chord,
stack: &KeymapStack,
active_buffer: Option<BufferId>,
active_modes: &[String],
active_modes: &[&str],
) -> Action {
self.pending.push(chord);
match stack.resolve(&self.pending, active_buffer, active_modes) {
@ -474,12 +470,12 @@ mod tests {
s.bind_buffer(id, &seq("C-s"), "buffer.save", src(3))
.unwrap();
// Buffer wins.
match s.resolve(&seq("C-s"), Some(id), &["normal".into()]) {
match s.resolve(&seq("C-s"), Some(id), &["normal"]) {
StackResolution::Bound(rb) => assert_eq!(rb.binding.command, "buffer.save"),
other => panic!("got {other:?}"),
}
// No buffer: mode wins.
match s.resolve(&seq("C-s"), None, &["normal".into()]) {
match s.resolve(&seq("C-s"), None, &["normal"]) {
StackResolution::Bound(rb) => {
assert_eq!(rb.binding.command, "mode.save");
assert_eq!(rb.scope, Scope::Mode("normal".into()));

View File

@ -3033,6 +3033,39 @@ fn install_buffer_module(lua: &Lua, registry: &SharedRegistry) -> mlua::Result<T
)?;
}
{
let reg = registry.clone();
buffer.set(
"major_mode",
lua.create_function(move |_, id: BufferIdLua| {
let r = reg.borrow();
Ok(resolve(&r, id.0)?.major_mode().map(str::to_owned))
})?,
)?;
}
{
let reg = registry.clone();
buffer.set(
"set_major_mode",
lua.create_function(move |_, (id, mode): (BufferIdLua, Value)| {
let mode = match mode {
Value::Nil => None,
Value::String(mode) => Some(mode.to_str()?.to_owned()),
other => {
return Err(mlua::Error::external(format!(
"pmacs.buffer.set_major_mode: mode must be a string or nil, got {}",
other.type_name()
)));
}
};
let mut r = reg.borrow_mut();
resolve_mut(&mut r, id.0)?.set_major_mode(mode);
Ok(())
})?,
)?;
}
{
let reg = registry.clone();
buffer.set(
@ -5465,18 +5498,26 @@ fn install_help_module(
help_t.set(
"show_key",
lua.create_function(move |lua, sequence: String| {
// Resolve against the active window's buffer so
// help.show_key surfaces buffer-local bindings ---
// mirrors the same fix as pmacs.describe.key (M8.8
// audit finding 1).
let active_buffer = lua
.app_data_ref::<SharedCore>()
.map(|core| core.borrow().active_buffer_id());
// Copy the mode name before taking the mutable registry
// borrow used to replace *help*. The borrowed slice below
// then cannot outlive or conflict with help-buffer mutation.
let active_mode = {
let r = reg.borrow();
active_buffer
.and_then(|id| r.get(id).ok())
.and_then(crate::buffer::Buffer::major_mode)
.map(str::to_owned)
};
let active_mode = active_mode.as_deref();
let active_modes = active_mode.as_slice();
let result = {
let mut r = reg.borrow_mut();
let c = cmds.borrow();
let k = kms.borrow();
help::render_key(&mut r, &c, &k, active_buffer, &sequence)
help::render_key(&mut r, &c, &k, active_buffer, active_modes, &sequence)
};
if let Some((id, edits)) = result.as_ref() {
queue_generated_buffer_edits(lua, *id, edits);
@ -5859,6 +5900,10 @@ fn log_buffer_removed_error(lua: &Lua, source: &SourceLocation, err: &mlua::Erro
}
}
#[allow(
clippy::too_many_lines,
reason = "six describe bindings share one coherent registry surface; splitting them adds ceremony without clarifying borrow lifetimes"
)]
fn install_describe_module(
lua: &Lua,
registry: &SharedRegistry,
@ -5885,27 +5930,30 @@ fn install_describe_module(
}
{
let reg = registry.clone();
let cmds = commands.clone();
let kms = keymaps.clone();
describe.set(
"key",
lua.create_function(move |lua, sequence: String| {
let chords = parse_sequence(&sequence).map_err(mlua::Error::external)?;
// Resolve against the active window's buffer scope so
// buffer-local bindings (`scope = "buffer"`) actually
// surface --- without this, a `pmacs-magit.stage`
// binding on the magit buffer is invisible to
// describe-key, even when the user is sitting on
// that buffer with their cursor. `&[]` for the
// mode list mirrors what `dispatch_key` passes
// today (no mode system yet); when modes land, both
// call sites update together.
let active_buffer = lua
.app_data_ref::<SharedCore>()
.map(|core| core.borrow().active_buffer_id());
let km = kms.borrow();
let r = km.resolve(&chords, active_buffer, &[]);
match r {
// Keep the registry borrow only across pure resolution.
// `ResolvedBinding` owns its scope, so creating the Lua
// result table cannot retain a Buffer borrow or re-enter
// Lua while one is live.
let resolution = {
let r = reg.borrow();
let active_mode = active_buffer
.and_then(|id| r.get(id).ok())
.and_then(crate::buffer::Buffer::major_mode);
let active_modes = active_mode.as_slice();
let km = kms.borrow();
km.resolve(&chords, active_buffer, active_modes)
};
match resolution {
crate::keymap_stack::StackResolution::Bound(rb) => {
let cmds = cmds.borrow();
Ok(Value::Table(key_info_table(
@ -6169,6 +6217,26 @@ pub fn install_editor(lua: &Lua, core: &SharedCore) -> mlua::Result<()> {
let pmacs: Table = lua.globals().get("pmacs")?;
let editor = lua.create_table()?;
{
let cc = core.clone();
let registry = core.borrow().registry.clone();
editor.set(
"active_modes",
lua.create_function(move |lua, ()| {
let active_buffer = cc.borrow().active_buffer_id();
let mode = {
let r = registry.borrow();
resolve(&r, active_buffer)?.major_mode().map(str::to_owned)
};
let modes = lua.create_table()?;
if let Some(mode) = mode {
modes.set(1, mode)?;
}
Ok(modes)
})?,
)?;
}
install_motion(&editor, lua, core)?;
install_editing(&editor, lua, core)?;
install_history(&editor, lua, core)?;
@ -12796,6 +12864,112 @@ mod tests {
(lua, reg, cmds, kms, hks)
}
fn attach_test_editor(lua: &Lua, registry: &SharedRegistry) -> SharedCore {
let core = Rc::new(RefCell::new(EditorCore::new(registry.clone())));
install_editor(lua, &core).expect("install editor");
core
}
#[test]
fn buffer_major_mode_is_strict_and_rejects_stale_ids() {
let (lua, _reg, _cmds, _kms, _hks) = fresh();
let (wrong_mode, wrong_id, stale_get, stale_set): (String, String, String, String) = lua
.load(
r#"
local id = pmacs.buffer.create("mode-test")
assert(pmacs.buffer.major_mode(id) == nil)
pmacs.buffer.set_major_mode(id, "rust")
assert(pmacs.buffer.major_mode(id) == "rust")
pmacs.buffer.set_major_mode(id, nil)
assert(pmacs.buffer.major_mode(id) == nil)
local ok_type, err_type =
pcall(pmacs.buffer.set_major_mode, id, 42)
assert(not ok_type)
local ok_id, err_id = pcall(pmacs.buffer.major_mode, 1)
assert(not ok_id)
pmacs.buffer.remove(id)
local ok_get, err_get = pcall(pmacs.buffer.major_mode, id)
local ok_set, err_set =
pcall(pmacs.buffer.set_major_mode, id, "rust")
assert(not ok_get and not ok_set)
return tostring(err_type), tostring(err_id),
tostring(err_get), tostring(err_set)
"#,
)
.eval()
.unwrap();
assert!(wrong_mode.contains("string"), "{wrong_mode}");
assert!(wrong_id.contains("buffer handle"), "{wrong_id}");
assert!(stale_get.contains("stale buffer handle"), "{stale_get}");
assert!(stale_set.contains("stale buffer handle"), "{stale_set}");
}
#[test]
fn editor_active_modes_tracks_the_active_buffers_major_mode() {
let (lua, reg, _cmds, _kms, _hks) = fresh();
let core = attach_test_editor(&lua, &reg);
let active = core.borrow().active_buffer_id();
lua.globals()
.set("active_buffer", BufferIdLua(active))
.unwrap();
lua.load(
r#"
local modes = pmacs.editor.active_modes()
assert(type(modes) == "table" and #modes == 0)
pmacs.buffer.set_major_mode(active_buffer, "rust")
modes = pmacs.editor.active_modes()
assert(#modes == 1 and modes[1] == "rust")
pmacs.buffer.set_major_mode(active_buffer, nil)
assert(#pmacs.editor.active_modes() == 0)
"#,
)
.exec()
.unwrap();
}
#[test]
fn describe_key_uses_the_active_buffers_major_mode() {
let (lua, reg, _cmds, kms, _hks) = fresh();
let core = attach_test_editor(&lua, &reg);
let active = core.borrow().active_buffer_id();
reg.borrow_mut()
.get_mut(active)
.unwrap()
.set_major_mode(Some("rust".to_owned()));
{
let mut keymaps = kms.borrow_mut();
keymaps
.bind_global(
&parse_sequence("C-s").unwrap(),
"global.save",
SourceLocation::default(),
)
.unwrap();
keymaps
.bind_mode(
"rust",
&parse_sequence("C-s").unwrap(),
"rust.save",
SourceLocation::default(),
)
.unwrap();
}
let (command, scope): (String, String) = lua
.load(
r#"
local info = pmacs.describe.key("C-s")
return info.command, info.scope
"#,
)
.eval()
.unwrap();
assert_eq!(command, "rust.save");
assert_eq!(scope, "mode:rust");
}
/// A theme handle with one syntax entry and nonzero counters, for
/// pinning that failed commits change nothing and successful ones
/// bump from the PRIOR values (themes arc Q#TH6).

View File

@ -0,0 +1,500 @@
//! Mode-system wiring acceptance over the real daemon process.
//!
//! One ordered scenario keeps the dispatch assertions observable: every
//! checkpoint is itself reached through a wire key event, and the final
//! statusline marker is published only after all Lua-side assertions pass.
//! Statusline assertions consume the daemon's real grid-render payloads.
use std::os::unix::net::UnixStream;
use std::path::Path;
use std::time::{Duration, Instant};
use pmacs::cell::{Cell, CellSize, Glyph};
use pmacs::protocol::{
AttachRequest, FrontendEvent, FrontendId, Hello, InstanceMessage, Key, KeyEvent, Modifiers,
PROTOCOL_VERSION,
};
use pmacs::transport::{read_message, write_message};
mod common;
use common::daemon::{TestDaemon, build_default_caps};
struct Client {
stream: UnixStream,
frontend_id: FrontendId,
}
const ROWS: u32 = 30;
const COLS: u32 = 160;
struct Grid {
cells: Vec<Cell>,
}
impl Grid {
fn new() -> Self {
Self {
cells: vec![Cell::default(); (ROWS * COLS) as usize],
}
}
fn apply(&mut self, spans: Vec<pmacs::cell::DiffSpan>) {
for span in spans {
let start = (span.start.row * COLS + span.start.col) as usize;
for (offset, cell) in span.cells.into_iter().enumerate() {
self.cells[start + offset] = cell;
}
}
}
fn text(&self) -> String {
let mut text = String::with_capacity((ROWS * (COLS + 1)) as usize);
for row in 0..ROWS {
for column in 0..COLS {
let cell = &self.cells[(row * COLS + column) as usize];
let ch = match &cell.glyph {
Glyph::Char(ch) => *ch,
Glyph::Cluster(bytes) => std::str::from_utf8(bytes)
.ok()
.and_then(|value| value.chars().next())
.unwrap_or(' '),
Glyph::Continuation => ' ',
};
text.push(ch);
}
text.push('\n');
}
text
}
}
fn attach(daemon: &TestDaemon) -> (Client, Grid) {
let mut stream = daemon.connect();
stream
.set_read_timeout(Some(Duration::from_millis(100)))
.expect("set daemon read timeout");
let hello: Hello = read_message(&mut stream).expect("read daemon Hello");
assert_eq!(hello.protocol_version, PROTOCOL_VERSION);
write_message(
&mut stream,
&AttachRequest {
protocol_version: PROTOCOL_VERSION,
frontend_capabilities: build_default_caps(),
initial_size: CellSize::new(ROWS, COLS),
},
)
.expect("attach grid frontend");
let deadline = Instant::now() + Duration::from_secs(5);
let mut grid = Grid::new();
loop {
assert!(
Instant::now() < deadline,
"initial full-grid frame timed out"
);
if let Ok(InstanceMessage::CellDelta {
spans,
full_grid: true,
}) = read_message::<InstanceMessage>(&mut stream)
{
grid.apply(spans);
break;
}
}
(
Client {
stream,
frontend_id: hello.assigned_frontend_id,
},
grid,
)
}
fn send_key(client: &mut Client, key: Key, mods: Modifiers) {
write_message(
&mut client.stream,
&FrontendEvent::Key(KeyEvent {
frontend_id: client.frontend_id,
key,
mods,
timestamp_ns: 0,
}),
)
.expect("send daemon key event");
}
fn send_ctrl_chord(client: &mut Client, second: char) {
send_key(client, Key::Char('c'), Modifiers::CTRL);
send_key(client, Key::Char(second), Modifiers::CTRL);
}
fn checkpoint(client: &mut Client, n: u8) {
send_key(client, Key::F(n), Modifiers::NONE);
}
fn pump_grid_until(
client: &mut Client,
grid: &mut Grid,
what: &str,
predicate: impl Fn(&str) -> bool,
) -> String {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let text = grid.text();
assert!(
!text.contains("MSW_ERROR:"),
"daemon-side Lua checkpoint failed:\n{text}"
);
if predicate(&text) {
return text;
}
assert!(
Instant::now() < deadline,
"grid update timed out waiting for {what}; current grid:\n{text}"
);
if let Ok(InstanceMessage::CellDelta { spans, .. }) =
read_message::<InstanceMessage>(&mut client.stream)
{
grid.apply(spans);
}
}
}
fn lua_string(path: &Path) -> String {
format!("{:?}", path.to_string_lossy())
}
#[allow(
clippy::too_many_lines,
reason = "one ordered daemon session preserves dispatch state across all ten acceptance checks"
)]
#[test]
fn mode_system_wiring_is_observable_end_to_end() {
let fixtures = tempfile::tempdir().expect("fixture tempdir");
let rust = fixtures.path().join("dispatch.rs");
let python = fixtures.path().join("dispatch.py");
let unknown = fixtures.path().join("dispatch.txt");
let server = fixtures.path().join("dispatch.msw");
std::fs::write(&rust, "// MSW_RUST_FIXTURE\nfn main() {}\n").unwrap();
std::fs::write(&python, "# MSW_PYTHON_FIXTURE\nprint('ok')\n").unwrap();
std::fs::write(&unknown, "MSW_UNKNOWN_FIXTURE\n").unwrap();
std::fs::write(&server, "MSW_SERVER_ONLY_FIXTURE\n").unwrap();
let init_template = r#"
-- Ordinary fixtures must never inherit the built-in real-server registry.
pmacs.lsp.config = {}
pmacs.lsp.filetypes.msw = "serveronly"
local RUST_PATH = __RUST_PATH__
local PYTHON_PATH = __PYTHON_PATH__
local UNKNOWN_PATH = __UNKNOWN_PATH__
local SERVER_PATH = __SERVER_PATH__
local S = { rust_hits = 0 }
_G.MSW_STATE = S
_G.MSW_RESULT = false
_G.MSW_ERROR = nil
local function eq(actual, expected, label)
assert(actual == expected,
label .. ": expected " .. tostring(expected) .. ", got " .. tostring(actual))
end
local function current_modes(expected)
local modes = pmacs.editor.active_modes()
if expected == nil then
eq(#modes, 0, "active mode count")
else
eq(#modes, 1, "active mode count")
eq(modes[1], expected, "active mode")
end
end
local function command(name, body)
pmacs.command.define {
name = name,
description = "mode-system wiring acceptance command " .. name,
fn = function()
local ok, err = pcall(body)
if not ok then
_G.MSW_ERROR = ("MSW_ERROR:" .. tostring(err)):sub(1, 200)
end
end,
}
end
local function global_key(sequence, name)
pmacs.keymap.bind { scope = "global", sequence = sequence, command = name }
end
command("test.rust-only", function()
S.rust_hits = S.rust_hits + 1
end)
command("test.priority-buffer", function() S.priority_hit = "buffer" end)
command("test.priority-mode", function() S.priority_hit = "mode" end)
command("test.priority-global", function() S.priority_hit = "global" end)
command("test.parity-mode", function() S.parity_hit = "mode" end)
command("test.parity-global", function() S.parity_hit = "global" end)
pmacs.keymap.bind {
scope = "mode", mode = "rust", sequence = "C-c C-c", command = "test.rust-only",
}
pmacs.keymap.bind {
scope = "mode", mode = "rust", sequence = "C-c C-p", command = "test.priority-mode",
}
pmacs.keymap.bind {
scope = "global", sequence = "C-c C-p", command = "test.priority-global",
}
pmacs.keymap.bind {
scope = "mode", mode = "rust", sequence = "C-c C-d", command = "test.parity-mode",
}
pmacs.keymap.bind {
scope = "global", sequence = "C-c C-d", command = "test.parity-global",
}
pmacs.statusline.register {
name = "mode-system-result",
side = "right",
priority = 999,
face = "ui.modeline",
fn = function()
if _G.MSW_ERROR then return _G.MSW_ERROR end
if _G.MSW_RESULT then return "MODE-SYSTEM-WIRING-PASS" end
return nil
end,
}
command("test.step-1", function()
local provider
for _, candidate in ipairs(pmacs.statusline.providers()) do
if candidate.name == "mode" then provider = candidate end
end
assert(provider ~= nil, "built-in mode provider is registered")
eq(provider.side, "left", "mode provider side")
eq(provider.priority, 0, "mode provider priority")
eq(provider.face, "ui.modeline", "mode provider face")
eq(provider.enabled, true, "mode provider enabled")
S.rust = pmacs.buffer.find_or_open(RUST_PATH)
eq(pmacs.buffer.major_mode(S.rust), "rust", "rust initialization")
current_modes("rust")
S.python = pmacs.buffer.find_or_open(PYTHON_PATH)
eq(pmacs.buffer.major_mode(S.python), "python", "python initialization")
current_modes("python")
S.unknown = pmacs.buffer.find_or_open(UNKNOWN_PATH)
eq(pmacs.buffer.major_mode(S.unknown), nil, "unknown initialization")
current_modes(nil)
S.server = pmacs.buffer.find_or_open(SERVER_PATH)
eq(pmacs.buffer.major_mode(S.server), "serveronly", "server-only initialization")
current_modes("serveronly")
eq(pmacs.parse._has_view(S.server), false, "server-only parse view")
pmacs.keymap.bind {
scope = "buffer", buffer = S.rust,
sequence = "C-c C-p", command = "test.priority-buffer",
}
-- Leave a Rust-active/Python-passive split for real statusline evaluation.
pmacs.window.switch_buffer(S.rust)
pmacs.window.split_vertical()
pmacs.window.focus_next()
pmacs.window.switch_buffer(S.python)
pmacs.window.focus_next()
eq(tostring(pmacs.window.buffer()), tostring(S.rust), "focused split buffer")
eq(pmacs.buffer.major_mode(S.rust), "rust", "rust survived switching")
eq(pmacs.buffer.major_mode(S.python), "python", "python survived switching")
end)
command("test.step-2", function()
eq(S.rust_hits, 1, "Rust mode dispatch")
pmacs.window.switch_buffer(S.python)
current_modes("python")
end)
command("test.step-3", function()
eq(S.rust_hits, 1, "Python must not dispatch Rust binding")
pmacs.window.switch_buffer(S.unknown)
eq(pmacs.buffer.major_mode(S.unknown), nil, "unknown stays mode-less")
current_modes(nil)
end)
command("test.step-4", function()
eq(S.rust_hits, 1, "mode-less buffer must not dispatch Rust binding")
pmacs.window.switch_buffer(S.rust)
S.priority_hit = nil
end)
command("test.step-5", function()
eq(S.priority_hit, "buffer", "buffer scope precedence")
pmacs.keymap.unbind {
scope = "buffer", buffer = S.rust, sequence = "C-c C-p",
}
S.priority_hit = nil
end)
command("test.step-6", function()
eq(S.priority_hit, "mode", "mode scope precedence")
pmacs.window.switch_buffer(S.python)
S.priority_hit = nil
end)
command("test.step-7", function()
eq(S.priority_hit, "global", "global scope fallback")
pmacs.window.switch_buffer(S.rust)
S.parity_hit = nil
end)
command("test.step-8", function()
eq(S.parity_hit, "mode", "parity binding dispatch")
local described = pmacs.describe.key("C-c C-d")
assert(described ~= nil, "describe.key returns the mode binding")
eq(described.command, "test.parity-mode", "describe.key command")
eq(described.scope, "mode:rust", "describe.key scope")
local help_id = pmacs.help.show_key("C-c C-d")
assert(help_id ~= nil, "help.show_key returns a help buffer")
local body = help_id:slice(0, help_id:len())
assert(body:find("Runs: %[command: test%.parity%-mode%]"), body)
assert(body:find("Scope: mode:rust", 1, true), body)
help_id = pmacs.help.show_command("test.parity-mode")
body = help_id:slice(0, help_id:len())
local link_start = body:find("%[key: C%-c C%-d @mode:rust%]")
assert(link_start ~= nil, "mode command help link carries @mode:rust: " .. body)
pmacs.window.switch_buffer(help_id)
local followed = pmacs.help.follow_link(link_start + 6)
assert(followed ~= nil, "mode key link follows while *help* is active")
local followed_body = followed:slice(0, followed:len())
assert(followed_body:find("Runs: %[command: test%.parity%-mode%]"), followed_body)
assert(followed_body:find("Scope: mode:rust", 1, true), followed_body)
pmacs.window.switch_buffer(S.rust)
pmacs.buffer.set_major_mode(S.rust, "markdown")
pmacs.window.switch_buffer(S.python)
pmacs.window.switch_buffer(S.rust)
eq(pmacs.buffer.major_mode(S.rust), "markdown", "explicit override survives switches")
current_modes("markdown")
end)
command("test.step-9", function()
eq(pmacs.buffer.major_mode(S.rust), "markdown", "override remains live")
pmacs.buffer.set_major_mode(S.rust, nil)
pmacs.window.switch_buffer(S.python)
pmacs.window.switch_buffer(S.rust)
eq(pmacs.buffer.major_mode(S.rust), nil, "explicit clear survives switches")
current_modes(nil)
S.clear_baseline = S.rust_hits
end)
command("test.step-10", function()
eq(S.rust_hits, S.clear_baseline, "cleared Rust mode must not dispatch")
pmacs.window.switch_buffer(S.server)
eq(pmacs.buffer.major_mode(S.server), "serveronly", "server-only mode survives")
current_modes("serveronly")
eq(pmacs.parse._has_view(S.server), false, "server-only language stays parser-free")
pmacs.window.switch_buffer(S.unknown)
eq(pmacs.buffer.major_mode(S.unknown), nil, "unknown mode remains nil")
current_modes(nil)
_G.MSW_RESULT = true
end)
for n = 1, 10 do
global_key("<f" .. n .. ">", "test.step-" .. n)
end
"#;
let init = init_template
.replace("__RUST_PATH__", &lua_string(&rust))
.replace("__PYTHON_PATH__", &lua_string(&python))
.replace("__UNKNOWN_PATH__", &lua_string(&unknown))
.replace("__SERVER_PATH__", &lua_string(&server));
let daemon = TestDaemon::spawn_with_config(&init);
let (mut client, mut grid) = attach(&daemon);
// Initialize through real after-load hooks and leave Rust focused with
// Python in the passive split. The daemon's ordinary grid painter must
// render each window from its own buffer context.
checkpoint(&mut client, 1);
let split_text = pump_grid_until(
&mut client,
&mut grid,
"Rust-active/Python-passive mode lines",
|text| {
text.contains("dispatch.rs")
&& text.contains("dispatch.py")
&& text.contains("(rust)")
&& text.contains("(python)")
},
);
let split_modeline = split_text
.lines()
.find(|line| line.contains("dispatch.rs") && line.contains("dispatch.py"))
.expect("both split mode lines occupy the split's modeline row");
assert!(split_modeline.contains("(rust)"), "{split_modeline}");
assert!(split_modeline.contains("(python)"), "{split_modeline}");
// 1-2: the exact mode binding fires in Rust, not Python or no-mode text.
send_ctrl_chord(&mut client, 'c');
checkpoint(&mut client, 2);
send_ctrl_chord(&mut client, 'c');
checkpoint(&mut client, 3);
send_ctrl_chord(&mut client, 'c');
// The active unknown-language pane omits its mode segment while the
// passive Python pane retains its own. This distinguishes empty output
// from a provider accidentally reading the focused/other buffer.
let unknown_text = pump_grid_until(&mut client, &mut grid, "mode-less active buffer", |text| {
text.contains("dispatch.txt") && text.contains("dispatch.py")
});
let unknown_modeline = unknown_text
.lines()
.find(|line| line.contains("dispatch.txt") && line.contains("dispatch.py"))
.expect("unknown and passive Python mode lines share a row");
let passive_start = unknown_modeline
.find("dispatch.py")
.expect("passive Python buffer name");
assert!(
!unknown_modeline[..passive_start].contains('('),
"unknown-language mode line must omit a mode segment: {unknown_modeline}"
);
assert!(
unknown_modeline[passive_start..].contains("(python)"),
"passive Python mode line keeps its own mode: {unknown_modeline}"
);
// 5: buffer-local, then mode, then global, all driven through dispatch.
checkpoint(&mut client, 4);
send_ctrl_chord(&mut client, 'p');
checkpoint(&mut client, 5);
send_ctrl_chord(&mut client, 'p');
checkpoint(&mut client, 6);
send_ctrl_chord(&mut client, 'p');
// 10: first prove the mode binding dispatched, then compare describe,
// show-key, and followed @mode link rendering against that result.
checkpoint(&mut client, 7);
send_ctrl_chord(&mut client, 'd');
checkpoint(&mut client, 8);
// 7-8: override and clear each survive switch-away/back; the cleared mode
// no longer dispatches the detected-language binding.
checkpoint(&mut client, 9);
send_ctrl_chord(&mut client, 'c');
checkpoint(&mut client, 10);
// The marker is painted only if every Lua assertion, including
// active_modes and server-only parse gating, completed successfully.
pump_grid_until(
&mut client,
&mut grid,
"mode-system success marker",
|text| text.contains("MODE-SYSTEM-WIRING-PASS"),
);
}

View File

@ -124,8 +124,9 @@ fn a01_04_registry_contract_limits_epochs_and_results() {
assert!(baseline_mode.ends_with(" L1:C1 All "));
let initial = state.statusline_registry.borrow().providers();
assert_eq!(initial.len(), 1, "builtin lsp provider is discoverable");
assert_eq!(initial[0].name, "lsp");
assert_eq!(initial.len(), 2, "builtin providers are discoverable");
assert!(initial.iter().any(|provider| provider.name == "mode"));
assert!(initial.iter().any(|provider| provider.name == "lsp"));
let before_epochs = {
let registry = state.statusline_registry.borrow();
(registry.layout_epoch(), registry.face_set_epoch())