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) = self.filesystem.open_file_for_append(&backing_path) {
397 let mut writer = BufWriter::new(&mut *file);
398 if let Err(e) = state.append_visible_screen(&mut writer) {
399 tracing::warn!(
400 "Failed to sync terminal {:?} to backing file: {}",
401 terminal_id,
402 e
403 );
404 }
405 }
406 }
407 }
408 }
409 }
410
411 pub fn try_restore_session(&mut self) -> Result<bool, SessionError> {
415 tracing::debug!("Attempting to restore session for {:?}", self.working_dir);
416 match Session::load(&self.working_dir)? {
417 Some(session) => {
418 tracing::info!("Found session, applying...");
419 self.apply_session(&session)?;
420 Ok(true)
421 }
422 None => {
423 tracing::debug!("No session found for {:?}", self.working_dir);
424 Ok(false)
425 }
426 }
427 }
428
429 pub fn apply_session(&mut self, session: &Session) -> Result<(), SessionError> {
431 tracing::debug!(
432 "Applying session with {} split states",
433 session.split_states.len()
434 );
435
436 if let Some(line_numbers) = session.config_overrides.line_numbers {
438 self.config.editor.line_numbers = line_numbers;
439 }
440 if let Some(relative_line_numbers) = session.config_overrides.relative_line_numbers {
441 self.config.editor.relative_line_numbers = relative_line_numbers;
442 }
443 if let Some(line_wrap) = session.config_overrides.line_wrap {
444 self.config.editor.line_wrap = line_wrap;
445 }
446 if let Some(syntax_highlighting) = session.config_overrides.syntax_highlighting {
447 self.config.editor.syntax_highlighting = syntax_highlighting;
448 }
449 if let Some(enable_inlay_hints) = session.config_overrides.enable_inlay_hints {
450 self.config.editor.enable_inlay_hints = enable_inlay_hints;
451 }
452 if let Some(mouse_enabled) = session.config_overrides.mouse_enabled {
453 self.mouse_enabled = mouse_enabled;
454 }
455 if let Some(menu_bar_hidden) = session.config_overrides.menu_bar_hidden {
456 self.menu_bar_visible = !menu_bar_hidden;
457 }
458
459 self.search_case_sensitive = session.search_options.case_sensitive;
461 self.search_whole_word = session.search_options.whole_word;
462 self.search_use_regex = session.search_options.use_regex;
463 self.search_confirm_each = session.search_options.confirm_each;
464
465 tracing::debug!(
467 "Restoring histories: {} search, {} replace, {} goto_line",
468 session.histories.search.len(),
469 session.histories.replace.len(),
470 session.histories.goto_line.len()
471 );
472 for item in &session.histories.search {
473 self.get_or_create_prompt_history("search")
474 .push(item.clone());
475 }
476 for item in &session.histories.replace {
477 self.get_or_create_prompt_history("replace")
478 .push(item.clone());
479 }
480 for item in &session.histories.goto_line {
481 self.get_or_create_prompt_history("goto_line")
482 .push(item.clone());
483 }
484
485 self.file_explorer_visible = session.file_explorer.visible;
487 self.file_explorer_width_percent = session.file_explorer.width_percent;
488
489 if session.file_explorer.show_hidden {
492 self.pending_file_explorer_show_hidden = Some(true);
493 }
494 if session.file_explorer.show_gitignored {
495 self.pending_file_explorer_show_gitignored = Some(true);
496 }
497
498 if self.file_explorer_visible && self.file_explorer.is_none() {
501 self.init_file_explorer();
502 }
503
504 let file_paths = collect_file_paths_from_states(&session.split_states);
507 tracing::debug!(
508 "Session has {} files to restore: {:?}",
509 file_paths.len(),
510 file_paths
511 );
512 let mut path_to_buffer: HashMap<PathBuf, BufferId> = HashMap::new();
513
514 for rel_path in file_paths {
515 let abs_path = self.working_dir.join(&rel_path);
516 tracing::trace!(
517 "Checking file: {:?} (exists: {})",
518 abs_path,
519 abs_path.exists()
520 );
521 if abs_path.exists() {
522 match self.open_file_internal(&abs_path) {
524 Ok(buffer_id) => {
525 tracing::debug!("Opened file {:?} as buffer {:?}", rel_path, buffer_id);
526 path_to_buffer.insert(rel_path, buffer_id);
527 }
528 Err(e) => {
529 tracing::warn!("Failed to open file {:?}: {}", abs_path, e);
530 }
531 }
532 } else {
533 tracing::debug!("Skipping non-existent file: {:?}", abs_path);
534 }
535 }
536
537 tracing::debug!("Opened {} files from session", path_to_buffer.len());
538
539 if !session.external_files.is_empty() {
542 tracing::debug!(
543 "Restoring {} external files: {:?}",
544 session.external_files.len(),
545 session.external_files
546 );
547 for abs_path in &session.external_files {
548 if abs_path.exists() {
549 match self.open_file_internal(abs_path) {
550 Ok(buffer_id) => {
551 tracing::debug!(
552 "Restored external file {:?} as buffer {:?}",
553 abs_path,
554 buffer_id
555 );
556 }
557 Err(e) => {
558 tracing::warn!("Failed to restore external file {:?}: {}", abs_path, e);
559 }
560 }
561 } else {
562 tracing::debug!("Skipping non-existent external file: {:?}", abs_path);
563 }
564 }
565 }
566
567 let mut terminal_buffer_map: HashMap<usize, BufferId> = HashMap::new();
569 if !session.terminals.is_empty() {
570 if let Some(ref bridge) = self.async_bridge {
571 self.terminal_manager.set_async_bridge(bridge.clone());
572 }
573 for terminal in &session.terminals {
574 if let Some(buffer_id) = self.restore_terminal_from_session(terminal) {
575 terminal_buffer_map.insert(terminal.terminal_index, buffer_id);
576 }
577 }
578 }
579
580 let mut split_id_map: HashMap<usize, SplitId> = HashMap::new();
583 self.restore_split_node(
584 &session.split_layout,
585 &path_to_buffer,
586 &terminal_buffer_map,
587 &session.split_states,
588 &mut split_id_map,
589 true, );
591
592 if let Some(&new_active_split) = split_id_map.get(&session.active_split_id) {
596 self.split_manager.set_active_split(new_active_split);
597 }
598
599 for (key, bookmark) in &session.bookmarks {
601 if let Some(&buffer_id) = path_to_buffer.get(&bookmark.file_path) {
602 if let Some(buffer) = self.buffers.get(&buffer_id) {
604 let pos = bookmark.position.min(buffer.buffer.len());
605 self.bookmarks.insert(
606 *key,
607 Bookmark {
608 buffer_id,
609 position: pos,
610 },
611 );
612 }
613 }
614 }
615
616 tracing::debug!(
617 "Session restore complete: {} splits, {} buffers",
618 self.split_view_states.len(),
619 self.buffers.len()
620 );
621
622 Ok(())
623 }
624
625 fn restore_terminal_from_session(
634 &mut self,
635 terminal: &SerializedTerminalSession,
636 ) -> Option<BufferId> {
637 let terminals_root = self.dir_context.terminal_dir_for(&self.working_dir);
639 let log_path = if terminal.log_path.is_absolute() {
640 terminal.log_path.clone()
641 } else {
642 terminals_root.join(&terminal.log_path)
643 };
644 let backing_path = if terminal.backing_path.is_absolute() {
645 terminal.backing_path.clone()
646 } else {
647 terminals_root.join(&terminal.backing_path)
648 };
649
650 let _ = self.filesystem.create_dir_all(
651 log_path
652 .parent()
653 .or_else(|| backing_path.parent())
654 .unwrap_or(&terminals_root),
655 );
656
657 let predicted_id = self.terminal_manager.next_terminal_id();
659 self.terminal_log_files
660 .insert(predicted_id, log_path.clone());
661 self.terminal_backing_files
662 .insert(predicted_id, backing_path.clone());
663
664 let terminal_id = match self.terminal_manager.spawn(
666 terminal.cols,
667 terminal.rows,
668 terminal.cwd.clone(),
669 Some(log_path.clone()),
670 Some(backing_path.clone()),
671 ) {
672 Ok(id) => id,
673 Err(e) => {
674 tracing::warn!(
675 "Failed to restore terminal {}: {}",
676 terminal.terminal_index,
677 e
678 );
679 return None;
680 }
681 };
682
683 if terminal_id != predicted_id {
685 self.terminal_log_files
686 .insert(terminal_id, log_path.clone());
687 self.terminal_backing_files
688 .insert(terminal_id, backing_path.clone());
689 self.terminal_log_files.remove(&predicted_id);
690 self.terminal_backing_files.remove(&predicted_id);
691 }
692
693 let buffer_id = self.create_terminal_buffer_detached(terminal_id);
695
696 self.load_terminal_backing_file_as_buffer(buffer_id, &backing_path);
699
700 Some(buffer_id)
701 }
702
703 fn load_terminal_backing_file_as_buffer(&mut self, buffer_id: BufferId, backing_path: &Path) {
708 if !backing_path.exists() {
710 return;
711 }
712
713 let large_file_threshold = self.config.editor.large_file_threshold_bytes as usize;
714 if let Ok(new_state) = EditorState::from_file_with_languages(
715 backing_path,
716 self.terminal_width,
717 self.terminal_height,
718 large_file_threshold,
719 &self.grammar_registry,
720 &self.config.languages,
721 std::sync::Arc::clone(&self.filesystem),
722 ) {
723 if let Some(state) = self.buffers.get_mut(&buffer_id) {
724 *state = new_state;
725 let total = state.buffer.total_bytes();
727 state.primary_cursor_mut().position = total;
728 state.buffer.set_modified(false);
730 state.editing_disabled = true;
732 state.margins.set_line_numbers(false);
733 }
734 }
735 }
736
737 fn open_file_internal(&mut self, path: &Path) -> Result<BufferId, SessionError> {
739 for (buffer_id, metadata) in &self.buffer_metadata {
741 if let Some(file_path) = metadata.file_path() {
742 if file_path == path {
743 return Ok(*buffer_id);
744 }
745 }
746 }
747
748 self.open_file(path).map_err(SessionError::Io)
750 }
751
752 fn restore_split_node(
754 &mut self,
755 node: &SerializedSplitNode,
756 path_to_buffer: &HashMap<PathBuf, BufferId>,
757 terminal_buffers: &HashMap<usize, BufferId>,
758 split_states: &HashMap<usize, SerializedSplitViewState>,
759 split_id_map: &mut HashMap<usize, SplitId>,
760 is_first_leaf: bool,
761 ) {
762 match node {
763 SerializedSplitNode::Leaf {
764 file_path,
765 split_id,
766 } => {
767 let buffer_id = file_path
769 .as_ref()
770 .and_then(|p| path_to_buffer.get(p).copied())
771 .unwrap_or(self.active_buffer());
772
773 let current_split_id = if is_first_leaf {
774 let split_id_val = self.split_manager.active_split();
776 let _ = self.split_manager.set_split_buffer(split_id_val, buffer_id);
777 split_id_val
778 } else {
779 self.split_manager.active_split()
781 };
782
783 split_id_map.insert(*split_id, current_split_id);
785
786 self.restore_split_view_state(
788 current_split_id,
789 *split_id,
790 split_states,
791 path_to_buffer,
792 terminal_buffers,
793 );
794 }
795 SerializedSplitNode::Terminal {
796 terminal_index,
797 split_id,
798 } => {
799 let buffer_id = terminal_buffers
800 .get(terminal_index)
801 .copied()
802 .unwrap_or(self.active_buffer());
803
804 let current_split_id = if is_first_leaf {
805 let split_id_val = self.split_manager.active_split();
806 let _ = self.split_manager.set_split_buffer(split_id_val, buffer_id);
807 split_id_val
808 } else {
809 self.split_manager.active_split()
810 };
811
812 split_id_map.insert(*split_id, current_split_id);
813
814 let _ = self
815 .split_manager
816 .set_split_buffer(current_split_id, buffer_id);
817
818 self.restore_split_view_state(
819 current_split_id,
820 *split_id,
821 split_states,
822 path_to_buffer,
823 terminal_buffers,
824 );
825 }
826 SerializedSplitNode::Split {
827 direction,
828 first,
829 second,
830 ratio,
831 split_id,
832 } => {
833 self.restore_split_node(
835 first,
836 path_to_buffer,
837 terminal_buffers,
838 split_states,
839 split_id_map,
840 is_first_leaf,
841 );
842
843 let second_buffer_id =
845 get_first_leaf_buffer(second, path_to_buffer, terminal_buffers)
846 .unwrap_or(self.active_buffer());
847
848 let split_direction = match direction {
850 SerializedSplitDirection::Horizontal => SplitDirection::Horizontal,
851 SerializedSplitDirection::Vertical => SplitDirection::Vertical,
852 };
853
854 match self
856 .split_manager
857 .split_active(split_direction, second_buffer_id, *ratio)
858 {
859 Ok(new_split_id) => {
860 let mut view_state = SplitViewState::with_buffer(
862 self.terminal_width,
863 self.terminal_height,
864 second_buffer_id,
865 );
866 view_state.viewport.line_wrap_enabled = self.config.editor.line_wrap;
867 self.split_view_states.insert(new_split_id, view_state);
868
869 split_id_map.insert(*split_id, new_split_id);
871
872 self.restore_split_node(
874 second,
875 path_to_buffer,
876 terminal_buffers,
877 split_states,
878 split_id_map,
879 false,
880 );
881 }
882 Err(e) => {
883 tracing::error!("Failed to create split during session restore: {}", e);
884 }
885 }
886 }
887 }
888 }
889
890 fn restore_split_view_state(
892 &mut self,
893 current_split_id: SplitId,
894 saved_split_id: usize,
895 split_states: &HashMap<usize, SerializedSplitViewState>,
896 path_to_buffer: &HashMap<PathBuf, BufferId>,
897 terminal_buffers: &HashMap<usize, BufferId>,
898 ) {
899 let Some(split_state) = split_states.get(&saved_split_id) else {
901 return;
902 };
903
904 let Some(view_state) = self.split_view_states.get_mut(¤t_split_id) else {
905 return;
906 };
907
908 let mut active_buffer_id: Option<BufferId> = None;
909
910 if !split_state.open_tabs.is_empty() {
911 for tab in &split_state.open_tabs {
912 match tab {
913 SerializedTabRef::File(rel_path) => {
914 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
915 if !view_state.open_buffers.contains(&buffer_id) {
916 view_state.open_buffers.push(buffer_id);
917 }
918 if terminal_buffers.values().any(|&tid| tid == buffer_id) {
919 view_state.viewport.line_wrap_enabled = false;
920 }
921 }
922 }
923 SerializedTabRef::Terminal(index) => {
924 if let Some(&buffer_id) = terminal_buffers.get(index) {
925 if !view_state.open_buffers.contains(&buffer_id) {
926 view_state.open_buffers.push(buffer_id);
927 }
928 view_state.viewport.line_wrap_enabled = false;
929 }
930 }
931 }
932 }
933
934 if let Some(active_idx) = split_state.active_tab_index {
935 if let Some(tab) = split_state.open_tabs.get(active_idx) {
936 active_buffer_id = match tab {
937 SerializedTabRef::File(rel) => path_to_buffer.get(rel).copied(),
938 SerializedTabRef::Terminal(index) => terminal_buffers.get(index).copied(),
939 };
940 }
941 }
942 } else {
943 for rel_path in &split_state.open_files {
945 if let Some(&buffer_id) = path_to_buffer.get(rel_path) {
946 if !view_state.open_buffers.contains(&buffer_id) {
947 view_state.open_buffers.push(buffer_id);
948 }
949 }
950 }
951
952 let active_file_path = split_state.open_files.get(split_state.active_file_index);
953 active_buffer_id =
954 active_file_path.and_then(|rel_path| path_to_buffer.get(rel_path).copied());
955 }
956
957 if let Some(active_id) = active_buffer_id {
959 for (rel_path, file_state) in &split_state.file_states {
961 let buffer_for_path = path_to_buffer.get(rel_path).copied();
962 if buffer_for_path == Some(active_id) {
963 if let Some(buffer) = self.buffers.get(&active_id) {
964 let max_pos = buffer.buffer.len();
965 let cursor_pos = file_state.cursor.position.min(max_pos);
966
967 view_state.cursors.primary_mut().position = cursor_pos;
969 view_state.cursors.primary_mut().anchor =
970 file_state.cursor.anchor.map(|a| a.min(max_pos));
971 view_state.cursors.primary_mut().sticky_column =
972 file_state.cursor.sticky_column;
973
974 view_state.viewport.top_byte = file_state.scroll.top_byte.min(max_pos);
976 view_state.viewport.top_view_line_offset =
977 file_state.scroll.top_view_line_offset;
978 view_state.viewport.left_column = file_state.scroll.left_column;
979 view_state.viewport.set_skip_resize_sync();
982
983 tracing::trace!(
984 "Restored SplitViewState for {:?}: cursor={}, top_byte={}",
985 rel_path,
986 cursor_pos,
987 view_state.viewport.top_byte
988 );
989 }
990
991 if let Some(editor_state) = self.buffers.get_mut(&active_id) {
993 let max_pos = editor_state.buffer.len();
994 let cursor_pos = file_state.cursor.position.min(max_pos);
995 editor_state.cursors.primary_mut().position = cursor_pos;
996 editor_state.cursors.primary_mut().anchor =
997 file_state.cursor.anchor.map(|a| a.min(max_pos));
998 editor_state.cursors.primary_mut().sticky_column =
999 file_state.cursor.sticky_column;
1000 }
1002 break;
1003 }
1004 }
1005
1006 let _ = self
1008 .split_manager
1009 .set_split_buffer(current_split_id, active_id);
1010 }
1011
1012 view_state.view_mode = match split_state.view_mode {
1014 SerializedViewMode::Source => ViewMode::Source,
1015 SerializedViewMode::Compose => ViewMode::Compose,
1016 };
1017 view_state.compose_width = split_state.compose_width;
1018 view_state.tab_scroll_offset = split_state.tab_scroll_offset;
1019 }
1020}
1021
1022fn get_first_leaf_buffer(
1024 node: &SerializedSplitNode,
1025 path_to_buffer: &HashMap<PathBuf, BufferId>,
1026 terminal_buffers: &HashMap<usize, BufferId>,
1027) -> Option<BufferId> {
1028 match node {
1029 SerializedSplitNode::Leaf { file_path, .. } => file_path
1030 .as_ref()
1031 .and_then(|p| path_to_buffer.get(p).copied()),
1032 SerializedSplitNode::Terminal { terminal_index, .. } => {
1033 terminal_buffers.get(terminal_index).copied()
1034 }
1035 SerializedSplitNode::Split { first, .. } => {
1036 get_first_leaf_buffer(first, path_to_buffer, terminal_buffers)
1037 }
1038 }
1039}
1040
1041fn serialize_split_node(
1046 node: &SplitNode,
1047 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1048 working_dir: &Path,
1049 terminal_buffers: &HashMap<BufferId, TerminalId>,
1050 terminal_indices: &HashMap<TerminalId, usize>,
1051) -> SerializedSplitNode {
1052 match node {
1053 SplitNode::Leaf {
1054 buffer_id,
1055 split_id,
1056 } => {
1057 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
1058 if let Some(index) = terminal_indices.get(terminal_id) {
1059 return SerializedSplitNode::Terminal {
1060 terminal_index: *index,
1061 split_id: split_id.0,
1062 };
1063 }
1064 }
1065
1066 let file_path = buffer_metadata
1067 .get(buffer_id)
1068 .and_then(|meta| meta.file_path())
1069 .and_then(|abs_path| {
1070 abs_path
1071 .strip_prefix(working_dir)
1072 .ok()
1073 .map(|p| p.to_path_buf())
1074 });
1075
1076 SerializedSplitNode::Leaf {
1077 file_path,
1078 split_id: split_id.0,
1079 }
1080 }
1081 SplitNode::Split {
1082 direction,
1083 first,
1084 second,
1085 ratio,
1086 split_id,
1087 } => SerializedSplitNode::Split {
1088 direction: match direction {
1089 SplitDirection::Horizontal => SerializedSplitDirection::Horizontal,
1090 SplitDirection::Vertical => SerializedSplitDirection::Vertical,
1091 },
1092 first: Box::new(serialize_split_node(
1093 first,
1094 buffer_metadata,
1095 working_dir,
1096 terminal_buffers,
1097 terminal_indices,
1098 )),
1099 second: Box::new(serialize_split_node(
1100 second,
1101 buffer_metadata,
1102 working_dir,
1103 terminal_buffers,
1104 terminal_indices,
1105 )),
1106 ratio: *ratio,
1107 split_id: split_id.0,
1108 },
1109 }
1110}
1111
1112fn serialize_split_view_state(
1113 view_state: &crate::view::split::SplitViewState,
1114 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1115 working_dir: &Path,
1116 active_buffer: Option<BufferId>,
1117 terminal_buffers: &HashMap<BufferId, TerminalId>,
1118 terminal_indices: &HashMap<TerminalId, usize>,
1119) -> SerializedSplitViewState {
1120 let mut open_tabs = Vec::new();
1121 let mut open_files = Vec::new();
1122 let mut active_tab_index = None;
1123
1124 for buffer_id in &view_state.open_buffers {
1125 let tab_index = open_tabs.len();
1126 if let Some(terminal_id) = terminal_buffers.get(buffer_id) {
1127 if let Some(idx) = terminal_indices.get(terminal_id) {
1128 open_tabs.push(SerializedTabRef::Terminal(*idx));
1129 if Some(*buffer_id) == active_buffer {
1130 active_tab_index = Some(tab_index);
1131 }
1132 continue;
1133 }
1134 }
1135
1136 if let Some(rel_path) = buffer_metadata
1137 .get(buffer_id)
1138 .and_then(|meta| meta.file_path())
1139 .and_then(|abs_path| abs_path.strip_prefix(working_dir).ok())
1140 {
1141 open_tabs.push(SerializedTabRef::File(rel_path.to_path_buf()));
1142 open_files.push(rel_path.to_path_buf());
1143 if Some(*buffer_id) == active_buffer {
1144 active_tab_index = Some(tab_index);
1145 }
1146 }
1147 }
1148
1149 let active_file_index = active_tab_index
1151 .and_then(|idx| open_tabs.get(idx))
1152 .and_then(|tab| match tab {
1153 SerializedTabRef::File(path) => {
1154 Some(open_files.iter().position(|p| p == path).unwrap_or(0))
1155 }
1156 _ => None,
1157 })
1158 .unwrap_or(0);
1159
1160 let mut file_states = HashMap::new();
1162 if let Some(active_id) = active_buffer {
1163 if let Some(meta) = buffer_metadata.get(&active_id) {
1164 if let Some(abs_path) = meta.file_path() {
1165 if let Ok(rel_path) = abs_path.strip_prefix(working_dir) {
1166 let primary_cursor = view_state.cursors.primary();
1167
1168 file_states.insert(
1169 rel_path.to_path_buf(),
1170 SerializedFileState {
1171 cursor: SerializedCursor {
1172 position: primary_cursor.position,
1173 anchor: primary_cursor.anchor,
1174 sticky_column: primary_cursor.sticky_column,
1175 },
1176 additional_cursors: view_state
1177 .cursors
1178 .iter()
1179 .skip(1) .map(|(_, cursor)| SerializedCursor {
1181 position: cursor.position,
1182 anchor: cursor.anchor,
1183 sticky_column: cursor.sticky_column,
1184 })
1185 .collect(),
1186 scroll: SerializedScroll {
1187 top_byte: view_state.viewport.top_byte,
1188 top_view_line_offset: view_state.viewport.top_view_line_offset,
1189 left_column: view_state.viewport.left_column,
1190 },
1191 },
1192 );
1193 }
1194 }
1195 }
1196 }
1197
1198 SerializedSplitViewState {
1199 open_tabs,
1200 active_tab_index,
1201 open_files,
1202 active_file_index,
1203 file_states,
1204 tab_scroll_offset: view_state.tab_scroll_offset,
1205 view_mode: match view_state.view_mode {
1206 ViewMode::Source => SerializedViewMode::Source,
1207 ViewMode::Compose => SerializedViewMode::Compose,
1208 },
1209 compose_width: view_state.compose_width,
1210 }
1211}
1212
1213fn serialize_bookmarks(
1214 bookmarks: &HashMap<char, Bookmark>,
1215 buffer_metadata: &HashMap<BufferId, super::types::BufferMetadata>,
1216 working_dir: &Path,
1217) -> HashMap<char, SerializedBookmark> {
1218 bookmarks
1219 .iter()
1220 .filter_map(|(key, bookmark)| {
1221 buffer_metadata
1222 .get(&bookmark.buffer_id)
1223 .and_then(|meta| meta.file_path())
1224 .and_then(|abs_path| {
1225 abs_path.strip_prefix(working_dir).ok().map(|rel_path| {
1226 (
1227 *key,
1228 SerializedBookmark {
1229 file_path: rel_path.to_path_buf(),
1230 position: bookmark.position,
1231 },
1232 )
1233 })
1234 })
1235 })
1236 .collect()
1237}
1238
1239fn collect_file_paths_from_states(
1241 split_states: &HashMap<usize, SerializedSplitViewState>,
1242) -> Vec<PathBuf> {
1243 let mut paths = Vec::new();
1244 for state in split_states.values() {
1245 if !state.open_tabs.is_empty() {
1246 for tab in &state.open_tabs {
1247 if let SerializedTabRef::File(path) = tab {
1248 if !paths.contains(path) {
1249 paths.push(path.clone());
1250 }
1251 }
1252 }
1253 } else {
1254 for path in &state.open_files {
1255 if !paths.contains(path) {
1256 paths.push(path.clone());
1257 }
1258 }
1259 }
1260 }
1261 paths
1262}
1263
1264fn get_expanded_dirs(
1266 explorer: &crate::view::file_tree::FileTreeView,
1267 working_dir: &Path,
1268) -> Vec<PathBuf> {
1269 let mut expanded = Vec::new();
1270 let tree = explorer.tree();
1271
1272 for node in tree.all_nodes() {
1274 if node.is_expanded() && node.is_dir() {
1275 if let Ok(rel_path) = node.entry.path.strip_prefix(working_dir) {
1277 expanded.push(rel_path.to_path_buf());
1278 }
1279 }
1280 }
1281
1282 expanded
1283}