1use anyhow::Result as AnyhowResult;
11
12use crate::model::event::BufferId;
13
14use super::Editor;
15
16impl Editor {
17 pub(crate) fn sync_lsp_after_recovery_replay(&mut self, buffer_id: BufferId) {
25 self.active_window_mut()
26 .sync_lsp_after_recovery_replay(buffer_id);
27 }
28
29 pub fn start_recovery_session(&mut self) -> AnyhowResult<()> {
31 Ok(self.recovery_service.lock().unwrap().start_session()?)
32 }
33
34 pub fn end_recovery_session(&mut self) -> AnyhowResult<()> {
36 let hot_exit = self.config.editor.hot_exit;
37
38 if hot_exit {
39 for (_, state) in self
42 .windows
43 .get_mut(&self.active_window)
44 .map(|w| &mut w.buffers)
45 .expect("active window present")
46 {
47 if state.buffer.is_modified() {
48 state.buffer.set_recovery_pending(true);
49 }
50 }
51 self.save_pending_recovery_buffers()?;
52
53 let preserve_ids = self.recovery_ids_to_preserve();
55 Ok(self
56 .recovery_service
57 .lock()
58 .unwrap()
59 .end_session_preserving(&preserve_ids)?)
60 } else {
61 Ok(self.recovery_service.lock().unwrap().end_session()?)
62 }
63 }
64
65 fn recovery_ids_to_preserve(&self) -> Vec<String> {
67 let hot_exit = self.config.editor.hot_exit;
68
69 self.active_window()
70 .buffer_metadata
71 .iter()
72 .filter_map(|(buffer_id, meta)| {
73 if meta.hidden_from_tabs || meta.is_virtual() {
74 return None;
75 }
76 if !hot_exit {
77 return None;
78 }
79 let state = self
80 .windows
81 .get(&self.active_window)
82 .map(|w| &w.buffers)
83 .expect("active window present")
84 .get(buffer_id)?;
85 if !state.buffer.is_modified() {
86 return None;
87 }
88 let path = meta.file_path()?;
89 let is_unnamed = path.as_os_str().is_empty();
90 if is_unnamed && state.buffer.total_bytes() == 0 {
91 return None;
92 }
93 meta.recovery_id.clone().or_else(|| {
95 let file_path = state.buffer.file_path().map(|p| p.to_path_buf());
96 Some(
97 self.recovery_service
98 .lock()
99 .unwrap()
100 .get_buffer_id(file_path.as_deref()),
101 )
102 })
103 })
104 .collect()
105 }
106
107 pub fn has_recovery_files(&self) -> AnyhowResult<bool> {
109 Ok(self
110 .recovery_service
111 .lock()
112 .unwrap()
113 .should_offer_recovery()?)
114 }
115
116 pub fn list_recoverable_files(
118 &self,
119 ) -> AnyhowResult<Vec<crate::services::recovery::RecoveryEntry>> {
120 Ok(self.recovery_service.lock().unwrap().list_recoverable()?)
121 }
122
123 pub fn recover_all_buffers(&mut self) -> AnyhowResult<usize> {
126 use crate::services::recovery::RecoveryResult;
127
128 let entries = self.recovery_service.lock().unwrap().list_recoverable()?;
129 let mut recovered_count = 0;
130
131 for entry in entries {
132 let accepted = self
133 .recovery_service
134 .lock()
135 .unwrap()
136 .accept_recovery(&entry);
137 match accepted {
138 Ok(RecoveryResult::Recovered {
139 original_path,
140 content,
141 }) => {
142 let text = String::from_utf8_lossy(&content).into_owned();
144
145 if let Some(path) = original_path {
146 match self.open_file(&path) {
148 Ok(buffer_id) => {
149 {
151 let state = self.active_state_mut();
152 let total = state.buffer.total_bytes();
153 state.buffer.delete(0..total);
154 state.buffer.insert(0, &text);
155 state.buffer.set_modified(true);
157 }
158 self.active_event_log_mut().clear_saved_position();
161 self.sync_lsp_after_recovery_replay(buffer_id);
165 recovered_count += 1;
166 tracing::info!("Recovered buffer: {}", path.display());
167 }
168 Err(e) => {
169 if let Some(confirmation) = e.downcast_ref::<
171 crate::model::buffer::LargeFileEncodingConfirmation,
172 >() {
173 self.start_large_file_encoding_confirmation(confirmation);
174 } else {
175 tracing::warn!("Failed to recover buffer {}: {}", path.display(), e);
176 }
177 }
178 }
179 } else {
180 let buffer_id = self.new_buffer();
182 {
183 let state = self.active_state_mut();
184 state.buffer.insert(0, &text);
185 state.buffer.set_modified(true);
186 }
187 self.active_event_log_mut().clear_saved_position();
190 self.sync_lsp_after_recovery_replay(buffer_id);
191 recovered_count += 1;
192 tracing::info!("Recovered unsaved buffer");
193 }
194 }
195 Ok(RecoveryResult::RecoveredChunks {
196 original_path,
197 chunks,
198 }) => {
199 if let Ok(buffer_id) = self.open_file(&original_path) {
201 {
202 let state = self.active_state_mut();
203
204 for chunk in chunks.into_iter().rev() {
207 let text = String::from_utf8_lossy(&chunk.content).into_owned();
208 if chunk.original_len > 0 {
209 state
210 .buffer
211 .delete(chunk.offset..chunk.offset + chunk.original_len);
212 }
213 state.buffer.insert(chunk.offset, &text);
214 }
215
216 state.buffer.set_modified(true);
218 }
219 self.active_event_log_mut().clear_saved_position();
222 self.sync_lsp_after_recovery_replay(buffer_id);
223 recovered_count += 1;
224 tracing::info!("Recovered buffer with chunks: {}", original_path.display());
225 }
226 }
227 Ok(RecoveryResult::OriginalFileModified { id, original_path }) => {
228 tracing::warn!(
229 "Recovery file {} skipped: original file {} was modified",
230 id,
231 original_path.display()
232 );
233 let name = original_path
236 .file_name()
237 .unwrap_or_default()
238 .to_string_lossy();
239 self.set_status_message(format!(
240 "{} changed on disk; unsaved changes not restored",
241 name
242 ));
243 }
244 Ok(RecoveryResult::Corrupted { id, reason }) => {
245 tracing::warn!("Recovery file {} corrupted: {}", id, reason);
246 }
247 Ok(RecoveryResult::NotFound { id }) => {
248 tracing::warn!("Recovery file {} not found", id);
249 }
250 Err(e) => {
251 tracing::warn!("Failed to recover {}: {}", entry.id, e);
252 }
253 }
254 }
255
256 Ok(recovered_count)
257 }
258
259 pub fn discard_all_recovery(&mut self) -> AnyhowResult<usize> {
262 Ok(self
263 .recovery_service
264 .lock()
265 .unwrap()
266 .discard_all_recovery()?)
267 }
268
269 pub fn try_restore_hot_exit_buffers(&mut self) -> AnyhowResult<usize> {
284 use crate::services::recovery::RecoveryResult;
285
286 if !self.config.editor.hot_exit {
287 return Ok(0);
288 }
289
290 let entries = self.recovery_service.lock().unwrap().list_recoverable()?;
291 if entries.is_empty() {
292 return Ok(0);
293 }
294
295 let mut restored = 0;
296 for entry in entries {
297 let loaded = self.recovery_service.lock().unwrap().load_recovery(&entry);
298 match loaded {
299 Ok(RecoveryResult::Recovered {
300 original_path,
301 content,
302 }) => {
303 let text = String::from_utf8_lossy(&content).into_owned();
304 if let Some(path) = original_path {
305 match self.open_file(&path) {
306 Ok(buffer_id) => {
307 {
308 let state = self.active_state_mut();
309 let total = state.buffer.total_bytes();
310 state.buffer.delete(0..total);
311 state.buffer.insert(0, &text);
312 state.buffer.set_modified(true);
313 state.buffer.set_recovery_pending(false);
314 }
315 self.active_event_log_mut().clear_saved_position();
316 self.sync_lsp_after_recovery_replay(buffer_id);
317 restored += 1;
318 tracing::info!(
319 "Hot-exit restore: reopened {} with unsaved changes",
320 path.display()
321 );
322 }
323 Err(e) => {
324 if let Some(confirmation) = e.downcast_ref::<
325 crate::model::buffer::LargeFileEncodingConfirmation,
326 >() {
327 self.start_large_file_encoding_confirmation(confirmation);
328 } else {
329 tracing::warn!(
330 "Hot-exit restore failed to open {}: {}",
331 path.display(),
332 e
333 );
334 }
335 }
336 }
337 } else {
338 let buffer_id = self.new_buffer();
342 {
343 let state = self.active_state_mut();
344 state.buffer.insert(0, &text);
345 state.buffer.set_modified(true);
346 state.buffer.set_recovery_pending(false);
347 }
348 self.active_event_log_mut().clear_saved_position();
349 if let Some(meta) =
350 self.active_window_mut().buffer_metadata.get_mut(&buffer_id)
351 {
352 meta.recovery_id = Some(entry.id.clone());
353 }
354 self.sync_lsp_after_recovery_replay(buffer_id);
355 restored += 1;
356 tracing::info!(
357 "Hot-exit restore: reopened unnamed buffer (recovery_id={})",
358 entry.id
359 );
360 }
361 }
362 Ok(RecoveryResult::RecoveredChunks {
363 original_path,
364 chunks,
365 }) => match self.open_file(&original_path) {
366 Ok(buffer_id) => {
367 {
368 let state = self.active_state_mut();
369 for chunk in chunks.into_iter().rev() {
370 let text = String::from_utf8_lossy(&chunk.content).into_owned();
371 if chunk.original_len > 0 {
372 state
373 .buffer
374 .delete(chunk.offset..chunk.offset + chunk.original_len);
375 }
376 state.buffer.insert(chunk.offset, &text);
377 }
378 state.buffer.set_modified(true);
379 state.buffer.set_recovery_pending(false);
380 }
381 self.active_event_log_mut().clear_saved_position();
382 self.sync_lsp_after_recovery_replay(buffer_id);
383 restored += 1;
384 tracing::info!(
385 "Hot-exit restore: reopened {} with chunked changes",
386 original_path.display()
387 );
388 }
389 Err(e) => {
390 tracing::warn!(
391 "Hot-exit restore failed to open {}: {}",
392 original_path.display(),
393 e
394 );
395 }
396 },
397 Ok(RecoveryResult::OriginalFileModified { id, original_path }) => {
398 tracing::warn!(
399 "Hot-exit restore skipped {}: original file {} changed on disk",
400 id,
401 original_path.display()
402 );
403 }
404 Ok(RecoveryResult::Corrupted { id, reason }) => {
405 tracing::warn!("Hot-exit restore skipped {}: corrupted ({})", id, reason);
406 }
407 Ok(RecoveryResult::NotFound { id }) => {
408 tracing::warn!("Hot-exit restore: recovery file {} missing", id);
409 }
410 Err(e) => {
411 tracing::warn!("Hot-exit restore: failed to load {}: {}", entry.id, e);
412 }
413 }
414 }
415
416 Ok(restored)
417 }
418
419 pub fn auto_recovery_save_dirty_buffers(&mut self) -> AnyhowResult<usize> {
422 if !self.recovery_service.lock().unwrap().is_enabled() {
423 return Ok(0);
424 }
425
426 let interval = std::time::Duration::from_secs(
427 self.config.editor.auto_recovery_save_interval_secs as u64,
428 );
429 if self
430 .time_source
431 .elapsed_since(self.active_window().last_auto_recovery_save)
432 < interval
433 {
434 return Ok(0);
435 }
436
437 let saved = self.save_pending_recovery_buffers()?;
438 self.active_window_mut().last_auto_recovery_save = self.time_source.now();
439 Ok(saved)
440 }
441
442 fn save_pending_recovery_buffers(&mut self) -> AnyhowResult<usize> {
445 if !self.recovery_service.lock().unwrap().is_enabled() {
446 return Ok(0);
447 }
448
449 let buffers_needing_recovery: Vec<_> = self
452 .buffers()
453 .iter()
454 .filter_map(|(buffer_id, state)| {
455 if state.is_composite_buffer {
456 return None;
457 }
458 if let Some(meta) = self.active_window().buffer_metadata.get(buffer_id) {
459 if meta.hidden_from_tabs || meta.is_virtual() {
460 return None;
461 }
462 }
463 if state.buffer.is_recovery_pending() {
464 Some(*buffer_id)
465 } else {
466 None
467 }
468 })
469 .collect();
470
471 for buffer_id in &buffers_needing_recovery {
473 let needs_id = self
474 .active_window()
475 .buffer_metadata
476 .get(buffer_id)
477 .map(|meta| {
478 let path = meta.file_path();
479 let is_unnamed = path.map(|p| p.as_os_str().is_empty()).unwrap_or(true);
480 is_unnamed && meta.recovery_id.is_none()
481 })
482 .unwrap_or(false);
483
484 if needs_id {
485 let new_id = crate::services::recovery::generate_buffer_id();
486 if let Some(meta) = self.active_window_mut().buffer_metadata.get_mut(buffer_id) {
487 meta.recovery_id = Some(new_id);
488 }
489 }
490 }
491
492 let buffer_info: Vec<_> = buffers_needing_recovery
494 .into_iter()
495 .filter_map(|buffer_id| {
496 let state = self
497 .windows
498 .get(&self.active_window)
499 .map(|w| &w.buffers)
500 .expect("active window present")
501 .get(&buffer_id)?;
502 let meta = self.active_window().buffer_metadata.get(&buffer_id)?;
503 let path = state.buffer.file_path().map(|p| p.to_path_buf());
504 let recovery_id = if let Some(ref stored_id) = meta.recovery_id {
505 stored_id.clone()
506 } else {
507 self.recovery_service
508 .lock()
509 .unwrap()
510 .get_buffer_id(path.as_deref())
511 };
512 let recovery_pending = state.buffer.is_recovery_pending();
513 if self
514 .recovery_service
515 .lock()
516 .unwrap()
517 .needs_auto_recovery_save(&recovery_id, recovery_pending)
518 {
519 Some((buffer_id, recovery_id, path))
520 } else {
521 None
522 }
523 })
524 .collect();
525
526 let mut saved_count = 0;
527 for (buffer_id, recovery_id, path) in buffer_info {
528 if self.save_buffer_to_recovery(&buffer_id, &recovery_id, path.as_deref())? {
529 saved_count += 1;
530 }
531 }
532 Ok(saved_count)
533 }
534
535 pub fn is_active_buffer_recovery_dirty(&self) -> bool {
538 if let Some(state) = self
539 .windows
540 .get(&self.active_window)
541 .map(|w| &w.buffers)
542 .expect("active window present")
543 .get(&self.active_buffer())
544 {
545 state.buffer.is_recovery_pending()
546 } else {
547 false
548 }
549 }
550
551 pub fn delete_buffer_recovery(&mut self, buffer_id: BufferId) -> AnyhowResult<()> {
553 let recovery_id = {
555 let meta = self.active_window().buffer_metadata.get(&buffer_id);
556 let state = self
557 .windows
558 .get(&self.active_window)
559 .map(|w| &w.buffers)
560 .expect("active window present")
561 .get(&buffer_id);
562
563 if let Some(stored_id) = meta.and_then(|m| m.recovery_id.clone()) {
564 stored_id
565 } else if let Some(state) = state {
566 let path = state.buffer.file_path().map(|p| p.to_path_buf());
567 self.recovery_service
568 .lock()
569 .unwrap()
570 .get_buffer_id(path.as_deref())
571 } else {
572 return Ok(());
573 }
574 };
575
576 self.recovery_service
577 .lock()
578 .unwrap()
579 .delete_buffer_recovery(&recovery_id)?;
580
581 if let Some(state) = self
583 .windows
584 .get_mut(&self.active_window)
585 .map(|w| &mut w.buffers)
586 .expect("active window present")
587 .get_mut(&buffer_id)
588 {
589 state.buffer.set_recovery_pending(false);
590 }
591 Ok(())
592 }
593
594 fn save_buffer_to_recovery(
600 &mut self,
601 buffer_id: &BufferId,
602 recovery_id: &str,
603 path: Option<&std::path::Path>,
604 ) -> AnyhowResult<bool> {
605 let state = match self
606 .windows
607 .get_mut(&self.active_window)
608 .map(|w| &mut w.buffers)
609 .expect("active window present")
610 .get_mut(buffer_id)
611 {
612 Some(s) => s,
613 None => return Ok(false),
614 };
615 let line_count = state.buffer.line_count();
616
617 if state.buffer.is_large_file() {
618 let chunks = state.buffer.get_recovery_chunks();
619 if chunks.is_empty() {
620 state.buffer.set_recovery_pending(false);
621 return Ok(false);
622 }
623 let recovery_chunks: Vec<_> = chunks
624 .into_iter()
625 .map(|(offset, content)| {
626 crate::services::recovery::types::RecoveryChunk::new(offset, 0, content)
627 })
628 .collect();
629 let original_size = state.buffer.original_file_size().unwrap_or(0);
630 let final_size = state.buffer.total_bytes();
631 self.recovery_service.lock().unwrap().save_buffer(
632 recovery_id,
633 recovery_chunks,
634 path,
635 None,
636 line_count,
637 original_size,
638 final_size,
639 )?;
640 } else {
641 let total_bytes = state.buffer.total_bytes();
642 let content = match state.buffer.get_text_range_mut(0, total_bytes) {
643 Ok(bytes) => bytes,
644 Err(e) => {
645 tracing::warn!("Failed to get buffer content for recovery save: {}", e);
646 return Ok(false);
647 }
648 };
649 let chunks = vec![crate::services::recovery::types::RecoveryChunk::new(
650 0, 0, content,
651 )];
652 self.recovery_service.lock().unwrap().save_buffer(
653 recovery_id,
654 chunks,
655 path,
656 None,
657 line_count,
658 0,
659 total_bytes,
660 )?;
661 }
662
663 state.buffer.set_recovery_pending(false);
664 Ok(true)
665 }
666}