diff --git a/crates/tui/src/tui/diff_render.rs b/crates/tui/src/tui/diff_render.rs index 7303c4fd..80120b3b 100644 --- a/crates/tui/src/tui/diff_render.rs +++ b/crates/tui/src/tui/diff_render.rs @@ -2,7 +2,7 @@ use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; -use unicode_width::UnicodeWidthStr; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use crate::palette; @@ -357,6 +357,14 @@ fn wrap_text(text: &str, width: usize) -> Vec { for word in text.split_whitespace() { let word_width = word.width(); + if word_width > width { + if !current.is_empty() { + lines.push(std::mem::take(&mut current)); + current_width = 0; + } + push_word_breaking_chars(word, width, &mut current, &mut current_width, &mut lines); + continue; + } let additional = if current.is_empty() { word_width } else { @@ -385,6 +393,24 @@ fn wrap_text(text: &str, width: usize) -> Vec { lines } +fn push_word_breaking_chars( + word: &str, + width: usize, + current: &mut String, + current_width: &mut usize, + lines: &mut Vec, +) { + for ch in word.chars() { + let char_width = ch.width().unwrap_or(1); + if *current_width + char_width > width && *current_width > 0 { + lines.push(std::mem::take(current)); + *current_width = 0; + } + current.push(ch); + *current_width += char_width; + } +} + #[cfg(test)] mod tests { use super::*; @@ -450,4 +476,16 @@ diff --git a/src/a.rs b/src/a.rs "deleted line should carry - gutter: {text:?}" ); } + + #[test] + fn wrap_text_breaks_overlong_cjk_runs() { + let text = "这是一个非常长的中文字符串".repeat(10); + let lines = wrap_text(&text, 16); + + for line in &lines { + assert!(line.width() <= 16, "line {line:?} exceeds width 16"); + } + + assert_eq!(lines.join(""), text); + } } diff --git a/crates/tui/src/tui/pager.rs b/crates/tui/src/tui/pager.rs index 8b3a72a5..f646c2db 100644 --- a/crates/tui/src/tui/pager.rs +++ b/crates/tui/src/tui/pager.rs @@ -23,7 +23,7 @@ use ratatui::{ text::{Line, Span}, widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap}, }; -use unicode_width::UnicodeWidthStr; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use crate::palette; use crate::tui::views::{ModalKind, ModalView, ViewAction, ViewEvent}; @@ -482,6 +482,14 @@ fn wrap_text(text: &str, width: usize) -> Vec { for word in text.split_whitespace() { let word_width = word.width(); + if word_width > width { + if !current.is_empty() { + lines.push(std::mem::take(&mut current)); + current_width = 0; + } + push_word_breaking_chars(word, width, &mut current, &mut current_width, &mut lines); + continue; + } let additional = if current.is_empty() { word_width } else { @@ -510,6 +518,24 @@ fn wrap_text(text: &str, width: usize) -> Vec { lines } +fn push_word_breaking_chars( + word: &str, + width: usize, + current: &mut String, + current_width: &mut usize, + lines: &mut Vec, +) { + for ch in word.chars() { + let char_width = ch.width().unwrap_or(1); + if *current_width + char_width > width && *current_width > 0 { + lines.push(std::mem::take(current)); + *current_width = 0; + } + current.push(ch); + *current_width += char_width; + } +} + #[cfg(test)] mod tests { use super::*; @@ -886,4 +912,16 @@ mod tests { "expected a Yellow/DarkGray highlight cell on the matched-line text columns" ); } + + #[test] + fn wrap_text_breaks_overlong_cjk_runs() { + let text = "这是一个非常长的中文字符串".repeat(10); + let lines = wrap_text(&text, 16); + + for line in &lines { + assert!(line.width() <= 16, "line {line:?} exceeds width 16"); + } + + assert_eq!(lines.join(""), text); + } }