session B2 — text editing in pmacs-gpu (Key round-trip)
Broadens the send gate from motion-only (B1) to plain text editing:
`should_forward_key` forwards Char / Backspace / Enter / Delete / Tab
in addition to motion keys. Editing rides the same round trip B1
proved — the daemon's `dispatch_key` self-inserts / deletes on the
viewport-aligned buffer, authors the CRDT op (`CrdtOpOrigin::DaemonKey`,
excluded from no recipient), and broadcasts it back; pmacs-gpu applies
it (existing session-3 CrdtOp path) and the edit also propagates to the
TUI. No editing logic in the frontend.
Ctrl/Alt/Meta chords are deliberately withheld: they drive commands and
minibuffer flows the GUI can't render or interact with yet (the
minibuffer is instance-side global state; a GUI frontend opening one
with no way to see/cancel it would wedge input). Those land in a later
command-parity session with GUI minibuffer rendering. Shift is not a
chord modifier — Shift+a already arrives as `Char('A')`.
Test `should_forward_key_gates_editing_keys_and_excludes_chords`:
editing keys + uppercase forward; Ctrl/Alt + char withheld; motion
keys forward regardless of modifiers.
Gates green: fmt; clippy --all-targets --workspace -D warnings;
pmacs-gpu unit 20 (+1). Daemon/lib untouched (it already dispatches
semantic-frontend keys, B1 fix).
Awaiting visual confirmation: typing in pmacs-gpu inserts text that
propagates to the TUI; backspace/enter/delete work; CRDT stays
converged. Per the framing's process rule, not merged until confirmed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
da9a63aa10
commit
5888ced15a
|
|
@ -371,12 +371,13 @@ impl ApplicationHandler<AppEvent> for App {
|
|||
event_loop.exit();
|
||||
return;
|
||||
}
|
||||
// Session B1 forwards cursor-motion keys only; editing
|
||||
// keys (chars, Backspace, Enter, Delete) open in B2.
|
||||
// `translate_key` handles the full set so B2 just drops
|
||||
// the `is_motion_key` gate.
|
||||
// Session B2 forwards cursor motion + plain text editing
|
||||
// (Char / Backspace / Enter / Delete / Tab). Ctrl/Alt/
|
||||
// Meta chords are withheld — they drive commands and
|
||||
// minibuffer flows the GUI can't render or interact with
|
||||
// yet (a later session adds GUI minibuffer + chords).
|
||||
if let Some((pkey, pmods)) = translate_key(&key.logical_key, self.modifiers)
|
||||
&& is_motion_key(pkey)
|
||||
&& should_forward_key(pkey, pmods)
|
||||
&& let Some(client) = self.attach_client.as_ref()
|
||||
{
|
||||
if debug_input() {
|
||||
|
|
@ -1801,9 +1802,9 @@ fn translate_key(
|
|||
Some((pkey, pmods))
|
||||
}
|
||||
|
||||
/// Session B1 forwards cursor-motion keys only; editing keys wait for
|
||||
/// B2. Keeping the gate as a single predicate means B2 removes one
|
||||
/// call site, not a translation rewrite.
|
||||
/// Cursor-motion keys — forwarded with any modifier set (e.g. `C-Left`
|
||||
/// is word-motion, `S-Down` extends a selection; the daemon's keymap
|
||||
/// decides).
|
||||
fn is_motion_key(key: ProtocolKey) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
|
|
@ -1818,6 +1819,33 @@ fn is_motion_key(key: ProtocolKey) -> bool {
|
|||
)
|
||||
}
|
||||
|
||||
/// Whether to forward a translated key to the daemon (session B2).
|
||||
/// Motion keys go through with any modifiers. Plain text-editing keys
|
||||
/// (`Char` / `Backspace` / `Enter` / `Delete` / `Tab`) go through only
|
||||
/// *without* a Ctrl/Alt/Meta chord modifier: a bare key edits text,
|
||||
/// but a chord drives commands and minibuffer flows the GUI can't
|
||||
/// render or interact with yet (deferred to a later session). Shift is
|
||||
/// not a chord modifier — `Shift`+a already arrives as `Char('A')`.
|
||||
fn should_forward_key(key: ProtocolKey, mods: Modifiers) -> bool {
|
||||
if is_motion_key(key) {
|
||||
return true;
|
||||
}
|
||||
let chord = mods.contains(Modifiers::CTRL)
|
||||
|| mods.contains(Modifiers::ALT)
|
||||
|| mods.contains(Modifiers::META);
|
||||
if chord {
|
||||
return false;
|
||||
}
|
||||
matches!(
|
||||
key,
|
||||
ProtocolKey::Char(_)
|
||||
| ProtocolKey::Backspace
|
||||
| ProtocolKey::Enter
|
||||
| ProtocolKey::Delete
|
||||
| ProtocolKey::Tab
|
||||
)
|
||||
}
|
||||
|
||||
/// Buffer-absolute byte offset of the start of each `\n`-delimited
|
||||
/// line (index 0 = byte 0). Indexed by cosmic-text's
|
||||
/// `LayoutRun::line_i` to rebase line-relative glyph offsets.
|
||||
|
|
@ -2251,6 +2279,36 @@ mod tests {
|
|||
assert!(!is_motion_key(bk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_forward_key_gates_editing_keys_and_excludes_chords() {
|
||||
let none = Modifiers::NONE;
|
||||
let ctrl = Modifiers::CTRL;
|
||||
let shift = Modifiers::SHIFT;
|
||||
|
||||
// Plain text-editing keys forward.
|
||||
for key in [
|
||||
ProtocolKey::Char('a'),
|
||||
ProtocolKey::Char('A'),
|
||||
ProtocolKey::Backspace,
|
||||
ProtocolKey::Enter,
|
||||
ProtocolKey::Delete,
|
||||
ProtocolKey::Tab,
|
||||
] {
|
||||
assert!(should_forward_key(key, none), "{key:?} should forward");
|
||||
}
|
||||
// Shift is not a chord modifier (Shift+a already arrives as 'A').
|
||||
assert!(should_forward_key(ProtocolKey::Char('A'), shift));
|
||||
|
||||
// Ctrl/Alt/Meta + a non-motion key is a chord — withheld in B2.
|
||||
assert!(!should_forward_key(ProtocolKey::Char('x'), ctrl));
|
||||
assert!(!should_forward_key(ProtocolKey::Char('f'), Modifiers::ALT));
|
||||
|
||||
// Motion keys forward regardless of modifiers (C-Left = word-left).
|
||||
assert!(should_forward_key(ProtocolKey::Left, ctrl));
|
||||
assert!(should_forward_key(ProtocolKey::Down, shift));
|
||||
assert!(should_forward_key(ProtocolKey::PageUp, none));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_key_carries_modifiers() {
|
||||
use winit::keyboard::{Key as WKey, ModifiersState, NamedKey};
|
||||
|
|
|
|||
Loading…
Reference in New Issue