Skip to main content

ass_editor/utils/indexing/
fst_search.rs

1//! [`DocumentSearch`] implementation for [`FstSearchIndex`].
2
3use super::common::calculate_hash;
4use super::fst::FstSearchIndex;
5use crate::core::{EditorDocument, Position, Range, Result};
6use crate::utils::search::{DocumentSearch, SearchOptions, SearchResult, SearchStats};
7
8use fst::{automaton, IntoStreamer, Streamer};
9
10use std::time::Instant;
11
12#[cfg(feature = "search-index")]
13impl DocumentSearch for FstSearchIndex {
14    fn build_index(&mut self, document: &EditorDocument) -> Result<()> {
15        let content = document.text();
16        self.content_hash = calculate_hash(&content);
17        self.build_time = {
18            #[cfg(feature = "std")]
19            {
20                Instant::now()
21            }
22            #[cfg(not(feature = "std"))]
23            {
24                0
25            }
26        };
27
28        let words = self.extract_words(&content);
29        self.build_fst(words)?;
30
31        Ok(())
32    }
33
34    fn update_index(&mut self, document: &EditorDocument, _changes: &[Range]) -> Result<()> {
35        // For simplicity, rebuild entire index on changes
36        // In production, this could be optimized for incremental updates
37        self.build_index(document)
38    }
39
40    fn search<'a>(
41        &'a self,
42        pattern: &str,
43        options: &SearchOptions,
44    ) -> Result<Vec<SearchResult<'a>>> {
45        #[cfg(feature = "std")]
46        let _start_time = Instant::now();
47        let mut results = Vec::new();
48
49        if let Some(ref fst_set) = self.fst_set {
50            let query = if options.case_sensitive {
51                pattern.to_string()
52            } else {
53                pattern.to_lowercase()
54            };
55
56            // For simplicity, use subsequence automaton for all searches
57            // In production, this could be optimized with different automaton types
58            let automaton = automaton::Subsequence::new(&query);
59            let mut stream = fst_set.search(automaton).into_stream();
60            let mut count = 0;
61
62            while let Some(key) = stream.next() {
63                if options.max_results > 0 && count >= options.max_results {
64                    break;
65                }
66
67                let key_str = String::from_utf8_lossy(key);
68                if let Some(entries) = self.position_map.get(key_str.as_ref()) {
69                    for entry in entries {
70                        // Apply scope filtering
71                        if self.matches_scope(entry, &options.scope) {
72                            results.push(SearchResult {
73                                start: entry.position,
74                                end: Position::new(entry.position.offset + pattern.len()),
75                                text: std::borrow::Cow::Owned(pattern.to_string()),
76                                context: std::borrow::Cow::Owned(entry.context.clone()),
77                                line: entry.line,
78                                column: entry.column,
79                            });
80                            count += 1;
81
82                            if options.max_results > 0 && count >= options.max_results {
83                                break;
84                            }
85                        }
86                    }
87                }
88            }
89        }
90
91        Ok(results)
92    }
93
94    fn find_replace<'a>(
95        &'a self,
96        document: &mut EditorDocument,
97        pattern: &str,
98        replacement: &str,
99        options: &SearchOptions,
100    ) -> Result<Vec<SearchResult<'a>>> {
101        let results = self.search(pattern, options)?;
102
103        // Apply replacements in reverse order to maintain position validity
104        let mut sorted_results = results.clone();
105        sorted_results.sort_by_key(|r| core::cmp::Reverse(r.start.offset));
106
107        for result in &sorted_results {
108            let range = Range::new(result.start, result.end);
109            document.replace(range, replacement)?;
110        }
111
112        Ok(results)
113    }
114
115    fn stats(&self) -> SearchStats {
116        SearchStats {
117            match_count: self.position_map.len(),
118            search_time_us: {
119                #[cfg(feature = "std")]
120                {
121                    self.build_time.elapsed().as_micros() as u64
122                }
123                #[cfg(not(feature = "std"))]
124                {
125                    0
126                }
127            },
128            hit_limit: false,
129            index_size: self.index_size,
130        }
131    }
132
133    fn clear_index(&mut self) {
134        self.fst_set = None;
135        self.position_map.clear();
136        self.index_size = 0;
137    }
138}