fresh/app/
recovery_actions.rs1use anyhow::Result as AnyhowResult;
11
12use crate::model::event::BufferId;
13
14use super::Editor;
15
16impl Editor {
17 pub fn start_recovery_session(&mut self) -> AnyhowResult<()> {
19 Ok(self.recovery_service.start_session()?)
20 }
21
22 pub fn end_recovery_session(&mut self) -> AnyhowResult<()> {
24 let hot_exit = self.config.editor.hot_exit;
25
26 if hot_exit {
27 for (_, state) in self.buffers.iter_mut() {
30 if state.buffer.is_modified() {
31 state.buffer.set_recovery_pending(true);
32 }
33 }
34 self.save_pending_recovery_buffers()?;
35
36 let preserve_ids = self.recovery_ids_to_preserve();
38 Ok(self
39 .recovery_service
40 .end_session_preserving(&preserve_ids)?)
41 } else {
42 Ok(self.recovery_service.end_session()?)
43 }
44 }
45
46 fn recovery_ids_to_preserve(&self) -> Vec<String> {
48 let hot_exit = self.config.editor.hot_exit;
49
50 self.buffer_metadata
51 .iter()
52 .filter_map(|(buffer_id, meta)| {
53 if meta.hidden_from_tabs || meta.is_virtual() {
54 return None;
55 }
56 if !hot_exit {
57 return None;
58 }
59 let state = self.buffers.get(buffer_id)?;
60 if !state.buffer.is_modified() {
61 return None;
62 }
63 let path = meta.file_path()?;
64 let is_unnamed = path.as_os_str().is_empty();
65 if is_unnamed && state.buffer.total_bytes() == 0 {
66 return None;
67 }
68 meta.recovery_id.clone().or_else(|| {
70 let file_path = state.buffer.file_path().map(|p| p.to_path_buf());
71 Some(self.recovery_service.get_buffer_id(file_path.as_deref()))
72 })
73 })
74 .collect()
75 }
76
77 pub fn has_recovery_files(&self) -> AnyhowResult<bool> {
79 Ok(self.recovery_service.should_offer_recovery()?)
80 }
81
82 pub fn list_recoverable_files(
84 &self,
85 ) -> AnyhowResult<Vec<crate::services::recovery::RecoveryEntry>> {
86 Ok(self.recovery_service.list_recoverable()?)
87 }
88
89 pub fn recover_all_buffers(&mut self) -> AnyhowResult<usize> {
92 use crate::services::recovery::RecoveryResult;
93
94 let entries = self.recovery_service.list_recoverable()?;
95 let mut recovered_count = 0;
96
97 for entry in entries {
98 match self.recovery_service.accept_recovery(&entry) {
99 Ok(RecoveryResult::Recovered {
100 original_path,
101 content,
102 }) => {
103 let text = String::from_utf8_lossy(&content).into_owned();
105
106 if let Some(path) = original_path {
107 match self.open_file(&path) {
109 Ok(_) => {
110 {
112 let state = self.active_state_mut();
113 let total = state.buffer.total_bytes();
114 state.buffer.delete(0..total);
115 state.buffer.insert(0, &text);
116 state.buffer.set_modified(true);
118 }
119 self.active_event_log_mut().clear_saved_position();
122 recovered_count += 1;
123 tracing::info!("Recovered buffer: {}", path.display());
124 }
125 Err(e) => {
126 if let Some(confirmation) = e.downcast_ref::<
128 crate::model::buffer::LargeFileEncodingConfirmation,
129 >() {
130 self.start_large_file_encoding_confirmation(confirmation);
131 } else {
132 tracing::warn!("Failed to recover buffer {}: {}", path.display(), e);
133 }
134 }
135 }
136 } else {
137 self.new_buffer();
139 {
140 let state = self.active_state_mut();
141 state.buffer.insert(0, &text);
142 state.buffer.set_modified(true);
143 }
144 self.active_event_log_mut().clear_saved_position();
147 recovered_count += 1;
148 tracing::info!("Recovered unsaved buffer");
149 }
150 }
151 Ok(RecoveryResult::RecoveredChunks {
152 original_path,
153 chunks,
154 }) => {
155 if self.open_file(&original_path).is_ok() {
157 {
158 let state = self.active_state_mut();
159
160 for chunk in chunks.into_iter().rev() {
163 let text = String::from_utf8_lossy(&chunk.content).into_owned();
164 if chunk.original_len > 0 {
165 state
166 .buffer
167 .delete(chunk.offset..chunk.offset + chunk.original_len);
168 }
169 state.buffer.insert(chunk.offset, &text);
170 }
171
172 state.buffer.set_modified(true);
174 }
175 self.active_event_log_mut().clear_saved_position();
178 recovered_count += 1;
179 tracing::info!("Recovered buffer with chunks: {}", original_path.display());
180 }
181 }
182 Ok(RecoveryResult::OriginalFileModified { id, original_path }) => {
183 tracing::warn!(
184 "Recovery file {} skipped: original file {} was modified",
185 id,
186 original_path.display()
187 );
188 let name = original_path
191 .file_name()
192 .unwrap_or_default()
193 .to_string_lossy();
194 self.set_status_message(format!(
195 "{} changed on disk; unsaved changes not restored",
196 name
197 ));
198 }
199 Ok(RecoveryResult::Corrupted { id, reason }) => {
200 tracing::warn!("Recovery file {} corrupted: {}", id, reason);
201 }
202 Ok(RecoveryResult::NotFound { id }) => {
203 tracing::warn!("Recovery file {} not found", id);
204 }
205 Err(e) => {
206 tracing::warn!("Failed to recover {}: {}", entry.id, e);
207 }
208 }
209 }
210
211 Ok(recovered_count)
212 }
213
214 pub fn discard_all_recovery(&mut self) -> AnyhowResult<usize> {
217 Ok(self.recovery_service.discard_all_recovery()?)
218 }
219
220 pub fn auto_recovery_save_dirty_buffers(&mut self) -> AnyhowResult<usize> {
223 if !self.recovery_service.is_enabled() {
224 return Ok(0);
225 }
226
227 let interval = std::time::Duration::from_secs(
228 self.config.editor.auto_recovery_save_interval_secs as u64,
229 );
230 if self.time_source.elapsed_since(self.last_auto_recovery_save) < interval {
231 return Ok(0);
232 }
233
234 let saved = self.save_pending_recovery_buffers()?;
235 self.last_auto_recovery_save = self.time_source.now();
236 Ok(saved)
237 }
238
239 fn save_pending_recovery_buffers(&mut self) -> AnyhowResult<usize> {
242 if !self.recovery_service.is_enabled() {
243 return Ok(0);
244 }
245
246 let buffers_needing_recovery: Vec<_> = self
249 .buffers
250 .iter()
251 .filter_map(|(buffer_id, state)| {
252 if state.is_composite_buffer {
253 return None;
254 }
255 if let Some(meta) = self.buffer_metadata.get(buffer_id) {
256 if meta.hidden_from_tabs || meta.is_virtual() {
257 return None;
258 }
259 }
260 if state.buffer.is_recovery_pending() {
261 Some(*buffer_id)
262 } else {
263 None
264 }
265 })
266 .collect();
267
268 for buffer_id in &buffers_needing_recovery {
270 let needs_id = self
271 .buffer_metadata
272 .get(buffer_id)
273 .map(|meta| {
274 let path = meta.file_path();
275 let is_unnamed = path.map(|p| p.as_os_str().is_empty()).unwrap_or(true);
276 is_unnamed && meta.recovery_id.is_none()
277 })
278 .unwrap_or(false);
279
280 if needs_id {
281 let new_id = crate::services::recovery::generate_buffer_id();
282 if let Some(meta) = self.buffer_metadata.get_mut(buffer_id) {
283 meta.recovery_id = Some(new_id);
284 }
285 }
286 }
287
288 let buffer_info: Vec<_> = buffers_needing_recovery
290 .into_iter()
291 .filter_map(|buffer_id| {
292 let state = self.buffers.get(&buffer_id)?;
293 let meta = self.buffer_metadata.get(&buffer_id)?;
294 let path = state.buffer.file_path().map(|p| p.to_path_buf());
295 let recovery_id = if let Some(ref stored_id) = meta.recovery_id {
296 stored_id.clone()
297 } else {
298 self.recovery_service.get_buffer_id(path.as_deref())
299 };
300 let recovery_pending = state.buffer.is_recovery_pending();
301 if self
302 .recovery_service
303 .needs_auto_recovery_save(&recovery_id, recovery_pending)
304 {
305 Some((buffer_id, recovery_id, path))
306 } else {
307 None
308 }
309 })
310 .collect();
311
312 let mut saved_count = 0;
313 for (buffer_id, recovery_id, path) in buffer_info {
314 if self.save_buffer_to_recovery(&buffer_id, &recovery_id, path.as_deref())? {
315 saved_count += 1;
316 }
317 }
318 Ok(saved_count)
319 }
320
321 pub fn is_active_buffer_recovery_dirty(&self) -> bool {
324 if let Some(state) = self.buffers.get(&self.active_buffer()) {
325 state.buffer.is_recovery_pending()
326 } else {
327 false
328 }
329 }
330
331 pub fn delete_buffer_recovery(&mut self, buffer_id: BufferId) -> AnyhowResult<()> {
333 let recovery_id = {
335 let meta = self.buffer_metadata.get(&buffer_id);
336 let state = self.buffers.get(&buffer_id);
337
338 if let Some(stored_id) = meta.and_then(|m| m.recovery_id.clone()) {
339 stored_id
340 } else if let Some(state) = state {
341 let path = state.buffer.file_path().map(|p| p.to_path_buf());
342 self.recovery_service.get_buffer_id(path.as_deref())
343 } else {
344 return Ok(());
345 }
346 };
347
348 self.recovery_service.delete_buffer_recovery(&recovery_id)?;
349
350 if let Some(state) = self.buffers.get_mut(&buffer_id) {
352 state.buffer.set_recovery_pending(false);
353 }
354 Ok(())
355 }
356
357 fn save_buffer_to_recovery(
363 &mut self,
364 buffer_id: &BufferId,
365 recovery_id: &str,
366 path: Option<&std::path::Path>,
367 ) -> AnyhowResult<bool> {
368 let state = match self.buffers.get_mut(buffer_id) {
369 Some(s) => s,
370 None => return Ok(false),
371 };
372 let line_count = state.buffer.line_count();
373
374 if state.buffer.is_large_file() {
375 let chunks = state.buffer.get_recovery_chunks();
376 if chunks.is_empty() {
377 state.buffer.set_recovery_pending(false);
378 return Ok(false);
379 }
380 let recovery_chunks: Vec<_> = chunks
381 .into_iter()
382 .map(|(offset, content)| {
383 crate::services::recovery::types::RecoveryChunk::new(offset, 0, content)
384 })
385 .collect();
386 let original_size = state.buffer.original_file_size().unwrap_or(0);
387 let final_size = state.buffer.total_bytes();
388 self.recovery_service.save_buffer(
389 recovery_id,
390 recovery_chunks,
391 path,
392 None,
393 line_count,
394 original_size,
395 final_size,
396 )?;
397 } else {
398 let total_bytes = state.buffer.total_bytes();
399 let content = match state.buffer.get_text_range_mut(0, total_bytes) {
400 Ok(bytes) => bytes,
401 Err(e) => {
402 tracing::warn!("Failed to get buffer content for recovery save: {}", e);
403 return Ok(false);
404 }
405 };
406 let chunks = vec![crate::services::recovery::types::RecoveryChunk::new(
407 0, 0, content,
408 )];
409 self.recovery_service.save_buffer(
410 recovery_id,
411 chunks,
412 path,
413 None,
414 line_count,
415 0,
416 total_bytes,
417 )?;
418 }
419
420 state.buffer.set_recovery_pending(false);
421 Ok(true)
422 }
423}