1#[cfg(feature = "steel")]
4use steel_derive::Steel;
5use unicode_width::UnicodeWidthStr;
6
7use crate::{
8 errors::AppError,
9 keyboard::{KeyCode, KeyModifiers},
10};
11
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub enum Field {
14 Text(TextField),
15 Checkbox(CheckboxField),
16}
17
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct FieldError {
20 pub short: String,
21 pub long: String,
22}
23
24#[derive(Clone, Debug, Eq, PartialEq)]
25#[cfg_attr(feature = "steel", derive(Steel))]
26pub struct TextField {
27 text: String,
28 cursor_idx: usize,
29 error: Option<FieldError>,
30}
31
32impl TextField {
35 pub fn new(initial: &str) -> Self {
36 Self {
37 text: initial.to_string(),
38 cursor_idx: initial.chars().count(),
39 error: None,
40 }
41 }
42
43 pub fn text(&self) -> &str {
44 &self.text
45 }
46
47 pub fn visual_cursor_pos(&self) -> usize {
48 let prefix: String = self.text.chars().take(self.cursor_idx).collect();
49 UnicodeWidthStr::width(prefix.as_str())
50 }
51
52 pub fn move_cursor_left(&mut self) {
53 self.move_cursor_left_by(1);
54 }
55
56 pub fn move_cursor_start(&mut self) {
57 self.cursor_idx = 0;
58 }
59
60 fn move_cursor_left_by(&mut self, n: usize) {
61 let cursor_moved_left = self.cursor_idx.saturating_sub(n);
62 self.cursor_idx = self.clamp_cursor(cursor_moved_left);
63 }
64
65 pub fn move_cursor_right(&mut self) {
66 self.move_cursor_right_by(1);
67 }
68
69 fn move_cursor_right_by(&mut self, n: usize) {
70 let cursor_moved_right = self.cursor_idx.saturating_add(n);
71 self.cursor_idx = self.clamp_cursor(cursor_moved_right);
72 }
73
74 pub fn move_cursor_end(&mut self) {
75 self.cursor_idx = self.text.chars().count();
76 }
77
78 pub fn enter_char(&mut self, new_char: char) {
79 let index = self.byte_index();
80 self.text.insert(index, new_char);
81 self.move_cursor_right();
82 }
83
84 fn byte_index(&self) -> usize {
85 self.text
86 .char_indices()
87 .map(|(i, _)| i)
88 .nth(self.cursor_idx)
89 .unwrap_or(self.text.len())
90 }
91
92 pub fn delete_char(&mut self) {
93 if self.cursor_idx == 0 {
94 return;
95 }
96
97 let before_char = self.text.chars().take(self.cursor_idx - 1);
98 let after_char = self.text.chars().skip(self.cursor_idx);
99
100 self.text = before_char.chain(after_char).collect();
101 self.move_cursor_left();
102 }
103
104 pub fn delete_char_forward(&mut self) {
105 let before_char = self.text.chars().take(self.cursor_idx);
106 let after_char = self.text.chars().skip(self.cursor_idx + 1);
107
108 self.text = before_char.chain(after_char).collect();
109 }
110
111 fn previous_word_start(&self) -> usize {
112 if self.cursor_idx == 0 {
113 return 0;
114 }
115
116 let before_char = self.text.chars().take(self.cursor_idx).collect::<Vec<_>>();
117 let mut idx = self.cursor_idx - 1;
118 while idx > 0 && before_char[idx] == ' ' {
119 idx -= 1;
120 }
121 while idx > 0 && before_char[idx - 1] != ' ' {
122 idx -= 1;
123 }
124 idx
125 }
126
127 pub fn move_cursor_back_word(&mut self) {
128 self.cursor_idx = self.previous_word_start();
129 }
130
131 pub fn delete_word_backward(&mut self) {
132 let new_cursor_pos = self.previous_word_start();
133 let before_char = self.text.chars().take(new_cursor_pos);
134 let after_char = self.text.chars().skip(self.cursor_idx);
135
136 self.text = before_char.chain(after_char).collect();
137 self.cursor_idx = new_cursor_pos;
138 }
139
140 fn next_word_start(&self) -> usize {
141 let after_char = self.text.chars().skip(self.cursor_idx).collect::<Vec<_>>();
142 let mut idx = 0;
143 let num_chars = after_char.len();
144 while idx < num_chars && after_char[idx] != ' ' {
145 idx += 1;
146 }
147 while idx < num_chars && after_char[idx] == ' ' {
148 idx += 1;
149 }
150 self.cursor_idx + idx
151 }
152
153 pub fn move_cursor_forward_word(&mut self) {
154 self.cursor_idx = self.next_word_start();
155 }
156
157 pub fn delete_word_forward(&mut self) {
158 let before_char = self.text.chars().take(self.cursor_idx);
159 let after_char = self.text.chars().skip(self.next_word_start());
160
161 self.text = before_char.chain(after_char).collect();
162 }
163
164 fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
165 new_cursor_pos.clamp(0, self.text.chars().count())
166 }
167
168 pub fn clear(&mut self) {
169 self.text.clear();
170 self.cursor_idx = 0;
171 }
172
173 pub fn set_text(&mut self, text: &str) {
174 self.text = text.to_string();
175 self.cursor_idx = self.clamp_cursor(text.chars().count());
176 }
177
178 pub fn insert_text(&mut self, text: &str) {
179 let index = self.byte_index();
180 self.text.insert_str(index, text);
181 self.cursor_idx += text.chars().count();
182 }
183
184 pub fn cursor_idx(&self) -> usize {
185 self.cursor_idx
186 }
187
188 pub fn set_cursor_idx(&mut self, idx: usize) {
189 self.cursor_idx = self.clamp_cursor(idx);
190 }
191
192 pub fn delete_to_start(&mut self) {
193 self.text = self.text.chars().skip(self.cursor_idx).collect();
194 self.cursor_idx = 0;
195 }
196
197 pub fn set_error(&mut self, short: String, long: String) {
198 self.error = Some(FieldError { short, long });
199 }
200
201 pub fn clear_error(&mut self) {
202 self.error = None;
203 }
204
205 fn handle_keys(&mut self, code: KeyCode, modifiers: KeyModifiers) {
206 match (code, modifiers) {
207 (KeyCode::Char('w'), KeyModifiers::CONTROL)
208 | (KeyCode::Backspace, KeyModifiers::ALT) => {
209 self.delete_word_backward();
210 }
211 (KeyCode::Char('u'), KeyModifiers::CONTROL)
212 | (KeyCode::Backspace, KeyModifiers::META) => {
213 self.delete_to_start();
214 }
215 (KeyCode::Backspace, _) => {
216 self.delete_char();
217 }
218 (KeyCode::Left | KeyCode::Char('b' | 'B'), _)
219 if modifiers.contains(KeyModifiers::ALT) =>
220 {
221 self.move_cursor_back_word();
222 }
223 (KeyCode::Home, _) => {
224 self.move_cursor_start();
225 }
226 (KeyCode::Left, _) => {
227 self.move_cursor_left();
228 }
229 (KeyCode::Right | KeyCode::Char('f' | 'F'), _)
230 if modifiers.contains(KeyModifiers::ALT) =>
231 {
232 self.move_cursor_forward_word();
233 }
234 (KeyCode::Right, KeyModifiers::META) | (KeyCode::End, _) => {
235 self.move_cursor_end();
236 }
237 (KeyCode::Right, _) => {
238 self.move_cursor_right();
239 }
240 (KeyCode::Char('d') | KeyCode::Delete, KeyModifiers::ALT) => {
241 self.delete_word_forward();
242 }
243 (KeyCode::Delete, _) => {
244 self.delete_char_forward();
245 }
246 (KeyCode::Char(value), _) => {
247 self.enter_char(value);
248 }
249 _ => {}
250 }
251 }
252}
253
254#[derive(Clone, Debug, Eq, PartialEq)]
255pub struct CheckboxField {
256 pub checked: bool,
257 pub error: Option<FieldError>, }
259
260impl CheckboxField {
261 pub fn new(initial: bool) -> Self {
262 Self {
263 checked: initial,
264 error: None,
265 }
266 }
267
268 pub fn handle_keys(&mut self, code: KeyCode, _modifiers: KeyModifiers) {
269 if code == KeyCode::Char(' ') {
270 self.checked = !self.checked;
271 }
272 }
273}
274
275#[derive(Clone, Debug, Eq, PartialEq)]
276pub enum FieldName {
277 Search,
278 Replace,
279 FixedStrings,
280 WholeWord,
281 MatchCase,
282 IncludeFiles,
283 ExcludeFiles,
284}
285
286impl FieldName {
287 pub fn title(&self) -> &str {
288 match self {
289 FieldName::Search => "Search text",
290 FieldName::Replace => "Replace text",
291 FieldName::FixedStrings => "Fixed strings",
292 FieldName::WholeWord => "Match whole word",
293 FieldName::MatchCase => "Match case",
294 FieldName::IncludeFiles => "Files to include",
295 FieldName::ExcludeFiles => "Files to exclude",
296 }
297 }
298}
299
300#[derive(Clone, Debug, Eq, PartialEq)]
301pub struct SearchField {
302 pub name: FieldName,
303 pub field: Field,
304 pub set_by_cli: bool,
305}
306
307impl SearchField {
308 pub fn new_text(name: FieldName, initial: &str, set_by_cli: bool) -> Self {
309 let field = Field::Text(TextField::new(initial));
310 Self {
311 name,
312 field,
313 set_by_cli,
314 }
315 }
316
317 pub fn new_checkbox(name: FieldName, initial: bool, set_by_cli: bool) -> Self {
318 let field = Field::Checkbox(CheckboxField::new(initial));
319 Self {
320 name,
321 field,
322 set_by_cli,
323 }
324 }
325
326 pub fn handle_keys(
327 &mut self,
328 code: KeyCode,
329 modifiers: KeyModifiers,
330 disable_prepopulated_fields: bool,
331 ) {
332 if self.set_by_cli && disable_prepopulated_fields {
333 return;
334 }
335 self.clear_error();
336 match &mut self.field {
337 Field::Text(f) => f.handle_keys(code, modifiers),
338 Field::Checkbox(f) => f.handle_keys(code, modifiers),
339 }
340 }
341
342 pub fn cursor_pos(&self) -> Option<usize> {
343 match &self.field {
344 Field::Text(f) => Some(f.visual_cursor_pos()),
345 Field::Checkbox(_) => None,
346 }
347 }
348
349 pub fn clear_error(&mut self) {
350 match &mut self.field {
351 Field::Text(f) => f.clear_error(),
352 Field::Checkbox(_) => {} }
354 }
355
356 pub fn error(&self) -> Option<FieldError> {
357 match &self.field {
358 Field::Text(f) => f.error.clone(),
359 Field::Checkbox(f) => f.error.clone(),
360 }
361 }
362}
363
364pub const NUM_SEARCH_FIELDS: u16 = 7;
365
366#[derive(Clone, Debug, Eq, PartialEq)]
367pub struct SearchFields {
368 pub fields: [SearchField; NUM_SEARCH_FIELDS as usize],
369 pub highlighted: usize,
370}
371
372macro_rules! define_field_accessor {
373 ($method_name:ident, $field_name:expr, $field_variant:ident, $return_type:ty) => {
374 pub fn $method_name(&self) -> $return_type {
375 let field = self
376 .fields
377 .iter()
378 .find(|SearchField { name, .. }| *name == $field_name)
379 .expect("Couldn't find field");
380
381 if let Field::$field_variant(ref inner) = field.field {
382 inner
383 } else {
384 panic!("Incorrect field type")
385 }
386 }
387 };
388}
389
390macro_rules! define_field_accessor_mut {
391 ($method_name:ident, $field_name:expr, $field_variant:ident, $return_type:ty) => {
392 pub fn $method_name(&mut self) -> $return_type {
393 let field = self
394 .fields
395 .iter_mut()
396 .find(|SearchField { name, .. }| *name == $field_name)
397 .expect("Couldn't find field");
398
399 if let Field::$field_variant(inner) = &mut field.field {
400 inner
401 } else {
402 panic!("Incorrect field type")
403 }
404 }
405 };
406}
407
408impl SearchFields {
409 define_field_accessor!(search, FieldName::Search, Text, &TextField);
411 define_field_accessor!(replace, FieldName::Replace, Text, &TextField);
412 define_field_accessor!(
413 fixed_strings,
414 FieldName::FixedStrings,
415 Checkbox,
416 &CheckboxField
417 );
418 define_field_accessor!(whole_word, FieldName::WholeWord, Checkbox, &CheckboxField);
419 define_field_accessor!(match_case, FieldName::MatchCase, Checkbox, &CheckboxField);
420 define_field_accessor!(include_files, FieldName::IncludeFiles, Text, &TextField);
421 define_field_accessor!(exclude_files, FieldName::ExcludeFiles, Text, &TextField);
422
423 define_field_accessor_mut!(search_mut, FieldName::Search, Text, &mut TextField);
424 define_field_accessor_mut!(
425 include_files_mut,
426 FieldName::IncludeFiles,
427 Text,
428 &mut TextField
429 );
430 define_field_accessor_mut!(
431 exclude_files_mut,
432 FieldName::ExcludeFiles,
433 Text,
434 &mut TextField
435 );
436
437 #[allow(clippy::needless_pass_by_value)]
438 pub fn with_values(
439 search_field_values: &SearchFieldValues<'_>,
440 disable_prepopulated_fields: bool,
441 ) -> Self {
442 let fields = [
443 SearchField::new_text(
444 FieldName::Search,
445 search_field_values.search.value,
446 search_field_values.search.set_by_cli,
447 ),
448 SearchField::new_text(
449 FieldName::Replace,
450 search_field_values.replace.value,
451 search_field_values.replace.set_by_cli,
452 ),
453 SearchField::new_checkbox(
454 FieldName::FixedStrings,
455 search_field_values.fixed_strings.value,
456 search_field_values.fixed_strings.set_by_cli,
457 ),
458 SearchField::new_checkbox(
459 FieldName::WholeWord,
460 search_field_values.match_whole_word.value,
461 search_field_values.match_whole_word.set_by_cli,
462 ),
463 SearchField::new_checkbox(
464 FieldName::MatchCase,
465 search_field_values.match_case.value,
466 search_field_values.match_case.set_by_cli,
467 ),
468 SearchField::new_text(
469 FieldName::IncludeFiles,
470 search_field_values.include_files.value,
471 search_field_values.include_files.set_by_cli,
472 ),
473 SearchField::new_text(
474 FieldName::ExcludeFiles,
475 search_field_values.exclude_files.value,
476 search_field_values.exclude_files.set_by_cli,
477 ),
478 ];
479
480 Self {
481 highlighted: Self::initial_highlight_position(&fields, disable_prepopulated_fields),
482 fields,
483 }
484 }
485
486 fn initial_highlight_position(
487 fields: &[SearchField],
488 disable_prepopulated_fields: bool,
489 ) -> usize {
490 if disable_prepopulated_fields {
491 fields
492 .iter()
493 .enumerate()
494 .find_map(|(idx, field)| if !field.set_by_cli { Some(idx) } else { None })
495 .unwrap_or(0)
496 } else {
497 0
498 }
499 }
500
501 pub fn highlighted_field(&self) -> &SearchField {
502 &self.fields[self.highlighted]
503 }
504
505 pub fn highlighted_field_mut(&mut self) -> &mut SearchField {
506 &mut self.fields[self.highlighted]
507 }
508
509 fn focus_impl(&mut self, backward: bool, disable_prepopulated_fields: bool) {
510 let step = if backward {
511 self.fields.len().saturating_sub(1)
512 } else {
513 1
514 };
515
516 let initial = self.highlighted;
517 let mut next = (initial + step).rem_euclid(self.fields.len());
518 if disable_prepopulated_fields {
519 while self.fields[next].set_by_cli && next != initial {
520 next = (next + step).rem_euclid(self.fields.len());
521 }
522 }
523 self.highlighted = next;
524 }
525
526 pub fn focus_next(&mut self, disable_prepopulated_fields: bool) {
527 self.focus_impl(false, disable_prepopulated_fields);
528 }
529
530 pub fn focus_prev(&mut self, disable_prepopulated_fields: bool) {
531 self.focus_impl(true, disable_prepopulated_fields);
532 }
533
534 pub fn errors(&self) -> Vec<AppError> {
535 self.fields
536 .iter()
537 .filter_map(|field| {
538 field.error().map(|err| AppError {
539 name: field.name.title().to_string(),
540 long: err.long,
541 })
542 })
543 .collect::<Vec<_>>()
544 }
545}
546
547#[derive(Clone, Debug, Eq, PartialEq)]
548pub struct FieldValue<T> {
549 pub value: T,
550 pub set_by_cli: bool,
551}
552
553impl<T> FieldValue<T> {
554 pub fn new(value: T, set_by_cli: bool) -> Self {
555 Self { value, set_by_cli }
556 }
557}
558
559impl<T: Default> Default for FieldValue<T> {
560 fn default() -> Self {
561 Self {
562 value: T::default(),
563 set_by_cli: false,
564 }
565 }
566}
567
568#[derive(Clone, Debug, Eq, PartialEq)]
569pub struct SearchFieldValues<'a> {
570 pub search: FieldValue<&'a str>,
571 pub replace: FieldValue<&'a str>,
572 pub fixed_strings: FieldValue<bool>,
573 pub match_whole_word: FieldValue<bool>,
574 pub match_case: FieldValue<bool>,
575 pub include_files: FieldValue<&'a str>,
576 pub exclude_files: FieldValue<&'a str>,
577}
578
579impl<'a> Default for SearchFieldValues<'a> {
580 fn default() -> SearchFieldValues<'a> {
581 Self {
582 search: FieldValue::new(Self::DEFAULT_SEARCH, false),
583 replace: FieldValue::new(Self::DEFAULT_REPLACE, false),
584 fixed_strings: FieldValue::new(Self::DEFAULT_FIXED_STRINGS, false),
585 match_whole_word: FieldValue::new(Self::DEFAULT_WHOLE_WORD, false),
586 match_case: FieldValue::new(Self::DEFAULT_MATCH_CASE, false),
587 include_files: FieldValue::new(Self::DEFAULT_INCLUDE_FILES, false),
588 exclude_files: FieldValue::new(Self::DEFAULT_EXCLUDE_FILES, false),
589 }
590 }
591}
592
593impl SearchFieldValues<'_> {
594 const DEFAULT_SEARCH: &'static str = "";
595 const DEFAULT_REPLACE: &'static str = "";
596 const DEFAULT_FIXED_STRINGS: bool = false;
597 const DEFAULT_WHOLE_WORD: bool = false;
598 const DEFAULT_MATCH_CASE: bool = true;
599 const DEFAULT_INCLUDE_FILES: &'static str = "";
600 const DEFAULT_EXCLUDE_FILES: &'static str = "";
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606
607 #[test]
608 fn test_text_field_operations() {
609 let mut field = TextField::new("");
610
611 for c in "Hello".chars() {
613 field.enter_char(c);
614 }
615 assert_eq!(field.text(), "Hello");
616 assert_eq!(field.cursor_idx, 5);
617
618 field.move_cursor_left();
619 assert_eq!(field.cursor_idx, 4);
620 field.move_cursor_right();
621 assert_eq!(field.cursor_idx, 5);
622 field.move_cursor_start();
623 assert_eq!(field.cursor_idx, 0);
624 field.move_cursor_end();
625 assert_eq!(field.cursor_idx, 5);
626
627 field.clear();
628 for c in "Hello world".chars() {
629 field.enter_char(c);
630 }
631 field.move_cursor_start();
632 field.move_cursor_forward_word();
633 assert_eq!(field.cursor_idx, 6);
634 field.move_cursor_forward_word();
635 assert_eq!(field.cursor_idx, 11);
636 field.move_cursor_forward_word();
637 assert_eq!(field.cursor_idx, 11);
638 field.move_cursor_back_word();
639 assert_eq!(field.cursor_idx, 6);
640
641 field.move_cursor_start();
643 field.delete_char_forward();
644 assert_eq!(field.text(), "ello world");
645 field.move_cursor_end();
646 field.delete_char();
647 assert_eq!(field.text(), "ello worl");
648 field.move_cursor_start();
649 field.delete_word_forward();
650 assert_eq!(field.text(), "worl");
651 field.move_cursor_end();
652 field.delete_word_backward();
653 assert_eq!(field.text(), "");
654 }
655
656 #[test]
657 fn test_unicode_text_handling() {
658 let mut field = TextField::new("");
659
660 field.enter_char('👍');
662 assert_eq!(field.text(), "👍");
663 assert_eq!(field.cursor_idx, 1);
664
665 field.enter_char('🎉');
666 assert_eq!(field.text(), "👍🎉");
667 assert_eq!(field.cursor_idx, 2);
668
669 field.clear();
671 for c in "こんにちは".chars() {
672 field.enter_char(c);
673 }
674 assert_eq!(field.text(), "こんにちは");
675 assert_eq!(field.cursor_idx, 5);
676
677 field.move_cursor_left();
679 assert_eq!(field.cursor_idx, 4);
680 field.delete_char();
681 assert_eq!(field.text(), "こんには");
682
683 field.clear();
685 for c in "Hello👋World🌍".chars() {
686 field.enter_char(c);
687 }
688 assert_eq!(field.text(), "Hello👋World🌍");
689 assert_eq!(field.cursor_idx, 12);
690
691 field.move_cursor_start();
693 for _ in 0..5 {
694 field.move_cursor_right();
695 }
696 assert_eq!(field.cursor_idx, 5);
697 field.delete_char_forward();
698 assert_eq!(field.text(), "HelloWorld🌍");
699 }
700
701 #[test]
702 fn test_empty_text_edge_cases() {
703 let mut field = TextField::new("");
704
705 assert_eq!(field.text(), "");
707 assert_eq!(field.cursor_idx, 0);
708
709 field.delete_char();
710 assert_eq!(field.text(), "");
711 assert_eq!(field.cursor_idx, 0);
712
713 field.delete_char_forward();
714 assert_eq!(field.text(), "");
715 assert_eq!(field.cursor_idx, 0);
716
717 field.move_cursor_left();
718 assert_eq!(field.cursor_idx, 0);
719
720 field.move_cursor_right();
721 assert_eq!(field.cursor_idx, 0);
722
723 field.delete_word_backward();
724 assert_eq!(field.text(), "");
725
726 field.delete_word_forward();
727 assert_eq!(field.text(), "");
728
729 field.move_cursor_forward_word();
730 assert_eq!(field.cursor_idx, 0);
731
732 field.move_cursor_back_word();
733 assert_eq!(field.cursor_idx, 0);
734 }
735
736 #[test]
737 fn test_cursor_boundary_cases() {
738 let mut field = TextField::new("Test");
739
740 field.move_cursor_start();
742 field.move_cursor_left();
743 assert_eq!(field.cursor_idx, 0);
744
745 field.delete_char();
746 assert_eq!(field.text(), "Test");
747 assert_eq!(field.cursor_idx, 0);
748
749 field.move_cursor_end();
751 field.move_cursor_right();
752 assert_eq!(field.cursor_idx, 4);
753
754 field.delete_char_forward();
755 assert_eq!(field.text(), "Test");
756 assert_eq!(field.cursor_idx, 4);
757
758 field.move_cursor_left_by(10);
760 assert_eq!(field.cursor_idx, 0);
761
762 field.move_cursor_right_by(10);
763 assert_eq!(field.cursor_idx, 4);
764 }
765
766 #[test]
767 fn test_word_navigation_with_punctuation() {
768 let mut field = TextField::new("");
769
770 for c in "hello, world! foo-bar_baz.qux".chars() {
772 field.enter_char(c);
773 }
774
775 field.move_cursor_start();
776 field.move_cursor_forward_word();
777 assert_eq!(field.cursor_idx, 7); field.move_cursor_forward_word();
780 assert_eq!(field.cursor_idx, 14); field.move_cursor_forward_word();
783 assert_eq!(field.cursor_idx, 29); field.move_cursor_back_word();
786 assert_eq!(field.cursor_idx, 14); field.move_cursor_back_word();
789 assert_eq!(field.cursor_idx, 7); field.delete_word_forward();
793 assert_eq!(field.text(), "hello, foo-bar_baz.qux");
794
795 field.move_cursor_end();
796 field.delete_word_backward();
797 assert_eq!(field.text(), "hello, ");
798 }
799
800 #[test]
801 fn test_word_navigation_special_cases() {
802 let mut field = TextField::new("");
803
804 for c in "hello world".chars() {
806 field.enter_char(c);
807 }
808 field.move_cursor_start();
809 field.move_cursor_forward_word();
810 assert_eq!(field.cursor_idx, 10); field.clear();
814 for c in " hello ".chars() {
815 field.enter_char(c);
816 }
817 field.move_cursor_start();
818 field.move_cursor_forward_word();
819 assert_eq!(field.cursor_idx, 3); field.move_cursor_end();
822 field.move_cursor_back_word();
823 assert_eq!(field.cursor_idx, 3); field.clear();
827 for c in " ".chars() {
828 field.enter_char(c);
829 }
830 field.move_cursor_start();
831 field.move_cursor_forward_word();
832 assert_eq!(field.cursor_idx, 5);
833
834 field.move_cursor_back_word();
835 assert_eq!(field.cursor_idx, 0);
836 }
837
838 #[test]
839 fn test_visual_cursor_position() {
840 let mut field = TextField::new("");
841
842 for c in "Hello".chars() {
844 field.enter_char(c);
845 }
846 assert_eq!(field.visual_cursor_pos(), 5);
847
848 field.clear();
850 for c in "你好".chars() {
851 field.enter_char(c);
852 }
853 assert_eq!(field.visual_cursor_pos(), 4); field.clear();
857 for c in "Hi你好".chars() {
858 field.enter_char(c);
859 }
860 assert_eq!(field.visual_cursor_pos(), 6); field.clear();
864 field.enter_char('😀');
865 assert_eq!(field.visual_cursor_pos(), 2);
866
867 field.clear();
869 for c in "e\u{0301}".chars() {
870 field.enter_char(c);
872 }
873 assert_eq!(field.visual_cursor_pos(), 1); }
875
876 #[test]
877 fn test_cursor_position_with_wide_chars() {
878 let mut field = TextField::new("");
879
880 for c in "A你B好C".chars() {
882 field.enter_char(c);
883 }
884
885 field.move_cursor_start();
887 assert_eq!(field.cursor_idx, 0);
888 assert_eq!(field.visual_cursor_pos(), 0);
889
890 field.move_cursor_right();
891 assert_eq!(field.cursor_idx, 1); assert_eq!(field.visual_cursor_pos(), 1);
893
894 field.move_cursor_right();
895 assert_eq!(field.cursor_idx, 2); assert_eq!(field.visual_cursor_pos(), 3); field.move_cursor_right();
899 assert_eq!(field.cursor_idx, 3); assert_eq!(field.visual_cursor_pos(), 4); field.delete_char();
904 assert_eq!(field.text(), "A你好C");
905 assert_eq!(field.cursor_idx, 2);
906
907 field.delete_char();
908 assert_eq!(field.text(), "A好C");
909 assert_eq!(field.cursor_idx, 1);
910 }
911
912 #[test]
913 fn test_set_text_and_insert_text() {
914 let mut field = TextField::new("initial");
915
916 field.set_text("new text");
918 assert_eq!(field.text(), "new text");
919 assert_eq!(field.cursor_idx, 8);
920
921 field.set_cursor_idx(100);
923 assert_eq!(field.cursor_idx, 8);
924
925 field.set_text("short");
926 assert_eq!(field.text(), "short");
927 assert_eq!(field.cursor_idx, 5); field.move_cursor_start();
931 field.insert_text("pre-");
932 assert_eq!(field.text(), "pre-short");
933 assert_eq!(field.cursor_idx, 4);
934
935 field.move_cursor_end();
936 field.insert_text("-post");
937 assert_eq!(field.text(), "pre-short-post");
938 assert_eq!(field.cursor_idx, 14);
939
940 field.set_cursor_idx(9); field.insert_text("[mid]");
943 assert_eq!(field.text(), "pre-short[mid]-post");
944 assert_eq!(field.cursor_idx, 14); }
946
947 #[test]
948 fn test_delete_to_start() {
949 let mut field = TextField::new("Hello World");
950
951 field.set_cursor_idx(6); field.delete_to_start();
953 assert_eq!(field.text(), "World");
954 assert_eq!(field.cursor_idx, 0);
955
956 field.set_text("Test");
958 field.move_cursor_end();
959 field.delete_to_start();
960 assert_eq!(field.text(), "");
961 assert_eq!(field.cursor_idx, 0);
962
963 field.set_text("Test");
965 field.move_cursor_start();
966 field.delete_to_start();
967 assert_eq!(field.text(), "Test");
968 assert_eq!(field.cursor_idx, 0);
969 }
970
971 #[test]
972 fn test_complex_unicode_scenarios() {
973 let mut field = TextField::new("");
974
975 let test_string = "café☕ 世界🌏 Здравствуй👋";
977 for c in test_string.chars() {
978 field.enter_char(c);
979 }
980 assert_eq!(field.text(), test_string);
981
982 field.move_cursor_start();
984 field.move_cursor_forward_word();
985 assert_eq!(field.cursor_idx, 6); field.move_cursor_forward_word();
988 assert_eq!(field.cursor_idx, 10); field.delete_word_backward();
992 assert_eq!(field.text(), "café☕ Здравствуй👋");
993
994 field.move_cursor_start();
996 for _ in 0..4 {
997 field.move_cursor_right();
998 }
999 assert_eq!(field.cursor_idx, 4); assert_eq!(field.visual_cursor_pos(), 4); field.move_cursor_right(); assert_eq!(field.cursor_idx, 5);
1004 assert_eq!(field.visual_cursor_pos(), 6); }
1006
1007 #[test]
1008 fn test_byte_index_calculation() {
1009 let mut field = TextField::new("a你b");
1010
1011 field.set_cursor_idx(0);
1013 assert_eq!(field.byte_index(), 0);
1014
1015 field.set_cursor_idx(1); assert_eq!(field.byte_index(), 1);
1017
1018 field.set_cursor_idx(2); assert_eq!(field.byte_index(), 4); field.set_cursor_idx(3); assert_eq!(field.byte_index(), 5);
1023
1024 field.set_cursor_idx(10); assert_eq!(field.cursor_idx, 3); assert_eq!(field.byte_index(), 5);
1028 }
1029
1030 #[test]
1031 fn test_checkbox_field() {
1032 let mut field = CheckboxField::new(false);
1033 assert!(!field.checked);
1034
1035 field.handle_keys(KeyCode::Char(' '), KeyModifiers::NONE);
1036 assert!(field.checked);
1037
1038 field.handle_keys(KeyCode::Char(' '), KeyModifiers::NONE);
1039 assert!(!field.checked);
1040
1041 field.handle_keys(KeyCode::Enter, KeyModifiers::NONE);
1042 assert!(!field.checked);
1043 }
1044
1045 #[test]
1046 fn test_search_fields() {
1047 let mut search_fields = SearchFields::with_values(&SearchFieldValues::default(), true);
1048
1049 assert_eq!(search_fields.highlighted, 0);
1051 search_fields.focus_next(true);
1052 assert_eq!(search_fields.highlighted, 1);
1053 search_fields.focus_next(true);
1054 assert_eq!(search_fields.highlighted, 2);
1055 search_fields.focus_next(true);
1056 assert_eq!(search_fields.highlighted, 3);
1057 search_fields.focus_next(true);
1058 assert_eq!(search_fields.highlighted, 4);
1059 search_fields.focus_next(true);
1060 assert_eq!(search_fields.highlighted, 5);
1061 search_fields.focus_next(true);
1062 assert_eq!(search_fields.highlighted, 6);
1063 search_fields.focus_next(true);
1064 assert_eq!(search_fields.highlighted, 0);
1065 search_fields.focus_prev(true);
1066 assert_eq!(search_fields.highlighted, 6);
1067 search_fields.focus_next(true);
1068 assert_eq!(search_fields.highlighted, 0);
1069
1070 for c in "test search".chars() {
1071 search_fields.highlighted_field_mut().handle_keys(
1072 KeyCode::Char(c),
1073 KeyModifiers::NONE,
1074 true,
1075 );
1076 }
1077 assert_eq!(search_fields.search().text(), "test search");
1078
1079 search_fields.focus_next(true);
1080 assert_eq!(search_fields.highlighted, 1);
1081 for c in "test replace".chars() {
1082 search_fields.highlighted_field_mut().handle_keys(
1083 KeyCode::Char(c),
1084 KeyModifiers::NONE,
1085 true,
1086 );
1087 }
1088 assert_eq!(search_fields.replace().text(), "test replace");
1089
1090 search_fields.focus_next(true);
1091 assert_eq!(search_fields.highlighted, 2);
1092 search_fields.highlighted_field_mut().handle_keys(
1093 KeyCode::Char(' '),
1094 KeyModifiers::NONE,
1095 true,
1096 );
1097 assert!(search_fields.fixed_strings().checked);
1098
1099 assert_eq!(search_fields.search().text(), "test search");
1100 assert_eq!(search_fields.fixed_strings().checked, true);
1101
1102 search_fields.highlighted_field_mut().handle_keys(
1103 KeyCode::Char(' '),
1104 KeyModifiers::NONE,
1105 true,
1106 );
1107
1108 assert_eq!(search_fields.search().text(), "test search");
1109 assert_eq!(search_fields.fixed_strings().checked, false);
1110 }
1111
1112 #[test]
1113 fn test_focus_with_locked_disabled_fields() {
1114 let mut search_fields = SearchFields::with_values(
1115 &SearchFieldValues {
1116 search: FieldValue::new("prepopulated", true),
1117 replace: FieldValue::new("", false),
1118 fixed_strings: FieldValue::new(true, true),
1119 match_whole_word: FieldValue::new(false, false),
1120 match_case: FieldValue::new(true, false),
1121 include_files: FieldValue::new("*.rs", true),
1122 exclude_files: FieldValue::new("", false),
1123 },
1124 true,
1125 );
1126
1127 assert_eq!(search_fields.highlighted, 1);
1128 assert_eq!(search_fields.highlighted_field().name, FieldName::Replace);
1129
1130 search_fields.focus_next(true);
1131 assert_eq!(search_fields.highlighted, 3);
1132 assert_eq!(search_fields.highlighted_field().name, FieldName::WholeWord);
1133
1134 search_fields.focus_next(true);
1135 assert_eq!(search_fields.highlighted, 4);
1136 assert_eq!(search_fields.highlighted_field().name, FieldName::MatchCase);
1137
1138 search_fields.focus_next(true);
1139 assert_eq!(search_fields.highlighted, 6);
1140 assert_eq!(
1141 search_fields.highlighted_field().name,
1142 FieldName::ExcludeFiles
1143 );
1144
1145 search_fields.focus_next(true);
1146 assert_eq!(search_fields.highlighted, 1);
1147 assert_eq!(search_fields.highlighted_field().name, FieldName::Replace);
1148
1149 search_fields.focus_prev(true);
1150 assert_eq!(search_fields.highlighted, 6);
1151 assert_eq!(
1152 search_fields.highlighted_field().name,
1153 FieldName::ExcludeFiles
1154 );
1155
1156 search_fields.focus_prev(true);
1157 assert_eq!(search_fields.highlighted, 4);
1158 assert_eq!(search_fields.highlighted_field().name, FieldName::MatchCase);
1159 }
1160
1161 #[test]
1162 fn test_focus_with_unlocked_disabled_fields() {
1163 let mut search_fields = SearchFields::with_values(
1164 &SearchFieldValues {
1165 search: FieldValue::new("prepopulated", true),
1166 replace: FieldValue::new("", false),
1167 fixed_strings: FieldValue::new(true, true),
1168 match_whole_word: FieldValue::new(false, false),
1169 match_case: FieldValue::new(true, false),
1170 include_files: FieldValue::new("*.rs", true),
1171 exclude_files: FieldValue::new("", false),
1172 },
1173 false,
1174 );
1175
1176 assert_eq!(search_fields.highlighted, 0);
1177
1178 search_fields.focus_next(false);
1179 assert_eq!(search_fields.highlighted, 1);
1180 search_fields.focus_next(false);
1181 assert_eq!(search_fields.highlighted, 2);
1182 search_fields.focus_next(false);
1183 assert_eq!(search_fields.highlighted, 3);
1184 }
1185
1186 #[test]
1187 fn test_focus_all_fields_disabled_and_locked() {
1188 let mut search_fields = SearchFields::with_values(
1189 &SearchFieldValues {
1190 search: FieldValue::new("search", true),
1191 replace: FieldValue::new("replace", true),
1192 fixed_strings: FieldValue::new(true, true),
1193 match_whole_word: FieldValue::new(false, true),
1194 match_case: FieldValue::new(true, true),
1195 include_files: FieldValue::new("*.rs", true),
1196 exclude_files: FieldValue::new("*.txt", true),
1197 },
1198 true,
1199 );
1200
1201 assert_eq!(search_fields.highlighted, 0);
1202
1203 search_fields.focus_next(true);
1204 assert_eq!(search_fields.highlighted, 0);
1205
1206 search_fields.focus_prev(true);
1207 assert_eq!(search_fields.highlighted, 0);
1208 }
1209
1210 #[test]
1211 fn test_initial_highlight_position_with_prepopulated_fields_disable_true() {
1212 let search_fields = SearchFields::with_values(
1213 &SearchFieldValues {
1214 search: FieldValue::new("cli_search", true),
1215 replace: FieldValue::new("cli_replace", true),
1216 fixed_strings: FieldValue::new(false, false),
1217 match_whole_word: FieldValue::new(false, false),
1218 match_case: FieldValue::new(true, false),
1219 include_files: FieldValue::new("*.rs", true),
1220 exclude_files: FieldValue::new("", false),
1221 },
1222 true,
1223 );
1224
1225 assert_eq!(search_fields.highlighted, 2);
1226 assert_eq!(
1227 search_fields.highlighted_field().name,
1228 FieldName::FixedStrings
1229 );
1230 }
1231
1232 #[test]
1233 fn test_initial_highlight_position_with_prepopulated_fields_disable_false() {
1234 let search_fields = SearchFields::with_values(
1235 &SearchFieldValues {
1236 search: FieldValue::new("cli_search", true),
1237 replace: FieldValue::new("cli_replace", true),
1238 fixed_strings: FieldValue::new(false, false),
1239 match_whole_word: FieldValue::new(false, false),
1240 match_case: FieldValue::new(true, false),
1241 include_files: FieldValue::new("*.rs", true),
1242 exclude_files: FieldValue::new("", false),
1243 },
1244 false,
1245 );
1246
1247 assert_eq!(search_fields.highlighted, 0);
1248 assert_eq!(search_fields.highlighted_field().name, FieldName::Search);
1249 }
1250}