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