B1 fix — align semantic frontend's window to its displayed buffer
Picks up the "arrow keys do nothing in the GUI" investigation. Root
cause is the multi-buffer mismatch the manual investigation theorized,
now confirmed in code and tested:
- `build_fresh_frontend_view` binds an attaching frontend's window to
LOCAL's active buffer (a scratch the TUI never switched LOCAL away
from).
- `send_buffer_snapshots` ships a snapshot per buffer in registry
order; pmacs-gpu treats each as "switch visible buffer", so its
`current_buffer_id` (and what it displays) becomes the LAST one — the
file the TUI opened.
- So the GUI displays the file, but its daemon-side window edits the
scratch. Arrow keys → `dispatch_key` → move the scratch cursor →
`CursorByte { buffer_id: scratch }` → pmacs-gpu ignores it (its
`current_buffer_id` is the file). The caret never tracks.
Fix: the `Viewport` event already declares which buffer the frontend
is displaying. The daemon now calls `align_semantic_window_to_buffer`
on it — re-pointing the semantic frontend's window at the declared
buffer (rebuild the cheap `TextView` line index, reset cursor; a
semantic frontend has no grid overlays to migrate, it renders from the
wire). Input and the `CursorByte` it produces then target the buffer
the user is actually looking at. The guard makes it a no-op when the
buffer is unchanged (so per-edit Viewport re-declarations don't reset
the cursor).
Tests:
- `viewport_aligns_semantic_window_to_displayed_buffer` — window
starts on scratch, declares the file via align, a key then
self-inserts into the *file*.
- `semantic_frontend_key_event_reaches_the_core` (from the prior
commit) still green.
Also adds `PMACS_GPU_DEBUG_INPUT=1`: logs keys sent and each
`CursorByte` with `buf`/`current`/`match` so the displayed-vs-edited
buffer alignment is visible at a glance on retest.
Gates green: fmt; clippy --all-targets --workspace -D warnings
(default + crdt); pmacs lib 1334; crdt daemon tests 7; pmacs-gpu unit
18; m4_acceptance 88; m11_5_semantic_acceptance 2.
Still needs visual confirmation (arrow keys move the caret in a
running pmacs-gpu) before merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
98ef140a84
commit
4cd968bc0c
|
|
@ -378,9 +378,13 @@ impl ApplicationHandler<AppEvent> for App {
|
|||
if let Some((pkey, pmods)) = translate_key(&key.logical_key, self.modifiers)
|
||||
&& is_motion_key(pkey)
|
||||
&& let Some(client) = self.attach_client.as_ref()
|
||||
&& let Err(e) = client.send_key(pkey, pmods)
|
||||
{
|
||||
eprintln!("pmacs-gpu: send_key failed: {e}");
|
||||
if debug_input() {
|
||||
eprintln!("pmacs-gpu send_key: {pkey:?} mods={pmods:?}");
|
||||
}
|
||||
if let Err(e) = client.send_key(pkey, pmods) {
|
||||
eprintln!("pmacs-gpu: send_key failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowEvent::Resized(size) => {
|
||||
|
|
@ -846,6 +850,14 @@ impl State {
|
|||
buffer_id,
|
||||
byte_pos,
|
||||
} => {
|
||||
if debug_input() {
|
||||
eprintln!(
|
||||
"pmacs-gpu cursor: buf={buffer_id:?} byte={byte_pos} \
|
||||
current={:?} match={}",
|
||||
self.current_buffer_id,
|
||||
self.current_buffer_id == Some(buffer_id)
|
||||
);
|
||||
}
|
||||
self.own_cursor = Some(OwnCursor {
|
||||
buffer_id,
|
||||
byte: byte_pos,
|
||||
|
|
@ -1711,6 +1723,17 @@ fn debug_frame() -> bool {
|
|||
*FLAG.get_or_init(|| std::env::var_os("PMACS_GPU_DEBUG_FRAME").is_some())
|
||||
}
|
||||
|
||||
/// One-shot env flag: `PMACS_GPU_DEBUG_INPUT=1` logs the input path —
|
||||
/// keys sent and `CursorByte` received (with the buffer it targets vs
|
||||
/// the buffer being displayed). The buffer comparison is the B1
|
||||
/// diagnostic: if `CursorByte` targets a different buffer than
|
||||
/// `current`, the caret won't track (the displayed/edited buffers are
|
||||
/// out of sync).
|
||||
fn debug_input() -> bool {
|
||||
static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*FLAG.get_or_init(|| std::env::var_os("PMACS_GPU_DEBUG_INPUT").is_some())
|
||||
}
|
||||
|
||||
/// Translate a winit logical key + current modifier state into a
|
||||
/// protocol `(Key, Modifiers)`. Returns `None` for keys the protocol
|
||||
/// has no representation for (the daemon ignores `Key::Unknown`, so
|
||||
|
|
|
|||
132
src/daemon.rs
132
src/daemon.rs
|
|
@ -1246,6 +1246,7 @@ fn handle_session_established(
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_lines)] // per-variant dispatcher match.
|
||||
fn handle_dispatcher_event(
|
||||
event: DispatcherEvent,
|
||||
editor: &mut EditorState,
|
||||
|
|
@ -1342,8 +1343,20 @@ fn handle_dispatcher_event(
|
|||
// session never sends this; if one does, there is
|
||||
// no `SemanticRenderState` to update and it is a
|
||||
// benign no-op.
|
||||
if let Some(sem) = semantic_states.get_mut(&source) {
|
||||
sem.set_viewport(buffer_id, visible, generation);
|
||||
if semantic_states.contains_key(&source) {
|
||||
// Phase B (B1) — the Viewport declares *which
|
||||
// buffer this frontend is displaying*. Align its
|
||||
// editor window to that buffer so keyboard input
|
||||
// (`dispatch_key`) and the `CursorByte` it emits
|
||||
// target the displayed buffer. Without this, a
|
||||
// semantic frontend's window stays bound to
|
||||
// LOCAL's attach-time buffer (often a scratch the
|
||||
// user isn't viewing), so arrow keys moved an
|
||||
// off-screen cursor and the caret never tracked.
|
||||
align_semantic_window_to_buffer(editor, source, buffer_id);
|
||||
if let Some(sem) = semantic_states.get_mut(&source) {
|
||||
sem.set_viewport(buffer_id, visible, generation);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
|
@ -1843,6 +1856,53 @@ fn handle_remote_crdt_op(
|
|||
/// `pmacs.editor.open(path)` to switch their window to a different
|
||||
/// buffer; the per-frontend window-tree refactor (M10.8 Q1) makes
|
||||
/// this independent.
|
||||
/// Re-point a semantic frontend's active window at `buffer_id` — the
|
||||
/// buffer it just declared (via `FrontendEvent::Viewport`) that it is
|
||||
/// displaying. No-op when the window is already on that buffer or the
|
||||
/// buffer is gone.
|
||||
///
|
||||
/// A semantic frontend renders from the wire (`StyleSpans` + its local
|
||||
/// CRDT replica), so its daemon-side window holds only the cursor and
|
||||
/// the buffer identity — no grid overlays to migrate. Rebuilding the
|
||||
/// `TextView` (a cheap line index) and resetting the cursor is the
|
||||
/// whole switch. This is the input/display alignment fix for B1: the
|
||||
/// frontend's *declared* buffer becomes the buffer its keys edit and
|
||||
/// its `CursorByte` reports.
|
||||
fn align_semantic_window_to_buffer(
|
||||
editor: &mut EditorState,
|
||||
fid: FrontendId,
|
||||
buffer_id: crate::buffer::BufferId,
|
||||
) {
|
||||
use crate::text_view::TextView;
|
||||
|
||||
let text_view = {
|
||||
let core = editor.core.borrow();
|
||||
let Some(win_id) = core.views.get(&fid).map(|v| v.active) else {
|
||||
return;
|
||||
};
|
||||
if core.windows.get(&win_id).map(|w| w.buffer_id) == Some(buffer_id) {
|
||||
return; // Already displaying this buffer.
|
||||
}
|
||||
let reg = core.registry.borrow();
|
||||
let Ok(buf) = reg.get(buffer_id) else {
|
||||
return; // Unknown buffer — leave the window as-is.
|
||||
};
|
||||
TextView::new(buf)
|
||||
};
|
||||
|
||||
let mut core = editor.core.borrow_mut();
|
||||
let Some(win_id) = core.views.get(&fid).map(|v| v.active) else {
|
||||
return;
|
||||
};
|
||||
if let Some(win) = core.windows.get_mut(&win_id) {
|
||||
win.buffer_id = buffer_id;
|
||||
win.text_view = text_view;
|
||||
win.cursor = 0;
|
||||
win.selection = None;
|
||||
win.overlays.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn build_fresh_frontend_view(editor: &mut EditorState) -> crate::window::FrontendView {
|
||||
use crate::text_view::TextView;
|
||||
use crate::window::{FrontendView, Layout, Window, WindowId};
|
||||
|
|
@ -2182,4 +2242,72 @@ mod tests {
|
|||
(pre-B1 the dispatcher dropped it)"
|
||||
);
|
||||
}
|
||||
|
||||
/// B1 input/display alignment: a semantic frontend's window is bound
|
||||
/// to LOCAL's attach-time buffer, but the buffer it *displays* is
|
||||
/// the one it declares via `Viewport`. `align_semantic_window_to_buffer`
|
||||
/// re-points the window so keys edit the displayed buffer — without
|
||||
/// it, arrow keys moved an off-screen cursor in the wrong buffer and
|
||||
/// the caret never tracked.
|
||||
#[cfg(feature = "crdt")]
|
||||
#[test]
|
||||
fn viewport_aligns_semantic_window_to_displayed_buffer() {
|
||||
use crate::editor::EditorState;
|
||||
use crate::protocol::FrontendId;
|
||||
use pmacs_protocol::{Key, KeyEvent, Modifiers};
|
||||
|
||||
let mut editor = EditorState::new();
|
||||
let scratch = editor.core.borrow().active_window().buffer_id;
|
||||
let file = {
|
||||
let core = editor.core.borrow();
|
||||
core.registry
|
||||
.borrow_mut()
|
||||
.create_from_bytes("file".to_owned(), b"hello\nworld\n")
|
||||
};
|
||||
assert_ne!(scratch, file);
|
||||
|
||||
// Attach: window shares LOCAL's active (scratch).
|
||||
let fid = FrontendId(99);
|
||||
let view = build_fresh_frontend_view(&mut editor);
|
||||
editor.core.borrow_mut().register_frontend_view(fid, view);
|
||||
assert_eq!(
|
||||
editor
|
||||
.core
|
||||
.borrow()
|
||||
.active_window_for(fid)
|
||||
.unwrap()
|
||||
.buffer_id,
|
||||
scratch
|
||||
);
|
||||
|
||||
// The frontend declares it is displaying the file buffer.
|
||||
align_semantic_window_to_buffer(&mut editor, fid, file);
|
||||
assert_eq!(
|
||||
editor
|
||||
.core
|
||||
.borrow()
|
||||
.active_window_for(fid)
|
||||
.unwrap()
|
||||
.buffer_id,
|
||||
file,
|
||||
"Viewport must re-point the window at the displayed buffer"
|
||||
);
|
||||
|
||||
// A key now edits the *displayed* buffer, advancing its cursor.
|
||||
apply_semantic_input_event(
|
||||
&mut editor,
|
||||
FrontendEvent::Key(KeyEvent {
|
||||
frontend_id: fid,
|
||||
key: Key::Char('Z'),
|
||||
mods: Modifiers::NONE,
|
||||
timestamp_ns: 0,
|
||||
}),
|
||||
CellSize::new(24, 80),
|
||||
);
|
||||
assert_eq!(
|
||||
editor.core.borrow().active_window_for(fid).unwrap().cursor,
|
||||
1,
|
||||
"key must self-insert into the displayed buffer, not the attach-time scratch"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue