//! CUA region semantics — Backspace / Delete consume the active //! selection (set by Shift+motion) before falling back to their //! single-codepoint behavior. //! //! Regression for the pmacs-gpu report "select a region with //! shift+arrows then backspace doesn't delete as expected": the //! `buffer.delete-backward` / `buffer.delete-forward` commands called //! straight into the single-codepoint core primitives and never //! consulted `active_region()`. The behavior is frontend-agnostic //! (the GPU round-trips BS through the same dispatch), so the TUI //! dispatch path exercised here covers both. 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 type_str(s: &mut EditorState, text: &str) { for ch in text.chars() { s.dispatch_key( FrontendId::LOCAL, key(KeyCode::Char(ch), KeyModifiers::NONE), ); } } /// `(buffer text, region active?, cursor)` probed through the Lua /// surface — the same introspection a user-facing script would use. fn probe(s: &EditorState) -> (String, bool, i64) { s.lua_host .lua() .load( " local b = pmacs.window.buffer() local text = b:slice(0, b:len()) return text, pmacs.editor.region() ~= nil, pmacs.editor.cursor() ", ) .eval() .expect("probe buffer state") } #[test] fn backspace_deletes_the_shift_selected_region() { let mut s = EditorState::new_with_roots(&crate::iso::roots()); type_str(&mut s, "hello"); // Shift+Left three times: region [2, 5), cursor at 2. for _ in 0..3 { s.dispatch_key(FrontendId::LOCAL, key(KeyCode::Left, KeyModifiers::SHIFT)); } let (_, region_active, _) = probe(&s); assert!(region_active, "shift+arrows must leave an active region"); s.dispatch_key( FrontendId::LOCAL, key(KeyCode::Backspace, KeyModifiers::NONE), ); let (text, region_active, cursor) = probe(&s); assert_eq!(text, "he", "backspace must delete the whole region"); assert!(!region_active, "the region clears with its deletion"); assert_eq!(cursor, 2, "cursor lands at the deleted region's start"); // Without a region, backspace keeps single-codepoint semantics. s.dispatch_key( FrontendId::LOCAL, key(KeyCode::Backspace, KeyModifiers::NONE), ); let (text, _, cursor) = probe(&s); assert_eq!(text, "h", "no region ⇒ plain single-codepoint backspace"); 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_with_roots(&crate::iso::roots()); 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_with_roots(&crate::iso::roots()); type_str(&mut s, "hello"); // Select "llo" (region [2, 5), cursor at 2), then type 'X': // CUA type-over replaces the region with the typed char. for _ in 0..3 { s.dispatch_key(FrontendId::LOCAL, key(KeyCode::Left, KeyModifiers::SHIFT)); } s.dispatch_key( FrontendId::LOCAL, key(KeyCode::Char('X'), KeyModifiers::SHIFT), ); let (text, region_active, cursor) = probe(&s); assert_eq!(text, "heX", "typing must replace the selected region"); assert!(!region_active, "the region is consumed by the replacement"); assert_eq!(cursor, 3, "cursor sits after the typed char"); // Enter over a selection replaces it with a newline. s.dispatch_key(FrontendId::LOCAL, key(KeyCode::Left, KeyModifiers::SHIFT)); s.dispatch_key(FrontendId::LOCAL, key(KeyCode::Enter, KeyModifiers::NONE)); let (text, region_active, cursor) = probe(&s); assert_eq!(text, "he\n", "Enter must replace the selected region"); assert!(!region_active); assert_eq!(cursor, 3); // Without a selection, typing keeps plain insert semantics. s.dispatch_key( FrontendId::LOCAL, key(KeyCode::Char('z'), KeyModifiers::NONE), ); let (text, _, cursor) = probe(&s); assert_eq!(text, "he\nz", "no region ⇒ plain insert at the cursor"); assert_eq!(cursor, 4); } #[test] fn type_over_is_a_single_undo_step() { let mut s = EditorState::new_with_roots(&crate::iso::roots()); type_str(&mut s, "hello"); // Select "llo" (region [2, 5)) and type 'X' → "heX". for _ in 0..3 { s.dispatch_key(FrontendId::LOCAL, key(KeyCode::Left, KeyModifiers::SHIFT)); } s.dispatch_key( FrontendId::LOCAL, key(KeyCode::Char('X'), KeyModifiers::SHIFT), ); let (text, _, _) = probe(&s); assert_eq!(text, "heX"); // A single undo restores the pre-type-over text — not the // half-replaced "hel" the old delete-then-insert pair left after // one undo. s.lua_host .lua() .load("pmacs.editor.undo()") .exec() .expect("undo"); let (text, _, _) = probe(&s); assert_eq!(text, "hello", "type-over undoes in one step"); } #[test] fn delete_forward_deletes_the_shift_selected_region() { let mut s = EditorState::new_with_roots(&crate::iso::roots()); type_str(&mut s, "world"); // Shift+Home-equivalent: extend left over the whole word. for _ in 0..5 { s.dispatch_key(FrontendId::LOCAL, key(KeyCode::Left, KeyModifiers::SHIFT)); } s.dispatch_key(FrontendId::LOCAL, key(KeyCode::Delete, KeyModifiers::NONE)); let (text, region_active, cursor) = probe(&s); assert_eq!(text, "", "Delete must consume the whole region"); assert!(!region_active); assert_eq!(cursor, 0); // Without a region, Delete keeps forward single-codepoint semantics. type_str(&mut s, "ab"); s.dispatch_key(FrontendId::LOCAL, key(KeyCode::Left, KeyModifiers::NONE)); s.dispatch_key(FrontendId::LOCAL, key(KeyCode::Left, KeyModifiers::NONE)); s.dispatch_key(FrontendId::LOCAL, key(KeyCode::Delete, KeyModifiers::NONE)); let (text, _, cursor) = probe(&s); assert_eq!(text, "b", "no region ⇒ plain forward delete at cursor"); assert_eq!(cursor, 0); } // Isolated bootstrap storage roots (see the module docs): an // integration test is compiled without `cfg(test)`, so a raw // `EditorState::new()` would read the developer's real `init.lua` and // write into their real data root. #[path = "common/iso.rs"] mod iso;