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 if !self.filesystem.is_remote_connected() {
29 anyhow::bail!(
30 "Cannot save: remote connection lost ({})",
31 self.filesystem
32 .remote_connection_info()
33 .unwrap_or("unknown host")
34 );
35 }
36
37 let path = self
38 .active_state()
39 .buffer
40 .file_path()
41 .map(|p| p.to_path_buf());
42
43 match self.active_state_mut().buffer.save() {
44 Ok(()) => self.finalize_save(path),
45 Err(e) => {
46 if let Some(sudo_info) = e.downcast_ref::<SudoSaveRequired>() {
47 let info = sudo_info.clone();
48 self.start_prompt(
49 t!("prompt.sudo_save_confirm").to_string(),
50 PromptType::ConfirmSudoSave { info },
51 );
52 Ok(())
53 } else if let Some(path) = path {
54 let is_not_found = e
56 .downcast_ref::<std::io::Error>()
57 .is_some_and(|io_err| io_err.kind() == std::io::ErrorKind::NotFound);
58 if is_not_found {
59 if let Some(parent) = path.parent() {
60 if !self.filesystem.exists(parent) {
61 let dir_name = parent
62 .strip_prefix(&self.working_dir)
63 .unwrap_or(parent)
64 .display()
65 .to_string();
66 self.start_prompt(
67 t!("buffer.create_directory_confirm", name = &dir_name)
68 .to_string(),
69 PromptType::ConfirmCreateDirectory { path },
70 );
71 return Ok(());
72 }
73 }
74 }
75 Err(e)
76 } else {
77 Err(e)
78 }
79 }
80 }
81 }
82
83 pub(crate) fn finalize_save(&mut self, path: Option<PathBuf>) -> anyhow::Result<()> {
85 let buffer_id = self.active_buffer();
86 self.finalize_save_buffer(buffer_id, path, false)
87 }
88
89 pub(crate) fn finalize_save_buffer(
91 &mut self,
92 buffer_id: BufferId,
93 path: Option<PathBuf>,
94 silent: bool,
95 ) -> anyhow::Result<()> {
96 if let Some(ref p) = path {
98 if let Some(state) = self.buffers.get_mut(&buffer_id) {
99 if state.language == "text" {
100 let first_line = state.buffer.first_line_lossy();
101 let detected =
102 crate::primitives::detected_language::DetectedLanguage::from_path(
103 p,
104 first_line.as_deref(),
105 &self.grammar_registry,
106 &self.config.languages,
107 );
108 state.apply_language(detected);
109 }
110 }
111 }
112
113 if !silent {
114 self.status_message = Some(t!("status.file_saved").to_string());
115 }
116
117 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
119 event_log.mark_saved();
120 }
121
122 if let Some(ref p) = path {
124 if let Ok(metadata) = self.filesystem.metadata(p) {
125 if let Some(mtime) = metadata.modified {
126 self.file_mod_times.insert(p.clone(), mtime);
127 }
128 }
129 }
130
131 self.notify_lsp_save_buffer(buffer_id);
133
134 if let Err(e) = self.delete_buffer_recovery(buffer_id) {
136 tracing::warn!("Failed to delete recovery file: {}", e);
137 }
138
139 if let Some(ref p) = path {
141 self.emit_event(
142 crate::model::control_event::events::FILE_SAVED.name,
143 serde_json::json!({
144 "path": p.display().to_string()
145 }),
146 );
147 }
148
149 if let Some(ref p) = path {
151 self.plugin_manager.run_hook(
152 "after_file_save",
153 crate::services::plugins::hooks::HookArgs::AfterFileSave {
154 buffer_id,
155 path: p.clone(),
156 },
157 );
158 }
159
160 if !silent {
166 match self.run_on_save_actions() {
167 Ok(true) => {
168 if self.status_message.as_deref() == Some(&t!("status.file_saved")) {
171 self.status_message =
172 Some(t!("status.file_saved_with_actions").to_string());
173 }
174 }
176 Ok(false) => {
177 }
179 Err(e) => {
180 self.status_message = Some(e);
182 }
183 }
184 }
185
186 Ok(())
187 }
188
189 pub fn auto_save_persistent_buffers(&mut self) -> anyhow::Result<usize> {
192 if !self.config.editor.auto_save_enabled {
193 return Ok(0);
194 }
195
196 let interval =
198 std::time::Duration::from_secs(self.config.editor.auto_save_interval_secs as u64);
199 if self
200 .time_source
201 .elapsed_since(self.last_persistent_auto_save)
202 < interval
203 {
204 return Ok(0);
205 }
206
207 self.last_persistent_auto_save = self.time_source.now();
208
209 let mut to_save = Vec::new();
211 for (id, state) in &self.buffers {
212 if state.buffer.is_modified() {
213 if let Some(path) = state.buffer.file_path() {
214 to_save.push((*id, path.to_path_buf()));
215 }
216 }
217 }
218
219 let mut count = 0;
220 for (id, path) in to_save {
221 if let Some(state) = self.buffers.get_mut(&id) {
222 match state.buffer.save() {
223 Ok(()) => {
224 self.finalize_save_buffer(id, Some(path), true)?;
225 count += 1;
226 }
227 Err(e) => {
228 if e.downcast_ref::<SudoSaveRequired>().is_some() {
230 tracing::debug!(
231 "Auto-save skipped for {:?} (sudo required)",
232 path.display()
233 );
234 } else {
235 tracing::warn!("Auto-save failed for {:?}: {}", path.display(), e);
236 }
237 }
238 }
239 }
240 }
241
242 Ok(count)
243 }
244
245 pub fn save_all_on_exit(&mut self) -> anyhow::Result<usize> {
249 let mut to_save = Vec::new();
250 for (id, state) in &self.buffers {
251 if state.buffer.is_modified() {
252 if let Some(path) = state.buffer.file_path() {
253 if !path.as_os_str().is_empty() {
254 to_save.push((*id, path.to_path_buf()));
255 }
256 }
257 }
258 }
259
260 let mut count = 0;
261 for (id, path) in to_save {
262 if let Some(state) = self.buffers.get_mut(&id) {
263 match state.buffer.save() {
264 Ok(()) => {
265 self.finalize_save_buffer(id, Some(path), true)?;
266 count += 1;
267 }
268 Err(e) => {
269 if e.downcast_ref::<SudoSaveRequired>().is_some() {
270 tracing::debug!(
271 "Auto-save on exit skipped for {} (sudo required)",
272 path.display()
273 );
274 } else {
275 tracing::warn!(
276 "Auto-save on exit failed for {}: {}",
277 path.display(),
278 e
279 );
280 }
281 }
282 }
283 }
284 }
285
286 Ok(count)
287 }
288
289 pub fn revert_file(&mut self) -> anyhow::Result<bool> {
292 let path = match self.active_state().buffer.file_path() {
293 Some(p) => p.to_path_buf(),
294 None => {
295 self.status_message = Some(t!("status.no_file_to_revert").to_string());
296 return Ok(false);
297 }
298 };
299
300 if !path.exists() {
301 self.status_message =
302 Some(t!("status.file_not_exists", path = path.display().to_string()).to_string());
303 return Ok(false);
304 }
305
306 let active_split = self.split_manager.active_split();
308 let (old_top_byte, old_left_column) = self
309 .split_view_states
310 .get(&active_split)
311 .map(|vs| (vs.viewport.top_byte, vs.viewport.left_column))
312 .unwrap_or((0, 0));
313 let old_cursors = self.active_cursors().clone();
314
315 let old_buffer_settings = self.active_state().buffer_settings.clone();
317 let old_editing_disabled = self.active_state().editing_disabled;
318
319 let mut new_state = EditorState::from_file_with_languages(
321 &path,
322 self.terminal_width,
323 self.terminal_height,
324 self.config.editor.large_file_threshold_bytes as usize,
325 &self.grammar_registry,
326 &self.config.languages,
327 std::sync::Arc::clone(&self.filesystem),
328 )?;
329
330 let new_file_size = new_state.buffer.len();
332 let mut restored_cursors = old_cursors;
333 restored_cursors.map(|cursor| {
334 cursor.position = cursor.position.min(new_file_size);
335 cursor.clear_selection();
337 });
338 new_state.buffer_settings = old_buffer_settings;
340 new_state.editing_disabled = old_editing_disabled;
341 let buffer_id = self.active_buffer();
345 if let Some(state) = self.buffers.get_mut(&buffer_id) {
346 *state = new_state;
347 }
349
350 let active_split = self.split_manager.active_split();
352 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
353 view_state.cursors = restored_cursors;
354 }
355
356 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
358 view_state.viewport.top_byte = old_top_byte.min(new_file_size);
359 view_state.viewport.left_column = old_left_column;
360 }
361
362 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
364 *event_log = EventLog::new();
365 }
366
367 self.seen_byte_ranges.remove(&buffer_id);
369
370 if let Ok(metadata) = self.filesystem.metadata(&path) {
372 if let Some(mtime) = metadata.modified {
373 self.file_mod_times.insert(path.clone(), mtime);
374 }
375 }
376
377 self.notify_lsp_file_changed(&path);
379
380 self.status_message = Some(t!("status.reverted").to_string());
381 Ok(true)
382 }
383
384 pub fn toggle_auto_revert(&mut self) {
386 self.auto_revert_enabled = !self.auto_revert_enabled;
387
388 if self.auto_revert_enabled {
389 self.status_message = Some(t!("status.auto_revert_enabled").to_string());
390 } else {
391 self.status_message = Some(t!("status.auto_revert_disabled").to_string());
392 }
393 }
394
395 pub fn poll_file_changes(&mut self) -> bool {
404 if !self.auto_revert_enabled {
406 return false;
407 }
408
409 let mut any_changed = false;
411 if let Some(ref rx) = self.pending_file_poll_rx {
412 match rx.try_recv() {
413 Ok(results) => {
414 self.pending_file_poll_rx = None;
415 any_changed = self.process_file_poll_results(results);
416 }
417 Err(std::sync::mpsc::TryRecvError::Empty) => {
418 return false;
420 }
421 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
422 self.pending_file_poll_rx = None;
424 }
425 }
426 }
427
428 let poll_interval =
430 std::time::Duration::from_millis(self.config.editor.auto_revert_poll_interval_ms);
431 let elapsed = self.time_source.elapsed_since(self.last_auto_revert_poll);
432 tracing::trace!(
433 "poll_file_changes: elapsed={:?}, poll_interval={:?}",
434 elapsed,
435 poll_interval
436 );
437 if elapsed < poll_interval {
438 return any_changed;
439 }
440 self.last_auto_revert_poll = self.time_source.now();
441
442 let files_to_check: Vec<PathBuf> = self
444 .buffers
445 .values()
446 .filter_map(|state| state.buffer.file_path().map(PathBuf::from))
447 .collect();
448
449 if files_to_check.is_empty() {
450 return any_changed;
451 }
452
453 let (tx, rx) = std::sync::mpsc::channel();
455 let fs = self.filesystem.clone();
456 std::thread::Builder::new()
457 .name("poll-file-changes".to_string())
458 .spawn(move || {
459 let results: Vec<(PathBuf, Option<std::time::SystemTime>)> = files_to_check
460 .into_iter()
461 .map(|path| {
462 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
463 (path, mtime)
464 })
465 .collect();
466 if tx.send(results).is_err() {}
469 })
470 .ok();
471 self.pending_file_poll_rx = Some(rx);
472
473 any_changed
474 }
475
476 fn process_file_poll_results(
478 &mut self,
479 results: Vec<(PathBuf, Option<std::time::SystemTime>)>,
480 ) -> bool {
481 let mut any_changed = false;
482 for (path, mtime_opt) in results {
483 let Some(current_mtime) = mtime_opt else {
484 continue;
485 };
486
487 if let Some(&stored_mtime) = self.file_mod_times.get(&path) {
488 if current_mtime != stored_mtime {
489 let path_str = path.display().to_string();
490 if self.handle_async_file_changed(path_str) {
491 any_changed = true;
492 }
493 }
494 } else {
495 self.file_mod_times.insert(path, current_mtime);
497 }
498 }
499 any_changed
500 }
501
502 pub fn poll_file_tree_changes(&mut self) -> bool {
510 use crate::view::file_tree::NodeId;
511
512 let mut any_refreshed = false;
514 if let Some(ref rx) = self.pending_dir_poll_rx {
515 match rx.try_recv() {
516 Ok((dir_results, git_index_mtime)) => {
517 self.pending_dir_poll_rx = None;
518 any_refreshed = self.process_dir_poll_results(dir_results, git_index_mtime);
519 }
520 Err(std::sync::mpsc::TryRecvError::Empty) => {
521 return false;
522 }
523 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
524 self.pending_dir_poll_rx = None;
525 }
526 }
527 }
528
529 let poll_interval =
531 std::time::Duration::from_millis(self.config.editor.file_tree_poll_interval_ms);
532 if self.time_source.elapsed_since(self.last_file_tree_poll) < poll_interval {
533 return any_refreshed;
534 }
535 self.last_file_tree_poll = self.time_source.now();
536
537 if !self.git_index_resolved {
541 self.git_index_resolved = true;
542 if let Some(path) = self.resolve_git_index() {
543 if let Ok(meta) = self.filesystem.metadata(&path) {
544 if let Some(mtime) = meta.modified {
545 self.dir_mod_times.insert(path, mtime);
546 }
547 }
548 }
549 }
550
551 let Some(explorer) = &self.file_explorer else {
553 return any_refreshed;
554 };
555
556 let expanded_dirs: Vec<(NodeId, PathBuf)> = explorer
558 .tree()
559 .all_nodes()
560 .filter(|node| node.is_dir() && node.is_expanded())
561 .map(|node| (node.id, node.entry.path.clone()))
562 .collect();
563
564 let git_index_path: Option<PathBuf> = self
566 .dir_mod_times
567 .keys()
568 .find(|p| p.ends_with(".git/index") || p.ends_with(".git\\index"))
569 .cloned();
570
571 if expanded_dirs.is_empty() && git_index_path.is_none() {
572 return any_refreshed;
573 }
574
575 let (tx, rx) = std::sync::mpsc::channel();
577 let fs = self.filesystem.clone();
578 std::thread::Builder::new()
579 .name("poll-dir-changes".to_string())
580 .spawn(move || {
581 let results: Vec<(NodeId, PathBuf, Option<std::time::SystemTime>)> = expanded_dirs
582 .into_iter()
583 .map(|(node_id, path)| {
584 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
585 (node_id, path, mtime)
586 })
587 .collect();
588
589 let git_index_mtime = git_index_path.and_then(|path| {
591 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
592 Some((path, mtime?))
593 });
594
595 if tx.send((results, git_index_mtime)).is_err() {}
597 })
598 .ok();
599 self.pending_dir_poll_rx = Some(rx);
600
601 any_refreshed
602 }
603
604 fn process_dir_poll_results(
606 &mut self,
607 results: Vec<(
608 crate::view::file_tree::NodeId,
609 PathBuf,
610 Option<std::time::SystemTime>,
611 )>,
612 git_index_mtime: Option<(PathBuf, std::time::SystemTime)>,
613 ) -> bool {
614 let mut dirs_to_refresh = Vec::new();
615
616 for (node_id, path, mtime_opt) in results {
617 let Some(current_mtime) = mtime_opt else {
618 continue;
619 };
620
621 if let Some(&stored_mtime) = self.dir_mod_times.get(&path) {
622 if current_mtime != stored_mtime {
623 self.dir_mod_times.insert(path.clone(), current_mtime);
624 dirs_to_refresh.push(node_id);
625 tracing::debug!("Directory changed: {:?}", path);
626 }
627 } else {
628 self.dir_mod_times.insert(path, current_mtime);
629 }
630 }
631
632 let git_index_changed = if let Some((path, current_mtime)) = git_index_mtime {
634 if let Some(&stored_mtime) = self.dir_mod_times.get(&path) {
635 if current_mtime != stored_mtime {
636 self.dir_mod_times.insert(path, current_mtime);
637 self.plugin_manager.run_hook(
638 "focus_gained",
639 crate::services::plugins::hooks::HookArgs::FocusGained,
640 );
641 true
642 } else {
643 false
644 }
645 } else {
646 false
647 }
648 } else {
649 false
650 };
651
652 if dirs_to_refresh.is_empty() && !git_index_changed {
653 return false;
654 }
655
656 if let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) {
658 for node_id in dirs_to_refresh {
659 let tree = explorer.tree_mut();
660 if let Err(e) = runtime.block_on(tree.refresh_node(node_id)) {
661 tracing::warn!("Failed to refresh directory: {}", e);
662 }
663 }
664 }
665
666 true
667 }
668
669 fn resolve_git_index(&self) -> Option<PathBuf> {
673 let spawner = &self.process_spawner;
674 let cwd = self.working_dir.to_string_lossy().to_string();
675
676 let result = if let Some(ref rt) = self.tokio_runtime {
680 rt.block_on(spawner.spawn(
681 "git".to_string(),
682 vec!["rev-parse".to_string(), "--git-dir".to_string()],
683 Some(cwd),
684 ))
685 } else {
686 return None;
689 };
690
691 let output = result.ok()?;
692 if output.exit_code != 0 {
693 return None;
694 }
695 let git_dir = output.stdout.trim();
696 let git_dir_path = if std::path::Path::new(git_dir).is_absolute() {
697 PathBuf::from(git_dir)
698 } else {
699 self.working_dir.join(git_dir)
700 };
701 Some(git_dir_path.join("index"))
702 }
703
704 pub(crate) fn notify_lsp_file_opened(
707 &mut self,
708 path: &Path,
709 buffer_id: BufferId,
710 metadata: &mut BufferMetadata,
711 ) {
712 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
714 tracing::debug!("No buffer state for file: {}", path.display());
715 return;
716 };
717
718 let Some(uri) = metadata.file_uri().cloned() else {
719 tracing::warn!(
720 "No URI in metadata for file: {} (failed to compute absolute path)",
721 path.display()
722 );
723 return;
724 };
725
726 let file_size = self
728 .filesystem
729 .metadata(path)
730 .ok()
731 .map(|m| m.size)
732 .unwrap_or(0);
733 if file_size > self.config.editor.large_file_threshold_bytes {
734 let reason = format!("File too large ({} bytes)", file_size);
735 tracing::warn!(
736 "Skipping LSP for large file: {} ({})",
737 path.display(),
738 reason
739 );
740 metadata.disable_lsp(reason);
741 return;
742 }
743
744 let text = match self
746 .buffers
747 .get(&buffer_id)
748 .and_then(|state| state.buffer.to_string())
749 {
750 Some(t) => t,
751 None => {
752 tracing::debug!("Buffer not fully loaded for LSP notification");
753 return;
754 }
755 };
756
757 let enable_inlay_hints = self.config.editor.enable_inlay_hints;
758 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
759
760 let (last_line, last_char, buffer_version) = self
762 .buffers
763 .get(&buffer_id)
764 .map(|state| {
765 let line_count = state.buffer.line_count().unwrap_or(1000);
766 (
767 line_count.saturating_sub(1) as u32,
768 10000u32,
769 state.buffer.version(),
770 )
771 })
772 .unwrap_or((999, 10000, 0));
773
774 let Some(lsp) = &mut self.lsp else {
776 tracing::debug!("No LSP manager available");
777 return;
778 };
779
780 tracing::debug!("LSP manager available for file: {}", path.display());
781 tracing::debug!(
782 "Detected language: {} for file: {}",
783 language,
784 path.display()
785 );
786 tracing::debug!("Using URI from metadata: {}", uri.as_str());
787 tracing::debug!("Attempting to spawn LSP client for language: {}", language);
788
789 match lsp.try_spawn(&language, Some(path)) {
790 LspSpawnResult::Spawned => {
791 for sh in lsp.get_handles_mut(&language) {
796 tracing::info!("Sending didOpen to LSP '{}' for: {}", sh.name, uri.as_str());
797 if let Err(e) = sh
798 .handle
799 .did_open(uri.clone(), text.clone(), language.clone())
800 {
801 tracing::warn!("Failed to send didOpen to LSP '{}': {}", sh.name, e);
802 } else {
803 metadata.lsp_opened_with.insert(sh.handle.id());
804 }
805 }
806
807 if let Some(client) = lsp.get_handle_mut(&language) {
809 let request_id = self.next_lsp_request_id;
810 self.next_lsp_request_id += 1;
811 if let Err(e) =
812 client.document_diagnostic(request_id, uri.clone(), previous_result_id)
813 {
814 tracing::debug!(
815 "Failed to request pull diagnostics (server may not support): {}",
816 e
817 );
818 } else {
819 tracing::info!(
820 "Requested pull diagnostics for {} (request_id={})",
821 uri.as_str(),
822 request_id
823 );
824 }
825
826 if enable_inlay_hints {
828 let request_id = self.next_lsp_request_id;
829 self.next_lsp_request_id += 1;
830
831 if let Err(e) =
832 client.inlay_hints(request_id, uri.clone(), 0, 0, last_line, last_char)
833 {
834 tracing::debug!(
835 "Failed to request inlay hints (server may not support): {}",
836 e
837 );
838 } else {
839 self.pending_inlay_hints_requests.insert(
840 request_id,
841 super::InlayHintsRequest {
842 buffer_id,
843 version: buffer_version,
844 },
845 );
846 tracing::info!(
847 "Requested inlay hints for {} (request_id={})",
848 uri.as_str(),
849 request_id
850 );
851 }
852 }
853 }
854
855 self.schedule_folding_ranges_refresh(buffer_id);
857 }
858 LspSpawnResult::NotAutoStart => {
859 tracing::debug!(
860 "LSP for {} not auto-starting (auto_start=false). Use command palette to start manually.",
861 language
862 );
863 }
864 LspSpawnResult::NotConfigured => {
865 tracing::debug!("No LSP server configured for language: {}", language);
866 }
867 LspSpawnResult::Failed => {
868 tracing::warn!("Failed to spawn LSP client for language: {}", language);
869 }
870 }
871 }
872
873 pub(crate) fn watch_file(&mut self, path: &Path) {
876 if let Ok(metadata) = self.filesystem.metadata(path) {
878 if let Some(mtime) = metadata.modified {
879 self.file_mod_times.insert(path.to_path_buf(), mtime);
880 }
881 }
882 }
883
884 pub(crate) fn notify_lsp_file_changed(&mut self, path: &Path) {
886 use crate::services::lsp::manager::LspSpawnResult;
887
888 let Some(lsp_uri) = super::types::file_path_to_lsp_uri(path) else {
889 return;
890 };
891
892 let Some((buffer_id, content, language)) = self
894 .buffers
895 .iter()
896 .find(|(_, s)| s.buffer.file_path() == Some(path))
897 .and_then(|(id, state)| {
898 state
899 .buffer
900 .to_string()
901 .map(|t| (*id, t, state.language.clone()))
902 })
903 else {
904 return;
905 };
906
907 let spawn_result = {
909 let Some(lsp) = self.lsp.as_mut() else {
910 return;
911 };
912 lsp.try_spawn(&language, Some(path))
913 };
914
915 if spawn_result != LspSpawnResult::Spawned {
917 return;
918 }
919
920 {
922 let opened_with = self
923 .buffer_metadata
924 .get(&buffer_id)
925 .map(|m| m.lsp_opened_with.clone())
926 .unwrap_or_default();
927
928 if let Some(lsp) = self.lsp.as_mut() {
929 for sh in lsp.get_handles_mut(&language) {
930 if opened_with.contains(&sh.handle.id()) {
931 continue;
932 }
933 if let Err(e) =
934 sh.handle
935 .did_open(lsp_uri.clone(), content.clone(), language.clone())
936 {
937 tracing::warn!(
938 "Failed to send didOpen to LSP '{}' before didChange: {}",
939 sh.name,
940 e
941 );
942 } else {
943 tracing::debug!(
944 "Sent didOpen for {} to LSP '{}' before file change notification",
945 lsp_uri.as_str(),
946 sh.name
947 );
948 }
949 }
950 }
951
952 if let Some(lsp) = self.lsp.as_ref() {
954 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
955 for sh in lsp.get_handles(&language) {
956 metadata.lsp_opened_with.insert(sh.handle.id());
957 }
958 }
959 }
960 }
961
962 if let Some(lsp) = &mut self.lsp {
964 let content_change = TextDocumentContentChangeEvent {
965 range: None, range_length: None,
967 text: content,
968 };
969 for sh in lsp.get_handles_mut(&language) {
970 if let Err(e) = sh
971 .handle
972 .did_change(lsp_uri.clone(), vec![content_change.clone()])
973 {
974 tracing::warn!("Failed to notify LSP '{}' of file change: {}", sh.name, e);
975 }
976 }
977 }
978 }
979
980 pub(crate) fn revert_buffer_by_id(
986 &mut self,
987 buffer_id: BufferId,
988 path: &Path,
989 ) -> anyhow::Result<()> {
990 let old_cursors = self
994 .split_view_states
995 .values()
996 .find_map(|vs| {
997 if vs.keyed_states.contains_key(&buffer_id) {
998 vs.keyed_states.get(&buffer_id).map(|bs| bs.cursors.clone())
999 } else {
1000 None
1001 }
1002 })
1003 .unwrap_or_default();
1004 let (old_buffer_settings, old_editing_disabled) = self
1005 .buffers
1006 .get(&buffer_id)
1007 .map(|s| (s.buffer_settings.clone(), s.editing_disabled))
1008 .unwrap_or_default();
1009
1010 let mut new_state = EditorState::from_file_with_languages(
1012 path,
1013 self.terminal_width,
1014 self.terminal_height,
1015 self.config.editor.large_file_threshold_bytes as usize,
1016 &self.grammar_registry,
1017 &self.config.languages,
1018 std::sync::Arc::clone(&self.filesystem),
1019 )?;
1020
1021 let new_file_size = new_state.buffer.len();
1023
1024 let mut restored_cursors = old_cursors;
1026 restored_cursors.map(|cursor| {
1027 cursor.position = cursor.position.min(new_file_size);
1028 cursor.clear_selection();
1029 });
1030 new_state.buffer_settings = old_buffer_settings;
1032 new_state.editing_disabled = old_editing_disabled;
1033 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1037 *state = new_state;
1038 }
1039
1040 for vs in self.split_view_states.values_mut() {
1042 if let Some(buf_state) = vs.keyed_states.get_mut(&buffer_id) {
1043 buf_state.cursors = restored_cursors.clone();
1044 }
1045 }
1046
1047 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
1049 *event_log = EventLog::new();
1050 }
1051
1052 self.seen_byte_ranges.remove(&buffer_id);
1054
1055 if let Ok(metadata) = self.filesystem.metadata(path) {
1057 if let Some(mtime) = metadata.modified {
1058 self.file_mod_times.insert(path.to_path_buf(), mtime);
1059 }
1060 }
1061
1062 self.notify_lsp_file_changed(path);
1064
1065 Ok(())
1066 }
1067
1068 pub fn handle_file_changed(&mut self, changed_path: &str) {
1070 let path = PathBuf::from(changed_path);
1071
1072 let buffer_ids: Vec<BufferId> = self
1074 .buffers
1075 .iter()
1076 .filter(|(_, state)| state.buffer.file_path() == Some(&path))
1077 .map(|(id, _)| *id)
1078 .collect();
1079
1080 if buffer_ids.is_empty() {
1081 return;
1082 }
1083
1084 for buffer_id in buffer_ids {
1085 if self.terminal_buffers.contains_key(&buffer_id) {
1088 continue;
1089 }
1090
1091 let state = match self.buffers.get(&buffer_id) {
1092 Some(s) => s,
1093 None => continue,
1094 };
1095
1096 let current_mtime = match self
1100 .filesystem
1101 .metadata(&path)
1102 .ok()
1103 .and_then(|m| m.modified)
1104 {
1105 Some(mtime) => mtime,
1106 None => continue, };
1108
1109 let dominated_by_stored = self
1110 .file_mod_times
1111 .get(&path)
1112 .map(|stored| current_mtime <= *stored)
1113 .unwrap_or(false);
1114
1115 if dominated_by_stored {
1116 continue;
1117 }
1118
1119 if state.buffer.is_modified() {
1121 self.status_message = Some(format!(
1122 "File {} changed on disk (buffer has unsaved changes)",
1123 path.display()
1124 ));
1125 continue;
1126 }
1127
1128 if self.auto_revert_enabled {
1130 let still_needs_revert = self
1134 .file_mod_times
1135 .get(&path)
1136 .map(|stored| current_mtime > *stored)
1137 .unwrap_or(true);
1138
1139 if !still_needs_revert {
1140 continue;
1141 }
1142
1143 let is_active_buffer = buffer_id == self.active_buffer();
1145
1146 if is_active_buffer {
1147 if let Err(e) = self.revert_file() {
1149 tracing::error!("Failed to auto-revert file {:?}: {}", path, e);
1150 } else {
1151 tracing::info!("Auto-reverted file: {:?}", path);
1152 }
1153 } else {
1154 if let Err(e) = self.revert_buffer_by_id(buffer_id, &path) {
1157 tracing::error!("Failed to auto-revert background file {:?}: {}", path, e);
1158 } else {
1159 tracing::info!("Auto-reverted file: {:?}", path);
1160 }
1161 }
1162
1163 self.watch_file(&path);
1165 }
1166 }
1167 }
1168
1169 pub fn check_save_conflict(&self) -> Option<std::time::SystemTime> {
1172 let path = self.active_state().buffer.file_path()?;
1173
1174 let current_mtime = self
1176 .filesystem
1177 .metadata(path)
1178 .ok()
1179 .and_then(|m| m.modified)?;
1180
1181 match self.file_mod_times.get(path) {
1183 Some(recorded_mtime) if current_mtime > *recorded_mtime => {
1184 Some(current_mtime)
1186 }
1187 _ => None,
1188 }
1189 }
1190}