feat(panels): listview module, Q#P6 round-trip seam, references panel
Arc 1b phase 1 (framing: docs/lsp-panels-framing.md).
Q#P6 (the one Rust change): EditorCore.round_trip_buffers +
pmacs.buffer.set_round_trip_input(buf, on); dispatch_idle() reports
false while a marked buffer is active, so semantic frontends'
optimistic-apply stays off -- RET reaches a panel's buffer-local visit
binding instead of locally inserting a newline, and typing dispatches
into the edit path where the read-only intercept rejects it (a CRDT
import would bypass the intercept chain entirely). Pruned on kill.
Q#P1/P2/P3: builtin/runtime/listview.lua generalizes the *buffer-list*
idiom -- pmacs.listview.open{name, header, rows, on_visit, on_refresh}
owns ensure-buffer (recreates if user-killed), wholesale render with
bypass_intercept, line->item map, buffer-local RET/SPC/n/p/g/q keymap,
previous-buffer capture + q restore (never another panel; scratch
fallback), cursor re-seat after render, the read-only intercept, and
the Q#P6 mark. Panels are buffers: both frontends render them with
zero protocol change.
Q#P4: lsp.find-references (M-?) opens *references* -- one row per
location, paths shortened against the project root, RET visits via the
shared SP-4 template (jump ring, find_or_open, cursor walk; extracted
as visit_location for the phase-2 outline to reuse).
Acceptance: tests/listview_acceptance.rs -- open/seat/visit, header
non-visitable, q restore, read-only rejection, dispatch_idle gate,
refresh re-render + re-seat.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d1a7683c2b
commit
b25b47334d
|
|
@ -0,0 +1,169 @@
|
|||
-- listview.lua --- reusable read-only list panels (Arc 1b, Q#P1).
|
||||
--
|
||||
-- Generalizes the *buffer-list* idiom (builtin/commands/default.lua)
|
||||
-- into `pmacs.listview.open{...}`: a persistent named buffer,
|
||||
-- wholesale re-render, buffer-local RET/n/p/g/q keymap, a
|
||||
-- line->item map, previous-buffer capture + `q` restore, and the two
|
||||
-- disciplines the hand-rolled original lacks --- a read-only
|
||||
-- intercept (Q#P3; the panel's own renders write with
|
||||
-- bypass_intercept) and the Q#P6 round-trip-input mark, so a
|
||||
-- semantic frontend's RET dispatches into the visit binding instead
|
||||
-- of optimistically inserting a newline.
|
||||
--
|
||||
-- Panels are buffers, so both frontends render them with zero
|
||||
-- protocol change (Q#P2: switch-in-place; the GPU cannot show
|
||||
-- splits). Framing: docs/lsp-panels-framing.md.
|
||||
--
|
||||
-- pmacs.listview.open {
|
||||
-- name = "*references*",
|
||||
-- header = "12 references RET visit n/p move g refresh q quit",
|
||||
-- rows = { { text = "src/foo.rs:12:4", item = <any> }, ... },
|
||||
-- on_visit = function(item) ... end, -- RET/SPC (optional)
|
||||
-- on_refresh = function() return rows end, -- g (optional)
|
||||
-- }
|
||||
|
||||
pmacs.listview = pmacs.listview or {}
|
||||
|
||||
-- name -> { buffer, prev, header, line_to_item, on_visit, on_refresh }
|
||||
local panels = {}
|
||||
|
||||
local function find_buffer_by_name(name)
|
||||
for _, id in ipairs(pmacs.buffer.list()) do
|
||||
local ok, d = pcall(pmacs.describe.buffer, id)
|
||||
if ok and d and d.name == name then return id end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- The panel record whose buffer the active window shows, or nil.
|
||||
local function panel_for_current_buffer()
|
||||
local buf = pmacs.window.buffer()
|
||||
if not buf then return nil end
|
||||
local ok, d = pcall(pmacs.describe.buffer, buf)
|
||||
if not (ok and d) then return nil end
|
||||
return panels[d.name]
|
||||
end
|
||||
|
||||
-- Wholesale re-render: header + one line per row, rebuilding the
|
||||
-- line->item map (data lines are 1-based; the header is line 0).
|
||||
-- Panel writes bypass the read-only intercept.
|
||||
local function render(p, rows)
|
||||
local lines = { p.header }
|
||||
p.line_to_item = {}
|
||||
for _, row in ipairs(rows) do
|
||||
lines[#lines + 1] = row.text
|
||||
p.line_to_item[#lines - 1] = row.item
|
||||
end
|
||||
local body = table.concat(lines, "\n")
|
||||
local buf = p.buffer
|
||||
local len = buf:len()
|
||||
if len > 0 then buf:delete(0, len, { bypass_intercept = true }) end
|
||||
if #body > 0 then buf:insert(0, body, { bypass_intercept = true }) end
|
||||
end
|
||||
|
||||
-- Re-seat the cursor on data line `line` (1-based, clamped).
|
||||
-- `switch_active_buffer` zeroes the window cursor, so a fresh switch
|
||||
-- puts us on the header; walk down from there.
|
||||
local function seat_cursor(p, line)
|
||||
local count = #p.line_to_item
|
||||
if count == 0 then return end
|
||||
local target = math.max(1, math.min(line or 1, count))
|
||||
for _ = 1, target do
|
||||
pmacs.editor.move_down()
|
||||
end
|
||||
end
|
||||
|
||||
local function bind_local_keymap(buf)
|
||||
local function bind(seq, command)
|
||||
pmacs.keymap.bind { scope = "buffer", buffer = buf, sequence = seq, command = command }
|
||||
end
|
||||
bind("RET", "listview.visit")
|
||||
bind("SPC", "listview.visit")
|
||||
bind("n", "cursor.down")
|
||||
bind("<down>", "cursor.down")
|
||||
bind("p", "cursor.up")
|
||||
bind("<up>", "cursor.up")
|
||||
bind("g", "listview.refresh")
|
||||
bind("q", "listview.quit")
|
||||
end
|
||||
|
||||
-- Build (or adopt) the persistent panel record for `name`. Handles a
|
||||
-- user-killed panel buffer by recreating it.
|
||||
local function ensure_panel(name)
|
||||
local p = panels[name]
|
||||
if p and p.buffer:is_valid() then return p end
|
||||
local buf = find_buffer_by_name(name) or pmacs.buffer.create(name)
|
||||
p = { buffer = buf, line_to_item = {} }
|
||||
panels[name] = p
|
||||
-- Read-only (Q#P3): every non-bypass edit is rejected. The
|
||||
-- intercept lives as long as the buffer; no teardown (the
|
||||
-- buffer-list precedent for its keymap).
|
||||
pmacs.buffer.add_intercept(buf, function()
|
||||
error(name .. " is read-only")
|
||||
end)
|
||||
-- Q#P6: semantic frontends must round-trip keys while this panel
|
||||
-- is focused (RET = visit, not an optimistic newline).
|
||||
pmacs.buffer.set_round_trip_input(buf, true)
|
||||
bind_local_keymap(buf)
|
||||
return p
|
||||
end
|
||||
|
||||
function pmacs.listview.open(spec)
|
||||
assert(type(spec) == "table" and type(spec.name) == "string",
|
||||
"listview.open: spec.name (string) required")
|
||||
local p = ensure_panel(spec.name)
|
||||
p.header = spec.header or spec.name
|
||||
p.on_visit = spec.on_visit
|
||||
p.on_refresh = spec.on_refresh
|
||||
-- Remember where to return on `q` --- but never another panel
|
||||
-- (chained panels would trap `q` in a loop; restore targets the
|
||||
-- last real buffer).
|
||||
local active = pmacs.window.buffer()
|
||||
if active and not panel_for_current_buffer() then
|
||||
p.prev = active
|
||||
end
|
||||
render(p, spec.rows or {})
|
||||
pmacs.window.switch_buffer(p.buffer)
|
||||
seat_cursor(p, 1)
|
||||
end
|
||||
|
||||
pmacs.command.define {
|
||||
name = "listview.visit",
|
||||
description = "Visit the list-panel item under the cursor.",
|
||||
fn = function()
|
||||
local p = panel_for_current_buffer()
|
||||
if not p then return end
|
||||
local item = p.line_to_item[pmacs.editor.cursor_line()]
|
||||
if item ~= nil and p.on_visit then p.on_visit(item) end
|
||||
end,
|
||||
}
|
||||
|
||||
pmacs.command.define {
|
||||
name = "listview.refresh",
|
||||
description = "Re-run the list panel's data source and re-render.",
|
||||
fn = function()
|
||||
local p = panel_for_current_buffer()
|
||||
if not (p and p.on_refresh) then return end
|
||||
local saved = pmacs.editor.cursor_line()
|
||||
local rows = p.on_refresh() or {}
|
||||
render(p, rows)
|
||||
-- The wholesale rewrite leaves the window cursor at a stale byte
|
||||
-- offset; re-enter the buffer to reset, then re-seat.
|
||||
pmacs.window.switch_buffer(p.buffer)
|
||||
seat_cursor(p, saved)
|
||||
end,
|
||||
}
|
||||
|
||||
pmacs.command.define {
|
||||
name = "listview.quit",
|
||||
description = "Leave the list panel, restoring the previous buffer.",
|
||||
fn = function()
|
||||
local p = panel_for_current_buffer()
|
||||
if not p then return end
|
||||
local target = p.prev
|
||||
if not (target and target:is_valid()) then
|
||||
target = find_buffer_by_name("*scratch*") or pmacs.buffer.create("*scratch*")
|
||||
end
|
||||
pmacs.window.switch_buffer(target)
|
||||
end,
|
||||
}
|
||||
|
|
@ -1214,6 +1214,41 @@ function pmacs.lsp.go_to_definition()
|
|||
end)
|
||||
end
|
||||
|
||||
-- Visit one LSP location (Arc 1b): the SP-4 cross-file template ---
|
||||
-- jump ring, find-or-open, cursor walk. Same-buffer hits skip the
|
||||
-- open. Shared by the references panel (and the outline in phase 2).
|
||||
local function visit_location(loc)
|
||||
local path = pmacs.lsp.path_for_uri(loc.uri)
|
||||
if not path then
|
||||
pmacs.editor.set_status("LSP: cannot decode target uri " .. tostring(loc.uri))
|
||||
return
|
||||
end
|
||||
pmacs.editor.push_jump()
|
||||
local ok, err = pcall(pmacs.buffer.find_or_open, path)
|
||||
if not ok then
|
||||
-- Open failed: drop the origin we just pushed so M-, isn't left
|
||||
-- pointing at a jump that never happened.
|
||||
pmacs.editor.jump_back()
|
||||
pmacs.editor.set_status("LSP: failed to open " .. path .. ": " .. tostring(err))
|
||||
return
|
||||
end
|
||||
move_active_cursor_to(loc.line, loc.col)
|
||||
end
|
||||
|
||||
-- Shorten `path` against the project root of `relative_to` (a path in
|
||||
-- the same project) for panel display; falls back to the full path.
|
||||
local function display_path(path, relative_to)
|
||||
local ok, proj = pcall(pmacs.project.detect, relative_to or path)
|
||||
if ok and proj and proj.root then
|
||||
local root = proj.root
|
||||
if root:sub(-1) ~= "/" then root = root .. "/" end
|
||||
if path:sub(1, #root) == root then
|
||||
return path:sub(#root + 1)
|
||||
end
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
function pmacs.lsp.find_references()
|
||||
local rec = attached_for_active()
|
||||
if not rec then
|
||||
|
|
@ -1236,13 +1271,27 @@ function pmacs.lsp.find_references()
|
|||
pmacs.editor.set_status("LSP: no references found")
|
||||
return
|
||||
end
|
||||
-- v1 surfaces a modeline summary (count + first hit); a
|
||||
-- references list buffer is future UX work, like the hover panel.
|
||||
local first = locs[1]
|
||||
-- Arc 1b: a browsable *references* panel. RET visits (jump ring
|
||||
-- included, so M-, returns); q restores this buffer.
|
||||
local here = pmacs.lsp.path_for_uri(rec.uri)
|
||||
local rows = {}
|
||||
for _, loc in ipairs(locs) do
|
||||
local path = pmacs.lsp.path_for_uri(loc.uri) or loc.uri
|
||||
rows[#rows + 1] = {
|
||||
text = string.format("%s:%d:%d", display_path(path, here), loc.line + 1, loc.col + 1),
|
||||
item = loc,
|
||||
}
|
||||
end
|
||||
pmacs.listview.open {
|
||||
name = "*references*",
|
||||
header = string.format(
|
||||
"%d reference%s RET visit n/p move q quit",
|
||||
#locs, (#locs == 1 and "" or "s")),
|
||||
rows = rows,
|
||||
on_visit = visit_location,
|
||||
}
|
||||
pmacs.editor.set_status(string.format(
|
||||
"LSP: %d reference%s; first at %s:%d:%d",
|
||||
#locs, (#locs == 1 and "" or "s"),
|
||||
first.uri, first.line + 1, first.col + 1))
|
||||
"LSP: %d reference%s", #locs, (#locs == 1 and "" or "s")))
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -276,6 +276,15 @@ impl EditorState {
|
|||
// auto-attach buffer hooks, key-bound commands. Loaded last so
|
||||
// every dependency table (`pmacs.lsp`, `pmacs.parse`,
|
||||
// `pmacs.window`, etc.) already exists.
|
||||
// Arc 1b: the reusable list-panel module. Loaded before
|
||||
// lsp.lua, whose panel commands (references, outline) call
|
||||
// `pmacs.listview.open`.
|
||||
lua_host
|
||||
.eval(
|
||||
Some("@pmacs/builtin/runtime/listview.lua"),
|
||||
include_str!("../builtin/runtime/listview.lua"),
|
||||
)
|
||||
.expect("load listview builtin chunk");
|
||||
lua_host
|
||||
.eval(
|
||||
Some("@pmacs/builtin/runtime/lsp.lua"),
|
||||
|
|
@ -508,8 +517,15 @@ impl EditorState {
|
|||
let core = self.core.borrow();
|
||||
// A live context menu shadows the keymap too (Q#CM1): keys must
|
||||
// round-trip so the daemon's `dispatch_menu_key` drives the menu
|
||||
// rather than the frontend self-inserting.
|
||||
!core.minibuffer.is_active() && !core.search_active() && !core.menu_is_open()
|
||||
// rather than the frontend self-inserting. A round-trip buffer
|
||||
// (Arc 1b Q#P6 — a focused panel) is the buffer-shaped member of
|
||||
// the same family: RET must reach its buffer-local bindings and
|
||||
// typing must reach its read-only intercept, neither of which an
|
||||
// optimistic local edit would do.
|
||||
!core.minibuffer.is_active()
|
||||
&& !core.search_active()
|
||||
&& !core.menu_is_open()
|
||||
&& !core.active_buffer_round_trips()
|
||||
}
|
||||
|
||||
/// `frontend_id` records which frontend produced the event. v0.1
|
||||
|
|
|
|||
|
|
@ -193,6 +193,16 @@ pub struct EditorCore {
|
|||
/// same state the dispatch path navigates and the Lua driver
|
||||
/// publishes into — the completion twin of `menu`.
|
||||
pub completion_popup: crate::completion::SharedCompletionPopup,
|
||||
/// Buffers whose input must round-trip (Arc 1b, Q#P6). While one
|
||||
/// of these is the active buffer,
|
||||
/// [`crate::editor::EditorState::dispatch_idle`] reports `false`,
|
||||
/// so semantic frontends' optimistic-apply stays off: RET reaches
|
||||
/// buffer-local bindings (a panel's visit) instead of locally
|
||||
/// inserting `\n`, and plain typing dispatches into the edit path
|
||||
/// where a read-only intercept can reject it — a CRDT-import
|
||||
/// write would bypass the intercept chain entirely. Marked from
|
||||
/// Lua via `pmacs.buffer.set_round_trip_input`; pruned on kill.
|
||||
round_trip_buffers: std::collections::HashSet<BufferId>,
|
||||
}
|
||||
|
||||
impl EditorCore {
|
||||
|
|
@ -233,6 +243,7 @@ impl EditorCore {
|
|||
pending_clipboard: None,
|
||||
menu: crate::menu::make_shared_menu(),
|
||||
completion_popup: crate::completion::make_shared_popup(),
|
||||
round_trip_buffers: std::collections::HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1980,6 +1991,25 @@ impl EditorCore {
|
|||
true
|
||||
}
|
||||
|
||||
// ---- round-trip input buffers (Arc 1b, Q#P6) ----------------------------
|
||||
|
||||
/// Mark (or unmark) `buffer_id` as requiring round-trip input.
|
||||
/// See the field doc on `round_trip_buffers` for the semantics.
|
||||
pub fn set_round_trip_input(&mut self, buffer_id: BufferId, on: bool) {
|
||||
if on {
|
||||
self.round_trip_buffers.insert(buffer_id);
|
||||
} else {
|
||||
self.round_trip_buffers.remove(&buffer_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// True while the active buffer requires round-trip input (a
|
||||
/// panel or other buffer-local-keymap surface is focused).
|
||||
#[must_use]
|
||||
pub fn active_buffer_round_trips(&self) -> bool {
|
||||
self.round_trip_buffers.contains(&self.active_buffer_id())
|
||||
}
|
||||
|
||||
/// Ensure the active window carries a
|
||||
/// [`crate::completion::CompletionView`] overlay (deduped by kind).
|
||||
/// The view reads the shared popup, so one instance suffices; it
|
||||
|
|
@ -2012,6 +2042,7 @@ impl EditorCore {
|
|||
return Err("cannot kill the last remaining buffer".into());
|
||||
}
|
||||
}
|
||||
self.round_trip_buffers.remove(&buffer_id);
|
||||
let fallback = {
|
||||
let mut reg = self.registry.borrow_mut();
|
||||
match reg.find_by_name("*scratch*") {
|
||||
|
|
|
|||
|
|
@ -2366,6 +2366,25 @@ fn install_buffer_module(lua: &Lua, registry: &SharedRegistry) -> mlua::Result<T
|
|||
)?;
|
||||
}
|
||||
|
||||
{
|
||||
// Arc 1b Q#P6: mark (or unmark) a buffer as requiring
|
||||
// round-trip input. While a marked buffer is active,
|
||||
// `dispatch_idle` reports false, so semantic frontends'
|
||||
// optimistic-apply stays off — RET reaches the buffer-local
|
||||
// bindings (a panel's visit) and typing reaches the read-only
|
||||
// intercept instead of landing as a CRDT import that bypasses
|
||||
// it. `pmacs.listview` marks every panel it creates.
|
||||
buffer.set(
|
||||
"set_round_trip_input",
|
||||
lua.create_function(move |lua, (id, on): (BufferIdLua, bool)| {
|
||||
if let Some(core) = lua.app_data_ref::<SharedCore>() {
|
||||
core.borrow_mut().set_round_trip_input(id.0, on);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
|
||||
{
|
||||
// T M4.5 L1: find-or-open. If a buffer is already bound to
|
||||
// `path`, switch to it (preserving unsaved edits — no
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
//! List-panel acceptance (Arc 1b phase 1) --- the `pmacs.listview`
|
||||
//! module end-to-end through `dispatch_key`: open/navigate/visit,
|
||||
//! `q` restore, the Q#P3 read-only intercept, the Q#P6 round-trip
|
||||
//! gate (`dispatch_idle` false while a panel is focused), and
|
||||
//! refresh. The references panel itself needs a live LSP and is
|
||||
//! validated manually / via the m4 harness; these tests drive the
|
||||
//! substrate hermetically.
|
||||
//!
|
||||
//! Framing: docs/lsp-panels-framing.md.
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
|
||||
use pmacs::editor::EditorState;
|
||||
use pmacs::protocol::FrontendId;
|
||||
|
||||
fn key(code: KeyCode, mods: KeyModifiers) -> KeyEvent {
|
||||
KeyEvent {
|
||||
code,
|
||||
modifiers: mods,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn press(s: &mut EditorState, code: KeyCode) {
|
||||
s.dispatch_key(FrontendId::LOCAL, key(code, KeyModifiers::NONE));
|
||||
}
|
||||
|
||||
/// Open a three-row test panel whose visits record into `_G.VISITED`.
|
||||
fn open_test_panel(s: &mut EditorState) {
|
||||
s.lua_host
|
||||
.lua()
|
||||
.load(
|
||||
r#"
|
||||
_G.VISITED = nil
|
||||
pmacs.listview.open {
|
||||
name = "*test-panel*",
|
||||
header = "3 items RET visit q quit",
|
||||
rows = {
|
||||
{ text = "alpha", item = "A" },
|
||||
{ text = "beta", item = "B" },
|
||||
{ text = "gamma", item = "C" },
|
||||
},
|
||||
on_visit = function(item) _G.VISITED = item end,
|
||||
on_refresh = function()
|
||||
return { { text = "delta", item = "D" } }
|
||||
end,
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.exec()
|
||||
.expect("open test panel");
|
||||
}
|
||||
|
||||
/// `(active buffer name, buffer text, cursor line, visited)` probed
|
||||
/// through the Lua surface.
|
||||
fn probe(s: &EditorState) -> (String, String, i64, Option<String>) {
|
||||
s.lua_host
|
||||
.lua()
|
||||
.load(
|
||||
r"
|
||||
local b = pmacs.window.buffer()
|
||||
local d = pmacs.describe.buffer(b)
|
||||
return d.name, b:slice(0, b:len()), pmacs.editor.cursor_line(), _G.VISITED
|
||||
",
|
||||
)
|
||||
.eval()
|
||||
.expect("probe panel state")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_seats_cursor_and_ret_visits_the_row() {
|
||||
let mut s = EditorState::new();
|
||||
open_test_panel(&mut s);
|
||||
let (name, text, line, _) = probe(&s);
|
||||
assert_eq!(name, "*test-panel*");
|
||||
assert!(text.starts_with("3 items"), "header renders first");
|
||||
assert_eq!(line, 1, "the cursor opens on the first data row");
|
||||
|
||||
press(&mut s, KeyCode::Char('n')); // buffer-local: cursor.down
|
||||
press(&mut s, KeyCode::Enter);
|
||||
let (_, _, _, visited) = probe(&s);
|
||||
assert_eq!(visited.as_deref(), Some("B"), "RET visits the second row");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_row_is_not_visitable() {
|
||||
let mut s = EditorState::new();
|
||||
open_test_panel(&mut s);
|
||||
press(&mut s, KeyCode::Char('p')); // up onto the header
|
||||
press(&mut s, KeyCode::Enter);
|
||||
let (_, _, _, visited) = probe(&s);
|
||||
assert_eq!(visited, None, "the header maps to no item");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn q_restores_the_previous_buffer() {
|
||||
let mut s = EditorState::new();
|
||||
open_test_panel(&mut s);
|
||||
press(&mut s, KeyCode::Char('q'));
|
||||
let (name, _, _, _) = probe(&s);
|
||||
assert_eq!(name, "*scratch*", "q returns to the buffer we came from");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_rejects_typing() {
|
||||
let mut s = EditorState::new();
|
||||
open_test_panel(&mut s);
|
||||
let (_, before, _, _) = probe(&s);
|
||||
press(&mut s, KeyCode::Char('z')); // unbound printable → self-insert → intercept rejects
|
||||
let (_, after, _, _) = probe(&s);
|
||||
assert_eq!(before, after, "the read-only intercept rejects self-insert");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_idle_is_false_while_a_panel_is_focused() {
|
||||
// Q#P6: while the panel is the active buffer, semantic frontends
|
||||
// must round-trip every key (RET = visit, not an optimistic \n).
|
||||
let mut s = EditorState::new();
|
||||
assert!(s.dispatch_idle(), "scratch buffer: idle");
|
||||
open_test_panel(&mut s);
|
||||
assert!(!s.dispatch_idle(), "panel focused: keys must round-trip");
|
||||
press(&mut s, KeyCode::Char('q'));
|
||||
assert!(s.dispatch_idle(), "restored buffer: idle again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_reruns_the_source_and_reseats() {
|
||||
let mut s = EditorState::new();
|
||||
open_test_panel(&mut s);
|
||||
press(&mut s, KeyCode::Char('g'));
|
||||
let (_, text, line, _) = probe(&s);
|
||||
assert!(text.contains("delta"), "g re-renders from on_refresh");
|
||||
assert!(!text.contains("alpha"), "old rows are gone");
|
||||
assert_eq!(line, 1, "cursor re-seats on a data row after refresh");
|
||||
press(&mut s, KeyCode::Enter);
|
||||
let (_, _, _, visited) = probe(&s);
|
||||
assert_eq!(visited.as_deref(), Some("D"), "the refreshed row visits");
|
||||
}
|
||||
Loading…
Reference in New Issue