From 2339a10b037f37d8baaa2da823803290fb626a07 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Wed, 10 Jun 2026 11:48:23 -0400 Subject: [PATCH] pmacs-gpu: forward chorded deletion keys (C-BS word delete) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backspace/Delete with modifiers were withheld by the chord filter, so C-BS / C-DEL / M-BS — word-level deletes in the default keymap — silently did nothing in the GPU while C- word motion worked. Deletion keys now forward with their modifiers exactly like motion keys; an unbound chord is a harmless no-op at the daemon keymap, and chorded deletes never apply optimistically (optimistic_delete_range requires empty modifiers), so they always round-trip into their bound commands. Acceptance test drives C-BS / C-DEL through the real dispatch path. Co-Authored-By: Claude Fable 5 --- pmacs-gpu/src/main.rs | 34 +++++++++++++++++++++++----------- tests/cua_region_acceptance.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/pmacs-gpu/src/main.rs b/pmacs-gpu/src/main.rs index ff5ba31..44f57e5 100644 --- a/pmacs-gpu/src/main.rs +++ b/pmacs-gpu/src/main.rs @@ -2649,26 +2649,31 @@ 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')`. +/// Motion keys go through with any modifiers (C- is word +/// motion). Deletion keys do too: C-BS / C-DEL / M-BS are word-level +/// deletes in the default keymap — the same editing-command family as +/// chorded motion, and an unbound chord is a harmless no-op at the +/// daemon keymap. (Chorded deletes never apply optimistically: +/// `optimistic_delete_range` requires empty modifiers, so they always +/// round-trip into their bound commands.) The remaining text keys +/// (`Char` / `Enter` / `Tab`) go through only *without* a +/// Ctrl/Alt/Meta chord modifier: a bare key edits text, but those +/// chords drive 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; } + if matches!(key, ProtocolKey::Backspace | ProtocolKey::Delete) { + return true; + } if !is_plain_text_modifiers(mods) { return false; } matches!( key, - ProtocolKey::Char(_) - | ProtocolKey::Backspace - | ProtocolKey::Enter - | ProtocolKey::Delete - | ProtocolKey::Tab + ProtocolKey::Char(_) | ProtocolKey::Enter | ProtocolKey::Tab ) } @@ -3600,6 +3605,13 @@ mod tests { assert!(should_forward_key(ProtocolKey::Left, ctrl)); assert!(should_forward_key(ProtocolKey::Down, shift)); assert!(should_forward_key(ProtocolKey::PageUp, none)); + + // Deletion keys forward regardless of modifiers too — C-BS / + // C-DEL / M-BS are word-level deletes in the default keymap, + // the same editing-command family as chorded motion. + assert!(should_forward_key(ProtocolKey::Backspace, ctrl)); + assert!(should_forward_key(ProtocolKey::Delete, ctrl)); + assert!(should_forward_key(ProtocolKey::Backspace, Modifiers::ALT)); } #[test] diff --git a/tests/cua_region_acceptance.rs b/tests/cua_region_acceptance.rs index 717e402..26eec00 100644 --- a/tests/cua_region_acceptance.rs +++ b/tests/cua_region_acceptance.rs @@ -74,6 +74,32 @@ fn backspace_deletes_the_shift_selected_region() { assert_eq!(cursor, 1); } +/// C-Backspace deletes the previous word (and C-Delete the next), +/// mirroring C-arrow word motion. The pmacs-gpu frontend forwards +/// chorded deletion keys to this same dispatch path. +#[test] +fn ctrl_backspace_deletes_the_previous_word() { + let mut s = EditorState::new(); + type_str(&mut s, "alpha beta"); + + s.dispatch_key( + FrontendId::LOCAL, + key(KeyCode::Backspace, KeyModifiers::CONTROL), + ); + let (text, _, cursor) = probe(&s); + assert_eq!(text, "alpha ", "C-BS deletes back through the previous word"); + assert_eq!(cursor, 6); + + s.dispatch_key(FrontendId::LOCAL, key(KeyCode::Left, KeyModifiers::CONTROL)); + s.dispatch_key( + FrontendId::LOCAL, + key(KeyCode::Delete, KeyModifiers::CONTROL), + ); + let (text, _, cursor) = probe(&s); + assert_eq!(text, " ", "C-DEL deletes forward through the next word"); + assert_eq!(cursor, 0); +} + #[test] fn typing_replaces_the_shift_selected_region() { let mut s = EditorState::new();