feat(window): adopter placement opt-in and the Stage 1 acceptance suite
- `listview.open`, `compile.run`, and `pmacs.terminal.open` all take the same strict `display = "current" | "panel"`, validated before any buffer, session, process, or wrapper exists. Omission keeps today's behavior; Stage 3 flips the default. - `listview.quit` / `compile.quit` delegate to `window.quit` only when the buffer really is in a side window, so the presentation is deleted or restored instead of leaving a source buffer stranded in the slot. - LSP `visit_location`, LSP go-to-definition, and compile `visit_error` route through `display_file`, so a visit from a panel lands in the document target and fires its hook with that window active. - `window.quit`'s Delete arm focuses the revalidated remembered origin. - Capability fallback discards an accompanying `height` rather than rejecting the call. - `window.min-height` clamps a below-floor value on read instead of refusing the write. - `tests/bottom_panel_stage1_acceptance.rs`: 42 tests over the framing's Stage 1 criteria, including the two production `Layout::compute` callers, the recursive minima, hide/reappear, the final-focus matrix, quit chains at the depth cap, per-frontend jump origins, the divider, and a real-PTY pin of Bet B1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6c8a76e235
commit
683c9b86aa
|
|
@ -734,6 +734,16 @@ end)
|
||||||
-- has its own worker path.
|
-- has its own worker path.
|
||||||
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
|
||||||
|
-- supersedes anything, rewrites the buffer, or spawns a process, so
|
||||||
|
-- an unknown value leaves no half-started run behind. In Stages 1-2
|
||||||
|
-- omission means "current"; Stage 3 flips the default.
|
||||||
|
local display = opts.display
|
||||||
|
if display ~= nil and display ~= "current" and display ~= "panel" then
|
||||||
|
error(string.format(
|
||||||
|
"compile.run: unknown display %q (expected \"current\" or \"panel\")",
|
||||||
|
tostring(display)))
|
||||||
|
end
|
||||||
-- q-target discipline (Q#CM11): capture only when coming from a
|
-- q-target discipline (Q#CM11): capture only when coming from a
|
||||||
-- non-generated buffer, so `g` reruns don't re-capture and
|
-- non-generated buffer, so `g` reruns don't re-capture and
|
||||||
-- compile → g → q restores the original buffer.
|
-- compile → g → q restores the original buffer.
|
||||||
|
|
@ -805,7 +815,16 @@ local function start_run(slot, cmdline, opts)
|
||||||
-- attach here stacked a duplicate render view per run (round-5
|
-- attach here stacked a duplicate render view per run (round-5
|
||||||
-- finding 1; translation itself is buffer-level and unaffected by
|
-- finding 1; translation itself is buffer-level and unaffected by
|
||||||
-- attachment count).
|
-- attachment count).
|
||||||
pmacs.window.switch_buffer(slot.buf)
|
-- The FIRST display of this run is the side-affine one (Q#BP3): a
|
||||||
|
-- persistent *compilation* already visible in a document window must
|
||||||
|
-- not preempt the requested panel. Compile output is passive, so it
|
||||||
|
-- takes `select = false` explicitly; a recompile simply reuses the
|
||||||
|
-- panel it is already in.
|
||||||
|
if display == "panel" then
|
||||||
|
pmacs.window.display(slot.buf, { side = "bottom", select = false })
|
||||||
|
else
|
||||||
|
pmacs.window.switch_buffer(slot.buf)
|
||||||
|
end
|
||||||
if not ok then
|
if not ok then
|
||||||
emit_text_raw(slot, string.format("[%s spawn failed: %s]\n", slot.label, tostring(proc)))
|
emit_text_raw(slot, string.format("[%s spawn failed: %s]\n", slot.label, tostring(proc)))
|
||||||
slot.expected_rev = buf:revision()
|
slot.expected_rev = buf:revision()
|
||||||
|
|
@ -866,7 +885,9 @@ local function visit_error(slot, idx)
|
||||||
if not e then return end
|
if not e then return end
|
||||||
local path = resolve_error_path(slot, e.file)
|
local path = resolve_error_path(slot, e.file)
|
||||||
pmacs.editor.push_jump()
|
pmacs.editor.push_jump()
|
||||||
local ok, err = pcall(pmacs.buffer.find_or_open, path)
|
-- Bottom-panel arc (Q#BP11b): RET from a compilation PANEL opens the
|
||||||
|
-- source in the document target, leaving the panel where it is.
|
||||||
|
local ok, err = pcall(pmacs.window.display_file, path, { select = true })
|
||||||
if not ok then
|
if not ok then
|
||||||
pmacs.editor.jump_back()
|
pmacs.editor.jump_back()
|
||||||
pmacs.editor.set_status(slot.label .. ": failed to open " .. path .. ": " .. tostring(err))
|
pmacs.editor.set_status(slot.label .. ": failed to open " .. path .. ": " .. tostring(err))
|
||||||
|
|
@ -995,6 +1016,15 @@ pmacs.command.define {
|
||||||
fn = function()
|
fn = function()
|
||||||
local slot = slot_for_buffer(pmacs.window.buffer())
|
local slot = slot_for_buffer(pmacs.window.buffer())
|
||||||
if not slot then return end
|
if not slot then return end
|
||||||
|
-- Bottom-panel arc (Q#BP11b): in a side window, `q` deletes or
|
||||||
|
-- restores the PRESENTATION rather than leaving a source buffer
|
||||||
|
-- stranded in the panel slot. Capability fallback and pre-arc
|
||||||
|
-- placement keep today's previous-buffer restore below.
|
||||||
|
local params = pmacs.window.params()
|
||||||
|
if params and params.side and params.quit_action then
|
||||||
|
pmacs.window.quit()
|
||||||
|
return
|
||||||
|
end
|
||||||
local target = slot.prev
|
local target = slot.prev
|
||||||
if not (target and target:is_valid()) then
|
if not (target and target:is_valid()) then
|
||||||
target = buffer_named("*scratch*") or pmacs.buffer.create("*scratch*")
|
target = buffer_named("*scratch*") or pmacs.buffer.create("*scratch*")
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,25 @@ function pmacs.listview.open(spec)
|
||||||
p.prev = active
|
p.prev = active
|
||||||
end
|
end
|
||||||
render(p, spec.rows or {})
|
render(p, spec.rows or {})
|
||||||
pmacs.window.switch_buffer(p.buffer)
|
-- Bottom-panel arc (Q#BP11b): the placement opt-in. `seat_cursor` and
|
||||||
|
-- `listview.refresh` are active-window-only, so an interactive panel
|
||||||
|
-- MUST take `select = true` or it would silently seat the wrong
|
||||||
|
-- window. In Stages 1-2 omitting `display` keeps today's raw switch;
|
||||||
|
-- Stage 3 flips the default. An unknown value errors before anything
|
||||||
|
-- is displayed.
|
||||||
|
local display = spec.display
|
||||||
|
if display ~= nil and display ~= "current" and display ~= "panel" then
|
||||||
|
error(string.format(
|
||||||
|
"listview.open: unknown display %q (expected \"current\" or \"panel\")",
|
||||||
|
tostring(display)))
|
||||||
|
end
|
||||||
|
if display == "panel" then
|
||||||
|
p.side = true
|
||||||
|
pmacs.window.display(p.buffer, { side = "bottom", select = true })
|
||||||
|
else
|
||||||
|
p.side = false
|
||||||
|
pmacs.window.switch_buffer(p.buffer)
|
||||||
|
end
|
||||||
seat_cursor(p, 1)
|
seat_cursor(p, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -160,6 +178,15 @@ pmacs.command.define {
|
||||||
fn = function()
|
fn = function()
|
||||||
local p = panel_for_current_buffer()
|
local p = panel_for_current_buffer()
|
||||||
if not p then return end
|
if not p then return end
|
||||||
|
-- Bottom-panel arc (Q#BP11b): `q` keeps its name and its
|
||||||
|
-- user-visible behavior, delegating to `window.quit` only when the
|
||||||
|
-- listview really is in a side window. Capability fallback (and any
|
||||||
|
-- pre-arc placement) keeps the previous-buffer switch below.
|
||||||
|
local params = pmacs.window.params()
|
||||||
|
if params and params.side and params.quit_action then
|
||||||
|
pmacs.window.quit()
|
||||||
|
return
|
||||||
|
end
|
||||||
local target = p.prev
|
local target = p.prev
|
||||||
if not (target and target:is_valid()) then
|
if not (target and target:is_valid()) then
|
||||||
target = find_buffer_by_name("*scratch*") or pmacs.buffer.create("*scratch*")
|
target = find_buffer_by_name("*scratch*") or pmacs.buffer.create("*scratch*")
|
||||||
|
|
|
||||||
|
|
@ -1565,7 +1565,11 @@ function pmacs.lsp.go_to_definition()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
pmacs.editor.push_jump()
|
pmacs.editor.push_jump()
|
||||||
local ok2, oerr = pcall(pmacs.buffer.find_or_open, path)
|
-- Bottom-panel arc (Q#BP11b): the target-aware load. `find_or_open`
|
||||||
|
-- switches the ACTIVE window, which would replace a focused panel;
|
||||||
|
-- `display_file` resolves the DOCUMENT target first and fires the
|
||||||
|
-- load/switch hook with that window active.
|
||||||
|
local ok2, oerr = pcall(pmacs.window.display_file, path, { select = true })
|
||||||
if not ok2 then
|
if not ok2 then
|
||||||
-- Open failed: drop the origin we just pushed so M-, isn't
|
-- Open failed: drop the origin we just pushed so M-, isn't
|
||||||
-- left pointing at a jump that never happened.
|
-- left pointing at a jump that never happened.
|
||||||
|
|
@ -1601,7 +1605,9 @@ local function visit_location(loc)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
pmacs.editor.push_jump()
|
pmacs.editor.push_jump()
|
||||||
local ok, err = pcall(pmacs.buffer.find_or_open, path)
|
-- Bottom-panel arc (Q#BP11b): a visit FROM a panel must land in the
|
||||||
|
-- document target and leave the panel intact.
|
||||||
|
local ok, err = pcall(pmacs.window.display_file, path, { select = true })
|
||||||
if not ok then
|
if not ok then
|
||||||
-- Open failed: drop the origin we just pushed so M-, isn't left
|
-- Open failed: drop the origin we just pushed so M-, isn't left
|
||||||
-- pointing at a jump that never happened.
|
-- pointing at a jump that never happened.
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,17 @@ pmacs.config.define {
|
||||||
-- (drag and the commands below) and is deliberately ignored by the
|
-- (drag and the commands below) and is deliberately ignored by the
|
||||||
-- ordinary layout pass and by frame-resize reconciliation, so raising it
|
-- ordinary layout pass and by frame-resize reconciliation, so raising it
|
||||||
-- can never invalidate a layout that already exists.
|
-- can never invalidate a layout that already exists.
|
||||||
|
--
|
||||||
|
-- The registry floor is 1 rather than 2 on purpose: a value below the
|
||||||
|
-- STRUCTURAL floor is clamped when it is read, not rejected when it is
|
||||||
|
-- written, so a user who asks for a smaller minimum simply gets the
|
||||||
|
-- smallest one the layout can actually honor.
|
||||||
pmacs.config.define {
|
pmacs.config.define {
|
||||||
name = "window.min-height",
|
name = "window.min-height",
|
||||||
description = "Smallest outer rows interactive resize will leave a window.",
|
description = "Smallest outer rows interactive resize will leave a window.",
|
||||||
type = "integer",
|
type = "integer",
|
||||||
default = 2,
|
default = 2,
|
||||||
min = 2,
|
min = 1,
|
||||||
mutability = "live",
|
mutability = "live",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -905,7 +905,7 @@ fn peer_declared_terminal_support(
|
||||||
/// from the daemon's own negotiated state, and Stage 2 turns the version
|
/// from the daemon's own negotiated state, and Stage 2 turns the version
|
||||||
/// arm on (`semantic_render && negotiated_protocol_version >=
|
/// arm on (`semantic_render && negotiated_protocol_version >=
|
||||||
/// PANEL_MIN_VERSION`).
|
/// PANEL_MIN_VERSION`).
|
||||||
fn peer_declared_panel_support(session_state: &crate::presence::SessionState) -> bool {
|
fn peer_declared_panel_support(session_state: crate::presence::SessionState) -> bool {
|
||||||
!session_state.negotiated_capabilities.semantic_render
|
!session_state.negotiated_capabilities.semantic_render
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1808,7 +1808,7 @@ fn handle_session_established(
|
||||||
let fresh_view = build_fresh_frontend_view(
|
let fresh_view = build_fresh_frontend_view(
|
||||||
editor,
|
editor,
|
||||||
!session_state.negotiated_capabilities.semantic_render,
|
!session_state.negotiated_capabilities.semantic_render,
|
||||||
peer_declared_panel_support(&session_state),
|
peer_declared_panel_support(session_state),
|
||||||
);
|
);
|
||||||
{
|
{
|
||||||
let mut core = editor.core.borrow_mut();
|
let mut core = editor.core.borrow_mut();
|
||||||
|
|
|
||||||
|
|
@ -841,14 +841,13 @@ impl EditorState {
|
||||||
.get(&window_id)
|
.get(&window_id)
|
||||||
.map(|window| window.buffer_id);
|
.map(|window| window.buffer_id);
|
||||||
if let Some(buffer_id) = buffer_id {
|
if let Some(buffer_id) = buffer_id {
|
||||||
let _ = self
|
let _ = self.terminal_manager.borrow_mut().release_controller(
|
||||||
.terminal_manager
|
crate::terminal::TerminalViewKey {
|
||||||
.borrow_mut()
|
|
||||||
.release_controller(crate::terminal::TerminalViewKey {
|
|
||||||
frontend_id,
|
frontend_id,
|
||||||
window_id,
|
window_id,
|
||||||
buffer_id,
|
buffer_id,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outcome.changed
|
outcome.changed
|
||||||
|
|
@ -1931,7 +1930,8 @@ impl EditorState {
|
||||||
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);
|
||||||
}
|
}
|
||||||
MouseEventKind::Up(MouseButton::Left) => self.window_drag = None,
|
// Any other event — release, a different button, a
|
||||||
|
// wheel notch — ends the gesture.
|
||||||
_ => self.window_drag = None,
|
_ => self.window_drag = None,
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -2062,9 +2062,12 @@ impl EditorState {
|
||||||
/// Arm a divider drag if `owner`'s bottom row really is an exposed
|
/// Arm a divider drag if `owner`'s bottom row really is an exposed
|
||||||
/// segment of a horizontal boundary (Q#BP5).
|
/// segment of a horizontal boundary (Q#BP5).
|
||||||
fn arm_window_drag(&mut self, frontend_id: FrontendId, owner: WindowId, cell_row: u32) {
|
fn arm_window_drag(&mut self, frontend_id: FrontendId, owner: WindowId, cell_row: u32) {
|
||||||
let is_divider = self.core.borrow().views.get(&frontend_id).is_some_and(|view| {
|
let is_divider = self
|
||||||
view.layout.boundary_below(owner).is_some()
|
.core
|
||||||
});
|
.borrow()
|
||||||
|
.views
|
||||||
|
.get(&frontend_id)
|
||||||
|
.is_some_and(|view| view.layout.boundary_below(owner).is_some());
|
||||||
self.window_drag = is_divider.then_some(WindowDragState {
|
self.window_drag = is_divider.then_some(WindowDragState {
|
||||||
frontend_id,
|
frontend_id,
|
||||||
owner,
|
owner,
|
||||||
|
|
@ -2078,14 +2081,22 @@ impl EditorState {
|
||||||
/// layout mutation mid-drag cannot move a boundary that no longer
|
/// layout mutation mid-drag cannot move a boundary that no longer
|
||||||
/// exists. Motion is applied incrementally and re-anchored each
|
/// exists. Motion is applied incrementally and re-anchored each
|
||||||
/// event, so the clamp absorbs over-travel instead of accumulating it.
|
/// event, so the clamp absorbs over-travel instead of accumulating it.
|
||||||
fn drag_window_boundary(&mut self, frontend_id: FrontendId, cell_row: u32, term_size: CellSize) {
|
fn drag_window_boundary(
|
||||||
|
&mut self,
|
||||||
|
frontend_id: FrontendId,
|
||||||
|
cell_row: u32,
|
||||||
|
term_size: CellSize,
|
||||||
|
) {
|
||||||
let Some(drag) = self.window_drag else {
|
let Some(drag) = self.window_drag else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if drag.frontend_id != frontend_id {
|
if drag.frontend_id != frontend_id {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.window_drag = Some(WindowDragState { last_row: cell_row, ..drag });
|
self.window_drag = Some(WindowDragState {
|
||||||
|
last_row: cell_row,
|
||||||
|
..drag
|
||||||
|
});
|
||||||
let delta = i64::from(cell_row) - i64::from(drag.last_row);
|
let delta = i64::from(cell_row) - i64::from(drag.last_row);
|
||||||
let Ok(delta) = i32::try_from(delta) else {
|
let Ok(delta) = i32::try_from(delta) else {
|
||||||
return;
|
return;
|
||||||
|
|
@ -3126,13 +3137,14 @@ pub fn paint_frame(
|
||||||
// upper child is a nested subtree exposes SEVERAL leaf segments along
|
// upper child is a nested subtree exposes SEVERAL leaf segments along
|
||||||
// the same edge, so the root panel divider is full width even when
|
// the same edge, so the root panel divider is full width even when
|
||||||
// the document subtree ends in several columns.
|
// the document subtree ends in several columns.
|
||||||
let divider_windows: Vec<WindowId> = core.views.get(&frontend_id).map_or_else(Vec::new, |view| {
|
let divider_windows: Vec<WindowId> =
|
||||||
view.layout
|
core.views.get(&frontend_id).map_or_else(Vec::new, |view| {
|
||||||
.iter_ids()
|
view.layout
|
||||||
.into_iter()
|
.iter_ids()
|
||||||
.filter(|id| view.layout.boundary_below(*id).is_some())
|
.into_iter()
|
||||||
.collect()
|
.filter(|id| view.layout.boundary_below(*id).is_some())
|
||||||
});
|
.collect()
|
||||||
|
});
|
||||||
let divider_style = theme.face("ui.divider");
|
let divider_style = theme.face("ui.divider");
|
||||||
|
|
||||||
// Clear the whole grid first so windows that shrink on resize
|
// Clear the whole grid first so windows that shrink on resize
|
||||||
|
|
@ -7073,22 +7085,14 @@ mod tests {
|
||||||
} else {
|
} else {
|
||||||
panic!("expected split");
|
panic!("expected split");
|
||||||
}
|
}
|
||||||
let p1 = s
|
let p1 = s.core.borrow().active_layout().compute(
|
||||||
.core
|
crate::window::Rect::new(0, 0, 24, 90),
|
||||||
.borrow()
|
&std::collections::HashMap::new(),
|
||||||
.active_layout()
|
);
|
||||||
.compute(
|
let p2 = s.core.borrow().active_layout().compute(
|
||||||
crate::window::Rect::new(0, 0, 24, 90),
|
crate::window::Rect::new(0, 0, 24, 60),
|
||||||
&std::collections::HashMap::new(),
|
&std::collections::HashMap::new(),
|
||||||
);
|
);
|
||||||
let p2 = s
|
|
||||||
.core
|
|
||||||
.borrow()
|
|
||||||
.active_layout()
|
|
||||||
.compute(
|
|
||||||
crate::window::Rect::new(0, 0, 24, 60),
|
|
||||||
&std::collections::HashMap::new(),
|
|
||||||
);
|
|
||||||
// Both should preserve the 2:1 ratio. Find the two windows
|
// Both should preserve the 2:1 ratio. Find the two windows
|
||||||
// and verify the larger:smaller ratio is 2:1 in both.
|
// and verify the larger:smaller ratio is 2:1 in both.
|
||||||
let wider1 = p1.values().map(|r| r.size.cols).max().unwrap();
|
let wider1 = p1.values().map(|r| r.size.cols).max().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -882,10 +882,7 @@ impl EditorCore {
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Any load failure other than `NotFound`.
|
/// Any load failure other than `NotFound`.
|
||||||
pub fn resolve_target_buffer(
|
pub fn resolve_target_buffer(&mut self, path: &Path) -> Result<(BufferId, HookKind), String> {
|
||||||
&mut self,
|
|
||||||
path: &Path,
|
|
||||||
) -> Result<(BufferId, HookKind), String> {
|
|
||||||
match self.get_or_load_buffer(path) {
|
match self.get_or_load_buffer(path) {
|
||||||
Ok((buffer_id, true)) => Ok((buffer_id, HookKind::AfterLoad)),
|
Ok((buffer_id, true)) => Ok((buffer_id, HookKind::AfterLoad)),
|
||||||
Ok((buffer_id, false)) => Ok((buffer_id, HookKind::AfterSwitch)),
|
Ok((buffer_id, false)) => Ok((buffer_id, HookKind::AfterSwitch)),
|
||||||
|
|
@ -1060,11 +1057,7 @@ impl EditorCore {
|
||||||
pub fn jump_back(&mut self) -> bool {
|
pub fn jump_back(&mut self) -> bool {
|
||||||
let fid = self.active_frontend;
|
let fid = self.active_frontend;
|
||||||
loop {
|
loop {
|
||||||
let Some(entry) = self
|
let Some(entry) = self.jump_ring.get_mut(&fid).and_then(std::vec::Vec::pop) else {
|
||||||
.jump_ring
|
|
||||||
.get_mut(&fid)
|
|
||||||
.and_then(std::vec::Vec::pop)
|
|
||||||
else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if !self.registry.borrow().contains(entry.buffer_id) {
|
if !self.registry.borrow().contains(entry.buffer_id) {
|
||||||
|
|
@ -1081,12 +1074,20 @@ impl EditorCore {
|
||||||
&& !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);
|
self.set_active_window_id(entry.window_id);
|
||||||
} else if entry.side_origin {
|
} else {
|
||||||
continue;
|
// A stale SIDE origin is skipped outright: switching a
|
||||||
} else if self.active_buffer_id() != entry.buffer_id
|
// panel's buffer into the document window is exactly the
|
||||||
&& self.switch_active_buffer(entry.buffer_id).is_err()
|
// duplicate-presentation corruption this design removes.
|
||||||
{
|
// A stale non-side origin keeps today's active-window
|
||||||
continue;
|
// fallback.
|
||||||
|
if entry.side_origin {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.active_buffer_id() != entry.buffer_id
|
||||||
|
&& self.switch_active_buffer(entry.buffer_id).is_err()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let clamped = entry.position.min(self.active_buffer_len());
|
let clamped = entry.position.min(self.active_buffer_len());
|
||||||
let aw = self.active_window_mut();
|
let aw = self.active_window_mut();
|
||||||
|
|
@ -2713,10 +2714,8 @@ impl EditorCore {
|
||||||
}
|
}
|
||||||
self.active_layout_mut().close_window(target);
|
self.active_layout_mut().close_window(target);
|
||||||
self.windows.remove(&target);
|
self.windows.remove(&target);
|
||||||
if target_is_side {
|
if target_is_side && let Some(view) = self.views.get_mut(&fid) {
|
||||||
if let Some(view) = self.views.get_mut(&fid) {
|
view.panel_hidden = false;
|
||||||
view.panel_hidden = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Pick an adjacent window as the new focus, preferring a document.
|
// Pick an adjacent window as the new focus, preferring a document.
|
||||||
let ids = self.active_layout().iter_ids();
|
let ids = self.active_layout().iter_ids();
|
||||||
|
|
@ -3002,10 +3001,9 @@ impl EditorCore {
|
||||||
self.windows.remove(&side);
|
self.windows.remove(&side);
|
||||||
if was_active
|
if was_active
|
||||||
&& let Ok(target) = self.non_side_target(fid)
|
&& let Ok(target) = self.non_side_target(fid)
|
||||||
|
&& let Some(view) = self.views.get_mut(&fid)
|
||||||
{
|
{
|
||||||
if let Some(view) = self.views.get_mut(&fid) {
|
view.active = target;
|
||||||
view.active = target;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// A remembered origin pointing at a now-dead window is cleared by
|
// A remembered origin pointing at a now-dead window is cleared by
|
||||||
// `non_side_target`'s revalidation on next use; nothing else here
|
// `non_side_target`'s revalidation on next use; nothing else here
|
||||||
|
|
@ -3050,14 +3048,34 @@ impl EditorCore {
|
||||||
};
|
};
|
||||||
match action {
|
match action {
|
||||||
QuitAction::Delete => {
|
QuitAction::Delete => {
|
||||||
let saved_active = self.views.get(&fid).map(|view| view.active);
|
// Capture the remembered origin BEFORE the window dies:
|
||||||
|
// executing `Delete` focuses the revalidated origin, not
|
||||||
|
// merely whatever leaf the wrapper collapse surfaced
|
||||||
|
// (Q#BP11b). Entering the panel from document window B
|
||||||
|
// must therefore return focus to B, not to the window
|
||||||
|
// that happened to create the panel.
|
||||||
|
let origin = self
|
||||||
|
.windows
|
||||||
|
.get(&target)
|
||||||
|
.and_then(|window| window.params.origin_document());
|
||||||
self.remove_side_window(fid, target);
|
self.remove_side_window(fid, target);
|
||||||
Ok(QuitOutcome::Deleted {
|
let origin_valid = origin.is_some_and(|origin| {
|
||||||
focus: self
|
self.views
|
||||||
.views
|
|
||||||
.get(&fid)
|
.get(&fid)
|
||||||
.map(|view| view.active)
|
.is_some_and(|view| view.layout.iter_ids().contains(&origin))
|
||||||
.or(saved_active),
|
&& !self
|
||||||
|
.windows
|
||||||
|
.get(&origin)
|
||||||
|
.is_some_and(crate::window::Window::is_side)
|
||||||
|
});
|
||||||
|
if origin_valid
|
||||||
|
&& let Some(origin) = origin
|
||||||
|
&& let Some(view) = self.views.get_mut(&fid)
|
||||||
|
{
|
||||||
|
view.active = origin;
|
||||||
|
}
|
||||||
|
Ok(QuitOutcome::Deleted {
|
||||||
|
focus: self.views.get(&fid).map(|view| view.active),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
QuitAction::Restore {
|
QuitAction::Restore {
|
||||||
|
|
@ -3073,7 +3091,7 @@ impl EditorCore {
|
||||||
self.install_buffer_in_window(target, buffer_id)?;
|
self.install_buffer_in_window(target, buffer_id)?;
|
||||||
let len = {
|
let len = {
|
||||||
let reg = self.registry.borrow();
|
let reg = self.registry.borrow();
|
||||||
reg.get(buffer_id).map(Buffer::len).unwrap_or(0)
|
reg.get(buffer_id).map_or(0, Buffer::len)
|
||||||
};
|
};
|
||||||
let window = self
|
let window = self
|
||||||
.windows
|
.windows
|
||||||
|
|
@ -3091,10 +3109,7 @@ impl EditorCore {
|
||||||
window.view_top = view_top;
|
window.view_top = view_top;
|
||||||
window.goal_col = goal_col;
|
window.goal_col = goal_col;
|
||||||
window.selection = selection.filter(|sel| sel.anchor <= len);
|
window.selection = selection.filter(|sel| sel.anchor <= len);
|
||||||
Ok(QuitOutcome::Restored {
|
Ok(QuitOutcome::Restored { target, buffer_id })
|
||||||
target,
|
|
||||||
buffer_id,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3117,8 +3132,7 @@ impl EditorCore {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn frontend_area_rows(&self, fid: FrontendId) -> Option<u32> {
|
pub fn frontend_area_rows(&self, fid: FrontendId) -> Option<u32> {
|
||||||
let geometry = self.views.get(&fid)?.frame_geometry?;
|
let geometry = self.views.get(&fid)?.frame_geometry?;
|
||||||
(geometry.total.rows >= 2 && geometry.total.cols > 0)
|
(geometry.total.rows >= 2 && geometry.total.cols > 0).then(|| geometry.total.rows - 1)
|
||||||
.then(|| geometry.total.rows - 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cache a frontend's authoritative frame capacity (Q#BP2b).
|
/// Cache a frontend's authoritative frame capacity (Q#BP2b).
|
||||||
|
|
@ -3207,6 +3221,10 @@ impl EditorCore {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// When `win` is not live in `fid`'s layout, when the panel is
|
/// When `win` is not live in `fid`'s layout, when the panel is
|
||||||
/// hidden, or when no adjustable horizontal boundary exists.
|
/// hidden, or when no adjustable horizontal boundary exists.
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_lines,
|
||||||
|
reason = "one boundary-resize transaction: resolve, snapshot minima, clamp, write back"
|
||||||
|
)]
|
||||||
pub fn resize_boundary(
|
pub fn resize_boundary(
|
||||||
&mut self,
|
&mut self,
|
||||||
fid: FrontendId,
|
fid: FrontendId,
|
||||||
|
|
@ -3260,9 +3278,9 @@ impl EditorCore {
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
view.layout
|
view.layout.boundary_below(win).ok_or_else(|| {
|
||||||
.boundary_below(win)
|
"window.resize: no adjustable horizontal boundary".to_string()
|
||||||
.ok_or_else(|| "window.resize: no adjustable horizontal boundary".to_string())?,
|
})?,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
@ -3423,13 +3441,7 @@ impl EditorCore {
|
||||||
target: placement.target,
|
target: placement.target,
|
||||||
saved_active,
|
saved_active,
|
||||||
select,
|
select,
|
||||||
created_side: matches!(
|
created_side: matches!(placement.kind, PlacementKind::Side { created: true, .. }),
|
||||||
placement.kind,
|
|
||||||
PlacementKind::Side {
|
|
||||||
created: true,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3494,7 +3506,12 @@ impl EditorCore {
|
||||||
if let Ok(preferred) = self.non_side_target(fid) {
|
if let Ok(preferred) = self.non_side_target(fid) {
|
||||||
candidates.push(preferred);
|
candidates.push(preferred);
|
||||||
}
|
}
|
||||||
candidates.extend(view.layout.iter_ids().into_iter().filter(|id| !is_side(*id)));
|
candidates.extend(
|
||||||
|
view.layout
|
||||||
|
.iter_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|id| !is_side(*id)),
|
||||||
|
);
|
||||||
candidates
|
candidates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|id| eligible(*id))
|
.find(|id| eligible(*id))
|
||||||
|
|
@ -3506,6 +3523,10 @@ impl EditorCore {
|
||||||
/// otherwise a persistent `*compilation*` buffer already visible in a
|
/// otherwise a persistent `*compilation*` buffer already visible in a
|
||||||
/// document window makes `{side = "bottom"}` silently ignore its
|
/// document window makes `{side = "bottom"}` silently ignore its
|
||||||
/// requested placement.
|
/// requested placement.
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_lines,
|
||||||
|
reason = "Q#BP3's precedence ladder reads as one ordered policy"
|
||||||
|
)]
|
||||||
fn resolve_placement(
|
fn resolve_placement(
|
||||||
&self,
|
&self,
|
||||||
fid: FrontendId,
|
fid: FrontendId,
|
||||||
|
|
@ -3594,7 +3615,12 @@ impl EditorCore {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if request.height.is_some() {
|
} else if request.side.is_none() && request.height.is_some() {
|
||||||
|
// A freestanding `height` with no side request is a mistake.
|
||||||
|
// A `height` that arrived WITH a side request and fell
|
||||||
|
// through (not panel-capable, or the one slot is dedicated
|
||||||
|
// elsewhere) is discarded, not rejected — capability
|
||||||
|
// fallback must not turn into an error (Q#BP2c).
|
||||||
return Err("display: `height` requires a side window".into());
|
return Err("display: `height` requires a side window".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3624,11 +3650,17 @@ impl EditorCore {
|
||||||
if let Ok(preferred) = self.non_side_target(fid) {
|
if let Ok(preferred) = self.non_side_target(fid) {
|
||||||
candidates.push(preferred);
|
candidates.push(preferred);
|
||||||
}
|
}
|
||||||
candidates.extend(view.layout.iter_ids().into_iter().filter(|id| !is_side(*id)));
|
candidates.extend(
|
||||||
|
view.layout
|
||||||
|
.iter_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|id| !is_side(*id)),
|
||||||
|
);
|
||||||
for candidate in candidates {
|
for candidate in candidates {
|
||||||
let eligible = self.windows.get(&candidate).is_some_and(|w| {
|
let eligible = self
|
||||||
!w.params.dedicated || w.buffer_id == request.buffer_id
|
.windows
|
||||||
});
|
.get(&candidate)
|
||||||
|
.is_some_and(|w| !w.params.dedicated || w.buffer_id == request.buffer_id);
|
||||||
if eligible {
|
if eligible {
|
||||||
return Ok(Placement {
|
return Ok(Placement {
|
||||||
target: candidate,
|
target: candidate,
|
||||||
|
|
@ -3681,7 +3713,8 @@ impl EditorCore {
|
||||||
let requested_side = request.side.unwrap_or(Side::Bottom);
|
let requested_side = request.side.unwrap_or(Side::Bottom);
|
||||||
|
|
||||||
if created {
|
if created {
|
||||||
let rows = Self::clamp_panel_rows(request.height.unwrap_or(request.default_panel_rows))?;
|
let rows =
|
||||||
|
Self::clamp_panel_rows(request.height.unwrap_or(request.default_panel_rows))?;
|
||||||
let origin = self.non_side_target(fid).ok();
|
let origin = self.non_side_target(fid).ok();
|
||||||
let text_view = {
|
let text_view = {
|
||||||
let reg = self.registry.borrow();
|
let reg = self.registry.borrow();
|
||||||
|
|
|
||||||
|
|
@ -8501,8 +8501,8 @@ fn install_terminal(
|
||||||
let supervisor = supervisor.clone();
|
let supervisor = supervisor.clone();
|
||||||
terminal.set(
|
terminal.set(
|
||||||
"_open",
|
"_open",
|
||||||
lua.create_function(move |lua, spec: Table| -> mlua::Result<BufferIdLua> {
|
lua.create_function(move |lua, spec_table: Table| -> mlua::Result<BufferIdLua> {
|
||||||
let spec = parse_terminal_spec(&spec)?;
|
let spec = parse_terminal_spec(&spec_table)?;
|
||||||
let core = lua
|
let core = lua
|
||||||
.app_data_ref::<SharedCore>()
|
.app_data_ref::<SharedCore>()
|
||||||
.map(|core| core.clone())
|
.map(|core| core.clone())
|
||||||
|
|
@ -8515,37 +8515,54 @@ fn install_terminal(
|
||||||
"pmacs.terminal.open: target frontend has no active window",
|
"pmacs.terminal.open: target frontend has no active window",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
// Bottom-panel arc (Q#BP11b): parse placement BEFORE the
|
||||||
|
// session, process, buffer, or wrapper exists, so an
|
||||||
|
// unknown `display` value creates nothing to roll back.
|
||||||
|
let placement = window_panel::parse_adopter_placement(
|
||||||
|
&core,
|
||||||
|
frontend_id,
|
||||||
|
"pmacs.terminal.open",
|
||||||
|
spec_table.get::<Option<String>>("display")?.as_deref(),
|
||||||
|
spec_table.get::<Option<u64>>("window")?,
|
||||||
|
)?;
|
||||||
let buffer_id = {
|
let buffer_id = {
|
||||||
let mut manager = manager.borrow_mut();
|
let mut manager = manager.borrow_mut();
|
||||||
manager
|
manager
|
||||||
.open(spec, &mut core.borrow_mut(), &mut supervisor.borrow_mut())
|
.open(spec, &mut core.borrow_mut(), &mut supervisor.borrow_mut())
|
||||||
.map_err(mlua::Error::external)?
|
.map_err(mlua::Error::external)?
|
||||||
};
|
};
|
||||||
let key = {
|
let outcome = match window_panel::place_adopter_buffer(
|
||||||
let mut core = core.borrow_mut();
|
lua,
|
||||||
if let Err(error) = core.switch_active_buffer_for(frontend_id, buffer_id) {
|
&core,
|
||||||
|
frontend_id,
|
||||||
|
buffer_id,
|
||||||
|
&placement,
|
||||||
|
true,
|
||||||
|
) {
|
||||||
|
Ok(outcome) => outcome,
|
||||||
|
Err(error) => {
|
||||||
|
let mut core = core.borrow_mut();
|
||||||
let _ = core.registry.borrow_mut().remove(buffer_id);
|
let _ = core.registry.borrow_mut().remove(buffer_id);
|
||||||
manager
|
manager
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.prune(&mut core, &mut supervisor.borrow_mut());
|
.prune(&mut core, &mut supervisor.borrow_mut());
|
||||||
return Err(mlua::Error::external(format!(
|
return Err(error);
|
||||||
"pmacs.terminal.open: active-window switch failed: {error}"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
crate::terminal::TerminalViewKey::new(
|
|
||||||
frontend_id,
|
|
||||||
core.views
|
|
||||||
.get(&frontend_id)
|
|
||||||
.expect("checked frontend has active view")
|
|
||||||
.active,
|
|
||||||
buffer_id,
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
let key =
|
||||||
|
crate::terminal::TerminalViewKey::new(frontend_id, outcome.target, buffer_id);
|
||||||
let claimed = {
|
let claimed = {
|
||||||
let mut manager = manager.borrow_mut();
|
let mut manager = manager.borrow_mut();
|
||||||
manager.register_view(key) && manager.claim_controller(key)
|
manager.register_view(key) && manager.claim_controller(key)
|
||||||
};
|
};
|
||||||
if !claimed {
|
if !claimed {
|
||||||
|
// Placement failure removes any side wrapper this
|
||||||
|
// transaction created, BEFORE the existing
|
||||||
|
// session/buffer rollback completes (Q#BP11b).
|
||||||
|
if outcome.created_side {
|
||||||
|
core.borrow_mut()
|
||||||
|
.remove_side_window(frontend_id, outcome.target);
|
||||||
|
}
|
||||||
let mut core = core.borrow_mut();
|
let mut core = core.borrow_mut();
|
||||||
let _ = core.registry.borrow_mut().remove(buffer_id);
|
let _ = core.registry.borrow_mut().remove(buffer_id);
|
||||||
manager
|
manager
|
||||||
|
|
@ -8555,7 +8572,7 @@ fn install_terminal(
|
||||||
"pmacs.terminal.open: failed to claim the new terminal view",
|
"pmacs.terminal.open: failed to claim the new terminal view",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
run_hook_if_defined(lua, "buffer.after-switch", mlua::MultiValue::new());
|
window_panel::finish_adopter_placement(lua, &core, frontend_id, outcome)?;
|
||||||
Ok(BufferIdLua(buffer_id))
|
Ok(BufferIdLua(buffer_id))
|
||||||
})?,
|
})?,
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -8717,6 +8734,10 @@ fn parse_terminal_spec(table: &Table) -> mlua::Result<crate::terminal::TerminalS
|
||||||
"rows",
|
"rows",
|
||||||
"cols",
|
"cols",
|
||||||
"scrollback_rows",
|
"scrollback_rows",
|
||||||
|
// Bottom-panel arc (Q#BP11b): placement, parsed separately by
|
||||||
|
// `_open` and never part of the child's `TerminalSpec`.
|
||||||
|
"display",
|
||||||
|
"window",
|
||||||
];
|
];
|
||||||
let mut unknown = None;
|
let mut unknown = None;
|
||||||
table.clone().for_each(|key: Value, _: Value| {
|
table.clone().for_each(|key: Value, _: Value| {
|
||||||
|
|
@ -12300,9 +12321,7 @@ fn install_window_module(lua: &Lua, core: &SharedCore) -> mlua::Result<Table> {
|
||||||
win.set(
|
win.set(
|
||||||
"close_others",
|
"close_others",
|
||||||
lua.create_function(move |_, ()| {
|
lua.create_function(move |_, ()| {
|
||||||
cc.borrow_mut()
|
cc.borrow_mut().close_others().map_err(mlua::Error::runtime)
|
||||||
.close_others()
|
|
||||||
.map_err(mlua::Error::runtime)
|
|
||||||
})?,
|
})?,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,10 +120,11 @@ fn complete_display(
|
||||||
let target_ok = visible(core, fid, outcome.target);
|
let target_ok = visible(core, fid, outcome.target);
|
||||||
let saved_ok = visible(core, fid, outcome.saved_active);
|
let saved_ok = visible(core, fid, outcome.saved_active);
|
||||||
let final_focus = match (outcome.select, target_ok, saved_ok) {
|
let final_focus = match (outcome.select, target_ok, saved_ok) {
|
||||||
(true, true, _) => Some(outcome.target),
|
// `select = true` KEEPS the target selected.
|
||||||
(true, false, true) => Some(outcome.saved_active),
|
(true, true, _) | (false, true, false) => Some(outcome.target),
|
||||||
(false, _, true) => Some(outcome.saved_active),
|
// `select = false` restores the saved window even when it is the
|
||||||
(false, true, false) => Some(outcome.target),
|
// panel — a passive display from a focused panel must not blur it.
|
||||||
|
(true, false, true) | (false, _, true) => Some(outcome.saved_active),
|
||||||
// Both ids died with the hook: fall back to the non-side target
|
// Both ids died with the hook: fall back to the non-side target
|
||||||
// rule rather than leaving focus on a dead window.
|
// rule rather than leaving focus on a dead window.
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|
@ -202,8 +203,135 @@ fn lookup_window(core: &SharedCore, fid: FrontendId, raw: u64) -> mlua::Result<W
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A parsed adopter placement request (Q#BP11b).
|
||||||
|
///
|
||||||
|
/// `listview`, compile, and terminal all take the same strict
|
||||||
|
/// `display = "current" | "panel"` value. In Stages 1–2 omission means
|
||||||
|
/// `"current"`; Stage 3 flips omission to `"panel"`. Explicit
|
||||||
|
/// `"current"` always preserves the adopter's pre-arc selected-window
|
||||||
|
/// behavior and is the user-facing opt-out from that flip.
|
||||||
|
pub(crate) enum AdopterPlacement {
|
||||||
|
/// Today's behavior: the raw switch into the frontend's active
|
||||||
|
/// window, deliberately bypassing display-policy dedication.
|
||||||
|
Current,
|
||||||
|
/// The bottom panel.
|
||||||
|
Panel,
|
||||||
|
/// An exact target window.
|
||||||
|
Window(WindowId),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse an adopter's placement **before** it creates a buffer, session,
|
||||||
|
/// process, or wrapper — so an unknown value leaves nothing to roll back.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// An unknown `display` value, a `window` combined with
|
||||||
|
/// `display = "panel"`, or a window id that is not live in the acting
|
||||||
|
/// frontend's layout.
|
||||||
|
pub(crate) fn parse_adopter_placement(
|
||||||
|
core: &SharedCore,
|
||||||
|
fid: FrontendId,
|
||||||
|
operation: &str,
|
||||||
|
display: Option<&str>,
|
||||||
|
window: Option<u64>,
|
||||||
|
) -> mlua::Result<AdopterPlacement> {
|
||||||
|
let display = match display {
|
||||||
|
None | Some("current") => AdopterPlacement::Current,
|
||||||
|
Some("panel") => AdopterPlacement::Panel,
|
||||||
|
Some(other) => {
|
||||||
|
return Err(mlua::Error::runtime(format!(
|
||||||
|
"{operation}: unknown display {other:?} (expected \"current\" or \"panel\")"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match (window, &display) {
|
||||||
|
(Some(_), AdopterPlacement::Panel) => Err(mlua::Error::runtime(format!(
|
||||||
|
"{operation}: `window` and `display = \"panel\"` are mutually exclusive"
|
||||||
|
))),
|
||||||
|
(Some(raw), _) => Ok(AdopterPlacement::Window(lookup_window(core, fid, raw)?)),
|
||||||
|
(None, _) => Ok(display),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install `buffer_id` per `placement`, returning Phase 1's outcome
|
||||||
|
/// (Q#BP11b).
|
||||||
|
///
|
||||||
|
/// `Current` keeps the pre-arc raw switch: it is the deliberate escape
|
||||||
|
/// hatch every existing adopter caller already relies on, and it does not
|
||||||
|
/// consult display-policy dedication.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Any placement failure. The caller owns its own session/buffer
|
||||||
|
/// rollback, and inspects `created_side` to remove a wrapper this
|
||||||
|
/// transaction created.
|
||||||
|
pub(crate) fn place_adopter_buffer(
|
||||||
|
lua: &Lua,
|
||||||
|
core: &SharedCore,
|
||||||
|
fid: FrontendId,
|
||||||
|
buffer_id: crate::buffer::BufferId,
|
||||||
|
placement: &AdopterPlacement,
|
||||||
|
select: bool,
|
||||||
|
) -> mlua::Result<DisplayOutcome> {
|
||||||
|
if matches!(placement, AdopterPlacement::Current) {
|
||||||
|
let mut borrowed = core.borrow_mut();
|
||||||
|
borrowed
|
||||||
|
.switch_active_buffer_for(fid, buffer_id)
|
||||||
|
.map_err(mlua::Error::runtime)?;
|
||||||
|
let target = borrowed
|
||||||
|
.views
|
||||||
|
.get(&fid)
|
||||||
|
.map(|view| view.active)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
mlua::Error::runtime("adopter placement: acting frontend has no active window")
|
||||||
|
})?;
|
||||||
|
return Ok(DisplayOutcome {
|
||||||
|
target,
|
||||||
|
saved_active: target,
|
||||||
|
select: true,
|
||||||
|
created_side: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut request = DisplayRequest::new(buffer_id);
|
||||||
|
match placement {
|
||||||
|
AdopterPlacement::Panel => request.side = Some(Side::Bottom),
|
||||||
|
AdopterPlacement::Window(window) => request.window = Some(*window),
|
||||||
|
AdopterPlacement::Current => unreachable!("handled above"),
|
||||||
|
}
|
||||||
|
request.select = Some(select);
|
||||||
|
request.default_panel_rows = config_u32(
|
||||||
|
lua,
|
||||||
|
"window.panel-height",
|
||||||
|
Some(buffer_id),
|
||||||
|
DEFAULT_PANEL_ROWS,
|
||||||
|
)
|
||||||
|
.max(MIN_WINDOW_OUTER_ROWS);
|
||||||
|
core.borrow_mut()
|
||||||
|
.display_buffer(fid, &request)
|
||||||
|
.map_err(mlua::Error::runtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase 2 for an adopter that had to interleave its own work (claiming a
|
||||||
|
/// terminal controller, seating a cursor) between placement and the hook.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Propagates the final-focus resolution error when both window ids died
|
||||||
|
/// inside the hook.
|
||||||
|
pub(crate) fn finish_adopter_placement(
|
||||||
|
lua: &Lua,
|
||||||
|
core: &SharedCore,
|
||||||
|
fid: FrontendId,
|
||||||
|
outcome: DisplayOutcome,
|
||||||
|
) -> mlua::Result<()> {
|
||||||
|
complete_display(lua, core, fid, outcome, HookKind::AfterSwitch)
|
||||||
|
}
|
||||||
|
|
||||||
/// Install the bottom-panel surface onto the existing `pmacs.window`
|
/// Install the bottom-panel surface onto the existing `pmacs.window`
|
||||||
/// table.
|
/// table.
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_lines,
|
||||||
|
reason = "one flat list of bindings, each following the same \
|
||||||
|
acting-frontend / Rc-borrow shape; splitting them fragments \
|
||||||
|
a coherent surface"
|
||||||
|
)]
|
||||||
pub(crate) fn install(lua: &Lua, core: &SharedCore, win: &Table) -> mlua::Result<()> {
|
pub(crate) fn install(lua: &Lua, core: &SharedCore, win: &Table) -> mlua::Result<()> {
|
||||||
{
|
{
|
||||||
let cc = core.clone();
|
let cc = core.clone();
|
||||||
|
|
@ -386,10 +514,7 @@ pub(crate) fn install(lua: &Lua, core: &SharedCore, win: &Table) -> mlua::Result
|
||||||
.quit_action()
|
.quit_action()
|
||||||
.map_or(0, crate::window::QuitAction::depth),
|
.map_or(0, crate::window::QuitAction::depth),
|
||||||
)?;
|
)?;
|
||||||
table.set(
|
table.set("hidden", window.is_side() && core.panel_hidden_for(fid))?;
|
||||||
"hidden",
|
|
||||||
window.is_side() && core.panel_hidden_for(fid),
|
|
||||||
)?;
|
|
||||||
Ok(table)
|
Ok(table)
|
||||||
})?,
|
})?,
|
||||||
)?;
|
)?;
|
||||||
|
|
@ -400,41 +525,43 @@ pub(crate) fn install(lua: &Lua, core: &SharedCore, win: &Table) -> mlua::Result
|
||||||
let cc = core.clone();
|
let cc = core.clone();
|
||||||
win.set(
|
win.set(
|
||||||
"set_params",
|
"set_params",
|
||||||
lua.create_function(move |lua, (target, opts): (u64, Table)| -> mlua::Result<()> {
|
lua.create_function(
|
||||||
let fid = acting_frontend(lua, &cc);
|
move |lua, (target, opts): (u64, Table)| -> mlua::Result<()> {
|
||||||
let id = lookup_window(&cc, fid, target)?;
|
let fid = acting_frontend(lua, &cc);
|
||||||
for key in ["side", "origin_document", "quit_action"] {
|
let id = lookup_window(&cc, fid, target)?;
|
||||||
if opts.get::<Value>(key)? != Value::Nil {
|
for key in ["side", "origin_document", "quit_action"] {
|
||||||
return Err(mlua::Error::runtime(format!(
|
if opts.get::<Value>(key)? != Value::Nil {
|
||||||
"pmacs.window.set_params: `{key}` is not settable"
|
return Err(mlua::Error::runtime(format!(
|
||||||
)));
|
"pmacs.window.set_params: `{key}` is not settable"
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
let height = match opts.get::<Option<u32>>("fixed_rows")? {
|
||||||
let height = match opts.get::<Option<u32>>("fixed_rows")? {
|
Some(rows) => Some(
|
||||||
Some(rows) => Some(
|
crate::editor_core::EditorCore::clamp_panel_rows(rows)
|
||||||
crate::editor_core::EditorCore::clamp_panel_rows(rows)
|
.map_err(mlua::Error::runtime)?,
|
||||||
.map_err(mlua::Error::runtime)?,
|
),
|
||||||
),
|
None => None,
|
||||||
None => None,
|
};
|
||||||
};
|
let dedicated = opts.get::<Option<bool>>("dedicated")?;
|
||||||
let dedicated = opts.get::<Option<bool>>("dedicated")?;
|
{
|
||||||
{
|
let mut core = cc.borrow_mut();
|
||||||
let mut core = cc.borrow_mut();
|
let window = core.windows.get_mut(&id).ok_or_else(|| {
|
||||||
let window = core.windows.get_mut(&id).ok_or_else(|| {
|
mlua::Error::runtime("pmacs.window.set_params: window not live")
|
||||||
mlua::Error::runtime("pmacs.window.set_params: window not live")
|
})?;
|
||||||
})?;
|
if let Some(rows) = height {
|
||||||
if let Some(rows) = height {
|
// Inert on an ordinary window by construction:
|
||||||
// Inert on an ordinary window by construction:
|
// the fixed map is built from side windows only.
|
||||||
// the fixed map is built from side windows only.
|
window.params.fixed_rows = Some(rows);
|
||||||
window.params.fixed_rows = Some(rows);
|
}
|
||||||
|
if let Some(dedicated) = dedicated {
|
||||||
|
window.params.dedicated = dedicated;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(dedicated) = dedicated {
|
reconcile_panel_layout(lua, &cc, fid);
|
||||||
window.params.dedicated = dedicated;
|
Ok(())
|
||||||
}
|
},
|
||||||
}
|
)?,
|
||||||
reconcile_panel_layout(lua, &cc, fid);
|
|
||||||
Ok(())
|
|
||||||
})?,
|
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -466,8 +593,7 @@ pub(crate) fn install(lua: &Lua, core: &SharedCore, win: &Table) -> mlua::Result
|
||||||
.iter_ids()
|
.iter_ids()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|id| {
|
.map(|id| {
|
||||||
let buffer_id =
|
let buffer_id = core.windows.get(&id).map(|w| w.buffer_id);
|
||||||
core.windows.get(&id).map(|w| w.buffer_id);
|
|
||||||
(
|
(
|
||||||
id,
|
id,
|
||||||
config_u32(
|
config_u32(
|
||||||
|
|
|
||||||
|
|
@ -975,20 +975,21 @@ fn compute_node(
|
||||||
let mut flexible_used: u32 = 0;
|
let mut flexible_used: u32 = 0;
|
||||||
let mut cursor: u32 = 0;
|
let mut cursor: u32 = 0;
|
||||||
for (i, child) in children.iter().enumerate() {
|
for (i, child) in children.iter().enumerate() {
|
||||||
let extent = match extents[i] {
|
let extent = if let Some(rows) = extents[i] {
|
||||||
Some(rows) => rows,
|
rows
|
||||||
None => {
|
} else {
|
||||||
let w = weights.get(i).copied().unwrap_or(1).max(1);
|
let w = weights.get(i).copied().unwrap_or(1).max(1);
|
||||||
let e = if Some(i) == last_flexible {
|
let e = if Some(i) == last_flexible {
|
||||||
remainder - flexible_used
|
remainder - flexible_used
|
||||||
} else if total == 0 {
|
} else {
|
||||||
0
|
remainder
|
||||||
} else {
|
.checked_mul(w)
|
||||||
remainder * w / total
|
.unwrap_or(remainder)
|
||||||
};
|
.checked_div(total)
|
||||||
flexible_used += e;
|
.unwrap_or(0)
|
||||||
e
|
};
|
||||||
}
|
flexible_used += e;
|
||||||
|
e
|
||||||
};
|
};
|
||||||
let child_area = match orientation {
|
let child_area = match orientation {
|
||||||
Orientation::Horizontal => Rect {
|
Orientation::Horizontal => Rect {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue