1use super::{SearchFilter, SearchResult, build_descendants};
2use crate::Lb;
3use crate::model::file::File;
4use nucleo::{
5 Matcher, Nucleo,
6 pattern::{CaseMatching, Normalization},
7};
8use std::cmp::Reverse;
9use std::collections::{HashMap, HashSet};
10use std::sync::Arc;
11use uuid::Uuid;
12
13pub(crate) fn split_path(path: &str) -> (&str, &str) {
15 let path = path.trim_end_matches('/');
17 match path.rfind('/') {
18 Some(idx) if idx > 0 => (&path[..idx], &path[idx + 1..]),
19 Some(_) => ("/", &path[1..]), None => ("", path),
21 }
22}
23
24#[derive(Clone)]
25struct PathEntry {
26 file: File,
27 path: String,
28 filename: String,
29 parent_path: String,
30}
31
32fn path_result(entry: &PathEntry, path_indices: Vec<u32>) -> SearchResult {
33 SearchResult {
34 id: entry.file.id,
35 filename: entry.filename.clone(),
36 parent_path: entry.parent_path.clone(),
37 is_folder: entry.file.is_folder(),
38 path_indices,
39 path_matches: Vec::new(),
40 content_matches: Vec::new(),
41 }
42}
43
44pub struct PathSearcher {
45 nucleo: Nucleo<PathEntry>,
46 results: Vec<SearchResult>,
47 submitted_query: String,
48 descendants: HashMap<Uuid, Vec<Uuid>>,
49 path_to_id: HashMap<String, Uuid>,
50 filter_ids: Option<HashSet<Uuid>>,
51}
52
53impl PathSearcher {
54 pub async fn new(lb: &Lb) -> Self {
55 let files = lb.list_metadatas().await.unwrap_or_default();
56 let mut paths = lb.list_paths_with_ids(None).await.unwrap_or_default();
57 paths.retain(|(_, path)| path != "/");
58
59 let notify = Arc::new(|| {});
60 let nucleo: Nucleo<PathEntry> = Nucleo::new(nucleo::Config::DEFAULT, notify, None, 1);
61 let injector = nucleo.injector();
62
63 for (id, path) in &paths {
64 if let Some(file) = files.iter().find(|f| f.id == *id) {
65 let (parent_path, filename) = split_path(path);
66 injector.push(
67 PathEntry {
68 file: file.clone(),
69 path: path.clone(),
70 filename: filename.to_string(),
71 parent_path: parent_path.to_string(),
72 },
73 |entry, cols| {
74 cols[0] = entry.path.as_str().into();
75 },
76 );
77 }
78 }
79
80 let descendants = build_descendants(&files);
81 let path_to_id = paths.iter().map(|(id, path)| (path.clone(), *id)).collect();
82
83 let mut searcher = Self {
84 nucleo,
85 results: Vec::new(),
86 submitted_query: String::new(),
87 descendants,
88 path_to_id,
89 filter_ids: None,
90 };
91 searcher.query("");
92 searcher
93 }
94
95 pub fn update_filter(&mut self, filter: Option<SearchFilter>) {
97 self.filter_ids = super::resolve_filter(filter, &self.path_to_id, &self.descendants);
98 self.rebuild();
99 }
100
101 pub fn query(&mut self, input: &str) {
103 self.nucleo.pattern.reparse(
104 0,
105 input,
106 CaseMatching::Smart,
107 Normalization::Smart,
108 input.starts_with(&self.submitted_query),
109 );
110 self.submitted_query = input.to_string();
111
112 while self.nucleo.tick(10).running {}
113
114 self.rebuild();
115 }
116
117 fn rebuild(&mut self) {
120 self.results.clear();
121 let snapshot = self.nucleo.snapshot();
122
123 if self.submitted_query.is_empty() {
124 let mut entries: Vec<&PathEntry> = (0..snapshot.matched_item_count())
125 .filter_map(|i| snapshot.get_matched_item(i).map(|item| item.data))
126 .filter(|e| {
127 self.filter_ids
128 .as_ref()
129 .is_none_or(|ids| ids.contains(&e.file.id))
130 })
131 .collect();
132 entries.sort_by_key(|e| Reverse(e.file.last_modified));
133 self.results.extend(
134 entries
135 .into_iter()
136 .take(100)
137 .map(|e| path_result(e, Vec::new())),
138 );
139 return;
140 }
141
142 let mut matcher = Matcher::new(nucleo::Config::DEFAULT);
143
144 for i in 0..snapshot.matched_item_count() {
145 if self.results.len() >= 100 {
146 break;
147 }
148 if let Some(item) = snapshot.get_matched_item(i) {
149 if let Some(ids) = &self.filter_ids {
150 if !ids.contains(&item.data.file.id) {
151 continue;
152 }
153 }
154
155 let mut indices = Vec::new();
156 self.nucleo.pattern.column_pattern(0).indices(
157 item.matcher_columns[0].slice(..),
158 &mut matcher,
159 &mut indices,
160 );
161
162 self.results.push(path_result(item.data, indices));
163 }
164 }
165 }
166
167 pub fn results(&self) -> &[SearchResult] {
169 &self.results
170 }
171}