feat(prompt): add support for wide characters in prompt preview
Some checks failed
Publish Docker Images / coverage-and-sonar (push) Failing after 5m24s
Some checks failed
Publish Docker Images / coverage-and-sonar (push) Failing after 5m24s
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1664,6 +1664,7 @@ dependencies = [
|
||||
"textwrap",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -31,6 +31,7 @@ lazy-regex = { version = "3.5.1", features = ["lite"] }
|
||||
thiserror = "2.0.18"
|
||||
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] }
|
||||
textwrap = "0.16.2"
|
||||
unicode-width = "0.2.2"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.1.2"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//! in production while accepting mock implementations in tests.
|
||||
|
||||
use inquire::{Confirm, Text};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
commit::types::{BreakingChange, CommitType, Description, Scope},
|
||||
@@ -40,14 +41,19 @@ pub trait Prompter: Send + Sync {
|
||||
}
|
||||
|
||||
fn format_message_box(message: &str) -> String {
|
||||
let preview_width = 72 + 2; // max width + space padding
|
||||
let preview_width = message
|
||||
.split('\n')
|
||||
.map(|line| line.width())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
.max(72);
|
||||
let mut lines: Vec<String> = Vec::new();
|
||||
lines.push(format!("┌{}┐", "─".repeat(preview_width)));
|
||||
for line in message.split("\n") {
|
||||
let padding = 72_usize.saturating_sub(line.chars().count());
|
||||
lines.push(format!("┌{}┐", "─".repeat(preview_width + 2)));
|
||||
for line in message.split('\n') {
|
||||
let padding = preview_width.saturating_sub(line.width());
|
||||
lines.push(format!("│ {line}{:padding$} │", ""));
|
||||
}
|
||||
lines.push(format!("└{}┘", "─".repeat(preview_width)));
|
||||
lines.push(format!("└{}┘", "─".repeat(preview_width + 2)));
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
@@ -270,4 +276,91 @@ mod tests {
|
||||
let expected = format!("│ {line_72} │");
|
||||
assert_eq!(lines[1], expected);
|
||||
}
|
||||
|
||||
/// A single CJK character (display width 2) is padded as if it occupies 2 columns,
|
||||
/// not 1 — so the right-hand padding is 70 spaces, not 71
|
||||
#[test]
|
||||
fn format_message_box_single_cjk_char() {
|
||||
let result = format_message_box("字");
|
||||
let lines: Vec<&str> = result.split('\n').collect();
|
||||
let expected = format!("│ 字{:70} │", "");
|
||||
assert_eq!(lines[1], expected);
|
||||
}
|
||||
|
||||
/// A single emoji (display width 2) is padded as if it occupies 2 columns
|
||||
#[test]
|
||||
fn format_message_box_single_emoji() {
|
||||
let result = format_message_box("🦀");
|
||||
let lines: Vec<&str> = result.split('\n').collect();
|
||||
let expected = format!("│ 🦀{:70} │", "");
|
||||
assert_eq!(lines[1], expected);
|
||||
}
|
||||
|
||||
/// Mixed ASCII and CJK: padding accounts for the display width of the whole line
|
||||
///
|
||||
/// "feat: " = 6 display cols, "漢字" = 4 display cols → total 10, padding = 62
|
||||
#[test]
|
||||
fn format_message_box_mixed_ascii_and_cjk() {
|
||||
let result = format_message_box("feat: 漢字");
|
||||
let lines: Vec<&str> = result.split('\n').collect();
|
||||
let expected = format!("│ feat: 漢字{:62} │", "");
|
||||
assert_eq!(lines[1], expected);
|
||||
}
|
||||
|
||||
/// When a line exceeds 72 display columns the border expands to fit (width + 2 dashes)
|
||||
#[test]
|
||||
fn format_message_box_border_expands_beyond_72() {
|
||||
let line_73 = "a".repeat(73);
|
||||
let result = format_message_box(&line_73);
|
||||
let lines: Vec<&str> = result.split('\n').collect();
|
||||
let dashes = "─".repeat(75); // 73 + 2
|
||||
assert_eq!(lines[0], format!("┌{dashes}┐"));
|
||||
assert_eq!(lines[lines.len() - 1], format!("└{dashes}┘"));
|
||||
}
|
||||
|
||||
/// A line that sets the box width gets zero right-hand padding
|
||||
#[test]
|
||||
fn format_message_box_widest_line_has_no_padding() {
|
||||
let line_73 = "a".repeat(73);
|
||||
let result = format_message_box(&line_73);
|
||||
let lines: Vec<&str> = result.split('\n').collect();
|
||||
assert_eq!(lines[1], format!("│ {line_73} │"));
|
||||
}
|
||||
|
||||
/// In a multi-line message, shorter lines are padded out to match the widest line
|
||||
#[test]
|
||||
fn format_message_box_shorter_lines_padded_to_widest() {
|
||||
let long_line = "a".repeat(80);
|
||||
let result = format_message_box(&format!("{long_line}\nshort"));
|
||||
let lines: Vec<&str> = result.split('\n').collect();
|
||||
assert_eq!(lines[1], format!("│ {long_line} │"));
|
||||
assert_eq!(lines[2], format!("│ short{:75} │", "")); // 80 - 5 = 75
|
||||
}
|
||||
|
||||
/// All rows have equal char count when the box expands beyond 72
|
||||
#[test]
|
||||
fn format_message_box_all_rows_same_width_when_expanded() {
|
||||
let long_line = "a".repeat(80);
|
||||
let result = format_message_box(&format!("{long_line}\nshort"));
|
||||
let widths: Vec<usize> = result.split('\n').map(|l| l.chars().count()).collect();
|
||||
let expected = widths[0];
|
||||
assert!(
|
||||
widths.iter().all(|&w| w == expected),
|
||||
"rows have differing widths: {:?}",
|
||||
widths
|
||||
);
|
||||
}
|
||||
|
||||
/// Wide characters can also trigger box expansion beyond 72 columns
|
||||
///
|
||||
/// 37 CJK characters × 2 display columns = 74 display columns → border uses 76 dashes
|
||||
#[test]
|
||||
fn format_message_box_wide_chars_expand_box() {
|
||||
let wide_line = "字".repeat(37); // 74 display cols
|
||||
let result = format_message_box(&wide_line);
|
||||
let lines: Vec<&str> = result.split('\n').collect();
|
||||
let dashes = "─".repeat(76); // 74 + 2
|
||||
assert_eq!(lines[0], format!("┌{dashes}┐"));
|
||||
assert_eq!(lines[1], format!("│ {wide_line} │")); // no padding
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user