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 paths = lb.list_paths_with_ids(None).await.unwrap_or_default();
57 let by_id: HashMap<Uuid, File> = files.into_iter().map(|f| (f.id, f)).collect();
58 Self::from_files(
59 paths
60 .into_iter()
61 .filter_map(|(id, path)| by_id.get(&id).cloned().map(|file| (file, path))),
62 )
63 }
64
65 pub fn from_files(entries: impl IntoIterator<Item = (File, String)>) -> Self {
69 let notify = Arc::new(|| {});
70 let nucleo: Nucleo<PathEntry> = Nucleo::new(nucleo::Config::DEFAULT, notify, None, 1);
71 let injector = nucleo.injector();
72
73 let mut files = Vec::new();
74 let mut path_to_id = HashMap::new();
75
76 for (file, path) in entries {
77 if path == "/" {
78 files.push(file);
79 continue;
80 }
81 let (parent_path, filename) = split_path(&path);
82 injector.push(
83 PathEntry {
84 file: file.clone(),
85 path: path.clone(),
86 filename: filename.to_string(),
87 parent_path: parent_path.to_string(),
88 },
89 |entry, cols| {
90 cols[0] = entry.path.as_str().into();
91 },
92 );
93 path_to_id.insert(path, file.id);
94 files.push(file);
95 }
96
97 let descendants = build_descendants(&files);
98
99 let mut searcher = Self {
100 nucleo,
101 results: Vec::new(),
102 submitted_query: String::new(),
103 descendants,
104 path_to_id,
105 filter_ids: None,
106 };
107 searcher.query("");
108 searcher
109 }
110
111 pub fn update_filter(&mut self, filter: Option<SearchFilter>) {
113 self.filter_ids = super::resolve_filter(filter, &self.path_to_id, &self.descendants);
114 self.rebuild();
115 }
116
117 pub fn query(&mut self, input: &str) {
119 self.nucleo.pattern.reparse(
120 0,
121 input,
122 CaseMatching::Smart,
123 Normalization::Smart,
124 input.starts_with(&self.submitted_query),
125 );
126 self.submitted_query = input.to_string();
127
128 while self.nucleo.tick(10).running {}
129
130 self.rebuild();
131 }
132
133 fn rebuild(&mut self) {
136 self.results.clear();
137 let snapshot = self.nucleo.snapshot();
138
139 if self.submitted_query.is_empty() {
140 let mut entries: Vec<&PathEntry> = (0..snapshot.matched_item_count())
141 .filter_map(|i| snapshot.get_matched_item(i).map(|item| item.data))
142 .filter(|e| {
143 self.filter_ids
144 .as_ref()
145 .is_none_or(|ids| ids.contains(&e.file.id))
146 })
147 .collect();
148 entries.sort_by_key(|e| Reverse(e.file.last_modified));
149 self.results.extend(
150 entries
151 .into_iter()
152 .take(100)
153 .map(|e| path_result(e, Vec::new())),
154 );
155 return;
156 }
157
158 let mut matcher = Matcher::new(nucleo::Config::DEFAULT);
159
160 for i in 0..snapshot.matched_item_count() {
161 if self.results.len() >= 100 {
162 break;
163 }
164 if let Some(item) = snapshot.get_matched_item(i) {
165 if let Some(ids) = &self.filter_ids {
166 if !ids.contains(&item.data.file.id) {
167 continue;
168 }
169 }
170
171 let mut indices = Vec::new();
172 self.nucleo.pattern.column_pattern(0).indices(
173 item.matcher_columns[0].slice(..),
174 &mut matcher,
175 &mut indices,
176 );
177
178 self.results.push(path_result(item.data, indices));
179 }
180 }
181 }
182
183 pub fn results(&self) -> &[SearchResult] {
185 &self.results
186 }
187}