1use crate::model::buffer::SudoSaveRequired;
12use crate::model::filesystem::FileSystem;
13use crate::view::file_tree::FileTreeView;
14use crate::view::prompt::PromptType;
15use std::path::{Path, PathBuf};
16
17use lsp_types::TextDocumentContentChangeEvent;
18use rust_i18n::t;
19
20use crate::model::event::{BufferId, EventLog};
21use crate::services::lsp::manager::LspSpawnResult;
22use crate::state::EditorState;
23
24use super::{BufferMetadata, Editor};
25
26impl Editor {
27 pub fn save(&mut self) -> anyhow::Result<()> {
29 if !self.authority.filesystem.is_remote_connected() {
31 anyhow::bail!(
32 "Cannot save: remote connection lost ({})",
33 self.authority
34 .filesystem
35 .remote_connection_info()
36 .unwrap_or("unknown host")
37 );
38 }
39
40 let path = self
41 .active_state()
42 .buffer
43 .file_path()
44 .map(|p| p.to_path_buf());
45
46 match self.active_state_mut().buffer.save() {
47 Ok(()) => self.finalize_save(path),
48 Err(e) => {
49 if let Some(sudo_info) = e.downcast_ref::<SudoSaveRequired>() {
50 let info = sudo_info.clone();
51 self.start_prompt(
52 t!("prompt.sudo_save_confirm").to_string(),
53 PromptType::ConfirmSudoSave { info },
54 );
55 Ok(())
56 } else if let Some(path) = path {
57 let is_not_found = e
59 .downcast_ref::<std::io::Error>()
60 .is_some_and(|io_err| io_err.kind() == std::io::ErrorKind::NotFound);
61 if is_not_found {
62 if let Some(parent) = path.parent() {
63 if !self.authority.filesystem.exists(parent) {
64 let dir_name = parent
65 .strip_prefix(&self.working_dir)
66 .unwrap_or(parent)
67 .display()
68 .to_string();
69 self.start_prompt(
70 t!("buffer.create_directory_confirm", name = &dir_name)
71 .to_string(),
72 PromptType::ConfirmCreateDirectory { path },
73 );
74 return Ok(());
75 }
76 }
77 }
78 Err(e)
79 } else {
80 Err(e)
81 }
82 }
83 }
84 }
85
86 pub(crate) fn finalize_save(&mut self, path: Option<PathBuf>) -> anyhow::Result<()> {
88 let buffer_id = self.active_buffer();
89 self.finalize_save_buffer(buffer_id, path, false)
90 }
91
92 pub(crate) fn finalize_save_buffer(
94 &mut self,
95 buffer_id: BufferId,
96 path: Option<PathBuf>,
97 silent: bool,
98 ) -> anyhow::Result<()> {
99 if let Some(ref p) = path {
101 if let Some(state) = self.buffers.get_mut(&buffer_id) {
102 if state.language == "text" {
103 let first_line = state.buffer.first_line_lossy();
104 let detected =
105 crate::primitives::detected_language::DetectedLanguage::from_path(
106 p,
107 first_line.as_deref(),
108 &self.grammar_registry,
109 &self.config.languages,
110 );
111 state.apply_language(detected);
112 }
113 }
114 }
115
116 if !silent {
117 self.status_message = Some(t!("status.file_saved").to_string());
118 }
119
120 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
122 event_log.mark_saved();
123 }
124
125 if let Some(ref p) = path {
127 if let Ok(metadata) = self.authority.filesystem.metadata(p) {
128 if let Some(mtime) = metadata.modified {
129 self.file_mod_times.insert(p.clone(), mtime);
130 }
131 }
132 }
133
134 if let Some(ref p) = path {
137 if p.file_name().and_then(|n| n.to_str()) == Some(".gitignore") {
138 if let Some(parent) = p.parent() {
139 let parent = parent.to_path_buf();
140 let fs = self.authority.filesystem.clone();
141 if let Some(explorer) = self.file_explorer.as_mut() {
142 load_gitignore_via_fs(fs.as_ref(), explorer, &parent);
143 }
144 }
145 }
146 }
147
148 self.notify_lsp_save_buffer(buffer_id);
150
151 if let Err(e) = self.delete_buffer_recovery(buffer_id) {
153 tracing::warn!("Failed to delete recovery file: {}", e);
154 }
155
156 if let Some(ref p) = path {
158 self.emit_event(
159 crate::model::control_event::events::FILE_SAVED.name,
160 serde_json::json!({
161 "path": p.display().to_string()
162 }),
163 );
164 }
165
166 if let Some(ref p) = path {
168 self.plugin_manager.run_hook(
169 "after_file_save",
170 crate::services::plugins::hooks::HookArgs::AfterFileSave {
171 buffer_id,
172 path: p.clone(),
173 },
174 );
175 }
176
177 if !silent {
183 match self.run_on_save_actions() {
184 Ok(true) => {
185 if self.status_message.as_deref() == Some(&t!("status.file_saved")) {
188 self.status_message =
189 Some(t!("status.file_saved_with_actions").to_string());
190 }
191 }
193 Ok(false) => {
194 }
196 Err(e) => {
197 self.status_message = Some(e);
199 }
200 }
201 }
202
203 Ok(())
204 }
205
206 pub fn auto_save_persistent_buffers(&mut self) -> anyhow::Result<usize> {
209 if !self.config.editor.auto_save_enabled {
210 return Ok(0);
211 }
212
213 let interval =
215 std::time::Duration::from_secs(self.config.editor.auto_save_interval_secs as u64);
216 if self
217 .time_source
218 .elapsed_since(self.last_persistent_auto_save)
219 < interval
220 {
221 return Ok(0);
222 }
223
224 self.last_persistent_auto_save = self.time_source.now();
225
226 let mut to_save = Vec::new();
228 for (id, state) in &self.buffers {
229 if state.buffer.is_modified() {
230 if let Some(path) = state.buffer.file_path() {
231 to_save.push((*id, path.to_path_buf()));
232 }
233 }
234 }
235
236 let mut count = 0;
237 for (id, path) in to_save {
238 if let Some(state) = self.buffers.get_mut(&id) {
239 match state.buffer.save() {
240 Ok(()) => {
241 self.finalize_save_buffer(id, Some(path), true)?;
242 count += 1;
243 }
244 Err(e) => {
245 if e.downcast_ref::<SudoSaveRequired>().is_some() {
247 tracing::debug!(
248 "Auto-save skipped for {:?} (sudo required)",
249 path.display()
250 );
251 } else {
252 tracing::warn!("Auto-save failed for {:?}: {}", path.display(), e);
253 }
254 }
255 }
256 }
257 }
258
259 Ok(count)
260 }
261
262 pub fn save_all_on_exit(&mut self) -> anyhow::Result<usize> {
266 let mut to_save = Vec::new();
267 for (id, state) in &self.buffers {
268 if state.buffer.is_modified() {
269 if let Some(path) = state.buffer.file_path() {
270 if !path.as_os_str().is_empty() {
271 to_save.push((*id, path.to_path_buf()));
272 }
273 }
274 }
275 }
276
277 let mut count = 0;
278 for (id, path) in to_save {
279 if let Some(state) = self.buffers.get_mut(&id) {
280 match state.buffer.save() {
281 Ok(()) => {
282 self.finalize_save_buffer(id, Some(path), true)?;
283 count += 1;
284 }
285 Err(e) => {
286 if e.downcast_ref::<SudoSaveRequired>().is_some() {
287 tracing::debug!(
288 "Auto-save on exit skipped for {} (sudo required)",
289 path.display()
290 );
291 } else {
292 tracing::warn!(
293 "Auto-save on exit failed for {}: {}",
294 path.display(),
295 e
296 );
297 }
298 }
299 }
300 }
301 }
302
303 Ok(count)
304 }
305
306 pub fn revert_file(&mut self) -> anyhow::Result<bool> {
309 let path = match self.active_state().buffer.file_path() {
310 Some(p) => p.to_path_buf(),
311 None => {
312 self.status_message = Some(t!("status.no_file_to_revert").to_string());
313 return Ok(false);
314 }
315 };
316
317 if !path.exists() {
318 self.status_message =
319 Some(t!("status.file_not_exists", path = path.display().to_string()).to_string());
320 return Ok(false);
321 }
322
323 let active_split = self.split_manager.active_split();
325 let (old_top_byte, old_left_column) = self
326 .split_view_states
327 .get(&active_split)
328 .map(|vs| (vs.viewport.top_byte, vs.viewport.left_column))
329 .unwrap_or((0, 0));
330 let old_cursors = self.active_cursors().clone();
331
332 let old_buffer_settings = self.active_state().buffer_settings.clone();
334 let old_editing_disabled = self.active_state().editing_disabled;
335
336 let mut new_state = EditorState::from_file_with_languages(
338 &path,
339 self.terminal_width,
340 self.terminal_height,
341 self.config.editor.large_file_threshold_bytes as usize,
342 &self.grammar_registry,
343 &self.config.languages,
344 std::sync::Arc::clone(&self.authority.filesystem),
345 )?;
346
347 let new_file_size = new_state.buffer.len();
349 let mut restored_cursors = old_cursors;
350 restored_cursors.map(|cursor| {
351 cursor.position = cursor.position.min(new_file_size);
352 cursor.clear_selection();
354 });
355 new_state.buffer_settings = old_buffer_settings;
357 new_state.editing_disabled = old_editing_disabled;
358 let buffer_id = self.active_buffer();
362 if let Some(state) = self.buffers.get_mut(&buffer_id) {
363 *state = new_state;
364 }
366
367 let active_split = self.split_manager.active_split();
369 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
370 view_state.cursors = restored_cursors;
371 }
372
373 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
375 view_state.viewport.top_byte = old_top_byte.min(new_file_size);
376 view_state.viewport.left_column = old_left_column;
377 }
378
379 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
381 *event_log = EventLog::new();
382 }
383
384 self.seen_byte_ranges.remove(&buffer_id);
386
387 if let Ok(metadata) = self.authority.filesystem.metadata(&path) {
389 if let Some(mtime) = metadata.modified {
390 self.file_mod_times.insert(path.clone(), mtime);
391 }
392 }
393
394 self.notify_lsp_file_changed(&path);
396
397 self.status_message = Some(t!("status.reverted").to_string());
398 Ok(true)
399 }
400
401 pub fn toggle_auto_revert(&mut self) {
403 self.auto_revert_enabled = !self.auto_revert_enabled;
404
405 if self.auto_revert_enabled {
406 self.status_message = Some(t!("status.auto_revert_enabled").to_string());
407 } else {
408 self.status_message = Some(t!("status.auto_revert_disabled").to_string());
409 }
410 }
411
412 pub fn poll_file_changes(&mut self) -> bool {
421 if !self.auto_revert_enabled {
423 return false;
424 }
425
426 let mut any_changed = false;
428 if let Some(ref rx) = self.pending_file_poll_rx {
429 match rx.try_recv() {
430 Ok(results) => {
431 self.pending_file_poll_rx = None;
432 any_changed = self.process_file_poll_results(results);
433 }
434 Err(std::sync::mpsc::TryRecvError::Empty) => {
435 return false;
437 }
438 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
439 self.pending_file_poll_rx = None;
441 }
442 }
443 }
444
445 let poll_interval =
447 std::time::Duration::from_millis(self.config.editor.auto_revert_poll_interval_ms);
448 let elapsed = self.time_source.elapsed_since(self.last_auto_revert_poll);
449 tracing::trace!(
450 "poll_file_changes: elapsed={:?}, poll_interval={:?}",
451 elapsed,
452 poll_interval
453 );
454 if elapsed < poll_interval {
455 return any_changed;
456 }
457 self.last_auto_revert_poll = self.time_source.now();
458
459 let files_to_check: Vec<PathBuf> = self
461 .buffers
462 .values()
463 .filter_map(|state| state.buffer.file_path().map(PathBuf::from))
464 .collect();
465
466 if files_to_check.is_empty() {
467 return any_changed;
468 }
469
470 let (tx, rx) = std::sync::mpsc::channel();
472 let fs = self.authority.filesystem.clone();
473 std::thread::Builder::new()
474 .name("poll-file-changes".to_string())
475 .spawn(move || {
476 let results: Vec<(PathBuf, Option<std::time::SystemTime>)> = files_to_check
477 .into_iter()
478 .map(|path| {
479 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
480 (path, mtime)
481 })
482 .collect();
483 if tx.send(results).is_err() {}
486 })
487 .ok();
488 self.pending_file_poll_rx = Some(rx);
489
490 any_changed
491 }
492
493 fn process_file_poll_results(
495 &mut self,
496 results: Vec<(PathBuf, Option<std::time::SystemTime>)>,
497 ) -> bool {
498 let mut any_changed = false;
499 for (path, mtime_opt) in results {
500 let Some(current_mtime) = mtime_opt else {
501 continue;
502 };
503
504 if let Some(&stored_mtime) = self.file_mod_times.get(&path) {
505 if current_mtime != stored_mtime {
506 let path_str = path.display().to_string();
507 if self.handle_async_file_changed(path_str) {
508 any_changed = true;
509 }
510 }
511 } else {
512 self.file_mod_times.insert(path, current_mtime);
514 }
515 }
516 any_changed
517 }
518
519 pub fn poll_file_tree_changes(&mut self) -> bool {
527 use crate::view::file_tree::NodeId;
528
529 let mut any_refreshed = false;
531 let mut dir_poll_pending = false;
532 if let Some(ref rx) = self.pending_dir_poll_rx {
533 match rx.try_recv() {
534 Ok((dir_results, git_index_mtime)) => {
535 self.pending_dir_poll_rx = None;
536 any_refreshed = self.process_dir_poll_results(dir_results, git_index_mtime);
537 }
538 Err(std::sync::mpsc::TryRecvError::Empty) => {
539 dir_poll_pending = true;
540 }
541 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
542 self.pending_dir_poll_rx = None;
543 }
544 }
545 }
546
547 let poll_interval =
549 std::time::Duration::from_millis(self.config.editor.file_tree_poll_interval_ms);
550 if self.time_source.elapsed_since(self.last_file_tree_poll) < poll_interval {
551 return any_refreshed;
552 }
553 self.last_file_tree_poll = self.time_source.now();
554
555 if self.sync_gitignores_from_disk() {
561 any_refreshed = true;
562 }
563
564 if dir_poll_pending {
566 return any_refreshed;
567 }
568
569 if !self.git_index_resolved {
573 self.git_index_resolved = true;
574 if let Some(path) = self.resolve_git_index() {
575 if let Ok(meta) = self.authority.filesystem.metadata(&path) {
576 if let Some(mtime) = meta.modified {
577 self.dir_mod_times.insert(path, mtime);
578 }
579 }
580 }
581 }
582
583 let Some(explorer) = &self.file_explorer else {
585 return any_refreshed;
586 };
587
588 let expanded_dirs: Vec<(NodeId, PathBuf)> = explorer
590 .tree()
591 .all_nodes()
592 .filter(|node| node.is_dir() && node.is_expanded())
593 .map(|node| (node.id, node.entry.path.clone()))
594 .collect();
595
596 let git_index_path: Option<PathBuf> = self
598 .dir_mod_times
599 .keys()
600 .find(|p| p.ends_with(".git/index") || p.ends_with(".git\\index"))
601 .cloned();
602
603 if expanded_dirs.is_empty() && git_index_path.is_none() {
604 return any_refreshed;
605 }
606
607 let (tx, rx) = std::sync::mpsc::channel();
609 let fs = self.authority.filesystem.clone();
610 std::thread::Builder::new()
611 .name("poll-dir-changes".to_string())
612 .spawn(move || {
613 let results: Vec<(NodeId, PathBuf, Option<std::time::SystemTime>)> = expanded_dirs
614 .into_iter()
615 .map(|(node_id, path)| {
616 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
617 (node_id, path, mtime)
618 })
619 .collect();
620
621 let git_index_mtime = git_index_path.and_then(|path| {
623 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
624 Some((path, mtime?))
625 });
626
627 if tx.send((results, git_index_mtime)).is_err() {}
629 })
630 .ok();
631 self.pending_dir_poll_rx = Some(rx);
632
633 any_refreshed
634 }
635
636 fn process_dir_poll_results(
638 &mut self,
639 results: Vec<(
640 crate::view::file_tree::NodeId,
641 PathBuf,
642 Option<std::time::SystemTime>,
643 )>,
644 git_index_mtime: Option<(PathBuf, std::time::SystemTime)>,
645 ) -> bool {
646 let mut dirs_to_refresh: Vec<(crate::view::file_tree::NodeId, PathBuf)> = Vec::new();
647
648 for (node_id, path, mtime_opt) in results {
649 let Some(current_mtime) = mtime_opt else {
650 continue;
651 };
652
653 if let Some(&stored_mtime) = self.dir_mod_times.get(&path) {
654 if current_mtime != stored_mtime {
655 self.dir_mod_times.insert(path.clone(), current_mtime);
656 dirs_to_refresh.push((node_id, path.clone()));
657 tracing::debug!("Directory changed: {:?}", path);
658 }
659 } else {
660 self.dir_mod_times.insert(path, current_mtime);
661 }
662 }
663
664 let git_index_changed = if let Some((path, current_mtime)) = git_index_mtime {
666 if let Some(&stored_mtime) = self.dir_mod_times.get(&path) {
667 if current_mtime != stored_mtime {
668 self.dir_mod_times.insert(path, current_mtime);
669 self.plugin_manager.run_hook(
670 "focus_gained",
671 crate::services::plugins::hooks::HookArgs::FocusGained {},
672 );
673 true
674 } else {
675 false
676 }
677 } else {
678 false
679 }
680 } else {
681 false
682 };
683
684 if dirs_to_refresh.is_empty() && !git_index_changed {
685 return false;
686 }
687
688 let refreshed_dirs: Vec<PathBuf> = dirs_to_refresh.iter().map(|(_, p)| p.clone()).collect();
693 self.refresh_file_tree_dirs(&refreshed_dirs);
694 let fs = self.authority.filesystem.clone();
695 if let Some(explorer) = self.file_explorer.as_mut() {
696 for dir in refreshed_dirs {
697 load_gitignore_via_fs(fs.as_ref(), explorer, &dir);
698 }
699 }
700
701 true
702 }
703
704 pub fn refresh_file_tree_dirs(&mut self, paths: &[PathBuf]) {
729 let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) else {
730 return;
731 };
732 let cursor_path: Option<PathBuf> = explorer.get_selected_entry().map(|e| e.path.clone());
733 for path in paths {
738 let Some(id_now) = explorer.tree().get_node_by_path(path).map(|n| n.id) else {
739 continue;
740 };
741 let tree = explorer.tree_mut();
742 if let Err(e) = runtime.block_on(tree.reload_expanded_node(id_now)) {
743 tracing::warn!("Failed to refresh directory {:?}: {}", path, e);
744 }
745 }
746 if let Some(path) = cursor_path {
747 if explorer.tree().get_node_by_path(&path).is_some() {
748 explorer.navigate_to_path(&path);
749 } else {
750 let root_id = explorer.tree().root_id();
751 explorer.set_selected(Some(root_id));
752 }
753 }
754 }
755
756 fn sync_gitignores_from_disk(&mut self) -> bool {
759 let fs = self.authority.filesystem.clone();
760 let Some(explorer) = self.file_explorer.as_mut() else {
761 return false;
762 };
763 let dirs = explorer.ignore_patterns().loaded_gitignore_dirs();
764 let mut changed = false;
765 for dir in dirs {
766 let gitignore_path = dir.join(".gitignore");
767 match fs.metadata(&gitignore_path) {
768 Err(_) => {
769 explorer.ignore_patterns_mut().remove_gitignore(&dir);
770 changed = true;
771 }
772 Ok(meta) => {
773 let stored = explorer.ignore_patterns().stored_gitignore_mtime(&dir);
774 if stored != meta.modified {
775 load_gitignore_via_fs(fs.as_ref(), explorer, &dir);
776 changed = true;
777 }
778 }
779 }
780 }
781 changed
782 }
783
784 fn resolve_git_index(&self) -> Option<PathBuf> {
788 let spawner = &self.authority.process_spawner;
789 let cwd = self.working_dir.to_string_lossy().to_string();
790
791 let result = if let Some(ref rt) = self.tokio_runtime {
795 rt.block_on(spawner.spawn(
796 "git".to_string(),
797 vec!["rev-parse".to_string(), "--git-dir".to_string()],
798 Some(cwd),
799 ))
800 } else {
801 return None;
804 };
805
806 let output = result.ok()?;
807 if output.exit_code != 0 {
808 return None;
809 }
810 let git_dir = output.stdout.trim();
811 let git_dir_path = if std::path::Path::new(git_dir).is_absolute() {
812 PathBuf::from(git_dir)
813 } else {
814 self.working_dir.join(git_dir)
815 };
816 Some(git_dir_path.join("index"))
817 }
818
819 pub(crate) fn notify_lsp_file_opened(
822 &mut self,
823 path: &Path,
824 buffer_id: BufferId,
825 metadata: &mut BufferMetadata,
826 ) {
827 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
829 tracing::debug!("No buffer state for file: {}", path.display());
830 return;
831 };
832
833 let Some(uri) = metadata.file_uri().cloned() else {
834 tracing::warn!(
835 "No URI in metadata for file: {} (failed to compute absolute path)",
836 path.display()
837 );
838 return;
839 };
840
841 let file_size = self
843 .authority
844 .filesystem
845 .metadata(path)
846 .ok()
847 .map(|m| m.size)
848 .unwrap_or(0);
849 if file_size > self.config.editor.large_file_threshold_bytes {
850 let reason = format!("File too large ({} bytes)", file_size);
851 tracing::debug!(
852 "Skipping LSP for large file: {} ({})",
853 path.display(),
854 reason
855 );
856 metadata.disable_lsp(reason);
857 return;
858 }
859
860 let text = match self
862 .buffers
863 .get(&buffer_id)
864 .and_then(|state| state.buffer.to_string())
865 {
866 Some(t) => t,
867 None => {
868 tracing::debug!("Buffer not fully loaded for LSP notification");
869 return;
870 }
871 };
872
873 let enable_inlay_hints = self.config.editor.enable_inlay_hints;
874 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
875
876 let (last_line, last_char, buffer_version) = self
878 .buffers
879 .get(&buffer_id)
880 .map(|state| {
881 let line_count = state.buffer.line_count().unwrap_or(1000);
882 (
883 line_count.saturating_sub(1) as u32,
884 10000u32,
885 state.buffer.version(),
886 )
887 })
888 .unwrap_or((999, 10000, 0));
889
890 let Some(lsp) = &mut self.lsp else {
892 tracing::debug!("No LSP manager available");
893 return;
894 };
895
896 tracing::debug!("LSP manager available for file: {}", path.display());
897 tracing::debug!(
898 "Detected language: {} for file: {}",
899 language,
900 path.display()
901 );
902 tracing::debug!("Using URI from metadata: {}", uri.as_str());
903 tracing::debug!("Attempting to spawn LSP client for language: {}", language);
904
905 match lsp.try_spawn(&language, Some(path)) {
906 LspSpawnResult::Spawned => {
907 for sh in lsp.get_handles_mut(&language) {
912 tracing::info!("Sending didOpen to LSP '{}' for: {}", sh.name, uri.as_str());
913 if let Err(e) =
914 sh.handle
915 .did_open(uri.as_uri().clone(), text.clone(), language.clone())
916 {
917 tracing::warn!("Failed to send didOpen to LSP '{}': {}", sh.name, e);
918 } else {
919 metadata.lsp_opened_with.insert(sh.handle.id());
920 }
921 }
922
923 if let Some(sh) =
930 lsp.handle_for_feature_mut(&language, crate::types::LspFeature::Diagnostics)
931 {
932 let request_id = self.next_lsp_request_id;
933 self.next_lsp_request_id += 1;
934 if let Err(e) = sh.handle.document_diagnostic(
935 request_id,
936 uri.as_uri().clone(),
937 previous_result_id,
938 ) {
939 tracing::debug!("Failed to request pull diagnostics: {}", e);
940 } else {
941 tracing::info!(
942 "Requested pull diagnostics for {} (request_id={})",
943 uri.as_str(),
944 request_id
945 );
946 }
947 }
948
949 if enable_inlay_hints {
950 if let Some(sh) =
951 lsp.handle_for_feature_mut(&language, crate::types::LspFeature::InlayHints)
952 {
953 let request_id = self.next_lsp_request_id;
954 self.next_lsp_request_id += 1;
955
956 if let Err(e) = sh.handle.inlay_hints(
957 request_id,
958 uri.as_uri().clone(),
959 0,
960 0,
961 last_line,
962 last_char,
963 ) {
964 tracing::debug!("Failed to request inlay hints: {}", e);
965 } else {
966 self.pending_inlay_hints_requests.insert(
967 request_id,
968 super::InlayHintsRequest {
969 buffer_id,
970 version: buffer_version,
971 },
972 );
973 tracing::info!(
974 "Requested inlay hints for {} (request_id={})",
975 uri.as_str(),
976 request_id
977 );
978 }
979 }
980 }
981
982 self.schedule_folding_ranges_refresh(buffer_id);
984 }
985 LspSpawnResult::NotAutoStart => {
986 tracing::debug!(
987 "LSP for {} not auto-starting (auto_start=false). Use command palette to start manually.",
988 language
989 );
990 if self.lsp_auto_prompt_enabled
1011 && !self.auto_start_prompted_languages.contains(&language)
1012 && !self.is_lsp_language_user_dismissed(&language)
1013 {
1014 self.pending_auto_start_prompts.insert(language);
1015 }
1016 }
1017 LspSpawnResult::NotConfigured => {
1018 tracing::debug!("No LSP server configured for language: {}", language);
1019 }
1020 LspSpawnResult::Disabled => {
1021 tracing::debug!("LSP disabled in config for language: {}", language);
1022 }
1023 LspSpawnResult::Failed => {
1024 tracing::warn!("Failed to spawn LSP client for language: {}", language);
1025 }
1026 }
1027 }
1028
1029 pub(crate) fn watch_file(&mut self, path: &Path) {
1032 if let Ok(metadata) = self.authority.filesystem.metadata(path) {
1034 if let Some(mtime) = metadata.modified {
1035 self.file_mod_times.insert(path.to_path_buf(), mtime);
1036 }
1037 }
1038 }
1039
1040 pub(crate) fn notify_lsp_file_changed(&mut self, path: &Path) {
1042 use crate::services::lsp::manager::LspSpawnResult;
1043
1044 let Some(lsp_uri) = super::types::file_path_to_lsp_uri_with_translation(
1045 path,
1046 self.authority.path_translation.as_ref(),
1047 ) else {
1048 return;
1049 };
1050
1051 let Some((buffer_id, content, language)) = self
1053 .buffers
1054 .iter()
1055 .find(|(_, s)| s.buffer.file_path() == Some(path))
1056 .and_then(|(id, state)| {
1057 state
1058 .buffer
1059 .to_string()
1060 .map(|t| (*id, t, state.language.clone()))
1061 })
1062 else {
1063 return;
1064 };
1065
1066 let spawn_result = {
1068 let Some(lsp) = self.lsp.as_mut() else {
1069 return;
1070 };
1071 lsp.try_spawn(&language, Some(path))
1072 };
1073
1074 if spawn_result != LspSpawnResult::Spawned {
1076 return;
1077 }
1078
1079 {
1081 let opened_with = self
1082 .buffer_metadata
1083 .get(&buffer_id)
1084 .map(|m| m.lsp_opened_with.clone())
1085 .unwrap_or_default();
1086
1087 if let Some(lsp) = self.lsp.as_mut() {
1088 for sh in lsp.get_handles_mut(&language) {
1089 if opened_with.contains(&sh.handle.id()) {
1090 continue;
1091 }
1092 if let Err(e) =
1093 sh.handle
1094 .did_open(lsp_uri.clone(), content.clone(), language.clone())
1095 {
1096 tracing::warn!(
1097 "Failed to send didOpen to LSP '{}' before didChange: {}",
1098 sh.name,
1099 e
1100 );
1101 } else {
1102 tracing::debug!(
1103 "Sent didOpen for {} to LSP '{}' before file change notification",
1104 lsp_uri.as_str(),
1105 sh.name
1106 );
1107 }
1108 }
1109 }
1110
1111 if let Some(lsp) = self.lsp.as_ref() {
1113 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
1114 for sh in lsp.get_handles(&language) {
1115 metadata.lsp_opened_with.insert(sh.handle.id());
1116 }
1117 }
1118 }
1119 }
1120
1121 if let Some(lsp) = &mut self.lsp {
1123 let content_change = TextDocumentContentChangeEvent {
1124 range: None, range_length: None,
1126 text: content,
1127 };
1128 for sh in lsp.get_handles_mut(&language) {
1129 if let Err(e) = sh
1130 .handle
1131 .did_change(lsp_uri.clone(), vec![content_change.clone()])
1132 {
1133 tracing::warn!("Failed to notify LSP '{}' of file change: {}", sh.name, e);
1134 }
1135 }
1136 }
1137 }
1138
1139 pub(crate) fn revert_buffer_by_id(
1145 &mut self,
1146 buffer_id: BufferId,
1147 path: &Path,
1148 ) -> anyhow::Result<()> {
1149 let old_cursors = self
1153 .split_view_states
1154 .values()
1155 .find_map(|vs| {
1156 if vs.keyed_states.contains_key(&buffer_id) {
1157 vs.keyed_states.get(&buffer_id).map(|bs| bs.cursors.clone())
1158 } else {
1159 None
1160 }
1161 })
1162 .unwrap_or_default();
1163 let (old_buffer_settings, old_editing_disabled) = self
1164 .buffers
1165 .get(&buffer_id)
1166 .map(|s| (s.buffer_settings.clone(), s.editing_disabled))
1167 .unwrap_or_default();
1168
1169 let mut new_state = EditorState::from_file_with_languages(
1171 path,
1172 self.terminal_width,
1173 self.terminal_height,
1174 self.config.editor.large_file_threshold_bytes as usize,
1175 &self.grammar_registry,
1176 &self.config.languages,
1177 std::sync::Arc::clone(&self.authority.filesystem),
1178 )?;
1179
1180 let new_file_size = new_state.buffer.len();
1182
1183 let mut restored_cursors = old_cursors;
1185 restored_cursors.map(|cursor| {
1186 cursor.position = cursor.position.min(new_file_size);
1187 cursor.clear_selection();
1188 });
1189 new_state.buffer_settings = old_buffer_settings;
1191 new_state.editing_disabled = old_editing_disabled;
1192 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1196 *state = new_state;
1197 }
1198
1199 for vs in self.split_view_states.values_mut() {
1201 if let Some(buf_state) = vs.keyed_states.get_mut(&buffer_id) {
1202 buf_state.cursors = restored_cursors.clone();
1203 }
1204 }
1205
1206 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
1208 *event_log = EventLog::new();
1209 }
1210
1211 self.seen_byte_ranges.remove(&buffer_id);
1213
1214 if let Ok(metadata) = self.authority.filesystem.metadata(path) {
1216 if let Some(mtime) = metadata.modified {
1217 self.file_mod_times.insert(path.to_path_buf(), mtime);
1218 }
1219 }
1220
1221 self.notify_lsp_file_changed(path);
1223
1224 Ok(())
1225 }
1226
1227 pub fn handle_file_changed(&mut self, changed_path: &str) {
1229 let path = PathBuf::from(changed_path);
1230
1231 let buffer_ids: Vec<BufferId> = self
1233 .buffers
1234 .iter()
1235 .filter(|(_, state)| state.buffer.file_path() == Some(&path))
1236 .map(|(id, _)| *id)
1237 .collect();
1238
1239 if buffer_ids.is_empty() {
1240 return;
1241 }
1242
1243 for buffer_id in buffer_ids {
1244 if self.terminal_buffers.contains_key(&buffer_id) {
1247 continue;
1248 }
1249
1250 let state = match self.buffers.get(&buffer_id) {
1251 Some(s) => s,
1252 None => continue,
1253 };
1254
1255 let current_mtime = match self
1259 .authority
1260 .filesystem
1261 .metadata(&path)
1262 .ok()
1263 .and_then(|m| m.modified)
1264 {
1265 Some(mtime) => mtime,
1266 None => continue, };
1268
1269 let dominated_by_stored = self
1270 .file_mod_times
1271 .get(&path)
1272 .map(|stored| current_mtime <= *stored)
1273 .unwrap_or(false);
1274
1275 if dominated_by_stored {
1276 continue;
1277 }
1278
1279 if state.buffer.is_modified() {
1281 self.status_message = Some(format!(
1282 "File {} changed on disk (buffer has unsaved changes)",
1283 path.display()
1284 ));
1285 continue;
1286 }
1287
1288 if self.auto_revert_enabled {
1290 let still_needs_revert = self
1294 .file_mod_times
1295 .get(&path)
1296 .map(|stored| current_mtime > *stored)
1297 .unwrap_or(true);
1298
1299 if !still_needs_revert {
1300 continue;
1301 }
1302
1303 let is_active_buffer = buffer_id == self.active_buffer();
1305
1306 if is_active_buffer {
1307 if let Err(e) = self.revert_file() {
1309 tracing::error!("Failed to auto-revert file {:?}: {}", path, e);
1310 } else {
1311 tracing::info!("Auto-reverted file: {:?}", path);
1312 }
1313 } else {
1314 if let Err(e) = self.revert_buffer_by_id(buffer_id, &path) {
1317 tracing::error!("Failed to auto-revert background file {:?}: {}", path, e);
1318 } else {
1319 tracing::info!("Auto-reverted file: {:?}", path);
1320 }
1321 }
1322
1323 self.watch_file(&path);
1325 }
1326 }
1327 }
1328
1329 pub fn check_save_conflict(&self) -> Option<std::time::SystemTime> {
1332 let path = self.active_state().buffer.file_path()?;
1333
1334 let current_mtime = self
1336 .authority
1337 .filesystem
1338 .metadata(path)
1339 .ok()
1340 .and_then(|m| m.modified)?;
1341
1342 match self.file_mod_times.get(path) {
1344 Some(recorded_mtime) if current_mtime > *recorded_mtime => {
1345 Some(current_mtime)
1347 }
1348 _ => None,
1349 }
1350 }
1351}
1352
1353pub(crate) fn load_gitignore_via_fs(fs: &dyn FileSystem, explorer: &mut FileTreeView, dir: &Path) {
1358 let gitignore_path = dir.join(".gitignore");
1359 let meta = match fs.metadata(&gitignore_path) {
1360 Ok(m) => m,
1361 Err(_) => return,
1362 };
1363 let bytes = match fs.read_file(&gitignore_path) {
1364 Ok(b) => b,
1365 Err(e) => {
1366 tracing::warn!("Failed to read {:?}: {}", gitignore_path, e);
1367 return;
1368 }
1369 };
1370 explorer.load_gitignore_from_bytes(dir, &bytes, meta.modified);
1371}