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::{detect_language, 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 if let Some(ref p) = path {
54 let buffer_id = self.active_buffer();
55 if let Some(state) = self.buffers.get_mut(&buffer_id) {
56 if state.language == "text" {
57 if let Some(filename) = p.file_name().and_then(|n| n.to_str()) {
58 state.set_language_from_name(filename, &self.grammar_registry);
59 }
60 }
61 }
62 }
63
64 self.status_message = Some(t!("status.file_saved").to_string());
65
66 self.active_event_log_mut().mark_saved();
68
69 if let Some(ref p) = path {
71 if let Ok(metadata) = self.filesystem.metadata(p) {
72 if let Some(mtime) = metadata.modified {
73 self.file_mod_times.insert(p.clone(), mtime);
74 }
75 }
76 }
77
78 self.notify_lsp_save();
80
81 let _ = self.delete_buffer_recovery(self.active_buffer());
83
84 if let Some(ref p) = path {
86 self.emit_event(
87 crate::model::control_event::events::FILE_SAVED.name,
88 serde_json::json!({
89 "path": p.display().to_string()
90 }),
91 );
92 }
93
94 if let Some(ref p) = path {
96 let buffer_id = self.active_buffer();
97 self.plugin_manager.run_hook(
98 "after_file_save",
99 crate::services::plugins::hooks::HookArgs::AfterFileSave {
100 buffer_id,
101 path: p.clone(),
102 },
103 );
104 }
105
106 match self.run_on_save_actions() {
108 Ok(true) => {
109 if self.status_message.as_deref() == Some(&t!("status.file_saved")) {
112 self.status_message = Some(t!("status.file_saved_with_actions").to_string());
113 }
114 }
116 Ok(false) => {
117 }
119 Err(e) => {
120 self.status_message = Some(e);
122 }
123 }
124
125 Ok(())
126 }
127
128 pub fn revert_file(&mut self) -> anyhow::Result<bool> {
131 let path = match self.active_state().buffer.file_path() {
132 Some(p) => p.to_path_buf(),
133 None => {
134 self.status_message = Some(t!("status.no_file_to_revert").to_string());
135 return Ok(false);
136 }
137 };
138
139 if !path.exists() {
140 self.status_message =
141 Some(t!("status.file_not_exists", path = path.display().to_string()).to_string());
142 return Ok(false);
143 }
144
145 let active_split = self.split_manager.active_split();
147 let (old_top_byte, old_left_column) = self
148 .split_view_states
149 .get(&active_split)
150 .map(|vs| (vs.viewport.top_byte, vs.viewport.left_column))
151 .unwrap_or((0, 0));
152 let old_cursors = self.active_state().cursors.clone();
153
154 let mut new_state = EditorState::from_file_with_languages(
156 &path,
157 self.terminal_width,
158 self.terminal_height,
159 self.config.editor.large_file_threshold_bytes as usize,
160 &self.grammar_registry,
161 &self.config.languages,
162 std::sync::Arc::clone(&self.filesystem),
163 )?;
164
165 let new_file_size = new_state.buffer.len();
167 let mut restored_cursors = old_cursors;
168 restored_cursors.map(|cursor| {
169 cursor.position = cursor.position.min(new_file_size);
170 cursor.clear_selection();
172 });
173 new_state.cursors = restored_cursors;
174
175 let buffer_id = self.active_buffer();
177 if let Some(state) = self.buffers.get_mut(&buffer_id) {
178 *state = new_state;
179 }
181
182 let active_split = self.split_manager.active_split();
184 if let Some(view_state) = self.split_view_states.get_mut(&active_split) {
185 view_state.viewport.top_byte = old_top_byte.min(new_file_size);
186 view_state.viewport.left_column = old_left_column;
187 }
188
189 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
191 *event_log = EventLog::new();
192 }
193
194 self.seen_byte_ranges.remove(&buffer_id);
196
197 if let Ok(metadata) = self.filesystem.metadata(&path) {
199 if let Some(mtime) = metadata.modified {
200 self.file_mod_times.insert(path.clone(), mtime);
201 }
202 }
203
204 self.notify_lsp_file_changed(&path);
206
207 self.status_message = Some(t!("status.reverted").to_string());
208 Ok(true)
209 }
210
211 pub fn toggle_auto_revert(&mut self) {
213 self.auto_revert_enabled = !self.auto_revert_enabled;
214
215 if self.auto_revert_enabled {
216 self.status_message = Some(t!("status.auto_revert_enabled").to_string());
217 } else {
218 self.status_message = Some(t!("status.auto_revert_disabled").to_string());
219 }
220 }
221
222 pub fn poll_file_changes(&mut self) -> bool {
227 if !self.auto_revert_enabled {
229 return false;
230 }
231
232 let poll_interval =
234 std::time::Duration::from_millis(self.config.editor.auto_revert_poll_interval_ms);
235 let elapsed = self.time_source.elapsed_since(self.last_auto_revert_poll);
236 tracing::trace!(
237 "poll_file_changes: elapsed={:?}, poll_interval={:?}",
238 elapsed,
239 poll_interval
240 );
241 if elapsed < poll_interval {
242 return false;
243 }
244 self.last_auto_revert_poll = self.time_source.now();
245
246 let files_to_check: Vec<PathBuf> = self
248 .buffers
249 .values()
250 .filter_map(|state| state.buffer.file_path().map(PathBuf::from))
251 .collect();
252
253 let mut any_changed = false;
254
255 for path in files_to_check {
256 let current_mtime = match self.filesystem.metadata(&path) {
258 Ok(meta) => match meta.modified {
259 Some(mtime) => mtime,
260 None => continue,
261 },
262 Err(_) => continue, };
264
265 if let Some(&stored_mtime) = self.file_mod_times.get(&path) {
267 if current_mtime != stored_mtime {
268 let path_str = path.display().to_string();
272 if self.handle_async_file_changed(path_str) {
273 any_changed = true;
274 }
275 }
276 } else {
277 self.file_mod_times.insert(path, current_mtime);
279 }
280 }
281
282 any_changed
283 }
284
285 pub fn poll_file_tree_changes(&mut self) -> bool {
290 let poll_interval =
292 std::time::Duration::from_millis(self.config.editor.file_tree_poll_interval_ms);
293 if self.time_source.elapsed_since(self.last_file_tree_poll) < poll_interval {
294 return false;
295 }
296 self.last_file_tree_poll = self.time_source.now();
297
298 let Some(explorer) = &self.file_explorer else {
300 return false;
301 };
302
303 use crate::view::file_tree::NodeId;
305 let expanded_dirs: Vec<(NodeId, PathBuf)> = explorer
306 .tree()
307 .all_nodes()
308 .filter(|node| node.is_dir() && node.is_expanded())
309 .map(|node| (node.id, node.entry.path.clone()))
310 .collect();
311
312 let mut dirs_to_refresh: Vec<NodeId> = Vec::new();
314
315 for (node_id, path) in expanded_dirs {
316 let current_mtime = match self.filesystem.metadata(&path) {
318 Ok(meta) => match meta.modified {
319 Some(mtime) => mtime,
320 None => continue,
321 },
322 Err(_) => continue, };
324
325 if let Some(&stored_mtime) = self.dir_mod_times.get(&path) {
327 if current_mtime != stored_mtime {
328 self.dir_mod_times.insert(path.clone(), current_mtime);
330 dirs_to_refresh.push(node_id);
331 tracing::debug!("Directory changed: {:?}", path);
332 }
333 } else {
334 self.dir_mod_times.insert(path, current_mtime);
336 }
337 }
338
339 if dirs_to_refresh.is_empty() {
341 return false;
342 }
343
344 if let (Some(runtime), Some(explorer)) = (&self.tokio_runtime, &mut self.file_explorer) {
346 for node_id in dirs_to_refresh {
347 let tree = explorer.tree_mut();
348 if let Err(e) = runtime.block_on(tree.refresh_node(node_id)) {
349 tracing::warn!("Failed to refresh directory: {}", e);
350 }
351 }
352 }
353
354 true
355 }
356
357 pub(crate) fn notify_lsp_file_opened(
360 &mut self,
361 path: &Path,
362 buffer_id: BufferId,
363 metadata: &mut BufferMetadata,
364 ) {
365 let Some(language) = detect_language(path, &self.config.languages) else {
367 tracing::debug!("No language detected for file: {}", path.display());
368 return;
369 };
370
371 let Some(uri) = metadata.file_uri().cloned() else {
372 tracing::warn!(
373 "No URI in metadata for file: {} (failed to compute absolute path)",
374 path.display()
375 );
376 return;
377 };
378
379 let file_size = self
381 .filesystem
382 .metadata(path)
383 .ok()
384 .map(|m| m.size)
385 .unwrap_or(0);
386 if file_size > self.config.editor.large_file_threshold_bytes {
387 let reason = format!("File too large ({} bytes)", file_size);
388 tracing::warn!(
389 "Skipping LSP for large file: {} ({})",
390 path.display(),
391 reason
392 );
393 metadata.disable_lsp(reason);
394 return;
395 }
396
397 let text = match self
399 .buffers
400 .get(&buffer_id)
401 .and_then(|state| state.buffer.to_string())
402 {
403 Some(t) => t,
404 None => {
405 tracing::debug!("Buffer not fully loaded for LSP notification");
406 return;
407 }
408 };
409
410 let enable_inlay_hints = self.config.editor.enable_inlay_hints;
411 let previous_result_id = self.diagnostic_result_ids.get(uri.as_str()).cloned();
412
413 let (last_line, last_char) = self
415 .buffers
416 .get(&buffer_id)
417 .map(|state| {
418 let line_count = state.buffer.line_count().unwrap_or(1000);
419 (line_count.saturating_sub(1) as u32, 10000u32)
420 })
421 .unwrap_or((999, 10000));
422
423 let Some(lsp) = &mut self.lsp else {
425 tracing::debug!("No LSP manager available");
426 return;
427 };
428
429 tracing::debug!("LSP manager available for file: {}", path.display());
430 tracing::debug!(
431 "Detected language: {} for file: {}",
432 language,
433 path.display()
434 );
435 tracing::debug!("Using URI from metadata: {}", uri.as_str());
436 tracing::debug!("Attempting to spawn LSP client for language: {}", language);
437
438 match lsp.try_spawn(&language) {
439 LspSpawnResult::Spawned => {
440 if let Some(client) = lsp.get_handle_mut(&language) {
441 tracing::info!("Sending didOpen to LSP for: {}", uri.as_str());
443 if let Err(e) = client.did_open(uri.clone(), text, language.clone()) {
444 tracing::warn!("Failed to send didOpen to LSP: {}", e);
445 return;
446 }
447 tracing::info!("Successfully sent didOpen to LSP");
448
449 metadata.lsp_opened_with.insert(client.id());
451
452 let request_id = self.next_lsp_request_id;
454 self.next_lsp_request_id += 1;
455 if let Err(e) =
456 client.document_diagnostic(request_id, uri.clone(), previous_result_id)
457 {
458 tracing::debug!(
459 "Failed to request pull diagnostics (server may not support): {}",
460 e
461 );
462 } else {
463 tracing::info!(
464 "Requested pull diagnostics for {} (request_id={})",
465 uri.as_str(),
466 request_id
467 );
468 }
469
470 if enable_inlay_hints {
472 let request_id = self.next_lsp_request_id;
473 self.next_lsp_request_id += 1;
474 self.pending_inlay_hints_request = Some(request_id);
475
476 if let Err(e) =
477 client.inlay_hints(request_id, uri.clone(), 0, 0, last_line, last_char)
478 {
479 tracing::debug!(
480 "Failed to request inlay hints (server may not support): {}",
481 e
482 );
483 self.pending_inlay_hints_request = None;
484 } else {
485 tracing::info!(
486 "Requested inlay hints for {} (request_id={})",
487 uri.as_str(),
488 request_id
489 );
490 }
491 }
492 }
493 }
494 LspSpawnResult::NotAutoStart => {
495 tracing::debug!(
496 "LSP for {} not auto-starting (auto_start=false). Use command palette to start manually.",
497 language
498 );
499 }
500 LspSpawnResult::Failed => {
501 tracing::warn!("Failed to spawn LSP client for language: {}", language);
502 }
503 }
504 }
505
506 pub(crate) fn watch_file(&mut self, path: &Path) {
509 if let Ok(metadata) = self.filesystem.metadata(path) {
511 if let Some(mtime) = metadata.modified {
512 self.file_mod_times.insert(path.to_path_buf(), mtime);
513 }
514 }
515 }
516
517 pub(crate) fn notify_lsp_file_changed(&mut self, path: &Path) {
519 use crate::services::lsp::manager::LspSpawnResult;
520
521 let Ok(uri) = url::Url::from_file_path(path) else {
522 return;
523 };
524 let Ok(lsp_uri) = uri.as_str().parse::<lsp_types::Uri>() else {
525 return;
526 };
527 let Some(language) = detect_language(path, &self.config.languages) else {
528 return;
529 };
530
531 let Some((buffer_id, content)) = self
533 .buffers
534 .iter()
535 .find(|(_, s)| s.buffer.file_path() == Some(path))
536 .and_then(|(id, state)| state.buffer.to_string().map(|t| (*id, t)))
537 else {
538 return;
539 };
540
541 let spawn_result = {
543 let Some(lsp) = self.lsp.as_mut() else {
544 return;
545 };
546 lsp.try_spawn(&language)
547 };
548
549 if spawn_result != LspSpawnResult::Spawned {
551 return;
552 }
553
554 let handle_id = {
556 let Some(lsp) = self.lsp.as_mut() else {
557 return;
558 };
559 let Some(handle) = lsp.get_handle_mut(&language) else {
560 return;
561 };
562 handle.id()
563 };
564
565 let needs_open = {
567 let Some(metadata) = self.buffer_metadata.get(&buffer_id) else {
568 return;
569 };
570 !metadata.lsp_opened_with.contains(&handle_id)
571 };
572
573 if needs_open {
574 if let Some(lsp) = self.lsp.as_mut() {
576 if let Some(handle) = lsp.get_handle_mut(&language) {
577 if let Err(e) =
578 handle.did_open(lsp_uri.clone(), content.clone(), language.clone())
579 {
580 tracing::warn!("Failed to send didOpen before didChange: {}", e);
581 return;
582 }
583 tracing::debug!(
584 "Sent didOpen for {} to LSP handle {} before file change notification",
585 lsp_uri.as_str(),
586 handle_id
587 );
588 }
589 }
590
591 if let Some(metadata) = self.buffer_metadata.get_mut(&buffer_id) {
593 metadata.lsp_opened_with.insert(handle_id);
594 }
595 }
596
597 if let Some(lsp) = &mut self.lsp {
599 if let Some(client) = lsp.get_handle_mut(&language) {
600 let content_change = TextDocumentContentChangeEvent {
601 range: None, range_length: None,
603 text: content,
604 };
605 if let Err(e) = client.did_change(lsp_uri, vec![content_change]) {
606 tracing::warn!("Failed to notify LSP of file change: {}", e);
607 }
608 }
609 }
610 }
611
612 pub(crate) fn revert_buffer_by_id(
618 &mut self,
619 buffer_id: BufferId,
620 path: &Path,
621 ) -> anyhow::Result<()> {
622 let new_state = EditorState::from_file_with_languages(
624 path,
625 self.terminal_width,
626 self.terminal_height,
627 self.config.editor.large_file_threshold_bytes as usize,
628 &self.grammar_registry,
629 &self.config.languages,
630 std::sync::Arc::clone(&self.filesystem),
631 )?;
632
633 let new_file_size = new_state.buffer.len();
635
636 let old_cursors = self
638 .buffers
639 .get(&buffer_id)
640 .map(|s| s.cursors.clone())
641 .unwrap_or_default();
642
643 if let Some(state) = self.buffers.get_mut(&buffer_id) {
645 *state = new_state;
646
647 let mut restored_cursors = old_cursors;
649 restored_cursors.map(|cursor| {
650 cursor.position = cursor.position.min(new_file_size);
651 cursor.clear_selection();
652 });
653 state.cursors = restored_cursors;
654 }
655
656 if let Some(event_log) = self.event_logs.get_mut(&buffer_id) {
658 *event_log = EventLog::new();
659 }
660
661 self.seen_byte_ranges.remove(&buffer_id);
663
664 if let Ok(metadata) = self.filesystem.metadata(path) {
666 if let Some(mtime) = metadata.modified {
667 self.file_mod_times.insert(path.to_path_buf(), mtime);
668 }
669 }
670
671 self.notify_lsp_file_changed(path);
673
674 Ok(())
675 }
676
677 pub fn handle_file_changed(&mut self, changed_path: &str) {
679 let path = PathBuf::from(changed_path);
680
681 let buffer_ids: Vec<BufferId> = self
683 .buffers
684 .iter()
685 .filter(|(_, state)| state.buffer.file_path() == Some(&path))
686 .map(|(id, _)| *id)
687 .collect();
688
689 if buffer_ids.is_empty() {
690 return;
691 }
692
693 for buffer_id in buffer_ids {
694 if self.terminal_buffers.contains_key(&buffer_id) {
697 continue;
698 }
699
700 let state = match self.buffers.get(&buffer_id) {
701 Some(s) => s,
702 None => continue,
703 };
704
705 let current_mtime = match self
709 .filesystem
710 .metadata(&path)
711 .ok()
712 .and_then(|m| m.modified)
713 {
714 Some(mtime) => mtime,
715 None => continue, };
717
718 let dominated_by_stored = self
719 .file_mod_times
720 .get(&path)
721 .map(|stored| current_mtime <= *stored)
722 .unwrap_or(false);
723
724 if dominated_by_stored {
725 continue;
726 }
727
728 if state.buffer.is_modified() {
730 self.status_message = Some(format!(
731 "File {} changed on disk (buffer has unsaved changes)",
732 path.display()
733 ));
734 continue;
735 }
736
737 if self.auto_revert_enabled {
739 let still_needs_revert = self
743 .file_mod_times
744 .get(&path)
745 .map(|stored| current_mtime > *stored)
746 .unwrap_or(true);
747
748 if !still_needs_revert {
749 continue;
750 }
751
752 let is_active_buffer = buffer_id == self.active_buffer();
754
755 if is_active_buffer {
756 if let Err(e) = self.revert_file() {
758 tracing::error!("Failed to auto-revert file {:?}: {}", path, e);
759 } else {
760 tracing::info!("Auto-reverted file: {:?}", path);
761 }
762 } else {
763 if let Err(e) = self.revert_buffer_by_id(buffer_id, &path) {
766 tracing::error!("Failed to auto-revert background file {:?}: {}", path, e);
767 } else {
768 tracing::info!("Auto-reverted file: {:?}", path);
769 }
770 }
771
772 self.watch_file(&path);
774 }
775 }
776 }
777
778 pub fn check_save_conflict(&self) -> Option<std::time::SystemTime> {
781 let path = self.active_state().buffer.file_path()?;
782
783 let current_mtime = match self.filesystem.metadata(path).ok().and_then(|m| m.modified) {
785 Some(mtime) => mtime,
786 None => return None, };
788
789 match self.file_mod_times.get(path) {
791 Some(recorded_mtime) if current_mtime > *recorded_mtime => {
792 Some(current_mtime)
794 }
795 _ => None,
796 }
797 }
798}