Skip to main content

lb_rs/search/
path.rs

1use super::SearchResult;
2use crate::Lb;
3use crate::model::file::File;
4use nucleo::{
5    Matcher, Nucleo,
6    pattern::{CaseMatching, Normalization},
7};
8use std::sync::Arc;
9
10/// Split a path into (parent_path, filename).
11pub(crate) fn split_path(path: &str) -> (&str, &str) {
12    // Strip trailing slash for folders
13    let path = path.trim_end_matches('/');
14    match path.rfind('/') {
15        Some(idx) if idx > 0 => (&path[..idx], &path[idx + 1..]),
16        Some(_) => ("/", &path[1..]), // root case: "/filename"
17        None => ("", path),
18    }
19}
20
21#[derive(Clone)]
22struct PathEntry {
23    file: File,
24    path: String,
25    filename: String,
26    parent_path: String,
27}
28
29pub struct PathSearcher {
30    nucleo: Nucleo<PathEntry>,
31    results: Vec<SearchResult>,
32    submitted_query: String,
33}
34
35impl PathSearcher {
36    pub async fn new(lb: &Lb) -> Self {
37        let files = lb.list_metadatas().await.unwrap_or_default();
38        let mut paths = lb.list_paths_with_ids(None).await.unwrap_or_default();
39        paths.retain(|(_, path)| path != "/");
40
41        let notify = Arc::new(|| {});
42        let nucleo: Nucleo<PathEntry> = Nucleo::new(nucleo::Config::DEFAULT, notify, None, 1);
43        let injector = nucleo.injector();
44
45        for (id, path) in &paths {
46            if let Some(file) = files.iter().find(|f| f.id == *id) {
47                let (parent_path, filename) = split_path(path);
48                injector.push(
49                    PathEntry {
50                        file: file.clone(),
51                        path: path.clone(),
52                        filename: filename.to_string(),
53                        parent_path: parent_path.to_string(),
54                    },
55                    |entry, cols| {
56                        cols[0] = entry.path.as_str().into();
57                    },
58                );
59            }
60        }
61
62        Self { nucleo, results: Vec::new(), submitted_query: String::new() }
63    }
64
65    /// Update the search query. Results available via `results()`.
66    pub fn query(&mut self, input: &str) {
67        self.nucleo.pattern.reparse(
68            0,
69            input,
70            CaseMatching::Smart,
71            Normalization::Smart,
72            input.starts_with(&self.submitted_query),
73        );
74        self.submitted_query = input.to_string();
75
76        while self.nucleo.tick(10).running {}
77
78        // Build results
79        self.results.clear();
80        let snapshot = self.nucleo.snapshot();
81        let count = snapshot.matched_item_count().min(100);
82        let mut matcher = Matcher::new(nucleo::Config::DEFAULT);
83
84        for i in 0..count {
85            if let Some(item) = snapshot.get_matched_item(i) {
86                let mut indices = Vec::new();
87                self.nucleo.pattern.column_pattern(0).indices(
88                    item.matcher_columns[0].slice(..),
89                    &mut matcher,
90                    &mut indices,
91                );
92
93                self.results.push(SearchResult {
94                    id: item.data.file.id,
95                    filename: item.data.filename.clone(),
96                    parent_path: item.data.parent_path.clone(),
97                    path_indices: indices,
98                    path_matches: Vec::new(),
99                    content_matches: Vec::new(),
100                });
101            }
102        }
103    }
104
105    /// Get search results.
106    pub fn results(&self) -> &[SearchResult] {
107        &self.results
108    }
109}