diff --git a/COMMIT_MESSAGE_ISSUE_5922.txt b/COMMIT_MESSAGE_ISSUE_5922.txt new file mode 100644 index 00000000000..92a39672cc9 --- /dev/null +++ b/COMMIT_MESSAGE_ISSUE_5922.txt @@ -0,0 +1 @@ +fix(tui): allow AltGr printable keys on Windows (#5922) diff --git a/PR_BODY.md b/PR_BODY.md index f9128ce12ac..33944682a6a 100644 --- a/PR_BODY.md +++ b/PR_BODY.md @@ -1,9 +1,10 @@ -## Overview -- AutoRunPhase now carries struct payloads; controller exposes helpers (`is_active`, `is_paused_manual`, `resume_after_submit`, `awaiting_coordinator_submit`, `awaiting_review`, `in_transient_recovery`). -- ChatWidget hot paths (manual pause, coordinator routing, ESC handling, review exit) rely on helpers/`matches!` instead of raw booleans. +## Summary +- allow Windows AltGr combos (Control+Alt) to insert printable characters without swallowing them, while keeping Ctrl+Alt shortcuts intact +- add Windows-only unit tests around `TextArea` and an integration-style ComposerInput test to cover `/`, `@`, and Ctrl+Alt+H +- document the fix via regression tests so future Windows keyboard regressions are caught early -## Tests -- `./build-fast.sh` +## Testing +- ./build-fast.sh +- cargo test -p code-tui --test windows_altgr -- --ignored *(fails: local cargo registry copy of `cc` 1.2.41 is missing generated modules; clear/update the registry and rerun on Windows)* -## Follow-ups -- See `docs/auto-drive-phase-migration-TODO.md` for remaining legacy-flag removals and snapshot coverage. +Closes #5922. diff --git a/PR_BODY_ISSUE_5922.md b/PR_BODY_ISSUE_5922.md new file mode 100644 index 00000000000..dbd1c4ab11b --- /dev/null +++ b/PR_BODY_ISSUE_5922.md @@ -0,0 +1,10 @@ +## Summary +- treat Windows AltGr (Control+Alt) key chords as printable characters so `/`, `@`, and other symbols insert correctly in the composer and terminal cells +- keep Ctrl+Alt shortcuts (e.g., Ctrl+Alt+H delete word) by excluding ASCII letters from the AltGr path and add Windows-only regression tests +- cover the change with new `TextArea` unit tests and a ComposerInput integration-style test to prevent future regressions + +## Testing +- ./build-fast.sh +- cargo test -p code-tui --test windows_altgr -- --ignored *(fails: local cargo registry copy of `cc` 1.2.41 is missing generated modules; clear/update the registry and rerun on Windows)* + +Closes #5922. diff --git a/code-rs/tui/src/bottom_pane/textarea.rs b/code-rs/tui/src/bottom_pane/textarea.rs index f5aecd736f3..719de18360b 100644 --- a/code-rs/tui/src/bottom_pane/textarea.rs +++ b/code-rs/tui/src/bottom_pane/textarea.rs @@ -246,6 +246,20 @@ impl TextArea { code: KeyCode::Char(c), .. } if matches!(c, '\n' | '\r') => self.insert_str("\n"), + #[cfg(target_os = "windows")] + KeyEvent { + code: KeyCode::Char(c), + modifiers, + .. + } if (modifiers == (KeyModifiers::CONTROL | KeyModifiers::ALT) + || modifiers == (KeyModifiers::CONTROL + | KeyModifiers::ALT + | KeyModifiers::SHIFT)) + && !c.is_ascii_control() + && !c.is_ascii_alphabetic() => + { + self.insert_str(&c.to_string()); + } KeyEvent { code: KeyCode::Char(c), // Insert plain characters (and Shift-modified). Do NOT insert when ALT is held, @@ -909,6 +923,62 @@ impl TextArea { } } +#[cfg(test)] +mod tests { + use super::*; + use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + + #[cfg(target_os = "windows")] + #[test] + fn altgr_control_alt_characters_insert_printables() { + let mut textarea = TextArea::new(); + let cases = [ + ('/', KeyModifiers::CONTROL | KeyModifiers::ALT), + ('@', KeyModifiers::CONTROL | KeyModifiers::ALT), + ('{', KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT), + ]; + + for (ch, modifiers) in cases { + textarea.set_text(""); + textarea.set_cursor(0); + textarea.input(KeyEvent::new(KeyCode::Char(ch), modifiers)); + assert_eq!(textarea.text(), ch.to_string(), "expected AltGr combination to insert {ch}"); + } + } + + #[test] + fn ctrl_alt_letter_shortcut_preserved() { + let mut textarea = TextArea::new(); + textarea.set_text("word"); + textarea.set_cursor(textarea.text().len()); + + textarea.input(KeyEvent::new( + KeyCode::Char('h'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )); + + assert_eq!(textarea.text(), "", "Ctrl+Alt+H should still delete backward word"); + } + + #[cfg(not(target_os = "windows"))] + #[test] + fn ctrl_alt_symbol_shortcut_is_ignored_for_text_insertion() { + let mut textarea = TextArea::new(); + textarea.set_text(""); + textarea.set_cursor(0); + + textarea.input(KeyEvent::new( + KeyCode::Char('@'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )); + + assert!( + textarea.text().is_empty(), + "Ctrl+Alt symbol should not insert printable characters on non-Windows" + ); + } +} + impl WidgetRef for &TextArea { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let lines = self.wrapped_lines(area.width); @@ -955,4 +1025,3 @@ impl TextArea { } } } - diff --git a/code-rs/tui/tests/non_windows_shortcuts.rs b/code-rs/tui/tests/non_windows_shortcuts.rs new file mode 100644 index 00000000000..4725384279c --- /dev/null +++ b/code-rs/tui/tests/non_windows_shortcuts.rs @@ -0,0 +1,32 @@ +#![cfg(not(target_os = "windows"))] + +use code_tui::ComposerInput; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +#[test] +fn composer_ctrl_alt_letter_shortcut_deletes_word() { + let mut composer = ComposerInput::new(); + composer.handle_paste("word".to_string()); + + let _ = composer.input(KeyEvent::new( + KeyCode::Char('h'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )); + + assert!(composer.text().is_empty(), "Ctrl+Alt+H should delete the previous word"); +} + +#[test] +fn composer_ctrl_alt_symbol_does_not_insert_text() { + let mut composer = ComposerInput::new(); + + let _ = composer.input(KeyEvent::new( + KeyCode::Char('@'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )); + + assert!( + composer.text().is_empty(), + "Ctrl+Alt symbol should be treated as a shortcut, not inserted text" + ); +} diff --git a/code-rs/tui/tests/windows_altgr.rs b/code-rs/tui/tests/windows_altgr.rs new file mode 100644 index 00000000000..368d4e9f4ca --- /dev/null +++ b/code-rs/tui/tests/windows_altgr.rs @@ -0,0 +1,34 @@ +#![cfg(target_os = "windows")] + +use code_tui::public_widgets::ComposerInput; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +#[test] +fn composer_input_altgr_characters_insert_text() { + let mut composer = ComposerInput::new(); + + let cases = [ + ('/', KeyModifiers::CONTROL | KeyModifiers::ALT), + ('@', KeyModifiers::CONTROL | KeyModifiers::ALT), + ('{', KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT), + ]; + + for (ch, modifiers) in cases { + composer.clear(); + let _ = composer.input(KeyEvent::new(KeyCode::Char(ch), modifiers)); + assert_eq!(composer.text(), ch.to_string(), "AltGr input should insert printable character"); + } +} + +#[test] +fn composer_input_ctrl_alt_letter_shortcut_still_deletes_word() { + let mut composer = ComposerInput::new(); + composer.handle_paste("word".to_string()); + + let _ = composer.input(KeyEvent::new( + KeyCode::Char('h'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )); + + assert!(composer.text().is_empty(), "Ctrl+Alt+H should delete the previous word"); +}