use super::types::DiagramNode;
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct SearchState {
pub(crate) active: bool,
pub(crate) query: String,
pub(crate) matches: Vec<usize>,
pub(crate) current_match: usize,
}
impl SearchState {
pub(crate) fn start(&mut self) {
self.active = true;
self.query.clear();
self.matches.clear();
self.current_match = 0;
}
pub(crate) fn cancel(&mut self) {
self.active = false;
self.query.clear();
self.matches.clear();
self.current_match = 0;
}
pub(crate) fn input(&mut self, ch: char, nodes: &[DiagramNode]) {
self.query.push(ch);
self.recompute_matches(nodes);
}
pub(crate) fn backspace(&mut self, nodes: &[DiagramNode]) {
self.query.pop();
self.recompute_matches(nodes);
}
pub(crate) fn next_match(&mut self) {
if !self.matches.is_empty() {
self.current_match = (self.current_match + 1) % self.matches.len();
}
}
pub(crate) fn prev_match(&mut self) {
if !self.matches.is_empty() {
self.current_match = if self.current_match == 0 {
self.matches.len() - 1
} else {
self.current_match - 1
};
}
}
pub(crate) fn current_node_index(&self) -> Option<usize> {
self.matches.get(self.current_match).copied()
}
fn recompute_matches(&mut self, nodes: &[DiagramNode]) {
let query_lower = self.query.to_lowercase();
self.matches = nodes
.iter()
.enumerate()
.filter(|(_, node)| {
if query_lower.is_empty() {
return false;
}
node.id().to_lowercase().contains(&query_lower)
|| node.label().to_lowercase().contains(&query_lower)
})
.map(|(idx, _)| idx)
.collect();
if self.current_match >= self.matches.len() {
self.current_match = 0;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_nodes() -> Vec<DiagramNode> {
vec![
DiagramNode::new("api", "API Gateway"),
DiagramNode::new("auth", "Auth Service"),
DiagramNode::new("db", "Database"),
DiagramNode::new("cache", "Redis Cache"),
]
}
#[test]
fn test_start_and_cancel() {
let mut search = SearchState::default();
assert!(!search.active);
search.start();
assert!(search.active);
assert!(search.query.is_empty());
search.cancel();
assert!(!search.active);
}
#[test]
fn test_search_by_label() {
let nodes = make_nodes();
let mut search = SearchState::default();
search.start();
search.input('a', &nodes);
search.input('p', &nodes);
search.input('i', &nodes);
assert_eq!(search.matches.len(), 1);
assert_eq!(search.matches[0], 0);
}
#[test]
fn test_search_case_insensitive() {
let nodes = make_nodes();
let mut search = SearchState::default();
search.start();
search.input('D', &nodes);
search.input('a', &nodes);
search.input('t', &nodes);
assert_eq!(search.matches.len(), 1);
assert_eq!(search.matches[0], 2);
}
#[test]
fn test_search_multiple_matches() {
let nodes = make_nodes();
let mut search = SearchState::default();
search.start();
search.input('a', &nodes);
assert!(search.matches.len() >= 3);
}
#[test]
fn test_next_prev_match() {
let nodes = make_nodes();
let mut search = SearchState::default();
search.start();
search.input('a', &nodes);
let first = search.current_node_index();
search.next_match();
let second = search.current_node_index();
assert_ne!(first, second);
search.prev_match();
assert_eq!(search.current_node_index(), first);
}
#[test]
fn test_backspace() {
let nodes = make_nodes();
let mut search = SearchState::default();
search.start();
search.input('a', &nodes);
search.input('p', &nodes);
search.input('i', &nodes);
assert_eq!(search.matches.len(), 1);
search.backspace(&nodes);
assert!(!search.matches.is_empty());
search.backspace(&nodes);
search.backspace(&nodes);
assert!(search.matches.is_empty());
}
#[test]
fn test_match_contains() {
let nodes = make_nodes();
let mut search = SearchState::default();
search.start();
search.input('d', &nodes);
search.input('b', &nodes);
assert!(search.matches.contains(&2)); assert!(!search.matches.contains(&0)); }
#[test]
fn test_empty_query_no_matches() {
let _nodes = make_nodes();
let mut search = SearchState::default();
search.start();
assert!(search.matches.is_empty());
assert_eq!(search.current_node_index(), None);
}
#[test]
fn test_next_wraps_around() {
let nodes = make_nodes();
let mut search = SearchState::default();
search.start();
search.input('a', &nodes);
let count = search.matches.len();
for _ in 0..count {
search.next_match();
}
assert_eq!(search.current_match, 0);
}
}