1use crate::model::buffer::SudoSaveRequired;
12use crate::view::prompt::PromptType;
13use std::path::{Path, PathBuf};
14
15use lsp_types::TextDocumentContentChangeEvent;
16use rust_i18n::t;
17
18use crate::model::event::{BufferId, EventLog};
19use crate::services::lsp::manager::LspSpawnResult;
20use crate::state::EditorState;
21
22use super::{BufferMetadata, Editor};
23
24impl Editor {
25 pub fn save(&mut self) -> anyhow::Result<()> {
27 let path = self
28 .active_state()
29 .buffer
30 .file_path()
31 .map(|p| p.to_path_buf());
32
33 match self.active_state_mut().buffer.save() {
34 Ok(()) => self.finalize_save(path),
35 Err(e) => {
36 if let Some(sudo_info) = e.downcast_ref::<SudoSaveRequired>() {
37 let info = sudo_info.clone();
38 self.start_prompt(
39 t!("prompt.sudo_save_confirm").to_string(),
40 PromptType::ConfirmSudoSave { info },
41 );
42 Ok(())
43 } else {
44 Err(e)
45 }
46 }
47 }
48 }
49
50 pub(crate) fn finalize_save(&mut self, path: Option<PathBuf>) -> anyhow::Result<()> {
52 let buffer_id = self.active_buffer();
53 self.finalize_save_buffer(buffer_id, path, false)
54 }
55
56 pub(crate) fn finalize_save_buffer(
58 &mut self,
59 buffer_id: BufferId,
60 path: Option<PathBuf>,
61 silent: bool,
62 ) -> anyhow::Result<()> {
63 if let Some(ref p) = path {
65 if let Some(state) = self.buffers.get_mut(&buffer_id) {
66 if state.language == "text" {
67 let detected =
68 crate::primitives::detected_language::DetectedLanguage::from_path(
69 p,
70 &self.grammar_registry,
71 &self.config.languages,
72 );
73 state.apply_language(detected);
74 }
75 }
76 }
77
78 if !silent {
79 self.status_message = Some(t!("status.file_saved").to_string());
80 }
81
82 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
84 event_log.mark_saved();
85 }
86
87 if let Some(ref p) = path {
89 if let Ok(metadata) = self.filesystem.metadata(p) {
90 if let Some(mtime) = metadata.modified {
91 self.file_mod_times.insert(p.clone(), mtime);
92 }
93 }
94 }
95
96 self.notify_lsp_save_buffer(buffer_id);
98
99 if let Err(e) = self.delete_buffer_recovery(buffer_id) {
101 tracing::warn!("Failed to delete recovery file: {}", e);
102 }
103
104 if let Some(ref p) = path {
106 self.emit_event(
107 crate::model::control_event::events::FILE_SAVED.name,
108 serde_json::json!({
109 "path": p.display().to_string()
110 }),
111 );
112 }
113
114 if let Some(ref p) = path {
116 self.plugin_manager.run_hook(
117 "after_file_save",
118 crate::services::plugins::hooks::HookArgs::AfterFileSave {
119 buffer_id,
120 path: p.clone(),
121 },
122 );
123 }
124
125 if !silent {
131 match self.run_on_save_actions() {
132 Ok(true) => {
133 if self.status_message.as_deref() == Some(&t!("status.file_saved")) {
136 self.status_message =
137 Some(t!("status.file_saved_with_actions").to_string());
138 }
139 }
141 Ok(false) => {
142 }
144 Err(e) => {
145 self.status_message = Some(e);
147 }
148 }
149 }
150
151 Ok(())
152 }
153
154 pub fn auto_save_persistent_buffers(&mut self) -> anyhow::Result<usize> {
157 if !self.config.editor.auto_save_enabled {
158 return Ok(0);
159 }
160
161 let interval =
163 std::time::Duration::from_secs(self.config.editor.auto_save_interval_secs as u64);
164 if self
165 .time_source
166 .elapsed_since(self.last_persistent_auto_save)
167 < interval
168 {
169 return Ok(0);
170 }
171
172 self.last_persistent_auto_save = self.time_source.now();
173
174 let mut to_save = Vec::new();
176 for (id, state) in &self.buffers {
177 if state.buffer.is_modified() {
178 if let Some(path) = state.buffer.file_path() {
179 to_save.push((*id, path.to_path_buf()));
180 }
181 }
182 }
183
184 let mut count = 0;
185 for (id, path) in to_save {
186 if let Some(state) = self.buffers.get_mut(&id) {
187 match state.buffer.save() {
188 Ok(()) => {
189 self.finalize_save_buffer(id, Some(path), true)?;
190 count += 1;
191 }
192 Err(e) => {
193 if e.downcast_ref::<SudoSaveRequired>().is_some() {
195 tracing::debug!(
196 "Auto-save skipped for {:?} (sudo required)",
197 path.display()
198 );
199 } else {
200 tracing::warn!("Auto-save failed for {:?}: {}", path.display(), e);
201 }
202 }
203 }
204 }
205 }
206
207 Ok(count)
208 }
209
210 pub fn save_all_on_exit(&mut self) -> anyhow::Result<usize> {
214 let mut to_save = Vec::new();
215 for (id, state) in &self.buffers {
216 if state.buffer.is_modified() {
217 if let Some(path) = state.buffer.file_path() {
218 if !path.as_os_str().is_empty() {
219 to_save.push((*id, path.to_path_buf()));
220 }
221 }
222 }
223 }
224
225 let mut count = 0;
226 for (id, path) in to_save {
227 if let Some(state) = self.buffers.get_mut(&id) {
228 match state.buffer.save() {
229 Ok(()) => {
230 self.finalize_save_buffer(id, Some(path), true)?;
231 count += 1;
232 }
233 Err(e) => {
234 if e.downcast_ref::<SudoSaveRequired>().is_some() {
235 tracing::debug!(
236 "Auto-save on exit skipped for {} (sudo required)",
237 path.display()
238 );
239 } else {
240 tracing::warn!(
241 "Auto-save on exit failed for {}: {}",
242 path.display(),
243 e
244 );
245 }
246 }
247 }
248 }
249 }
250
251 Ok(count)
252 }
253
254 pub fn revert_file(&mut self) -> anyhow::Result<bool> {
257 let path = match self.active_state().buffer.file_path() {
258 Some(p) => p.to_path_buf(),
259 None => {
260 self.status_message = Some(t!("status.no_file_to_revert").to_string());
261 return Ok(false);
262 }
263 };
264
265 if !path.exists() {
266 self.status_message =
267 Some(t!("status.file_not_exists", path = path.display().to_string()).to_string());
268 return Ok(false);
269 }
270
271 let active_split = self.split_manager.active_split();
273 let (old_top_byte, old_left_column) = self
274 .split_view_states
275 .get(&active_split)
276 .map(|vs| (vs.viewport.top_byte, vs.viewport.left_column))
277 .unwrap_or((0, 0));
278 let old_cursors = self.active_cursors().clone();
279
280 let old_buffer_settings = self.active_state().buffer_settings.clone();
282 let old_editing_disabled = self.active_state().editing_disabled;
283
284 let mut new_state = EditorState::from_file_with_languages(
286 &path,
287 self.terminal_width,
288 self.terminal_height,
289 self.config.editor.large_file_threshold_bytes as usize,
290 &self.grammar_registry,
291 &self.config.languages,
292 std::sync::Arc::clone(&self.filesystem),
293 )?;
294
295 let new_file_size = new_state.buffer.len();
297 let mut restored_cursors = old_cursors;
298 restored_cursors.map(|cursor| {
299 cursor.position = cursor.position.min(new_file_size);
300 cursor.clear_selection();
302 });
303 new_state.buffer_settings = old_buffer_settings;
305 new_state.editing_disabled = old_editing_disabled;
306 let buffer_id = self.active_buffer();
310 if let Some(state) = self.buffers.get_mut(&buffer_id) {
311 *state = new_state;
312 }
314
315 let active_split = self.split_manager.active_split();
317 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
318 view_state.cursors = restored_cursors;
319 }
320
321 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
323 view_state.viewport.top_byte = old_top_byte.min(new_file_size);
324 view_state.viewport.left_column = old_left_column;
325 }
326
327 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
329 *event_log = EventLog::new();
330 }
331
332 self.seen_byte_ranges.remove(&buffer_id);
334
335 if let Ok(metadata) = self.filesystem.metadata(&path) {
337 if let Some(mtime) = metadata.modified {
338 self.file_mod_times.insert(path.clone(), mtime);
339 }
340 }
341
342 self.notify_lsp_file_changed(&path);
344
345 self.status_message = Some(t!("status.reverted").to_string());
346 Ok(true)
347 }
348
349 pub fn toggle_auto_revert(&mut self) {
351 self.auto_revert_enabled = !self.auto_revert_enabled;
352
353 if self.auto_revert_enabled {
354 self.status_message = Some(t!("status.auto_revert_enabled").to_string());
355 } else {
356 self.status_message = Some(t!("status.auto_revert_disabled").to_string());
357 }
358 }
359
360 pub fn poll_file_changes(&mut self) -> bool {
365 if !self.auto_revert_enabled {
367 return false;
368 }
369
370 let poll_interval =
372 std::time::Duration::from_millis(self.config.editor.auto_revert_poll_interval_ms);
373 let elapsed = self.time_source.elapsed_since(self.last_auto_revert_poll);
374 tracing::trace!(
375 "poll_file_changes: elapsed={:?}, poll_interval={:?}",
376 elapsed,
377 poll_interval
378 );
379 if elapsed < poll_interval {
380 return false;
381 }
382 self.last_auto_revert_poll = self.time_source.now();
383
384 let files_to_check: Vec<PathBuf> = self
386 .buffers
387 .values()
388 .filter_map(|state| state.buffer.file_path().map(PathBuf::from))
389 .collect();
390
391 let mut any_changed = false;
392
393 for path in files_to_check {
394 let current_mtime = match self.filesystem.metadata(&path) {
396 Ok(meta) => match meta.modified {
397 Some(mtime) => mtime,
398 None => continue,
399 },
400 Err(_) => continue, };
402
403 if let Some(&stored_mtime) = self.file_mod_times.get(&path) {
405 if current_mtime != stored_mtime {
406 let path_str = path.display().to_string();
410 if self.handle_async_file_changed(path_str) {
411 any_changed = true;
412 }
413 }
414 } else {
415 self.file_mod_times.insert(path, current_mtime);
417 }
418 }
419
420 any_changed
421 }
422
423 pub fn poll_file_tree_changes(&mut self) -> bool {
428 let poll_interval =
430 std::time::Duration::from_millis(self.config.editor.file_tree_poll_interval_ms);
431 if self.time_source.elapsed_since(self.last_file_tree_poll) < poll_interval {
432 return false;
433 }
434 self.last_file_tree_poll = self.time_source.now();
435
436 let Some(explorer) = &self.file_explorer else {
438 return false;
439 };
440
441 use crate::view::file_tree::NodeId;
443 let expanded_dirs: Vec<(NodeId, PathBuf)> = explorer
444 .tree()
445 .all_nodes()
446 .filter(|node| node.is_dir() && node.is_expanded())
447 .map(|node| (node.id, node.entry.path.clone()))
448 .collect();
449
450 let mut dirs_to_refresh: Vec<NodeId> = Vec::new();
452
453 for (node_id, path) in expanded_dirs {
454 let current_mtime = match self.filesystem.metadata(&path) {
456 Ok(meta) => match meta.modified {
457 Some(mtime) => mtime,
458 None => continue,
459 },
460 Err(_) => continue, };
462
463 if let Some(&stored_mtime) = self.dir_mod_times.get(&path) {
465 if current_mtime != stored_mtime {
466 self.dir_mod_times.insert(path.clone(), current_mtime);
468 dirs_to_refresh.push(node_id);
469 tracing::debug!("Directory changed: {:?}", path);
470 }
471 } else {
472 self.dir_mod_times.insert(path, current_mtime);
474 }
475 }
476
477 let git_index_changed = self.check_git_index_mtime();
480
481 if dirs_to_refresh.is_empty() && !git_index_changed {
483 return false;
484 }
485
486 if let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) {
488 for node_id in dirs_to_refresh {
489 let tree = explorer.tree_mut();
490 if let Err(e) = runtime.block_on(tree.refresh_node(node_id)) {
491 tracing::warn!("Failed to refresh directory: {}", e);
492 }
493 }
494 }
495
496 true
497 }
498
499 fn check_git_index_mtime(&mut self) -> bool {
504 if !self.git_index_resolved {
506 self.git_index_resolved = true;
507 if let Some(path) = Self::resolve_git_index(&self.working_dir) {
508 if let Ok(mtime) = std::fs::metadata(&path).and_then(|m| m.modified()) {
509 self.dir_mod_times.insert(path, mtime);
510 }
511 }
512 return false; }
514
515 let git_index_path = self
518 .dir_mod_times
519 .keys()
520 .find(|p| p.ends_with(".git/index") || p.ends_with(".git\\index"))
521 .cloned();
522
523 let Some(path) = git_index_path else {
524 return false; };
526
527 let current_mtime = match std::fs::metadata(&path).and_then(|m| m.modified()) {
528 Ok(mtime) => mtime,
529 Err(_) => return false,
530 };
531
532 let stored_mtime = self.dir_mod_times[&path];
533 if current_mtime != stored_mtime {
534 self.dir_mod_times.insert(path, current_mtime);
535 self.plugin_manager.run_hook(
536 "focus_gained",
537 crate::services::plugins::hooks::HookArgs::FocusGained,
538 );
539 return true;
540 }
541
542 false
543 }
544
545 fn resolve_git_index(working_dir: &Path) -> Option<PathBuf> {
548 let output = std::process::Command::new("git")
549 .args(["rev-parse", "--git-dir"])
550 .current_dir(working_dir)
551 .output()
552 .ok()?;
553 if !output.status.success() {
554 return None;
555 }
556 let git_dir = std::str::from_utf8(&output.stdout).ok()?.trim();
557 let git_dir_path = if std::path::Path::new(git_dir).is_absolute() {
558 PathBuf::from(git_dir)
559 } else {
560 working_dir.join(git_dir)
561 };
562 Some(git_dir_path.join("index"))
563 }
564
565 pub(crate) fn notify_lsp_file_opened(
568 &mut self,
569 path: &Path,
570 buffer_id: BufferId,
571 metadata: &mut BufferMetadata,
572 ) {
573 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
575 tracing::debug!("No buffer state for file: {}", path.display());
576 return;
577 };
578
579 let Some(uri) = metadata.file_uri().cloned() else {
580 tracing::warn!(
581 "No URI in metadata for file: {} (failed to compute absolute path)",
582 path.display()
583 );
584 return;
585 };
586
587 let file_size = self
589 .filesystem
590 .metadata(path)
591 .ok()
592 .map(|m| m.size)
593 .unwrap_or(0);
594 if file_size > self.config.editor.large_file_threshold_bytes {
595 let reason = format!("File too large ({} bytes)", file_size);
596 tracing::warn!(
597 "Skipping LSP for large file: {} ({})",
598 path.display(),
599 reason
600 );
601 metadata.disable_lsp(reason);
602 return;
603 }
604
605 let text = match self
607 .buffers
608 .get(&buffer_id)
609 .and_then(|state| state.buffer.to_string())
610 {
611 Some(t) => t,
612 None => {
613 tracing::debug!("Buffer not fully loaded for LSP notification");
614 return;
615 }
616 };
617
618 let enable_inlay_hints = self.config.editor.enable_inlay_hints;
619 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
620
621 let (last_line, last_char) = self
623 .buffers
624 .get(&buffer_id)
625 .map(|state| {
626 let line_count = state.buffer.line_count().unwrap_or(1000);
627 (line_count.saturating_sub(1) as u32, 10000u32)
628 })
629 .unwrap_or((999, 10000));
630
631 let Some(lsp) = &mut self.lsp else {
633 tracing::debug!("No LSP manager available");
634 return;
635 };
636
637 tracing::debug!("LSP manager available for file: {}", path.display());
638 tracing::debug!(
639 "Detected language: {} for file: {}",
640 language,
641 path.display()
642 );
643 tracing::debug!("Using URI from metadata: {}", uri.as_str());
644 tracing::debug!("Attempting to spawn LSP client for language: {}", language);
645
646 match lsp.try_spawn(&language, Some(path)) {
647 LspSpawnResult::Spawned => {
648 if let Some(client) = lsp.get_handle_mut(&language) {
649 tracing::info!("Sending didOpen to LSP for: {}", uri.as_str());
651 if let Err(e) = client.did_open(uri.clone(), text, language.clone()) {
652 tracing::warn!("Failed to send didOpen to LSP: {}", e);
653 return;
654 }
655 tracing::info!("Successfully sent didOpen to LSP");
656
657 metadata.lsp_opened_with.insert(client.id());
659
660 let request_id = self.next_lsp_request_id;
662 self.next_lsp_request_id += 1;
663 if let Err(e) =
664 client.document_diagnostic(request_id, uri.clone(), previous_result_id)
665 {
666 tracing::debug!(
667 "Failed to request pull diagnostics (server may not support): {}",
668 e
669 );
670 } else {
671 tracing::info!(
672 "Requested pull diagnostics for {} (request_id={})",
673 uri.as_str(),
674 request_id
675 );
676 }
677
678 if enable_inlay_hints {
680 let request_id = self.next_lsp_request_id;
681 self.next_lsp_request_id += 1;
682 self.pending_inlay_hints_request = Some(request_id);
683
684 if let Err(e) =
685 client.inlay_hints(request_id, uri.clone(), 0, 0, last_line, last_char)
686 {
687 tracing::debug!(
688 "Failed to request inlay hints (server may not support): {}",
689 e
690 );
691 self.pending_inlay_hints_request = None;
692 } else {
693 tracing::info!(
694 "Requested inlay hints for {} (request_id={})",
695 uri.as_str(),
696 request_id
697 );
698 }
699 }
700
701 self.schedule_folding_ranges_refresh(buffer_id);
703 }
704 }
705 LspSpawnResult::NotAutoStart => {
706 tracing::debug!(
707 "LSP for {} not auto-starting (auto_start=false). Use command palette to start manually.",
708 language
709 );
710 }
711 LspSpawnResult::NotConfigured => {
712 tracing::debug!("No LSP server configured for language: {}", language);
713 }
714 LspSpawnResult::Failed => {
715 tracing::warn!("Failed to spawn LSP client for language: {}", language);
716 }
717 }
718 }
719
720 pub(crate) fn watch_file(&mut self, path: &Path) {
723 if let Ok(metadata) = self.filesystem.metadata(path) {
725 if let Some(mtime) = metadata.modified {
726 self.file_mod_times.insert(path.to_path_buf(), mtime);
727 }
728 }
729 }
730
731 pub(crate) fn notify_lsp_file_changed(&mut self, path: &Path) {
733 use crate::services::lsp::manager::LspSpawnResult;
734
735 let Some(lsp_uri) = super::types::file_path_to_lsp_uri(path) else {
736 return;
737 };
738
739 let Some((buffer_id, content, language)) = self
741 .buffers
742 .iter()
743 .find(|(_, s)| s.buffer.file_path() == Some(path))
744 .and_then(|(id, state)| {
745 state
746 .buffer
747 .to_string()
748 .map(|t| (*id, t, state.language.clone()))
749 })
750 else {
751 return;
752 };
753
754 let spawn_result = {
756 let Some(lsp) = self.lsp.as_mut() else {
757 return;
758 };
759 lsp.try_spawn(&language, Some(path))
760 };
761
762 if spawn_result != LspSpawnResult::Spawned {
764 return;
765 }
766
767 let handle_id = {
769 let Some(lsp) = self.lsp.as_mut() else {
770 return;
771 };
772 let Some(handle) = lsp.get_handle_mut(&language) else {
773 return;
774 };
775 handle.id()
776 };
777
778 let needs_open = {
780 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
781 return;
782 };
783 !metadata.lsp_opened_with.contains(&handle_id)
784 };
785
786 if needs_open {
787 if let Some(lsp) = self.lsp.as_mut() {
789 if let Some(handle) = lsp.get_handle_mut(&language) {
790 if let Err(e) =
791 handle.did_open(lsp_uri.clone(), content.clone(), language.clone())
792 {
793 tracing::warn!("Failed to send didOpen before didChange: {}", e);
794 return;
795 }
796 tracing::debug!(
797 "Sent didOpen for {} to LSP handle {} before file change notification",
798 lsp_uri.as_str(),
799 handle_id
800 );
801 }
802 }
803
804 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
806 metadata.lsp_opened_with.insert(handle_id);
807 }
808 }
809
810 if let Some(lsp) = &mut self.lsp {
812 let content_change = TextDocumentContentChangeEvent {
813 range: None, range_length: None,
815 text: content,
816 };
817 for sh in lsp.get_handles_mut(&language) {
818 if let Err(e) = sh
819 .handle
820 .did_change(lsp_uri.clone(), vec![content_change.clone()])
821 {
822 tracing::warn!("Failed to notify LSP '{}' of file change: {}", sh.name, e);
823 }
824 }
825 }
826 }
827
828 pub(crate) fn revert_buffer_by_id(
834 &mut self,
835 buffer_id: BufferId,
836 path: &Path,
837 ) -> anyhow::Result<()> {
838 let old_cursors = self
842 .split_view_states
843 .values()
844 .find_map(|vs| {
845 if vs.keyed_states.contains_key(&buffer_id) {
846 vs.keyed_states.get(&buffer_id).map(|bs| bs.cursors.clone())
847 } else {
848 None
849 }
850 })
851 .unwrap_or_default();
852 let (old_buffer_settings, old_editing_disabled) = self
853 .buffers
854 .get(&buffer_id)
855 .map(|s| (s.buffer_settings.clone(), s.editing_disabled))
856 .unwrap_or_default();
857
858 let mut new_state = EditorState::from_file_with_languages(
860 path,
861 self.terminal_width,
862 self.terminal_height,
863 self.config.editor.large_file_threshold_bytes as usize,
864 &self.grammar_registry,
865 &self.config.languages,
866 std::sync::Arc::clone(&self.filesystem),
867 )?;
868
869 let new_file_size = new_state.buffer.len();
871
872 let mut restored_cursors = old_cursors;
874 restored_cursors.map(|cursor| {
875 cursor.position = cursor.position.min(new_file_size);
876 cursor.clear_selection();
877 });
878 new_state.buffer_settings = old_buffer_settings;
880 new_state.editing_disabled = old_editing_disabled;
881 if let Some(state) = self.buffers.get_mut(&buffer_id) {
885 *state = new_state;
886 }
887
888 for vs in self.split_view_states.values_mut() {
890 if let Some(buf_state) = vs.keyed_states.get_mut(&buffer_id) {
891 buf_state.cursors = restored_cursors.clone();
892 }
893 }
894
895 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
897 *event_log = EventLog::new();
898 }
899
900 self.seen_byte_ranges.remove(&buffer_id);
902
903 if let Ok(metadata) = self.filesystem.metadata(path) {
905 if let Some(mtime) = metadata.modified {
906 self.file_mod_times.insert(path.to_path_buf(), mtime);
907 }
908 }
909
910 self.notify_lsp_file_changed(path);
912
913 Ok(())
914 }
915
916 pub fn handle_file_changed(&mut self, changed_path: &str) {
918 let path = PathBuf::from(changed_path);
919
920 let buffer_ids: Vec<BufferId> = self
922 .buffers
923 .iter()
924 .filter(|(_, state)| state.buffer.file_path() == Some(&path))
925 .map(|(id, _)| *id)
926 .collect();
927
928 if buffer_ids.is_empty() {
929 return;
930 }
931
932 for buffer_id in buffer_ids {
933 if self.terminal_buffers.contains_key(&buffer_id) {
936 continue;
937 }
938
939 let state = match self.buffers.get(&buffer_id) {
940 Some(s) => s,
941 None => continue,
942 };
943
944 let current_mtime = match self
948 .filesystem
949 .metadata(&path)
950 .ok()
951 .and_then(|m| m.modified)
952 {
953 Some(mtime) => mtime,
954 None => continue, };
956
957 let dominated_by_stored = self
958 .file_mod_times
959 .get(&path)
960 .map(|stored| current_mtime <= *stored)
961 .unwrap_or(false);
962
963 if dominated_by_stored {
964 continue;
965 }
966
967 if state.buffer.is_modified() {
969 self.status_message = Some(format!(
970 "File {} changed on disk (buffer has unsaved changes)",
971 path.display()
972 ));
973 continue;
974 }
975
976 if self.auto_revert_enabled {
978 let still_needs_revert = self
982 .file_mod_times
983 .get(&path)
984 .map(|stored| current_mtime > *stored)
985 .unwrap_or(true);
986
987 if !still_needs_revert {
988 continue;
989 }
990
991 let is_active_buffer = buffer_id == self.active_buffer();
993
994 if is_active_buffer {
995 if let Err(e) = self.revert_file() {
997 tracing::error!("Failed to auto-revert file {:?}: {}", path, e);
998 } else {
999 tracing::info!("Auto-reverted file: {:?}", path);
1000 }
1001 } else {
1002 if let Err(e) = self.revert_buffer_by_id(buffer_id, &path) {
1005 tracing::error!("Failed to auto-revert background file {:?}: {}", path, e);
1006 } else {
1007 tracing::info!("Auto-reverted file: {:?}", path);
1008 }
1009 }
1010
1011 self.watch_file(&path);
1013 }
1014 }
1015 }
1016
1017 pub fn check_save_conflict(&self) -> Option<std::time::SystemTime> {
1020 let path = self.active_state().buffer.file_path()?;
1021
1022 let current_mtime = self
1024 .filesystem
1025 .metadata(path)
1026 .ok()
1027 .and_then(|m| m.modified)?;
1028
1029 match self.file_mod_times.get(path) {
1031 Some(recorded_mtime) if current_mtime > *recorded_mtime => {
1032 Some(current_mtime)
1034 }
1035 _ => None,
1036 }
1037 }
1038}