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