1use unicode_width::UnicodeWidthStr;
6
7#[must_use]
18pub fn expand_tabs(s: &str, tab_width: usize) -> String {
19 let mut result = String::with_capacity(s.len());
20 let mut column = 0;
21
22 for ch in s.chars() {
23 if ch == '\t' {
24 let spaces = tab_width - (column % tab_width);
26 result.push_str(&" ".repeat(spaces));
27 column += spaces;
28 } else if !ch.is_control() {
29 result.push(ch);
31 column += ch.to_string().width();
32 }
33 }
35
36 result
37}
38
39#[must_use]
54pub fn truncate_chars(s: &str, max_chars: usize) -> String {
55 if max_chars == 0 {
56 return String::new();
57 }
58
59 let char_count = s.chars().count();
60 if char_count <= max_chars {
61 s.to_string()
62 } else if max_chars <= 3 {
63 s.chars().take(max_chars).collect()
64 } else {
65 format!("{}...", s.chars().take(max_chars - 3).collect::<String>())
66 }
67}
68
69#[must_use]
80pub fn truncate_width(s: &str, max_width: usize) -> String {
81 if max_width == 0 {
82 return String::new();
83 }
84
85 let s_width = s.width();
86 if s_width <= max_width {
87 return s.to_string();
88 }
89
90 if max_width <= 1 {
91 return ".".to_string();
92 }
93
94 let target_width = max_width - 1;
96
97 let mut result = String::new();
98 let mut current_width = 0;
99
100 for ch in s.chars() {
101 let ch_width = ch.to_string().width();
102 if current_width + ch_width > target_width {
103 break;
104 }
105 result.push(ch);
106 current_width += ch_width;
107 }
108
109 result.push('…');
110 result
111}
112
113#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn test_truncate_chars_no_truncation() {
123 assert_eq!(truncate_chars("hello", 10), "hello");
124 assert_eq!(truncate_chars("hello", 5), "hello");
125 }
126
127 #[test]
128 fn test_truncate_chars_with_truncation() {
129 assert_eq!(truncate_chars("hello world", 8), "hello...");
130 assert_eq!(truncate_chars("hello world", 6), "hel...");
131 }
132
133 #[test]
134 fn test_truncate_chars_edge_cases() {
135 assert_eq!(truncate_chars("hello", 0), "");
136 assert_eq!(truncate_chars("hello", 3), "hel");
137 assert_eq!(truncate_chars("hello", 2), "he");
138 }
139
140 #[test]
141 fn test_truncate_width_no_truncation() {
142 assert_eq!(truncate_width("hello", 10), "hello");
143 assert_eq!(truncate_width("hello", 5), "hello");
144 }
145
146 #[test]
147 fn test_truncate_width_with_truncation() {
148 assert_eq!(truncate_width("hello world", 8), "hello w…");
149 assert_eq!(truncate_width("hello world", 6), "hello…");
150 }
151
152 #[test]
153 fn test_truncate_width_edge_cases() {
154 assert_eq!(truncate_width("hello", 0), "");
155 assert_eq!(truncate_width("hello", 1), ".");
156 assert_eq!(truncate_width("hello", 2), "h…");
157 }
158
159 #[test]
160 fn test_truncate_width_unicode() {
161 let cjk = "你好世界"; assert_eq!(cjk.width(), 8);
164
165 let result = truncate_width(cjk, 6);
166 assert!(result.width() <= 6);
167 }
168}