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",
|
"textwrap",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ lazy-regex = { version = "3.5.1", features = ["lite"] }
|
|||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] }
|
||||||
textwrap = "0.16.2"
|
textwrap = "0.16.2"
|
||||||
|
unicode-width = "0.2.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2.1.2"
|
assert_cmd = "2.1.2"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
//! in production while accepting mock implementations in tests.
|
//! in production while accepting mock implementations in tests.
|
||||||
|
|
||||||
use inquire::{Confirm, Text};
|
use inquire::{Confirm, Text};
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
commit::types::{BreakingChange, CommitType, Description, Scope},
|
commit::types::{BreakingChange, CommitType, Description, Scope},
|
||||||
@@ -40,14 +41,19 @@ pub trait Prompter: Send + Sync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn format_message_box(message: &str) -> String {
|
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();
|
let mut lines: Vec<String> = Vec::new();
|
||||||
lines.push(format!("┌{}┐", "─".repeat(preview_width)));
|
lines.push(format!("┌{}┐", "─".repeat(preview_width + 2)));
|
||||||
for line in message.split("\n") {
|
for line in message.split('\n') {
|
||||||
let padding = 72_usize.saturating_sub(line.chars().count());
|
let padding = preview_width.saturating_sub(line.width());
|
||||||
lines.push(format!("│ {line}{:padding$} │", ""));
|
lines.push(format!("│ {line}{:padding$} │", ""));
|
||||||
}
|
}
|
||||||
lines.push(format!("└{}┘", "─".repeat(preview_width)));
|
lines.push(format!("└{}┘", "─".repeat(preview_width + 2)));
|
||||||
lines.join("\n")
|
lines.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,4 +276,91 @@ mod tests {
|
|||||||
let expected = format!("│ {line_72} │");
|
let expected = format!("│ {line_72} │");
|
||||||
assert_eq!(lines[1], expected);
|
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