1use crossterm::event::{KeyCode, KeyModifiers};
2
3use crate::components::{Component, Event, ViewContext};
4use crate::line::Line;
5use crate::rendering::frame::Frame;
6use crate::rendering::soft_wrap::{display_width_text, soft_wrap_text_byte_offset, soft_wrap_text_position};
7
8pub struct TextField {
10 pub value: String,
11 cursor_pos: usize,
12 content_width: usize,
13}
14
15impl TextField {
16 pub fn new(value: String) -> Self {
17 let cursor_pos = value.len();
18 Self { value, cursor_pos, content_width: usize::MAX }
19 }
20
21 pub fn set_content_width(&mut self, width: usize) {
22 self.content_width = width.max(1);
23 }
24
25 pub fn cursor_pos(&self) -> usize {
26 self.cursor_pos
27 }
28
29 pub fn set_cursor_pos(&mut self, pos: usize) {
30 self.cursor_pos = pos.min(self.value.len());
31 }
32
33 pub fn insert_at_cursor(&mut self, c: char) {
34 self.value.insert(self.cursor_pos, c);
35 self.cursor_pos += c.len_utf8();
36 }
37
38 pub fn insert_str_at_cursor(&mut self, s: &str) {
39 self.value.insert_str(self.cursor_pos, s);
40 self.cursor_pos += s.len();
41 }
42
43 pub fn delete_before_cursor(&mut self) -> bool {
44 let Some((prev, _)) = self.value[..self.cursor_pos].char_indices().next_back() else {
45 return false;
46 };
47 self.value.drain(prev..self.cursor_pos);
48 self.cursor_pos = prev;
49 true
50 }
51
52 pub fn set_value(&mut self, value: String) {
53 self.cursor_pos = value.len();
54 self.value = value;
55 }
56
57 pub fn clear(&mut self) {
58 self.value.clear();
59 self.cursor_pos = 0;
60 }
61
62 pub fn to_json(&self) -> serde_json::Value {
63 serde_json::Value::String(self.value.clone())
64 }
65
66 pub fn render_field(&self, context: &ViewContext, focused: bool) -> Vec<Line> {
67 let mut line = Line::new(&self.value);
68 if focused {
69 line.push_styled("▏", context.theme.primary());
70 }
71 vec![line]
72 }
73
74 pub fn single_line_window(&self, avail: usize) -> (String, usize) {
78 let cursor_col = display_width_text(&self.value[..self.cursor_pos]);
79 let scroll = cursor_col.saturating_sub(avail);
80 let mut visible = String::new();
81 let mut col = 0;
82
83 for (offset, ch) in self.value.char_indices() {
84 let char_width = display_width_text(&self.value[offset..offset + ch.len_utf8()]);
85 if col < scroll {
86 col += char_width;
87 continue;
88 }
89 if col + char_width > scroll + avail {
90 break;
91 }
92 visible.push(ch);
93 col += char_width;
94 }
95
96 (visible, cursor_col - scroll)
97 }
98
99 fn delete_after_cursor(&mut self) {
100 if let Some(c) = self.value[self.cursor_pos..].chars().next() {
101 self.value.drain(self.cursor_pos..self.cursor_pos + c.len_utf8());
102 }
103 }
104
105 fn delete_word_backward(&mut self) {
106 let end = self.cursor_pos;
107 let start = self.word_start_backward();
108 self.cursor_pos = start;
109 self.value.drain(start..end);
110 }
111
112 fn word_end_forward(&mut self) {
113 let len = self.value.len();
114 while self.cursor_pos < len {
115 let ch = self.value[self.cursor_pos..].chars().next().unwrap();
116 if ch.is_whitespace() {
117 break;
118 }
119 self.cursor_pos += ch.len_utf8();
120 }
121 while self.cursor_pos < len {
122 let ch = self.value[self.cursor_pos..].chars().next().unwrap();
123 if !ch.is_whitespace() {
124 break;
125 }
126 self.cursor_pos += ch.len_utf8();
127 }
128 }
129
130 fn move_cursor_up(&mut self, content_width: usize) {
131 if content_width == 0 {
132 return;
133 }
134 let (row, col) = self.visual_position(self.cursor_pos, content_width);
135 if row == 0 {
136 self.cursor_pos = 0;
137 } else {
138 self.cursor_pos = self.byte_pos_at_visual(row - 1, col, content_width);
139 }
140 }
141
142 fn move_cursor_down(&mut self, content_width: usize) {
143 if content_width == 0 {
144 return;
145 }
146 let (row, col) = self.visual_position(self.cursor_pos, content_width);
147 let max_row = self.visual_position(self.value.len(), content_width).0;
148 if row >= max_row {
149 self.cursor_pos = self.value.len();
150 } else {
151 self.cursor_pos = self.byte_pos_at_visual(row + 1, col, content_width);
152 }
153 }
154
155 fn word_start_backward(&self) -> usize {
156 let mut pos = self.cursor_pos;
157 while pos > 0 {
158 let (i, ch) = self.value[..pos].char_indices().next_back().unwrap();
159 if !ch.is_whitespace() {
160 break;
161 }
162 pos = i;
163 }
164 while pos > 0 {
165 let (i, ch) = self.value[..pos].char_indices().next_back().unwrap();
166 if ch.is_whitespace() {
167 break;
168 }
169 pos = i;
170 }
171 pos
172 }
173
174 fn hard_line_start(&self) -> usize {
175 self.value[..self.cursor_pos].rfind('\n').map_or(0, |pos| pos + '\n'.len_utf8())
176 }
177
178 fn hard_line_end(&self) -> usize {
179 self.value[self.cursor_pos..].find('\n').map_or(self.value.len(), |pos| self.cursor_pos + pos)
180 }
181
182 pub fn is_cursor_on_first_visual_line(&self) -> bool {
183 self.visual_position(self.cursor_pos, self.content_width).0 == 0
184 }
185
186 pub fn is_cursor_on_last_visual_line(&self) -> bool {
187 let cursor_row = self.visual_position(self.cursor_pos, self.content_width).0;
188 let max_row = self.visual_position(self.value.len(), self.content_width).0;
189 cursor_row >= max_row
190 }
191
192 fn visual_position(&self, byte_pos: usize, content_width: usize) -> (usize, usize) {
193 soft_wrap_text_position(&self.value, byte_pos, content_width)
194 }
195
196 fn byte_pos_at_visual(&self, target_row: usize, target_col: usize, content_width: usize) -> usize {
197 soft_wrap_text_byte_offset(&self.value, target_row, target_col, content_width)
198 }
199}
200
201impl Component for TextField {
202 type Message = ();
203
204 async fn on_event(&mut self, event: &Event) -> Option<Vec<Self::Message>> {
205 match event {
206 Event::Key(key) => {
207 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
208 let alt = key.modifiers.contains(KeyModifiers::ALT);
209 match key.code {
210 KeyCode::Char('a') if ctrl => {
211 self.cursor_pos = self.hard_line_start();
212 Some(vec![])
213 }
214 KeyCode::Char('e') if ctrl => {
215 self.cursor_pos = self.hard_line_end();
216 Some(vec![])
217 }
218 KeyCode::Char('w') if ctrl => {
219 self.delete_word_backward();
220 Some(vec![])
221 }
222 KeyCode::Char('u') if ctrl => {
223 self.value.drain(..self.cursor_pos);
224 self.cursor_pos = 0;
225 Some(vec![])
226 }
227 KeyCode::Char('k') if ctrl => {
228 self.value.truncate(self.cursor_pos);
229 Some(vec![])
230 }
231 KeyCode::Backspace if alt => {
232 self.delete_word_backward();
233 Some(vec![])
234 }
235 KeyCode::Left if alt || ctrl => {
236 self.cursor_pos = self.word_start_backward();
237 Some(vec![])
238 }
239 KeyCode::Char('b') if alt => {
240 self.cursor_pos = self.word_start_backward();
241 Some(vec![])
242 }
243 KeyCode::Right if alt || ctrl => {
244 self.word_end_forward();
245 Some(vec![])
246 }
247 KeyCode::Char('f') if alt => {
248 self.word_end_forward();
249 Some(vec![])
250 }
251 KeyCode::Delete => {
252 self.delete_after_cursor();
253 Some(vec![])
254 }
255 KeyCode::Char(c) if !ctrl => {
256 self.insert_at_cursor(c);
257 Some(vec![])
258 }
259 KeyCode::Backspace => {
260 self.delete_before_cursor();
261 Some(vec![])
262 }
263 KeyCode::Left => {
264 self.cursor_pos =
265 self.value[..self.cursor_pos].char_indices().next_back().map_or(0, |(i, _)| i);
266 Some(vec![])
267 }
268 KeyCode::Right => {
269 if let Some(c) = self.value[self.cursor_pos..].chars().next() {
270 self.cursor_pos += c.len_utf8();
271 }
272 Some(vec![])
273 }
274 KeyCode::Home => {
275 self.cursor_pos = 0;
276 Some(vec![])
277 }
278 KeyCode::End => {
279 self.cursor_pos = self.value.len();
280 Some(vec![])
281 }
282 KeyCode::Up => {
283 self.move_cursor_up(self.content_width);
284 Some(vec![])
285 }
286 KeyCode::Down => {
287 self.move_cursor_down(self.content_width);
288 Some(vec![])
289 }
290 _ => None,
291 }
292 }
293 Event::Paste(text) => {
294 self.insert_str_at_cursor(text);
295 Some(vec![])
296 }
297 _ => None,
298 }
299 }
300
301 fn render(&mut self, context: &ViewContext) -> Frame {
302 Frame::new(self.render_field(context, true))
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crossterm::event::{KeyEvent, KeyModifiers};
310
311 fn key(code: KeyCode) -> KeyEvent {
312 KeyEvent::new(code, KeyModifiers::NONE)
313 }
314 fn ctrl(code: KeyCode) -> KeyEvent {
315 KeyEvent::new(code, KeyModifiers::CONTROL)
316 }
317 fn alt(code: KeyCode) -> KeyEvent {
318 KeyEvent::new(code, KeyModifiers::ALT)
319 }
320 fn field(text: &str) -> TextField {
321 TextField::new(text.to_string())
322 }
323 fn field_at(text: &str, cursor: usize) -> TextField {
324 let mut f = field(text);
325 f.set_cursor_pos(cursor);
326 f
327 }
328
329 async fn send(f: &mut TextField, evt: Event) -> Option<Vec<()>> {
330 f.on_event(&evt).await
331 }
332 async fn send_key(f: &mut TextField, k: KeyEvent) -> Option<Vec<()>> {
333 send(f, Event::Key(k)).await
334 }
335
336 fn assert_state(f: &TextField, value: &str, cursor: usize) {
338 assert_eq!(f.value, value, "value mismatch");
339 assert_eq!(f.cursor_pos(), cursor, "cursor mismatch");
340 }
341
342 #[tokio::test]
343 async fn typing_appends_characters() {
344 let mut f = field("");
345 send_key(&mut f, key(KeyCode::Char('h'))).await;
346 send_key(&mut f, key(KeyCode::Char('i'))).await;
347 assert_eq!(f.value, "hi");
348 }
349
350 #[tokio::test]
351 async fn backspace_variants() {
352 let mut f = field("abc");
354 send_key(&mut f, key(KeyCode::Backspace)).await;
355 assert_eq!(f.value, "ab");
356
357 let mut f = field("");
359 send_key(&mut f, key(KeyCode::Backspace)).await;
360 assert_eq!(f.value, "");
361
362 let mut f = field_at("hello", 3);
364 send_key(&mut f, key(KeyCode::Backspace)).await;
365 assert_state(&f, "helo", 2);
366 }
367
368 #[test]
369 fn to_json_returns_string_value() {
370 assert_eq!(field("hello").to_json(), serde_json::json!("hello"));
371 }
372
373 #[tokio::test]
374 async fn unhandled_keys_are_ignored() {
375 let mut f = field("");
376 assert!(send_key(&mut f, key(KeyCode::F(1))).await.is_none());
377 }
378
379 #[tokio::test]
380 async fn paste_variants() {
381 let mut f = field("");
383 let outcome = send(&mut f, Event::Paste("hello".into())).await;
384 assert!(outcome.is_some());
385 assert_eq!(f.value, "hello");
386
387 let mut f = field_at("hd", 1);
389 send(&mut f, Event::Paste("ello worl".into())).await;
390 assert_state(&f, "hello world", 10);
391 }
392
393 #[test]
394 fn cursor_starts_at_end() {
395 assert_eq!(field("hello").cursor_pos(), 5);
396 }
397
398 #[tokio::test]
399 async fn cursor_movement_single_keys() {
400 let cases: Vec<(&str, Option<usize>, KeyEvent, usize)> = vec![
402 ("hello", None, key(KeyCode::Left), 4),
403 ("hello", None, key(KeyCode::Right), 5),
404 ("", None, key(KeyCode::Left), 0),
405 ("hello", None, key(KeyCode::Home), 0),
406 ("hello", Some(0), key(KeyCode::End), 5),
407 ("hello", None, ctrl(KeyCode::Char('a')), 0),
408 ("hello", Some(0), ctrl(KeyCode::Char('e')), 5),
409 ];
410 for (text, cursor, k, expected) in cases {
411 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
412 send_key(&mut f, k).await;
413 assert_eq!(f.cursor_pos(), expected, "failed for key {k:?} on {text:?}");
414 }
415 }
416
417 #[tokio::test]
418 async fn insert_at_middle() {
419 let mut f = field_at("hllo", 1);
420 send_key(&mut f, key(KeyCode::Char('e'))).await;
421 assert_state(&f, "hello", 2);
422 }
423
424 #[tokio::test]
425 async fn multibyte_utf8_navigation() {
426 let mut f = field("a中b");
427 assert_eq!(f.cursor_pos(), 5);
428 for expected in [4, 1, 0] {
429 send_key(&mut f, key(KeyCode::Left)).await;
430 assert_eq!(f.cursor_pos(), expected);
431 }
432 for expected in [1, 4] {
433 send_key(&mut f, key(KeyCode::Right)).await;
434 assert_eq!(f.cursor_pos(), expected);
435 }
436 }
437
438 #[test]
439 fn set_value_moves_cursor_to_end() {
440 let mut f = field("");
441 f.set_value("hello".to_string());
442 assert_state(&f, "hello", 5);
443 }
444
445 #[test]
446 fn clear_resets_cursor() {
447 let mut f = field("hello");
448 f.clear();
449 assert_state(&f, "", 0);
450 }
451
452 #[tokio::test]
453 async fn delete_forward_variants() {
454 let mut f = field_at("hello", 2);
456 send_key(&mut f, key(KeyCode::Delete)).await;
457 assert_state(&f, "helo", 2);
458
459 let mut f = field("hello");
461 send_key(&mut f, key(KeyCode::Delete)).await;
462 assert_eq!(f.value, "hello");
463
464 let mut f = field_at("a中b", 1);
466 send_key(&mut f, key(KeyCode::Delete)).await;
467 assert_state(&f, "ab", 1);
468 }
469
470 #[tokio::test]
471 async fn ctrl_w_variants() {
472 let cases: Vec<(&str, Option<usize>, &str, usize)> = vec![
474 ("hello world", None, "hello ", 6),
475 ("hello ", None, "", 0),
476 ("hello", Some(0), "hello", 0),
477 ("hello world", Some(8), "hello rld", 6),
478 ("", None, "", 0),
479 ];
480 for (text, cursor, exp_val, exp_cur) in cases {
481 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
482 send_key(&mut f, ctrl(KeyCode::Char('w'))).await;
483 assert_state(&f, exp_val, exp_cur);
484 }
485 }
486
487 #[tokio::test]
488 async fn alt_backspace_deletes_word() {
489 let mut f = field("hello world");
490 send_key(&mut f, alt(KeyCode::Backspace)).await;
491 assert_eq!(f.value, "hello ");
492 }
493
494 #[tokio::test]
495 async fn ctrl_u_variants() {
496 let mut f = field_at("hello world", 5);
497 send_key(&mut f, ctrl(KeyCode::Char('u'))).await;
498 assert_state(&f, " world", 0);
499
500 let mut f = field_at("hello", 0);
502 send_key(&mut f, ctrl(KeyCode::Char('u'))).await;
503 assert_state(&f, "hello", 0);
504 }
505
506 #[tokio::test]
507 async fn ctrl_k_variants() {
508 let mut f = field_at("hello world", 5);
509 send_key(&mut f, ctrl(KeyCode::Char('k'))).await;
510 assert_state(&f, "hello", 5);
511
512 let mut f = field("hello");
514 send_key(&mut f, ctrl(KeyCode::Char('k'))).await;
515 assert_eq!(f.value, "hello");
516 }
517
518 #[tokio::test]
519 async fn word_navigation() {
520 let cases: Vec<(&str, Option<usize>, KeyEvent, usize)> = vec![
522 ("hello world", None, alt(KeyCode::Left), 6),
523 ("hello world", Some(8), alt(KeyCode::Left), 6),
524 ("hello", Some(0), alt(KeyCode::Left), 0),
525 ("hello world", None, ctrl(KeyCode::Left), 6),
526 ("hello world", Some(0), alt(KeyCode::Right), 6),
527 ("hello", None, alt(KeyCode::Right), 5),
528 ("a中 b", Some(0), alt(KeyCode::Right), 5),
529 ("hello world", Some(0), ctrl(KeyCode::Right), 6),
530 ];
531 for (text, cursor, k, expected) in cases {
532 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
533 send_key(&mut f, k).await;
534 assert_eq!(f.cursor_pos(), expected, "failed for {k:?} on {text:?} at {cursor:?}");
535 }
536 }
537
538 #[test]
539 fn move_cursor_up_cases() {
540 let cases: Vec<(&str, Option<usize>, usize, usize)> =
542 vec![("hello world", Some(3), 10, 0), ("hello world", Some(8), 5, 2), ("hello", Some(3), 0, 3)];
543 for (text, cursor, width, expected) in cases {
544 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
545 f.move_cursor_up(width);
546 assert_eq!(f.cursor_pos(), expected, "up failed: {text:?} cursor={cursor:?} w={width}");
547 }
548 }
549
550 #[test]
551 fn move_cursor_up_wide_chars() {
552 let mut f = field("中中中中中");
553 f.move_cursor_up(5);
554 assert_eq!(f.cursor_pos(), 9);
555 }
556
557 #[test]
558 fn move_cursor_down_cases() {
559 let cases: Vec<(&str, Option<usize>, usize, usize)> = vec![
561 ("hello world", Some(0), 20, 11),
562 ("hello world", Some(3), 5, 9),
563 ("hello world", Some(8), 5, 11),
564 ("", None, 10, 0),
565 ];
566 for (text, cursor, width, expected) in cases {
567 let mut f = cursor.map_or_else(|| field(text), |c| field_at(text, c));
568 f.move_cursor_down(width);
569 assert_eq!(f.cursor_pos(), expected, "down failed: {text:?} cursor={cursor:?} w={width}");
570 }
571 }
572
573 #[test]
574 fn is_cursor_on_first_visual_line() {
575 let mut f = field_at("hello world", 3);
576 f.set_content_width(5);
577 assert!(f.is_cursor_on_first_visual_line());
578
579 let mut f = field_at("hello world", 8);
580 f.set_content_width(5);
581 assert!(!f.is_cursor_on_first_visual_line());
582 }
583
584 #[test]
585 fn is_cursor_on_last_visual_line() {
586 let mut f = field_at("hello world", 11);
587 f.set_content_width(5);
588 assert!(f.is_cursor_on_last_visual_line());
589
590 let mut f = field_at("hello world", 3);
591 f.set_content_width(5);
592 assert!(!f.is_cursor_on_last_visual_line());
593
594 let mut f = field_at("hello world", 8);
595 f.set_content_width(5);
596 assert!(f.is_cursor_on_last_visual_line());
597 }
598
599 #[test]
600 fn single_line_is_both_first_and_last() {
601 let mut f = field_at("hello", 3);
602 f.set_content_width(20);
603 assert!(f.is_cursor_on_first_visual_line());
604 assert!(f.is_cursor_on_last_visual_line());
605 }
606
607 #[test]
608 fn empty_field_is_both_first_and_last() {
609 let mut f = field("");
610 f.set_content_width(20);
611 assert!(f.is_cursor_on_first_visual_line());
612 assert!(f.is_cursor_on_last_visual_line());
613 }
614}