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 detected =
101 crate::primitives::detected_language::DetectedLanguage::from_path(
102 p,
103 &self.grammar_registry,
104 &self.config.languages,
105 );
106 state.apply_language(detected);
107 }
108 }
109 }
110
111 if !silent {
112 self.status_message = Some(t!("status.file_saved").to_string());
113 }
114
115 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
117 event_log.mark_saved();
118 }
119
120 if let Some(ref p) = path {
122 if let Ok(metadata) = self.filesystem.metadata(p) {
123 if let Some(mtime) = metadata.modified {
124 self.file_mod_times.insert(p.clone(), mtime);
125 }
126 }
127 }
128
129 self.notify_lsp_save_buffer(buffer_id);
131
132 if let Err(e) = self.delete_buffer_recovery(buffer_id) {
134 tracing::warn!("Failed to delete recovery file: {}", e);
135 }
136
137 if let Some(ref p) = path {
139 self.emit_event(
140 crate::model::control_event::events::FILE_SAVED.name,
141 serde_json::json!({
142 "path": p.display().to_string()
143 }),
144 );
145 }
146
147 if let Some(ref p) = path {
149 self.plugin_manager.run_hook(
150 "after_file_save",
151 crate::services::plugins::hooks::HookArgs::AfterFileSave {
152 buffer_id,
153 path: p.clone(),
154 },
155 );
156 }
157
158 if !silent {
164 match self.run_on_save_actions() {
165 Ok(true) => {
166 if self.status_message.as_deref() == Some(&t!("status.file_saved")) {
169 self.status_message =
170 Some(t!("status.file_saved_with_actions").to_string());
171 }
172 }
174 Ok(false) => {
175 }
177 Err(e) => {
178 self.status_message = Some(e);
180 }
181 }
182 }
183
184 Ok(())
185 }
186
187 pub fn auto_save_persistent_buffers(&mut self) -> anyhow::Result<usize> {
190 if !self.config.editor.auto_save_enabled {
191 return Ok(0);
192 }
193
194 let interval =
196 std::time::Duration::from_secs(self.config.editor.auto_save_interval_secs as u64);
197 if self
198 .time_source
199 .elapsed_since(self.last_persistent_auto_save)
200 < interval
201 {
202 return Ok(0);
203 }
204
205 self.last_persistent_auto_save = self.time_source.now();
206
207 let mut to_save = Vec::new();
209 for (id, state) in &self.buffers {
210 if state.buffer.is_modified() {
211 if let Some(path) = state.buffer.file_path() {
212 to_save.push((*id, path.to_path_buf()));
213 }
214 }
215 }
216
217 let mut count = 0;
218 for (id, path) in to_save {
219 if let Some(state) = self.buffers.get_mut(&id) {
220 match state.buffer.save() {
221 Ok(()) => {
222 self.finalize_save_buffer(id, Some(path), true)?;
223 count += 1;
224 }
225 Err(e) => {
226 if e.downcast_ref::<SudoSaveRequired>().is_some() {
228 tracing::debug!(
229 "Auto-save skipped for {:?} (sudo required)",
230 path.display()
231 );
232 } else {
233 tracing::warn!("Auto-save failed for {:?}: {}", path.display(), e);
234 }
235 }
236 }
237 }
238 }
239
240 Ok(count)
241 }
242
243 pub fn save_all_on_exit(&mut self) -> anyhow::Result<usize> {
247 let mut to_save = Vec::new();
248 for (id, state) in &self.buffers {
249 if state.buffer.is_modified() {
250 if let Some(path) = state.buffer.file_path() {
251 if !path.as_os_str().is_empty() {
252 to_save.push((*id, path.to_path_buf()));
253 }
254 }
255 }
256 }
257
258 let mut count = 0;
259 for (id, path) in to_save {
260 if let Some(state) = self.buffers.get_mut(&id) {
261 match state.buffer.save() {
262 Ok(()) => {
263 self.finalize_save_buffer(id, Some(path), true)?;
264 count += 1;
265 }
266 Err(e) => {
267 if e.downcast_ref::<SudoSaveRequired>().is_some() {
268 tracing::debug!(
269 "Auto-save on exit skipped for {} (sudo required)",
270 path.display()
271 );
272 } else {
273 tracing::warn!(
274 "Auto-save on exit failed for {}: {}",
275 path.display(),
276 e
277 );
278 }
279 }
280 }
281 }
282 }
283
284 Ok(count)
285 }
286
287 pub fn revert_file(&mut self) -> anyhow::Result<bool> {
290 let path = match self.active_state().buffer.file_path() {
291 Some(p) => p.to_path_buf(),
292 None => {
293 self.status_message = Some(t!("status.no_file_to_revert").to_string());
294 return Ok(false);
295 }
296 };
297
298 if !path.exists() {
299 self.status_message =
300 Some(t!("status.file_not_exists", path = path.display().to_string()).to_string());
301 return Ok(false);
302 }
303
304 let active_split = self.split_manager.active_split();
306 let (old_top_byte, old_left_column) = self
307 .split_view_states
308 .get(&active_split)
309 .map(|vs| (vs.viewport.top_byte, vs.viewport.left_column))
310 .unwrap_or((0, 0));
311 let old_cursors = self.active_cursors().clone();
312
313 let old_buffer_settings = self.active_state().buffer_settings.clone();
315 let old_editing_disabled = self.active_state().editing_disabled;
316
317 let mut new_state = EditorState::from_file_with_languages(
319 &path,
320 self.terminal_width,
321 self.terminal_height,
322 self.config.editor.large_file_threshold_bytes as usize,
323 &self.grammar_registry,
324 &self.config.languages,
325 std::sync::Arc::clone(&self.filesystem),
326 )?;
327
328 let new_file_size = new_state.buffer.len();
330 let mut restored_cursors = old_cursors;
331 restored_cursors.map(|cursor| {
332 cursor.position = cursor.position.min(new_file_size);
333 cursor.clear_selection();
335 });
336 new_state.buffer_settings = old_buffer_settings;
338 new_state.editing_disabled = old_editing_disabled;
339 let buffer_id = self.active_buffer();
343 if let Some(state) = self.buffers.get_mut(&buffer_id) {
344 *state = new_state;
345 }
347
348 let active_split = self.split_manager.active_split();
350 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
351 view_state.cursors = restored_cursors;
352 }
353
354 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
356 view_state.viewport.top_byte = old_top_byte.min(new_file_size);
357 view_state.viewport.left_column = old_left_column;
358 }
359
360 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
362 *event_log = EventLog::new();
363 }
364
365 self.seen_byte_ranges.remove(&buffer_id);
367
368 if let Ok(metadata) = self.filesystem.metadata(&path) {
370 if let Some(mtime) = metadata.modified {
371 self.file_mod_times.insert(path.clone(), mtime);
372 }
373 }
374
375 self.notify_lsp_file_changed(&path);
377
378 self.status_message = Some(t!("status.reverted").to_string());
379 Ok(true)
380 }
381
382 pub fn toggle_auto_revert(&mut self) {
384 self.auto_revert_enabled = !self.auto_revert_enabled;
385
386 if self.auto_revert_enabled {
387 self.status_message = Some(t!("status.auto_revert_enabled").to_string());
388 } else {
389 self.status_message = Some(t!("status.auto_revert_disabled").to_string());
390 }
391 }
392
393 pub fn poll_file_changes(&mut self) -> bool {
402 if !self.auto_revert_enabled {
404 return false;
405 }
406
407 let mut any_changed = false;
409 if let Some(ref rx) = self.pending_file_poll_rx {
410 match rx.try_recv() {
411 Ok(results) => {
412 self.pending_file_poll_rx = None;
413 any_changed = self.process_file_poll_results(results);
414 }
415 Err(std::sync::mpsc::TryRecvError::Empty) => {
416 return false;
418 }
419 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
420 self.pending_file_poll_rx = None;
422 }
423 }
424 }
425
426 let poll_interval =
428 std::time::Duration::from_millis(self.config.editor.auto_revert_poll_interval_ms);
429 let elapsed = self.time_source.elapsed_since(self.last_auto_revert_poll);
430 tracing::trace!(
431 "poll_file_changes: elapsed={:?}, poll_interval={:?}",
432 elapsed,
433 poll_interval
434 );
435 if elapsed < poll_interval {
436 return any_changed;
437 }
438 self.last_auto_revert_poll = self.time_source.now();
439
440 let files_to_check: Vec<PathBuf> = self
442 .buffers
443 .values()
444 .filter_map(|state| state.buffer.file_path().map(PathBuf::from))
445 .collect();
446
447 if files_to_check.is_empty() {
448 return any_changed;
449 }
450
451 let (tx, rx) = std::sync::mpsc::channel();
453 let fs = self.filesystem.clone();
454 std::thread::Builder::new()
455 .name("poll-file-changes".to_string())
456 .spawn(move || {
457 let results: Vec<(PathBuf, Option<std::time::SystemTime>)> = files_to_check
458 .into_iter()
459 .map(|path| {
460 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
461 (path, mtime)
462 })
463 .collect();
464 if tx.send(results).is_err() {}
467 })
468 .ok();
469 self.pending_file_poll_rx = Some(rx);
470
471 any_changed
472 }
473
474 fn process_file_poll_results(
476 &mut self,
477 results: Vec<(PathBuf, Option<std::time::SystemTime>)>,
478 ) -> bool {
479 let mut any_changed = false;
480 for (path, mtime_opt) in results {
481 let Some(current_mtime) = mtime_opt else {
482 continue;
483 };
484
485 if let Some(&stored_mtime) = self.file_mod_times.get(&path) {
486 if current_mtime != stored_mtime {
487 let path_str = path.display().to_string();
488 if self.handle_async_file_changed(path_str) {
489 any_changed = true;
490 }
491 }
492 } else {
493 self.file_mod_times.insert(path, current_mtime);
495 }
496 }
497 any_changed
498 }
499
500 pub fn poll_file_tree_changes(&mut self) -> bool {
508 use crate::view::file_tree::NodeId;
509
510 let mut any_refreshed = false;
512 if let Some(ref rx) = self.pending_dir_poll_rx {
513 match rx.try_recv() {
514 Ok((dir_results, git_index_mtime)) => {
515 self.pending_dir_poll_rx = None;
516 any_refreshed = self.process_dir_poll_results(dir_results, git_index_mtime);
517 }
518 Err(std::sync::mpsc::TryRecvError::Empty) => {
519 return false;
520 }
521 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
522 self.pending_dir_poll_rx = None;
523 }
524 }
525 }
526
527 let poll_interval =
529 std::time::Duration::from_millis(self.config.editor.file_tree_poll_interval_ms);
530 if self.time_source.elapsed_since(self.last_file_tree_poll) < poll_interval {
531 return any_refreshed;
532 }
533 self.last_file_tree_poll = self.time_source.now();
534
535 if !self.git_index_resolved {
539 self.git_index_resolved = true;
540 if let Some(path) = self.resolve_git_index() {
541 if let Ok(meta) = self.filesystem.metadata(&path) {
542 if let Some(mtime) = meta.modified {
543 self.dir_mod_times.insert(path, mtime);
544 }
545 }
546 }
547 }
548
549 let Some(explorer) = &self.file_explorer else {
551 return any_refreshed;
552 };
553
554 let expanded_dirs: Vec<(NodeId, PathBuf)> = explorer
556 .tree()
557 .all_nodes()
558 .filter(|node| node.is_dir() && node.is_expanded())
559 .map(|node| (node.id, node.entry.path.clone()))
560 .collect();
561
562 let git_index_path: Option<PathBuf> = self
564 .dir_mod_times
565 .keys()
566 .find(|p| p.ends_with(".git/index") || p.ends_with(".git\\index"))
567 .cloned();
568
569 if expanded_dirs.is_empty() && git_index_path.is_none() {
570 return any_refreshed;
571 }
572
573 let (tx, rx) = std::sync::mpsc::channel();
575 let fs = self.filesystem.clone();
576 std::thread::Builder::new()
577 .name("poll-dir-changes".to_string())
578 .spawn(move || {
579 let results: Vec<(NodeId, PathBuf, Option<std::time::SystemTime>)> = expanded_dirs
580 .into_iter()
581 .map(|(node_id, path)| {
582 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
583 (node_id, path, mtime)
584 })
585 .collect();
586
587 let git_index_mtime = git_index_path.and_then(|path| {
589 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
590 Some((path, mtime?))
591 });
592
593 if tx.send((results, git_index_mtime)).is_err() {}
595 })
596 .ok();
597 self.pending_dir_poll_rx = Some(rx);
598
599 any_refreshed
600 }
601
602 fn process_dir_poll_results(
604 &mut self,
605 results: Vec<(
606 crate::view::file_tree::NodeId,
607 PathBuf,
608 Option<std::time::SystemTime>,
609 )>,
610 git_index_mtime: Option<(PathBuf, std::time::SystemTime)>,
611 ) -> bool {
612 let mut dirs_to_refresh = Vec::new();
613
614 for (node_id, path, mtime_opt) in results {
615 let Some(current_mtime) = mtime_opt else {
616 continue;
617 };
618
619 if let Some(&stored_mtime) = self.dir_mod_times.get(&path) {
620 if current_mtime != stored_mtime {
621 self.dir_mod_times.insert(path.clone(), current_mtime);
622 dirs_to_refresh.push(node_id);
623 tracing::debug!("Directory changed: {:?}", path);
624 }
625 } else {
626 self.dir_mod_times.insert(path, current_mtime);
627 }
628 }
629
630 let git_index_changed = if let Some((path, current_mtime)) = git_index_mtime {
632 if let Some(&stored_mtime) = self.dir_mod_times.get(&path) {
633 if current_mtime != stored_mtime {
634 self.dir_mod_times.insert(path, current_mtime);
635 self.plugin_manager.run_hook(
636 "focus_gained",
637 crate::services::plugins::hooks::HookArgs::FocusGained,
638 );
639 true
640 } else {
641 false
642 }
643 } else {
644 false
645 }
646 } else {
647 false
648 };
649
650 if dirs_to_refresh.is_empty() && !git_index_changed {
651 return false;
652 }
653
654 if let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) {
656 for node_id in dirs_to_refresh {
657 let tree = explorer.tree_mut();
658 if let Err(e) = runtime.block_on(tree.refresh_node(node_id)) {
659 tracing::warn!("Failed to refresh directory: {}", e);
660 }
661 }
662 }
663
664 true
665 }
666
667 fn resolve_git_index(&self) -> Option<PathBuf> {
671 let spawner = &self.process_spawner;
672 let cwd = self.working_dir.to_string_lossy().to_string();
673
674 let result = if let Some(ref rt) = self.tokio_runtime {
678 rt.block_on(spawner.spawn(
679 "git".to_string(),
680 vec!["rev-parse".to_string(), "--git-dir".to_string()],
681 Some(cwd),
682 ))
683 } else {
684 return None;
687 };
688
689 let output = result.ok()?;
690 if output.exit_code != 0 {
691 return None;
692 }
693 let git_dir = output.stdout.trim();
694 let git_dir_path = if std::path::Path::new(git_dir).is_absolute() {
695 PathBuf::from(git_dir)
696 } else {
697 self.working_dir.join(git_dir)
698 };
699 Some(git_dir_path.join("index"))
700 }
701
702 pub(crate) fn notify_lsp_file_opened(
705 &mut self,
706 path: &Path,
707 buffer_id: BufferId,
708 metadata: &mut BufferMetadata,
709 ) {
710 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
712 tracing::debug!("No buffer state for file: {}", path.display());
713 return;
714 };
715
716 let Some(uri) = metadata.file_uri().cloned() else {
717 tracing::warn!(
718 "No URI in metadata for file: {} (failed to compute absolute path)",
719 path.display()
720 );
721 return;
722 };
723
724 let file_size = self
726 .filesystem
727 .metadata(path)
728 .ok()
729 .map(|m| m.size)
730 .unwrap_or(0);
731 if file_size > self.config.editor.large_file_threshold_bytes {
732 let reason = format!("File too large ({} bytes)", file_size);
733 tracing::warn!(
734 "Skipping LSP for large file: {} ({})",
735 path.display(),
736 reason
737 );
738 metadata.disable_lsp(reason);
739 return;
740 }
741
742 let text = match self
744 .buffers
745 .get(&buffer_id)
746 .and_then(|state| state.buffer.to_string())
747 {
748 Some(t) => t,
749 None => {
750 tracing::debug!("Buffer not fully loaded for LSP notification");
751 return;
752 }
753 };
754
755 let enable_inlay_hints = self.config.editor.enable_inlay_hints;
756 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
757
758 let (last_line, last_char) = self
760 .buffers
761 .get(&buffer_id)
762 .map(|state| {
763 let line_count = state.buffer.line_count().unwrap_or(1000);
764 (line_count.saturating_sub(1) as u32, 10000u32)
765 })
766 .unwrap_or((999, 10000));
767
768 let Some(lsp) = &mut self.lsp else {
770 tracing::debug!("No LSP manager available");
771 return;
772 };
773
774 tracing::debug!("LSP manager available for file: {}", path.display());
775 tracing::debug!(
776 "Detected language: {} for file: {}",
777 language,
778 path.display()
779 );
780 tracing::debug!("Using URI from metadata: {}", uri.as_str());
781 tracing::debug!("Attempting to spawn LSP client for language: {}", language);
782
783 match lsp.try_spawn(&language, Some(path)) {
784 LspSpawnResult::Spawned => {
785 for sh in lsp.get_handles_mut(&language) {
790 tracing::info!("Sending didOpen to LSP '{}' for: {}", sh.name, uri.as_str());
791 if let Err(e) = sh
792 .handle
793 .did_open(uri.clone(), text.clone(), language.clone())
794 {
795 tracing::warn!("Failed to send didOpen to LSP '{}': {}", sh.name, e);
796 } else {
797 metadata.lsp_opened_with.insert(sh.handle.id());
798 }
799 }
800
801 if let Some(client) = lsp.get_handle_mut(&language) {
803 let request_id = self.next_lsp_request_id;
804 self.next_lsp_request_id += 1;
805 if let Err(e) =
806 client.document_diagnostic(request_id, uri.clone(), previous_result_id)
807 {
808 tracing::debug!(
809 "Failed to request pull diagnostics (server may not support): {}",
810 e
811 );
812 } else {
813 tracing::info!(
814 "Requested pull diagnostics for {} (request_id={})",
815 uri.as_str(),
816 request_id
817 );
818 }
819
820 if enable_inlay_hints {
822 let request_id = self.next_lsp_request_id;
823 self.next_lsp_request_id += 1;
824 self.pending_inlay_hints_request = Some(request_id);
825
826 if let Err(e) =
827 client.inlay_hints(request_id, uri.clone(), 0, 0, last_line, last_char)
828 {
829 tracing::debug!(
830 "Failed to request inlay hints (server may not support): {}",
831 e
832 );
833 self.pending_inlay_hints_request = None;
834 } else {
835 tracing::info!(
836 "Requested inlay hints for {} (request_id={})",
837 uri.as_str(),
838 request_id
839 );
840 }
841 }
842 }
843
844 self.schedule_folding_ranges_refresh(buffer_id);
846 }
847 LspSpawnResult::NotAutoStart => {
848 tracing::debug!(
849 "LSP for {} not auto-starting (auto_start=false). Use command palette to start manually.",
850 language
851 );
852 }
853 LspSpawnResult::NotConfigured => {
854 tracing::debug!("No LSP server configured for language: {}", language);
855 }
856 LspSpawnResult::Failed => {
857 tracing::warn!("Failed to spawn LSP client for language: {}", language);
858 }
859 }
860 }
861
862 pub(crate) fn watch_file(&mut self, path: &Path) {
865 if let Ok(metadata) = self.filesystem.metadata(path) {
867 if let Some(mtime) = metadata.modified {
868 self.file_mod_times.insert(path.to_path_buf(), mtime);
869 }
870 }
871 }
872
873 pub(crate) fn notify_lsp_file_changed(&mut self, path: &Path) {
875 use crate::services::lsp::manager::LspSpawnResult;
876
877 let Some(lsp_uri) = super::types::file_path_to_lsp_uri(path) else {
878 return;
879 };
880
881 let Some((buffer_id, content, language)) = self
883 .buffers
884 .iter()
885 .find(|(_, s)| s.buffer.file_path() == Some(path))
886 .and_then(|(id, state)| {
887 state
888 .buffer
889 .to_string()
890 .map(|t| (*id, t, state.language.clone()))
891 })
892 else {
893 return;
894 };
895
896 let spawn_result = {
898 let Some(lsp) = self.lsp.as_mut() else {
899 return;
900 };
901 lsp.try_spawn(&language, Some(path))
902 };
903
904 if spawn_result != LspSpawnResult::Spawned {
906 return;
907 }
908
909 {
911 let opened_with = self
912 .buffer_metadata
913 .get(&buffer_id)
914 .map(|m| m.lsp_opened_with.clone())
915 .unwrap_or_default();
916
917 if let Some(lsp) = self.lsp.as_mut() {
918 for sh in lsp.get_handles_mut(&language) {
919 if opened_with.contains(&sh.handle.id()) {
920 continue;
921 }
922 if let Err(e) =
923 sh.handle
924 .did_open(lsp_uri.clone(), content.clone(), language.clone())
925 {
926 tracing::warn!(
927 "Failed to send didOpen to LSP '{}' before didChange: {}",
928 sh.name,
929 e
930 );
931 } else {
932 tracing::debug!(
933 "Sent didOpen for {} to LSP '{}' before file change notification",
934 lsp_uri.as_str(),
935 sh.name
936 );
937 }
938 }
939 }
940
941 if let Some(lsp) = self.lsp.as_ref() {
943 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
944 for sh in lsp.get_handles(&language) {
945 metadata.lsp_opened_with.insert(sh.handle.id());
946 }
947 }
948 }
949 }
950
951 if let Some(lsp) = &mut self.lsp {
953 let content_change = TextDocumentContentChangeEvent {
954 range: None, range_length: None,
956 text: content,
957 };
958 for sh in lsp.get_handles_mut(&language) {
959 if let Err(e) = sh
960 .handle
961 .did_change(lsp_uri.clone(), vec![content_change.clone()])
962 {
963 tracing::warn!("Failed to notify LSP '{}' of file change: {}", sh.name, e);
964 }
965 }
966 }
967 }
968
969 pub(crate) fn revert_buffer_by_id(
975 &mut self,
976 buffer_id: BufferId,
977 path: &Path,
978 ) -> anyhow::Result<()> {
979 let old_cursors = self
983 .split_view_states
984 .values()
985 .find_map(|vs| {
986 if vs.keyed_states.contains_key(&buffer_id) {
987 vs.keyed_states.get(&buffer_id).map(|bs| bs.cursors.clone())
988 } else {
989 None
990 }
991 })
992 .unwrap_or_default();
993 let (old_buffer_settings, old_editing_disabled) = self
994 .buffers
995 .get(&buffer_id)
996 .map(|s| (s.buffer_settings.clone(), s.editing_disabled))
997 .unwrap_or_default();
998
999 let mut new_state = EditorState::from_file_with_languages(
1001 path,
1002 self.terminal_width,
1003 self.terminal_height,
1004 self.config.editor.large_file_threshold_bytes as usize,
1005 &self.grammar_registry,
1006 &self.config.languages,
1007 std::sync::Arc::clone(&self.filesystem),
1008 )?;
1009
1010 let new_file_size = new_state.buffer.len();
1012
1013 let mut restored_cursors = old_cursors;
1015 restored_cursors.map(|cursor| {
1016 cursor.position = cursor.position.min(new_file_size);
1017 cursor.clear_selection();
1018 });
1019 new_state.buffer_settings = old_buffer_settings;
1021 new_state.editing_disabled = old_editing_disabled;
1022 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1026 *state = new_state;
1027 }
1028
1029 for vs in self.split_view_states.values_mut() {
1031 if let Some(buf_state) = vs.keyed_states.get_mut(&buffer_id) {
1032 buf_state.cursors = restored_cursors.clone();
1033 }
1034 }
1035
1036 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
1038 *event_log = EventLog::new();
1039 }
1040
1041 self.seen_byte_ranges.remove(&buffer_id);
1043
1044 if let Ok(metadata) = self.filesystem.metadata(path) {
1046 if let Some(mtime) = metadata.modified {
1047 self.file_mod_times.insert(path.to_path_buf(), mtime);
1048 }
1049 }
1050
1051 self.notify_lsp_file_changed(path);
1053
1054 Ok(())
1055 }
1056
1057 pub fn handle_file_changed(&mut self, changed_path: &str) {
1059 let path = PathBuf::from(changed_path);
1060
1061 let buffer_ids: Vec<BufferId> = self
1063 .buffers
1064 .iter()
1065 .filter(|(_, state)| state.buffer.file_path() == Some(&path))
1066 .map(|(id, _)| *id)
1067 .collect();
1068
1069 if buffer_ids.is_empty() {
1070 return;
1071 }
1072
1073 for buffer_id in buffer_ids {
1074 if self.terminal_buffers.contains_key(&buffer_id) {
1077 continue;
1078 }
1079
1080 let state = match self.buffers.get(&buffer_id) {
1081 Some(s) => s,
1082 None => continue,
1083 };
1084
1085 let current_mtime = match self
1089 .filesystem
1090 .metadata(&path)
1091 .ok()
1092 .and_then(|m| m.modified)
1093 {
1094 Some(mtime) => mtime,
1095 None => continue, };
1097
1098 let dominated_by_stored = self
1099 .file_mod_times
1100 .get(&path)
1101 .map(|stored| current_mtime <= *stored)
1102 .unwrap_or(false);
1103
1104 if dominated_by_stored {
1105 continue;
1106 }
1107
1108 if state.buffer.is_modified() {
1110 self.status_message = Some(format!(
1111 "File {} changed on disk (buffer has unsaved changes)",
1112 path.display()
1113 ));
1114 continue;
1115 }
1116
1117 if self.auto_revert_enabled {
1119 let still_needs_revert = self
1123 .file_mod_times
1124 .get(&path)
1125 .map(|stored| current_mtime > *stored)
1126 .unwrap_or(true);
1127
1128 if !still_needs_revert {
1129 continue;
1130 }
1131
1132 let is_active_buffer = buffer_id == self.active_buffer();
1134
1135 if is_active_buffer {
1136 if let Err(e) = self.revert_file() {
1138 tracing::error!("Failed to auto-revert file {:?}: {}", path, e);
1139 } else {
1140 tracing::info!("Auto-reverted file: {:?}", path);
1141 }
1142 } else {
1143 if let Err(e) = self.revert_buffer_by_id(buffer_id, &path) {
1146 tracing::error!("Failed to auto-revert background file {:?}: {}", path, e);
1147 } else {
1148 tracing::info!("Auto-reverted file: {:?}", path);
1149 }
1150 }
1151
1152 self.watch_file(&path);
1154 }
1155 }
1156 }
1157
1158 pub fn check_save_conflict(&self) -> Option<std::time::SystemTime> {
1161 let path = self.active_state().buffer.file_path()?;
1162
1163 let current_mtime = self
1165 .filesystem
1166 .metadata(path)
1167 .ok()
1168 .and_then(|m| m.modified)?;
1169
1170 match self.file_mod_times.get(path) {
1172 Some(recorded_mtime) if current_mtime > *recorded_mtime => {
1173 Some(current_mtime)
1175 }
1176 _ => None,
1177 }
1178 }
1179}