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(crate) fn collect_unnamed_modified_buffers(&self) -> Vec<BufferId> {
267 let mut out = Vec::new();
268 for (id, state) in &self.buffers {
269 if !state.buffer.is_modified() {
270 continue;
271 }
272 let is_unnamed = state
273 .buffer
274 .file_path()
275 .map(|p| p.as_os_str().is_empty())
276 .unwrap_or(true);
277 if is_unnamed {
278 out.push(*id);
279 }
280 }
281 out
282 }
283
284 pub(crate) fn start_next_quit_save_as(&mut self) -> bool {
288 while let Some(buffer_id) = self.pending_quit_unnamed_save.first().copied() {
289 let still_dirty_unnamed = self
291 .buffers
292 .get(&buffer_id)
293 .map(|s| {
294 s.buffer.is_modified()
295 && s.buffer
296 .file_path()
297 .map(|p| p.as_os_str().is_empty())
298 .unwrap_or(true)
299 })
300 .unwrap_or(false);
301 if !still_dirty_unnamed {
302 self.pending_quit_unnamed_save.remove(0);
303 continue;
304 }
305
306 self.set_active_buffer(buffer_id);
307 self.start_prompt(
308 t!("file.save_as_prompt").to_string(),
309 PromptType::SaveFileAs,
310 );
311 return true;
312 }
313 false
314 }
315
316 pub fn save_all_on_exit(&mut self) -> anyhow::Result<usize> {
320 let mut to_save = Vec::new();
321 for (id, state) in &self.buffers {
322 if state.buffer.is_modified() {
323 if let Some(path) = state.buffer.file_path() {
324 if !path.as_os_str().is_empty() {
325 to_save.push((*id, path.to_path_buf()));
326 }
327 }
328 }
329 }
330
331 let mut count = 0;
332 for (id, path) in to_save {
333 if let Some(state) = self.buffers.get_mut(&id) {
334 match state.buffer.save() {
335 Ok(()) => {
336 self.finalize_save_buffer(id, Some(path), true)?;
337 count += 1;
338 }
339 Err(e) => {
340 if e.downcast_ref::<SudoSaveRequired>().is_some() {
341 tracing::debug!(
342 "Auto-save on exit skipped for {} (sudo required)",
343 path.display()
344 );
345 } else {
346 tracing::warn!(
347 "Auto-save on exit failed for {}: {}",
348 path.display(),
349 e
350 );
351 }
352 }
353 }
354 }
355 }
356
357 Ok(count)
358 }
359
360 pub fn revert_file(&mut self) -> anyhow::Result<bool> {
363 let path = match self.active_state().buffer.file_path() {
364 Some(p) => p.to_path_buf(),
365 None => {
366 self.status_message = Some(t!("status.no_file_to_revert").to_string());
367 return Ok(false);
368 }
369 };
370
371 if !path.exists() {
372 self.status_message =
373 Some(t!("status.file_not_exists", path = path.display().to_string()).to_string());
374 return Ok(false);
375 }
376
377 let active_split = self.split_manager.active_split();
379 let (old_top_byte, old_left_column) = self
380 .split_view_states
381 .get(&active_split)
382 .map(|vs| (vs.viewport.top_byte, vs.viewport.left_column))
383 .unwrap_or((0, 0));
384 let old_cursors = self.active_cursors().clone();
385
386 let old_buffer_settings = self.active_state().buffer_settings.clone();
388 let old_editing_disabled = self.active_state().editing_disabled;
389
390 let mut new_state = EditorState::from_file_with_languages(
392 &path,
393 self.terminal_width,
394 self.terminal_height,
395 self.config.editor.large_file_threshold_bytes as usize,
396 &self.grammar_registry,
397 &self.config.languages,
398 std::sync::Arc::clone(&self.authority.filesystem),
399 )?;
400
401 let new_file_size = new_state.buffer.len();
403 let mut restored_cursors = old_cursors;
404 restored_cursors.map(|cursor| {
405 cursor.position = cursor.position.min(new_file_size);
406 cursor.clear_selection();
408 });
409 new_state.buffer_settings = old_buffer_settings;
411 new_state.editing_disabled = old_editing_disabled;
412 let buffer_id = self.active_buffer();
416 if let Some(state) = self.buffers.get_mut(&buffer_id) {
417 *state = new_state;
418 }
420
421 let active_split = self.split_manager.active_split();
423 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
424 view_state.cursors = restored_cursors;
425 }
426
427 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
429 view_state.viewport.top_byte = old_top_byte.min(new_file_size);
430 view_state.viewport.left_column = old_left_column;
431 }
432
433 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
435 *event_log = EventLog::new();
436 }
437
438 self.seen_byte_ranges.remove(&buffer_id);
440
441 if let Ok(metadata) = self.authority.filesystem.metadata(&path) {
443 if let Some(mtime) = metadata.modified {
444 self.file_mod_times.insert(path.clone(), mtime);
445 }
446 }
447
448 self.notify_lsp_file_changed(&path);
450
451 self.status_message = Some(t!("status.reverted").to_string());
452 Ok(true)
453 }
454
455 pub fn toggle_auto_revert(&mut self) {
457 self.auto_revert_enabled = !self.auto_revert_enabled;
458
459 if self.auto_revert_enabled {
460 self.status_message = Some(t!("status.auto_revert_enabled").to_string());
461 } else {
462 self.status_message = Some(t!("status.auto_revert_disabled").to_string());
463 }
464 }
465
466 pub fn poll_file_changes(&mut self) -> bool {
475 if !self.auto_revert_enabled {
477 return false;
478 }
479
480 let mut any_changed = false;
482 if let Some(ref rx) = self.pending_file_poll_rx {
483 match rx.try_recv() {
484 Ok(results) => {
485 self.pending_file_poll_rx = None;
486 any_changed = self.process_file_poll_results(results);
487 }
488 Err(std::sync::mpsc::TryRecvError::Empty) => {
489 return false;
491 }
492 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
493 self.pending_file_poll_rx = None;
495 }
496 }
497 }
498
499 let poll_interval =
501 std::time::Duration::from_millis(self.config.editor.auto_revert_poll_interval_ms);
502 let elapsed = self.time_source.elapsed_since(self.last_auto_revert_poll);
503 tracing::trace!(
504 "poll_file_changes: elapsed={:?}, poll_interval={:?}",
505 elapsed,
506 poll_interval
507 );
508 if elapsed < poll_interval {
509 return any_changed;
510 }
511 self.last_auto_revert_poll = self.time_source.now();
512
513 let files_to_check: Vec<PathBuf> = self
515 .buffers
516 .values()
517 .filter_map(|state| state.buffer.file_path().map(PathBuf::from))
518 .collect();
519
520 if files_to_check.is_empty() {
521 return any_changed;
522 }
523
524 let (tx, rx) = std::sync::mpsc::channel();
526 let fs = self.authority.filesystem.clone();
527 std::thread::Builder::new()
528 .name("poll-file-changes".to_string())
529 .spawn(move || {
530 let results: Vec<(PathBuf, Option<std::time::SystemTime>)> = files_to_check
531 .into_iter()
532 .map(|path| {
533 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
534 (path, mtime)
535 })
536 .collect();
537 if tx.send(results).is_err() {}
540 })
541 .ok();
542 self.pending_file_poll_rx = Some(rx);
543
544 any_changed
545 }
546
547 fn process_file_poll_results(
549 &mut self,
550 results: Vec<(PathBuf, Option<std::time::SystemTime>)>,
551 ) -> bool {
552 let mut any_changed = false;
553 for (path, mtime_opt) in results {
554 let Some(current_mtime) = mtime_opt else {
555 continue;
556 };
557
558 if let Some(&stored_mtime) = self.file_mod_times.get(&path) {
559 if current_mtime != stored_mtime {
560 let path_str = path.display().to_string();
561 if self.handle_async_file_changed(path_str) {
562 any_changed = true;
563 }
564 }
565 } else {
566 self.file_mod_times.insert(path, current_mtime);
568 }
569 }
570 any_changed
571 }
572
573 pub fn poll_file_tree_changes(&mut self) -> bool {
581 use crate::view::file_tree::NodeId;
582
583 let mut any_refreshed = false;
585 let mut dir_poll_pending = false;
586 if let Some(ref rx) = self.pending_dir_poll_rx {
587 match rx.try_recv() {
588 Ok((dir_results, git_index_mtime)) => {
589 self.pending_dir_poll_rx = None;
590 any_refreshed = self.process_dir_poll_results(dir_results, git_index_mtime);
591 }
592 Err(std::sync::mpsc::TryRecvError::Empty) => {
593 dir_poll_pending = true;
594 }
595 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
596 self.pending_dir_poll_rx = None;
597 }
598 }
599 }
600
601 let poll_interval =
603 std::time::Duration::from_millis(self.config.editor.file_tree_poll_interval_ms);
604 if self.time_source.elapsed_since(self.last_file_tree_poll) < poll_interval {
605 return any_refreshed;
606 }
607 self.last_file_tree_poll = self.time_source.now();
608
609 if self.sync_gitignores_from_disk() {
615 any_refreshed = true;
616 }
617
618 if dir_poll_pending {
620 return any_refreshed;
621 }
622
623 if !self.git_index_resolved {
627 self.git_index_resolved = true;
628 if let Some(path) = self.resolve_git_index() {
629 if let Ok(meta) = self.authority.filesystem.metadata(&path) {
630 if let Some(mtime) = meta.modified {
631 self.dir_mod_times.insert(path, mtime);
632 }
633 }
634 }
635 }
636
637 let Some(explorer) = &self.file_explorer else {
639 return any_refreshed;
640 };
641
642 let expanded_dirs: Vec<(NodeId, PathBuf)> = explorer
644 .tree()
645 .all_nodes()
646 .filter(|node| node.is_dir() && node.is_expanded())
647 .map(|node| (node.id, node.entry.path.clone()))
648 .collect();
649
650 let git_index_path: Option<PathBuf> = self
652 .dir_mod_times
653 .keys()
654 .find(|p| p.ends_with(".git/index") || p.ends_with(".git\\index"))
655 .cloned();
656
657 if expanded_dirs.is_empty() && git_index_path.is_none() {
658 return any_refreshed;
659 }
660
661 let (tx, rx) = std::sync::mpsc::channel();
663 let fs = self.authority.filesystem.clone();
664 std::thread::Builder::new()
665 .name("poll-dir-changes".to_string())
666 .spawn(move || {
667 let results: Vec<(NodeId, PathBuf, Option<std::time::SystemTime>)> = expanded_dirs
668 .into_iter()
669 .map(|(node_id, path)| {
670 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
671 (node_id, path, mtime)
672 })
673 .collect();
674
675 let git_index_mtime = git_index_path.and_then(|path| {
677 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
678 Some((path, mtime?))
679 });
680
681 if tx.send((results, git_index_mtime)).is_err() {}
683 })
684 .ok();
685 self.pending_dir_poll_rx = Some(rx);
686
687 any_refreshed
688 }
689
690 fn process_dir_poll_results(
692 &mut self,
693 results: Vec<(
694 crate::view::file_tree::NodeId,
695 PathBuf,
696 Option<std::time::SystemTime>,
697 )>,
698 git_index_mtime: Option<(PathBuf, std::time::SystemTime)>,
699 ) -> bool {
700 let mut dirs_to_refresh: Vec<(crate::view::file_tree::NodeId, PathBuf)> = Vec::new();
701
702 for (node_id, path, mtime_opt) in results {
703 let Some(current_mtime) = mtime_opt else {
704 continue;
705 };
706
707 if let Some(&stored_mtime) = self.dir_mod_times.get(&path) {
708 if current_mtime != stored_mtime {
709 self.dir_mod_times.insert(path.clone(), current_mtime);
710 dirs_to_refresh.push((node_id, path.clone()));
711 tracing::debug!("Directory changed: {:?}", path);
712 }
713 } else {
714 self.dir_mod_times.insert(path, current_mtime);
715 }
716 }
717
718 let git_index_changed = if let Some((path, current_mtime)) = git_index_mtime {
720 if let Some(&stored_mtime) = self.dir_mod_times.get(&path) {
721 if current_mtime != stored_mtime {
722 self.dir_mod_times.insert(path, current_mtime);
723 self.plugin_manager.run_hook(
724 "focus_gained",
725 crate::services::plugins::hooks::HookArgs::FocusGained {},
726 );
727 true
728 } else {
729 false
730 }
731 } else {
732 false
733 }
734 } else {
735 false
736 };
737
738 if dirs_to_refresh.is_empty() && !git_index_changed {
739 return false;
740 }
741
742 let refreshed_dirs: Vec<PathBuf> = dirs_to_refresh.iter().map(|(_, p)| p.clone()).collect();
747 self.refresh_file_tree_dirs(&refreshed_dirs);
748 let fs = self.authority.filesystem.clone();
749 if let Some(explorer) = self.file_explorer.as_mut() {
750 for dir in refreshed_dirs {
751 load_gitignore_via_fs(fs.as_ref(), explorer, &dir);
752 }
753 }
754
755 true
756 }
757
758 pub fn refresh_file_tree_dirs(&mut self, paths: &[PathBuf]) {
783 let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) else {
784 return;
785 };
786 let cursor_path: Option<PathBuf> = explorer.get_selected_entry().map(|e| e.path.clone());
787 for path in paths {
792 let Some(id_now) = explorer.tree().get_node_by_path(path).map(|n| n.id) else {
793 continue;
794 };
795 let tree = explorer.tree_mut();
796 if let Err(e) = runtime.block_on(tree.reload_expanded_node(id_now)) {
797 tracing::warn!("Failed to refresh directory {:?}: {}", path, e);
798 }
799 }
800 if let Some(path) = cursor_path {
801 if explorer.tree().get_node_by_path(&path).is_some() {
802 explorer.navigate_to_path(&path);
803 } else {
804 let root_id = explorer.tree().root_id();
805 explorer.set_selected(Some(root_id));
806 }
807 }
808 }
809
810 fn sync_gitignores_from_disk(&mut self) -> bool {
813 let fs = self.authority.filesystem.clone();
814 let Some(explorer) = self.file_explorer.as_mut() else {
815 return false;
816 };
817 let dirs = explorer.ignore_patterns().loaded_gitignore_dirs();
818 let mut changed = false;
819 for dir in dirs {
820 let gitignore_path = dir.join(".gitignore");
821 match fs.metadata(&gitignore_path) {
822 Err(_) => {
823 explorer.ignore_patterns_mut().remove_gitignore(&dir);
824 changed = true;
825 }
826 Ok(meta) => {
827 let stored = explorer.ignore_patterns().stored_gitignore_mtime(&dir);
828 if stored != meta.modified {
829 load_gitignore_via_fs(fs.as_ref(), explorer, &dir);
830 changed = true;
831 }
832 }
833 }
834 }
835 changed
836 }
837
838 fn resolve_git_index(&self) -> Option<PathBuf> {
842 let spawner = &self.authority.process_spawner;
843 let cwd = self.working_dir.to_string_lossy().to_string();
844
845 let result = if let Some(ref rt) = self.tokio_runtime {
849 rt.block_on(spawner.spawn(
850 "git".to_string(),
851 vec!["rev-parse".to_string(), "--git-dir".to_string()],
852 Some(cwd),
853 ))
854 } else {
855 return None;
858 };
859
860 let output = result.ok()?;
861 if output.exit_code != 0 {
862 return None;
863 }
864 let git_dir = output.stdout.trim();
865 let git_dir_path = if std::path::Path::new(git_dir).is_absolute() {
866 PathBuf::from(git_dir)
867 } else {
868 self.working_dir.join(git_dir)
869 };
870 Some(git_dir_path.join("index"))
871 }
872
873 pub(crate) fn notify_lsp_file_opened(
876 &mut self,
877 path: &Path,
878 buffer_id: BufferId,
879 metadata: &mut BufferMetadata,
880 ) {
881 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
883 tracing::debug!("No buffer state for file: {}", path.display());
884 return;
885 };
886
887 let Some(uri) = metadata.file_uri().cloned() else {
888 tracing::warn!(
889 "No URI in metadata for file: {} (failed to compute absolute path)",
890 path.display()
891 );
892 return;
893 };
894
895 let file_size = self
897 .authority
898 .filesystem
899 .metadata(path)
900 .ok()
901 .map(|m| m.size)
902 .unwrap_or(0);
903 if file_size > self.config.editor.large_file_threshold_bytes {
904 let reason = format!("File too large ({} bytes)", file_size);
905 tracing::debug!(
906 "Skipping LSP for large file: {} ({})",
907 path.display(),
908 reason
909 );
910 metadata.disable_lsp(reason);
911 return;
912 }
913
914 let text = match self
916 .buffers
917 .get(&buffer_id)
918 .and_then(|state| state.buffer.to_string())
919 {
920 Some(t) => t,
921 None => {
922 tracing::debug!("Buffer not fully loaded for LSP notification");
923 return;
924 }
925 };
926
927 let enable_inlay_hints = self.config.editor.enable_inlay_hints;
928 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
929
930 let (last_line, last_char, buffer_version) = self
932 .buffers
933 .get(&buffer_id)
934 .map(|state| {
935 let line_count = state.buffer.line_count().unwrap_or(1000);
936 (
937 line_count.saturating_sub(1) as u32,
938 10000u32,
939 state.buffer.version(),
940 )
941 })
942 .unwrap_or((999, 10000, 0));
943
944 let Some(lsp) = &mut self.lsp else {
946 tracing::debug!("No LSP manager available");
947 return;
948 };
949
950 tracing::debug!("LSP manager available for file: {}", path.display());
951 tracing::debug!(
952 "Detected language: {} for file: {}",
953 language,
954 path.display()
955 );
956 tracing::debug!("Using URI from metadata: {}", uri.as_str());
957 tracing::debug!("Attempting to spawn LSP client for language: {}", language);
958
959 match lsp.try_spawn(&language, Some(path)) {
960 LspSpawnResult::Spawned => {
961 for sh in lsp.get_handles_mut(&language) {
966 tracing::info!("Sending didOpen to LSP '{}' for: {}", sh.name, uri.as_str());
967 if let Err(e) =
968 sh.handle
969 .did_open(uri.as_uri().clone(), text.clone(), language.clone())
970 {
971 tracing::warn!("Failed to send didOpen to LSP '{}': {}", sh.name, e);
972 } else {
973 metadata.lsp_opened_with.insert(sh.handle.id());
974 }
975 }
976
977 if let Some(sh) =
984 lsp.handle_for_feature_mut(&language, crate::types::LspFeature::Diagnostics)
985 {
986 let request_id = self.next_lsp_request_id;
987 self.next_lsp_request_id += 1;
988 if let Err(e) = sh.handle.document_diagnostic(
989 request_id,
990 uri.as_uri().clone(),
991 previous_result_id,
992 ) {
993 tracing::debug!("Failed to request pull diagnostics: {}", e);
994 } else {
995 tracing::info!(
996 "Requested pull diagnostics for {} (request_id={})",
997 uri.as_str(),
998 request_id
999 );
1000 }
1001 }
1002
1003 if enable_inlay_hints {
1004 if let Some(sh) =
1005 lsp.handle_for_feature_mut(&language, crate::types::LspFeature::InlayHints)
1006 {
1007 let request_id = self.next_lsp_request_id;
1008 self.next_lsp_request_id += 1;
1009
1010 if let Err(e) = sh.handle.inlay_hints(
1011 request_id,
1012 uri.as_uri().clone(),
1013 0,
1014 0,
1015 last_line,
1016 last_char,
1017 ) {
1018 tracing::debug!("Failed to request inlay hints: {}", e);
1019 } else {
1020 self.pending_inlay_hints_requests.insert(
1021 request_id,
1022 super::InlayHintsRequest {
1023 buffer_id,
1024 version: buffer_version,
1025 },
1026 );
1027 tracing::info!(
1028 "Requested inlay hints for {} (request_id={})",
1029 uri.as_str(),
1030 request_id
1031 );
1032 }
1033 }
1034 }
1035
1036 self.schedule_folding_ranges_refresh(buffer_id);
1038 }
1039 LspSpawnResult::NotAutoStart => {
1040 tracing::debug!(
1041 "LSP for {} not auto-starting (auto_start=false). Click the LSP indicator to start manually.",
1042 language
1043 );
1044 }
1045 LspSpawnResult::NotConfigured => {
1046 tracing::debug!("No LSP server configured for language: {}", language);
1047 }
1048 LspSpawnResult::Disabled => {
1049 tracing::debug!("LSP disabled in config for language: {}", language);
1050 }
1051 LspSpawnResult::Failed => {
1052 tracing::warn!("Failed to spawn LSP client for language: {}", language);
1053 }
1054 }
1055 }
1056
1057 pub(crate) fn watch_file(&mut self, path: &Path) {
1060 if let Ok(metadata) = self.authority.filesystem.metadata(path) {
1062 if let Some(mtime) = metadata.modified {
1063 self.file_mod_times.insert(path.to_path_buf(), mtime);
1064 }
1065 }
1066 }
1067
1068 pub(crate) fn notify_lsp_file_changed(&mut self, path: &Path) {
1070 use crate::services::lsp::manager::LspSpawnResult;
1071
1072 let Some(lsp_uri) = super::types::file_path_to_lsp_uri_with_translation(
1073 path,
1074 self.authority.path_translation.as_ref(),
1075 ) else {
1076 return;
1077 };
1078
1079 let Some((buffer_id, content, language)) = self
1081 .buffers
1082 .iter()
1083 .find(|(_, s)| s.buffer.file_path() == Some(path))
1084 .and_then(|(id, state)| {
1085 state
1086 .buffer
1087 .to_string()
1088 .map(|t| (*id, t, state.language.clone()))
1089 })
1090 else {
1091 return;
1092 };
1093
1094 let spawn_result = {
1096 let Some(lsp) = self.lsp.as_mut() else {
1097 return;
1098 };
1099 lsp.try_spawn(&language, Some(path))
1100 };
1101
1102 if spawn_result != LspSpawnResult::Spawned {
1104 return;
1105 }
1106
1107 {
1109 let opened_with = self
1110 .buffer_metadata
1111 .get(&buffer_id)
1112 .map(|m| m.lsp_opened_with.clone())
1113 .unwrap_or_default();
1114
1115 if let Some(lsp) = self.lsp.as_mut() {
1116 for sh in lsp.get_handles_mut(&language) {
1117 if opened_with.contains(&sh.handle.id()) {
1118 continue;
1119 }
1120 if let Err(e) =
1121 sh.handle
1122 .did_open(lsp_uri.clone(), content.clone(), language.clone())
1123 {
1124 tracing::warn!(
1125 "Failed to send didOpen to LSP '{}' before didChange: {}",
1126 sh.name,
1127 e
1128 );
1129 } else {
1130 tracing::debug!(
1131 "Sent didOpen for {} to LSP '{}' before file change notification",
1132 lsp_uri.as_str(),
1133 sh.name
1134 );
1135 }
1136 }
1137 }
1138
1139 if let Some(lsp) = self.lsp.as_ref() {
1141 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
1142 for sh in lsp.get_handles(&language) {
1143 metadata.lsp_opened_with.insert(sh.handle.id());
1144 }
1145 }
1146 }
1147 }
1148
1149 if let Some(lsp) = &mut self.lsp {
1151 let content_change = TextDocumentContentChangeEvent {
1152 range: None, range_length: None,
1154 text: content,
1155 };
1156 for sh in lsp.get_handles_mut(&language) {
1157 if let Err(e) = sh
1158 .handle
1159 .did_change(lsp_uri.clone(), vec![content_change.clone()])
1160 {
1161 tracing::warn!("Failed to notify LSP '{}' of file change: {}", sh.name, e);
1162 }
1163 }
1164 }
1165 }
1166
1167 pub(crate) fn revert_buffer_by_id(
1173 &mut self,
1174 buffer_id: BufferId,
1175 path: &Path,
1176 ) -> anyhow::Result<()> {
1177 let old_cursors = self
1181 .split_view_states
1182 .values()
1183 .find_map(|vs| {
1184 if vs.keyed_states.contains_key(&buffer_id) {
1185 vs.keyed_states.get(&buffer_id).map(|bs| bs.cursors.clone())
1186 } else {
1187 None
1188 }
1189 })
1190 .unwrap_or_default();
1191 let (old_buffer_settings, old_editing_disabled) = self
1192 .buffers
1193 .get(&buffer_id)
1194 .map(|s| (s.buffer_settings.clone(), s.editing_disabled))
1195 .unwrap_or_default();
1196
1197 let mut new_state = EditorState::from_file_with_languages(
1199 path,
1200 self.terminal_width,
1201 self.terminal_height,
1202 self.config.editor.large_file_threshold_bytes as usize,
1203 &self.grammar_registry,
1204 &self.config.languages,
1205 std::sync::Arc::clone(&self.authority.filesystem),
1206 )?;
1207
1208 let new_file_size = new_state.buffer.len();
1210
1211 let mut restored_cursors = old_cursors;
1213 restored_cursors.map(|cursor| {
1214 cursor.position = cursor.position.min(new_file_size);
1215 cursor.clear_selection();
1216 });
1217 new_state.buffer_settings = old_buffer_settings;
1219 new_state.editing_disabled = old_editing_disabled;
1220 if let Some(state) = self.buffers.get_mut(&buffer_id) {
1224 *state = new_state;
1225 }
1226
1227 for vs in self.split_view_states.values_mut() {
1229 if let Some(buf_state) = vs.keyed_states.get_mut(&buffer_id) {
1230 buf_state.cursors = restored_cursors.clone();
1231 }
1232 }
1233
1234 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
1236 *event_log = EventLog::new();
1237 }
1238
1239 self.seen_byte_ranges.remove(&buffer_id);
1241
1242 if let Ok(metadata) = self.authority.filesystem.metadata(path) {
1244 if let Some(mtime) = metadata.modified {
1245 self.file_mod_times.insert(path.to_path_buf(), mtime);
1246 }
1247 }
1248
1249 self.notify_lsp_file_changed(path);
1251
1252 Ok(())
1253 }
1254
1255 pub fn handle_file_changed(&mut self, changed_path: &str) {
1257 let path = PathBuf::from(changed_path);
1258
1259 let buffer_ids: Vec<BufferId> = self
1261 .buffers
1262 .iter()
1263 .filter(|(_, state)| state.buffer.file_path() == Some(&path))
1264 .map(|(id, _)| *id)
1265 .collect();
1266
1267 if buffer_ids.is_empty() {
1268 return;
1269 }
1270
1271 for buffer_id in buffer_ids {
1272 if self.terminal_buffers.contains_key(&buffer_id) {
1275 continue;
1276 }
1277
1278 let state = match self.buffers.get(&buffer_id) {
1279 Some(s) => s,
1280 None => continue,
1281 };
1282
1283 let current_mtime = match self
1287 .authority
1288 .filesystem
1289 .metadata(&path)
1290 .ok()
1291 .and_then(|m| m.modified)
1292 {
1293 Some(mtime) => mtime,
1294 None => continue, };
1296
1297 let dominated_by_stored = self
1298 .file_mod_times
1299 .get(&path)
1300 .map(|stored| current_mtime <= *stored)
1301 .unwrap_or(false);
1302
1303 if dominated_by_stored {
1304 continue;
1305 }
1306
1307 if state.buffer.is_modified() {
1309 self.status_message = Some(format!(
1310 "File {} changed on disk (buffer has unsaved changes)",
1311 path.display()
1312 ));
1313 continue;
1314 }
1315
1316 if self.auto_revert_enabled {
1318 let still_needs_revert = self
1322 .file_mod_times
1323 .get(&path)
1324 .map(|stored| current_mtime > *stored)
1325 .unwrap_or(true);
1326
1327 if !still_needs_revert {
1328 continue;
1329 }
1330
1331 let is_active_buffer = buffer_id == self.active_buffer();
1333
1334 if is_active_buffer {
1335 if let Err(e) = self.revert_file() {
1337 tracing::error!("Failed to auto-revert file {:?}: {}", path, e);
1338 } else {
1339 tracing::info!("Auto-reverted file: {:?}", path);
1340 }
1341 } else {
1342 if let Err(e) = self.revert_buffer_by_id(buffer_id, &path) {
1345 tracing::error!("Failed to auto-revert background file {:?}: {}", path, e);
1346 } else {
1347 tracing::info!("Auto-reverted file: {:?}", path);
1348 }
1349 }
1350
1351 self.watch_file(&path);
1353 }
1354 }
1355 }
1356
1357 pub fn check_save_conflict(&self) -> Option<std::time::SystemTime> {
1360 let path = self.active_state().buffer.file_path()?;
1361
1362 let current_mtime = self
1364 .authority
1365 .filesystem
1366 .metadata(path)
1367 .ok()
1368 .and_then(|m| m.modified)?;
1369
1370 match self.file_mod_times.get(path) {
1372 Some(recorded_mtime) if current_mtime > *recorded_mtime => {
1373 Some(current_mtime)
1375 }
1376 _ => None,
1377 }
1378 }
1379}
1380
1381pub(crate) fn load_gitignore_via_fs(fs: &dyn FileSystem, explorer: &mut FileTreeView, dir: &Path) {
1386 let gitignore_path = dir.join(".gitignore");
1387 let meta = match fs.metadata(&gitignore_path) {
1388 Ok(m) => m,
1389 Err(_) => return,
1390 };
1391 let bytes = match fs.read_file(&gitignore_path) {
1392 Ok(b) => b,
1393 Err(e) => {
1394 tracing::warn!("Failed to read {:?}: {}", gitignore_path, e);
1395 return;
1396 }
1397 };
1398 explorer.load_gitignore_from_bytes(dir, &bytes, meta.modified);
1399}