fix(window): wire the side-window split guard and scope the divider drag
PR #155 review round 1. Finding 1 (must fix): `try_split_active` had no production caller — `pmacs.window.split_horizontal` / `split_vertical`, and therefore `C-x 2` / `C-x 3`, still went through plain `split_active`. Splitting a focused panel made the root wrapper's final child a split rather than `Leaf(side)`, which both `Layout::compute`'s fixed pass and `document_subtree` key on: the panel band reverted to 1:1 weight division and an ordinary window ended up living inside it. Both bindings now route through the guard, and acc26 asserts through the real Lua path — a direct core call passes with the guard unwired, which is how it survived the first round. Finding 2: the armed-drag early return now checks the arming frontend, so one frontend's in-flight gesture cannot cancel or swallow another's mouse events. New acc30c. Finding 3: `paint_mode_line_graphemes`'s doc block was left heading `paint_divider_segment`; moved back. Finding 4: a recompile carries no `display`, so it took the raw switch and duplicated a panel-placed `*compilation*` into the document window. `start_run` now detects that the buffer already owns the panel slot. `pmacs.window.buffer` gained an optional window argument so an adopter can ask without selecting the panel first. New acc19b. Stage-2 hazard pins the review asked for, both in `src/daemon.rs`: a fresh attach while LOCAL is focused in a panel inherits LOCAL's document buffer, and an initial-target bootstrap whose `after-load` hook creates and selects a panel still reasserts into a document window. Minor: dropped listview's dead `p.side`; documented `focus_window`'s caller-validates contract; `jump_back` restores through `focus_window` so the "every focus change" contract holds; `params` / `resize` default to the acting frontend's selected window rather than the ambient one; widened the flexible-division math to u64 intermediates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2a9c11461c
commit
90fc7a913e
|
|
@ -732,6 +732,17 @@ end)
|
||||||
|
|
||||||
-- Start a run in `slot`. Shared by compile and shell-command; grep
|
-- Start a run in `slot`. Shared by compile and shell-command; grep
|
||||||
-- has its own worker path.
|
-- has its own worker path.
|
||||||
|
-- Whether `buf` is currently the acting frontend's side-window buffer
|
||||||
|
-- (bottom-panel arc). Used so a recompile re-displays into the panel it
|
||||||
|
-- is already in rather than duplicating itself into the document window.
|
||||||
|
local function already_in_panel(buf)
|
||||||
|
if not buf then return false end
|
||||||
|
local panel = pmacs.window.panel()
|
||||||
|
if not panel then return false end
|
||||||
|
local ok, shown = pcall(pmacs.window.buffer, panel)
|
||||||
|
return ok and shown == buf
|
||||||
|
end
|
||||||
|
|
||||||
local function start_run(slot, cmdline, opts)
|
local function start_run(slot, cmdline, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
-- Bottom-panel arc (Q#BP11b): validate placement BEFORE the run
|
-- Bottom-panel arc (Q#BP11b): validate placement BEFORE the run
|
||||||
|
|
@ -818,9 +829,14 @@ local function start_run(slot, cmdline, opts)
|
||||||
-- The FIRST display of this run is the side-affine one (Q#BP3): a
|
-- The FIRST display of this run is the side-affine one (Q#BP3): a
|
||||||
-- persistent *compilation* already visible in a document window must
|
-- persistent *compilation* already visible in a document window must
|
||||||
-- not preempt the requested panel. Compile output is passive, so it
|
-- not preempt the requested panel. Compile output is passive, so it
|
||||||
-- takes `select = false` explicitly; a recompile simply reuses the
|
-- takes `select = false` explicitly.
|
||||||
-- panel it is already in.
|
--
|
||||||
if display == "panel" then
|
-- A recompile reaches here with NO `display` (only cmdline/cwd are
|
||||||
|
-- stored in `_last`), so the raw switch below would put this buffer in
|
||||||
|
-- the selected DOCUMENT window while the panel still shows it — the
|
||||||
|
-- duplicate presentation this arc removes elsewhere. Detect that the
|
||||||
|
-- buffer already owns the panel slot and keep it there.
|
||||||
|
if display == "panel" or already_in_panel(slot.buf) then
|
||||||
pmacs.window.display(slot.buf, { side = "bottom", select = false })
|
pmacs.window.display(slot.buf, { side = "bottom", select = false })
|
||||||
else
|
else
|
||||||
pmacs.window.switch_buffer(slot.buf)
|
pmacs.window.switch_buffer(slot.buf)
|
||||||
|
|
|
||||||
|
|
@ -136,10 +136,8 @@ function pmacs.listview.open(spec)
|
||||||
tostring(display)))
|
tostring(display)))
|
||||||
end
|
end
|
||||||
if display == "panel" then
|
if display == "panel" then
|
||||||
p.side = true
|
|
||||||
pmacs.window.display(p.buffer, { side = "bottom", select = true })
|
pmacs.window.display(p.buffer, { side = "bottom", select = true })
|
||||||
else
|
else
|
||||||
p.side = false
|
|
||||||
pmacs.window.switch_buffer(p.buffer)
|
pmacs.window.switch_buffer(p.buffer)
|
||||||
end
|
end
|
||||||
seat_cursor(p, 1)
|
seat_cursor(p, 1)
|
||||||
|
|
|
||||||
129
src/daemon.rs
129
src/daemon.rs
|
|
@ -4190,4 +4190,133 @@ mod tests {
|
||||||
"key must self-insert into the displayed buffer, not the attach-time scratch"
|
"key must self-insert into the displayed buffer, not the attach-time scratch"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bottom-panel arc, §1.3 #22 (framing acceptance 51's Stage-1 half).
|
||||||
|
///
|
||||||
|
/// A fresh no-target attach clones `LOCAL`'s **primary document**
|
||||||
|
/// buffer, not `local_view.active`. Stage 1 makes a TUI panel a real
|
||||||
|
/// focus target, so `LOCAL` can legitimately own focus in a panel at
|
||||||
|
/// attach time — and panel content must never become a newly attached
|
||||||
|
/// frontend's full-window document.
|
||||||
|
#[test]
|
||||||
|
fn fresh_attach_inherits_locals_document_buffer_not_its_focused_panel() {
|
||||||
|
let mut editor = EditorState::new();
|
||||||
|
let document_buffer = editor.core.borrow().active_buffer_id();
|
||||||
|
let panel_buffer = editor.core.borrow().registry.borrow_mut().create("*panel*");
|
||||||
|
// Open a bottom panel on LOCAL and focus it.
|
||||||
|
let panel = {
|
||||||
|
let mut core = editor.core.borrow_mut();
|
||||||
|
let mut request = crate::editor_core::DisplayRequest::new(panel_buffer);
|
||||||
|
request.side = Some(crate::window::Side::Bottom);
|
||||||
|
request.height = Some(5);
|
||||||
|
request.select = Some(true);
|
||||||
|
let outcome = core
|
||||||
|
.display_buffer(FrontendId::LOCAL, &request)
|
||||||
|
.expect("panel placement");
|
||||||
|
core.focus_window(FrontendId::LOCAL, outcome.target);
|
||||||
|
outcome.target
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
editor.core.borrow().views[&FrontendId::LOCAL].active,
|
||||||
|
panel,
|
||||||
|
"LOCAL really is focused in the panel"
|
||||||
|
);
|
||||||
|
|
||||||
|
let fid = FrontendId(123);
|
||||||
|
let view = build_fresh_frontend_view(&mut editor, false, false);
|
||||||
|
editor.core.borrow_mut().register_frontend_view(fid, view);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
editor
|
||||||
|
.core
|
||||||
|
.borrow()
|
||||||
|
.active_window_for(fid)
|
||||||
|
.expect("fresh view window")
|
||||||
|
.buffer_id,
|
||||||
|
document_buffer,
|
||||||
|
"the new frontend inherited LOCAL's DOCUMENT buffer; inheriting \
|
||||||
|
`local_view.active` would have made the panel its document"
|
||||||
|
);
|
||||||
|
assert_ne!(document_buffer, panel_buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bottom-panel arc, Q#BP11b / R4-B4 (framing acceptance 55's
|
||||||
|
/// Stage-1 half).
|
||||||
|
///
|
||||||
|
/// Stage 1 lets a startup hook create and select a side window. The
|
||||||
|
/// initial-target bootstrap must still reassert the requested buffer
|
||||||
|
/// in — and activate — a **non-side** document window, rather than
|
||||||
|
/// overwriting the panel merely because it became `view.active`.
|
||||||
|
#[test]
|
||||||
|
fn initial_target_reasserts_a_document_window_when_a_hook_selects_a_panel() {
|
||||||
|
use std::os::unix::ffi::OsStrExt as _;
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let target = dir.path().join("target.txt");
|
||||||
|
std::fs::write(&target, b"target contents\n").expect("write target");
|
||||||
|
|
||||||
|
let mut editor = EditorState::new();
|
||||||
|
editor
|
||||||
|
.lua_host
|
||||||
|
.lua()
|
||||||
|
.load(
|
||||||
|
r#"
|
||||||
|
pmacs.lsp.config = {}
|
||||||
|
pmacs.hook.add("buffer.after-load", function()
|
||||||
|
if HOOK_RAN then return end
|
||||||
|
HOOK_RAN = true
|
||||||
|
HOOK_PANEL = pmacs.window.display(
|
||||||
|
pmacs.buffer.create("*hook-panel*"),
|
||||||
|
{ side = "bottom", height = 4, select = true })
|
||||||
|
end)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.expect("install hook");
|
||||||
|
|
||||||
|
// A GRID session (panel-capable), which is the realistic shape
|
||||||
|
// for a hook-created panel in Stage 1 — and real geometry, so
|
||||||
|
// the panel is genuinely VISIBLE and focused when the reassert
|
||||||
|
// runs. Without the declaration, reconciliation would hide the
|
||||||
|
// panel and move focus out on its own, and the assertions below
|
||||||
|
// would pass without exercising the reassert at all.
|
||||||
|
let fid = FrontendId(124);
|
||||||
|
let view = build_fresh_frontend_view(&mut editor, true, true);
|
||||||
|
editor.core.borrow_mut().register_frontend_view(fid, view);
|
||||||
|
editor.sync_frame_geometry(fid, CellSize::new(24, 80));
|
||||||
|
|
||||||
|
let opened = open_initial_target(
|
||||||
|
&mut editor,
|
||||||
|
fid,
|
||||||
|
InitialTarget {
|
||||||
|
path: target.as_os_str().as_bytes().to_vec(),
|
||||||
|
cwd: dir.path().as_os_str().as_bytes().to_vec(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("bootstrap succeeds despite the panel-creating hook");
|
||||||
|
|
||||||
|
let core = editor.core.borrow();
|
||||||
|
assert!(
|
||||||
|
!core.views[&fid].panel_hidden,
|
||||||
|
"the hook's panel is visible, so focus really was on it when \
|
||||||
|
the reassert ran"
|
||||||
|
);
|
||||||
|
let active = core.views[&fid].active;
|
||||||
|
let active_window = core.windows.get(&active).expect("active window live");
|
||||||
|
assert!(
|
||||||
|
!active_window.is_side(),
|
||||||
|
"bootstrap activated a DOCUMENT window, not the hook's panel"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
active_window.buffer_id, opened.buffer_id,
|
||||||
|
"…showing the requested target"
|
||||||
|
);
|
||||||
|
let panel = core
|
||||||
|
.side_window_for(fid)
|
||||||
|
.expect("the hook's panel survived");
|
||||||
|
assert_ne!(
|
||||||
|
core.windows[&panel].buffer_id, opened.buffer_id,
|
||||||
|
"the panel was not overwritten with the target"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1925,7 +1925,15 @@ impl EditorState {
|
||||||
// pointer for the whole gesture, INCLUDING rows outside any
|
// pointer for the whole gesture, INCLUDING rows outside any
|
||||||
// window — otherwise tracking would stop the moment the pointer
|
// window — otherwise tracking would stop the moment the pointer
|
||||||
// crossed the frame's status row.
|
// crossed the frame's status row.
|
||||||
if self.window_drag.is_some() {
|
//
|
||||||
|
// Scoped to the ARMING frontend. The daemon routes every attached
|
||||||
|
// grid frontend through this same dispatcher, so an unscoped
|
||||||
|
// check would let one frontend's in-flight drag cancel and
|
||||||
|
// swallow another frontend's clicks.
|
||||||
|
if self
|
||||||
|
.window_drag
|
||||||
|
.is_some_and(|drag| drag.frontend_id == frontend_id)
|
||||||
|
{
|
||||||
match ev.kind {
|
match ev.kind {
|
||||||
MouseEventKind::Drag(MouseButton::Left) => {
|
MouseEventKind::Drag(MouseButton::Left) => {
|
||||||
self.drag_window_boundary(frontend_id, cell_row, term_size);
|
self.drag_window_boundary(frontend_id, cell_row, term_size);
|
||||||
|
|
@ -3860,9 +3868,6 @@ fn mode_line_grapheme_width(graphemes: &[ModeLineGrapheme]) -> u32 {
|
||||||
graphemes.iter().map(|grapheme| grapheme.width).sum()
|
graphemes.iter().map(|grapheme| grapheme.width).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint complete graphemes at a logical signed origin. A grapheme that
|
|
||||||
/// straddles either clip edge is omitted wholesale, so a wide glyph can never
|
|
||||||
/// leave a dangling half-cell at a window or left/right collision boundary.
|
|
||||||
/// Restyle one exposed segment of a horizontal split boundary and stamp
|
/// Restyle one exposed segment of a horizontal split boundary and stamp
|
||||||
/// its grip (Q#BP5a).
|
/// its grip (Q#BP5a).
|
||||||
///
|
///
|
||||||
|
|
@ -3890,6 +3895,9 @@ fn paint_divider_segment(
|
||||||
cell.glyph = crate::cell::Glyph::Char(DIVIDER_HANDLE_GLYPH);
|
cell.glyph = crate::cell::Glyph::Char(DIVIDER_HANDLE_GLYPH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Paint complete graphemes at a logical signed origin. A grapheme that
|
||||||
|
/// straddles either clip edge is omitted wholesale, so a wide glyph can never
|
||||||
|
/// leave a dangling half-cell at a window or left/right collision boundary.
|
||||||
fn paint_mode_line_graphemes(
|
fn paint_mode_line_graphemes(
|
||||||
grid: &mut crate::cell::CellGrid<'_>,
|
grid: &mut crate::cell::CellGrid<'_>,
|
||||||
rect: &crate::window::Rect,
|
rect: &crate::window::Rect,
|
||||||
|
|
|
||||||
|
|
@ -1073,7 +1073,12 @@ impl EditorCore {
|
||||||
.is_some_and(|window| window.buffer_id == entry.buffer_id)
|
.is_some_and(|window| window.buffer_id == entry.buffer_id)
|
||||||
&& !self.side_window_is_hidden(fid, entry.window_id);
|
&& !self.side_window_is_hidden(fid, entry.window_id);
|
||||||
if origin_valid {
|
if origin_valid {
|
||||||
self.set_active_window_id(entry.window_id);
|
// Through `focus_window`, not `set_active_window_id`:
|
||||||
|
// returning INTO a panel from a document window is a
|
||||||
|
// focus transition like any other, so it refreshes
|
||||||
|
// `origin_document` and a later `window.quit` returns to
|
||||||
|
// the window the jump came from.
|
||||||
|
self.focus_window(fid, entry.window_id);
|
||||||
} else {
|
} else {
|
||||||
// A stale SIDE origin is skipped outright: switching a
|
// A stale SIDE origin is skipped outright: switching a
|
||||||
// panel's buffer into the document window is exactly the
|
// panel's buffer into the document window is exactly the
|
||||||
|
|
@ -2667,6 +2672,12 @@ impl EditorCore {
|
||||||
|
|
||||||
/// Focus an explicit window in the acting frontend, refreshing the
|
/// Focus an explicit window in the acting frontend, refreshing the
|
||||||
/// panel's remembered document origin on the way (Q#BP2c).
|
/// panel's remembered document origin on the way (Q#BP2c).
|
||||||
|
///
|
||||||
|
/// **The caller must have validated `target`** — that it is live and
|
||||||
|
/// belongs to `fid`'s layout. Every Lua path does so through
|
||||||
|
/// `lookup_window` or the display transaction's own revalidation;
|
||||||
|
/// this function only `debug_assert!`s it, so a release-mode caller
|
||||||
|
/// passing a foreign or dead id would leave `view.active` dangling.
|
||||||
pub fn focus_window(&mut self, fid: FrontendId, target: WindowId) {
|
pub fn focus_window(&mut self, fid: FrontendId, target: WindowId) {
|
||||||
let Some(view) = self.views.get_mut(&fid) else {
|
let Some(view) = self.views.get_mut(&fid) else {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -12218,14 +12218,20 @@ fn install_window_module(lua: &Lua, core: &SharedCore) -> mlua::Result<Table> {
|
||||||
window_panel::install(lua, core, &win)?;
|
window_panel::install(lua, core, &win)?;
|
||||||
|
|
||||||
{
|
{
|
||||||
|
// Bottom-panel arc (Q#BP6): `try_split_active` refuses a side
|
||||||
|
// window. This binding is what `C-x 2` reaches, so the refusal
|
||||||
|
// has to live on THIS path — splitting the panel leaf would make
|
||||||
|
// the root wrapper's final child a split rather than
|
||||||
|
// `Leaf(side)`, and both `Layout::compute`'s fixed pass and
|
||||||
|
// `document_subtree` key on exactly that shape.
|
||||||
let cc = core.clone();
|
let cc = core.clone();
|
||||||
win.set(
|
win.set(
|
||||||
"split_horizontal",
|
"split_horizontal",
|
||||||
lua.create_function(move |_, ()| {
|
lua.create_function(move |_, ()| {
|
||||||
let new_id = cc
|
cc.borrow_mut()
|
||||||
.borrow_mut()
|
.try_split_active(crate::window::Orientation::Horizontal, true)
|
||||||
.split_active(crate::window::Orientation::Horizontal, true);
|
.map(crate::window::WindowId::raw)
|
||||||
Ok(new_id.raw())
|
.map_err(mlua::Error::runtime)
|
||||||
})?,
|
})?,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
@ -12235,10 +12241,10 @@ fn install_window_module(lua: &Lua, core: &SharedCore) -> mlua::Result<Table> {
|
||||||
win.set(
|
win.set(
|
||||||
"split_vertical",
|
"split_vertical",
|
||||||
lua.create_function(move |_, ()| {
|
lua.create_function(move |_, ()| {
|
||||||
let new_id = cc
|
cc.borrow_mut()
|
||||||
.borrow_mut()
|
.try_split_active(crate::window::Orientation::Vertical, true)
|
||||||
.split_active(crate::window::Orientation::Vertical, true);
|
.map(crate::window::WindowId::raw)
|
||||||
Ok(new_id.raw())
|
.map_err(mlua::Error::runtime)
|
||||||
})?,
|
})?,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
@ -12351,9 +12357,38 @@ fn install_window_module(lua: &Lua, core: &SharedCore) -> mlua::Result<Table> {
|
||||||
|
|
||||||
{
|
{
|
||||||
let cc = core.clone();
|
let cc = core.clone();
|
||||||
|
// With no argument: the selected window's buffer (unchanged).
|
||||||
|
// With an explicit window id: that window's buffer, validated
|
||||||
|
// against the acting frontend's layout like every other
|
||||||
|
// `WindowId`-taking operation (bottom-panel arc, Q#BP11) — an
|
||||||
|
// adopter has to be able to ask "is my buffer the one in the
|
||||||
|
// panel" without first selecting the panel.
|
||||||
win.set(
|
win.set(
|
||||||
"buffer",
|
"buffer",
|
||||||
lua.create_function(move |_, ()| Ok(BufferIdLua(cc.borrow().active_buffer_id())))?,
|
lua.create_function(move |lua, target: Option<u64>| -> mlua::Result<BufferIdLua> {
|
||||||
|
let Some(raw) = target else {
|
||||||
|
return Ok(BufferIdLua(cc.borrow().active_buffer_id()));
|
||||||
|
};
|
||||||
|
let fid = window_panel::acting_frontend(lua, &cc);
|
||||||
|
let core = cc.borrow();
|
||||||
|
let view = core.views.get(&fid).ok_or_else(|| {
|
||||||
|
mlua::Error::runtime("pmacs.window.buffer: acting frontend has no layout")
|
||||||
|
})?;
|
||||||
|
let id = view
|
||||||
|
.layout
|
||||||
|
.iter_ids()
|
||||||
|
.into_iter()
|
||||||
|
.find(|id| id.raw() == raw)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
mlua::Error::runtime(format!(
|
||||||
|
"pmacs.window.buffer: window {raw} is not live in this frontend's layout"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
core.windows
|
||||||
|
.get(&id)
|
||||||
|
.map(|window| BufferIdLua(window.buffer_id))
|
||||||
|
.ok_or_else(|| mlua::Error::runtime("pmacs.window.buffer: window not live"))
|
||||||
|
})?,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,19 @@ fn parse_request(
|
||||||
Ok(request)
|
Ok(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The ACTING frontend's selected window.
|
||||||
|
///
|
||||||
|
/// Not `active_window_id()`, which resolves through the ambient active
|
||||||
|
/// frontend: every other id in this module is `fid`-scoped, and the two
|
||||||
|
/// only coincide because dispatch happens to set `active_frontend` first.
|
||||||
|
fn selected_window(core: &SharedCore, fid: FrontendId) -> mlua::Result<WindowId> {
|
||||||
|
core.borrow()
|
||||||
|
.views
|
||||||
|
.get(&fid)
|
||||||
|
.map(|view| view.active)
|
||||||
|
.ok_or_else(|| mlua::Error::runtime("pmacs.window: acting frontend has no layout"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a raw Lua window id, refusing one that is not live in the
|
/// Resolve a raw Lua window id, refusing one that is not live in the
|
||||||
/// acting frontend's layout (Q#BP11).
|
/// acting frontend's layout (Q#BP11).
|
||||||
fn lookup_window(core: &SharedCore, fid: FrontendId, raw: u64) -> mlua::Result<WindowId> {
|
fn lookup_window(core: &SharedCore, fid: FrontendId, raw: u64) -> mlua::Result<WindowId> {
|
||||||
|
|
@ -484,7 +497,7 @@ pub(crate) fn install(lua: &Lua, core: &SharedCore, win: &Table) -> mlua::Result
|
||||||
let fid = acting_frontend(lua, &cc);
|
let fid = acting_frontend(lua, &cc);
|
||||||
let id = match target {
|
let id = match target {
|
||||||
Some(raw) => lookup_window(&cc, fid, raw)?,
|
Some(raw) => lookup_window(&cc, fid, raw)?,
|
||||||
None => cc.borrow().active_window_id(),
|
None => selected_window(&cc, fid)?,
|
||||||
};
|
};
|
||||||
let core = cc.borrow();
|
let core = cc.borrow();
|
||||||
let window = core
|
let window = core
|
||||||
|
|
@ -577,7 +590,7 @@ pub(crate) fn install(lua: &Lua, core: &SharedCore, win: &Table) -> mlua::Result
|
||||||
let fid = acting_frontend(lua, &cc);
|
let fid = acting_frontend(lua, &cc);
|
||||||
let id = match target {
|
let id = match target {
|
||||||
Some(raw) => lookup_window(&cc, fid, raw)?,
|
Some(raw) => lookup_window(&cc, fid, raw)?,
|
||||||
None => cc.borrow().active_window_id(),
|
None => selected_window(&cc, fid)?,
|
||||||
};
|
};
|
||||||
let area_rows = cc.borrow().frontend_area_rows(fid).ok_or_else(|| {
|
let area_rows = cc.borrow().frontend_area_rows(fid).ok_or_else(|| {
|
||||||
mlua::Error::runtime(
|
mlua::Error::runtime(
|
||||||
|
|
|
||||||
|
|
@ -979,14 +979,18 @@ fn compute_node(
|
||||||
rows
|
rows
|
||||||
} else {
|
} else {
|
||||||
let w = weights.get(i).copied().unwrap_or(1).max(1);
|
let w = weights.get(i).copied().unwrap_or(1).max(1);
|
||||||
|
// u64 intermediates: `remainder * w` is the only
|
||||||
|
// place this arithmetic could overflow a u32, and a
|
||||||
|
// saturating fallback there would hand a non-last
|
||||||
|
// child the whole remainder and underflow the last
|
||||||
|
// one. Widening deletes the case outright.
|
||||||
let e = if Some(i) == last_flexible {
|
let e = if Some(i) == last_flexible {
|
||||||
remainder - flexible_used
|
remainder - flexible_used
|
||||||
|
} else if total == 0 {
|
||||||
|
0
|
||||||
} else {
|
} else {
|
||||||
remainder
|
u32::try_from(u64::from(remainder) * u64::from(w) / u64::from(total))
|
||||||
.checked_mul(w)
|
|
||||||
.unwrap_or(remainder)
|
.unwrap_or(remainder)
|
||||||
.checked_div(total)
|
|
||||||
.unwrap_or(0)
|
|
||||||
};
|
};
|
||||||
flexible_used += e;
|
flexible_used += e;
|
||||||
e
|
e
|
||||||
|
|
|
||||||
|
|
@ -1300,6 +1300,55 @@ fn acc19_adopters_place_side_affinely_through_real_entry_points() {
|
||||||
assert_ne!(panel, document);
|
assert_ne!(panel, document);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A recompile carries no `display` (only cmdline/cwd are stored), so
|
||||||
|
/// the raw switch would put `*compilation*` in the selected DOCUMENT
|
||||||
|
/// window while the panel still shows it — the duplicate presentation
|
||||||
|
/// this arc removes elsewhere.
|
||||||
|
#[test]
|
||||||
|
fn acc19b_recompile_reuses_the_panel_instead_of_duplicating_into_the_document() {
|
||||||
|
let s = editor();
|
||||||
|
exec(&s, "pmacs.window.split_horizontal()");
|
||||||
|
exec(&s, "pmacs.compile.run(\"true\", { display = \"panel\" })");
|
||||||
|
let panel = side_window(&s).expect("compile opened a panel");
|
||||||
|
let compilation = s.core.borrow().windows[&panel].buffer_id;
|
||||||
|
|
||||||
|
// Focus a document window, then recompile — which reaches
|
||||||
|
// `start_run` with no `display` at all.
|
||||||
|
let document = s
|
||||||
|
.core
|
||||||
|
.borrow()
|
||||||
|
.non_side_target(FrontendId::LOCAL)
|
||||||
|
.expect("document");
|
||||||
|
s.core
|
||||||
|
.borrow_mut()
|
||||||
|
.focus_window(FrontendId::LOCAL, document);
|
||||||
|
let document_buffer = s.core.borrow().windows[&document].buffer_id;
|
||||||
|
exec(&s, "pmacs.command.invoke(\"compile.recompile\")");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
s.core.borrow().windows[&panel].buffer_id,
|
||||||
|
compilation,
|
||||||
|
"the recompile stayed in the panel"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
s.core.borrow().windows[&document].buffer_id,
|
||||||
|
document_buffer,
|
||||||
|
"…and did not duplicate itself into the document window"
|
||||||
|
);
|
||||||
|
|
||||||
|
// A compilation that is NOT in a panel keeps the pre-arc raw switch.
|
||||||
|
let s = editor();
|
||||||
|
exec(&s, "pmacs.compile.run(\"true\")");
|
||||||
|
assert!(side_window(&s).is_none());
|
||||||
|
let target = active_window(&s);
|
||||||
|
exec(&s, "pmacs.command.invoke(\"compile.recompile\")");
|
||||||
|
assert_eq!(active_window(&s), target);
|
||||||
|
assert!(
|
||||||
|
side_window(&s).is_none(),
|
||||||
|
"no panel is created out of nowhere"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 20 / 23 — quit: delete, restore chains, revalidation, and the cap
|
// 20 / 23 — quit: delete, restore chains, revalidation, and the cap
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -1549,27 +1598,30 @@ fn acc26_close_others_and_split_respect_the_side_window() {
|
||||||
let s = editor();
|
let s = editor();
|
||||||
exec(&s, "pmacs.window.split_horizontal()");
|
exec(&s, "pmacs.window.split_horizontal()");
|
||||||
let panel = open_panel(&s, "*panel*", 5);
|
let panel = open_panel(&s, "*panel*", 5);
|
||||||
// From a side window both are pointed errors.
|
// From a side window both are pointed errors — asserted through the
|
||||||
|
// REAL Lua bindings, which is what `C-x 1` / `C-x 2` / `C-x 3`
|
||||||
|
// reach. A direct `core.try_split_active(..)` call would pass even
|
||||||
|
// with the guard unwired, which is exactly how an unwired guard
|
||||||
|
// survives review.
|
||||||
exec(&s, "pmacs.window.focus_next()");
|
exec(&s, "pmacs.window.focus_next()");
|
||||||
while active_window(&s) != panel {
|
while active_window(&s) != panel {
|
||||||
exec(&s, "pmacs.window.focus_next()");
|
exec(&s, "pmacs.window.focus_next()");
|
||||||
}
|
}
|
||||||
assert!(s.core.borrow_mut().close_others().is_err());
|
let before = structure(&layout_root(&s));
|
||||||
assert!(
|
assert!(try_exec(&s, "pmacs.window.close_others()").is_err());
|
||||||
s.core
|
assert!(try_exec(&s, "pmacs.window.split_horizontal()").is_err());
|
||||||
.borrow_mut()
|
assert!(try_exec(&s, "pmacs.window.split_vertical()").is_err());
|
||||||
.try_split_active(Orientation::Horizontal, true)
|
|
||||||
.is_err()
|
|
||||||
);
|
|
||||||
assert!(side_window(&s).is_some(), "nothing was mutated");
|
assert!(side_window(&s).is_some(), "nothing was mutated");
|
||||||
|
assert_eq!(
|
||||||
|
before,
|
||||||
|
structure(&layout_root(&s)),
|
||||||
|
"the wrapper's final child is still Leaf(side)"
|
||||||
|
);
|
||||||
|
|
||||||
// From a document window, close_others deletes the panel too.
|
// From a document window, close_others deletes the panel too.
|
||||||
exec(&s, "pmacs.window.focus_next()");
|
exec(&s, "pmacs.window.focus_next()");
|
||||||
assert_ne!(active_window(&s), panel);
|
assert_ne!(active_window(&s), panel);
|
||||||
s.core
|
exec(&s, "pmacs.window.close_others()");
|
||||||
.borrow_mut()
|
|
||||||
.close_others()
|
|
||||||
.expect("document may close others");
|
|
||||||
assert!(side_window(&s).is_none());
|
assert!(side_window(&s).is_none());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
s.core.borrow().views[&FrontendId::LOCAL]
|
s.core.borrow().views[&FrontendId::LOCAL]
|
||||||
|
|
@ -1728,6 +1780,57 @@ fn acc30_divider_drag_writes_fixed_rows_and_weights_and_creates_no_selection() {
|
||||||
assert!(doubled > after, "the ratio scales with the frame");
|
assert!(doubled > after, "the ratio scales with the frame");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An armed drag owns the pointer for its OWN frontend only. The daemon
|
||||||
|
/// routes every attached grid frontend through one `dispatch_mouse`, so
|
||||||
|
/// an unscoped guard would let one frontend's in-flight gesture cancel
|
||||||
|
/// and swallow another frontend's clicks.
|
||||||
|
#[test]
|
||||||
|
fn acc30c_an_armed_drag_does_not_swallow_another_frontends_mouse_events() {
|
||||||
|
let mut s = editor();
|
||||||
|
let panel = open_panel(&s, "*panel*", 6);
|
||||||
|
let document = s
|
||||||
|
.core
|
||||||
|
.borrow()
|
||||||
|
.non_side_target(FrontendId::LOCAL)
|
||||||
|
.expect("document");
|
||||||
|
let other = FrontendId(30);
|
||||||
|
let other_window = attach_frontend(&s, other, true);
|
||||||
|
|
||||||
|
let rects = render(&s);
|
||||||
|
let divider_row = u16::try_from(rects[&document].origin.row + rects[&document].size.rows - 1)
|
||||||
|
.expect("row fits");
|
||||||
|
s.dispatch_mouse(
|
||||||
|
FrontendId::LOCAL,
|
||||||
|
mouse(MouseEventKind::Down(MouseButton::Left), divider_row, 3),
|
||||||
|
CellSize::new(ROWS, COLS),
|
||||||
|
);
|
||||||
|
let armed_rows = fixed_rows_of(&s, panel);
|
||||||
|
|
||||||
|
// A click from the OTHER frontend must be dispatched normally…
|
||||||
|
s.dispatch_mouse(
|
||||||
|
other,
|
||||||
|
mouse(MouseEventKind::Down(MouseButton::Left), 1, 2),
|
||||||
|
CellSize::new(ROWS, COLS),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
s.core.borrow().views[&other].active,
|
||||||
|
other_window,
|
||||||
|
"the peer's click reached its own window instead of being swallowed"
|
||||||
|
);
|
||||||
|
|
||||||
|
// …and LOCAL's gesture must still be armed and still work.
|
||||||
|
s.dispatch_mouse(
|
||||||
|
FrontendId::LOCAL,
|
||||||
|
mouse(MouseEventKind::Drag(MouseButton::Left), divider_row + 2, 3),
|
||||||
|
CellSize::new(ROWS, COLS),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
fixed_rows_of(&s, panel),
|
||||||
|
Some(armed_rows.expect("armed rows") - 2),
|
||||||
|
"the peer's event did not cancel LOCAL's in-flight drag"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn acc30b_ui_divider_face_resolves_and_paints_every_exposed_segment() {
|
fn acc30b_ui_divider_face_resolves_and_paints_every_exposed_segment() {
|
||||||
let s = editor();
|
let s = editor();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue