test: add ime input regressions
This commit is contained in:
parent
8375922c06
commit
4af28fbb65
|
|
@ -1979,6 +1979,56 @@ mod tests {
|
|||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn route_client_input_forwards_multilingual_ime_text_to_focused_pane() {
|
||||
let mut app = test_app();
|
||||
let mut workspace = Workspace::test_new("test");
|
||||
let focused = workspace.focused_pane_id().unwrap();
|
||||
let text = "中日한🙂";
|
||||
let (runtime, mut rx) =
|
||||
PaneRuntime::test_with_channel_capacity(80, 24, text.chars().count());
|
||||
workspace.tabs[0].runtimes.insert(focused, runtime);
|
||||
app.state.workspaces = vec![workspace];
|
||||
app.state.active = Some(0);
|
||||
app.state.selected = 0;
|
||||
app.state.mode = Mode::Terminal;
|
||||
|
||||
app.route_client_input(text.as_bytes().to_vec());
|
||||
|
||||
let mut forwarded = Vec::new();
|
||||
for _ in text.chars() {
|
||||
let chunk = rx.recv().await.unwrap();
|
||||
forwarded.extend_from_slice(&chunk);
|
||||
}
|
||||
assert_eq!(forwarded, text.as_bytes());
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn route_client_input_forwards_long_voice_like_cjk_text_without_truncation() {
|
||||
let mut app = test_app();
|
||||
let mut workspace = Workspace::test_new("test");
|
||||
let focused = workspace.focused_pane_id().unwrap();
|
||||
let text = "你好,今天我们测试一段比较长的语音输入。こんにちは。안녕하세요.🙂".repeat(64);
|
||||
let char_count = text.chars().count();
|
||||
let (runtime, mut rx) = PaneRuntime::test_with_channel_capacity(80, 24, char_count);
|
||||
workspace.tabs[0].runtimes.insert(focused, runtime);
|
||||
app.state.workspaces = vec![workspace];
|
||||
app.state.active = Some(0);
|
||||
app.state.selected = 0;
|
||||
app.state.mode = Mode::Terminal;
|
||||
|
||||
app.route_client_input(text.as_bytes().to_vec());
|
||||
|
||||
let mut forwarded = Vec::new();
|
||||
for _ in 0..char_count {
|
||||
let chunk = rx.recv().await.unwrap();
|
||||
forwarded.extend_from_slice(&chunk);
|
||||
}
|
||||
assert_eq!(forwarded, text.as_bytes());
|
||||
assert!(rx.try_recv().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_client_input_handles_mouse_events() {
|
||||
let mut app = test_app();
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ use std::time::Duration;
|
|||
|
||||
use crossterm::event::{
|
||||
DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
|
||||
EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
|
||||
EnableFocusChange, EnableMouseCapture, PopKeyboardEnhancementFlags,
|
||||
PushKeyboardEnhancementFlags,
|
||||
};
|
||||
use crossterm::execute;
|
||||
|
|
@ -146,11 +146,7 @@ fn setup_terminal() -> io::Result<TerminalGuard> {
|
|||
EnableMouseCapture,
|
||||
EnableBracketedPaste,
|
||||
EnableFocusChange,
|
||||
PushKeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
|
||||
)
|
||||
PushKeyboardEnhancementFlags(crate::input::ime_compatible_keyboard_enhancement_flags())
|
||||
)?;
|
||||
|
||||
// tmux doesn't understand kitty keyboard protocol push.
|
||||
|
|
|
|||
|
|
@ -6,5 +6,8 @@ mod parse;
|
|||
pub use encode::{
|
||||
encode_cursor_key, encode_mouse_button, encode_mouse_scroll, encode_terminal_key,
|
||||
};
|
||||
pub use model::{KeyboardProtocol, MouseProtocolEncoding, MouseProtocolMode, TerminalKey};
|
||||
pub use model::{
|
||||
ime_compatible_keyboard_enhancement_flags, KeyboardProtocol, MouseProtocolEncoding,
|
||||
MouseProtocolMode, TerminalKey,
|
||||
};
|
||||
pub use parse::parse_terminal_key_sequence;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, KeyboardEnhancementFlags};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TerminalKey {
|
||||
|
|
@ -40,6 +40,12 @@ impl From<KeyEvent> for TerminalKey {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn ime_compatible_keyboard_enhancement_flags() -> KeyboardEnhancementFlags {
|
||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum KeyboardProtocol {
|
||||
Legacy,
|
||||
|
|
@ -97,4 +103,14 @@ mod tests {
|
|||
KeyboardProtocol::Kitty { flags: 7 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyboard_enhancement_flags_stay_ime_compatible() {
|
||||
let flags = ime_compatible_keyboard_enhancement_flags();
|
||||
|
||||
assert!(flags.contains(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES));
|
||||
assert!(flags.contains(KeyboardEnhancementFlags::REPORT_EVENT_TYPES));
|
||||
assert!(flags.contains(KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS));
|
||||
assert!(!flags.contains(KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::io;
|
|||
|
||||
use crossterm::event::{
|
||||
DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
|
||||
EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
|
||||
EnableFocusChange, EnableMouseCapture, PopKeyboardEnhancementFlags,
|
||||
PushKeyboardEnhancementFlags,
|
||||
};
|
||||
use crossterm::execute;
|
||||
|
|
@ -454,11 +454,7 @@ fn main() -> io::Result<()> {
|
|||
EnableMouseCapture,
|
||||
EnableBracketedPaste,
|
||||
EnableFocusChange,
|
||||
PushKeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
|
||||
)
|
||||
PushKeyboardEnhancementFlags(crate::input::ime_compatible_keyboard_enhancement_flags())
|
||||
)?;
|
||||
|
||||
// tmux doesn't understand kitty keyboard protocol push (\e[>1u).
|
||||
|
|
|
|||
15
src/pane.rs
15
src/pane.rs
|
|
@ -925,7 +925,15 @@ impl PaneRuntime {
|
|||
#[cfg(test)]
|
||||
impl PaneRuntime {
|
||||
pub(crate) fn test_with_channel(cols: u16, rows: u16) -> (Self, mpsc::Receiver<Bytes>) {
|
||||
Self::test_with_channel_and_scrollback_bytes(cols, rows, 0, &[])
|
||||
Self::test_with_channel_and_scrollback_bytes(cols, rows, 0, &[], 4)
|
||||
}
|
||||
|
||||
pub(crate) fn test_with_channel_capacity(
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
capacity: usize,
|
||||
) -> (Self, mpsc::Receiver<Bytes>) {
|
||||
Self::test_with_channel_and_scrollback_bytes(cols, rows, 0, &[], capacity)
|
||||
}
|
||||
|
||||
pub(crate) fn test_with_screen_bytes(cols: u16, rows: u16, bytes: &[u8]) -> Self {
|
||||
|
|
@ -938,7 +946,7 @@ impl PaneRuntime {
|
|||
scrollback_limit_bytes: usize,
|
||||
bytes: &[u8],
|
||||
) -> Self {
|
||||
Self::test_with_channel_and_scrollback_bytes(cols, rows, scrollback_limit_bytes, bytes).0
|
||||
Self::test_with_channel_and_scrollback_bytes(cols, rows, scrollback_limit_bytes, bytes, 4).0
|
||||
}
|
||||
|
||||
fn test_with_channel_and_scrollback_bytes(
|
||||
|
|
@ -946,8 +954,9 @@ impl PaneRuntime {
|
|||
rows: u16,
|
||||
scrollback_limit_bytes: usize,
|
||||
bytes: &[u8],
|
||||
channel_capacity: usize,
|
||||
) -> (Self, mpsc::Receiver<Bytes>) {
|
||||
let (tx, rx) = mpsc::channel(4);
|
||||
let (tx, rx) = mpsc::channel(channel_capacity);
|
||||
let (resize_tx, _resize_rx) = watch::channel((rows, cols));
|
||||
let mut terminal =
|
||||
crate::ghostty::Terminal::new(cols, rows, scrollback_limit_bytes).unwrap();
|
||||
|
|
|
|||
|
|
@ -1006,6 +1006,103 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunked_cjk_utf8_waits_for_all_continuation_bytes() {
|
||||
let (tx, mut rx) = mpsc::channel(8);
|
||||
let mut buffer = Vec::new();
|
||||
let bytes = "好".as_bytes();
|
||||
|
||||
drain_chunk(&mut buffer, &tx, &bytes[..1]);
|
||||
assert_eq!(buffer, bytes[..1]);
|
||||
assert!(collect_events(&mut rx).is_empty());
|
||||
flush_incomplete_buffer(&mut buffer, &tx);
|
||||
assert_eq!(buffer, bytes[..1]);
|
||||
assert!(collect_events(&mut rx).is_empty());
|
||||
|
||||
drain_chunk(&mut buffer, &tx, &bytes[1..2]);
|
||||
assert_eq!(buffer, bytes[..2]);
|
||||
assert!(collect_events(&mut rx).is_empty());
|
||||
flush_incomplete_buffer(&mut buffer, &tx);
|
||||
assert_eq!(buffer, bytes[..2]);
|
||||
assert!(collect_events(&mut rx).is_empty());
|
||||
|
||||
drain_chunk(&mut buffer, &tx, &bytes[2..]);
|
||||
assert!(buffer.is_empty());
|
||||
let events = collect_events(&mut rx);
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_raw_key(
|
||||
events.into_iter().next().unwrap(),
|
||||
KeyCode::Char('好'),
|
||||
KeyModifiers::empty(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunked_four_byte_utf8_waits_for_all_continuation_bytes() {
|
||||
let (tx, mut rx) = mpsc::channel(8);
|
||||
let mut buffer = Vec::new();
|
||||
let bytes = "🙂".as_bytes();
|
||||
|
||||
for split in 1..bytes.len() {
|
||||
drain_chunk(&mut buffer, &tx, &bytes[split - 1..split]);
|
||||
assert_eq!(buffer, bytes[..split]);
|
||||
assert!(collect_events(&mut rx).is_empty());
|
||||
flush_incomplete_buffer(&mut buffer, &tx);
|
||||
assert_eq!(buffer, bytes[..split]);
|
||||
assert!(collect_events(&mut rx).is_empty());
|
||||
}
|
||||
|
||||
drain_chunk(&mut buffer, &tx, &bytes[bytes.len() - 1..]);
|
||||
assert!(buffer.is_empty());
|
||||
let events = collect_events(&mut rx);
|
||||
assert_eq!(events.len(), 1);
|
||||
assert_raw_key(
|
||||
events.into_iter().next().unwrap(),
|
||||
KeyCode::Char('🙂'),
|
||||
KeyModifiers::empty(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_multilingual_voice_like_burst_drains_without_truncation() {
|
||||
let text = "你好,今天我们测试一段比较长的语音输入。こんにちは。안녕하세요.🙂".repeat(128);
|
||||
assert!(
|
||||
text.len() > 4096,
|
||||
"test input should exceed the client read buffer"
|
||||
);
|
||||
let mut buffer = text.as_bytes().to_vec();
|
||||
|
||||
let chunks = drain_complete_input_bytes(&mut buffer);
|
||||
let rebuilt: Vec<u8> = chunks.into_iter().flatten().collect();
|
||||
|
||||
assert!(buffer.is_empty());
|
||||
assert_eq!(rebuilt, text.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_multilingual_burst_survives_one_byte_chunks_and_timeouts() {
|
||||
let text = "中文かなカナ한글🙂,。".repeat(64);
|
||||
let mut buffer = Vec::new();
|
||||
let mut rebuilt = Vec::new();
|
||||
|
||||
for byte in text.as_bytes() {
|
||||
buffer.push(*byte);
|
||||
for chunk in drain_complete_input_bytes(&mut buffer) {
|
||||
rebuilt.extend(chunk);
|
||||
}
|
||||
if !buffer.is_empty() {
|
||||
assert_eq!(flush_incomplete_input_bytes(&mut buffer), None);
|
||||
}
|
||||
}
|
||||
|
||||
for chunk in drain_complete_input_bytes(&mut buffer) {
|
||||
rebuilt.extend(chunk);
|
||||
}
|
||||
|
||||
assert!(buffer.is_empty());
|
||||
assert_eq!(rebuilt, text.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_ranges_tracks_byte_offsets() {
|
||||
use super::parse_raw_input_bytes_with_ranges;
|
||||
|
|
|
|||
|
|
@ -565,6 +565,23 @@ mod tests {
|
|||
assert_eq!(msg, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_input_large_multilingual_payload_roundtrip() {
|
||||
let text = "你好,今天我们测试一段比较长的语音输入。こんにちは。안녕하세요.🙂".repeat(1024);
|
||||
assert!(text.len() > 64 * 1024);
|
||||
assert!(text.len() < MAX_FRAME_SIZE);
|
||||
let msg = ClientMessage::Input {
|
||||
data: text.as_bytes().to_vec(),
|
||||
};
|
||||
|
||||
let encoded = bincode::serde::encode_to_vec(&msg, bincode::config::standard()).unwrap();
|
||||
let (decoded, consumed): (ClientMessage, _) =
|
||||
bincode::serde::decode_from_slice(&encoded, bincode::config::standard()).unwrap();
|
||||
|
||||
assert_eq!(consumed, encoded.len());
|
||||
assert_eq!(decoded, msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_resize_roundtrip() {
|
||||
let msg = ClientMessage::Resize { cols: 80, rows: 24 };
|
||||
|
|
|
|||
Loading…
Reference in New Issue