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
102 .windows
103 .get_mut(&self.active_window)
104 .map(|w| &mut w.buffers)
105 .expect("active window present")
106 .get_mut(&buffer_id)
107 {
108 if state.language == "text" {
109 let first_line = state.buffer.first_line_lossy();
110 let detected =
111 crate::primitives::detected_language::DetectedLanguage::from_path(
112 p,
113 first_line.as_deref(),
114 &self.grammar_registry,
115 &self.config.languages,
116 );
117 state.apply_language(detected);
118 }
119 }
120 }
121
122 if !silent {
123 self.active_window_mut().status_message = Some(t!("status.file_saved").to_string());
124 }
125
126 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
128 event_log.mark_saved();
129 }
130
131 if let Some(ref p) = path {
133 if let Ok(metadata) = self.authority.filesystem.metadata(p) {
134 if let Some(mtime) = metadata.modified {
135 self.file_mod_times_mut().insert(p.clone(), mtime);
136 }
137 }
138 }
139
140 if let Some(ref p) = path {
143 if p.file_name().and_then(|n| n.to_str()) == Some(".gitignore") {
144 if let Some(parent) = p.parent() {
145 let parent = parent.to_path_buf();
146 let fs = self.authority.filesystem.clone();
147 if let Some(explorer) = self.file_explorer_mut().as_mut() {
148 load_gitignore_via_fs(fs.as_ref(), explorer, &parent);
149 }
150 }
151 }
152 }
153
154 self.active_window_mut().notify_lsp_save_buffer(buffer_id);
156
157 if let Err(e) = self.delete_buffer_recovery(buffer_id) {
159 tracing::warn!("Failed to delete recovery file: {}", e);
160 }
161
162 if let Some(ref p) = path {
164 self.emit_event(
165 crate::model::control_event::events::FILE_SAVED.name,
166 serde_json::json!({
167 "path": p.display().to_string()
168 }),
169 );
170 }
171
172 if let Some(ref p) = path {
174 self.plugin_manager.read().unwrap().run_hook(
175 "after_file_save",
176 crate::services::plugins::hooks::HookArgs::AfterFileSave {
177 buffer_id,
178 path: p.clone(),
179 },
180 );
181 }
182
183 if !silent {
189 match self.run_on_save_actions() {
190 Ok(true) => {
191 if self.active_window_mut().status_message.as_deref()
194 == Some(&t!("status.file_saved"))
195 {
196 self.active_window_mut().status_message =
197 Some(t!("status.file_saved_with_actions").to_string());
198 }
199 }
201 Ok(false) => {
202 }
204 Err(e) => {
205 self.active_window_mut().status_message = Some(e);
207 }
208 }
209 }
210
211 Ok(())
212 }
213
214 pub fn auto_save_persistent_buffers(&mut self) -> anyhow::Result<usize> {
217 if !self.config.editor.auto_save_enabled {
218 return Ok(0);
219 }
220
221 let interval =
223 std::time::Duration::from_secs(self.config.editor.auto_save_interval_secs as u64);
224 if self
225 .time_source
226 .elapsed_since(self.active_window().last_persistent_auto_save)
227 < interval
228 {
229 return Ok(0);
230 }
231
232 self.active_window_mut().last_persistent_auto_save = self.time_source.now();
233
234 let mut to_save = Vec::new();
236 for (id, state) in self
237 .windows
238 .get(&self.active_window)
239 .map(|w| &w.buffers)
240 .expect("active window present")
241 {
242 if state.buffer.is_modified() {
243 if let Some(path) = state.buffer.file_path() {
244 to_save.push((*id, path.to_path_buf()));
245 }
246 }
247 }
248
249 let mut count = 0;
250 for (id, path) in to_save {
251 if let Some(state) = self
252 .windows
253 .get_mut(&self.active_window)
254 .map(|w| &mut w.buffers)
255 .expect("active window present")
256 .get_mut(&id)
257 {
258 match state.buffer.save() {
259 Ok(()) => {
260 self.finalize_save_buffer(id, Some(path), true)?;
261 count += 1;
262 }
263 Err(e) => {
264 if e.downcast_ref::<SudoSaveRequired>().is_some() {
266 tracing::debug!(
267 "Auto-save skipped for {:?} (sudo required)",
268 path.display()
269 );
270 } else {
271 tracing::warn!("Auto-save failed for {:?}: {}", path.display(), e);
272 }
273 }
274 }
275 }
276 }
277
278 Ok(count)
279 }
280
281 pub(crate) fn collect_unnamed_modified_buffers(&self) -> Vec<BufferId> {
286 let mut out = Vec::new();
287 for (id, state) in self
288 .windows
289 .get(&self.active_window)
290 .map(|w| &w.buffers)
291 .expect("active window present")
292 {
293 if !state.buffer.is_modified() {
294 continue;
295 }
296 let is_unnamed = state
297 .buffer
298 .file_path()
299 .map(|p| p.as_os_str().is_empty())
300 .unwrap_or(true);
301 if is_unnamed {
302 out.push(*id);
303 }
304 }
305 out
306 }
307
308 pub(crate) fn start_next_quit_save_as(&mut self) -> bool {
312 while let Some(buffer_id) = self
313 .active_window_mut()
314 .pending_quit_unnamed_save
315 .first()
316 .copied()
317 {
318 let still_dirty_unnamed = self
320 .buffers()
321 .get(&buffer_id)
322 .map(|s| {
323 s.buffer.is_modified()
324 && s.buffer
325 .file_path()
326 .map(|p| p.as_os_str().is_empty())
327 .unwrap_or(true)
328 })
329 .unwrap_or(false);
330 if !still_dirty_unnamed {
331 self.active_window_mut().pending_quit_unnamed_save.remove(0);
332 continue;
333 }
334
335 self.set_active_buffer(buffer_id);
336 self.start_prompt(
337 t!("file.save_as_prompt").to_string(),
338 PromptType::SaveFileAs,
339 );
340 return true;
341 }
342 false
343 }
344
345 pub fn save_all_on_exit(&mut self) -> anyhow::Result<usize> {
349 let mut to_save = Vec::new();
350 for (id, state) in self
351 .windows
352 .get(&self.active_window)
353 .map(|w| &w.buffers)
354 .expect("active window present")
355 {
356 if state.buffer.is_modified() {
357 if let Some(path) = state.buffer.file_path() {
358 if !path.as_os_str().is_empty() {
359 to_save.push((*id, path.to_path_buf()));
360 }
361 }
362 }
363 }
364
365 let mut count = 0;
366 for (id, path) in to_save {
367 if let Some(state) = self
368 .windows
369 .get_mut(&self.active_window)
370 .map(|w| &mut w.buffers)
371 .expect("active window present")
372 .get_mut(&id)
373 {
374 match state.buffer.save() {
375 Ok(()) => {
376 self.finalize_save_buffer(id, Some(path), true)?;
377 count += 1;
378 }
379 Err(e) => {
380 if e.downcast_ref::<SudoSaveRequired>().is_some() {
381 tracing::debug!(
382 "Auto-save on exit skipped for {} (sudo required)",
383 path.display()
384 );
385 } else {
386 tracing::warn!(
387 "Auto-save on exit failed for {}: {}",
388 path.display(),
389 e
390 );
391 }
392 }
393 }
394 }
395 }
396
397 Ok(count)
398 }
399
400 pub fn revert_file(&mut self) -> anyhow::Result<bool> {
403 let path = match self.active_state().buffer.file_path() {
404 Some(p) => p.to_path_buf(),
405 None => {
406 self.active_window_mut().status_message =
407 Some(t!("status.no_file_to_revert").to_string());
408 return Ok(false);
409 }
410 };
411
412 if !path.exists() {
413 self.active_window_mut().status_message =
414 Some(t!("status.file_not_exists", path = path.display().to_string()).to_string());
415 return Ok(false);
416 }
417
418 let active_split = self
420 .windows
421 .get(&self.active_window)
422 .and_then(|w| w.buffers.splits())
423 .map(|(mgr, _)| mgr)
424 .expect("active window must have a populated split layout")
425 .active_split();
426 let (old_top_byte, old_left_column) = self
427 .windows
428 .get(&self.active_window)
429 .and_then(|w| w.buffers.splits())
430 .map(|(_, vs)| vs)
431 .expect("active window must have a populated split layout")
432 .get(&active_split)
433 .map(|vs| (vs.viewport.top_byte, vs.viewport.left_column))
434 .unwrap_or((0, 0));
435 let old_cursors = self.active_cursors().clone();
436
437 let old_buffer_settings = self.active_state().buffer_settings.clone();
439 let old_editing_disabled = self.active_state().editing_disabled;
440
441 let mut new_state = EditorState::from_file_with_languages(
443 &path,
444 self.terminal_width,
445 self.terminal_height,
446 self.config.editor.large_file_threshold_bytes as usize,
447 &self.grammar_registry,
448 &self.config.languages,
449 std::sync::Arc::clone(&self.authority.filesystem),
450 )?;
451
452 let new_file_size = new_state.buffer.len();
454 let mut restored_cursors = old_cursors;
455 restored_cursors.map(|cursor| {
456 cursor.position = cursor.position.min(new_file_size);
457 cursor.clear_selection();
459 });
460 new_state.buffer_settings = old_buffer_settings;
462 new_state.editing_disabled = old_editing_disabled;
463 let buffer_id = self.active_buffer();
467 if let Some(state) = self
468 .windows
469 .get_mut(&self.active_window)
470 .map(|w| &mut w.buffers)
471 .expect("active window present")
472 .get_mut(&buffer_id)
473 {
474 *state = new_state;
475 }
477
478 let active_split = self
480 .windows
481 .get(&self.active_window)
482 .and_then(|w| w.buffers.splits())
483 .map(|(mgr, _)| mgr)
484 .expect("active window must have a populated split layout")
485 .active_split();
486 if let Some(view_state) = self
487 .windows
488 .get_mut(&self.active_window)
489 .and_then(|w| w.split_view_states_mut())
490 .expect("active window must have a populated split layout")
491 .get_mut(&active_split)
492 {
493 view_state.cursors = restored_cursors;
494 }
495
496 if let Some(view_state) = self
498 .windows
499 .get_mut(&self.active_window)
500 .and_then(|w| w.split_view_states_mut())
501 .expect("active window must have a populated split layout")
502 .get_mut(&active_split)
503 {
504 view_state.viewport.top_byte = old_top_byte.min(new_file_size);
505 view_state.viewport.left_column = old_left_column;
506 }
507
508 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
510 *event_log = EventLog::new();
511 }
512
513 self.active_window_mut().seen_byte_ranges.remove(&buffer_id);
515
516 if let Ok(metadata) = self.authority.filesystem.metadata(&path) {
518 if let Some(mtime) = metadata.modified {
519 self.file_mod_times_mut().insert(path.clone(), mtime);
520 }
521 }
522
523 self.notify_lsp_file_changed(&path);
525
526 self.active_window_mut().status_message = Some(t!("status.reverted").to_string());
527 Ok(true)
528 }
529
530 pub fn toggle_auto_revert(&mut self) {
532 self.active_window_mut().auto_revert_enabled = !self.active_window().auto_revert_enabled;
533
534 if self.active_window().auto_revert_enabled {
535 self.active_window_mut().status_message =
536 Some(t!("status.auto_revert_enabled").to_string());
537 } else {
538 self.active_window_mut().status_message =
539 Some(t!("status.auto_revert_disabled").to_string());
540 }
541 }
542
543 pub fn poll_file_changes(&mut self) -> bool {
552 if !self.active_window().auto_revert_enabled {
554 return false;
555 }
556
557 let mut any_changed = false;
559 if let Some(ref rx) = self.active_window_mut().pending_file_poll_rx {
560 match rx.try_recv() {
561 Ok(results) => {
562 self.active_window_mut().pending_file_poll_rx = None;
563 any_changed = self.process_file_poll_results(results);
564 }
565 Err(std::sync::mpsc::TryRecvError::Empty) => {
566 return false;
568 }
569 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
570 self.active_window_mut().pending_file_poll_rx = None;
572 }
573 }
574 }
575
576 let poll_interval =
578 std::time::Duration::from_millis(self.config.editor.auto_revert_poll_interval_ms);
579 let last_poll = self.active_window().last_auto_revert_poll;
580 let elapsed = self.time_source.elapsed_since(last_poll);
581 tracing::trace!(
582 "poll_file_changes: elapsed={:?}, poll_interval={:?}",
583 elapsed,
584 poll_interval
585 );
586 if elapsed < poll_interval {
587 return any_changed;
588 }
589 self.active_window_mut().last_auto_revert_poll = self.time_source.now();
590
591 let files_to_check: Vec<PathBuf> = self.buffers().paths();
593
594 if files_to_check.is_empty() {
595 return any_changed;
596 }
597
598 let (tx, rx) = std::sync::mpsc::channel();
600 let fs = self.authority.filesystem.clone();
601 std::thread::Builder::new()
602 .name("poll-file-changes".to_string())
603 .spawn(move || {
604 let results: Vec<(PathBuf, Option<std::time::SystemTime>)> = files_to_check
605 .into_iter()
606 .map(|path| {
607 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
608 (path, mtime)
609 })
610 .collect();
611 if tx.send(results).is_err() {}
614 })
615 .ok();
616 self.active_window_mut().pending_file_poll_rx = Some(rx);
617
618 any_changed
619 }
620
621 fn process_file_poll_results(
623 &mut self,
624 results: Vec<(PathBuf, Option<std::time::SystemTime>)>,
625 ) -> bool {
626 let mut any_changed = false;
627 for (path, mtime_opt) in results {
628 let Some(current_mtime) = mtime_opt else {
629 continue;
630 };
631
632 if let Some(&stored_mtime) = self.file_mod_times().get(&path) {
633 if current_mtime != stored_mtime {
634 let path_str = path.display().to_string();
635 if self.handle_async_file_changed(path_str) {
636 any_changed = true;
637 }
638 }
639 } else {
640 self.file_mod_times_mut().insert(path, current_mtime);
642 }
643 }
644 any_changed
645 }
646
647 pub fn poll_file_tree_changes(&mut self) -> bool {
655 use crate::view::file_tree::NodeId;
656
657 let mut any_refreshed = false;
659 let mut dir_poll_pending = false;
660 if let Some(ref rx) = self.active_window_mut().pending_dir_poll_rx {
661 match rx.try_recv() {
662 Ok((dir_results, git_index_mtime)) => {
663 self.active_window_mut().pending_dir_poll_rx = None;
664 any_refreshed = self.process_dir_poll_results(dir_results, git_index_mtime);
665 }
666 Err(std::sync::mpsc::TryRecvError::Empty) => {
667 dir_poll_pending = true;
668 }
669 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
670 self.active_window_mut().pending_dir_poll_rx = None;
671 }
672 }
673 }
674
675 let poll_interval =
677 std::time::Duration::from_millis(self.config.editor.file_tree_poll_interval_ms);
678 let last_tree_poll = self.active_window().last_file_tree_poll;
679 if self.time_source.elapsed_since(last_tree_poll) < poll_interval {
680 return any_refreshed;
681 }
682 self.active_window_mut().last_file_tree_poll = self.time_source.now();
683
684 if self.sync_gitignores_from_disk() {
690 any_refreshed = true;
691 }
692
693 if dir_poll_pending {
695 return any_refreshed;
696 }
697
698 if !self.active_window_mut().git_index_resolved {
702 self.active_window_mut().git_index_resolved = true;
703 if let Some(path) = self.resolve_git_index() {
704 if let Ok(meta) = self.authority.filesystem.metadata(&path) {
705 if let Some(mtime) = meta.modified {
706 self.active_window_mut().dir_mod_times.insert(path, mtime);
707 }
708 }
709 }
710 }
711
712 let Some(explorer) = self.file_explorer() else {
714 return any_refreshed;
715 };
716
717 let expanded_dirs: Vec<(NodeId, PathBuf)> = explorer
719 .tree()
720 .all_nodes()
721 .filter(|node| node.is_dir() && node.is_expanded())
722 .map(|node| (node.id, node.entry.path.clone()))
723 .collect();
724
725 let git_index_path: Option<PathBuf> = self
727 .active_window()
728 .dir_mod_times
729 .keys()
730 .find(|p| p.ends_with(".git/index") || p.ends_with(".git\\index"))
731 .cloned();
732
733 if expanded_dirs.is_empty() && git_index_path.is_none() {
734 return any_refreshed;
735 }
736
737 let (tx, rx) = std::sync::mpsc::channel();
739 let fs = self.authority.filesystem.clone();
740 std::thread::Builder::new()
741 .name("poll-dir-changes".to_string())
742 .spawn(move || {
743 let results: Vec<(NodeId, PathBuf, Option<std::time::SystemTime>)> = expanded_dirs
744 .into_iter()
745 .map(|(node_id, path)| {
746 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
747 (node_id, path, mtime)
748 })
749 .collect();
750
751 let git_index_mtime = git_index_path.and_then(|path| {
753 let mtime = fs.metadata(&path).ok().and_then(|m| m.modified);
754 Some((path, mtime?))
755 });
756
757 if tx.send((results, git_index_mtime)).is_err() {}
759 })
760 .ok();
761 self.active_window_mut().pending_dir_poll_rx = Some(rx);
762
763 any_refreshed
764 }
765
766 fn process_dir_poll_results(
768 &mut self,
769 results: Vec<(
770 crate::view::file_tree::NodeId,
771 PathBuf,
772 Option<std::time::SystemTime>,
773 )>,
774 git_index_mtime: Option<(PathBuf, std::time::SystemTime)>,
775 ) -> bool {
776 let mut dirs_to_refresh: Vec<(crate::view::file_tree::NodeId, PathBuf)> = Vec::new();
777
778 for (node_id, path, mtime_opt) in results {
779 let Some(current_mtime) = mtime_opt else {
780 continue;
781 };
782
783 if let Some(&stored_mtime) = self.active_window_mut().dir_mod_times.get(&path) {
784 if current_mtime != stored_mtime {
785 self.active_window_mut()
786 .dir_mod_times
787 .insert(path.clone(), current_mtime);
788 dirs_to_refresh.push((node_id, path.clone()));
789 tracing::debug!("Directory changed: {:?}", path);
790 }
791 } else {
792 self.active_window_mut()
793 .dir_mod_times
794 .insert(path, current_mtime);
795 }
796 }
797
798 let git_index_changed = if let Some((path, current_mtime)) = git_index_mtime {
800 if let Some(&stored_mtime) = self.active_window_mut().dir_mod_times.get(&path) {
801 if current_mtime != stored_mtime {
802 self.active_window_mut()
803 .dir_mod_times
804 .insert(path, current_mtime);
805 self.plugin_manager.read().unwrap().run_hook(
806 "focus_gained",
807 crate::services::plugins::hooks::HookArgs::FocusGained {},
808 );
809 true
810 } else {
811 false
812 }
813 } else {
814 false
815 }
816 } else {
817 false
818 };
819
820 if dirs_to_refresh.is_empty() && !git_index_changed {
821 return false;
822 }
823
824 let refreshed_dirs: Vec<PathBuf> = dirs_to_refresh.iter().map(|(_, p)| p.clone()).collect();
829 self.refresh_file_tree_dirs(&refreshed_dirs);
830 let fs = self.authority.filesystem.clone();
831 if let Some(explorer) = self.file_explorer_mut().as_mut() {
832 for dir in refreshed_dirs {
833 load_gitignore_via_fs(fs.as_ref(), explorer, &dir);
834 }
835 }
836
837 true
838 }
839
840 pub fn refresh_file_tree_dirs(&mut self, paths: &[PathBuf]) {
865 let active_id = self.active_window;
866 let (Some(runtime), Some(explorer)) = (
867 self.tokio_runtime.as_ref(),
868 self.windows
869 .get_mut(&active_id)
870 .and_then(|w| w.file_explorer.as_mut()),
871 ) else {
872 return;
873 };
874 let cursor_path: Option<PathBuf> = explorer.get_selected_entry().map(|e| e.path.clone());
875 for path in paths {
880 let Some(id_now) = explorer.tree().get_node_by_path(path).map(|n| n.id) else {
881 continue;
882 };
883 let tree = explorer.tree_mut();
884 if let Err(e) = runtime.block_on(tree.reload_expanded_node(id_now)) {
885 tracing::warn!("Failed to refresh directory {:?}: {}", path, e);
886 }
887 }
888 if let Some(path) = cursor_path {
889 if explorer.tree().get_node_by_path(&path).is_some() {
890 explorer.navigate_to_path(&path);
891 } else {
892 let root_id = explorer.tree().root_id();
893 explorer.set_selected(Some(root_id));
894 }
895 }
896 }
897
898 fn sync_gitignores_from_disk(&mut self) -> bool {
901 let fs = self.authority.filesystem.clone();
902 let Some(explorer) = self.file_explorer_mut() else {
903 return false;
904 };
905 let dirs = explorer.ignore_patterns().loaded_gitignore_dirs();
906 let mut changed = false;
907 for dir in dirs {
908 let gitignore_path = dir.join(".gitignore");
909 match fs.metadata(&gitignore_path) {
910 Err(_) => {
911 explorer.ignore_patterns_mut().remove_gitignore(&dir);
912 changed = true;
913 }
914 Ok(meta) => {
915 let stored = explorer.ignore_patterns().stored_gitignore_mtime(&dir);
916 if stored != meta.modified {
917 load_gitignore_via_fs(fs.as_ref(), explorer, &dir);
918 changed = true;
919 }
920 }
921 }
922 }
923 changed
924 }
925
926 fn resolve_git_index(&self) -> Option<PathBuf> {
930 let spawner = &self.authority.process_spawner;
931 let cwd = self.working_dir.to_string_lossy().to_string();
932
933 let result = if let Some(ref rt) = self.tokio_runtime {
937 rt.block_on(spawner.spawn(
938 "git".to_string(),
939 vec!["rev-parse".to_string(), "--git-dir".to_string()],
940 Some(cwd),
941 ))
942 } else {
943 return None;
946 };
947
948 let output = result.ok()?;
949 if output.exit_code != 0 {
950 return None;
951 }
952 let git_dir = output.stdout.trim();
953 let git_dir_path = if std::path::Path::new(git_dir).is_absolute() {
954 PathBuf::from(git_dir)
955 } else {
956 self.working_dir.join(git_dir)
957 };
958 Some(git_dir_path.join("index"))
959 }
960
961 pub(crate) fn notify_lsp_file_opened(
964 &mut self,
965 path: &Path,
966 buffer_id: BufferId,
967 metadata: &mut BufferMetadata,
968 ) {
969 let Some(language) = self
971 .windows
972 .get(&self.active_window)
973 .map(|w| &w.buffers)
974 .expect("active window present")
975 .get(&buffer_id)
976 .map(|s| s.language.clone())
977 else {
978 tracing::debug!("No buffer state for file: {}", path.display());
979 return;
980 };
981
982 let Some(uri) = metadata.file_uri().cloned() else {
983 tracing::warn!(
984 "No URI in metadata for file: {} (failed to compute absolute path)",
985 path.display()
986 );
987 return;
988 };
989
990 let file_size = self
992 .authority
993 .filesystem
994 .metadata(path)
995 .ok()
996 .map(|m| m.size)
997 .unwrap_or(0);
998 if file_size > self.config.editor.large_file_threshold_bytes {
999 let reason = format!("File too large ({} bytes)", file_size);
1000 tracing::debug!(
1001 "Skipping LSP for large file: {} ({})",
1002 path.display(),
1003 reason
1004 );
1005 metadata.disable_lsp(reason);
1006 return;
1007 }
1008
1009 let text = match self
1011 .buffers()
1012 .get(&buffer_id)
1013 .and_then(|state| state.buffer.to_string())
1014 {
1015 Some(t) => t,
1016 None => {
1017 tracing::debug!("Buffer not fully loaded for LSP notification");
1018 return;
1019 }
1020 };
1021
1022 let enable_inlay_hints = self.config.editor.enable_inlay_hints;
1023 let previous_result_id = self
1024 .active_window()
1025 .diagnostic_result_ids
1026 .get(uri.as_str())
1027 .cloned();
1028
1029 let (last_line, last_char, buffer_version) = self
1031 .buffers()
1032 .get(&buffer_id)
1033 .map(|state| {
1034 let line_count = state.buffer.line_count().unwrap_or(1000);
1035 (
1036 line_count.saturating_sub(1) as u32,
1037 10000u32,
1038 state.buffer.version(),
1039 )
1040 })
1041 .unwrap_or((999, 10000, 0));
1042
1043 let __active_id = self.active_window;
1045 let Some(__win) = self.windows.get_mut(&__active_id) else {
1046 tracing::debug!("No LSP manager available");
1047 return;
1048 };
1049 let Some(lsp) = __win.lsp.as_mut() else {
1050 tracing::debug!("No LSP manager available");
1051 return;
1052 };
1053 let __next_id = &mut __win.next_lsp_request_id;
1054
1055 tracing::debug!("LSP manager available for file: {}", path.display());
1056 tracing::debug!(
1057 "Detected language: {} for file: {}",
1058 language,
1059 path.display()
1060 );
1061 tracing::debug!("Using URI from metadata: {}", uri.as_str());
1062 tracing::debug!("Attempting to spawn LSP client for language: {}", language);
1063
1064 match lsp.try_spawn(&language, Some(path)) {
1065 LspSpawnResult::Spawned => {
1066 for sh in lsp.get_handles_mut(&language) {
1071 tracing::info!("Sending didOpen to LSP '{}' for: {}", sh.name, uri.as_str());
1072 if let Err(e) =
1073 sh.handle
1074 .did_open(uri.as_uri().clone(), text.clone(), language.clone())
1075 {
1076 tracing::warn!("Failed to send didOpen to LSP '{}': {}", sh.name, e);
1077 } else {
1078 metadata.lsp_opened_with.insert(sh.handle.id());
1079 }
1080 }
1081
1082 if let Some(sh) =
1089 lsp.handle_for_feature_mut(&language, crate::types::LspFeature::Diagnostics)
1090 {
1091 let request_id = {
1092 let id = *__next_id;
1093 *__next_id += 1;
1094 id
1095 };
1096 if let Err(e) = sh.handle.document_diagnostic(
1097 request_id,
1098 uri.as_uri().clone(),
1099 previous_result_id,
1100 ) {
1101 tracing::debug!("Failed to request pull diagnostics: {}", e);
1102 } else {
1103 tracing::info!(
1104 "Requested pull diagnostics for {} (request_id={})",
1105 uri.as_str(),
1106 request_id
1107 );
1108 }
1109 }
1110
1111 if enable_inlay_hints {
1112 if let Some(sh) =
1113 lsp.handle_for_feature_mut(&language, crate::types::LspFeature::InlayHints)
1114 {
1115 let request_id = {
1116 let id = *__next_id;
1117 *__next_id += 1;
1118 id
1119 };
1120
1121 if let Err(e) = sh.handle.inlay_hints(
1122 request_id,
1123 uri.as_uri().clone(),
1124 0,
1125 0,
1126 last_line,
1127 last_char,
1128 ) {
1129 tracing::debug!("Failed to request inlay hints: {}", e);
1130 } else {
1131 self.active_window_mut()
1132 .pending_inlay_hints_requests
1133 .insert(
1134 request_id,
1135 super::InlayHintsRequest {
1136 buffer_id,
1137 version: buffer_version,
1138 },
1139 );
1140 tracing::info!(
1141 "Requested inlay hints for {} (request_id={})",
1142 uri.as_str(),
1143 request_id
1144 );
1145 }
1146 }
1147 }
1148
1149 self.active_window_mut()
1151 .schedule_folding_ranges_refresh(buffer_id);
1152 }
1153 LspSpawnResult::NotAutoStart => {
1154 tracing::debug!(
1155 "LSP for {} not auto-starting (auto_start=false). Click the LSP indicator to start manually.",
1156 language
1157 );
1158 }
1159 LspSpawnResult::NotConfigured => {
1160 tracing::debug!("No LSP server configured for language: {}", language);
1161 }
1162 LspSpawnResult::Disabled => {
1163 tracing::debug!("LSP disabled in config for language: {}", language);
1164 }
1165 LspSpawnResult::Failed => {
1166 tracing::warn!("Failed to spawn LSP client for language: {}", language);
1167 }
1168 }
1169 }
1170
1171 pub(crate) fn watch_file(&mut self, path: &Path) {
1174 if let Ok(metadata) = self.authority.filesystem.metadata(path) {
1176 if let Some(mtime) = metadata.modified {
1177 self.file_mod_times_mut().insert(path.to_path_buf(), mtime);
1178 }
1179 }
1180 }
1181
1182 pub(crate) fn notify_lsp_file_changed(&mut self, path: &Path) {
1184 use crate::services::lsp::manager::LspSpawnResult;
1185
1186 let Some(lsp_uri) = super::types::file_path_to_lsp_uri_with_translation(
1187 path,
1188 self.authority.path_translation.as_ref(),
1189 ) else {
1190 return;
1191 };
1192
1193 let Some((buffer_id, content, language)) = self
1195 .buffers()
1196 .iter()
1197 .find(|(_, s)| s.buffer.file_path() == Some(path))
1198 .and_then(|(id, state)| {
1199 state
1200 .buffer
1201 .to_string()
1202 .map(|t| (*id, t, state.language.clone()))
1203 })
1204 else {
1205 return;
1206 };
1207
1208 let spawn_result = {
1210 let __active_id = self.active_window;
1211 let Some(lsp) = self
1212 .windows
1213 .get_mut(&__active_id)
1214 .and_then(|w| w.lsp.as_mut())
1215 else {
1216 return;
1217 };
1218 lsp.try_spawn(&language, Some(path))
1219 };
1220
1221 if spawn_result != LspSpawnResult::Spawned {
1223 return;
1224 }
1225
1226 {
1228 let opened_with = self
1229 .active_window()
1230 .buffer_metadata
1231 .get(&buffer_id)
1232 .map(|m| m.lsp_opened_with.clone())
1233 .unwrap_or_default();
1234
1235 let __active_id = self.active_window;
1236
1237 if let Some(lsp) = self
1238 .windows
1239 .get_mut(&__active_id)
1240 .and_then(|w| w.lsp.as_mut())
1241 {
1242 for sh in lsp.get_handles_mut(&language) {
1243 if opened_with.contains(&sh.handle.id()) {
1244 continue;
1245 }
1246 if let Err(e) =
1247 sh.handle
1248 .did_open(lsp_uri.clone(), content.clone(), language.clone())
1249 {
1250 tracing::warn!(
1251 "Failed to send didOpen to LSP '{}' before didChange: {}",
1252 sh.name,
1253 e
1254 );
1255 } else {
1256 tracing::debug!(
1257 "Sent didOpen for {} to LSP '{}' before file change notification",
1258 lsp_uri.as_str(),
1259 sh.name
1260 );
1261 }
1262 }
1263 }
1264
1265 let active_id = self.active_window;
1267 if let Some(__win) = self.windows.get_mut(&active_id) {
1268 if let (Some(lsp), Some(metadata)) = (
1269 __win.lsp.as_ref(),
1270 __win.buffer_metadata.get_mut(&buffer_id),
1271 ) {
1272 for sh in lsp.get_handles(&language) {
1273 metadata.lsp_opened_with.insert(sh.handle.id());
1274 }
1275 }
1276 }
1277 }
1278
1279 let __active_id = self.active_window;
1281 if let Some(lsp) = self
1282 .windows
1283 .get_mut(&__active_id)
1284 .and_then(|w| w.lsp.as_mut())
1285 {
1286 let content_change = TextDocumentContentChangeEvent {
1287 range: None, range_length: None,
1289 text: content,
1290 };
1291 for sh in lsp.get_handles_mut(&language) {
1292 if let Err(e) = sh
1293 .handle
1294 .did_change(lsp_uri.clone(), vec![content_change.clone()])
1295 {
1296 tracing::warn!("Failed to notify LSP '{}' of file change: {}", sh.name, e);
1297 }
1298 }
1299 }
1300 }
1301
1302 pub(crate) fn revert_buffer_by_id(
1308 &mut self,
1309 buffer_id: BufferId,
1310 path: &Path,
1311 ) -> anyhow::Result<()> {
1312 let old_cursors = self
1316 .windows
1317 .get(&self.active_window)
1318 .and_then(|w| w.buffers.splits())
1319 .map(|(_, vs)| vs)
1320 .expect("active window must have a populated split layout")
1321 .values()
1322 .find_map(|vs| {
1323 if vs.keyed_states.contains_key(&buffer_id) {
1324 vs.keyed_states.get(&buffer_id).map(|bs| bs.cursors.clone())
1325 } else {
1326 None
1327 }
1328 })
1329 .unwrap_or_default();
1330 let (old_buffer_settings, old_editing_disabled) = self
1331 .buffers()
1332 .get(&buffer_id)
1333 .map(|s| (s.buffer_settings.clone(), s.editing_disabled))
1334 .unwrap_or_default();
1335
1336 let mut new_state = EditorState::from_file_with_languages(
1338 path,
1339 self.terminal_width,
1340 self.terminal_height,
1341 self.config.editor.large_file_threshold_bytes as usize,
1342 &self.grammar_registry,
1343 &self.config.languages,
1344 std::sync::Arc::clone(&self.authority.filesystem),
1345 )?;
1346
1347 let new_file_size = new_state.buffer.len();
1349
1350 let mut restored_cursors = old_cursors;
1352 restored_cursors.map(|cursor| {
1353 cursor.position = cursor.position.min(new_file_size);
1354 cursor.clear_selection();
1355 });
1356 new_state.buffer_settings = old_buffer_settings;
1358 new_state.editing_disabled = old_editing_disabled;
1359 if let Some(state) = self
1363 .windows
1364 .get_mut(&self.active_window)
1365 .map(|w| &mut w.buffers)
1366 .expect("active window present")
1367 .get_mut(&buffer_id)
1368 {
1369 *state = new_state;
1370 }
1371
1372 for vs in self
1374 .windows
1375 .get_mut(&self.active_window)
1376 .and_then(|w| w.split_view_states_mut())
1377 .expect("active window must have a populated split layout")
1378 .values_mut()
1379 {
1380 if let Some(buf_state) = vs.keyed_states.get_mut(&buffer_id) {
1381 buf_state.cursors = restored_cursors.clone();
1382 }
1383 }
1384
1385 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1387 *event_log = EventLog::new();
1388 }
1389
1390 self.active_window_mut().seen_byte_ranges.remove(&buffer_id);
1392
1393 if let Ok(metadata) = self.authority.filesystem.metadata(path) {
1395 if let Some(mtime) = metadata.modified {
1396 self.file_mod_times_mut().insert(path.to_path_buf(), mtime);
1397 }
1398 }
1399
1400 self.notify_lsp_file_changed(path);
1402
1403 Ok(())
1404 }
1405
1406 pub fn handle_file_changed(&mut self, changed_path: &str) {
1408 let path = PathBuf::from(changed_path);
1409
1410 let buffer_ids: Vec<BufferId> = self
1412 .buffers()
1413 .iter()
1414 .filter(|(_, state)| state.buffer.file_path() == Some(&path))
1415 .map(|(id, _)| *id)
1416 .collect();
1417
1418 if buffer_ids.is_empty() {
1419 return;
1420 }
1421
1422 for buffer_id in buffer_ids {
1423 if self
1426 .active_window()
1427 .terminal_buffers
1428 .contains_key(&buffer_id)
1429 {
1430 continue;
1431 }
1432
1433 if let Some(meta) = self.active_window().buffer_metadata.get(&buffer_id) {
1440 if !meta.auto_revert_enabled {
1441 continue;
1442 }
1443 }
1444
1445 let state = match self
1446 .windows
1447 .get(&self.active_window)
1448 .map(|w| &w.buffers)
1449 .expect("active window present")
1450 .get(&buffer_id)
1451 {
1452 Some(s) => s,
1453 None => continue,
1454 };
1455
1456 let current_mtime = match self
1460 .authority
1461 .filesystem
1462 .metadata(&path)
1463 .ok()
1464 .and_then(|m| m.modified)
1465 {
1466 Some(mtime) => mtime,
1467 None => continue, };
1469
1470 let dominated_by_stored = self
1471 .file_mod_times()
1472 .get(&path)
1473 .map(|stored| current_mtime <= *stored)
1474 .unwrap_or(false);
1475
1476 if dominated_by_stored {
1477 continue;
1478 }
1479
1480 if state.buffer.is_modified() {
1482 self.active_window_mut().status_message = Some(format!(
1483 "File {} changed on disk (buffer has unsaved changes)",
1484 path.display()
1485 ));
1486 continue;
1487 }
1488
1489 if self.active_window().auto_revert_enabled {
1491 let still_needs_revert = self
1495 .file_mod_times()
1496 .get(&path)
1497 .map(|stored| current_mtime > *stored)
1498 .unwrap_or(true);
1499
1500 if !still_needs_revert {
1501 continue;
1502 }
1503
1504 let is_active_buffer = buffer_id == self.active_buffer();
1506
1507 if is_active_buffer {
1508 if let Err(e) = self.revert_file() {
1510 tracing::error!("Failed to auto-revert file {:?}: {}", path, e);
1511 } else {
1512 tracing::info!("Auto-reverted file: {:?}", path);
1513 }
1514 } else {
1515 if let Err(e) = self.revert_buffer_by_id(buffer_id, &path) {
1518 tracing::error!("Failed to auto-revert background file {:?}: {}", path, e);
1519 } else {
1520 tracing::info!("Auto-reverted file: {:?}", path);
1521 }
1522 }
1523
1524 self.watch_file(&path);
1526 }
1527 }
1528 }
1529
1530 pub fn check_save_conflict(&self) -> Option<std::time::SystemTime> {
1533 let path = self.active_state().buffer.file_path()?;
1534
1535 let current_mtime = self
1537 .authority
1538 .filesystem
1539 .metadata(path)
1540 .ok()
1541 .and_then(|m| m.modified)?;
1542
1543 match self.file_mod_times().get(path) {
1545 Some(recorded_mtime) if current_mtime > *recorded_mtime => {
1546 Some(current_mtime)
1548 }
1549 _ => None,
1550 }
1551 }
1552}
1553
1554pub(crate) fn load_gitignore_via_fs(fs: &dyn FileSystem, explorer: &mut FileTreeView, dir: &Path) {
1559 let gitignore_path = dir.join(".gitignore");
1560 let meta = match fs.metadata(&gitignore_path) {
1561 Ok(m) => m,
1562 Err(_) => return,
1563 };
1564 let bytes = match fs.read_file(&gitignore_path) {
1565 Ok(b) => b,
1566 Err(e) => {
1567 tracing::warn!("Failed to read {:?}: {}", gitignore_path, e);
1568 return;
1569 }
1570 };
1571 explorer.load_gitignore_from_bytes(dir, &bytes, meta.modified);
1572}