Skip to main content

lb_rs/search/
path.rs

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
13/// Split a path into (parent_path, filename).
14pub(crate) fn split_path(path: &str) -> (&str, &str) {
15    // Strip trailing slash for folders
16    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..]), // root case: "/filename"
20        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    /// Update the active filter and refresh results for the current query.
96    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    /// Update the search query. Results available via `results()`.
102    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    /// Rebuild `results` from the current query and filter without re-running the
118    /// matcher. Shared by `query` (after a reparse) and `update_filter`.
119    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    /// Get search results.
168    pub fn results(&self) -> &[SearchResult] {
169        &self.results
170    }
171}