use std::borrow::Cow;
use std::ops::Range;
use regex::{Captures, Regex, RegexBuilder};
use crate::flatjson::{FlatJson, Index};
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum SearchDirection {
Forward,
Reverse,
}
impl SearchDirection {
pub fn prompt_char(&self) -> char {
match self {
SearchDirection::Forward => '/',
SearchDirection::Reverse => '?',
}
}
}
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum JumpDirection {
Next,
Prev,
}
pub struct SearchState {
pub direction: SearchDirection,
pub search_term: String,
matches: Vec<Range<usize>>,
immediate_state: ImmediateSearchState,
pub ever_searched: bool,
}
pub enum ImmediateSearchState {
NotSearching,
MatchesVisible,
ActivelySearching {
last_match_jumped_to: usize,
last_search_into_collapsed_container: bool,
just_wrapped: bool,
},
}
pub type MatchRangeIter<'a> = std::slice::Iter<'a, Range<usize>>;
const STATIC_EMPTY_SLICE: &[Range<usize>] = &[];
lazy_static::lazy_static! {
static ref SQUARE_AND_CURLY_BRACKETS: Regex = Regex::new(r"(\\\[|\[|\\\]|\]|\\\{|\{|\\\}|\})").unwrap();
}
lazy_static::lazy_static! {
static ref UPPER_CASE: Regex = Regex::new("[[:upper:]]").unwrap();
}
impl SearchState {
pub fn empty() -> SearchState {
SearchState {
direction: SearchDirection::Forward,
search_term: "".to_owned(),
matches: vec![],
immediate_state: ImmediateSearchState::NotSearching,
ever_searched: false,
}
}
fn extract_search_term_and_case_sensitivity(search_input: &str) -> (&str, bool) {
let regex_input;
let mut case_sensitive_specified = false;
if let Some(stripped_of_slash) = search_input.strip_suffix('/') {
regex_input = stripped_of_slash;
} else if let Some(stripped_of_slash_s) = search_input.strip_suffix("/s") {
regex_input = stripped_of_slash_s;
case_sensitive_specified = true;
} else {
regex_input = search_input;
}
let case_sensitive = if case_sensitive_specified {
true
} else {
UPPER_CASE.is_match(regex_input)
};
(regex_input, case_sensitive)
}
fn invert_square_and_curly_bracket_escaping(regex: &str) -> Cow<str> {
SQUARE_AND_CURLY_BRACKETS.replace_all(regex, |caps: &Captures| match &caps[0] {
"\\[" => "[".to_owned(),
"[" => "\\[".to_owned(),
"\\]" => "]".to_owned(),
"]" => "\\]".to_owned(),
"\\{" => "{".to_owned(),
"{" => "\\{".to_owned(),
"\\}" => "}".to_owned(),
"}" => "\\}".to_owned(),
_ => unreachable!(),
})
}
pub fn initialize_search(
search_input: String,
haystack: &str,
direction: SearchDirection,
) -> Result<SearchState, String> {
let (regex_input, case_sensitive) =
Self::extract_search_term_and_case_sensitivity(&search_input);
if regex_input.is_empty() {
return Ok(Self::empty());
}
let inverted = Self::invert_square_and_curly_bracket_escaping(regex_input);
let regex = RegexBuilder::new(&inverted)
.case_insensitive(!case_sensitive)
.build()
.map_err(|e| format!("{e}").replace('\n', " "))?;
let matches: Vec<Range<usize>> = regex.find_iter(haystack).map(|m| m.range()).collect();
Ok(SearchState {
direction,
search_term: regex_input.to_owned(),
matches,
immediate_state: ImmediateSearchState::NotSearching,
ever_searched: true,
})
}
pub fn showing_matches(&self) -> bool {
match self.immediate_state {
ImmediateSearchState::NotSearching => false,
ImmediateSearchState::MatchesVisible
| ImmediateSearchState::ActivelySearching { .. } => true,
}
}
pub fn active_search_state(&self) -> Option<(usize, bool)> {
match self.immediate_state {
ImmediateSearchState::NotSearching | ImmediateSearchState::MatchesVisible => None,
ImmediateSearchState::ActivelySearching {
last_match_jumped_to,
just_wrapped,
..
} => Some((last_match_jumped_to, just_wrapped)),
}
}
pub fn num_matches(&self) -> usize {
self.matches.len()
}
pub fn any_matches(&self) -> bool {
!self.matches.is_empty()
}
pub fn no_matches_message(&self) -> String {
format!("Pattern not found: {}", self.search_term)
}
pub fn set_no_longer_actively_searching(&mut self) {
self.immediate_state = ImmediateSearchState::NotSearching;
}
pub fn set_matches_visible_if_actively_searching(&mut self) {
if let ImmediateSearchState::ActivelySearching { .. } = self.immediate_state {
self.immediate_state = ImmediateSearchState::MatchesVisible;
}
}
pub fn jump_to_match(
&mut self,
focused_row: Index,
flatjson: &FlatJson,
jump_direction: JumpDirection,
jumps: usize,
) -> usize {
if self.matches.is_empty() {
panic!("Shouldn't call jump_to_match if no matches");
}
let true_direction = self.true_direction(jump_direction);
let next_match_index = self.get_next_match(focused_row, flatjson, true_direction, jumps);
let row_containing_match = self.compute_destination_row(flatjson, next_match_index);
let next_focused_row = flatjson.first_visible_ancestor(row_containing_match);
let wrapped = if focused_row == next_focused_row {
if let Some((last_match_index, _)) = self.active_search_state() {
last_match_index == next_match_index
} else {
true
}
} else {
match true_direction {
SearchDirection::Forward => next_focused_row < focused_row,
SearchDirection::Reverse => next_focused_row > focused_row,
}
};
self.immediate_state = ImmediateSearchState::ActivelySearching {
last_match_jumped_to: next_match_index,
last_search_into_collapsed_container: row_containing_match != next_focused_row,
just_wrapped: wrapped,
};
next_focused_row
}
pub fn matches_iter(&self, range_start: usize) -> MatchRangeIter {
match self.immediate_state {
ImmediateSearchState::NotSearching => STATIC_EMPTY_SLICE.iter(),
ImmediateSearchState::MatchesVisible
| ImmediateSearchState::ActivelySearching { .. } => {
let search_result = self
.matches
.binary_search_by(|probe| probe.end.cmp(&range_start));
let start_index = match search_result {
Ok(i) => i,
Err(i) => i,
};
self.matches[start_index..].iter()
}
}
}
pub fn current_match_range(&self) -> Range<usize> {
match self.immediate_state {
ImmediateSearchState::NotSearching | ImmediateSearchState::MatchesVisible => 0..0,
ImmediateSearchState::ActivelySearching {
last_match_jumped_to,
..
} => self.matches[last_match_jumped_to].clone(),
}
}
fn true_direction(&self, jump_direction: JumpDirection) -> SearchDirection {
match (self.direction, jump_direction) {
(SearchDirection::Forward, JumpDirection::Next) => SearchDirection::Forward,
(SearchDirection::Forward, JumpDirection::Prev) => SearchDirection::Reverse,
(SearchDirection::Reverse, JumpDirection::Next) => SearchDirection::Reverse,
(SearchDirection::Reverse, JumpDirection::Prev) => SearchDirection::Forward,
}
}
fn get_next_match(
&mut self,
focused_row: Index,
flatjson: &FlatJson,
true_direction: SearchDirection,
jumps: usize,
) -> usize {
debug_assert!(jumps != 0);
match self.immediate_state {
ImmediateSearchState::NotSearching | ImmediateSearchState::MatchesVisible => {
let focused_row_range = flatjson[focused_row].range_represented_by_row();
match true_direction {
SearchDirection::Forward => {
let next_match = self.matches.partition_point(|match_range| {
match_range.start <= focused_row_range.end
});
let next_match_index = if next_match == self.matches.len() {
0
} else {
next_match
};
self.cycle_match(next_match_index, (jumps - 1) as isize)
}
SearchDirection::Reverse => {
let next_match = self.matches.partition_point(|match_range| {
match_range.end < focused_row_range.start
});
let next_match_index = if next_match == 0 {
self.matches.len() - 1
} else {
next_match - 1
};
self.cycle_match(next_match_index, -((jumps - 1) as isize))
}
}
}
ImmediateSearchState::ActivelySearching {
last_match_jumped_to,
last_search_into_collapsed_container,
..
} => {
let delta: isize = match true_direction {
SearchDirection::Forward => jumps as isize,
SearchDirection::Reverse => -(jumps as isize),
};
if last_search_into_collapsed_container {
let start_match = last_match_jumped_to;
let mut next_match = self.cycle_match(start_match, delta);
while next_match != start_match {
let next_destination_row =
self.compute_destination_row(flatjson, next_match);
let next_match_visible_ancestor =
flatjson.first_visible_ancestor(next_destination_row);
if next_match_visible_ancestor != focused_row {
break;
}
next_match = self.cycle_match(next_match, delta);
}
next_match
} else {
self.cycle_match(last_match_jumped_to, delta)
}
}
}
}
fn cycle_match(&self, match_index: usize, delta: isize) -> usize {
((match_index + self.matches.len()) as isize + delta) as usize % self.matches.len()
}
fn compute_destination_row(&self, flatjson: &FlatJson, match_index: usize) -> Index {
let match_range = &self.matches[match_index];
flatjson
.0
.partition_point(|row| row.range_represented_by_row().start <= match_range.start)
- 1
}
}
#[cfg(test)]
mod tests {
use crate::flatjson::parse_top_level_json;
use super::JumpDirection::*;
use super::SearchDirection::*;
use super::SearchState;
const SEARCHABLE: &str = r#"{
"1": "aaa",
"2": [
"3 bbb",
"4 aaa"
],
"6": {
"7": "aaa aaa",
"8": "ccc",
"9": "ddd"
},
"11": "bbb"
}"#;
#[test]
fn test_extract_search_term_and_case_sensitivity() {
let tests = vec![
("abc", ("abc", false)),
("Abc", ("Abc", true)),
("abc/", ("abc", false)),
("abc/s", ("abc", true)),
("abc/s/", ("abc/s", false)),
];
for (input, search_term_and_case_sensitivity) in tests.into_iter() {
assert_eq!(
search_term_and_case_sensitivity,
SearchState::extract_search_term_and_case_sensitivity(input),
);
}
}
#[test]
fn test_invert_square_and_curly_bracket_escaping() {
let tests = vec![
("[]", "\\[\\]"),
("{}", "\\{\\}"),
("\\[abc\\]", "[abc]"),
("\\{1,3\\}", "{1,3}"),
("\\[[]\\]", "[\\[\\]]"),
];
for (before, after) in tests.into_iter() {
assert_eq!(
after,
SearchState::invert_square_and_curly_bracket_escaping(before),
);
}
}
#[test]
fn test_basic_search_forward() {
let fj = parse_top_level_json(SEARCHABLE.to_owned()).unwrap();
let mut search = SearchState::initialize_search("aaa".to_owned(), &fj.1, Forward).unwrap();
assert_eq!(search.jump_to_match(0, &fj, Next, 1), 1);
assert_eq!(search.jump_to_match(1, &fj, Next, 1), 4);
assert_eq!(search.jump_to_match(4, &fj, Next, 1), 7);
assert_eq!(search.jump_to_match(7, &fj, Next, 1), 7);
assert_wrapped_state(&search, false);
assert_eq!(search.jump_to_match(7, &fj, Next, 1), 1);
assert_wrapped_state(&search, true);
assert_eq!(search.jump_to_match(1, &fj, Prev, 1), 7);
assert_wrapped_state(&search, true);
assert_eq!(search.jump_to_match(7, &fj, Prev, 1), 7);
assert_wrapped_state(&search, false);
assert_eq!(search.jump_to_match(7, &fj, Prev, 1), 4);
assert_eq!(search.jump_to_match(4, &fj, Prev, 1), 1);
assert_eq!(search.jump_to_match(1, &fj, Prev, 1), 7);
let mut search = SearchState::initialize_search("aaa".to_owned(), &fj.1, Forward).unwrap();
assert_eq!(search.jump_to_match(0, &fj, Next, 4), 7);
assert_eq!(search.jump_to_match(1, &fj, Next, 2), 4);
assert_eq!(search.jump_to_match(4, &fj, Next, 3), 1);
assert_eq!(search.jump_to_match(1, &fj, Prev, 2), 7);
assert_eq!(search.jump_to_match(7, &fj, Prev, 3), 7);
assert_eq!(search.jump_to_match(7, &fj, Next, 1), 1);
assert_eq!(search.jump_to_match(1, &fj, Next, 4_000_000_001), 4);
assert_eq!(search.jump_to_match(4, &fj, Prev, 4_000_000_001), 1);
}
#[test]
fn test_basic_search_backwards() {
let fj = parse_top_level_json(SEARCHABLE.to_owned()).unwrap();
let mut search = SearchState::initialize_search("aaa".to_owned(), &fj.1, Reverse).unwrap();
assert_eq!(search.jump_to_match(0, &fj, Next, 1), 7);
assert_wrapped_state(&search, true);
assert_eq!(search.jump_to_match(7, &fj, Next, 1), 7);
assert_eq!(search.jump_to_match(7, &fj, Next, 1), 4);
assert_eq!(search.jump_to_match(4, &fj, Next, 1), 1);
assert_wrapped_state(&search, false);
assert_eq!(search.jump_to_match(1, &fj, Prev, 1), 4);
assert_eq!(search.jump_to_match(4, &fj, Prev, 1), 7);
assert_eq!(search.jump_to_match(7, &fj, Prev, 1), 7);
assert_eq!(search.jump_to_match(7, &fj, Prev, 1), 1);
assert_wrapped_state(&search, true);
assert_eq!(search.jump_to_match(1, &fj, Prev, 1), 4);
assert_wrapped_state(&search, false);
let mut search = SearchState::initialize_search("aaa".to_owned(), &fj.1, Reverse).unwrap();
assert_eq!(search.jump_to_match(0, &fj, Next, 4), 1);
assert_eq!(search.jump_to_match(1, &fj, Next, 3), 4);
assert_eq!(search.jump_to_match(4, &fj, Next, 2), 7);
assert_eq!(search.jump_to_match(7, &fj, Prev, 2), 4);
assert_eq!(search.jump_to_match(4, &fj, Prev, 3), 1);
}
#[test]
fn test_search_collapsed_forward() {
let mut fj = parse_top_level_json(SEARCHABLE.to_owned()).unwrap();
let mut search = SearchState::initialize_search("aaa".to_owned(), &fj.1, Forward).unwrap();
fj.collapse(6);
assert_eq!(search.jump_to_match(0, &fj, Next, 1), 1);
assert_eq!(search.jump_to_match(1, &fj, Next, 1), 4);
assert_eq!(search.jump_to_match(4, &fj, Next, 1), 6);
assert_eq!(search.jump_to_match(6, &fj, Next, 1), 1);
assert_eq!(search.jump_to_match(1, &fj, Next, 1), 4);
assert_eq!(search.jump_to_match(4, &fj, Prev, 1), 1);
assert_eq!(search.jump_to_match(1, &fj, Prev, 1), 6);
assert_eq!(search.jump_to_match(6, &fj, Prev, 1), 4);
let mut search = SearchState::initialize_search("aaa".to_owned(), &fj.1, Forward).unwrap();
fj.collapse(6);
assert_eq!(search.jump_to_match(0, &fj, Next, 4), 6);
assert_eq!(search.jump_to_match(6, &fj, Next, 1), 1);
assert_eq!(search.jump_to_match(1, &fj, Next, 1), 4);
assert_eq!(search.jump_to_match(4, &fj, Next, 3), 1);
assert_eq!(search.jump_to_match(1, &fj, Prev, 2), 6);
assert_eq!(search.jump_to_match(6, &fj, Prev, 1), 4);
assert_eq!(search.jump_to_match(4, &fj, Prev, 1), 1);
assert_eq!(search.jump_to_match(1, &fj, Prev, 3), 4);
}
#[test]
fn test_search_collapsed_backwards() {
let mut fj = parse_top_level_json(SEARCHABLE.to_owned()).unwrap();
let mut search = SearchState::initialize_search("aaa".to_owned(), &fj.1, Reverse).unwrap();
fj.collapse(6);
assert_eq!(search.jump_to_match(0, &fj, Next, 1), 6);
assert_eq!(search.jump_to_match(6, &fj, Next, 1), 4);
assert_eq!(search.jump_to_match(4, &fj, Next, 1), 1);
assert_eq!(search.jump_to_match(1, &fj, Next, 1), 6);
assert_eq!(search.jump_to_match(6, &fj, Prev, 1), 1);
assert_eq!(search.jump_to_match(1, &fj, Prev, 1), 4);
assert_eq!(search.jump_to_match(4, &fj, Prev, 1), 6);
assert_eq!(search.jump_to_match(6, &fj, Prev, 1), 1);
let mut search = SearchState::initialize_search("aaa".to_owned(), &fj.1, Reverse).unwrap();
fj.collapse(6);
assert_eq!(search.jump_to_match(0, &fj, Prev, 4), 6);
assert_eq!(search.jump_to_match(6, &fj, Prev, 1), 1);
assert_eq!(search.jump_to_match(1, &fj, Prev, 1), 4);
assert_eq!(search.jump_to_match(4, &fj, Prev, 3), 1);
assert_eq!(search.jump_to_match(1, &fj, Next, 2), 6);
assert_eq!(search.jump_to_match(6, &fj, Next, 1), 4);
assert_eq!(search.jump_to_match(4, &fj, Next, 1), 1);
assert_eq!(search.jump_to_match(1, &fj, Next, 3), 4);
}
#[test]
fn test_no_wrap_when_opening_of_collapsed_container_and_contents_match_search() {
const TEST: &str = r#"{
"term": [
"term"
],
"key": "term"
}"#;
let mut fj = parse_top_level_json(TEST.to_owned()).unwrap();
let mut search = SearchState::initialize_search("term".to_owned(), &fj.1, Forward).unwrap();
fj.collapse(1);
assert_eq!(search.jump_to_match(0, &fj, Next, 1), 1);
assert_wrapped_state(&search, false);
assert_eq!(search.jump_to_match(1, &fj, Next, 1), 1);
assert_wrapped_state(&search, false);
assert_eq!(search.jump_to_match(1, &fj, Next, 1), 4);
assert_wrapped_state(&search, false);
assert_eq!(search.jump_to_match(4, &fj, Next, 1), 1);
assert_wrapped_state(&search, true);
}
#[track_caller]
fn assert_wrapped_state(search: &SearchState, expected: bool) {
if let Some((_, wrapped)) = search.active_search_state() {
assert_eq!(wrapped, expected);
} else {
assert!(false, "Not in an active search state");
}
}
}