fresh/app/
scan_orchestrators.rs1use rust_i18n::t;
10
11use crate::model::event::BufferId;
12use crate::view::prompt::PromptType;
13
14use super::Editor;
15
16impl Editor {
17 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 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 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 '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 let count = state.buffer.scan_leaf(leaf)?;
102 results.push((chunk.leaf_index, count));
103 }
104 Some((path, offset, len)) => {
105 io_work.push((chunk.leaf_index, path, offset, len));
107 }
108 }
109 }
110 }
111
112 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 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 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 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, Ok(true) => {} Err(e) => return Err(e),
253 }
254 }
255
256 Ok(())
257 }
258
259 fn finish_search_scan(&mut self) {
263 let Some(finished) = self.search_scan.take_finished() else {
264 return;
265 };
266
267 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}