1use crate::utils;
20
21use super::row::CursorCoord;
22use unicode_segmentation::UnicodeSegmentation;
23
24pub fn prompt_cursor(text: &str, cursor: usize, body_width: usize, indent: usize) -> CursorCoord {
35 let body_width = body_width.max(1);
36 let mut row = 0usize;
37 let mut col = indent;
38 let mut pos = 0usize;
39
40 for grapheme in text.graphemes(true) {
41 if pos == cursor {
42 return CursorCoord { row, col };
43 }
44
45 if grapheme.contains('\n') {
46 row += 1;
47 col = indent;
48 pos += 1;
49 continue;
50 }
51
52 let g_width = utils::grapheme_width(grapheme);
53 if col - indent + g_width > body_width {
54 row += 1;
55 col = indent;
56 }
57 col += g_width;
58 pos += 1;
59 }
60
61 CursorCoord { row, col }
62}
63
64pub fn prompt_rows(text: &str, body_width: usize) -> Vec<String> {
71 let body_width = body_width.max(1);
72 let mut rows = Vec::new();
73 let mut current = String::new();
74 let mut current_width = 0usize;
75
76 for grapheme in text.graphemes(true) {
77 if grapheme.contains('\n') {
78 rows.push(std::mem::take(&mut current));
79 current_width = 0;
80 continue;
81 }
82 let g_width = utils::grapheme_width(grapheme);
83 if current_width > 0 && current_width + g_width > body_width {
84 rows.push(std::mem::take(&mut current));
85 current_width = 0;
86 }
87 current.push_str(grapheme);
88 current_width += g_width;
89 }
90 rows.push(current);
91 rows
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn single_line_cursor_at_end() {
100 let coord = prompt_cursor("hello", 5, 80, 0);
101 assert_eq!(coord, CursorCoord { row: 0, col: 5 });
102 }
103
104 #[test]
105 fn single_line_cursor_in_middle() {
106 let coord = prompt_cursor("hello", 2, 80, 0);
107 assert_eq!(coord, CursorCoord { row: 0, col: 2 });
108 }
109
110 #[test]
111 fn single_line_cursor_at_start() {
112 let coord = prompt_cursor("hello", 0, 80, 0);
113 assert_eq!(coord, CursorCoord { row: 0, col: 0 });
114 }
115
116 #[test]
117 fn empty_input_cursor_at_origin() {
118 let coord = prompt_cursor("", 0, 80, 0);
119 assert_eq!(coord, CursorCoord { row: 0, col: 0 });
120 }
121
122 #[test]
123 fn wrapped_line_cursor_on_second_row() {
124 let coord = prompt_cursor("abcdef", 4, 3, 0);
125 assert_eq!(coord, CursorCoord { row: 1, col: 1 });
126 }
127
128 #[test]
129 fn wrapped_line_cursor_at_wrap_boundary() {
130 let coord = prompt_cursor("abcdef", 3, 3, 0);
131 assert_eq!(coord, CursorCoord { row: 0, col: 3 });
132 }
133
134 #[test]
135 fn explicit_newline_cursor_on_second_line() {
136 let coord = prompt_cursor("line1\nline2", 7, 80, 0);
137 assert_eq!(coord, CursorCoord { row: 1, col: 1 });
138 }
139
140 #[test]
141 fn explicit_newline_cursor_at_line_start() {
142 let coord = prompt_cursor("line1\nline2", 6, 80, 0);
143 assert_eq!(coord, CursorCoord { row: 1, col: 0 });
144 }
145
146 #[test]
147 fn explicit_newline_cursor_at_newline_char() {
148 let coord = prompt_cursor("line1\nline2", 5, 80, 0);
149 assert_eq!(coord, CursorCoord { row: 0, col: 5 });
150 }
151
152 #[test]
153 fn indented_prompt_offsets_column() {
154 let coord = prompt_cursor("hello", 2, 80, 3);
155 assert_eq!(coord, CursorCoord { row: 0, col: 5 });
156 }
157
158 #[test]
159 fn indented_wrapped_line_resets_indent() {
160 let coord = prompt_cursor("abcdef", 4, 3, 3);
161 assert_eq!(coord, CursorCoord { row: 1, col: 4 });
162 }
163
164 #[test]
165 fn indented_multiline_resets_indent() {
166 let coord = prompt_cursor("ab\ncd", 4, 80, 3);
167 assert_eq!(coord, CursorCoord { row: 1, col: 4 });
168 }
169
170 #[test]
171 fn multibyte_cursor_counts_graphemes_not_bytes() {
172 let coord = prompt_cursor("héllo", 2, 80, 0);
173 assert_eq!(coord, CursorCoord { row: 0, col: 2 });
174 }
175
176 #[test]
177 fn multibyte_wrapped_cursor() {
178 let coord = prompt_cursor("éééé", 3, 2, 0);
179 assert_eq!(coord, CursorCoord { row: 1, col: 1 });
180 }
181
182 #[test]
183 fn cursor_beyond_end_clamps_to_final_row() {
184 let coord = prompt_cursor("ab", 99, 80, 0);
185 assert_eq!(coord, CursorCoord { row: 0, col: 2 });
186 }
187
188 #[test]
189 fn cursor_beyond_end_multiline() {
190 let coord = prompt_cursor("ab\ncd", 99, 80, 0);
191 assert_eq!(coord, CursorCoord { row: 1, col: 2 });
192 }
193
194 #[test]
195 fn prompt_rows_single_line() {
196 let rows = prompt_rows("hello", 80);
197 assert_eq!(rows, vec!["hello"]);
198 }
199
200 #[test]
201 fn prompt_rows_explicit_newline() {
202 let rows = prompt_rows("ab\ncd", 80);
203 assert_eq!(rows, vec!["ab", "cd"]);
204 }
205
206 #[test]
207 fn prompt_rows_wrapped() {
208 let rows = prompt_rows("abcdef", 3);
209 assert_eq!(rows, vec!["abc", "def"]);
210 }
211
212 #[test]
213 fn prompt_rows_empty() {
214 let rows = prompt_rows("", 80);
215 assert_eq!(rows, vec![""]);
216 }
217
218 #[test]
219 fn combined_wrap_and_newline() {
220 let rows = prompt_rows("abcd\nef", 2);
221 assert_eq!(rows, vec!["ab", "cd", "ef"]);
222
223 let coord = prompt_cursor("abcd\nef", 6, 2, 0);
224 assert_eq!(coord, CursorCoord { row: 2, col: 1 });
225 }
226
227 #[test]
228 fn cjk_wide_char_cursor_column() {
229 let coord = prompt_cursor("中", 1, 80, 0);
230 assert_eq!(coord, CursorCoord { row: 0, col: 2 });
231 }
232
233 #[test]
234 fn cjk_wide_char_wraps_correctly() {
235 let rows = prompt_rows("中中", 3);
236 assert_eq!(rows, vec!["中", "中"]);
237 }
238
239 #[test]
240 fn cjk_wide_char_cursor_after_wrap() {
241 let coord = prompt_cursor("中中", 1, 3, 0);
242 assert_eq!(coord, CursorCoord { row: 0, col: 2 });
243 }
244
245 #[test]
246 fn cjk_mixed_with_ascii_wraps_by_width() {
247 let rows = prompt_rows("a中b", 3);
248 assert_eq!(rows, vec!["a中", "b"]);
249 }
250
251 #[test]
252 fn combining_mark_does_not_advance_column() {
253 let text = "e\u{0301}";
254 let coord = prompt_cursor(text, 1, 80, 0);
255 assert_eq!(coord, CursorCoord { row: 0, col: 1 });
256 }
257
258 #[test]
259 fn combining_mark_does_not_cause_extra_wrap() {
260 let text = "ab\u{0327}";
261 let rows = prompt_rows(text, 2);
262 assert_eq!(rows, vec!["ab\u{0327}"], "combining mark should not force a wrap");
263 }
264
265 #[test]
266 fn emoji_width_2_cursor() {
267 let coord = prompt_cursor("\u{1F600}", 1, 80, 0);
268 assert_eq!(coord, CursorCoord { row: 0, col: 2 });
269 }
270
271 #[test]
272 fn emoji_zwj_one_grapheme_cursor() {
273 let text = "a\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}b";
274 let coord = prompt_cursor(text, 1, 80, 0);
275 assert_eq!(coord, CursorCoord { row: 0, col: 1 });
276
277 let coord = prompt_cursor(text, 2, 80, 0);
278 assert_eq!(coord, CursorCoord { row: 0, col: 3 });
279 }
280
281 #[test]
282 fn emoji_wraps_by_width() {
283 let rows = prompt_rows("\u{1F600}\u{1F600}", 3);
284 assert_eq!(rows, vec!["\u{1F600}", "\u{1F600}"]);
285 }
286
287 #[test]
288 fn zwnj_does_not_advance_width() {
289 let text = "ab\u{200C}cd";
290 let coord = prompt_cursor(text, 3, 80, 0);
291 assert_eq!(coord, CursorCoord { row: 0, col: 3 });
292 }
293}