1use crate::model::buffer::SudoSaveRequired;
12use crate::view::prompt::PromptType;
13use std::path::{Path, PathBuf};
14
15use lsp_types::TextDocumentContentChangeEvent;
16use rust_i18n::t;
17
18use crate::model::event::{BufferId, EventLog};
19use crate::services::lsp::manager::LspSpawnResult;
20use crate::state::EditorState;
21
22use super::{BufferMetadata, Editor};
23
24impl Editor {
25 pub fn save(&mut self) -> anyhow::Result<()> {
27 let path = self
28 .active_state()
29 .buffer
30 .file_path()
31 .map(|p| p.to_path_buf());
32
33 match self.active_state_mut().buffer.save() {
34 Ok(()) => self.finalize_save(path),
35 Err(e) => {
36 if let Some(sudo_info) = e.downcast_ref::<SudoSaveRequired>() {
37 let info = sudo_info.clone();
38 self.start_prompt(
39 t!("prompt.sudo_save_confirm").to_string(),
40 PromptType::ConfirmSudoSave { info },
41 );
42 Ok(())
43 } else {
44 Err(e)
45 }
46 }
47 }
48 }
49
50 pub(crate) fn finalize_save(&mut self, path: Option<PathBuf>) -> anyhow::Result<()> {
52 let buffer_id = self.active_buffer();
53 self.finalize_save_buffer(buffer_id, path, false)
54 }
55
56 pub(crate) fn finalize_save_buffer(
58 &mut self,
59 buffer_id: BufferId,
60 path: Option<PathBuf>,
61 silent: bool,
62 ) -> anyhow::Result<()> {
63 if let Some(ref p) = path {
65 if let Some(state) = self.buffers.get_mut(&buffer_id) {
66 if state.language == "text" {
67 let detected =
68 crate::primitives::detected_language::DetectedLanguage::from_path(
69 p,
70 &self.grammar_registry,
71 &self.config.languages,
72 );
73 state.apply_language(detected);
74 }
75 }
76 }
77
78 if !silent {
79 self.status_message = Some(t!("status.file_saved").to_string());
80 }
81
82 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
84 event_log.mark_saved();
85 }
86
87 if let Some(ref p) = path {
89 if let Ok(metadata) = self.filesystem.metadata(p) {
90 if let Some(mtime) = metadata.modified {
91 self.file_mod_times.insert(p.clone(), mtime);
92 }
93 }
94 }
95
96 self.notify_lsp_save_buffer(buffer_id);
98
99 if let Err(e) = self.delete_buffer_recovery(buffer_id) {
101 tracing::warn!("Failed to delete recovery file: {}", e);
102 }
103
104 if let Some(ref p) = path {
106 self.emit_event(
107 crate::model::control_event::events::FILE_SAVED.name,
108 serde_json::json!({
109 "path": p.display().to_string()
110 }),
111 );
112 }
113
114 if let Some(ref p) = path {
116 self.plugin_manager.run_hook(
117 "after_file_save",
118 crate::services::plugins::hooks::HookArgs::AfterFileSave {
119 buffer_id,
120 path: p.clone(),
121 },
122 );
123 }
124
125 if !silent {
131 match self.run_on_save_actions() {
132 Ok(true) => {
133 if self.status_message.as_deref() == Some(&t!("status.file_saved")) {
136 self.status_message =
137 Some(t!("status.file_saved_with_actions").to_string());
138 }
139 }
141 Ok(false) => {
142 }
144 Err(e) => {
145 self.status_message = Some(e);
147 }
148 }
149 }
150
151 Ok(())
152 }
153
154 pub fn auto_save_persistent_buffers(&mut self) -> anyhow::Result<usize> {
157 if !self.config.editor.auto_save_enabled {
158 return Ok(0);
159 }
160
161 let interval =
163 std::time::Duration::from_secs(self.config.editor.auto_save_interval_secs as u64);
164 if self
165 .time_source
166 .elapsed_since(self.last_persistent_auto_save)
167 < interval
168 {
169 return Ok(0);
170 }
171
172 self.last_persistent_auto_save = self.time_source.now();
173
174 let mut to_save = Vec::new();
176 for (id, state) in &self.buffers {
177 if state.buffer.is_modified() {
178 if let Some(path) = state.buffer.file_path() {
179 to_save.push((*id, path.to_path_buf()));
180 }
181 }
182 }
183
184 let mut count = 0;
185 for (id, path) in to_save {
186 if let Some(state) = self.buffers.get_mut(&id) {
187 match state.buffer.save() {
188 Ok(()) => {
189 self.finalize_save_buffer(id, Some(path), true)?;
190 count += 1;
191 }
192 Err(e) => {
193 if e.downcast_ref::<SudoSaveRequired>().is_some() {
195 tracing::debug!(
196 "Auto-save skipped for {:?} (sudo required)",
197 path.display()
198 );
199 } else {
200 tracing::warn!("Auto-save failed for {:?}: {}", path.display(), e);
201 }
202 }
203 }
204 }
205 }
206
207 Ok(count)
208 }
209
210 pub fn revert_file(&mut self) -> anyhow::Result<bool> {
213 let path = match self.active_state().buffer.file_path() {
214 Some(p) => p.to_path_buf(),
215 None => {
216 self.status_message = Some(t!("status.no_file_to_revert").to_string());
217 return Ok(false);
218 }
219 };
220
221 if !path.exists() {
222 self.status_message =
223 Some(t!("status.file_not_exists", path = path.display().to_string()).to_string());
224 return Ok(false);
225 }
226
227 let active_split = self.split_manager.active_split();
229 let (old_top_byte, old_left_column) = self
230 .split_view_states
231 .get(&active_split)
232 .map(|vs| (vs.viewport.top_byte, vs.viewport.left_column))
233 .unwrap_or((0, 0));
234 let old_cursors = self.active_cursors().clone();
235
236 let old_buffer_settings = self.active_state().buffer_settings.clone();
238 let old_editing_disabled = self.active_state().editing_disabled;
239
240 let mut new_state = EditorState::from_file_with_languages(
242 &path,
243 self.terminal_width,
244 self.terminal_height,
245 self.config.editor.large_file_threshold_bytes as usize,
246 &self.grammar_registry,
247 &self.config.languages,
248 std::sync::Arc::clone(&self.filesystem),
249 )?;
250
251 let new_file_size = new_state.buffer.len();
253 let mut restored_cursors = old_cursors;
254 restored_cursors.map(|cursor| {
255 cursor.position = cursor.position.min(new_file_size);
256 cursor.clear_selection();
258 });
259 new_state.buffer_settings = old_buffer_settings;
261 new_state.editing_disabled = old_editing_disabled;
262 let buffer_id = self.active_buffer();
266 if let Some(state) = self.buffers.get_mut(&buffer_id) {
267 *state = new_state;
268 }
270
271 let active_split = self.split_manager.active_split();
273 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
274 view_state.cursors = restored_cursors;
275 }
276
277 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
279 view_state.viewport.top_byte = old_top_byte.min(new_file_size);
280 view_state.viewport.left_column = old_left_column;
281 }
282
283 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
285 *event_log = EventLog::new();
286 }
287
288 self.seen_byte_ranges.remove(&buffer_id);
290
291 if let Ok(metadata) = self.filesystem.metadata(&path) {
293 if let Some(mtime) = metadata.modified {
294 self.file_mod_times.insert(path.clone(), mtime);
295 }
296 }
297
298 self.notify_lsp_file_changed(&path);
300
301 self.status_message = Some(t!("status.reverted").to_string());
302 Ok(true)
303 }
304
305 pub fn toggle_auto_revert(&mut self) {
307 self.auto_revert_enabled = !self.auto_revert_enabled;
308
309 if self.auto_revert_enabled {
310 self.status_message = Some(t!("status.auto_revert_enabled").to_string());
311 } else {
312 self.status_message = Some(t!("status.auto_revert_disabled").to_string());
313 }
314 }
315
316 pub fn poll_file_changes(&mut self) -> bool {
321 if !self.auto_revert_enabled {
323 return false;
324 }
325
326 let poll_interval =
328 std::time::Duration::from_millis(self.config.editor.auto_revert_poll_interval_ms);
329 let elapsed = self.time_source.elapsed_since(self.last_auto_revert_poll);
330 tracing::trace!(
331 "poll_file_changes: elapsed={:?}, poll_interval={:?}",
332 elapsed,
333 poll_interval
334 );
335 if elapsed < poll_interval {
336 return false;
337 }
338 self.last_auto_revert_poll = self.time_source.now();
339
340 let files_to_check: Vec<PathBuf> = self
342 .buffers
343 .values()
344 .filter_map(|state| state.buffer.file_path().map(PathBuf::from))
345 .collect();
346
347 let mut any_changed = false;
348
349 for path in files_to_check {
350 let current_mtime = match self.filesystem.metadata(&path) {
352 Ok(meta) => match meta.modified {
353 Some(mtime) => mtime,
354 None => continue,
355 },
356 Err(_) => continue, };
358
359 if let Some(&stored_mtime) = self.file_mod_times.get(&path) {
361 if current_mtime != stored_mtime {
362 let path_str = path.display().to_string();
366 if self.handle_async_file_changed(path_str) {
367 any_changed = true;
368 }
369 }
370 } else {
371 self.file_mod_times.insert(path, current_mtime);
373 }
374 }
375
376 any_changed
377 }
378
379 pub fn poll_file_tree_changes(&mut self) -> bool {
384 let poll_interval =
386 std::time::Duration::from_millis(self.config.editor.file_tree_poll_interval_ms);
387 if self.time_source.elapsed_since(self.last_file_tree_poll) < poll_interval {
388 return false;
389 }
390 self.last_file_tree_poll = self.time_source.now();
391
392 let Some(explorer) = &self.file_explorer else {
394 return false;
395 };
396
397 use crate::view::file_tree::NodeId;
399 let expanded_dirs: Vec<(NodeId, PathBuf)> = explorer
400 .tree()
401 .all_nodes()
402 .filter(|node| node.is_dir() && node.is_expanded())
403 .map(|node| (node.id, node.entry.path.clone()))
404 .collect();
405
406 let mut dirs_to_refresh: Vec<NodeId> = Vec::new();
408
409 for (node_id, path) in expanded_dirs {
410 let current_mtime = match self.filesystem.metadata(&path) {
412 Ok(meta) => match meta.modified {
413 Some(mtime) => mtime,
414 None => continue,
415 },
416 Err(_) => continue, };
418
419 if let Some(&stored_mtime) = self.dir_mod_times.get(&path) {
421 if current_mtime != stored_mtime {
422 self.dir_mod_times.insert(path.clone(), current_mtime);
424 dirs_to_refresh.push(node_id);
425 tracing::debug!("Directory changed: {:?}", path);
426 }
427 } else {
428 self.dir_mod_times.insert(path, current_mtime);
430 }
431 }
432
433 if dirs_to_refresh.is_empty() {
435 return false;
436 }
437
438 if let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) {
440 for node_id in dirs_to_refresh {
441 let tree = explorer.tree_mut();
442 if let Err(e) = runtime.block_on(tree.refresh_node(node_id)) {
443 tracing::warn!("Failed to refresh directory: {}", e);
444 }
445 }
446 }
447
448 true
449 }
450
451 pub(crate) fn notify_lsp_file_opened(
454 &mut self,
455 path: &Path,
456 buffer_id: BufferId,
457 metadata: &mut BufferMetadata,
458 ) {
459 let Some(language) = self.buffers.get(&buffer_id).map(|s| s.language.clone()) else {
461 tracing::debug!("No buffer state for file: {}", path.display());
462 return;
463 };
464
465 let Some(uri) = metadata.file_uri().cloned() else {
466 tracing::warn!(
467 "No URI in metadata for file: {} (failed to compute absolute path)",
468 path.display()
469 );
470 return;
471 };
472
473 let file_size = self
475 .filesystem
476 .metadata(path)
477 .ok()
478 .map(|m| m.size)
479 .unwrap_or(0);
480 if file_size > self.config.editor.large_file_threshold_bytes {
481 let reason = format!("File too large ({} bytes)", file_size);
482 tracing::warn!(
483 "Skipping LSP for large file: {} ({})",
484 path.display(),
485 reason
486 );
487 metadata.disable_lsp(reason);
488 return;
489 }
490
491 let text = match self
493 .buffers
494 .get(&buffer_id)
495 .and_then(|state| state.buffer.to_string())
496 {
497 Some(t) => t,
498 None => {
499 tracing::debug!("Buffer not fully loaded for LSP notification");
500 return;
501 }
502 };
503
504 let enable_inlay_hints = self.config.editor.enable_inlay_hints;
505 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
506
507 let (last_line, last_char) = self
509 .buffers
510 .get(&buffer_id)
511 .map(|state| {
512 let line_count = state.buffer.line_count().unwrap_or(1000);
513 (line_count.saturating_sub(1) as u32, 10000u32)
514 })
515 .unwrap_or((999, 10000));
516
517 let Some(lsp) = &mut self.lsp else {
519 tracing::debug!("No LSP manager available");
520 return;
521 };
522
523 tracing::debug!("LSP manager available for file: {}", path.display());
524 tracing::debug!(
525 "Detected language: {} for file: {}",
526 language,
527 path.display()
528 );
529 tracing::debug!("Using URI from metadata: {}", uri.as_str());
530 tracing::debug!("Attempting to spawn LSP client for language: {}", language);
531
532 match lsp.try_spawn(&language) {
533 LspSpawnResult::Spawned => {
534 if let Some(client) = lsp.get_handle_mut(&language) {
535 tracing::info!("Sending didOpen to LSP for: {}", uri.as_str());
537 if let Err(e) = client.did_open(uri.clone(), text, language.clone()) {
538 tracing::warn!("Failed to send didOpen to LSP: {}", e);
539 return;
540 }
541 tracing::info!("Successfully sent didOpen to LSP");
542
543 metadata.lsp_opened_with.insert(client.id());
545
546 let request_id = self.next_lsp_request_id;
548 self.next_lsp_request_id += 1;
549 if let Err(e) =
550 client.document_diagnostic(request_id, uri.clone(), previous_result_id)
551 {
552 tracing::debug!(
553 "Failed to request pull diagnostics (server may not support): {}",
554 e
555 );
556 } else {
557 tracing::info!(
558 "Requested pull diagnostics for {} (request_id={})",
559 uri.as_str(),
560 request_id
561 );
562 }
563
564 if enable_inlay_hints {
566 let request_id = self.next_lsp_request_id;
567 self.next_lsp_request_id += 1;
568 self.pending_inlay_hints_request = Some(request_id);
569
570 if let Err(e) =
571 client.inlay_hints(request_id, uri.clone(), 0, 0, last_line, last_char)
572 {
573 tracing::debug!(
574 "Failed to request inlay hints (server may not support): {}",
575 e
576 );
577 self.pending_inlay_hints_request = None;
578 } else {
579 tracing::info!(
580 "Requested inlay hints for {} (request_id={})",
581 uri.as_str(),
582 request_id
583 );
584 }
585 }
586
587 self.schedule_folding_ranges_refresh(buffer_id);
589 }
590 }
591 LspSpawnResult::NotAutoStart => {
592 tracing::debug!(
593 "LSP for {} not auto-starting (auto_start=false). Use command palette to start manually.",
594 language
595 );
596 }
597 LspSpawnResult::NotConfigured => {
598 tracing::debug!("No LSP server configured for language: {}", language);
599 }
600 LspSpawnResult::Failed => {
601 tracing::warn!("Failed to spawn LSP client for language: {}", language);
602 }
603 }
604 }
605
606 pub(crate) fn watch_file(&mut self, path: &Path) {
609 if let Ok(metadata) = self.filesystem.metadata(path) {
611 if let Some(mtime) = metadata.modified {
612 self.file_mod_times.insert(path.to_path_buf(), mtime);
613 }
614 }
615 }
616
617 pub(crate) fn notify_lsp_file_changed(&mut self, path: &Path) {
619 use crate::services::lsp::manager::LspSpawnResult;
620
621 let Some(lsp_uri) = super::types::file_path_to_lsp_uri(path) else {
622 return;
623 };
624
625 let Some((buffer_id, content, language)) = self
627 .buffers
628 .iter()
629 .find(|(_, s)| s.buffer.file_path() == Some(path))
630 .and_then(|(id, state)| {
631 state
632 .buffer
633 .to_string()
634 .map(|t| (*id, t, state.language.clone()))
635 })
636 else {
637 return;
638 };
639
640 let spawn_result = {
642 let Some(lsp) = self.lsp.as_mut() else {
643 return;
644 };
645 lsp.try_spawn(&language)
646 };
647
648 if spawn_result != LspSpawnResult::Spawned {
650 return;
651 }
652
653 let handle_id = {
655 let Some(lsp) = self.lsp.as_mut() else {
656 return;
657 };
658 let Some(handle) = lsp.get_handle_mut(&language) else {
659 return;
660 };
661 handle.id()
662 };
663
664 let needs_open = {
666 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
667 return;
668 };
669 !metadata.lsp_opened_with.contains(&handle_id)
670 };
671
672 if needs_open {
673 if let Some(lsp) = self.lsp.as_mut() {
675 if let Some(handle) = lsp.get_handle_mut(&language) {
676 if let Err(e) =
677 handle.did_open(lsp_uri.clone(), content.clone(), language.clone())
678 {
679 tracing::warn!("Failed to send didOpen before didChange: {}", e);
680 return;
681 }
682 tracing::debug!(
683 "Sent didOpen for {} to LSP handle {} before file change notification",
684 lsp_uri.as_str(),
685 handle_id
686 );
687 }
688 }
689
690 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
692 metadata.lsp_opened_with.insert(handle_id);
693 }
694 }
695
696 if let Some(lsp) = &mut self.lsp {
698 if let Some(client) = lsp.get_handle_mut(&language) {
699 let content_change = TextDocumentContentChangeEvent {
700 range: None, range_length: None,
702 text: content,
703 };
704 if let Err(e) = client.did_change(lsp_uri, vec![content_change]) {
705 tracing::warn!("Failed to notify LSP of file change: {}", e);
706 }
707 }
708 }
709 }
710
711 pub(crate) fn revert_buffer_by_id(
717 &mut self,
718 buffer_id: BufferId,
719 path: &Path,
720 ) -> anyhow::Result<()> {
721 let old_cursors = self
725 .split_view_states
726 .values()
727 .find_map(|vs| {
728 if vs.keyed_states.contains_key(&buffer_id) {
729 vs.keyed_states.get(&buffer_id).map(|bs| bs.cursors.clone())
730 } else {
731 None
732 }
733 })
734 .unwrap_or_default();
735 let (old_buffer_settings, old_editing_disabled) = self
736 .buffers
737 .get(&buffer_id)
738 .map(|s| (s.buffer_settings.clone(), s.editing_disabled))
739 .unwrap_or_default();
740
741 let mut new_state = EditorState::from_file_with_languages(
743 path,
744 self.terminal_width,
745 self.terminal_height,
746 self.config.editor.large_file_threshold_bytes as usize,
747 &self.grammar_registry,
748 &self.config.languages,
749 std::sync::Arc::clone(&self.filesystem),
750 )?;
751
752 let new_file_size = new_state.buffer.len();
754
755 let mut restored_cursors = old_cursors;
757 restored_cursors.map(|cursor| {
758 cursor.position = cursor.position.min(new_file_size);
759 cursor.clear_selection();
760 });
761 new_state.buffer_settings = old_buffer_settings;
763 new_state.editing_disabled = old_editing_disabled;
764 if let Some(state) = self.buffers.get_mut(&buffer_id) {
768 *state = new_state;
769 }
770
771 for vs in self.split_view_states.values_mut() {
773 if let Some(buf_state) = vs.keyed_states.get_mut(&buffer_id) {
774 buf_state.cursors = restored_cursors.clone();
775 }
776 }
777
778 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
780 *event_log = EventLog::new();
781 }
782
783 self.seen_byte_ranges.remove(&buffer_id);
785
786 if let Ok(metadata) = self.filesystem.metadata(path) {
788 if let Some(mtime) = metadata.modified {
789 self.file_mod_times.insert(path.to_path_buf(), mtime);
790 }
791 }
792
793 self.notify_lsp_file_changed(path);
795
796 Ok(())
797 }
798
799 pub fn handle_file_changed(&mut self, changed_path: &str) {
801 let path = PathBuf::from(changed_path);
802
803 let buffer_ids: Vec<BufferId> = self
805 .buffers
806 .iter()
807 .filter(|(_, state)| state.buffer.file_path() == Some(&path))
808 .map(|(id, _)| *id)
809 .collect();
810
811 if buffer_ids.is_empty() {
812 return;
813 }
814
815 for buffer_id in buffer_ids {
816 if self.terminal_buffers.contains_key(&buffer_id) {
819 continue;
820 }
821
822 let state = match self.buffers.get(&buffer_id) {
823 Some(s) => s,
824 None => continue,
825 };
826
827 let current_mtime = match self
831 .filesystem
832 .metadata(&path)
833 .ok()
834 .and_then(|m| m.modified)
835 {
836 Some(mtime) => mtime,
837 None => continue, };
839
840 let dominated_by_stored = self
841 .file_mod_times
842 .get(&path)
843 .map(|stored| current_mtime <= *stored)
844 .unwrap_or(false);
845
846 if dominated_by_stored {
847 continue;
848 }
849
850 if state.buffer.is_modified() {
852 self.status_message = Some(format!(
853 "File {} changed on disk (buffer has unsaved changes)",
854 path.display()
855 ));
856 continue;
857 }
858
859 if self.auto_revert_enabled {
861 let still_needs_revert = self
865 .file_mod_times
866 .get(&path)
867 .map(|stored| current_mtime > *stored)
868 .unwrap_or(true);
869
870 if !still_needs_revert {
871 continue;
872 }
873
874 let is_active_buffer = buffer_id == self.active_buffer();
876
877 if is_active_buffer {
878 if let Err(e) = self.revert_file() {
880 tracing::error!("Failed to auto-revert file {:?}: {}", path, e);
881 } else {
882 tracing::info!("Auto-reverted file: {:?}", path);
883 }
884 } else {
885 if let Err(e) = self.revert_buffer_by_id(buffer_id, &path) {
888 tracing::error!("Failed to auto-revert background file {:?}: {}", path, e);
889 } else {
890 tracing::info!("Auto-reverted file: {:?}", path);
891 }
892 }
893
894 self.watch_file(&path);
896 }
897 }
898 }
899
900 pub fn check_save_conflict(&self) -> Option<std::time::SystemTime> {
903 let path = self.active_state().buffer.file_path()?;
904
905 let current_mtime = self
907 .filesystem
908 .metadata(path)
909 .ok()
910 .and_then(|m| m.modified)?;
911
912 match self.file_mod_times.get(path) {
914 Some(recorded_mtime) if current_mtime > *recorded_mtime => {
915 Some(current_mtime)
917 }
918 _ => None,
919 }
920 }
921}