use crate::viewer::{
error::{DotViewerError, DotViewerResult},
utils::{List, Tree, Trie},
};
use graphviz_rs::prelude::*;
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use rayon::prelude::*;
use regex::Regex;
type Matcher = fn(&str, &str, &Graph) -> Option<Vec<usize>>;
pub(crate) struct View {
pub title: String,
pub graph: Graph,
pub focus: Focus,
pub current: List<String>,
pub prevs: List<String>,
pub nexts: List<String>,
pub key: String,
pub matches: List<(usize, Vec<usize>)>,
pub trie: Trie,
pub subtree: Tree,
}
#[derive(PartialEq)]
pub(crate) enum Focus {
Current,
Prev,
Next,
}
impl View {
pub fn new(title: String, graph: Graph) -> DotViewerResult<Self> {
let node_ids = graph.topsort()?;
let node_ids = node_ids.iter().map(|&id| id.clone());
let trie = Trie::from_iter(node_ids.clone());
let focus = Focus::Current;
let current = List::from_iter(node_ids);
let prevs = List::from_iter(Vec::new());
let nexts = List::from_iter(Vec::new());
let key = String::new();
let matches = List::from_iter(Vec::new());
let subtree = Tree::from_graph(&graph);
let mut view =
Self { title, graph, focus, current, prevs, nexts, key, matches, trie, subtree };
view.update_adjacent().expect("there is always a selected current node on initialization");
Ok(view)
}
pub fn goto_first(&mut self) -> DotViewerResult<()> {
match &self.focus {
Focus::Current => {
self.current.first();
self.update_adjacent()?
}
Focus::Prev => self.prevs.first(),
Focus::Next => self.nexts.first(),
}
Ok(())
}
pub fn goto_last(&mut self) -> DotViewerResult<()> {
match &self.focus {
Focus::Current => {
self.current.last();
self.update_adjacent()?
}
Focus::Prev => self.prevs.last(),
Focus::Next => self.nexts.last(),
}
Ok(())
}
pub fn goto_adjacent(&mut self) -> DotViewerResult<()> {
let err = Err(DotViewerError::ViewerError("no node selected".to_string()));
match &self.focus {
Focus::Prev => self.prevs.selected().map_or(err, |id| self.goto(&id)),
Focus::Next => self.nexts.selected().map_or(err, |id| self.goto(&id)),
_ => err,
}
}
pub fn goto_match(&mut self) -> DotViewerResult<()> {
self.matched_id()
.map_or(Err(DotViewerError::ViewerError("no node selected".to_string())), |id| {
self.goto(&id)
})
}
pub fn goto(&mut self, id: &str) -> DotViewerResult<()> {
let idx = (self.current)
.find(id.to_string())
.ok_or(DotViewerError::ViewerError(format!("no such node {id:?}")))?;
self.current.select(idx);
self.update_adjacent()?;
Ok(())
}
pub fn filter(&mut self) -> DotViewerResult<View> {
let node_ids: Vec<&String> =
(self.matches.items.iter()).map(|(idx, _)| &self.current.items[*idx]).collect();
let graph = self.graph.filter(&node_ids);
if graph.is_empty() {
let key = &self.key;
return Err(DotViewerError::ViewerError(format!("no match for keyword {key}")));
}
Self::new(format!("{} - {}", self.title, self.key), graph)
}
pub fn subgraph(&mut self) -> DotViewerResult<View> {
let key = (self.subtree)
.selected()
.ok_or(DotViewerError::ViewerError("no subgraph selected".to_string()))?;
let subgraph =
self.graph.subgraph(&key).map_err(|e| DotViewerError::ViewerError(e.to_string()))?;
if subgraph.is_empty() {
return Err(DotViewerError::ViewerError("empty graph".to_string()));
}
let title = &self.title;
Self::new(format!("{title} - {key}"), subgraph)
}
pub fn neighbors(&mut self, depth: usize) -> DotViewerResult<View> {
let id = self.current_id();
let graph = self.graph.neighbors(&id, depth)?;
if graph.is_empty() {
return Err(DotViewerError::ViewerError("cannot define a neighbors graph".to_string()));
}
let title = &self.title;
Self::new(format!("{title} - neighbors-{id}-{depth}"), graph)
}
pub fn autocomplete(&mut self, key: &str) -> Option<String> {
self.trie.autocomplete(key)
}
pub fn update_adjacent(&mut self) -> DotViewerResult<()> {
let id = self.current_id();
let mut prevs = Vec::from_iter(self.graph.froms(&id)?);
prevs.sort_unstable();
let prevs = prevs.iter().map(|n| n.to_string());
self.prevs = List::from_iter(prevs);
let mut nexts = Vec::from_iter(self.graph.tos(&id)?);
nexts.sort_unstable();
let nexts = nexts.iter().map(|n| n.to_string());
self.nexts = List::from_iter(nexts);
Ok(())
}
fn update_matches(&mut self, matcher: Matcher, key: &str) {
let matches: Vec<(usize, Vec<usize>)> = (self.current.items.par_iter())
.enumerate()
.filter_map(|(idx, id)| matcher(id, key, &self.graph).map(|highlight| (idx, highlight)))
.collect();
self.key = key.to_string();
self.matches = List::from_iter(matches);
}
pub fn update_fuzzy(&mut self, key: &str) {
self.update_matches(match_fuzzy, key);
}
pub fn update_regex(&mut self, key: &str) {
self.update_matches(match_regex, key);
}
pub fn update_trie(&mut self) {
let nodes = self.matches.items.iter().map(|(idx, _)| self.current.items[*idx].clone());
self.trie = Trie::from_iter(nodes);
}
pub fn current_id(&self) -> String {
self.current.selected().expect("there is always a current id selected in a view")
}
pub fn matched_id(&self) -> Option<String> {
self.matches.selected().map(|(idx, _)| self.current.items[idx].clone())
}
pub fn progress_current(&self) -> String {
let idx = self.current.state.selected().unwrap();
let len = self.current.items.len();
let percentage = (idx as f32 / len as f32) * 100_f32;
format!("[{} / {} ({:.3}%)]", idx + 1, len, percentage)
}
pub fn progress_matches(&self) -> String {
let idx = self.matches.state.selected().unwrap();
let len = self.matches.items.len();
let percentage = (idx as f32 / len as f32) * 100_f32;
format!("[{} / {} ({:.3}%)]", idx + 1, len, percentage)
}
}
fn match_fuzzy(id: &str, key: &str, _graph: &Graph) -> Option<Vec<usize>> {
let matcher = SkimMatcherV2::default();
matcher.fuzzy_indices(id, key).map(|(_, idxs)| idxs)
}
fn match_regex(id: &str, key: &str, graph: &Graph) -> Option<Vec<usize>> {
if let Ok(matcher) = Regex::new(key) {
let node = graph.search_node(&id.to_string()).unwrap();
let mut buffer = Vec::new();
node.to_dot(0, &mut buffer).expect("to_dot should succeed");
let raw = std::str::from_utf8(&buffer).unwrap();
let highlight: Vec<usize> = (0..id.len()).collect();
matcher.is_match(raw).then_some(highlight)
} else {
None
}
}