1use std::collections::{HashMap, HashSet};
26use std::path::{Path, PathBuf};
27use std::time::Instant;
28
29use crate::state::EditorState;
30
31use crate::model::event::{BufferId, SplitDirection, SplitId};
32use crate::services::terminal::TerminalId;
33use crate::session::{
34 FileExplorerState, PersistedFileSession, SearchOptions, SerializedBookmark, SerializedCursor,
35 SerializedFileState, SerializedScroll, SerializedSplitDirection, SerializedSplitNode,
36 SerializedSplitViewState, SerializedTabRef, SerializedTerminalSession, SerializedViewMode,
37 Session, SessionConfigOverrides, SessionError, SessionHistories, SESSION_VERSION,
38};
39use crate::state::ViewMode;
40use crate::view::split::{SplitNode, SplitViewState};
41
42use super::types::Bookmark;
43use super::Editor;
44
45pub struct SessionTracker {
49 dirty: bool,
51 last_save: Instant,
53 save_interval: std::time::Duration,
55 enabled: bool,
57}
58
59impl SessionTracker {
60 pub fn new(enabled: bool) -> Self {
62 Self {
63 dirty: false,
64 last_save: Instant::now(),
65 save_interval: std::time::Duration::from_secs(5),
66 enabled,
67 }
68 }
69
70 pub fn is_enabled(&self) -> bool {
72 self.enabled
73 }
74
75 pub fn mark_dirty(&mut self) {
77 if self.enabled {
78 self.dirty = true;
79 }
80 }
81
82 pub fn should_save(&self) -> bool {
84 self.enabled && self.dirty && self.last_save.elapsed() >= self.save_interval
85 }
86
87 pub fn record_save(&mut self) {
89 self.dirty = false;
90 self.last_save = Instant::now();
91 }
92
93 pub fn is_dirty(&self) -> bool {
95 self.dirty
96 }
97}
98
99impl Editor {
100 pub fn capture_session(&self) -> Session {
102 tracing::debug!("Capturing session for {:?}", self.working_dir);
103
104 let mut terminals = Vec::new();
106 let mut terminal_indices: HashMap<TerminalId, usize> = HashMap::new();
107 let mut seen = HashSet::new();
108 for terminal_id in self.terminal_buffers.values().copied() {
109 if seen.insert(terminal_id) {
110 let idx = terminals.len();
111 terminal_indices.insert(terminal_id, idx);
112 let handle = self.terminal_manager.get(terminal_id);
113 let (cols, rows) = handle
114 .map(|h| h.size())
115 .unwrap_or((self.terminal_width, self.terminal_height));
116 let cwd = handle.and_then(|h| h.cwd());
117 let shell = handle
118 .map(|h| h.shell().to_string())
119 .unwrap_or_else(crate::services::terminal::detect_shell);
120 let log_path = self
121 .terminal_log_files
122 .get(&terminal_id)
123 .cloned()
124 .unwrap_or_else(|| {
125 let root = self.dir_context.terminal_dir_for(&self.working_dir);
126 root.join(format!("fresh-terminal-{}.log", terminal_id.0))
127 });
128 let backing_path = self
129 .terminal_backing_files
130 .get(&terminal_id)
131 .cloned()
132 .unwrap_or_else(|| {
133 let root = self.dir_context.terminal_dir_for(&self.working_dir);
134 root.join(format!("fresh-terminal-{}.txt", terminal_id.0))
135 });
136
137 terminals.push(SerializedTerminalSession {
138 terminal_index: idx,
139 cwd,
140 shell,
141 cols,
142 rows,
143 log_path,
144 backing_path,
145 });
146 }
147 }
148
149 let split_layout = serialize_split_node(
150 self.split_manager.root(),
151 &self.buffer_metadata,
152 &self.working_dir,
153 &self.terminal_buffers,
154 &terminal_indices,
155 );
156
157 let active_buffers: HashMap<SplitId, BufferId> = self
160 .split_manager
161 .root()
162 .get_leaves_with_rects(ratatui::layout::Rect::default())
163 .into_iter()
164 .map(|(split_id, buffer_id, _)| (split_id, buffer_id))
165 .collect();
166
167 let mut split_states = HashMap::new();
168 for (split_id, view_state) in &self.split_view_states {
169 let active_buffer = active_buffers.get(split_id).copied();
170 let serialized = serialize_split_view_state(
171 view_state,
172 &self.buffer_metadata,
173 &self.working_dir,
174 active_buffer,
175 &self.terminal_buffers,
176 &terminal_indices,
177 );
178 tracing::trace!(
179 "Split {:?}: {} open tabs, active_buffer={:?}",
180 split_id,
181 serialized.open_tabs.len(),
182 active_buffer
183 );
184 split_states.insert(split_id.0, serialized);
185 }
186
187 tracing::debug!(
188 "Captured {} split states, active_split={}",
189 split_states.len(),
190 self.split_manager.active_split().0
191 );
192
193 let file_explorer = if let Some(ref explorer) = self.file_explorer {
195 let expanded_dirs = get_expanded_dirs(explorer, &self.working_dir);
197 FileExplorerState {
198 visible: self.file_explorer_visible,
199 width_percent: self.file_explorer_width_percent,
200 expanded_dirs,
201 scroll_offset: explorer.get_scroll_offset(),
202 show_hidden: explorer.ignore_patterns().show_hidden(),
203 show_gitignored: explorer.ignore_patterns().show_gitignored(),
204 }
205 } else {
206 FileExplorerState {
207 visible: self.file_explorer_visible,
208 width_percent: self.file_explorer_width_percent,
209 expanded_dirs: Vec::new(),
210 scroll_offset: 0,
211 show_hidden: false,
212 show_gitignored: false,
213 }
214 };
215
216 let config_overrides = SessionConfigOverrides {
218 line_numbers: Some(self.config.editor.line_numbers),
219 relative_line_numbers: Some(self.config.editor.relative_line_numbers),
220 line_wrap: Some(self.config.editor.line_wrap),
221 syntax_highlighting: Some(self.config.editor.syntax_highlighting),
222 enable_inlay_hints: Some(self.config.editor.enable_inlay_hints),
223 mouse_enabled: Some(self.mouse_enabled),
224 menu_bar_hidden: Some(!self.menu_bar_visible),
225 };
226
227 let histories = SessionHistories {
229 search: self
230 .prompt_histories
231 .get("search")
232 .map(|h| h.items().to_vec())
233 .unwrap_or_default(),
234 replace: self
235 .prompt_histories
236 .get("replace")
237 .map(|h| h.items().to_vec())
238 .unwrap_or_default(),
239 command_palette: Vec::new(), goto_line: self
241 .prompt_histories
242 .get("goto_line")
243 .map(|h| h.items().to_vec())
244 .unwrap_or_default(),
245 open_file: Vec::new(), };
247 tracing::trace!(
248 "Captured histories: {} search, {} replace",
249 histories.search.len(),
250 histories.replace.len()
251 );
252
253 let search_options = SearchOptions {
255 case_sensitive: self.search_case_sensitive,
256 whole_word: self.search_whole_word,
257 use_regex: self.search_use_regex,
258 confirm_each: self.search_confirm_each,
259 };
260
261 let bookmarks =
263 serialize_bookmarks(&self.bookmarks, &self.buffer_metadata, &self.working_dir);
264
265 let external_files: Vec<PathBuf> = self
268 .buffer_metadata
269 .values()
270 .filter_map(|meta| meta.file_path())
271 .filter(|abs_path| abs_path.strip_prefix(&self.working_dir).is_err())
272 .cloned()
273 .collect();
274 if !external_files.is_empty() {
275 tracing::debug!("Captured {} external files", external_files.len());
276 }
277
278 Session {
279 version: SESSION_VERSION,
280 working_dir: self.working_dir.clone(),
281 split_layout,
282 active_split_id: self.split_manager.active_split().0,
283 split_states,
284 config_overrides,
285 file_explorer,
286 histories,
287 search_options,
288 bookmarks,
289 terminals,
290 external_files,
291 saved_at: std::time::SystemTime::now()
292 .duration_since(std::time::UNIX_EPOCH)
293 .unwrap_or_default()
294 .as_secs(),
295 }
296 }
297
298 pub fn save_session(&mut self) -> Result<(), SessionError> {
304 self.sync_all_terminal_backing_files();
306
307 self.save_all_global_file_states();
309
310 let session = self.capture_session();
311 session.save()
312 }
313
314 fn save_all_global_file_states(&self) {
316 for (split_id, view_state) in &self.split_view_states {
318 let active_buffer = self
320 .split_manager
321 .root()
322 .get_leaves_with_rects(ratatui::layout::Rect::default())
323 .into_iter()
324 .find(|(sid, _, _)| *sid == *split_id)
325 .map(|(_, buffer_id, _)| buffer_id);
326
327 if let Some(buffer_id) = active_buffer {
328 self.save_buffer_file_state(buffer_id, view_state);
329 }
330 }
331 }
332
333 fn save_buffer_file_state(&self, buffer_id: BufferId, view_state: &SplitViewState) {
335 let abs_path = match self.buffer_metadata.get(&buffer_id) {
337 Some(metadata) => match metadata.file_path() {
338 Some(path) => path.to_path_buf(),
339 None => return, },
341 None => return,
342 };
343
344 let primary_cursor = view_state.cursors.primary();
346 let file_state = SerializedFileState {
347 cursor: SerializedCursor {
348 position: primary_cursor.position,
349 anchor: primary_cursor.anchor,
350 sticky_column: primary_cursor.sticky_column,
351 },
352 additional_cursors: view_state
353 .cursors
354 .iter()
355 .skip(1)
356 .map(|(_, cursor)| SerializedCursor {
357 position: cursor.position,
358 anchor: cursor.anchor,
359 sticky_column: cursor.sticky_column,
360 })
361 .collect(),
362 scroll: SerializedScroll {
363 top_byte: view_state.viewport.top_byte,
364 top_view_line_offset: view_state.viewport.top_view_line_offset,
365 left_column: view_state.viewport.left_column,
366 },
367 };
368
369 PersistedFileSession::save(&abs_path, file_state);
371 }
372
373 fn sync_all_terminal_backing_files(&mut self) {
378 use std::io::BufWriter;
379
380 let terminals_to_sync: Vec<_> = self
382 .terminal_buffers
383 .values()
384 .copied()
385 .filter_map(|terminal_id| {
386 self.terminal_backing_files
387 .get(&terminal_id)
388 .map(|path| (terminal_id, path.clone()))
389 })
390 .collect();
391
392 for (terminal_id, backing_path) in terminals_to_sync {
393 if let Some(handle) = self.terminal_manager.get(terminal_id) {
394 if let Ok(state) = handle.state.lock() {
395 if let Ok(mut file) = std::fs::OpenOptions::new()
397 .create(true)
398 .append(true)
399 .open(&backing_path)
400 {
401 let mut writer = BufWriter::new(&mut file);
402 if let Err(e) = state.append_visible_screen(&mut writer) {
403 tracing::warn!(
404 "Failed to sync terminal {:?} to backing file: {}",
405 terminal_id,
406 e
407 );
408 }
409 }
410 }
411 }
412 }
413 }
414
415 pub fn try_restore_session(&mut self) -> Result<bool, SessionError> {
419 tracing::debug!("Attempting to restore session for {:?}", self.working_dir);
420 match Session::load(&self.working_dir)? {
421 Some(session) => {
422 tracing::info!("Found session, applying...");
423 self.apply_session(&session)?;
424 Ok(true)
425 }
426 None => {
427 tracing::debug!("No session found for {:?}", self.working_dir);
428 Ok(false)
429 }
430 }
431 }
432
433 pub fn apply_session(&mut self, session: &Session) -> Result<(), SessionError> {
435 tracing::debug!(
436 "Applying session with {} split states",
437 session.split_states.len()
438 );
439
440 if let Some(line_numbers) = session.config_overrides.line_numbers {
442 self.config.editor.line_numbers = line_numbers;
443 }
444 if let Some(relative_line_numbers) = session.config_overrides.relative_line_numbers {
445 self.config.editor.relative_line_numbers = relative_line_numbers;
446 }
447 if let Some(line_wrap) = session.config_overrides.line_wrap {
448 self.config.editor.line_wrap = line_wrap;
449 }
450 if let Some(syntax_highlighting) = session.config_overrides.syntax_highlighting {
451 self.config.editor.syntax_highlighting = syntax_highlighting;
452 }
453 if let Some(enable_inlay_hints) = session.config_overrides.enable_inlay_hints {
454 self.config.editor.enable_inlay_hints = enable_inlay_hints;
455 }
456 if let Some(mouse_enabled) = session.config_overrides.mouse_enabled {
457 self.mouse_enabled = mouse_enabled;
458 }
459 if let Some(menu_bar_hidden) = session.config_overrides.menu_bar_hidden {
460 self.menu_bar_visible = !menu_bar_hidden;
461 }
462
463 self.search_case_sensitive = session.search_options.case_sensitive;
465 self.search_whole_word = session.search_options.whole_word;
466 self.search_use_regex = session.search_options.use_regex;
467 self.search_confirm_each = session.search_options.confirm_each;
468
469 tracing::debug!(
471 "Restoring histories: {} search, {} replace, {} goto_line",
472 session.histories.search.len(),
473 session.histories.replace.len(),
474 session.histories.goto_line.len()
475 );
476 for item in &session.histories.search {
477 self.get_or_create_prompt_history("search")
478 .push(item.clone());
479 }
480 for item in &session.histories.replace {
481 self.get_or_create_prompt_history("replace")
482 .push(item.clone());
483 }
484 for item in &session.histories.goto_line {
485 self.get_or_create_prompt_history("goto_line")
486 .push(item.clone());
487 }
488
489 self.file_explorer_visible = session.file_explorer.visible;
491 self.file_explorer_width_percent = session.file_explorer.width_percent;
492
493 if session.file_explorer.show_hidden {
496 self.pending_file_explorer_show_hidden = Some(true);
497 }
498 if session.file_explorer.show_gitignored {
499 self.pending_file_explorer_show_gitignored = Some(true);
500 }
501
502 if self.file_explorer_visible && self.file_explorer.is_none() {
505 self.init_file_explorer();
506 }
507
508 let file_paths = collect_file_paths_from_states(&session.split_states);
511 tracing::debug!(
512 "Session has {} files to restore: {:?}",
513 file_paths.len(),
514 file_paths
515 );
516 let mut path_to_buffer: HashMap<PathBuf, BufferId> = HashMap::new();
517
518 for rel_path in file_paths {
519 let abs_path = self.working_dir.join(&rel_path);
520 tracing::trace!(
521 "Checking file: {:?} (exists: {})",
522 abs_path,
523 abs_path.exists()
524 );
525 if abs_path.exists() {
526 match self.open_file_internal(&abs_path) {
528 Ok(buffer_id) => {
529 tracing::debug!("Opened file {:?} as buffer {:?}", rel_path, buffer_id);
530 path_to_buffer.insert(rel_path, buffer_id);
531 }
532 Err(e) => {
533 tracing::warn!("Failed to open file {:?}: {}", abs_path, e);
534 }
535 }
536 } else {
537 tracing::debug!("Skipping non-existent file: {:?}", abs_path);
538 }
539 }
540
541 tracing::debug!("Opened {} files from session", path_to_buffer.len());
542
543 if !session.external_files.is_empty() {
546 tracing::debug!(
547 "Restoring {} external files: {:?}",
548 session.external_files.len(),
549 session.external_files
550 );
551 for abs_path in &session.external_files {
552 if abs_path.exists() {
553 match self.open_file_internal(abs_path) {
554 Ok(buffer_id) => {
555 tracing::debug!(
556 "Restored external file {:?} as buffer {:?}",
557 abs_path,
558 buffer_id
559 );
560 }
561 Err(e) => {
562 tracing::warn!("Failed to restore external file {:?}: {}", abs_path, e);
563 }
564 }
565 } else {
566 tracing::debug!("Skipping non-existent external file: {:?}", abs_path);
567 }
568 }
569 }
570
571 let mut terminal_buffer_map: HashMap<usize, BufferId> = HashMap::new();
573 if !session.terminals.is_empty() {
574 if let Some(ref bridge) = self.async_bridge {
575 self.terminal_manager.set_async_bridge(bridge.clone());
576 }
577 for terminal in &session.terminals {
578 if let Some(buffer_id) = self.restore_terminal_from_session(terminal) {
579 terminal_buffer_map.insert(terminal.terminal_index, buffer_id);
580 }
581 }
582 }
583
584 let mut split_id_map: HashMap<usize, SplitId> = HashMap::new();
587 self.restore_split_node(
588 &session.split_layout,
589 &path_to_buffer,
590 &terminal_buffer_map,
591 &session.split_states,
592 &mut split_id_map,
593 true, );
595
596 if let Some(&new_active_split) = split_id_map.get(&session.active_split_id) {
600 self.split_manager.set_active_split(new_active_split);
601 }
602
603 for (key, bookmark) in &session.bookmarks {
605 if let Some(&buffer_id) = path_to_buffer.get(&bookmark.file_path) {
606 if let Some(buffer) = self.buffers.get(&buffer_id) {
608 let pos = bookmark.position.min(buffer.buffer.len());
609 self.bookmarks.insert(
610 *key,
611 Bookmark {
612 buffer_id,
613 position: pos,
614 },
615 );
616 }
617 }
618 }
619
620 tracing::debug!(
621 "Session restore complete: {} splits, {} buffers",
622 self.split_view_states.len(),
623 self.buffers.len()
624 );
625
626 Ok(())
627 }
628
629 fn restore_terminal_from_session(
638 &mut self,
639 terminal: &SerializedTerminalSession,
640 ) -> Option<BufferId> {
641 let terminals_root = self.dir_context.terminal_dir_for(&self.working_dir);
643 let log_path = if terminal.log_path.is_absolute() {
644 terminal.log_path.clone()
645 } else {
646 terminals_root.join(&terminal.log_path)
647 };
648 let backing_path = if terminal.backing_path.is_absolute() {
649 terminal.backing_path.clone()
650 } else {
651 terminals_root.join(&terminal.backing_path)
652 };
653
654 let _ = std::fs::create_dir_all(
655 log_path
656 .parent()
657 .or_else(|| backing_path.parent())
658 .unwrap_or(&terminals_root),
659 );
660
661 let predicted_id = self.terminal_manager.next_terminal_id();
663 self.terminal_log_files
664 .insert(predicted_id, log_path.clone());
665 self.terminal_backing_files
666 .insert(predicted_id, backing_path.clone());
667
668 let terminal_id = match self.terminal_manager.spawn(
670 terminal.cols,
671 terminal.rows,
672 terminal.cwd.clone(),
673 Some(log_path.clone()),
674 Some(backing_path.clone()),
675 ) {
676 Ok(id) => id,
677 Err(e) => {
678 tracing::warn!(
679 "Failed to restore terminal {}: {}",
680 terminal.terminal_index,
681 e
682 );
683 return None;
684 }
685 };
686
687 if terminal_id != predicted_id {
689 self.terminal_log_files
690 .insert(terminal_id, log_path.clone());
691 self.terminal_backing_files
692 .insert(terminal_id, backing_path.clone());
693 self.terminal_log_files.remove(&predicted_id);
694 self.terminal_backing_files.remove(&predicted_id);
695 }
696
697 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
699
700 self.load_terminal_backing_file_as_buffer(buffer_id, &backing_path);
703
704 Some(buffer_id)
705 }
706
707 fn load_terminal_backing_file_as_buffer(&mut self, buffer_id: BufferId, backing_path: &Path) {
712 if !backing_path.exists() {
714 return;
715 }
716
717 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
718 if let Ok(new_state) = EditorState::from_file_with_languages(
719 backing_path,
720 self.terminal_width,
721 self.terminal_height,
722 large_file_threshold,
723 &self.grammar_registry,
724 &self.config.languages,
725 ) {
726 if let Some(state) = self.buffers.get_mut(&buffer_id) {
727 *state = new_state;
728 let total = state.buffer.total_bytes();
730 state.primary_cursor_mut().position = total;
731 state.buffer.set_modified(false);
733 state.editing_disabled = true;
735 state.margins.set_line_numbers(false);
736 }
737 }
738 }
739
740 fn open_file_internal(&mut self, path: &Path) -> Result<BufferId, SessionError> {
742 for (buffer_id, metadata) in &self.buffer_metadata {
744 if let Some(file_path) = metadata.file_path() {
745 if file_path == path {
746 return Ok(*buffer_id);
747 }
748 }
749 }
750
751 self.open_file(path).map_err(SessionError::Io)
753 }
754
755 fn restore_split_node(
757 &mut self,
758 node: &SerializedSplitNode,
759 path_to_buffer: &HashMap<PathBuf, BufferId>,
760 terminal_buffers: &HashMap<usize, BufferId>,
761 split_states: &HashMap<usize, SerializedSplitViewState>,
762 split_id_map: &mut HashMap<usize, SplitId>,
763 is_first_leaf: bool,
764 ) {
765 match node {
766 SerializedSplitNode::Leaf {
767 file_path,
768 split_id,
769 } => {
770 let buffer_id = file_path
772 .as_ref()
773 .and_then(|p| path_to_buffer.get(p).copied())
774 .unwrap_or(self.active_buffer());
775
776 let current_split_id = if is_first_leaf {
777 let split_id_val = self.split_manager.active_split();
779 let _ = self.split_manager.set_split_buffer(split_id_val, buffer_id);
780 split_id_val
781 } else {
782 self.split_manager.active_split()
784 };
785
786 split_id_map.insert(*split_id, current_split_id);
788
789 self.restore_split_view_state(
791 current_split_id,
792 *split_id,
793 split_states,
794 path_to_buffer,
795 terminal_buffers,
796 );
797 }
798 SerializedSplitNode::Terminal {
799 terminal_index,
800 split_id,
801 } => {
802 let buffer_id = terminal_buffers
803 .get(terminal_index)
804 .copied()
805 .unwrap_or(self.active_buffer());
806
807 let current_split_id = if is_first_leaf {
808 let split_id_val = self.split_manager.active_split();
809 let _ = self.split_manager.set_split_buffer(split_id_val, buffer_id);
810 split_id_val
811 } else {
812 self.split_manager.active_split()
813 };
814
815 split_id_map.insert(*split_id, current_split_id);
816
817 let _ = self
818 .split_manager
819 .set_split_buffer(current_split_id, buffer_id);
820
821 self.restore_split_view_state(
822 current_split_id,
823 *split_id,
824 split_states,
825 path_to_buffer,
826 terminal_buffers,
827 );
828 }
829 SerializedSplitNode::Split {
830 direction,
831 first,
832 second,
833 ratio,
834 split_id,
835 } => {
836 self.restore_split_node(
838 first,
839 path_to_buffer,
840 terminal_buffers,
841 split_states,
842 split_id_map,
843 is_first_leaf,
844 );
845
846 let second_buffer_id =
848 get_first_leaf_buffer(second, path_to_buffer, terminal_buffers)
849 .unwrap_or(self.active_buffer());
850
851 let split_direction = match direction {
853 SerializedSplitDirection::Horizontal => SplitDirection::Horizontal,
854 SerializedSplitDirection::Vertical => SplitDirection::Vertical,
855 };
856
857 match self
859 .split_manager
860 .split_active(split_direction, second_buffer_id, *ratio)
861 {
862 Ok(new_split_id) => {
863 let mut view_state = SplitViewState::with_buffer(
865 self.terminal_width,
866 self.terminal_height,
867 second_buffer_id,
868 );
869 view_state.viewport.line_wrap_enabled = self.config.editor.line_wrap;
870 self.split_view_states.insert(new_split_id, view_state);
871
872 split_id_map.insert(*split_id, new_split_id);
874
875 self.restore_split_node(
877 second,
878 path_to_buffer,
879 terminal_buffers,
880 split_states,
881 split_id_map,
882 false,
883 );
884 }
885 Err(e) => {
886 tracing::error!("Failed to create split during session restore: {}", e);
887 }
888 }
889 }
890 }
891 }
892
893 fn restore_split_view_state(
895 &mut self,
896 current_split_id: SplitId,
897 saved_split_id: usize,
898 split_states: &HashMap<usize, SerializedSplitViewState>,
899 path_to_buffer: &HashMap<PathBuf, BufferId>,
900 terminal_buffers: &HashMap<usize, BufferId>,
901 ) {
902 let Some(split_state) = split_states.get(&saved_split_id) else {
904 return;
905 };
906
907 let Some(view_state) = self.split_view_states.get_mut(¤t_split_id) else {
908 return;
909 };
910
911 let mut active_buffer_id: Option<BufferId> = None;
912
913 if !split_state.open_tabs.is_empty() {
914 for tab in &split_state.open_tabs {
915 match tab {
916 SerializedTabRef::File(rel_path) => {
917 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
918 if !view_state.open_buffers.contains(&buffer_id) {
919 view_state.open_buffers.push(buffer_id);
920 }
921 if terminal_buffers.values().any(|&tid| tid == buffer_id) {
922 view_state.viewport.line_wrap_enabled = false;
923 }
924 }
925 }
926 SerializedTabRef::Terminal(index) => {
927 if let Some(&buffer_id) = terminal_buffers.get(index) {
928 if !view_state.open_buffers.contains(&buffer_id) {
929 view_state.open_buffers.push(buffer_id);
930 }
931 view_state.viewport.line_wrap_enabled = false;
932 }
933 }
934 }
935 }
936
937 if let Some(active_idx) = split_state.active_tab_index {
938 if let Some(tab) = split_state.open_tabs.get(active_idx) {
939 active_buffer_id = match tab {
940 SerializedTabRef::File(rel) => path_to_buffer.get(rel).copied(),
941 SerializedTabRef::Terminal(index) => terminal_buffers.get(index).copied(),
942 };
943 }
944 }
945 } else {
946 for rel_path in &split_state.open_files {
948 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
949 if !view_state.open_buffers.contains(&buffer_id) {
950 view_state.open_buffers.push(buffer_id);
951 }
952 }
953 }
954
955 let active_file_path = split_state.open_files.get(split_state.active_file_index);
956 active_buffer_id =
957 active_file_path.and_then(|rel_path| path_to_buffer.get(rel_path).copied());
958 }
959
960 if let Some(active_id) = active_buffer_id {
962 for (rel_path, file_state) in &split_state.file_states {
964 let buffer_for_path = path_to_buffer.get(rel_path).copied();
965 if buffer_for_path == Some(active_id) {
966 if let Some(buffer) = self.buffers.get(&active_id) {
967 let max_pos = buffer.buffer.len();
968 let cursor_pos = file_state.cursor.position.min(max_pos);
969
970 view_state.cursors.primary_mut().position = cursor_pos;
972 view_state.cursors.primary_mut().anchor =
973 file_state.cursor.anchor.map(|a| a.min(max_pos));
974 view_state.cursors.primary_mut().sticky_column =
975 file_state.cursor.sticky_column;
976
977 view_state.viewport.top_byte = file_state.scroll.top_byte.min(max_pos);
979 view_state.viewport.top_view_line_offset =
980 file_state.scroll.top_view_line_offset;
981 view_state.viewport.left_column = file_state.scroll.left_column;
982 view_state.viewport.set_skip_resize_sync();
985
986 tracing::trace!(
987 "Restored SplitViewState for {:?}: cursor={}, top_byte={}",
988 rel_path,
989 cursor_pos,
990 view_state.viewport.top_byte
991 );
992 }
993
994 if let Some(editor_state) = self.buffers.get_mut(&active_id) {
996 let max_pos = editor_state.buffer.len();
997 let cursor_pos = file_state.cursor.position.min(max_pos);
998 editor_state.cursors.primary_mut().position = cursor_pos;
999 editor_state.cursors.primary_mut().anchor =
1000 file_state.cursor.anchor.map(|a| a.min(max_pos));
1001 editor_state.cursors.primary_mut().sticky_column =
1002 file_state.cursor.sticky_column;
1003 }
1005 break;
1006 }
1007 }
1008
1009 let _ = self
1011 .split_manager
1012 .set_split_buffer(current_split_id, active_id);
1013 }
1014
1015 view_state.view_mode = match split_state.view_mode {
1017 SerializedViewMode::Source => ViewMode::Source,
1018 SerializedViewMode::Compose => ViewMode::Compose,
1019 };
1020 view_state.compose_width = split_state.compose_width;
1021 view_state.tab_scroll_offset = split_state.tab_scroll_offset;
1022 }
1023}
1024
1025fn get_first_leaf_buffer(
1027 node: &SerializedSplitNode,
1028 path_to_buffer: &HashMap<PathBuf, BufferId>,
1029 terminal_buffers: &HashMap<usize, BufferId>,
1030) -> Option<BufferId> {
1031 match node {
1032 SerializedSplitNode::Leaf { file_path, .. } => file_path
1033 .as_ref()
1034 .and_then(|p| path_to_buffer.get(p).copied()),
1035 SerializedSplitNode::Terminal { terminal_index, .. } => {
1036 terminal_buffers.get(terminal_index).copied()
1037 }
1038 SerializedSplitNode::Split { first, .. } => {
1039 get_first_leaf_buffer(first, path_to_buffer, terminal_buffers)
1040 }
1041 }
1042}
1043
1044fn serialize_split_node(
1049 node: &SplitNode,
1050 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1051 working_dir: &Path,
1052 terminal_buffers: &HashMap<BufferId, TerminalId>,
1053 terminal_indices: &HashMap<TerminalId, usize>,
1054) -> SerializedSplitNode {
1055 match node {
1056 SplitNode::Leaf {
1057 buffer_id,
1058 split_id,
1059 } => {
1060 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
1061 if let Some(index) = terminal_indices.get(terminal_id) {
1062 return SerializedSplitNode::Terminal {
1063 terminal_index: *index,
1064 split_id: split_id.0,
1065 };
1066 }
1067 }
1068
1069 let file_path = buffer_metadata
1070 .get(buffer_id)
1071 .and_then(|meta| meta.file_path())
1072 .and_then(|abs_path| {
1073 abs_path
1074 .strip_prefix(working_dir)
1075 .ok()
1076 .map(|p| p.to_path_buf())
1077 });
1078
1079 SerializedSplitNode::Leaf {
1080 file_path,
1081 split_id: split_id.0,
1082 }
1083 }
1084 SplitNode::Split {
1085 direction,
1086 first,
1087 second,
1088 ratio,
1089 split_id,
1090 } => SerializedSplitNode::Split {
1091 direction: match direction {
1092 SplitDirection::Horizontal => SerializedSplitDirection::Horizontal,
1093 SplitDirection::Vertical => SerializedSplitDirection::Vertical,
1094 },
1095 first: Box::new(serialize_split_node(
1096 first,
1097 buffer_metadata,
1098 working_dir,
1099 terminal_buffers,
1100 terminal_indices,
1101 )),
1102 second: Box::new(serialize_split_node(
1103 second,
1104 buffer_metadata,
1105 working_dir,
1106 terminal_buffers,
1107 terminal_indices,
1108 )),
1109 ratio: *ratio,
1110 split_id: split_id.0,
1111 },
1112 }
1113}
1114
1115fn serialize_split_view_state(
1116 view_state: &crate::view::split::SplitViewState,
1117 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1118 working_dir: &Path,
1119 active_buffer: Option<BufferId>,
1120 terminal_buffers: &HashMap<BufferId, TerminalId>,
1121 terminal_indices: &HashMap<TerminalId, usize>,
1122) -> SerializedSplitViewState {
1123 let mut open_tabs = Vec::new();
1124 let mut open_files = Vec::new();
1125 let mut active_tab_index = None;
1126
1127 for buffer_id in &view_state.open_buffers {
1128 let tab_index = open_tabs.len();
1129 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
1130 if let Some(idx) = terminal_indices.get(terminal_id) {
1131 open_tabs.push(SerializedTabRef::Terminal(*idx));
1132 if Some(*buffer_id) == active_buffer {
1133 active_tab_index = Some(tab_index);
1134 }
1135 continue;
1136 }
1137 }
1138
1139 if let Some(rel_path) = buffer_metadata
1140 .get(buffer_id)
1141 .and_then(|meta| meta.file_path())
1142 .and_then(|abs_path| abs_path.strip_prefix(working_dir).ok())
1143 {
1144 open_tabs.push(SerializedTabRef::File(rel_path.to_path_buf()));
1145 open_files.push(rel_path.to_path_buf());
1146 if Some(*buffer_id) == active_buffer {
1147 active_tab_index = Some(tab_index);
1148 }
1149 }
1150 }
1151
1152 let active_file_index = active_tab_index
1154 .and_then(|idx| open_tabs.get(idx))
1155 .and_then(|tab| match tab {
1156 SerializedTabRef::File(path) => {
1157 Some(open_files.iter().position(|p| p == path).unwrap_or(0))
1158 }
1159 _ => None,
1160 })
1161 .unwrap_or(0);
1162
1163 let mut file_states = HashMap::new();
1165 if let Some(active_id) = active_buffer {
1166 if let Some(meta) = buffer_metadata.get(&active_id) {
1167 if let Some(abs_path) = meta.file_path() {
1168 if let Ok(rel_path) = abs_path.strip_prefix(working_dir) {
1169 let primary_cursor = view_state.cursors.primary();
1170
1171 file_states.insert(
1172 rel_path.to_path_buf(),
1173 SerializedFileState {
1174 cursor: SerializedCursor {
1175 position: primary_cursor.position,
1176 anchor: primary_cursor.anchor,
1177 sticky_column: primary_cursor.sticky_column,
1178 },
1179 additional_cursors: view_state
1180 .cursors
1181 .iter()
1182 .skip(1) .map(|(_, cursor)| SerializedCursor {
1184 position: cursor.position,
1185 anchor: cursor.anchor,
1186 sticky_column: cursor.sticky_column,
1187 })
1188 .collect(),
1189 scroll: SerializedScroll {
1190 top_byte: view_state.viewport.top_byte,
1191 top_view_line_offset: view_state.viewport.top_view_line_offset,
1192 left_column: view_state.viewport.left_column,
1193 },
1194 },
1195 );
1196 }
1197 }
1198 }
1199 }
1200
1201 SerializedSplitViewState {
1202 open_tabs,
1203 active_tab_index,
1204 open_files,
1205 active_file_index,
1206 file_states,
1207 tab_scroll_offset: view_state.tab_scroll_offset,
1208 view_mode: match view_state.view_mode {
1209 ViewMode::Source => SerializedViewMode::Source,
1210 ViewMode::Compose => SerializedViewMode::Compose,
1211 },
1212 compose_width: view_state.compose_width,
1213 }
1214}
1215
1216fn serialize_bookmarks(
1217 bookmarks: &HashMap<char, Bookmark>,
1218 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1219 working_dir: &Path,
1220) -> HashMap<char, SerializedBookmark> {
1221 bookmarks
1222 .iter()
1223 .filter_map(|(key, bookmark)| {
1224 buffer_metadata
1225 .get(&bookmark.buffer_id)
1226 .and_then(|meta| meta.file_path())
1227 .and_then(|abs_path| {
1228 abs_path.strip_prefix(working_dir).ok().map(|rel_path| {
1229 (
1230 *key,
1231 SerializedBookmark {
1232 file_path: rel_path.to_path_buf(),
1233 position: bookmark.position,
1234 },
1235 )
1236 })
1237 })
1238 })
1239 .collect()
1240}
1241
1242fn collect_file_paths_from_states(
1244 split_states: &HashMap<usize, SerializedSplitViewState>,
1245) -> Vec<PathBuf> {
1246 let mut paths = Vec::new();
1247 for state in split_states.values() {
1248 if !state.open_tabs.is_empty() {
1249 for tab in &state.open_tabs {
1250 if let SerializedTabRef::File(path) = tab {
1251 if !paths.contains(path) {
1252 paths.push(path.clone());
1253 }
1254 }
1255 }
1256 } else {
1257 for path in &state.open_files {
1258 if !paths.contains(path) {
1259 paths.push(path.clone());
1260 }
1261 }
1262 }
1263 }
1264 paths
1265}
1266
1267fn get_expanded_dirs(
1269 explorer: &crate::view::file_tree::FileTreeView,
1270 working_dir: &Path,
1271) -> Vec<PathBuf> {
1272 let mut expanded = Vec::new();
1273 let tree = explorer.tree();
1274
1275 for node in tree.all_nodes() {
1277 if node.is_expanded() && node.is_dir() {
1278 if let Ok(rel_path) = node.entry.path.strip_prefix(working_dir) {
1280 expanded.push(rel_path.to_path_buf());
1281 }
1282 }
1283 }
1284
1285 expanded
1286}