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 true
806 } else {
807 false
808 }
809 } else {
810 false
811 }
812 } else {
813 false
814 };
815
816 if dirs_to_refresh.is_empty() && !git_index_changed {
817 return false;
818 }
819
820 let refreshed_dirs: Vec<PathBuf> = dirs_to_refresh.iter().map(|(_, p)| p.clone()).collect();
825 self.refresh_file_tree_dirs(&refreshed_dirs);
826 let fs = self.authority().filesystem.clone();
827 if let Some(explorer) = self.file_explorer_mut().as_mut() {
828 for dir in refreshed_dirs {
829 load_gitignore_via_fs(fs.as_ref(), explorer, &dir);
830 }
831 }
832
833 if git_index_changed {
838 let cwd = self.working_dir().to_path_buf();
839 self.notify_file_explorer_change(&cwd);
840 }
841
842 true
843 }
844
845 pub fn refresh_file_tree_dirs(&mut self, paths: &[PathBuf]) {
870 let active_id = self.active_window;
871 let (Some(runtime), Some(explorer)) = (
872 self.tokio_runtime.as_ref(),
873 self.windows
874 .get_mut(&active_id)
875 .and_then(|w| w.file_explorer.as_mut()),
876 ) else {
877 return;
878 };
879 let cursor_path: Option<PathBuf> = explorer.get_selected_entry().map(|e| e.path.clone());
880 for path in paths {
885 let Some(id_now) = explorer.tree().get_node_by_path(path).map(|n| n.id) else {
886 continue;
887 };
888 let tree = explorer.tree_mut();
889 if let Err(e) = runtime.block_on(tree.reload_expanded_node(id_now)) {
890 tracing::warn!("Failed to refresh directory {:?}: {}", path, e);
891 }
892 }
893 if let Some(path) = cursor_path {
894 if explorer.tree().get_node_by_path(&path).is_some() {
895 explorer.navigate_to_path(&path);
896 } else {
897 let root_id = explorer.tree().root_id();
898 explorer.set_selected(Some(root_id));
899 }
900 }
901 }
902
903 fn sync_gitignores_from_disk(&mut self) -> bool {
906 let fs = self.authority().filesystem.clone();
907 let Some(explorer) = self.file_explorer_mut() else {
908 return false;
909 };
910 let dirs = explorer.ignore_patterns().loaded_gitignore_dirs();
911 let mut changed = false;
912 for dir in dirs {
913 let gitignore_path = dir.join(".gitignore");
914 match fs.metadata(&gitignore_path) {
915 Err(_) => {
916 explorer.ignore_patterns_mut().remove_gitignore(&dir);
917 changed = true;
918 }
919 Ok(meta) => {
920 let stored = explorer.ignore_patterns().stored_gitignore_mtime(&dir);
921 if stored != meta.modified {
922 load_gitignore_via_fs(fs.as_ref(), explorer, &dir);
923 changed = true;
924 }
925 }
926 }
927 }
928 changed
929 }
930
931 fn resolve_git_index(&self) -> Option<PathBuf> {
935 let spawner = &self.authority().process_spawner;
936 let cwd = self.working_dir().to_string_lossy().to_string();
937
938 let result = if let Some(ref rt) = self.tokio_runtime {
942 rt.block_on(spawner.spawn(
943 "git".to_string(),
944 vec!["rev-parse".to_string(), "--git-dir".to_string()],
945 Some(cwd),
946 ))
947 } else {
948 return None;
951 };
952
953 let output = result.ok()?;
954 if output.exit_code != 0 {
955 return None;
956 }
957 let git_dir = output.stdout.trim();
958 let git_dir_path = if std::path::Path::new(git_dir).is_absolute() {
959 PathBuf::from(git_dir)
960 } else {
961 self.working_dir().join(git_dir)
962 };
963 Some(git_dir_path.join("index"))
964 }
965
966 pub(crate) fn notify_lsp_file_opened(
974 &mut self,
975 path: &Path,
976 buffer_id: BufferId,
977 metadata: &mut BufferMetadata,
978 ) {
979 self.active_window_mut()
980 .notify_lsp_file_opened(path, buffer_id, metadata);
981 }
982
983 pub(crate) fn watch_file(&mut self, path: &Path) {
989 self.active_window_mut().watch_file(path);
990 }
991
992 pub(crate) fn notify_lsp_file_changed(&mut self, path: &Path) {
994 use crate::services::lsp::manager::LspSpawnResult;
995
996 let Some(lsp_uri) = super::types::file_path_to_lsp_uri_with_translation(
997 path,
998 self.authority().path_translation.as_ref(),
999 ) else {
1000 return;
1001 };
1002
1003 let Some((buffer_id, content, language)) = self
1005 .buffers()
1006 .iter()
1007 .find(|(_, s)| s.buffer.file_path() == Some(path))
1008 .and_then(|(id, state)| {
1009 state
1010 .buffer
1011 .to_string()
1012 .map(|t| (*id, t, state.language.clone()))
1013 })
1014 else {
1015 return;
1016 };
1017
1018 let spawn_result = {
1020 let __active_id = self.active_window;
1021 let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) else {
1022 return;
1023 };
1024 lsp.try_spawn(&language, Some(path))
1025 };
1026
1027 if spawn_result != LspSpawnResult::Spawned {
1029 return;
1030 }
1031
1032 {
1034 let opened_with = self
1035 .active_window()
1036 .buffer_metadata
1037 .get(&buffer_id)
1038 .map(|m| m.lsp_opened_with.clone())
1039 .unwrap_or_default();
1040
1041 let __active_id = self.active_window;
1042
1043 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
1044 for sh in lsp.get_handles_mut(&language) {
1045 if opened_with.contains(&sh.handle.id()) {
1046 continue;
1047 }
1048 if let Err(e) =
1049 sh.handle
1050 .did_open(lsp_uri.clone(), content.clone(), language.clone())
1051 {
1052 tracing::warn!(
1053 "Failed to send didOpen to LSP '{}' before didChange: {}",
1054 sh.name,
1055 e
1056 );
1057 } else {
1058 tracing::debug!(
1059 "Sent didOpen for {} to LSP '{}' before file change notification",
1060 lsp_uri.as_str(),
1061 sh.name
1062 );
1063 }
1064 }
1065 }
1066
1067 let active_id = self.active_window;
1069 if let Some(__win) = self.windows.get_mut(&active_id) {
1070 if let Some(metadata) = __win.buffer_metadata.get_mut(&buffer_id) {
1071 for sh in __win.lsp.get_handles(&language) {
1072 metadata.lsp_opened_with.insert(sh.handle.id());
1073 }
1074 }
1075 }
1076 }
1077
1078 let __active_id = self.active_window;
1080 if let Some(lsp) = self.windows.get_mut(&__active_id).map(|w| &mut w.lsp) {
1081 let content_change = TextDocumentContentChangeEvent {
1082 range: None, range_length: None,
1084 text: content,
1085 };
1086 for sh in lsp.get_handles_mut(&language) {
1087 if let Err(e) = sh
1088 .handle
1089 .did_change(lsp_uri.clone(), vec![content_change.clone()])
1090 {
1091 tracing::warn!("Failed to notify LSP '{}' of file change: {}", sh.name, e);
1092 }
1093 }
1094 }
1095 }
1096
1097 pub(crate) fn revert_buffer_by_id(
1103 &mut self,
1104 buffer_id: BufferId,
1105 path: &Path,
1106 ) -> anyhow::Result<()> {
1107 let old_cursors = self
1111 .windows
1112 .get(&self.active_window)
1113 .and_then(|w| w.buffers.splits())
1114 .map(|(_, vs)| vs)
1115 .expect("active window must have a populated split layout")
1116 .values()
1117 .find_map(|vs| {
1118 if vs.keyed_states.contains_key(&buffer_id) {
1119 vs.keyed_states.get(&buffer_id).map(|bs| bs.cursors.clone())
1120 } else {
1121 None
1122 }
1123 })
1124 .unwrap_or_default();
1125 let (old_buffer_settings, old_editing_disabled) = self
1126 .buffers()
1127 .get(&buffer_id)
1128 .map(|s| (s.buffer_settings.clone(), s.editing_disabled))
1129 .unwrap_or_default();
1130
1131 let mut new_state = EditorState::from_file_with_languages(
1133 path,
1134 self.terminal_width,
1135 self.terminal_height,
1136 self.config.editor.large_file_threshold_bytes as usize,
1137 &self.grammar_registry,
1138 &self.config.languages,
1139 std::sync::Arc::clone(&self.authority().filesystem),
1140 )?;
1141
1142 let new_file_size = new_state.buffer.len();
1144
1145 let mut restored_cursors = old_cursors;
1147 restored_cursors.map(|cursor| {
1148 cursor.position = cursor.position.min(new_file_size);
1149 cursor.clear_selection();
1150 });
1151 new_state.buffer_settings = old_buffer_settings;
1153 new_state.editing_disabled = old_editing_disabled;
1154 if let Some(state) = self
1158 .windows
1159 .get_mut(&self.active_window)
1160 .map(|w| &mut w.buffers)
1161 .expect("active window present")
1162 .get_mut(&buffer_id)
1163 {
1164 *state = new_state;
1165 }
1166
1167 for vs in self
1169 .windows
1170 .get_mut(&self.active_window)
1171 .and_then(|w| w.split_view_states_mut())
1172 .expect("active window must have a populated split layout")
1173 .values_mut()
1174 {
1175 if let Some(buf_state) = vs.keyed_states.get_mut(&buffer_id) {
1176 buf_state.cursors = restored_cursors.clone();
1177 }
1178 }
1179
1180 if let Some(event_log) = self.active_window_mut().event_logs.get_mut(&buffer_id) {
1182 *event_log = EventLog::new();
1183 }
1184
1185 self.active_window_mut().seen_byte_ranges.remove(&buffer_id);
1187
1188 if let Ok(metadata) = self.authority().filesystem.metadata(path) {
1190 if let Some(mtime) = metadata.modified {
1191 self.file_mod_times_mut().insert(path.to_path_buf(), mtime);
1192 }
1193 }
1194
1195 self.notify_lsp_file_changed(path);
1197
1198 Ok(())
1199 }
1200
1201 pub fn handle_file_changed(&mut self, changed_path: &str) {
1203 let path = PathBuf::from(changed_path);
1204
1205 let buffer_ids: Vec<BufferId> = self
1207 .buffers()
1208 .iter()
1209 .filter(|(_, state)| state.buffer.file_path() == Some(&path))
1210 .map(|(id, _)| *id)
1211 .collect();
1212
1213 if buffer_ids.is_empty() {
1214 return;
1215 }
1216
1217 for buffer_id in buffer_ids {
1218 if self
1221 .active_window()
1222 .terminal_buffers
1223 .contains_key(&buffer_id)
1224 {
1225 continue;
1226 }
1227
1228 if let Some(meta) = self.active_window().buffer_metadata.get(&buffer_id) {
1235 if !meta.auto_revert_enabled {
1236 continue;
1237 }
1238 }
1239
1240 let state = match self
1241 .windows
1242 .get(&self.active_window)
1243 .map(|w| &w.buffers)
1244 .expect("active window present")
1245 .get(&buffer_id)
1246 {
1247 Some(s) => s,
1248 None => continue,
1249 };
1250
1251 let current_mtime = match self
1255 .authority()
1256 .filesystem
1257 .metadata(&path)
1258 .ok()
1259 .and_then(|m| m.modified)
1260 {
1261 Some(mtime) => mtime,
1262 None => continue, };
1264
1265 let dominated_by_stored = self
1266 .file_mod_times()
1267 .get(&path)
1268 .map(|stored| current_mtime <= *stored)
1269 .unwrap_or(false);
1270
1271 if dominated_by_stored {
1272 continue;
1273 }
1274
1275 if state.buffer.is_modified() {
1277 self.active_window_mut().status_message = Some(format!(
1278 "File {} changed on disk (buffer has unsaved changes)",
1279 path.display()
1280 ));
1281 continue;
1282 }
1283
1284 if self.active_window().auto_revert_enabled {
1286 let still_needs_revert = self
1290 .file_mod_times()
1291 .get(&path)
1292 .map(|stored| current_mtime > *stored)
1293 .unwrap_or(true);
1294
1295 if !still_needs_revert {
1296 continue;
1297 }
1298
1299 let is_active_buffer = buffer_id == self.active_buffer();
1301
1302 if is_active_buffer {
1303 if let Err(e) = self.revert_file() {
1305 tracing::error!("Failed to auto-revert file {:?}: {}", path, e);
1306 } else {
1307 tracing::info!("Auto-reverted file: {:?}", path);
1308 }
1309 } else {
1310 if let Err(e) = self.revert_buffer_by_id(buffer_id, &path) {
1313 tracing::error!("Failed to auto-revert background file {:?}: {}", path, e);
1314 } else {
1315 tracing::info!("Auto-reverted file: {:?}", path);
1316 }
1317 }
1318
1319 self.watch_file(&path);
1321 }
1322 }
1323 }
1324
1325 pub fn check_save_conflict(&self) -> Option<std::time::SystemTime> {
1328 let path = self.active_state().buffer.file_path()?;
1329
1330 let current_mtime = self
1332 .authority()
1333 .filesystem
1334 .metadata(path)
1335 .ok()
1336 .and_then(|m| m.modified)?;
1337
1338 match self.file_mod_times().get(path) {
1340 Some(recorded_mtime) if current_mtime > *recorded_mtime => {
1341 Some(current_mtime)
1343 }
1344 _ => None,
1345 }
1346 }
1347}
1348
1349pub(crate) fn load_gitignore_via_fs(fs: &dyn FileSystem, explorer: &mut FileTreeView, dir: &Path) {
1354 let gitignore_path = dir.join(".gitignore");
1355 let meta = match fs.metadata(&gitignore_path) {
1356 Ok(m) => m,
1357 Err(_) => return,
1358 };
1359 let bytes = match fs.read_file(&gitignore_path) {
1360 Ok(b) => b,
1361 Err(e) => {
1362 tracing::warn!("Failed to read {:?}: {}", gitignore_path, e);
1363 return;
1364 }
1365 };
1366 explorer.load_gitignore_from_bytes(dir, &bytes, meta.modified);
1367}
1368
1369impl crate::app::window::Window {
1370 pub(crate) fn notify_lsp_file_opened(
1377 &mut self,
1378 path: &Path,
1379 buffer_id: BufferId,
1380 metadata: &mut BufferMetadata,
1381 ) {
1382 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
1384 tracing::debug!("No buffer state for file: {}", path.display());
1385 return;
1386 };
1387
1388 let Some(uri) = metadata.file_uri().cloned() else {
1389 tracing::warn!(
1390 "No URI in metadata for file: {} (failed to compute absolute path)",
1391 path.display()
1392 );
1393 return;
1394 };
1395
1396 let file_size = self
1398 .authority()
1399 .filesystem
1400 .metadata(path)
1401 .ok()
1402 .map(|m| m.size)
1403 .unwrap_or(0);
1404 if file_size > self.resources.config.editor.large_file_threshold_bytes {
1405 let reason = format!("File too large ({} bytes)", file_size);
1406 tracing::debug!(
1407 "Skipping LSP for large file: {} ({})",
1408 path.display(),
1409 reason
1410 );
1411 metadata.disable_lsp(reason);
1412 return;
1413 }
1414
1415 let text = match self
1417 .buffers
1418 .get(&buffer_id)
1419 .and_then(|state| state.buffer.to_string())
1420 {
1421 Some(t) => t,
1422 None => {
1423 tracing::debug!("Buffer not fully loaded for LSP notification");
1424 return;
1425 }
1426 };
1427
1428 let enable_inlay_hints = self.resources.config.editor.enable_inlay_hints;
1429 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
1430
1431 let (last_line, last_char, buffer_version) = self
1433 .buffers
1434 .get(&buffer_id)
1435 .map(|state| {
1436 let line_count = state.buffer.line_count().unwrap_or(1000);
1437 (
1438 line_count.saturating_sub(1) as u32,
1439 10000u32,
1440 state.buffer.version(),
1441 )
1442 })
1443 .unwrap_or((999, 10000, 0));
1444
1445 let lsp = &mut self.lsp;
1447 let __next_id = &mut self.next_lsp_request_id;
1448
1449 tracing::debug!("LSP manager available for file: {}", path.display());
1450 tracing::debug!(
1451 "Detected language: {} for file: {}",
1452 language,
1453 path.display()
1454 );
1455 tracing::debug!("Using URI from metadata: {}", uri.as_str());
1456 tracing::debug!("Attempting to spawn LSP client for language: {}", language);
1457
1458 match lsp.try_spawn(&language, Some(path)) {
1459 LspSpawnResult::Spawned => {
1460 for sh in lsp.get_handles_mut(&language) {
1465 tracing::info!("Sending didOpen to LSP '{}' for: {}", sh.name, uri.as_str());
1466 if let Err(e) =
1467 sh.handle
1468 .did_open(uri.as_uri().clone(), text.clone(), language.clone())
1469 {
1470 tracing::warn!("Failed to send didOpen to LSP '{}': {}", sh.name, e);
1471 } else {
1472 metadata.lsp_opened_with.insert(sh.handle.id());
1473 }
1474 }
1475
1476 if let Some(sh) =
1483 lsp.handle_for_feature_mut(&language, crate::types::LspFeature::Diagnostics)
1484 {
1485 let request_id = {
1486 let id = *__next_id;
1487 *__next_id += 1;
1488 id
1489 };
1490 if let Err(e) = sh.handle.document_diagnostic(
1491 request_id,
1492 uri.as_uri().clone(),
1493 previous_result_id,
1494 ) {
1495 tracing::debug!("Failed to request pull diagnostics: {}", e);
1496 } else {
1497 tracing::info!(
1498 "Requested pull diagnostics for {} (request_id={})",
1499 uri.as_str(),
1500 request_id
1501 );
1502 }
1503 }
1504
1505 if enable_inlay_hints {
1506 if let Some(sh) =
1507 lsp.handle_for_feature_mut(&language, crate::types::LspFeature::InlayHints)
1508 {
1509 let request_id = {
1510 let id = *__next_id;
1511 *__next_id += 1;
1512 id
1513 };
1514
1515 if let Err(e) = sh.handle.inlay_hints(
1516 request_id,
1517 uri.as_uri().clone(),
1518 0,
1519 0,
1520 last_line,
1521 last_char,
1522 ) {
1523 tracing::debug!("Failed to request inlay hints: {}", e);
1524 } else {
1525 self.pending_inlay_hints_requests.insert(
1526 request_id,
1527 super::InlayHintsRequest {
1528 buffer_id,
1529 version: buffer_version,
1530 },
1531 );
1532 tracing::info!(
1533 "Requested inlay hints for {} (request_id={})",
1534 uri.as_str(),
1535 request_id
1536 );
1537 }
1538 }
1539 }
1540
1541 self.schedule_folding_ranges_refresh(buffer_id);
1543 }
1544 LspSpawnResult::NotAutoStart => {
1545 tracing::debug!(
1546 "LSP for {} not auto-starting (auto_start=false). Click the LSP indicator to start manually.",
1547 language
1548 );
1549 }
1550 LspSpawnResult::NotConfigured => {
1551 tracing::debug!("No LSP server configured for language: {}", language);
1552 }
1553 LspSpawnResult::Disabled => {
1554 tracing::debug!("LSP disabled in config for language: {}", language);
1555 }
1556 LspSpawnResult::Failed => {
1557 tracing::warn!("Failed to spawn LSP client for language: {}", language);
1558 }
1559 }
1560 }
1561
1562 pub(crate) fn watch_file(&mut self, path: &Path) {
1565 if let Ok(metadata) = self.authority().filesystem.metadata(path) {
1566 if let Some(mtime) = metadata.modified {
1567 self.file_mod_times.insert(path.to_path_buf(), mtime);
1568 }
1569 }
1570 }
1571}