fix(tui): wrap CJK runs in diff and pager

Hard-wrap overlong CJK/no-whitespace runs in diff and pager text wrappers so they do not overflow the right edge.

Fixes #1571.

Co-authored-by: Aitensa <1900013029@pku.edu.cn>
This commit is contained in:
Eosin Ai
2026-05-15 02:46:58 +08:00
committed by GitHub
parent eef16f45fd
commit f7eb17b00f
2 changed files with 78 additions and 2 deletions
+39 -1
View File
@@ -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<String> {
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<String> {
lines
}
fn push_word_breaking_chars(
word: &str,
width: usize,
current: &mut String,
current_width: &mut usize,
lines: &mut Vec<String>,
) {
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);
}
}
+39 -1
View File
@@ -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<String> {
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<String> {
lines
}
fn push_word_breaking_chars(
word: &str,
width: usize,
current: &mut String,
current_width: &mut usize,
lines: &mut Vec<String>,
) {
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);
}
}