#[cfg(feature = "steel")]
use steel_derive::Steel;
use unicode_width::UnicodeWidthStr;
use crate::{
errors::AppError,
keyboard::{KeyCode, KeyModifiers},
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Field {
Text(TextField),
Checkbox(CheckboxField),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FieldError {
pub short: String,
pub long: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "steel", derive(Steel))]
pub struct TextField {
text: String,
cursor_idx: usize,
error: Option<FieldError>,
}
impl TextField {
pub fn new(initial: &str) -> Self {
Self {
text: initial.to_string(),
cursor_idx: initial.chars().count(),
error: None,
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn visual_cursor_pos(&self) -> usize {
let prefix: String = self.text.chars().take(self.cursor_idx).collect();
UnicodeWidthStr::width(prefix.as_str())
}
pub fn move_cursor_left(&mut self) {
self.move_cursor_left_by(1);
}
pub fn move_cursor_start(&mut self) {
self.cursor_idx = 0;
}
fn move_cursor_left_by(&mut self, n: usize) {
let cursor_moved_left = self.cursor_idx.saturating_sub(n);
self.cursor_idx = self.clamp_cursor(cursor_moved_left);
}
pub fn move_cursor_right(&mut self) {
self.move_cursor_right_by(1);
}
fn move_cursor_right_by(&mut self, n: usize) {
let cursor_moved_right = self.cursor_idx.saturating_add(n);
self.cursor_idx = self.clamp_cursor(cursor_moved_right);
}
pub fn move_cursor_end(&mut self) {
self.cursor_idx = self.text.chars().count();
}
pub fn enter_char(&mut self, new_char: char) {
let index = self.byte_index();
self.text.insert(index, new_char);
self.move_cursor_right();
}
fn byte_index(&self) -> usize {
self.text
.char_indices()
.map(|(i, _)| i)
.nth(self.cursor_idx)
.unwrap_or(self.text.len())
}
pub fn delete_char(&mut self) {
if self.cursor_idx == 0 {
return;
}
let before_char = self.text.chars().take(self.cursor_idx - 1);
let after_char = self.text.chars().skip(self.cursor_idx);
self.text = before_char.chain(after_char).collect();
self.move_cursor_left();
}
pub fn delete_char_forward(&mut self) {
let before_char = self.text.chars().take(self.cursor_idx);
let after_char = self.text.chars().skip(self.cursor_idx + 1);
self.text = before_char.chain(after_char).collect();
}
fn previous_word_start(&self) -> usize {
if self.cursor_idx == 0 {
return 0;
}
let before_char = self.text.chars().take(self.cursor_idx).collect::<Vec<_>>();
let mut idx = self.cursor_idx - 1;
while idx > 0 && before_char[idx] == ' ' {
idx -= 1;
}
while idx > 0 && before_char[idx - 1] != ' ' {
idx -= 1;
}
idx
}
pub fn move_cursor_back_word(&mut self) {
self.cursor_idx = self.previous_word_start();
}
pub fn delete_word_backward(&mut self) {
let new_cursor_pos = self.previous_word_start();
let before_char = self.text.chars().take(new_cursor_pos);
let after_char = self.text.chars().skip(self.cursor_idx);
self.text = before_char.chain(after_char).collect();
self.cursor_idx = new_cursor_pos;
}
fn next_word_start(&self) -> usize {
let after_char = self.text.chars().skip(self.cursor_idx).collect::<Vec<_>>();
let mut idx = 0;
let num_chars = after_char.len();
while idx < num_chars && after_char[idx] != ' ' {
idx += 1;
}
while idx < num_chars && after_char[idx] == ' ' {
idx += 1;
}
self.cursor_idx + idx
}
pub fn move_cursor_forward_word(&mut self) {
self.cursor_idx = self.next_word_start();
}
pub fn delete_word_forward(&mut self) {
let before_char = self.text.chars().take(self.cursor_idx);
let after_char = self.text.chars().skip(self.next_word_start());
self.text = before_char.chain(after_char).collect();
}
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
new_cursor_pos.clamp(0, self.text.chars().count())
}
pub fn clear(&mut self) {
self.text.clear();
self.cursor_idx = 0;
}
pub fn set_text(&mut self, text: &str) {
self.text = text.to_string();
self.cursor_idx = self.clamp_cursor(text.chars().count());
}
pub fn insert_text(&mut self, text: &str) {
let index = self.byte_index();
self.text.insert_str(index, text);
self.cursor_idx += text.chars().count();
}
pub fn cursor_idx(&self) -> usize {
self.cursor_idx
}
pub fn set_cursor_idx(&mut self, idx: usize) {
self.cursor_idx = self.clamp_cursor(idx);
}
pub fn delete_to_start(&mut self) {
self.text = self.text.chars().skip(self.cursor_idx).collect();
self.cursor_idx = 0;
}
pub fn set_error(&mut self, short: String, long: String) {
self.error = Some(FieldError { short, long });
}
pub fn clear_error(&mut self) {
self.error = None;
}
fn handle_keys(&mut self, code: KeyCode, modifiers: KeyModifiers) {
match (code, modifiers) {
(KeyCode::Char('w'), KeyModifiers::CONTROL)
| (KeyCode::Backspace, KeyModifiers::ALT) => {
self.delete_word_backward();
}
(KeyCode::Char('u'), KeyModifiers::CONTROL)
| (KeyCode::Backspace, KeyModifiers::META) => {
self.delete_to_start();
}
(KeyCode::Backspace, _) => {
self.delete_char();
}
(KeyCode::Left | KeyCode::Char('b' | 'B'), _)
if modifiers.contains(KeyModifiers::ALT) =>
{
self.move_cursor_back_word();
}
(KeyCode::Home, _) => {
self.move_cursor_start();
}
(KeyCode::Left, _) => {
self.move_cursor_left();
}
(KeyCode::Right | KeyCode::Char('f' | 'F'), _)
if modifiers.contains(KeyModifiers::ALT) =>
{
self.move_cursor_forward_word();
}
(KeyCode::Right, KeyModifiers::META) | (KeyCode::End, _) => {
self.move_cursor_end();
}
(KeyCode::Right, _) => {
self.move_cursor_right();
}
(KeyCode::Char('d') | KeyCode::Delete, KeyModifiers::ALT) => {
self.delete_word_forward();
}
(KeyCode::Delete, _) => {
self.delete_char_forward();
}
(KeyCode::Char(value), _) => {
self.enter_char(value);
}
_ => {}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CheckboxField {
pub checked: bool,
pub error: Option<FieldError>, }
impl CheckboxField {
pub fn new(initial: bool) -> Self {
Self {
checked: initial,
error: None,
}
}
pub fn handle_keys(&mut self, code: KeyCode, _modifiers: KeyModifiers) {
if code == KeyCode::Char(' ') {
self.checked = !self.checked;
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum FieldName {
Search,
Replace,
FixedStrings,
WholeWord,
MatchCase,
IncludeFiles,
ExcludeFiles,
}
impl FieldName {
pub fn title(&self) -> &str {
match self {
FieldName::Search => "Search text",
FieldName::Replace => "Replace text",
FieldName::FixedStrings => "Fixed strings",
FieldName::WholeWord => "Match whole word",
FieldName::MatchCase => "Match case",
FieldName::IncludeFiles => "Files to include",
FieldName::ExcludeFiles => "Files to exclude",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SearchField {
pub name: FieldName,
pub field: Field,
pub set_by_cli: bool,
}
impl SearchField {
pub fn new_text(name: FieldName, initial: &str, set_by_cli: bool) -> Self {
let field = Field::Text(TextField::new(initial));
Self {
name,
field,
set_by_cli,
}
}
pub fn new_checkbox(name: FieldName, initial: bool, set_by_cli: bool) -> Self {
let field = Field::Checkbox(CheckboxField::new(initial));
Self {
name,
field,
set_by_cli,
}
}
pub fn handle_keys(
&mut self,
code: KeyCode,
modifiers: KeyModifiers,
disable_prepopulated_fields: bool,
) {
if self.set_by_cli && disable_prepopulated_fields {
return;
}
self.clear_error();
match &mut self.field {
Field::Text(f) => f.handle_keys(code, modifiers),
Field::Checkbox(f) => f.handle_keys(code, modifiers),
}
}
pub fn cursor_pos(&self) -> Option<usize> {
match &self.field {
Field::Text(f) => Some(f.visual_cursor_pos()),
Field::Checkbox(_) => None,
}
}
pub fn clear_error(&mut self) {
match &mut self.field {
Field::Text(f) => f.clear_error(),
Field::Checkbox(_) => {} }
}
pub fn error(&self) -> Option<FieldError> {
match &self.field {
Field::Text(f) => f.error.clone(),
Field::Checkbox(f) => f.error.clone(),
}
}
}
pub const NUM_SEARCH_FIELDS: u16 = 7;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SearchFields {
pub fields: [SearchField; NUM_SEARCH_FIELDS as usize],
pub highlighted: usize,
}
macro_rules! define_field_accessor {
($method_name:ident, $field_name:expr, $field_variant:ident, $return_type:ty) => {
pub fn $method_name(&self) -> $return_type {
let field = self
.fields
.iter()
.find(|SearchField { name, .. }| *name == $field_name)
.expect("Couldn't find field");
if let Field::$field_variant(ref inner) = field.field {
inner
} else {
panic!("Incorrect field type")
}
}
};
}
macro_rules! define_field_accessor_mut {
($method_name:ident, $field_name:expr, $field_variant:ident, $return_type:ty) => {
pub fn $method_name(&mut self) -> $return_type {
let field = self
.fields
.iter_mut()
.find(|SearchField { name, .. }| *name == $field_name)
.expect("Couldn't find field");
if let Field::$field_variant(inner) = &mut field.field {
inner
} else {
panic!("Incorrect field type")
}
}
};
}
impl SearchFields {
define_field_accessor!(search, FieldName::Search, Text, &TextField);
define_field_accessor!(replace, FieldName::Replace, Text, &TextField);
define_field_accessor!(
fixed_strings,
FieldName::FixedStrings,
Checkbox,
&CheckboxField
);
define_field_accessor!(whole_word, FieldName::WholeWord, Checkbox, &CheckboxField);
define_field_accessor!(match_case, FieldName::MatchCase, Checkbox, &CheckboxField);
define_field_accessor!(include_files, FieldName::IncludeFiles, Text, &TextField);
define_field_accessor!(exclude_files, FieldName::ExcludeFiles, Text, &TextField);
define_field_accessor_mut!(search_mut, FieldName::Search, Text, &mut TextField);
define_field_accessor_mut!(
include_files_mut,
FieldName::IncludeFiles,
Text,
&mut TextField
);
define_field_accessor_mut!(
exclude_files_mut,
FieldName::ExcludeFiles,
Text,
&mut TextField
);
#[allow(clippy::needless_pass_by_value)]
pub fn with_values(
search_field_values: &SearchFieldValues<'_>,
disable_prepopulated_fields: bool,
) -> Self {
let fields = [
SearchField::new_text(
FieldName::Search,
search_field_values.search.value,
search_field_values.search.set_by_cli,
),
SearchField::new_text(
FieldName::Replace,
search_field_values.replace.value,
search_field_values.replace.set_by_cli,
),
SearchField::new_checkbox(
FieldName::FixedStrings,
search_field_values.fixed_strings.value,
search_field_values.fixed_strings.set_by_cli,
),
SearchField::new_checkbox(
FieldName::WholeWord,
search_field_values.match_whole_word.value,
search_field_values.match_whole_word.set_by_cli,
),
SearchField::new_checkbox(
FieldName::MatchCase,
search_field_values.match_case.value,
search_field_values.match_case.set_by_cli,
),
SearchField::new_text(
FieldName::IncludeFiles,
search_field_values.include_files.value,
search_field_values.include_files.set_by_cli,
),
SearchField::new_text(
FieldName::ExcludeFiles,
search_field_values.exclude_files.value,
search_field_values.exclude_files.set_by_cli,
),
];
Self {
highlighted: Self::initial_highlight_position(&fields, disable_prepopulated_fields),
fields,
}
}
fn initial_highlight_position(
fields: &[SearchField],
disable_prepopulated_fields: bool,
) -> usize {
if disable_prepopulated_fields {
fields
.iter()
.enumerate()
.find_map(|(idx, field)| if !field.set_by_cli { Some(idx) } else { None })
.unwrap_or(0)
} else {
0
}
}
pub fn highlighted_field(&self) -> &SearchField {
&self.fields[self.highlighted]
}
pub fn highlighted_field_mut(&mut self) -> &mut SearchField {
&mut self.fields[self.highlighted]
}
fn focus_impl(&mut self, backward: bool, disable_prepopulated_fields: bool) {
let step = if backward {
self.fields.len().saturating_sub(1)
} else {
1
};
let initial = self.highlighted;
let mut next = (initial + step).rem_euclid(self.fields.len());
if disable_prepopulated_fields {
while self.fields[next].set_by_cli && next != initial {
next = (next + step).rem_euclid(self.fields.len());
}
}
self.highlighted = next;
}
pub fn focus_next(&mut self, disable_prepopulated_fields: bool) {
self.focus_impl(false, disable_prepopulated_fields);
}
pub fn focus_prev(&mut self, disable_prepopulated_fields: bool) {
self.focus_impl(true, disable_prepopulated_fields);
}
pub fn errors(&self) -> Vec<AppError> {
self.fields
.iter()
.filter_map(|field| {
field.error().map(|err| AppError {
name: field.name.title().to_string(),
long: err.long,
})
})
.collect::<Vec<_>>()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FieldValue<T> {
pub value: T,
pub set_by_cli: bool,
}
impl<T> FieldValue<T> {
pub fn new(value: T, set_by_cli: bool) -> Self {
Self { value, set_by_cli }
}
}
impl<T: Default> Default for FieldValue<T> {
fn default() -> Self {
Self {
value: T::default(),
set_by_cli: false,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SearchFieldValues<'a> {
pub search: FieldValue<&'a str>,
pub replace: FieldValue<&'a str>,
pub fixed_strings: FieldValue<bool>,
pub match_whole_word: FieldValue<bool>,
pub match_case: FieldValue<bool>,
pub include_files: FieldValue<&'a str>,
pub exclude_files: FieldValue<&'a str>,
}
impl<'a> Default for SearchFieldValues<'a> {
fn default() -> SearchFieldValues<'a> {
Self {
search: FieldValue::new(Self::DEFAULT_SEARCH, false),
replace: FieldValue::new(Self::DEFAULT_REPLACE, false),
fixed_strings: FieldValue::new(Self::DEFAULT_FIXED_STRINGS, false),
match_whole_word: FieldValue::new(Self::DEFAULT_WHOLE_WORD, false),
match_case: FieldValue::new(Self::DEFAULT_MATCH_CASE, false),
include_files: FieldValue::new(Self::DEFAULT_INCLUDE_FILES, false),
exclude_files: FieldValue::new(Self::DEFAULT_EXCLUDE_FILES, false),
}
}
}
impl SearchFieldValues<'_> {
const DEFAULT_SEARCH: &'static str = "";
const DEFAULT_REPLACE: &'static str = "";
const DEFAULT_FIXED_STRINGS: bool = false;
const DEFAULT_WHOLE_WORD: bool = false;
const DEFAULT_MATCH_CASE: bool = true;
const DEFAULT_INCLUDE_FILES: &'static str = "";
const DEFAULT_EXCLUDE_FILES: &'static str = "";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_field_operations() {
let mut field = TextField::new("");
for c in "Hello".chars() {
field.enter_char(c);
}
assert_eq!(field.text(), "Hello");
assert_eq!(field.cursor_idx, 5);
field.move_cursor_left();
assert_eq!(field.cursor_idx, 4);
field.move_cursor_right();
assert_eq!(field.cursor_idx, 5);
field.move_cursor_start();
assert_eq!(field.cursor_idx, 0);
field.move_cursor_end();
assert_eq!(field.cursor_idx, 5);
field.clear();
for c in "Hello world".chars() {
field.enter_char(c);
}
field.move_cursor_start();
field.move_cursor_forward_word();
assert_eq!(field.cursor_idx, 6);
field.move_cursor_forward_word();
assert_eq!(field.cursor_idx, 11);
field.move_cursor_forward_word();
assert_eq!(field.cursor_idx, 11);
field.move_cursor_back_word();
assert_eq!(field.cursor_idx, 6);
field.move_cursor_start();
field.delete_char_forward();
assert_eq!(field.text(), "ello world");
field.move_cursor_end();
field.delete_char();
assert_eq!(field.text(), "ello worl");
field.move_cursor_start();
field.delete_word_forward();
assert_eq!(field.text(), "worl");
field.move_cursor_end();
field.delete_word_backward();
assert_eq!(field.text(), "");
}
#[test]
fn test_unicode_text_handling() {
let mut field = TextField::new("");
field.enter_char('👍');
assert_eq!(field.text(), "👍");
assert_eq!(field.cursor_idx, 1);
field.enter_char('🎉');
assert_eq!(field.text(), "👍🎉");
assert_eq!(field.cursor_idx, 2);
field.clear();
for c in "こんにちは".chars() {
field.enter_char(c);
}
assert_eq!(field.text(), "こんにちは");
assert_eq!(field.cursor_idx, 5);
field.move_cursor_left();
assert_eq!(field.cursor_idx, 4);
field.delete_char();
assert_eq!(field.text(), "こんには");
field.clear();
for c in "Hello👋World🌍".chars() {
field.enter_char(c);
}
assert_eq!(field.text(), "Hello👋World🌍");
assert_eq!(field.cursor_idx, 12);
field.move_cursor_start();
for _ in 0..5 {
field.move_cursor_right();
}
assert_eq!(field.cursor_idx, 5);
field.delete_char_forward();
assert_eq!(field.text(), "HelloWorld🌍");
}
#[test]
fn test_empty_text_edge_cases() {
let mut field = TextField::new("");
assert_eq!(field.text(), "");
assert_eq!(field.cursor_idx, 0);
field.delete_char();
assert_eq!(field.text(), "");
assert_eq!(field.cursor_idx, 0);
field.delete_char_forward();
assert_eq!(field.text(), "");
assert_eq!(field.cursor_idx, 0);
field.move_cursor_left();
assert_eq!(field.cursor_idx, 0);
field.move_cursor_right();
assert_eq!(field.cursor_idx, 0);
field.delete_word_backward();
assert_eq!(field.text(), "");
field.delete_word_forward();
assert_eq!(field.text(), "");
field.move_cursor_forward_word();
assert_eq!(field.cursor_idx, 0);
field.move_cursor_back_word();
assert_eq!(field.cursor_idx, 0);
}
#[test]
fn test_cursor_boundary_cases() {
let mut field = TextField::new("Test");
field.move_cursor_start();
field.move_cursor_left();
assert_eq!(field.cursor_idx, 0);
field.delete_char();
assert_eq!(field.text(), "Test");
assert_eq!(field.cursor_idx, 0);
field.move_cursor_end();
field.move_cursor_right();
assert_eq!(field.cursor_idx, 4);
field.delete_char_forward();
assert_eq!(field.text(), "Test");
assert_eq!(field.cursor_idx, 4);
field.move_cursor_left_by(10);
assert_eq!(field.cursor_idx, 0);
field.move_cursor_right_by(10);
assert_eq!(field.cursor_idx, 4);
}
#[test]
fn test_word_navigation_with_punctuation() {
let mut field = TextField::new("");
for c in "hello, world! foo-bar_baz.qux".chars() {
field.enter_char(c);
}
field.move_cursor_start();
field.move_cursor_forward_word();
assert_eq!(field.cursor_idx, 7);
field.move_cursor_forward_word();
assert_eq!(field.cursor_idx, 14);
field.move_cursor_forward_word();
assert_eq!(field.cursor_idx, 29);
field.move_cursor_back_word();
assert_eq!(field.cursor_idx, 14);
field.move_cursor_back_word();
assert_eq!(field.cursor_idx, 7);
field.delete_word_forward();
assert_eq!(field.text(), "hello, foo-bar_baz.qux");
field.move_cursor_end();
field.delete_word_backward();
assert_eq!(field.text(), "hello, ");
}
#[test]
fn test_word_navigation_special_cases() {
let mut field = TextField::new("");
for c in "hello world".chars() {
field.enter_char(c);
}
field.move_cursor_start();
field.move_cursor_forward_word();
assert_eq!(field.cursor_idx, 10);
field.clear();
for c in " hello ".chars() {
field.enter_char(c);
}
field.move_cursor_start();
field.move_cursor_forward_word();
assert_eq!(field.cursor_idx, 3);
field.move_cursor_end();
field.move_cursor_back_word();
assert_eq!(field.cursor_idx, 3);
field.clear();
for c in " ".chars() {
field.enter_char(c);
}
field.move_cursor_start();
field.move_cursor_forward_word();
assert_eq!(field.cursor_idx, 5);
field.move_cursor_back_word();
assert_eq!(field.cursor_idx, 0);
}
#[test]
fn test_visual_cursor_position() {
let mut field = TextField::new("");
for c in "Hello".chars() {
field.enter_char(c);
}
assert_eq!(field.visual_cursor_pos(), 5);
field.clear();
for c in "你好".chars() {
field.enter_char(c);
}
assert_eq!(field.visual_cursor_pos(), 4);
field.clear();
for c in "Hi你好".chars() {
field.enter_char(c);
}
assert_eq!(field.visual_cursor_pos(), 6);
field.clear();
field.enter_char('😀');
assert_eq!(field.visual_cursor_pos(), 2);
field.clear();
for c in "e\u{0301}".chars() {
field.enter_char(c);
}
assert_eq!(field.visual_cursor_pos(), 1); }
#[test]
fn test_cursor_position_with_wide_chars() {
let mut field = TextField::new("");
for c in "A你B好C".chars() {
field.enter_char(c);
}
field.move_cursor_start();
assert_eq!(field.cursor_idx, 0);
assert_eq!(field.visual_cursor_pos(), 0);
field.move_cursor_right();
assert_eq!(field.cursor_idx, 1); assert_eq!(field.visual_cursor_pos(), 1);
field.move_cursor_right();
assert_eq!(field.cursor_idx, 2); assert_eq!(field.visual_cursor_pos(), 3);
field.move_cursor_right();
assert_eq!(field.cursor_idx, 3); assert_eq!(field.visual_cursor_pos(), 4);
field.delete_char();
assert_eq!(field.text(), "A你好C");
assert_eq!(field.cursor_idx, 2);
field.delete_char();
assert_eq!(field.text(), "A好C");
assert_eq!(field.cursor_idx, 1);
}
#[test]
fn test_set_text_and_insert_text() {
let mut field = TextField::new("initial");
field.set_text("new text");
assert_eq!(field.text(), "new text");
assert_eq!(field.cursor_idx, 8);
field.set_cursor_idx(100);
assert_eq!(field.cursor_idx, 8);
field.set_text("short");
assert_eq!(field.text(), "short");
assert_eq!(field.cursor_idx, 5);
field.move_cursor_start();
field.insert_text("pre-");
assert_eq!(field.text(), "pre-short");
assert_eq!(field.cursor_idx, 4);
field.move_cursor_end();
field.insert_text("-post");
assert_eq!(field.text(), "pre-short-post");
assert_eq!(field.cursor_idx, 14);
field.set_cursor_idx(9); field.insert_text("[mid]");
assert_eq!(field.text(), "pre-short[mid]-post");
assert_eq!(field.cursor_idx, 14); }
#[test]
fn test_delete_to_start() {
let mut field = TextField::new("Hello World");
field.set_cursor_idx(6); field.delete_to_start();
assert_eq!(field.text(), "World");
assert_eq!(field.cursor_idx, 0);
field.set_text("Test");
field.move_cursor_end();
field.delete_to_start();
assert_eq!(field.text(), "");
assert_eq!(field.cursor_idx, 0);
field.set_text("Test");
field.move_cursor_start();
field.delete_to_start();
assert_eq!(field.text(), "Test");
assert_eq!(field.cursor_idx, 0);
}
#[test]
fn test_complex_unicode_scenarios() {
let mut field = TextField::new("");
let test_string = "café☕ 世界🌏 Здравствуй👋";
for c in test_string.chars() {
field.enter_char(c);
}
assert_eq!(field.text(), test_string);
field.move_cursor_start();
field.move_cursor_forward_word();
assert_eq!(field.cursor_idx, 6);
field.move_cursor_forward_word();
assert_eq!(field.cursor_idx, 10);
field.delete_word_backward();
assert_eq!(field.text(), "café☕ Здравствуй👋");
field.move_cursor_start();
for _ in 0..4 {
field.move_cursor_right();
}
assert_eq!(field.cursor_idx, 4); assert_eq!(field.visual_cursor_pos(), 4);
field.move_cursor_right(); assert_eq!(field.cursor_idx, 5);
assert_eq!(field.visual_cursor_pos(), 6); }
#[test]
fn test_byte_index_calculation() {
let mut field = TextField::new("a你b");
field.set_cursor_idx(0);
assert_eq!(field.byte_index(), 0);
field.set_cursor_idx(1); assert_eq!(field.byte_index(), 1);
field.set_cursor_idx(2); assert_eq!(field.byte_index(), 4);
field.set_cursor_idx(3); assert_eq!(field.byte_index(), 5);
field.set_cursor_idx(10); assert_eq!(field.cursor_idx, 3); assert_eq!(field.byte_index(), 5);
}
#[test]
fn test_checkbox_field() {
let mut field = CheckboxField::new(false);
assert!(!field.checked);
field.handle_keys(KeyCode::Char(' '), KeyModifiers::NONE);
assert!(field.checked);
field.handle_keys(KeyCode::Char(' '), KeyModifiers::NONE);
assert!(!field.checked);
field.handle_keys(KeyCode::Enter, KeyModifiers::NONE);
assert!(!field.checked);
}
#[test]
fn test_search_fields() {
let mut search_fields = SearchFields::with_values(&SearchFieldValues::default(), true);
assert_eq!(search_fields.highlighted, 0);
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 1);
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 2);
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 3);
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 4);
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 5);
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 6);
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 0);
search_fields.focus_prev(true);
assert_eq!(search_fields.highlighted, 6);
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 0);
for c in "test search".chars() {
search_fields.highlighted_field_mut().handle_keys(
KeyCode::Char(c),
KeyModifiers::NONE,
true,
);
}
assert_eq!(search_fields.search().text(), "test search");
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 1);
for c in "test replace".chars() {
search_fields.highlighted_field_mut().handle_keys(
KeyCode::Char(c),
KeyModifiers::NONE,
true,
);
}
assert_eq!(search_fields.replace().text(), "test replace");
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 2);
search_fields.highlighted_field_mut().handle_keys(
KeyCode::Char(' '),
KeyModifiers::NONE,
true,
);
assert!(search_fields.fixed_strings().checked);
assert_eq!(search_fields.search().text(), "test search");
assert_eq!(search_fields.fixed_strings().checked, true);
search_fields.highlighted_field_mut().handle_keys(
KeyCode::Char(' '),
KeyModifiers::NONE,
true,
);
assert_eq!(search_fields.search().text(), "test search");
assert_eq!(search_fields.fixed_strings().checked, false);
}
#[test]
fn test_focus_with_locked_disabled_fields() {
let mut search_fields = SearchFields::with_values(
&SearchFieldValues {
search: FieldValue::new("prepopulated", true),
replace: FieldValue::new("", false),
fixed_strings: FieldValue::new(true, true),
match_whole_word: FieldValue::new(false, false),
match_case: FieldValue::new(true, false),
include_files: FieldValue::new("*.rs", true),
exclude_files: FieldValue::new("", false),
},
true,
);
assert_eq!(search_fields.highlighted, 1);
assert_eq!(search_fields.highlighted_field().name, FieldName::Replace);
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 3);
assert_eq!(search_fields.highlighted_field().name, FieldName::WholeWord);
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 4);
assert_eq!(search_fields.highlighted_field().name, FieldName::MatchCase);
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 6);
assert_eq!(
search_fields.highlighted_field().name,
FieldName::ExcludeFiles
);
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 1);
assert_eq!(search_fields.highlighted_field().name, FieldName::Replace);
search_fields.focus_prev(true);
assert_eq!(search_fields.highlighted, 6);
assert_eq!(
search_fields.highlighted_field().name,
FieldName::ExcludeFiles
);
search_fields.focus_prev(true);
assert_eq!(search_fields.highlighted, 4);
assert_eq!(search_fields.highlighted_field().name, FieldName::MatchCase);
}
#[test]
fn test_focus_with_unlocked_disabled_fields() {
let mut search_fields = SearchFields::with_values(
&SearchFieldValues {
search: FieldValue::new("prepopulated", true),
replace: FieldValue::new("", false),
fixed_strings: FieldValue::new(true, true),
match_whole_word: FieldValue::new(false, false),
match_case: FieldValue::new(true, false),
include_files: FieldValue::new("*.rs", true),
exclude_files: FieldValue::new("", false),
},
false,
);
assert_eq!(search_fields.highlighted, 0);
search_fields.focus_next(false);
assert_eq!(search_fields.highlighted, 1);
search_fields.focus_next(false);
assert_eq!(search_fields.highlighted, 2);
search_fields.focus_next(false);
assert_eq!(search_fields.highlighted, 3);
}
#[test]
fn test_focus_all_fields_disabled_and_locked() {
let mut search_fields = SearchFields::with_values(
&SearchFieldValues {
search: FieldValue::new("search", true),
replace: FieldValue::new("replace", true),
fixed_strings: FieldValue::new(true, true),
match_whole_word: FieldValue::new(false, true),
match_case: FieldValue::new(true, true),
include_files: FieldValue::new("*.rs", true),
exclude_files: FieldValue::new("*.txt", true),
},
true,
);
assert_eq!(search_fields.highlighted, 0);
search_fields.focus_next(true);
assert_eq!(search_fields.highlighted, 0);
search_fields.focus_prev(true);
assert_eq!(search_fields.highlighted, 0);
}
#[test]
fn test_initial_highlight_position_with_prepopulated_fields_disable_true() {
let search_fields = SearchFields::with_values(
&SearchFieldValues {
search: FieldValue::new("cli_search", true),
replace: FieldValue::new("cli_replace", true),
fixed_strings: FieldValue::new(false, false),
match_whole_word: FieldValue::new(false, false),
match_case: FieldValue::new(true, false),
include_files: FieldValue::new("*.rs", true),
exclude_files: FieldValue::new("", false),
},
true,
);
assert_eq!(search_fields.highlighted, 2);
assert_eq!(
search_fields.highlighted_field().name,
FieldName::FixedStrings
);
}
#[test]
fn test_initial_highlight_position_with_prepopulated_fields_disable_false() {
let search_fields = SearchFields::with_values(
&SearchFieldValues {
search: FieldValue::new("cli_search", true),
replace: FieldValue::new("cli_replace", true),
fixed_strings: FieldValue::new(false, false),
match_whole_word: FieldValue::new(false, false),
match_case: FieldValue::new(true, false),
include_files: FieldValue::new("*.rs", true),
exclude_files: FieldValue::new("", false),
},
false,
);
assert_eq!(search_fields.highlighted, 0);
assert_eq!(search_fields.highlighted_field().name, FieldName::Search);
}
}