use crate::explore_regex::colors;
use crate::explore_regex::quick_ref::{QuickRefEntry, get_flattened_entries};
use edtui::{EditorMode, EditorState, Highlight, Index2, Lines, actions::InsertChar};
use fancy_regex::Regex;
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub enum InputFocus {
#[default]
Regex,
Sample,
QuickRef,
}
pub struct App {
pub input_focus: InputFocus,
pub regex_input: EditorState,
pub sample_text: EditorState,
pub compiled_regex: Option<Regex>,
pub regex_error: Option<String>,
pub match_count: usize,
pub show_quick_ref: bool,
pub show_help: bool,
pub quick_ref_selected: usize,
pub quick_ref_scroll: usize,
pub quick_ref_scroll_h: u16,
pub quick_ref_view_height: usize,
pub quick_ref_view_width: u16,
pub quick_ref_entries: Vec<QuickRefEntry>,
}
impl App {
pub fn new(input_string: String) -> Self {
let mut regex_input = EditorState::default();
regex_input.mode = EditorMode::Insert;
let mut sample_text = EditorState::new(Lines::from(input_string.as_str()));
sample_text.mode = EditorMode::Insert;
let entries = get_flattened_entries();
let initial_selected = Self::find_next_item(&entries, 0).unwrap_or(0);
Self {
input_focus: InputFocus::default(),
regex_input,
sample_text,
compiled_regex: None,
regex_error: None,
match_count: 0,
show_quick_ref: false,
show_help: false,
quick_ref_selected: initial_selected,
quick_ref_scroll: 0,
quick_ref_scroll_h: 0,
quick_ref_view_height: 0,
quick_ref_view_width: 0,
quick_ref_entries: entries,
}
}
pub fn get_sample_text(&self) -> String {
self.sample_text.lines.to_string()
}
pub fn get_regex_input(&self) -> String {
self.regex_input.lines.to_string()
}
pub fn compile_regex(&mut self) {
self.compiled_regex = None;
self.regex_error = None;
self.match_count = 0;
let regex_input = self.regex_input.lines.to_string();
if regex_input.is_empty() {
return;
}
match Regex::new(®ex_input) {
Ok(regex) => {
self.compiled_regex = Some(regex);
self.update_match_count();
}
Err(e) => {
self.regex_error = Some(e.to_string());
}
}
}
pub fn update_match_count(&mut self) {
if let Some(ref regex) = self.compiled_regex {
let sample_text = self.get_sample_text();
self.match_count = regex.captures_iter(&sample_text).flatten().count();
}
}
pub fn get_highlights(&self) -> Vec<Highlight> {
let text = self.get_sample_text();
let Some(regex) = &self.compiled_regex else {
return Vec::new();
};
let mut byte_to_pos: Vec<(usize, usize)> = vec![(0, 0); text.len() + 1];
let (mut row, mut col) = (0, 0);
for (i, ch) in text.char_indices() {
for pos in byte_to_pos.iter_mut().skip(i).take(ch.len_utf8()) {
*pos = (row, col);
}
if ch == '\n' {
row += 1;
col = 0;
} else {
col += 1;
}
}
byte_to_pos[text.len()] = (row, col);
let mut highlights = Vec::new();
for cap in regex.captures_iter(&text).flatten() {
for (group, m) in cap.iter().enumerate() {
let Some(m) = m else { continue };
let start = byte_to_pos[m.start()];
let end = byte_to_pos[m.end().saturating_sub(1)];
let size = m.end() - m.start();
highlights.push((
size,
Highlight::new(
Index2::new(start.0, start.1),
Index2::new(end.0, end.1),
colors::highlight_style(group),
),
));
}
}
highlights.sort_by_key(|(size, _)| *size);
highlights.into_iter().map(|(_, h)| h).collect()
}
pub fn toggle_quick_ref(&mut self) {
self.show_quick_ref = !self.show_quick_ref;
self.input_focus = if self.show_quick_ref {
InputFocus::QuickRef
} else {
InputFocus::Regex
};
}
pub fn close_quick_ref(&mut self) {
self.show_quick_ref = false;
self.input_focus = InputFocus::Regex;
}
pub fn toggle_help(&mut self) {
self.show_help = !self.show_help;
}
pub fn quick_ref_up(&mut self) {
if let Some(prev) = Self::find_prev_item(&self.quick_ref_entries, self.quick_ref_selected) {
self.quick_ref_selected = prev;
}
}
pub fn quick_ref_down(&mut self) {
if let Some(next) = Self::find_next_item(&self.quick_ref_entries, self.quick_ref_selected) {
self.quick_ref_selected = next;
}
}
pub fn quick_ref_page_up(&mut self) {
let target = self
.quick_ref_selected
.saturating_sub(self.quick_ref_view_height.max(1));
self.quick_ref_selected = Self::find_nearest_item(&self.quick_ref_entries, target)
.unwrap_or(self.quick_ref_selected);
}
pub fn quick_ref_page_down(&mut self) {
let target = (self.quick_ref_selected + self.quick_ref_view_height.max(1))
.min(self.quick_ref_entries.len().saturating_sub(1));
self.quick_ref_selected = Self::find_nearest_item_reverse(&self.quick_ref_entries, target)
.unwrap_or(self.quick_ref_selected);
}
pub fn insert_selected_quick_ref(&mut self) {
if let Some(QuickRefEntry::Item(item)) = self.quick_ref_entries.get(self.quick_ref_selected)
{
for ch in item.insert.chars() {
self.regex_input.execute(InsertChar(ch));
}
self.compile_regex();
}
}
pub fn quick_ref_scroll_left(&mut self) {
self.quick_ref_scroll_h = self.quick_ref_scroll_h.saturating_sub(4);
}
pub fn quick_ref_scroll_right(&mut self) {
self.quick_ref_scroll_h = self.quick_ref_scroll_h.saturating_add(4);
}
pub fn quick_ref_scroll_home(&mut self) {
self.quick_ref_scroll_h = 0;
}
fn is_selectable(entries: &[QuickRefEntry], idx: usize) -> bool {
matches!(entries.get(idx), Some(QuickRefEntry::Item(_)))
}
fn find_next_item(entries: &[QuickRefEntry], from: usize) -> Option<usize> {
((from + 1)..entries.len()).find(|&i| Self::is_selectable(entries, i))
}
fn find_prev_item(entries: &[QuickRefEntry], from: usize) -> Option<usize> {
(0..from).rev().find(|&i| Self::is_selectable(entries, i))
}
fn find_nearest_item(entries: &[QuickRefEntry], from: usize) -> Option<usize> {
(from..entries.len()).find(|&i| Self::is_selectable(entries, i))
}
fn find_nearest_item_reverse(entries: &[QuickRefEntry], from: usize) -> Option<usize> {
(0..=from).rev().find(|&i| Self::is_selectable(entries, i))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::explore_regex::quick_ref::QuickRefItem;
fn test_entries() -> Vec<QuickRefEntry> {
vec![
QuickRefEntry::Category("Cat1"),
QuickRefEntry::Item(QuickRefItem {
syntax: "a",
description: "desc a",
insert: "a",
}),
QuickRefEntry::Item(QuickRefItem {
syntax: "b",
description: "desc b",
insert: "b",
}),
QuickRefEntry::Category("Cat2"),
QuickRefEntry::Item(QuickRefItem {
syntax: "c",
description: "desc c",
insert: "c",
}),
QuickRefEntry::Category("Cat3"),
QuickRefEntry::Item(QuickRefItem {
syntax: "d",
description: "desc d",
insert: "d",
}),
QuickRefEntry::Item(QuickRefItem {
syntax: "e",
description: "desc e",
insert: "e",
}),
]
}
#[test]
fn test_is_selectable_item() {
let entries = test_entries();
assert!(App::is_selectable(&entries, 1)); assert!(App::is_selectable(&entries, 2)); assert!(App::is_selectable(&entries, 4)); }
#[test]
fn test_is_selectable_category() {
let entries = test_entries();
assert!(!App::is_selectable(&entries, 0)); assert!(!App::is_selectable(&entries, 3)); assert!(!App::is_selectable(&entries, 5)); }
#[test]
fn test_is_selectable_out_of_bounds() {
let entries = test_entries();
assert!(!App::is_selectable(&entries, 100));
}
#[test]
fn test_is_selectable_empty_list() {
let entries: Vec<QuickRefEntry> = vec![];
assert!(!App::is_selectable(&entries, 0));
}
#[test]
fn test_find_next_item_basic() {
let entries = test_entries();
assert_eq!(App::find_next_item(&entries, 1), Some(2));
}
#[test]
fn test_find_next_item_skips_category() {
let entries = test_entries();
assert_eq!(App::find_next_item(&entries, 2), Some(4));
}
#[test]
fn test_find_next_item_from_category() {
let entries = test_entries();
assert_eq!(App::find_next_item(&entries, 0), Some(1));
assert_eq!(App::find_next_item(&entries, 3), Some(4));
}
#[test]
fn test_find_next_item_at_end() {
let entries = test_entries();
assert_eq!(App::find_next_item(&entries, 7), None);
}
#[test]
fn test_find_next_item_empty_list() {
let entries: Vec<QuickRefEntry> = vec![];
assert_eq!(App::find_next_item(&entries, 0), None);
}
#[test]
fn test_find_prev_item_basic() {
let entries = test_entries();
assert_eq!(App::find_prev_item(&entries, 2), Some(1));
}
#[test]
fn test_find_prev_item_skips_category() {
let entries = test_entries();
assert_eq!(App::find_prev_item(&entries, 4), Some(2));
}
#[test]
fn test_find_prev_item_from_category() {
let entries = test_entries();
assert_eq!(App::find_prev_item(&entries, 3), Some(2));
assert_eq!(App::find_prev_item(&entries, 5), Some(4));
}
#[test]
fn test_find_prev_item_at_start() {
let entries = test_entries();
assert_eq!(App::find_prev_item(&entries, 1), None);
assert_eq!(App::find_prev_item(&entries, 0), None);
}
#[test]
fn test_find_prev_item_empty_list() {
let entries: Vec<QuickRefEntry> = vec![];
assert_eq!(App::find_prev_item(&entries, 0), None);
}
#[test]
fn test_find_nearest_item_on_item() {
let entries = test_entries();
assert_eq!(App::find_nearest_item(&entries, 1), Some(1));
}
#[test]
fn test_find_nearest_item_on_category() {
let entries = test_entries();
assert_eq!(App::find_nearest_item(&entries, 0), Some(1));
assert_eq!(App::find_nearest_item(&entries, 3), Some(4));
}
#[test]
fn test_find_nearest_item_past_end() {
let entries = test_entries();
assert_eq!(App::find_nearest_item(&entries, 100), None);
}
#[test]
fn test_find_nearest_item_empty_list() {
let entries: Vec<QuickRefEntry> = vec![];
assert_eq!(App::find_nearest_item(&entries, 0), None);
}
#[test]
fn test_find_nearest_item_reverse_on_item() {
let entries = test_entries();
assert_eq!(App::find_nearest_item_reverse(&entries, 4), Some(4));
}
#[test]
fn test_find_nearest_item_reverse_on_category() {
let entries = test_entries();
assert_eq!(App::find_nearest_item_reverse(&entries, 3), Some(2));
assert_eq!(App::find_nearest_item_reverse(&entries, 5), Some(4));
}
#[test]
fn test_find_nearest_item_reverse_at_start_category() {
let entries = test_entries();
assert_eq!(App::find_nearest_item_reverse(&entries, 0), None);
}
#[test]
fn test_find_nearest_item_reverse_empty_list() {
let entries: Vec<QuickRefEntry> = vec![];
assert_eq!(App::find_nearest_item_reverse(&entries, 0), None);
}
#[test]
fn test_quick_ref_down_moves_to_next_item() {
let mut app = create_test_app();
app.quick_ref_selected = 1;
app.quick_ref_down();
assert_eq!(app.quick_ref_selected, 2); }
#[test]
fn test_quick_ref_down_skips_category() {
let mut app = create_test_app();
app.quick_ref_selected = 2;
app.quick_ref_down();
assert_eq!(app.quick_ref_selected, 4); }
#[test]
fn test_quick_ref_down_stays_at_last_item() {
let mut app = create_test_app();
app.quick_ref_selected = 7;
app.quick_ref_down();
assert_eq!(app.quick_ref_selected, 7); }
#[test]
fn test_quick_ref_up_moves_to_prev_item() {
let mut app = create_test_app();
app.quick_ref_selected = 2;
app.quick_ref_up();
assert_eq!(app.quick_ref_selected, 1); }
#[test]
fn test_quick_ref_up_skips_category() {
let mut app = create_test_app();
app.quick_ref_selected = 4;
app.quick_ref_up();
assert_eq!(app.quick_ref_selected, 2); }
#[test]
fn test_quick_ref_up_stays_at_first_item() {
let mut app = create_test_app();
app.quick_ref_selected = 1;
app.quick_ref_up();
assert_eq!(app.quick_ref_selected, 1); }
#[test]
fn test_quick_ref_page_down() {
let mut app = create_test_app();
app.quick_ref_selected = 1; app.quick_ref_view_height = 3;
app.quick_ref_page_down();
assert_eq!(app.quick_ref_selected, 4);
}
#[test]
fn test_quick_ref_page_down_lands_on_category() {
let mut app = create_test_app();
app.quick_ref_selected = 1; app.quick_ref_view_height = 2;
app.quick_ref_page_down();
assert_eq!(app.quick_ref_selected, 2);
}
#[test]
fn test_quick_ref_page_down_at_end() {
let mut app = create_test_app();
app.quick_ref_selected = 7; app.quick_ref_view_height = 3;
app.quick_ref_page_down();
assert_eq!(app.quick_ref_selected, 7);
}
#[test]
fn test_quick_ref_page_up() {
let mut app = create_test_app();
app.quick_ref_selected = 7; app.quick_ref_view_height = 3;
app.quick_ref_page_up();
assert_eq!(app.quick_ref_selected, 4);
}
#[test]
fn test_quick_ref_page_up_lands_on_category() {
let mut app = create_test_app();
app.quick_ref_selected = 6; app.quick_ref_view_height = 3;
app.quick_ref_page_up();
assert_eq!(app.quick_ref_selected, 4);
}
#[test]
fn test_quick_ref_page_up_at_start() {
let mut app = create_test_app();
app.quick_ref_selected = 1; app.quick_ref_view_height = 3;
app.quick_ref_page_up();
assert_eq!(app.quick_ref_selected, 1);
}
#[test]
fn test_quick_ref_page_navigation_with_zero_height() {
let mut app = create_test_app();
app.quick_ref_selected = 2;
app.quick_ref_view_height = 0;
app.quick_ref_page_down();
assert_eq!(app.quick_ref_selected, 2);
}
#[test]
fn test_quick_ref_scroll_right() {
let mut app = create_test_app();
assert_eq!(app.quick_ref_scroll_h, 0);
app.quick_ref_scroll_right();
assert_eq!(app.quick_ref_scroll_h, 4);
app.quick_ref_scroll_right();
assert_eq!(app.quick_ref_scroll_h, 8);
}
#[test]
fn test_quick_ref_scroll_left() {
let mut app = create_test_app();
app.quick_ref_scroll_h = 8;
app.quick_ref_scroll_left();
assert_eq!(app.quick_ref_scroll_h, 4);
app.quick_ref_scroll_left();
assert_eq!(app.quick_ref_scroll_h, 0);
}
#[test]
fn test_quick_ref_scroll_left_at_zero() {
let mut app = create_test_app();
assert_eq!(app.quick_ref_scroll_h, 0);
app.quick_ref_scroll_left();
assert_eq!(app.quick_ref_scroll_h, 0); }
#[test]
fn test_quick_ref_scroll_home() {
let mut app = create_test_app();
app.quick_ref_scroll_h = 20;
app.quick_ref_scroll_home();
assert_eq!(app.quick_ref_scroll_h, 0);
}
#[test]
fn test_quick_ref_scroll_home_already_at_zero() {
let mut app = create_test_app();
assert_eq!(app.quick_ref_scroll_h, 0);
app.quick_ref_scroll_home();
assert_eq!(app.quick_ref_scroll_h, 0);
}
#[test]
fn test_compile_regex_with_unicode_pattern() {
let mut app = App::new(String::new());
app.regex_input = EditorState::new(Lines::from("\\p{L}+"));
app.compile_regex();
assert!(app.compiled_regex.is_some());
assert!(app.regex_error.is_none());
}
#[test]
fn test_get_highlights_no_regex() {
let app = App::new("hello world".to_string());
let highlights = app.get_highlights();
assert!(highlights.is_empty());
}
#[test]
fn test_get_highlights_no_matches() {
let mut app = App::new("hello world".to_string());
app.regex_input = EditorState::new(Lines::from("xyz"));
app.compile_regex();
let highlights = app.get_highlights();
assert!(highlights.is_empty());
}
#[test]
fn test_get_highlights_single_match() {
let mut app = App::new("hello world".to_string());
app.regex_input = EditorState::new(Lines::from("world"));
app.compile_regex();
let highlights = app.get_highlights();
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].start, Index2::new(0, 6));
assert_eq!(highlights[0].end, Index2::new(0, 10));
}
#[test]
fn test_get_highlights_multiple_matches() {
let mut app = App::new("cat dog cat".to_string());
app.regex_input = EditorState::new(Lines::from("cat"));
app.compile_regex();
let highlights = app.get_highlights();
assert_eq!(highlights.len(), 2);
assert_eq!(highlights[0].start, Index2::new(0, 0));
assert_eq!(highlights[0].end, Index2::new(0, 2));
assert_eq!(highlights[1].start, Index2::new(0, 8));
assert_eq!(highlights[1].end, Index2::new(0, 10));
}
#[test]
fn test_get_highlights_multiline() {
let mut app = App::new("hello\nworld".to_string());
app.regex_input = EditorState::new(Lines::from("world"));
app.compile_regex();
let highlights = app.get_highlights();
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].start, Index2::new(1, 0));
assert_eq!(highlights[0].end, Index2::new(1, 4));
}
#[test]
fn test_get_highlights_capture_groups() {
let mut app = App::new("hello world".to_string());
app.regex_input = EditorState::new(Lines::from("(hello) (world)"));
app.compile_regex();
let highlights = app.get_highlights();
assert_eq!(highlights.len(), 3);
assert_eq!(highlights[0].style, colors::highlight_style(1));
assert_eq!(highlights[1].style, colors::highlight_style(2));
assert_eq!(highlights[2].style, colors::highlight_style(0));
}
#[test]
fn test_get_highlights_unicode() {
let mut app = App::new("日本語テスト".to_string());
app.regex_input = EditorState::new(Lines::from("テスト"));
app.compile_regex();
let highlights = app.get_highlights();
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].start, Index2::new(0, 3));
assert_eq!(highlights[0].end, Index2::new(0, 5));
}
#[test]
fn test_get_highlighted_text_with_unicode_sample() {
let mut app = App::new("12345abcde項目".to_string());
app.regex_input = EditorState::new(Lines::from("\\w+"));
app.compile_regex();
let highlights = app.get_highlights();
assert_eq!(highlights.len(), 1);
assert_eq!(highlights[0].start, Index2::new(0, 0));
assert_eq!(highlights[0].end, Index2::new(0, 11));
}
fn create_test_app() -> App {
let mut app = App::new(String::new());
app.quick_ref_entries = test_entries();
app.quick_ref_selected = 1; app.quick_ref_view_height = 5;
app
}
}