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.send_lsp_changes_for_buffer(buffer_id, vec![full_change]);
38 }
39
40 pub fn start_recovery_session(&mut self) -> AnyhowResult<()> {
42 Ok(self.recovery_service.start_session()?)
43 }
44
45 pub fn end_recovery_session(&mut self) -> AnyhowResult<()> {
47 let hot_exit = self.config.editor.hot_exit;
48
49 if hot_exit {
50 for (_, state) in self.buffers.iter_mut() {
53 if state.buffer.is_modified() {
54 state.buffer.set_recovery_pending(true);
55 }
56 }
57 self.save_pending_recovery_buffers()?;
58
59 let preserve_ids = self.recovery_ids_to_preserve();
61 Ok(self
62 .recovery_service
63 .end_session_preserving(&preserve_ids)?)
64 } else {
65 Ok(self.recovery_service.end_session()?)
66 }
67 }
68
69 fn recovery_ids_to_preserve(&self) -> Vec<String> {
71 let hot_exit = self.config.editor.hot_exit;
72
73 self.buffer_metadata
74 .iter()
75 .filter_map(|(buffer_id, meta)| {
76 if meta.hidden_from_tabs || meta.is_virtual() {
77 return None;
78 }
79 if !hot_exit {
80 return None;
81 }
82 let state = self.buffers.get(buffer_id)?;
83 if !state.buffer.is_modified() {
84 return None;
85 }
86 let path = meta.file_path()?;
87 let is_unnamed = path.as_os_str().is_empty();
88 if is_unnamed && state.buffer.total_bytes() == 0 {
89 return None;
90 }
91 meta.recovery_id.clone().or_else(|| {
93 let file_path = state.buffer.file_path().map(|p| p.to_path_buf());
94 Some(self.recovery_service.get_buffer_id(file_path.as_deref()))
95 })
96 })
97 .collect()
98 }
99
100 pub fn has_recovery_files(&self) -> AnyhowResult<bool> {
102 Ok(self.recovery_service.should_offer_recovery()?)
103 }
104
105 pub fn list_recoverable_files(
107 &self,
108 ) -> AnyhowResult<Vec<crate::services::recovery::RecoveryEntry>> {
109 Ok(self.recovery_service.list_recoverable()?)
110 }
111
112 pub fn recover_all_buffers(&mut self) -> AnyhowResult<usize> {
115 use crate::services::recovery::RecoveryResult;
116
117 let entries = self.recovery_service.list_recoverable()?;
118 let mut recovered_count = 0;
119
120 for entry in entries {
121 match self.recovery_service.accept_recovery(&entry) {
122 Ok(RecoveryResult::Recovered {
123 original_path,
124 content,
125 }) => {
126 let text = String::from_utf8_lossy(&content).into_owned();
128
129 if let Some(path) = original_path {
130 match self.open_file(&path) {
132 Ok(buffer_id) => {
133 {
135 let state = self.active_state_mut();
136 let total = state.buffer.total_bytes();
137 state.buffer.delete(0..total);
138 state.buffer.insert(0, &text);
139 state.buffer.set_modified(true);
141 }
142 self.active_event_log_mut().clear_saved_position();
145 self.sync_lsp_after_recovery_replay(buffer_id);
149 recovered_count += 1;
150 tracing::info!("Recovered buffer: {}", path.display());
151 }
152 Err(e) => {
153 if let Some(confirmation) = e.downcast_ref::<
155 crate::model::buffer::LargeFileEncodingConfirmation,
156 >() {
157 self.start_large_file_encoding_confirmation(confirmation);
158 } else {
159 tracing::warn!("Failed to recover buffer {}: {}", path.display(), e);
160 }
161 }
162 }
163 } else {
164 let buffer_id = self.new_buffer();
166 {
167 let state = self.active_state_mut();
168 state.buffer.insert(0, &text);
169 state.buffer.set_modified(true);
170 }
171 self.active_event_log_mut().clear_saved_position();
174 self.sync_lsp_after_recovery_replay(buffer_id);
175 recovered_count += 1;
176 tracing::info!("Recovered unsaved buffer");
177 }
178 }
179 Ok(RecoveryResult::RecoveredChunks {
180 original_path,
181 chunks,
182 }) => {
183 if let Ok(buffer_id) = self.open_file(&original_path) {
185 {
186 let state = self.active_state_mut();
187
188 for chunk in chunks.into_iter().rev() {
191 let text = String::from_utf8_lossy(&chunk.content).into_owned();
192 if chunk.original_len > 0 {
193 state
194 .buffer
195 .delete(chunk.offset..chunk.offset + chunk.original_len);
196 }
197 state.buffer.insert(chunk.offset, &text);
198 }
199
200 state.buffer.set_modified(true);
202 }
203 self.active_event_log_mut().clear_saved_position();
206 self.sync_lsp_after_recovery_replay(buffer_id);
207 recovered_count += 1;
208 tracing::info!("Recovered buffer with chunks: {}", original_path.display());
209 }
210 }
211 Ok(RecoveryResult::OriginalFileModified { id, original_path }) => {
212 tracing::warn!(
213 "Recovery file {} skipped: original file {} was modified",
214 id,
215 original_path.display()
216 );
217 let name = original_path
220 .file_name()
221 .unwrap_or_default()
222 .to_string_lossy();
223 self.set_status_message(format!(
224 "{} changed on disk; unsaved changes not restored",
225 name
226 ));
227 }
228 Ok(RecoveryResult::Corrupted { id, reason }) => {
229 tracing::warn!("Recovery file {} corrupted: {}", id, reason);
230 }
231 Ok(RecoveryResult::NotFound { id }) => {
232 tracing::warn!("Recovery file {} not found", id);
233 }
234 Err(e) => {
235 tracing::warn!("Failed to recover {}: {}", entry.id, e);
236 }
237 }
238 }
239
240 Ok(recovered_count)
241 }
242
243 pub fn discard_all_recovery(&mut self) -> AnyhowResult<usize> {
246 Ok(self.recovery_service.discard_all_recovery()?)
247 }
248
249 pub fn try_restore_hot_exit_buffers(&mut self) -> AnyhowResult<usize> {
264 use crate::services::recovery::RecoveryResult;
265
266 if !self.config.editor.hot_exit {
267 return Ok(0);
268 }
269
270 let entries = self.recovery_service.list_recoverable()?;
271 if entries.is_empty() {
272 return Ok(0);
273 }
274
275 let mut restored = 0;
276 for entry in entries {
277 match self.recovery_service.load_recovery(&entry) {
278 Ok(RecoveryResult::Recovered {
279 original_path,
280 content,
281 }) => {
282 let text = String::from_utf8_lossy(&content).into_owned();
283 if let Some(path) = original_path {
284 match self.open_file(&path) {
285 Ok(buffer_id) => {
286 {
287 let state = self.active_state_mut();
288 let total = state.buffer.total_bytes();
289 state.buffer.delete(0..total);
290 state.buffer.insert(0, &text);
291 state.buffer.set_modified(true);
292 state.buffer.set_recovery_pending(false);
293 }
294 self.active_event_log_mut().clear_saved_position();
295 self.sync_lsp_after_recovery_replay(buffer_id);
296 restored += 1;
297 tracing::info!(
298 "Hot-exit restore: reopened {} with unsaved changes",
299 path.display()
300 );
301 }
302 Err(e) => {
303 if let Some(confirmation) = e.downcast_ref::<
304 crate::model::buffer::LargeFileEncodingConfirmation,
305 >() {
306 self.start_large_file_encoding_confirmation(confirmation);
307 } else {
308 tracing::warn!(
309 "Hot-exit restore failed to open {}: {}",
310 path.display(),
311 e
312 );
313 }
314 }
315 }
316 } else {
317 let buffer_id = self.new_buffer();
321 {
322 let state = self.active_state_mut();
323 state.buffer.insert(0, &text);
324 state.buffer.set_modified(true);
325 state.buffer.set_recovery_pending(false);
326 }
327 self.active_event_log_mut().clear_saved_position();
328 if let Some(meta) = self.buffer_metadata.get_mut(&buffer_id) {
329 meta.recovery_id = Some(entry.id.clone());
330 }
331 self.sync_lsp_after_recovery_replay(buffer_id);
332 restored += 1;
333 tracing::info!(
334 "Hot-exit restore: reopened unnamed buffer (recovery_id={})",
335 entry.id
336 );
337 }
338 }
339 Ok(RecoveryResult::RecoveredChunks {
340 original_path,
341 chunks,
342 }) => match self.open_file(&original_path) {
343 Ok(buffer_id) => {
344 {
345 let state = self.active_state_mut();
346 for chunk in chunks.into_iter().rev() {
347 let text = String::from_utf8_lossy(&chunk.content).into_owned();
348 if chunk.original_len > 0 {
349 state
350 .buffer
351 .delete(chunk.offset..chunk.offset + chunk.original_len);
352 }
353 state.buffer.insert(chunk.offset, &text);
354 }
355 state.buffer.set_modified(true);
356 state.buffer.set_recovery_pending(false);
357 }
358 self.active_event_log_mut().clear_saved_position();
359 self.sync_lsp_after_recovery_replay(buffer_id);
360 restored += 1;
361 tracing::info!(
362 "Hot-exit restore: reopened {} with chunked changes",
363 original_path.display()
364 );
365 }
366 Err(e) => {
367 tracing::warn!(
368 "Hot-exit restore failed to open {}: {}",
369 original_path.display(),
370 e
371 );
372 }
373 },
374 Ok(RecoveryResult::OriginalFileModified { id, original_path }) => {
375 tracing::warn!(
376 "Hot-exit restore skipped {}: original file {} changed on disk",
377 id,
378 original_path.display()
379 );
380 }
381 Ok(RecoveryResult::Corrupted { id, reason }) => {
382 tracing::warn!("Hot-exit restore skipped {}: corrupted ({})", id, reason);
383 }
384 Ok(RecoveryResult::NotFound { id }) => {
385 tracing::warn!("Hot-exit restore: recovery file {} missing", id);
386 }
387 Err(e) => {
388 tracing::warn!("Hot-exit restore: failed to load {}: {}", entry.id, e);
389 }
390 }
391 }
392
393 Ok(restored)
394 }
395
396 pub fn auto_recovery_save_dirty_buffers(&mut self) -> AnyhowResult<usize> {
399 if !self.recovery_service.is_enabled() {
400 return Ok(0);
401 }
402
403 let interval = std::time::Duration::from_secs(
404 self.config.editor.auto_recovery_save_interval_secs as u64,
405 );
406 if self.time_source.elapsed_since(self.last_auto_recovery_save) < interval {
407 return Ok(0);
408 }
409
410 let saved = self.save_pending_recovery_buffers()?;
411 self.last_auto_recovery_save = self.time_source.now();
412 Ok(saved)
413 }
414
415 fn save_pending_recovery_buffers(&mut self) -> AnyhowResult<usize> {
418 if !self.recovery_service.is_enabled() {
419 return Ok(0);
420 }
421
422 let buffers_needing_recovery: Vec<_> = self
425 .buffers
426 .iter()
427 .filter_map(|(buffer_id, state)| {
428 if state.is_composite_buffer {
429 return None;
430 }
431 if let Some(meta) = self.buffer_metadata.get(buffer_id) {
432 if meta.hidden_from_tabs || meta.is_virtual() {
433 return None;
434 }
435 }
436 if state.buffer.is_recovery_pending() {
437 Some(*buffer_id)
438 } else {
439 None
440 }
441 })
442 .collect();
443
444 for buffer_id in &buffers_needing_recovery {
446 let needs_id = self
447 .buffer_metadata
448 .get(buffer_id)
449 .map(|meta| {
450 let path = meta.file_path();
451 let is_unnamed = path.map(|p| p.as_os_str().is_empty()).unwrap_or(true);
452 is_unnamed && meta.recovery_id.is_none()
453 })
454 .unwrap_or(false);
455
456 if needs_id {
457 let new_id = crate::services::recovery::generate_buffer_id();
458 if let Some(meta) = self.buffer_metadata.get_mut(buffer_id) {
459 meta.recovery_id = Some(new_id);
460 }
461 }
462 }
463
464 let buffer_info: Vec<_> = buffers_needing_recovery
466 .into_iter()
467 .filter_map(|buffer_id| {
468 let state = self.buffers.get(&buffer_id)?;
469 let meta = self.buffer_metadata.get(&buffer_id)?;
470 let path = state.buffer.file_path().map(|p| p.to_path_buf());
471 let recovery_id = if let Some(ref stored_id) = meta.recovery_id {
472 stored_id.clone()
473 } else {
474 self.recovery_service.get_buffer_id(path.as_deref())
475 };
476 let recovery_pending = state.buffer.is_recovery_pending();
477 if self
478 .recovery_service
479 .needs_auto_recovery_save(&recovery_id, recovery_pending)
480 {
481 Some((buffer_id, recovery_id, path))
482 } else {
483 None
484 }
485 })
486 .collect();
487
488 let mut saved_count = 0;
489 for (buffer_id, recovery_id, path) in buffer_info {
490 if self.save_buffer_to_recovery(&buffer_id, &recovery_id, path.as_deref())? {
491 saved_count += 1;
492 }
493 }
494 Ok(saved_count)
495 }
496
497 pub fn is_active_buffer_recovery_dirty(&self) -> bool {
500 if let Some(state) = self.buffers.get(&self.active_buffer()) {
501 state.buffer.is_recovery_pending()
502 } else {
503 false
504 }
505 }
506
507 pub fn delete_buffer_recovery(&mut self, buffer_id: BufferId) -> AnyhowResult<()> {
509 let recovery_id = {
511 let meta = self.buffer_metadata.get(&buffer_id);
512 let state = self.buffers.get(&buffer_id);
513
514 if let Some(stored_id) = meta.and_then(|m| m.recovery_id.clone()) {
515 stored_id
516 } else if let Some(state) = state {
517 let path = state.buffer.file_path().map(|p| p.to_path_buf());
518 self.recovery_service.get_buffer_id(path.as_deref())
519 } else {
520 return Ok(());
521 }
522 };
523
524 self.recovery_service.delete_buffer_recovery(&recovery_id)?;
525
526 if let Some(state) = self.buffers.get_mut(&buffer_id) {
528 state.buffer.set_recovery_pending(false);
529 }
530 Ok(())
531 }
532
533 fn save_buffer_to_recovery(
539 &mut self,
540 buffer_id: &BufferId,
541 recovery_id: &str,
542 path: Option<&std::path::Path>,
543 ) -> AnyhowResult<bool> {
544 let state = match self.buffers.get_mut(buffer_id) {
545 Some(s) => s,
546 None => return Ok(false),
547 };
548 let line_count = state.buffer.line_count();
549
550 if state.buffer.is_large_file() {
551 let chunks = state.buffer.get_recovery_chunks();
552 if chunks.is_empty() {
553 state.buffer.set_recovery_pending(false);
554 return Ok(false);
555 }
556 let recovery_chunks: Vec<_> = chunks
557 .into_iter()
558 .map(|(offset, content)| {
559 crate::services::recovery::types::RecoveryChunk::new(offset, 0, content)
560 })
561 .collect();
562 let original_size = state.buffer.original_file_size().unwrap_or(0);
563 let final_size = state.buffer.total_bytes();
564 self.recovery_service.save_buffer(
565 recovery_id,
566 recovery_chunks,
567 path,
568 None,
569 line_count,
570 original_size,
571 final_size,
572 )?;
573 } else {
574 let total_bytes = state.buffer.total_bytes();
575 let content = match state.buffer.get_text_range_mut(0, total_bytes) {
576 Ok(bytes) => bytes,
577 Err(e) => {
578 tracing::warn!("Failed to get buffer content for recovery save: {}", e);
579 return Ok(false);
580 }
581 };
582 let chunks = vec![crate::services::recovery::types::RecoveryChunk::new(
583 0, 0, content,
584 )];
585 self.recovery_service.save_buffer(
586 recovery_id,
587 chunks,
588 path,
589 None,
590 line_count,
591 0,
592 total_bytes,
593 )?;
594 }
595
596 state.buffer.set_recovery_pending(false);
597 Ok(true)
598 }
599}