Skip to main content

fresh/app/
scan_orchestrators.rs

1//! Line-scan and search-scan orchestrators on `Editor`.
2//!
3//! Drive the chunked-scan subsystems extracted in phase 2 (LineScan,
4//! SearchScan) one batch per render frame. Coordinate with
5//! self.buffers (to read leaves and apply scan results), the tokio
6//! runtime (for concurrent filesystem I/O), and the status message
7//! (for progress reporting).
8
9use rust_i18n::t;
10
11use crate::model::event::BufferId;
12use crate::view::prompt::PromptType;
13
14use super::Editor;
15
16impl Editor {
17    /// Start an incremental line-feed scan for the active buffer.
18    ///
19    /// Shared by the `Action::ScanLineIndex` command and the Go to Line scan
20    /// confirmation prompt. Seeds `LineScan` so that `process_line_scan`
21    /// will advance the scan one batch per frame.
22    ///
23    /// When `open_goto_line` is true (Go to Line flow), the Go to Line prompt
24    /// opens automatically when the scan completes.
25    pub fn start_incremental_line_scan(&mut self, open_goto_line: bool) {
26        let buffer_id = self.active_buffer();
27        if let Some(state) = self.buffers.get_mut(&buffer_id) {
28            let (chunks, total_bytes) = state.buffer.prepare_line_scan();
29            let leaves = state.buffer.piece_tree_leaves();
30            self.line_scan
31                .start(buffer_id, leaves, chunks, total_bytes, open_goto_line);
32            self.set_status_message(t!("goto.scanning_progress", percent = 0).to_string());
33        }
34    }
35
36    /// Process chunks for the incremental line-feed scan.
37    /// Returns `true` if the UI should re-render (progress updated or scan finished).
38    pub fn process_line_scan(&mut self) -> bool {
39        let _span = tracing::info_span!("process_line_scan").entered();
40
41        let Some(buffer_id) = self.line_scan.buffer_id() else {
42            return false;
43        };
44
45        if let Err(e) = self.process_line_scan_batch(buffer_id) {
46            tracing::warn!("Line scan error: {e}");
47            self.finish_line_scan_with_error(e);
48            return true;
49        }
50
51        if self.line_scan.is_done() {
52            self.finish_line_scan_ok();
53        } else {
54            let pct = self.line_scan.progress_percent();
55            self.set_status_message(t!("goto.scanning_progress", percent = pct).to_string());
56        }
57        true
58    }
59
60    /// Process leaves concurrently, yielding for a render after each batch.
61    ///
62    /// For loaded leaves, delegates to `TextBuffer::scan_leaf` (shared counting
63    /// logic). For unloaded leaves, extracts I/O parameters and runs them
64    /// concurrently using `tokio::task::spawn_blocking` — each task calls
65    /// `count_line_feeds_in_range` on the filesystem, which remote implementations
66    /// override to count on the server without transferring data.
67    fn process_line_scan_batch(&mut self, buffer_id: BufferId) -> std::io::Result<()> {
68        let _span = tracing::info_span!("process_line_scan_batch").entered();
69        let concurrency = self.config.editor.read_concurrency.max(1);
70
71        let state = self.buffers.get(&buffer_id);
72
73        let mut results: Vec<(usize, usize)> = Vec::new();
74        let mut io_work: Vec<(usize, std::path::PathBuf, u64, usize)> = Vec::new();
75
76        // Pull chunks up to the concurrency budget, skipping already-known
77        // leaves. The budget is in terms of actual work items, so we keep
78        // asking for more chunks until we fill it or run out.
79        'outer: while results.len() + io_work.len() < concurrency {
80            let batch = self
81                .line_scan
82                .take_next_chunks(concurrency - (results.len() + io_work.len()));
83            if batch.is_empty() {
84                break;
85            }
86
87            for chunk in batch {
88                if chunk.already_known {
89                    continue;
90                }
91
92                let Some(state) = state else {
93                    break 'outer;
94                };
95
96                let leaf = &self.line_scan.leaves()[chunk.leaf_index];
97
98                match state.buffer.leaf_io_params(leaf) {
99                    None => {
100                        // Loaded: count in-memory via scan_leaf
101                        let count = state.buffer.scan_leaf(leaf)?;
102                        results.push((chunk.leaf_index, count));
103                    }
104                    Some((path, offset, len)) => {
105                        // Unloaded: batch for concurrent I/O
106                        io_work.push((chunk.leaf_index, path, offset, len));
107                    }
108                }
109            }
110        }
111
112        // Run I/O concurrently using tokio::task::spawn_blocking
113        if !io_work.is_empty() {
114            let fs = match state {
115                Some(s) => s.buffer.filesystem().clone(),
116                None => return Ok(()),
117            };
118
119            let rt = self
120                .tokio_runtime
121                .as_ref()
122                .ok_or_else(|| std::io::Error::other("async runtime not available"))?;
123
124            let io_results: Vec<std::io::Result<(usize, usize)>> = rt.block_on(async {
125                let mut handles = Vec::with_capacity(io_work.len());
126                for (leaf_idx, path, offset, len) in io_work {
127                    let fs = fs.clone();
128                    handles.push(tokio::task::spawn_blocking(move || {
129                        let count = fs.count_line_feeds_in_range(&path, offset, len)?;
130                        Ok((leaf_idx, count))
131                    }));
132                }
133
134                let mut results = Vec::with_capacity(handles.len());
135                for handle in handles {
136                    results.push(handle.await.unwrap());
137                }
138                results
139            });
140
141            for result in io_results {
142                results.push(result?);
143            }
144        }
145
146        for (leaf_idx, count) in results {
147            self.line_scan.append_update(leaf_idx, count);
148        }
149
150        Ok(())
151    }
152
153    fn finish_line_scan_ok(&mut self) {
154        let _span = tracing::info_span!("finish_line_scan_ok").entered();
155        let Some(finished) = self.line_scan.take_finished() else {
156            return;
157        };
158        if let Some(state) = self.buffers.get_mut(&finished.buffer_id) {
159            let _span = tracing::info_span!(
160                "rebuild_with_pristine_saved_root",
161                updates = finished.updates.len()
162            )
163            .entered();
164            state
165                .buffer
166                .rebuild_with_pristine_saved_root(&finished.updates);
167        }
168        self.set_status_message(t!("goto.scan_complete").to_string());
169        if finished.open_goto_line {
170            self.open_goto_line_if_active(finished.buffer_id);
171        }
172    }
173
174    fn finish_line_scan_with_error(&mut self, e: std::io::Error) {
175        let Some(finished) = self.line_scan.take_finished() else {
176            return;
177        };
178        self.set_status_message(t!("goto.scan_failed", error = e.to_string()).to_string());
179        if finished.open_goto_line {
180            self.open_goto_line_if_active(finished.buffer_id);
181        }
182    }
183
184    fn open_goto_line_if_active(&mut self, buffer_id: BufferId) {
185        if self.active_buffer() == buffer_id {
186            self.start_prompt(
187                t!("file.goto_line_prompt").to_string(),
188                PromptType::GotoLine,
189            );
190        }
191    }
192
193    // === Incremental Search Scan (for large files) ===
194
195    /// Process chunks for the incremental search scan.
196    /// Returns `true` if the UI should re-render (progress updated or scan finished).
197    pub fn process_search_scan(&mut self) -> bool {
198        let Some(buffer_id) = self.search_scan.buffer_id() else {
199            return false;
200        };
201
202        if let Err(e) = self.process_search_scan_batch(buffer_id) {
203            tracing::warn!("Search scan error: {e}");
204            self.search_scan.abandon();
205            self.set_status_message(format!("Search failed: {e}"));
206            return true;
207        }
208
209        if self.search_scan.is_done() {
210            self.finish_search_scan();
211        } else {
212            let pct = self.search_scan.progress_percent();
213            let match_count = self.search_scan.match_count();
214            self.set_status_message(format!(
215                "Searching... {}% ({} matches so far)",
216                pct, match_count
217            ));
218        }
219        true
220    }
221
222    /// Process a batch of search chunks by delegating to
223    /// `TextBuffer::search_scan_next_chunk`.
224    fn process_search_scan_batch(
225        &mut self,
226        buffer_id: crate::model::event::BufferId,
227    ) -> std::io::Result<()> {
228        let concurrency = self.config.editor.read_concurrency.max(1);
229
230        for _ in 0..concurrency {
231            if self.search_scan.is_done() {
232                break;
233            }
234
235            // Extract the ChunkedSearchState, run one chunk on the buffer,
236            // then put it back. This is the same take/restore dance the
237            // previous `Option<SearchScanState>` code did, now wrapped in
238            // the subsystem's API so we're not poking its internals.
239            let Some(mut chunked) = self.search_scan.take_chunked() else {
240                return Ok(());
241            };
242            let result = if let Some(state) = self.buffers.get_mut(&buffer_id) {
243                state.buffer.search_scan_next_chunk(&mut chunked)
244            } else {
245                Ok(false)
246            };
247            self.search_scan.restore_chunked(chunked);
248
249            match result {
250                Ok(false) => break, // scan complete
251                Ok(true) => {}      // more chunks
252                Err(e) => return Err(e),
253            }
254        }
255
256        Ok(())
257    }
258
259    /// Finalize the incremental search scan: take the accumulated matches
260    /// and hand them to `finalize_search()` which sets search_state, moves
261    /// the cursor, and creates viewport overlays.
262    fn finish_search_scan(&mut self) {
263        let Some(finished) = self.search_scan.take_finished() else {
264            return;
265        };
266
267        // The search scan loaded chunks via chunk_split_and_load, which
268        // restructures the piece tree.  Refresh saved_root so that
269        // diff_since_saved() can take the fast Arc::ptr_eq path.
270        if let Some(state) = self.buffers.get_mut(&finished.buffer_id) {
271            state.buffer.refresh_saved_root_if_unmodified();
272        }
273
274        if finished.match_ranges.is_empty() {
275            self.search_state = None;
276            self.set_status_message(format!("No matches found for '{}'", finished.query));
277            return;
278        }
279
280        self.finalize_search(
281            &finished.query,
282            finished.match_ranges,
283            finished.capped,
284            None,
285        );
286    }
287}