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, buffer_version) = self
760 .buffers
761 .get(&buffer_id)
762 .map(|state| {
763 let line_count = state.buffer.line_count().unwrap_or(1000);
764 (
765 line_count.saturating_sub(1) as u32,
766 10000u32,
767 state.buffer.version(),
768 )
769 })
770 .unwrap_or((999, 10000, 0));
771
772 let Some(lsp) = &mut self.lsp else {
774 tracing::debug!("No LSP manager available");
775 return;
776 };
777
778 tracing::debug!("LSP manager available for file: {}", path.display());
779 tracing::debug!(
780 "Detected language: {} for file: {}",
781 language,
782 path.display()
783 );
784 tracing::debug!("Using URI from metadata: {}", uri.as_str());
785 tracing::debug!("Attempting to spawn LSP client for language: {}", language);
786
787 match lsp.try_spawn(&language, Some(path)) {
788 LspSpawnResult::Spawned => {
789 for sh in lsp.get_handles_mut(&language) {
794 tracing::info!("Sending didOpen to LSP '{}' for: {}", sh.name, uri.as_str());
795 if let Err(e) = sh
796 .handle
797 .did_open(uri.clone(), text.clone(), language.clone())
798 {
799 tracing::warn!("Failed to send didOpen to LSP '{}': {}", sh.name, e);
800 } else {
801 metadata.lsp_opened_with.insert(sh.handle.id());
802 }
803 }
804
805 if let Some(client) = lsp.get_handle_mut(&language) {
807 let request_id = self.next_lsp_request_id;
808 self.next_lsp_request_id += 1;
809 if let Err(e) =
810 client.document_diagnostic(request_id, uri.clone(), previous_result_id)
811 {
812 tracing::debug!(
813 "Failed to request pull diagnostics (server may not support): {}",
814 e
815 );
816 } else {
817 tracing::info!(
818 "Requested pull diagnostics for {} (request_id={})",
819 uri.as_str(),
820 request_id
821 );
822 }
823
824 if enable_inlay_hints {
826 let request_id = self.next_lsp_request_id;
827 self.next_lsp_request_id += 1;
828
829 if let Err(e) =
830 client.inlay_hints(request_id, uri.clone(), 0, 0, last_line, last_char)
831 {
832 tracing::debug!(
833 "Failed to request inlay hints (server may not support): {}",
834 e
835 );
836 } else {
837 self.pending_inlay_hints_requests.insert(
838 request_id,
839 super::InlayHintsRequest {
840 buffer_id,
841 version: buffer_version,
842 },
843 );
844 tracing::info!(
845 "Requested inlay hints for {} (request_id={})",
846 uri.as_str(),
847 request_id
848 );
849 }
850 }
851 }
852
853 self.schedule_folding_ranges_refresh(buffer_id);
855 }
856 LspSpawnResult::NotAutoStart => {
857 tracing::debug!(
858 "LSP for {} not auto-starting (auto_start=false). Use command palette to start manually.",
859 language
860 );
861 }
862 LspSpawnResult::NotConfigured => {
863 tracing::debug!("No LSP server configured for language: {}", language);
864 }
865 LspSpawnResult::Failed => {
866 tracing::warn!("Failed to spawn LSP client for language: {}", language);
867 }
868 }
869 }
870
871 pub(crate) fn watch_file(&mut self, path: &Path) {
874 if let Ok(metadata) = self.filesystem.metadata(path) {
876 if let Some(mtime) = metadata.modified {
877 self.file_mod_times.insert(path.to_path_buf(), mtime);
878 }
879 }
880 }
881
882 pub(crate) fn notify_lsp_file_changed(&mut self, path: &Path) {
884 use crate::services::lsp::manager::LspSpawnResult;
885
886 let Some(lsp_uri) = super::types::file_path_to_lsp_uri(path) else {
887 return;
888 };
889
890 let Some((buffer_id, content, language)) = self
892 .buffers
893 .iter()
894 .find(|(_, s)| s.buffer.file_path() == Some(path))
895 .and_then(|(id, state)| {
896 state
897 .buffer
898 .to_string()
899 .map(|t| (*id, t, state.language.clone()))
900 })
901 else {
902 return;
903 };
904
905 let spawn_result = {
907 let Some(lsp) = self.lsp.as_mut() else {
908 return;
909 };
910 lsp.try_spawn(&language, Some(path))
911 };
912
913 if spawn_result != LspSpawnResult::Spawned {
915 return;
916 }
917
918 {
920 let opened_with = self
921 .buffer_metadata
922 .get(&buffer_id)
923 .map(|m| m.lsp_opened_with.clone())
924 .unwrap_or_default();
925
926 if let Some(lsp) = self.lsp.as_mut() {
927 for sh in lsp.get_handles_mut(&language) {
928 if opened_with.contains(&sh.handle.id()) {
929 continue;
930 }
931 if let Err(e) =
932 sh.handle
933 .did_open(lsp_uri.clone(), content.clone(), language.clone())
934 {
935 tracing::warn!(
936 "Failed to send didOpen to LSP '{}' before didChange: {}",
937 sh.name,
938 e
939 );
940 } else {
941 tracing::debug!(
942 "Sent didOpen for {} to LSP '{}' before file change notification",
943 lsp_uri.as_str(),
944 sh.name
945 );
946 }
947 }
948 }
949
950 if let Some(lsp) = self.lsp.as_ref() {
952 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
953 for sh in lsp.get_handles(&language) {
954 metadata.lsp_opened_with.insert(sh.handle.id());
955 }
956 }
957 }
958 }
959
960 if let Some(lsp) = &mut self.lsp {
962 let content_change = TextDocumentContentChangeEvent {
963 range: None, range_length: None,
965 text: content,
966 };
967 for sh in lsp.get_handles_mut(&language) {
968 if let Err(e) = sh
969 .handle
970 .did_change(lsp_uri.clone(), vec![content_change.clone()])
971 {
972 tracing::warn!("Failed to notify LSP '{}' of file change: {}", sh.name, e);
973 }
974 }
975 }
976 }
977
978 pub(crate) fn revert_buffer_by_id(
984 &mut self,
985 buffer_id: BufferId,
986 path: &Path,
987 ) -> anyhow::Result<()> {
988 let old_cursors = self
992 .split_view_states
993 .values()
994 .find_map(|vs| {
995 if vs.keyed_states.contains_key(&buffer_id) {
996 vs.keyed_states.get(&buffer_id).map(|bs| bs.cursors.clone())
997 } else {
998 None
999 }
1000 })
1001 .unwrap_or_default();
1002 let (old_buffer_settings, old_editing_disabled) = self
1003 .buffers
1004 .get(&buffer_id)
1005 .map(|s| (s.buffer_settings.clone(), s.editing_disabled))
1006 .unwrap_or_default();
1007
1008 let mut new_state = EditorState::from_file_with_languages(
1010 path,
1011 self.terminal_width,
1012 self.terminal_height,
1013 self.config.editor.large_file_threshold_bytes as usize,
1014 &self.grammar_registry,
1015 &self.config.languages,
1016 std::sync::Arc::clone(&self.filesystem),
1017 )?;
1018
1019 let new_file_size = new_state.buffer.len();
1021
1022 let mut restored_cursors = old_cursors;
1024 restored_cursors.map(|cursor| {
1025 cursor.position = cursor.position.min(new_file_size);
1026 cursor.clear_selection();
1027 });
1028 new_state.buffer_settings = old_buffer_settings;
1030 new_state.editing_disabled = old_editing_disabled;
1031 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1035 *state = new_state;
1036 }
1037
1038 for vs in self.split_view_states.values_mut() {
1040 if let Some(buf_state) = vs.keyed_states.get_mut(&buffer_id) {
1041 buf_state.cursors = restored_cursors.clone();
1042 }
1043 }
1044
1045 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
1047 *event_log = EventLog::new();
1048 }
1049
1050 self.seen_byte_ranges.remove(&buffer_id);
1052
1053 if let Ok(metadata) = self.filesystem.metadata(path) {
1055 if let Some(mtime) = metadata.modified {
1056 self.file_mod_times.insert(path.to_path_buf(), mtime);
1057 }
1058 }
1059
1060 self.notify_lsp_file_changed(path);
1062
1063 Ok(())
1064 }
1065
1066 pub fn handle_file_changed(&mut self, changed_path: &str) {
1068 let path = PathBuf::from(changed_path);
1069
1070 let buffer_ids: Vec<BufferId> = self
1072 .buffers
1073 .iter()
1074 .filter(|(_, state)| state.buffer.file_path() == Some(&path))
1075 .map(|(id, _)| *id)
1076 .collect();
1077
1078 if buffer_ids.is_empty() {
1079 return;
1080 }
1081
1082 for buffer_id in buffer_ids {
1083 if self.terminal_buffers.contains_key(&buffer_id) {
1086 continue;
1087 }
1088
1089 let state = match self.buffers.get(&buffer_id) {
1090 Some(s) => s,
1091 None => continue,
1092 };
1093
1094 let current_mtime = match self
1098 .filesystem
1099 .metadata(&path)
1100 .ok()
1101 .and_then(|m| m.modified)
1102 {
1103 Some(mtime) => mtime,
1104 None => continue, };
1106
1107 let dominated_by_stored = self
1108 .file_mod_times
1109 .get(&path)
1110 .map(|stored| current_mtime <= *stored)
1111 .unwrap_or(false);
1112
1113 if dominated_by_stored {
1114 continue;
1115 }
1116
1117 if state.buffer.is_modified() {
1119 self.status_message = Some(format!(
1120 "File {} changed on disk (buffer has unsaved changes)",
1121 path.display()
1122 ));
1123 continue;
1124 }
1125
1126 if self.auto_revert_enabled {
1128 let still_needs_revert = self
1132 .file_mod_times
1133 .get(&path)
1134 .map(|stored| current_mtime > *stored)
1135 .unwrap_or(true);
1136
1137 if !still_needs_revert {
1138 continue;
1139 }
1140
1141 let is_active_buffer = buffer_id == self.active_buffer();
1143
1144 if is_active_buffer {
1145 if let Err(e) = self.revert_file() {
1147 tracing::error!("Failed to auto-revert file {:?}: {}", path, e);
1148 } else {
1149 tracing::info!("Auto-reverted file: {:?}", path);
1150 }
1151 } else {
1152 if let Err(e) = self.revert_buffer_by_id(buffer_id, &path) {
1155 tracing::error!("Failed to auto-revert background file {:?}: {}", path, e);
1156 } else {
1157 tracing::info!("Auto-reverted file: {:?}", path);
1158 }
1159 }
1160
1161 self.watch_file(&path);
1163 }
1164 }
1165 }
1166
1167 pub fn check_save_conflict(&self) -> Option<std::time::SystemTime> {
1170 let path = self.active_state().buffer.file_path()?;
1171
1172 let current_mtime = self
1174 .filesystem
1175 .metadata(path)
1176 .ok()
1177 .and_then(|m| m.modified)?;
1178
1179 match self.file_mod_times.get(path) {
1181 Some(recorded_mtime) if current_mtime > *recorded_mtime => {
1182 Some(current_mtime)
1184 }
1185 _ => None,
1186 }
1187 }
1188}